From 91a6addc12032a0b93447845821f420fb894be5c Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 27 Jul 2023 14:44:17 -0700 Subject: [PATCH] feat: Add support for payloads to read and write methods. --- .../sdk-server/__tests__/Migration.test.ts | 49 ++++- packages/shared/sdk-server/src/Migration.ts | 207 +++++++++++------- .../shared/sdk-server/src/api/LDMigration.ts | 13 +- .../src/api/options/LDMigrationOptions.ts | 15 +- 4 files changed, 193 insertions(+), 91 deletions(-) diff --git a/packages/shared/sdk-server/__tests__/Migration.test.ts b/packages/shared/sdk-server/__tests__/Migration.test.ts index 22cc76b1c..aca8bebd1 100644 --- a/packages/shared/sdk-server/__tests__/Migration.test.ts +++ b/packages/shared/sdk-server/__tests__/Migration.test.ts @@ -123,7 +123,7 @@ describe('given an LDClient with test data', () => { [new LDSerialExecution(LDExecutionOrdering.Random), 'serial random'], [new LDConcurrentExecution(), 'concurrent'], ])('given different execution methods: %p %p', (execution) => { - it.each([ + describe.each([ [ LDMigrationStage.Off, 'old', @@ -172,9 +172,8 @@ describe('given an LDClient with test data', () => { nonAuthoritative: undefined, }, ], - ])( - 'uses the correct authoritative source: %p, read: %p, write: %j.', - async (stage, readValue, writeMatch) => { + ])('given each migration step: %p, read: %p, write: %j.', (stage, readValue, writeMatch) => { + it('uses the correct authoritative source', async () => { const migration = new Migration(client, { execution, latencyTracking: LDLatencyTracking.Disabled, @@ -202,8 +201,46 @@ describe('given an LDClient with test data', () => { const write = await migration.write(flagKey, { key: 'test-key' }, defaultStage!); // @ts-ignore Extended without writing types. expect(write).toMatchMigrationResult(writeMatch); - } - ); + }); + + it('correctly forwards the payload for read and write operations', async () => { + let receivedReadPayload: string | undefined; + let receivedWritePayload: string | undefined; + const migration = new Migration(client, { + execution, + latencyTracking: LDLatencyTracking.Disabled, + errorTracking: LDErrorTracking.Disabled, + readNew: async (payload) => { + receivedReadPayload = payload; + return LDMigrationSuccess('new'); + }, + writeNew: async (payload) => { + receivedWritePayload = payload; + return LDMigrationSuccess(false); + }, + readOld: async (payload) => { + receivedReadPayload = payload; + return LDMigrationSuccess('old'); + }, + writeOld: async (payload) => { + receivedWritePayload = payload; + return LDMigrationSuccess(true); + }, + }); + + const flagKey = 'migration'; + td.update(td.flag(flagKey).valueForAll(stage)); + + const payloadRead = Math.random().toString(10); + const payloadWrite = Math.random().toString(10); + await migration.read(flagKey, { key: 'test-key' }, LDMigrationStage.Off, payloadRead); + + await migration.write(flagKey, { key: 'test-key' }, LDMigrationStage.Off, payloadWrite); + + expect(receivedReadPayload).toEqual(payloadRead); + expect(receivedWritePayload).toEqual(payloadWrite); + }); + }); }); it.each([ diff --git a/packages/shared/sdk-server/src/Migration.ts b/packages/shared/sdk-server/src/Migration.ts index f837503ef..9a455088f 100644 --- a/packages/shared/sdk-server/src/Migration.ts +++ b/packages/shared/sdk-server/src/Migration.ts @@ -30,54 +30,93 @@ async function safeCall( } } -async function readSequentialRandom( - config: LDMigrationOptions +async function readSequentialRandom< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput +>( + config: LDMigrationOptions< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput + >, + payload?: TMigrationReadInput ): Promise> { // This number is not used for a purpose requiring cryptographic security. const randomIndex = Math.floor(Math.random() * 2); // Effectively flip a coin and do it on one order or the other. if (randomIndex === 0) { - const fromOld = await safeCall(() => config.readOld()); - const fromNew = await safeCall(() => config.readNew()); + const fromOld = await safeCall(() => config.readOld(payload)); + const fromNew = await safeCall(() => config.readNew(payload)); return { fromOld, fromNew }; } - const fromNew = await safeCall(() => config.readNew()); - const fromOld = await safeCall(() => config.readOld()); + const fromNew = await safeCall(() => config.readNew(payload)); + const fromOld = await safeCall(() => config.readOld(payload)); return { fromOld, fromNew }; } -async function readSequentialFixed( - config: LDMigrationOptions +async function readSequentialFixed< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput +>( + config: LDMigrationOptions< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput + >, + payload?: TMigrationReadInput ): Promise> { - const fromOld = await safeCall(() => config.readOld()); - const fromNew = await safeCall(() => config.readNew()); + const fromOld = await safeCall(() => config.readOld(payload)); + const fromNew = await safeCall(() => config.readNew(payload)); return { fromOld, fromNew }; } -async function readConcurrent( - config: LDMigrationOptions +async function readConcurrent< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput +>( + config: LDMigrationOptions< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput + >, + payload?: TMigrationReadInput ): Promise> { - const fromOldPromise = safeCall(() => config.readOld()); - const fromNewPromise = safeCall(() => config.readNew()); + const fromOldPromise = safeCall(() => config.readOld(payload)); + const fromNewPromise = safeCall(() => config.readNew(payload)); const [fromOld, fromNew] = await Promise.all([fromOldPromise, fromNewPromise]); return { fromOld, fromNew }; } -async function read( - config: LDMigrationOptions, - execution: LDSerialExecution | LDConcurrentExecution +async function read( + config: LDMigrationOptions< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput + >, + execution: LDSerialExecution | LDConcurrentExecution, + payload?: TMigrationReadInput ): Promise> { if (execution.type === LDExecution.Serial) { const serial = execution as LDSerialExecution; if (serial.ordering === LDExecutionOrdering.Fixed) { - return readSequentialFixed(config); + return readSequentialFixed(config, payload); } - return readSequentialRandom(config); + return readSequentialRandom(config, payload); } - return readConcurrent(config); + return readConcurrent(config, payload); } export function LDMigrationSuccess(result: TResult): LDMethodResult { @@ -94,130 +133,142 @@ export function LDMigrationError(error: Error): { success: false; error: Error } }; } -export default class Migration - implements LDMigration +export default class Migration< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput = any, + TMigrationWriteInput = any +> implements + LDMigration { private readonly execution: LDSerialExecution | LDConcurrentExecution; private readonly readTable: { [index: string]: ( - config: LDMigrationOptions + config: LDMigrationOptions< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput + >, + payload?: TMigrationReadInput ) => Promise>; } = { - [LDMigrationStage.Off]: async ( - config: LDMigrationOptions - ) => ({ origin: 'old', ...(await safeCall(() => config.readOld())) }), - [LDMigrationStage.DualWrite]: async ( - config: LDMigrationOptions - ) => ({ origin: 'old', ...(await safeCall(() => config.readOld())) }), - [LDMigrationStage.Shadow]: async ( - config: LDMigrationOptions - ) => { - const { fromOld } = await read(config, this.execution); + [LDMigrationStage.Off]: async (config, payload) => ({ + origin: 'old', + ...(await safeCall(() => config.readOld(payload))), + }), + [LDMigrationStage.DualWrite]: async (config, payload) => ({ + origin: 'old', + ...(await safeCall(() => config.readOld(payload))), + }), + [LDMigrationStage.Shadow]: async (config, payload) => { + const { fromOld } = await read(config, this.execution, payload); // TODO: Consistency check. return { origin: 'old', ...fromOld }; }, - [LDMigrationStage.Live]: async ( - config: LDMigrationOptions - ) => { - const { fromNew } = await read(config, this.execution); + [LDMigrationStage.Live]: async (config, payload) => { + const { fromNew } = await read(config, this.execution, payload); // TODO: Consistency check. return { origin: 'new', ...fromNew }; }, - [LDMigrationStage.RampDown]: async ( - config: LDMigrationOptions - ) => ({ origin: 'new', ...(await safeCall(() => config.readNew())) }), - [LDMigrationStage.Complete]: async ( - config: LDMigrationOptions - ) => ({ origin: 'new', ...(await safeCall(() => config.readNew())) }), + [LDMigrationStage.RampDown]: async (config, payload) => ({ + origin: 'new', + ...(await safeCall(() => config.readNew(payload))), + }), + [LDMigrationStage.Complete]: async (config, payload) => ({ + origin: 'new', + ...(await safeCall(() => config.readNew(payload))), + }), }; private readonly writeTable: { [index: string]: ( - config: LDMigrationOptions + config: LDMigrationOptions< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput + >, + payload?: TMigrationWriteInput ) => Promise>; } = { - [LDMigrationStage.Off]: async ( - config: LDMigrationOptions - ) => ({ authoritative: { origin: 'old', ...(await safeCall(() => config.writeOld())) } }), - [LDMigrationStage.DualWrite]: async ( - config: LDMigrationOptions - ) => { - const fromOld = await safeCall(() => config.writeOld()); + [LDMigrationStage.Off]: async (config, payload) => ({ + authoritative: { origin: 'old', ...(await safeCall(() => config.writeOld(payload))) }, + }), + [LDMigrationStage.DualWrite]: async (config, payload) => { + const fromOld = await safeCall(() => config.writeOld(payload)); if (!fromOld.success) { return { authoritative: { origin: 'old', ...fromOld }, }; } - const fromNew = await safeCall(() => config.writeNew()); + const fromNew = await safeCall(() => config.writeNew(payload)); return { authoritative: { origin: 'old', ...fromOld }, nonAuthoritative: { origin: 'new', ...fromNew }, }; }, - [LDMigrationStage.Shadow]: async ( - config: LDMigrationOptions - ) => { - const fromOld = await safeCall(() => config.writeOld()); + [LDMigrationStage.Shadow]: async (config, payload) => { + const fromOld = await safeCall(() => config.writeOld(payload)); if (!fromOld.success) { return { authoritative: { origin: 'old', ...fromOld }, }; } - const fromNew = await safeCall(() => config.writeNew()); + const fromNew = await safeCall(() => config.writeNew(payload)); return { authoritative: { origin: 'old', ...fromOld }, nonAuthoritative: { origin: 'new', ...fromNew }, }; }, - [LDMigrationStage.Live]: async ( - config: LDMigrationOptions - ) => { - const fromNew = await safeCall(() => config.writeNew()); + [LDMigrationStage.Live]: async (config, payload) => { + const fromNew = await safeCall(() => config.writeNew(payload)); if (!fromNew.success) { return { authoritative: { origin: 'new', ...fromNew } }; } - const fromOld = await safeCall(() => config.writeOld()); + const fromOld = await safeCall(() => config.writeOld(payload)); return { nonAuthoritative: { origin: 'old', ...fromOld }, authoritative: { origin: 'new', ...fromNew }, }; }, - [LDMigrationStage.RampDown]: async ( - config: LDMigrationOptions - ) => { - const fromNew = await safeCall(() => config.writeNew()); + [LDMigrationStage.RampDown]: async (config, payload) => { + const fromNew = await safeCall(() => config.writeNew(payload)); if (!fromNew.success) { return { authoritative: { origin: 'new', ...fromNew } }; } - const fromOld = await safeCall(() => config.writeOld()); + const fromOld = await safeCall(() => config.writeOld(payload)); return { nonAuthoritative: { origin: 'old', ...fromOld }, authoritative: { origin: 'new', ...fromNew }, }; }, - [LDMigrationStage.Complete]: async ( - config: LDMigrationOptions - ) => ({ authoritative: { origin: 'new', ...(await safeCall(() => config.writeNew())) } }), + [LDMigrationStage.Complete]: async (config, payload) => ({ + authoritative: { origin: 'new', ...(await safeCall(() => config.writeNew(payload))) }, + }), }; constructor( private readonly client: LDClient, - private readonly config: - | LDMigrationOptions - | LDMigrationOptions + private readonly config: LDMigrationOptions< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput + > ) { if (config.execution) { this.execution = config.execution; @@ -229,19 +280,21 @@ export default class Migration async read( key: string, context: LDContext, - defaultStage: LDMigrationStage + defaultStage: LDMigrationStage, + payload?: TMigrationReadInput ): Promise> { const stage = await this.client.variationMigration(key, context, defaultStage); - return this.readTable[stage](this.config); + return this.readTable[stage](this.config, payload); } async write( key: string, context: LDContext, - defaultStage: LDMigrationStage + defaultStage: LDMigrationStage, + payload?: TMigrationWriteInput ): Promise> { const stage = await this.client.variationMigration(key, context, defaultStage); - return this.writeTable[stage](this.config); + return this.writeTable[stage](this.config, payload); } } diff --git a/packages/shared/sdk-server/src/api/LDMigration.ts b/packages/shared/sdk-server/src/api/LDMigration.ts index 9eb063d8a..21458cde1 100644 --- a/packages/shared/sdk-server/src/api/LDMigration.ts +++ b/packages/shared/sdk-server/src/api/LDMigration.ts @@ -54,7 +54,12 @@ export type LDMigrationWriteResult = { * * TKTK */ -export interface LDMigration { +export interface LDMigration< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput +> { /** * TKTK * @@ -67,7 +72,8 @@ export interface LDMigration { read( key: string, context: LDContext, - defaultValue: LDMigrationStage + defaultValue: LDMigrationStage, + payload?: TMigrationReadInput ): Promise>; /** @@ -82,6 +88,7 @@ export interface LDMigration { write( key: string, context: LDContext, - defaultValue: LDMigrationStage + defaultValue: LDMigrationStage, + payload?: TMigrationWriteInput ): Promise>; } diff --git a/packages/shared/sdk-server/src/api/options/LDMigrationOptions.ts b/packages/shared/sdk-server/src/api/options/LDMigrationOptions.ts index 9f829cb45..7846efee4 100644 --- a/packages/shared/sdk-server/src/api/options/LDMigrationOptions.ts +++ b/packages/shared/sdk-server/src/api/options/LDMigrationOptions.ts @@ -79,7 +79,12 @@ export class LDConcurrentExecution { /** * Configuration for a migration. */ -export interface LDMigrationOptions { +export interface LDMigrationOptions< + TMigrationRead, + TMigrationWrite, + TMigrationReadInput, + TMigrationWriteInput +> { /** * Configure how the migration should execute. If omitted the execution will * be concurrent. @@ -103,22 +108,22 @@ export interface LDMigrationOptions { /** * TKTK */ - readNew: () => Promise>; + readNew: (payload?: TMigrationReadInput) => Promise>; /** * TKTK */ - writeNew: () => Promise>; + writeNew: (payload?: TMigrationWriteInput) => Promise>; /** * TKTK */ - readOld: () => Promise>; + readOld: (payload?: TMigrationReadInput) => Promise>; /** * TKTK */ - writeOld: () => Promise>; + writeOld: (payload?: TMigrationWriteInput) => Promise>; /** * TKTK