From 800cbe2e6662251e2bcd51482ed0bc4d0db10460 Mon Sep 17 00:00:00 2001 From: 3m1n3nc3 Date: Wed, 15 Jul 2026 07:22:57 +0100 Subject: [PATCH] fix: retain additional data in async responses --- src/GenericResource.ts | 40 +++++++++++++++++++++++++-------------- src/Resource.ts | 34 ++++++++++++++++++++++----------- src/ResourceCollection.ts | 34 ++++++++++++++++++++++----------- tests/express.spec.ts | 32 +++++++++++++++++++++++++++++++ tests/generic.spec.ts | 21 ++++++++++++++++++++ 5 files changed, 125 insertions(+), 36 deletions(-) diff --git a/src/GenericResource.ts b/src/GenericResource.ts index 5c22931..0a8738c 100644 --- a/src/GenericResource.ts +++ b/src/GenericResource.ts @@ -46,6 +46,7 @@ export class GenericResource< private body: GenericBody = { data: {} as any } private pendingData?: unknown private pendingDataCollected = false + private pendingAdditional: Record[] = [] private res?: Response public resource: R public collects?: typeof Resource @@ -313,6 +314,7 @@ export class GenericResource< rootKey ) as GenericBody this.body = this.applySerializePlugins(this.body) as GenericBody + this.applyPendingAdditional() } // if (this.collects) console.log(this.body, this.constructor.name, this.collects.name) @@ -384,6 +386,7 @@ export class GenericResource< rootKey ) as GenericBody this.body = this.applySerializePlugins(this.body) as GenericBody + this.applyPendingAdditional() } return undefined @@ -429,25 +432,34 @@ export class GenericResource< additional>(extra: X) { this.called.additional = true this.json() + this.pendingAdditional.push({ ...extra }) - const extraData = extra.data + if (this.called.json) { + this.applyPendingAdditional() + } - delete extra.data - delete extra.pagination + return this + } - const payloadKey = this.getPayloadKey() - if (extraData && payloadKey && typeof this.body[payloadKey] !== 'undefined') { - this.body[payloadKey] = Array.isArray(this.body[payloadKey]) - ? [...this.body[payloadKey], ...extraData] - : { ...this.body[payloadKey], ...extraData } - } + private applyPendingAdditional() { + for (const extra of this.pendingAdditional.splice(0)) { + const extraData = extra.data - this.body = { - ...this.body, - ...extra, - } + delete extra.data + delete extra.pagination - return this + const payloadKey = this.getPayloadKey() + if (extraData && payloadKey && typeof this.body[payloadKey] !== 'undefined') { + this.body[payloadKey] = Array.isArray(this.body[payloadKey]) + ? [...this.body[payloadKey], ...extraData] + : { ...this.body[payloadKey], ...extraData } + } + + this.body = { + ...this.body, + ...extra, + } + } } /** diff --git a/src/Resource.ts b/src/Resource.ts index 9f259f8..32f2821 100644 --- a/src/Resource.ts +++ b/src/Resource.ts @@ -39,6 +39,7 @@ export class Resource ex [key: string]: any; private body: ResourceBody = { data: {} as any } private pendingData?: unknown + private pendingAdditional: Record[] = [] private res?: Response public resource: R protected withResponseContext?: { @@ -235,6 +236,7 @@ export class Resource ex this.body = appendRootProperties(this.body, customMeta, rootKey) as ResourceBody this.body = this.applySerializePlugins(this.body) as ResourceBody + this.applyPendingAdditional() } return this @@ -284,6 +286,7 @@ export class Resource ex this.body = appendRootProperties(this.body, customMeta, rootKey) as ResourceBody this.body = this.applySerializePlugins(this.body) as ResourceBody + this.applyPendingAdditional() } return undefined @@ -329,23 +332,32 @@ export class Resource ex additional>(extra: X) { this.called.additional = true this.json() + this.pendingAdditional.push({ ...extra }) - const payloadKey = this.getPayloadKey() - - if (extra.data && payloadKey && typeof this.body[payloadKey] !== 'undefined') { - this.body[payloadKey] = Array.isArray(this.body[payloadKey]) - ? [...this.body[payloadKey], ...extra.data] - : { ...this.body[payloadKey], ...extra.data } - } - - this.body = { - ...this.body, - ...extra, + if (this.called.json) { + this.applyPendingAdditional() } return this } + private applyPendingAdditional() { + for (const extra of this.pendingAdditional.splice(0)) { + const payloadKey = this.getPayloadKey() + + if (extra.data && payloadKey && typeof this.body[payloadKey] !== 'undefined') { + this.body[payloadKey] = Array.isArray(this.body[payloadKey]) + ? [...this.body[payloadKey], ...extra.data] + : { ...this.body[payloadKey], ...extra.data } + } + + this.body = { + ...this.body, + ...extra, + } + } + } + /** * Build a response object, optionally accepting a raw response to mutate in withResponse. */ diff --git a/src/ResourceCollection.ts b/src/ResourceCollection.ts index 4700f06..2e90f73 100644 --- a/src/ResourceCollection.ts +++ b/src/ResourceCollection.ts @@ -45,6 +45,7 @@ export class ResourceCollection< private body: CollectionBody = { data: [] as any } private pendingData?: unknown private pendingDataCollected = false + private pendingAdditional: Record[] = [] private res?: Response public resource: R public collects?: typeof Resource @@ -322,6 +323,7 @@ export class ResourceCollection< rootKey ) as CollectionBody this.body = this.applySerializePlugins(this.body) as CollectionBody + this.applyPendingAdditional() } return this @@ -395,6 +397,7 @@ export class ResourceCollection< rootKey ) as CollectionBody this.body = this.applySerializePlugins(this.body) as CollectionBody + this.applyPendingAdditional() } return undefined @@ -481,22 +484,31 @@ export class ResourceCollection< additional>(extra: X) { this.called.additional = true this.json() + this.pendingAdditional.push({ ...extra }) - delete extra.cursor - delete extra.pagination + if (this.called.json) { + this.applyPendingAdditional() + } - const payloadKey = this.getPayloadKey() + return this + } - if (extra.data && payloadKey && Array.isArray(this.body[payloadKey])) { - this.body[payloadKey] = [...this.body[payloadKey], ...extra.data] as never - } + private applyPendingAdditional() { + for (const extra of this.pendingAdditional.splice(0)) { + delete extra.cursor + delete extra.pagination - this.body = { - ...this.body, - ...extra, - } + const payloadKey = this.getPayloadKey() - return this + if (extra.data && payloadKey && Array.isArray(this.body[payloadKey])) { + this.body[payloadKey] = [...this.body[payloadKey], ...extra.data] as never + } + + this.body = { + ...this.body, + ...extra, + } + } } /** diff --git a/tests/express.spec.ts b/tests/express.spec.ts index d71638a..787199f 100644 --- a/tests/express.spec.ts +++ b/tests/express.spec.ts @@ -352,6 +352,28 @@ describe('Connect-style Requests (Express)', () => { return await new ProfileCollection(models, res).response().setStatusCode(202) }) + app.get('/additional/test', async (_, res) => { + return await new UserResource({ + id: 2, + name: 'John', + profile: { + id: 20, + displayName: 'John Doe', + }, + }, res) + .additional({ data: { fromAdditional: true }, status: 'success' }) + .response() + }) + + app.get('/all/additional/test', async (_, res) => { + return await new ProfileCollection(models, res) + .additional({ + data: [{ id: 3, displayName: 'Additional' }], + status: 'success', + }) + .response() + }) + await request(app).get('/test').expect(202).expect({ data: { id: 1, @@ -366,6 +388,16 @@ describe('Connect-style Requests (Express)', () => { await request(app).get('/all/test').expect(202).expect({ data: models.all(), }) + + await request(app).get('/additional/test').expect({ + data: { fromAdditional: true }, + status: 'success', + }) + + await request(app).get('/all/additional/test').expect({ + data: [{ id: 3, displayName: 'Additional' }], + status: 'success', + }) }) it('should allow class-level withResponse hook to customize headers/status/body', async () => { diff --git a/tests/generic.spec.ts b/tests/generic.spec.ts index 8fa7fdb..557ae1a 100644 --- a/tests/generic.spec.ts +++ b/tests/generic.spec.ts @@ -129,4 +129,25 @@ describe('Generic Core', () => { ], }) }) + + it('should retain additional data after async serialization', async () => { + class AsyncGenericResource extends GenericResource { + data(): any { + return Promise.resolve({ id: 1, name: 'Jane' }) + } + } + + const body = await new AsyncGenericResource({ id: 1, name: 'Jane' }) + .additional({ data: { fromAdditional: true }, status: 'success' }) + .response() + + expect(body).toEqual({ + data: { + id: 1, + name: 'Jane', + fromAdditional: true, + }, + status: 'success', + }) + }) })