diff --git a/sdk-common/__tests__/AttributeReference.test.ts b/sdk-common/__tests__/AttributeReference.test.ts new file mode 100644 index 0000000000..c1936dd068 --- /dev/null +++ b/sdk-common/__tests__/AttributeReference.test.ts @@ -0,0 +1,74 @@ +import { LDContextCommon } from '../src'; +import AttributeReference from '../src/AttributeReference'; + +function AsContextCommon(values: Record): LDContextCommon { + return { + ...values, + key: 'potato', + }; +} + +describe.each([ + new AttributeReference('/'), + new AttributeReference(''), + new AttributeReference('//'), + new AttributeReference(''), + new AttributeReference('', true), + new AttributeReference('_meta'), + new AttributeReference('/_meta'), + new AttributeReference('/_meta/secondary'), +])('when given invalid attribute references', (reference) => { + it('should not be valid', () => { + expect(reference.isValid).toBeFalsy(); + }); + + it('should not be able to get a value', () => { + expect(reference.get(AsContextCommon({ + '/': true, + '//': true, + '/~3': true, + '': true, + _meta: { secondary: true }, + }))).toBeUndefined(); + }); +}); + +describe.each([ + [new AttributeReference('/', true), { '/': true }, true], + [new AttributeReference('//', true), { '//': 17 }, 17], + [new AttributeReference('/~0', true), { '/~0': 'string' }, 'string'], + [new AttributeReference('a~b', true), { 'a~b': 'another' }, 'another'], + [new AttributeReference('a~b'), { 'a~b': 'another' }, 'another'], + [new AttributeReference('a/b', true), { 'a/b': true }, true], + [new AttributeReference('a/b'), { 'a/b': true }, true], + [new AttributeReference('/a~1~0b'), { 'a/~b': true }, true], + [new AttributeReference('/a~0b'), { 'a~b': true }, true], + [new AttributeReference(' /a/b', true), { ' /a/b': true }, true], + [new AttributeReference(' /a/b', false), { ' /a/b': true }, true], + [new AttributeReference('/a/b'), { a: { b: 'c' } }, 'c'], + [new AttributeReference('/a/1'), { a: ['b', 'c'] }, 'c'], +])('when given valid attribute references', (reference, object, expected) => { + it('should be valid', () => { + expect(reference.isValid).toBeTruthy(); + }); + + it('should be able to get a value', () => { + expect(reference.get(AsContextCommon(object))).toEqual(expected); + }); +}); + +describe.each([ + [new AttributeReference('name'), { }], + [new AttributeReference('/a/b'), { a: {} }], + [new AttributeReference('/a/0'), { a: 'test' }], + [new AttributeReference('/a/b'), { a: null }], + [new AttributeReference('/a/7'), { a: [0, 1] }], +])('should gracefully handle values that do not exist', (reference, object) => { + it('should be valid', () => { + expect(reference.isValid).toBeTruthy(); + }); + + it('should not be able to get a value', () => { + expect(reference.get(AsContextCommon(object))).toBeUndefined(); + }); +}); diff --git a/sdk-common/__tests__/Context.test.ts b/sdk-common/__tests__/Context.test.ts new file mode 100644 index 0000000000..2942b9f578 --- /dev/null +++ b/sdk-common/__tests__/Context.test.ts @@ -0,0 +1,221 @@ +import AttributeReference from '../src/AttributeReference'; +import Context from '../src/Context'; + +// A sample of invalid characters. +const invalidSampleChars = [...`#$%&'()*+,/:;<=>?@[\\]^\`{|}~ ¡¢£¤¥¦§¨©ª«¬­®¯°±² +³´µ¶·¸¹º»¼½¾¿À汉字`]; +const badKinds = invalidSampleChars.map((char) => ({ kind: char, key: 'test' })); + +describe.each([ + {}, + { kind: 'kind', key: 'kind' }, + { kind: {}, key: 'key' }, + { kind: 17, key: 'key' }, + { kind: 'multi', key: 'key' }, + { kind: 'multi', bad: 'value' }, + { kind: 'multi', 'p@rty': 'value' }, + { + kind: 'multi', + bad: { + key: 17, + }, + }, + ...badKinds, +])('given invalid LDContext', (ldConext) => { + it('should not create a context', () => { + // Force TS to accept our bad contexts. + // @ts-ignore + expect(Context.FromLDContext(ldConext)).toBeUndefined(); + }); +}); + +describe.each([ + { + key: 'test', + name: 'context name', + custom: { cat: 'calico', '/dog~~//': 'lab' }, + anonymous: true, + secondary: 'secondary', + privateAttributeNames: ['/dog~~//'], + }, + { + kind: 'user', + key: 'test', + name: 'context name', + cat: 'calico', + transient: true, + _meta: { secondary: 'secondary', privateAttributes: ['/~1dog~0~0~1~1'] }, + }, + { + kind: 'multi', + user: { + key: 'test', + cat: 'calico', + transient: true, + name: 'context name', + _meta: { secondary: 'secondary', privateAttributes: ['/~1dog~0~0~1~1'] }, + }, + }, +])('given a series of equivalent good user contexts', (ldConext) => { + // Here we are providing good contexts, but the types derived from + // the parameterization are causing some problems. + // @ts-ignore + const context = Context.FromLDContext(ldConext); + + it('should create a context', () => { + expect(context).toBeDefined(); + }); + + it('should get the same values', () => { + expect(context?.valueForKind('user', new AttributeReference('cat'))).toEqual('calico'); + expect(context?.valueForKind('user', new AttributeReference('name'))).toEqual('context name'); + expect(context?.kinds).toStrictEqual(['user']); + expect(context?.kindsAndKeys).toStrictEqual({ user: 'test' }); + // Canonical keys for 'user' contexts are just the key. + expect(context?.canonicalKey).toEqual('test'); + expect(context?.valueForKind('user', new AttributeReference('transient'))).toBeTruthy(); + expect(context?.secondary('user')).toEqual('secondary'); + expect(context?.isMultiKind).toBeFalsy(); + expect(context?.privateAttributes('user')?.[0].redactionName) + .toEqual(new AttributeReference('/~1dog~0~0~1~1').redactionName); + }); + + it('should not get values for a context kind that does not exist', () => { + expect(context?.valueForKind('org', new AttributeReference('cat'))).toBeUndefined(); + }); + + it('should have the correct kinds', () => { + expect(context?.kinds).toEqual(['user']); + }); + + it('should have the correct kinds and keys', () => { + expect(context?.kindsAndKeys).toEqual({ user: 'test' }); + }); +}); + +describe('given a valid legacy user without custom attributes', () => { + const context = Context.FromLDContext({ + key: 'test', + name: 'context name', + custom: { cat: 'calico', '/dog~~//': 'lab' }, + anonymous: true, + secondary: 'secondary', + privateAttributeNames: ['/dog~~//'], + }); + + it('should create a context', () => { + expect(context).toBeDefined(); + }); + + it('should get expected values', () => { + expect(context?.valueForKind('user', new AttributeReference('name'))).toEqual('context name'); + expect(context?.kinds).toStrictEqual(['user']); + expect(context?.kindsAndKeys).toStrictEqual({ user: 'test' }); + // Canonical keys for 'user' contexts are just the key. + expect(context?.canonicalKey).toEqual('test'); + expect(context?.valueForKind('user', new AttributeReference('transient'))).toBeTruthy(); + expect(context?.secondary('user')).toEqual('secondary'); + expect(context?.isMultiKind).toBeFalsy(); + expect(context?.privateAttributes('user')?.[0].redactionName) + .toEqual(new AttributeReference('/~1dog~0~0~1~1').redactionName); + }); +}); + +describe('given a non-user single kind context', () => { + const context = Context.FromLDContext({ + kind: 'org', + // Key will be URL encoded. + key: 'Org/Key', + value: 'OrgValue', + }); + it('should have the correct canonical key', () => { + expect(context?.canonicalKey).toEqual('org:Org%2FKey'); + }); + + it('secondary should not be defined when not present', () => { + expect(context?.secondary('org')).toBeUndefined(); + }); + + it('should have the correct kinds', () => { + expect(context?.kinds).toEqual(['org']); + }); + + it('should have the correct kinds and keys', () => { + expect(context?.kindsAndKeys).toEqual({ org: 'Org/Key' }); + }); +}); + +it('secondary should be defined when present', () => { + const context = Context.FromLDContext({ + kind: 'org', + // Key will be URL encoded. + key: 'Org/Key', + value: 'OrgValue', + _meta: { secondary: 'secondary' }, + }); + expect(context?.secondary('org')).toEqual('secondary'); +}); + +it('secondary should be undefined when meta is present, but secondary is not', () => { + const context = Context.FromLDContext({ + kind: 'org', + // Key will be URL encoded. + key: 'Org/Key', + value: 'OrgValue', + _meta: {}, + }); + expect(context?.secondary('org')).toBeUndefined(); +}); + +it('secondary key should be undefined when not a string', () => { + const context = Context.FromLDContext({ + // @ts-ignore + kind: 'org', + // Key will be URL encoded. + key: 'Org/Key', + // @ts-ignore + value: 'OrgValue', + // @ts-ignore + _meta: { secondary: 17 }, // This really displeases typescript. + }); + expect(context?.secondary('org')).toBeUndefined(); +}); + +describe('given a multi-kind context', () => { + const context = Context.FromLDContext({ + kind: 'multi', + + org: { + key: 'OrgKey', + value: 'OrgValue', + _meta: { + secondary: 'value', + }, + }, + user: { + key: 'User /Key', + // Key will be URL encoded. + value: 'UserValue', + }, + }); + + it('should have the correct canonical key', () => { + expect(context?.canonicalKey).toEqual('org:OrgKey:user:User%20%2FKey'); + }); + + it('should get values from the correct context', () => { + expect(context?.valueForKind('org', new AttributeReference('value'))).toEqual('OrgValue'); + expect(context?.valueForKind('user', new AttributeReference('value'))).toEqual('UserValue'); + + expect(context?.secondary('org')).toEqual('value'); + expect(context?.secondary('user')).toBeUndefined(); + }); + + it('should have the correct kinds', () => { + expect(context?.kinds).toEqual(['org', 'user']); + }); + + it('should have the correct kinds and keys', () => { + expect(context?.kindsAndKeys).toEqual({ org: 'OrgKey', user: 'User /Key' }); + }); +}); diff --git a/server-sdk-common/__tests__/options/validators.test.ts b/sdk-common/__tests__/validators.test.ts similarity index 97% rename from server-sdk-common/__tests__/options/validators.test.ts rename to sdk-common/__tests__/validators.test.ts index 3eb73b57ec..c18b72223b 100644 --- a/server-sdk-common/__tests__/options/validators.test.ts +++ b/sdk-common/__tests__/validators.test.ts @@ -1,4 +1,4 @@ -import TypeValidators, { TypeArray } from '../../src/options/validators'; +import { TypeArray, TypeValidators } from '../src'; const stringValue = 'this is a string'; const numberValue = 3.14159; diff --git a/sdk-common/src/AttributeReference.ts b/sdk-common/src/AttributeReference.ts new file mode 100644 index 0000000000..bf835adf04 --- /dev/null +++ b/sdk-common/src/AttributeReference.ts @@ -0,0 +1,123 @@ +import { LDContextCommon } from './api'; + +/** + * Converts a literal to a ref string. + * @param value + * @returns An escaped literal which can be used as a ref. + */ +function toRefString(value: string): string { + return `/${value.replace(/~/g, '~0').replace(/\//g, '~1')}`; +} + +/** + * Produce a literal from a ref component. + * @param ref + * @returns A literal version of the ref. + */ +function unescape(ref: string): string { + return ref.indexOf('~') ? ref.replace(/~1/g, '/').replace(/~0/g, '~') : ref; +} + +function getComponents(reference: string): string[] { + const referenceWithoutPrefix = reference.startsWith('/') ? reference.substring(1) : reference; + return referenceWithoutPrefix + .split('/') + .map((component) => (unescape(component))); +} + +function isLiteral(reference: string): boolean { + return !reference.startsWith('/'); +} + +function validate(reference: string): boolean { + return !reference.match(/\/\/|(^\/.*~[^0|^1])|~$/); +} + +export default class AttributeReference { + public readonly isValid; + + /** + * When redacting attributes this name can be directly added to the list of + * redactions. + */ + public readonly redactionName; + + private readonly components: string[]; + + /** + * Take an attribute reference string, or literal string, and produce + * an attribute reference. + * + * Legacy user objects would have been created with names not + * references. So, in that case, we need to use them as a component + * without escaping them. + * + * e.g. A user could contain a custom attribute of `/a` which would + * become the literal `a` if treated as a reference. Which would cause + * it to no longer be redacted. + * @param refOrLiteral The attribute reference string or literal string. + * @param literal it true the value should be treated as a literal. + */ + public constructor(refOrLiteral: string, literal: boolean = false) { + if (!literal) { + this.redactionName = refOrLiteral; + if (refOrLiteral === '' || refOrLiteral === '/' || !validate(refOrLiteral)) { + this.isValid = false; + this.components = []; + return; + } + + if (isLiteral(refOrLiteral)) { + this.components = [refOrLiteral]; + } else if (refOrLiteral.indexOf('/', 1) < 0) { + this.components = [unescape(refOrLiteral.slice(1))]; + } else { + this.components = getComponents(refOrLiteral); + } + // The items inside of '_meta' are not intended to be addressable. + // Excluding it as a valid reference means that we can make it non-addressable + // without having to copy all the attributes out of the context object + // provided by the user. + if (this.components[0] === '_meta') { + this.isValid = false; + } else { + this.isValid = true; + } + } else { + const literalVal = refOrLiteral; + this.components = [literalVal]; + this.isValid = literalVal !== ''; + // Literals which start with '/' need escaped to prevent ambiguity. + this.redactionName = literalVal.startsWith('/') ? toRefString(literalVal) : literalVal; + } + } + + public get(target: LDContextCommon) { + const { components, isValid } = this; + if (!isValid) { + return undefined; + } + + let current = target; + + // This doesn't use a range based for loops, because those use generators. + // See `no-restricted-syntax`. + // It also doesn't use a collection method because this logic is more + // straightforward with a loop. + for (let index = 0; index < components.length; index += 1) { + const component = components[index]; + if ( + current !== null + && current !== undefined + // See https://eslint.org/docs/rules/no-prototype-builtins + && Object.prototype.hasOwnProperty.call(current, component) + && typeof current === 'object' + ) { + current = current[component]; + } else { + return undefined; + } + } + return current; + } +} diff --git a/sdk-common/src/Context.ts b/sdk-common/src/Context.ts new file mode 100644 index 0000000000..65f889b705 --- /dev/null +++ b/sdk-common/src/Context.ts @@ -0,0 +1,389 @@ +/* eslint-disable no-underscore-dangle */ +// eslint-disable-next-line max-classes-per-file +import { + LDSingleKindContext, LDMultiKindContext, LDUser, LDContextCommon, +} from './api/context'; +import { LDContext } from './api/LDContext'; +import AttributeReference from './AttributeReference'; +import { TypeValidators } from './validators'; + +// The general strategy for the context is to tranform the passed in context +// as little as possible. We do convert the legacy users to a single kind +// context, but we do not translate all passed contexts into a rigid structure. +// The context will have to be copied for events, but we want to avoid any +// copying that we can. +// So we validate that the information we are given is correct, and then we +// just proxy calls with a nicely typed interface. +// This is to reduce work on the hot-path. Later, for event processing, deeper +// cloning of the context will be done. + +// Validates a kind excluding check that it isn't "kind". +const KindValidator = TypeValidators.StringMatchingRegex(/^(\w|\.|-)+$/); + +// The API allows for calling with an `LDContext` which is +// `LDUser | LDSingleKindContext | LDMultiKindContext`. When ingesting a context +// first the type must be determined to allow us to put it into a consistent type. + +/** + * Check if a context is a single kind context. + * @param context + * @returns frue if the context is a single kind context. + */ +function isSingleKind(context: LDContext): context is LDSingleKindContext { + if ('kind' in context) { + return TypeValidators.String.is(context.kind) && context.kind !== 'multi'; + } + return false; +} + +/** + * Check if a context is a multi-kind context. + * @param context + * @returns true if it is a multi-kind context. + */ +function isMultiKind(context: LDContext): context is LDMultiKindContext { + if ('kind' in context) { + return TypeValidators.String.is(context.kind) && context.kind === 'multi'; + } + return false; +} + +/** + * Check if a context is a legacy user context. + * @param context + * @returns true if it is a legacy user context. + */ +function isLegacyUser(context: LDContext): context is LDUser { + return !('kind' in context) || context.kind === null || context.kind === undefined; +} + +/** + * Check if the given value is a LDContextCommon. + * @param kindOrContext + * @returns true if it is an LDContextCommon + * + * Due to a limitation in the expressiveness of these highly polymorphic types any field + * in a multi-kind context can either be a context or 'kind'. So we need to re-assure + * the compiler that it isn't the word multi. + * + * Because we do not allow top level values in a multi-kind context we can validate + * that as well. + */ +function isContextCommon(kindOrContext: 'multi' | LDContextCommon): kindOrContext is LDContextCommon { + return TypeValidators.Object.is(kindOrContext); +} + +/** + * Validate a context kind. + * @param kind + * @returns true if the kind is valid. + */ +function validKind(kind: string) { + return KindValidator.is(kind) && kind !== 'kind'; +} + +/** + * Validate a context key. + * @param key + * @returns true if the key is valid. + */ +function validKey(key: string) { + return TypeValidators.String.is(key) && key !== ''; +} + +function processPrivateAttributes( + privateAttributes?: string[], + literals: boolean = false, +): AttributeReference[] { + if (privateAttributes) { + return privateAttributes.map( + (privateAttribute) => new AttributeReference(privateAttribute, literals), + ); + } + return []; +} + +function defined(value: any) { + return value !== null && value !== undefined; +} + +/** + * Convert a legacy user to a single kind context. + * @param user + * @returns A single kind context. + */ +function legacyToSingleKind(user: LDUser): LDSingleKindContext { + const singleKindContext: LDSingleKindContext = { + ...(user.custom || []), + kind: 'user', + key: user.key, + }; + + // For legacy users we never established a difference between null + // and undefined for inputs. Because transient can be used in evaluations + // we would want it to not possibly match true/false unless defined. + // Which is different than coercing a null/undefined transient as `false`. + if (defined(user.anonymous)) { + const transient = !!user.anonymous; + delete singleKindContext.anonymous; + singleKindContext.transient = transient; + } + + if (defined(user.secondary)) { + singleKindContext._meta = {}; + const { secondary } = user; + delete singleKindContext.secondary; + singleKindContext._meta.secondary = secondary; + } + + // TODO: Determine if we want to enforce typing. Previously we would have + // stringified these at event type. + singleKindContext.name = user.name; + singleKindContext.ip = user.ip; + singleKindContext.firstName = user.firstName; + singleKindContext.lastName = user.lastName; + singleKindContext.email = user.email; + singleKindContext.avatar = user.avatar; + singleKindContext.country = user.country; + + // We are not pulling private attributes over because we will serialize + // those from attribute references for events. + + return singleKindContext; +} + +/** + * Container for a context/contexts. Because contexts come from external code + * they must be thoroughly validated and then formed to comply with + * the type system. + */ +export default class Context { + private context?: LDContextCommon; + + private isMulti: boolean = false; + + private isUser: boolean = false; + + private contexts: Record = {}; + + private privateAttributeReferences?: Record; + + public readonly kind: string; + + /** + * Contexts should be created using the static factory method {@link Context.FromLDContext}. + * @param kind The kind of the context. + * + * The factory methods are static functions within the class because they access private + * implementation details, so they cannot be free functions. + */ + private constructor(kind: string) { + this.kind = kind; + } + + private static getValueFromContext( + reference: AttributeReference, + context?: LDContextCommon, + ): any { + if (!context || !reference.isValid) { + return undefined; + } + + return reference.get(context); + } + + private contextForKind(kind: string): LDContextCommon | undefined { + if (this.isMulti) { + return this.contexts[kind]; + } + if (this.kind === kind) { + return this.context; + } + return undefined; + } + + private static FromMultiKindContext(context: LDMultiKindContext): Context | undefined { + const kinds = Object.keys(context).filter((key) => key !== 'kind'); + const kindsValid = kinds.every(validKind) && kinds.length; + + if (!kindsValid) { + return undefined; + } + + const privateAttributes: Record = {}; + let contextsAreObjects = true; + const contexts = kinds.reduce((acc: Record, kind) => { + const singleContext = context[kind]; + if (isContextCommon(singleContext)) { + acc[kind] = singleContext; + privateAttributes[kind] = processPrivateAttributes(singleContext._meta?.privateAttributes); + } else { + // No early break isn't the most efficient, but it is an error condition. + contextsAreObjects = false; + } + return acc; + }, {}); + + if (!contextsAreObjects) { + return undefined; + } + + if (!Object.values(contexts).every((part) => validKey(part.key))) { + return undefined; + } + + // There was only a single kind in the multi-kind context. + // So we can just translate this to a single-kind context. + // TODO: Node was not doing this. So we should determine if we want to do this. + // it would make it consistent with strongly typed SDKs. + if (kinds.length === 1) { + const kind = kinds[0]; + const created = new Context(kind); + created.context = contexts[kind]; + created.privateAttributeReferences = privateAttributes; + created.isUser = kind === 'user'; + return created; + } + + const created = new Context(context.kind); + created.contexts = contexts; + created.privateAttributeReferences = privateAttributes; + + created.isMulti = true; + return created; + } + + private static FromSingleKindContext(context: LDSingleKindContext): Context | undefined { + const { key, kind } = context; + const kindValid = validKind(kind); + const keyValid = validKey(key); + + if (keyValid && kindValid) { + // The JSON interfaces uses dangling _. + // eslint-disable-next-line no-underscore-dangle + const privateAttributeReferences = processPrivateAttributes(context._meta?.privateAttributes); + const created = new Context(kind); + created.isUser = kind === 'user'; + created.context = context; + created.privateAttributeReferences = { + [kind]: privateAttributeReferences, + }; + return created; + } + return undefined; + } + + private static FromLegacyUser(context: LDUser): Context | undefined { + const keyValid = context.key !== undefined && context.key !== null; + // For legacy users we allow empty keys. + if (!keyValid) { + return undefined; + } + const created = new Context('user'); + created.isUser = true; + created.context = legacyToSingleKind(context); + created.privateAttributeReferences = { + user: processPrivateAttributes(context.privateAttributeNames, true), + }; + return created; + } + + /** + * Attempt to create a {@link Context} from an {@link LDContext}. + * @param context The input context to create a Context from. + * @returns a {@link Context} or `undefined` if one could not be created. + */ + public static FromLDContext(context: LDContext): Context | undefined { + if (isSingleKind(context)) { + return Context.FromSingleKindContext(context); + } if (isMultiKind(context)) { + return Context.FromMultiKindContext(context); + } if (isLegacyUser(context)) { + return Context.FromLegacyUser(context); + } + return undefined; + } + + /** + * Attempt to get a value for the given context kind using the given reference. + * @param kind The kind of the context to get the value for. + * @param reference The reference to the value to get. + * @returns a value or `undefined` if one is not found. + */ + public valueForKind(kind: string, reference: AttributeReference): any | undefined { + return Context.getValueFromContext(reference, this.contextForKind(kind)); + } + + /** + * Attempt to get a secondary key from a context. + * @param kind The kind of the context to get the secondary key for. + * @returns the secondary key, or undefined if not present or not a string. + */ + public secondary(kind: string): string | undefined { + const context = this.contextForKind(kind); + if (defined(context?._meta?.secondary) + && TypeValidators.String.is(context?._meta?.secondary)) { + return context?._meta?.secondary; + } + return undefined; + } + + /** + * True if this is a multi-kind context. + */ + public get isMultiKind(): boolean { + return this.isMulti; + } + + /** + * Get the canonical key for this context. + */ + public get canonicalKey(): string { + if (this.isUser) { + return this.context!.key; + } + if (this.isMulti) { + return Object.keys(this.contexts).map((key) => `${key}:${encodeURIComponent(this.contexts[key].key)}`) + .join(':'); + } + return `${this.kind}:${encodeURIComponent(this.context!.key)}`; + } + + /** + * Get the kinds of this context. + */ + public get kinds(): string[] { + if (this.isMulti) { + return Object.keys(this.contexts); + } + return [this.kind]; + } + + /** + * Get the kinds, and their keys, for this context. + */ + public get kindsAndKeys(): Record { + if (this.isMulti) { + return Object.entries(this.contexts) + .reduce((acc: Record, [kind, context]) => { + acc[kind] = context.key; + return acc; + }, {}); + } + return { [this.kind]: this.context!.key }; + } + + /** + * Get the attribute references. + * + * For now this is for testing and therefore is flagged internal. + * It will not be accessible outside this package. + * + * @internal + * + * @param kind + */ + public privateAttributes(kind: string): AttributeReference[] { + return this.privateAttributeReferences?.[kind] || []; + } +} diff --git a/server-sdk-common/src/api/LDContext.ts b/sdk-common/src/api/LDContext.ts similarity index 85% rename from server-sdk-common/src/api/LDContext.ts rename to sdk-common/src/api/LDContext.ts index 5c9f6cb9b2..6d608228ba 100644 --- a/server-sdk-common/src/api/LDContext.ts +++ b/sdk-common/src/api/LDContext.ts @@ -1,6 +1,6 @@ import { LDMultiKindContext } from './context/LDMultiKindContext'; import { LDSingleKindContext } from './context/LDSingleKindContext'; -import { LDUser } from './LDUser'; +import { LDUser } from './context/LDUser'; /** * A LaunchDarkly context object. diff --git a/server-sdk-common/src/api/context/LDContextCommon.ts b/sdk-common/src/api/context/LDContextCommon.ts similarity index 82% rename from server-sdk-common/src/api/context/LDContextCommon.ts rename to sdk-common/src/api/context/LDContextCommon.ts index 67941172c0..8c45b01cc5 100644 --- a/server-sdk-common/src/api/context/LDContextCommon.ts +++ b/sdk-common/src/api/context/LDContextCommon.ts @@ -22,6 +22,11 @@ export interface LDContextCommon { */ _meta?: LDContextMeta; + /** + * If true, the context will _not_ appear on the Contexts page in the LaunchDarkly dashboard. + */ + transient?: boolean; + /** * Any additional attributes associated with the context. */ diff --git a/server-sdk-common/src/api/context/LDContextMeta.ts b/sdk-common/src/api/context/LDContextMeta.ts similarity index 90% rename from server-sdk-common/src/api/context/LDContextMeta.ts rename to sdk-common/src/api/context/LDContextMeta.ts index 9115450f02..b49efcc0cd 100644 --- a/server-sdk-common/src/api/context/LDContextMeta.ts +++ b/sdk-common/src/api/context/LDContextMeta.ts @@ -6,11 +6,6 @@ * cannot be addressed in targetting rules. */ export interface LDContextMeta { - /** - * If true, the context will _not_ appear on the Contexts page in the LaunchDarkly dashboard. - */ - transient?: boolean; - /** * An optional secondary key for a context. * diff --git a/server-sdk-common/src/api/context/LDMultiKindContext.ts b/sdk-common/src/api/context/LDMultiKindContext.ts similarity index 100% rename from server-sdk-common/src/api/context/LDMultiKindContext.ts rename to sdk-common/src/api/context/LDMultiKindContext.ts diff --git a/server-sdk-common/src/api/context/LDSingleKindContext.ts b/sdk-common/src/api/context/LDSingleKindContext.ts similarity index 100% rename from server-sdk-common/src/api/context/LDSingleKindContext.ts rename to sdk-common/src/api/context/LDSingleKindContext.ts diff --git a/server-sdk-common/src/api/LDUser.ts b/sdk-common/src/api/context/LDUser.ts similarity index 100% rename from server-sdk-common/src/api/LDUser.ts rename to sdk-common/src/api/context/LDUser.ts diff --git a/server-sdk-common/src/api/context/index.ts b/sdk-common/src/api/context/index.ts similarity index 84% rename from server-sdk-common/src/api/context/index.ts rename to sdk-common/src/api/context/index.ts index 7babf9615c..c7ff48d3ee 100644 --- a/server-sdk-common/src/api/context/index.ts +++ b/sdk-common/src/api/context/index.ts @@ -2,3 +2,4 @@ export * from './LDContextCommon'; export * from './LDContextMeta'; export * from './LDMultiKindContext'; export * from './LDSingleKindContext'; +export * from './LDUser'; diff --git a/sdk-common/src/api/index.ts b/sdk-common/src/api/index.ts new file mode 100644 index 0000000000..371c16e0e1 --- /dev/null +++ b/sdk-common/src/api/index.ts @@ -0,0 +1,2 @@ +export * from './context'; +export * from './LDContext'; diff --git a/sdk-common/src/index.ts b/sdk-common/src/index.ts index e69de29bb2..d06405c841 100644 --- a/sdk-common/src/index.ts +++ b/sdk-common/src/index.ts @@ -0,0 +1,2 @@ +export * from './api'; +export * from './validators'; diff --git a/server-sdk-common/src/options/validators.ts b/sdk-common/src/validators.ts similarity index 96% rename from server-sdk-common/src/options/validators.ts rename to sdk-common/src/validators.ts index 64c3ca5999..bdebc3f7fc 100644 --- a/server-sdk-common/src/options/validators.ts +++ b/sdk-common/src/validators.ts @@ -10,8 +10,6 @@ /** * Interface for type validation. - * - * @internal */ export interface TypeValidator { is(u:unknown): boolean; @@ -20,8 +18,6 @@ export interface TypeValidator { /** * Validate a factory or instance. - * - * @internal */ export class FactoryOrInstance implements TypeValidator { is(factoryOrInstance: unknown) { @@ -40,8 +36,6 @@ export class FactoryOrInstance implements TypeValidator { /** * Validate a basic type. - * - * @internal */ export class Type implements TypeValidator { private typeName: string; @@ -65,6 +59,9 @@ export class Type implements TypeValidator { } } +/** + * Validate an array of the specified type. + */ export class TypeArray implements TypeValidator { private typeName: string; @@ -92,8 +89,6 @@ export class TypeArray implements TypeValidator { /** * Validate a value is a number and is greater or eval than a minimum. - * - * @internal */ export class NumberWithMinimum extends Type { readonly min: number; @@ -110,8 +105,6 @@ export class NumberWithMinimum extends Type { /** * Validate a value is a string and it matches the given expression. - * - * @internal */ export class StringMatchingRegex extends Type { readonly expression: RegExp; @@ -128,10 +121,8 @@ export class StringMatchingRegex extends Type { /** * A set of standard type validators. - * - * @internal */ -export default class TypeValidators { +export class TypeValidators { static readonly String = new Type('string', ''); static readonly Number = new Type('number', 0); diff --git a/server-sdk-common/__tests__/Logger.ts b/server-sdk-common/__tests__/Logger.ts index 0082fa2cee..980cf362a8 100644 --- a/server-sdk-common/__tests__/Logger.ts +++ b/server-sdk-common/__tests__/Logger.ts @@ -64,9 +64,9 @@ export default class TestLogger implements LDLogger { (receivedMessage) => receivedMessage.match(expectedMessage.matches), ); if (index < 0) { - throw new Error(`Did not find expected message: ${expectedMessage}`); + throw new Error(`Did not find expected message: ${expectedMessage} received: ${this.messages}`); } else if (matched[expectedMessage.level].indexOf(index) >= 0) { - throw new Error(`Did not find expected message: ${expectedMessage}`); + throw new Error(`Did not find expected message: ${expectedMessage} received: ${this.messages}`); } else { matched[expectedMessage.level].push(index); } diff --git a/server-sdk-common/package.json b/server-sdk-common/package.json index 97edcb76bd..1d57466323 100644 --- a/server-sdk-common/package.json +++ b/server-sdk-common/package.json @@ -17,6 +17,9 @@ "build": "tsc" }, "license": "Apache-2.0", + "dependencies": { + "@launchdarkly/js-sdk-common": "@launchdarkly/js-sdk-common" + }, "devDependencies": { "@types/jest": "^27.4.1", "jest": "^27.5.1", diff --git a/server-sdk-common/src/LDClientImpl.ts b/server-sdk-common/src/LDClientImpl.ts index 99a6f0dcb1..d5cda587f3 100644 --- a/server-sdk-common/src/LDClientImpl.ts +++ b/server-sdk-common/src/LDClientImpl.ts @@ -1,7 +1,8 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable class-methods-use-this */ +import { LDContext } from '@launchdarkly/js-sdk-common'; import { - LDClient, LDContext, LDEvaluationDetail, LDFlagsState, LDFlagsStateOptions, + LDClient, LDEvaluationDetail, LDFlagsState, LDFlagsStateOptions, } from './api'; import BigSegmentStoreStatusProvider from './BigSegmentStatusProviderImpl'; import { Platform } from './platform'; diff --git a/server-sdk-common/src/api/LDClient.ts b/server-sdk-common/src/api/LDClient.ts index ada8704a04..222c3a05c7 100644 --- a/server-sdk-common/src/api/LDClient.ts +++ b/server-sdk-common/src/api/LDClient.ts @@ -1,4 +1,4 @@ -import { LDContext } from './LDContext'; +import { LDContext } from '@launchdarkly/js-sdk-common'; import { LDEvaluationDetail } from './data/LDEvaluationDetail'; import { LDFlagsState } from './data/LDFlagsState'; import { LDFlagsStateOptions } from './data/LDFlagsStateOptions'; diff --git a/server-sdk-common/src/api/index.ts b/server-sdk-common/src/api/index.ts index bcdb25da43..59edda462f 100644 --- a/server-sdk-common/src/api/index.ts +++ b/server-sdk-common/src/api/index.ts @@ -1,12 +1,9 @@ -export * from './context'; export * from './data'; export * from './options'; export * from './LDClient'; export * from './LDLogger'; export * from './LDLogLevel'; export * from './subsystems/LDStreamProcessor'; -export * from './LDContext'; -export * from './LDUser'; // These are items that should be less frequently used, and therefore they // are namespaced to reduce clutter amongst the top level exports. diff --git a/server-sdk-common/src/options/ApplicationTags.ts b/server-sdk-common/src/options/ApplicationTags.ts index 00f9305016..43b4aea5e6 100644 --- a/server-sdk-common/src/options/ApplicationTags.ts +++ b/server-sdk-common/src/options/ApplicationTags.ts @@ -1,6 +1,6 @@ +import { TypeValidators } from '@launchdarkly/js-sdk-common'; import OptionMessages from './OptionMessages'; import { ValidatedOptions } from './ValidatedOptions'; -import TypeValidators from './validators'; /** * Expression to validate characters that are allowed in tag keys and values. diff --git a/server-sdk-common/src/options/Configuration.ts b/server-sdk-common/src/options/Configuration.ts index 8fe8fe44f4..a58394f28c 100644 --- a/server-sdk-common/src/options/Configuration.ts +++ b/server-sdk-common/src/options/Configuration.ts @@ -1,3 +1,4 @@ +import { NumberWithMinimum, TypeValidator, TypeValidators } from '@launchdarkly/js-sdk-common'; import { LDLogger, LDOptions, LDProxyOptions, LDTLSOptions, } from '../api'; @@ -5,7 +6,6 @@ import ApplicationTags from './ApplicationTags'; import OptionMessages from './OptionMessages'; import ServiceEndpoints from './ServiceEndpoints'; import { ValidatedOptions } from './ValidatedOptions'; -import TypeValidators, { NumberWithMinimum, TypeValidator } from './validators'; // Once things are internal to the implementation of the SDK we can depend on // types. Calls to the SDK could contain anything without any regard to typing.