From 3ab90b3bec13dff6f2f867db303e4d7d74748545 Mon Sep 17 00:00:00 2001 From: Romain Lanz Date: Thu, 15 Oct 2026 06:49:57 +0000 Subject: [PATCH 1/4] perf: create debugging error only for undefined items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transform() and paginate() created a RuntimeException on every call to get a useful stack trace if the resource turned out to be undefined. Capturing that stack costs a few microseconds per call, and with one call per row and per relation it dominated the CPU profile of nested responses. The error is only thrown by Item.resolve() for an undefined resource, and Collection never reads it. Item now receives the error only when the value is undefined, still created at the transform() call site so the stack keeps pointing to the calling transformer. Collection and paginate() no longer receive one. debuggingError becomes optional in the Item and Collection constructors. Item.resolve() creates the error itself when none was given. On a paginated response with 840 nested transformers, a serialization went from ~1480 µs to ~276 µs (Node 26.10, tsdown build). --- src/base_transformer.ts | 14 ++++++++------ src/resource/collection.ts | 4 ++-- src/resource/item.ts | 10 ++++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/base_transformer.ts b/src/base_transformer.ts index 7ba7180..1793ee9 100644 --- a/src/base_transformer.ts +++ b/src/base_transformer.ts @@ -152,19 +152,24 @@ export abstract class BaseTransformer { } if (Array.isArray(unwrappedValue)) { - return new Collection([unwrappedValue, rest], this, 1, 'toObject', new RuntimeException()) + return new Collection([unwrappedValue, rest], this, 1, 'toObject') } if (unwrappedValue === null) { return null } + /** + * Capturing a stack trace is expensive, so the debugging error is only + * created when the item cannot be resolved. Creating it here keeps the + * stack pointing to the transformer call site. + */ return new Item, 1, 'toObject'>( [unwrappedValue, rest], this, 1, 'toObject', - new RuntimeException() + unwrappedValue === undefined ? new RuntimeException() : undefined ) } @@ -194,10 +199,7 @@ export abstract class BaseTransformer { metaData: Record, ...rest: ExtractTransformerRestTypes> ): Paginator, 1, 'toObject'> { - return new Paginator( - new Collection([data, rest], this, 1, 'toObject', new RuntimeException()), - metaData - ) + return new Paginator(new Collection([data, rest], this, 1, 'toObject'), metaData) } /** diff --git a/src/resource/collection.ts b/src/resource/collection.ts index b99eb08..721a707 100644 --- a/src/resource/collection.ts +++ b/src/resource/collection.ts @@ -46,7 +46,7 @@ export class Collection< /** * Private debugging error instance for troubleshooting */ - #debuggingError: RuntimeException + #debuggingError?: RuntimeException /** * Type identifier for the collection resource @@ -68,7 +68,7 @@ export class Collection< public transformer: { new (...args: any[]): Transformer }, public maxDepth: MaxDepth, public variant: Variant, - debuggingError: RuntimeException + debuggingError?: RuntimeException ) { this.#debuggingError = debuggingError } diff --git a/src/resource/item.ts b/src/resource/item.ts index 87e6e1d..1dedaf5 100644 --- a/src/resource/item.ts +++ b/src/resource/item.ts @@ -8,7 +8,7 @@ */ import type { ContainerResolver } from '@adonisjs/fold' -import type { RuntimeException } from '@poppinss/exception' +import { RuntimeException } from '@poppinss/exception' import { transformAndResolve } from '../helpers.ts' import type { ExtractTransformerVariants, Next, UnpackAsTopLevelItem } from '../types.ts' @@ -40,7 +40,7 @@ export class Item< /** * Private debugging error instance for troubleshooting */ - #debuggingError: RuntimeException + #debuggingError?: RuntimeException /** * Type identifier for the item resource @@ -55,14 +55,15 @@ export class Item< * @param transformer - Constructor for the transformer class * @param maxDepth - Maximum depth for nested transformations * @param variant - Variant method name to use for transformation - * @param debuggingError - Runtime exception for debugging purposes + * @param debuggingError - Error thrown when the resource is undefined. Create it at the + * transformer call site to get a useful stack trace */ constructor( protected transformerData: [resource: any, ...rest: any[]], public transformer: { new (...args: any[]): Transformer }, public maxDepth: MaxDepth, public variant: Variant, - debuggingError: RuntimeException + debuggingError?: RuntimeException ) { this.#debuggingError = debuggingError } @@ -153,6 +154,7 @@ export class Item< * If its undefined after unpacking, then throw an error */ if (resource === undefined) { + this.#debuggingError ??= new RuntimeException() this.#debuggingError.message = 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' throw this.#debuggingError From a37cf4f34c934c1ad8fd67f2ffa9e58764261320 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 15 Oct 2026 14:21:01 +0530 Subject: [PATCH 2/4] test: assert undefined value errors point to the transformer call site Cover nested items, items inside collections and paginators, nested collections, variants, and items created without a debugging error. --- tests/debugging_error.spec.ts | 222 ++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 tests/debugging_error.spec.ts diff --git a/tests/debugging_error.spec.ts b/tests/debugging_error.spec.ts new file mode 100644 index 0000000..3cac90b --- /dev/null +++ b/tests/debugging_error.spec.ts @@ -0,0 +1,222 @@ +/* + * @adonisjs/http-transformers + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { RuntimeException } from '@poppinss/exception' +import { Item } from '../src/resource/item.ts' +import { BaseTransformer } from '../src/base_transformer.ts' +import { apiSerializer, container } from './helpers.ts' + +const UNDEFINED_VALUE_MESSAGE = + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + +/** + * Runs the callback and returns the error it throws. Serializing a top-level + * item throws synchronously, hence the callback instead of a promise + */ +async function captureError(callback: () => unknown): Promise { + try { + await callback() + } catch (error) { + return error as Error + } + throw new Error('Expected callback to throw') +} + +/** + * Returns the function names of the top stack frames of an error + */ +function topFrames(error: Error, count: number) { + return error + .stack!.split('\n') + .map((line) => line.trim()) + .filter((line) => line.startsWith('at ')) + .slice(0, count) + .map((line) => line.replace(/^at (async )?/, '').split(' ')[0]) +} + +class Profile { + declare bio: string +} + +class User { + declare id: number + declare profile: Profile +} + +class Comment { + declare id: number + declare author: User +} + +class Post { + declare id: number + declare comments: Comment[] +} + +class ProfileTransformer extends BaseTransformer { + toObject() { + return { bio: this.resource.bio } + } + + summary() { + return { bio: this.resource.bio.slice(0, 10) } + } +} + +class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + profile: ProfileTransformer.transform(this.resource.profile), + } + } + + withProfileSummary() { + return { + id: this.resource.id, + profile: ProfileTransformer.transform(this.resource.profile).useVariant('summary').depth(2), + } + } +} + +class CommentTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + author: UserTransformer.transform(this.resource.author), + } + } +} + +class PostTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + comments: CommentTransformer.transform(this.resource.comments).depth(2), + } + } +} + +function createUser(id: number, withProfile: boolean) { + const user = new User() + user.id = id + if (withProfile) { + user.profile = new Profile() + user.profile.bio = 'Hello world from AdonisJS' + } + return user +} + +test.group('Transformer | undefined value stack trace', () => { + test('point to the caller of a top-level transform call', async ({ assert }) => { + const error = await captureError(() => + apiSerializer.serialize( + UserTransformer.transform(undefined as any), + container.createResolver() + ) + ) + + assert.instanceOf(error, RuntimeException) + assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) + assert.equal(topFrames(error, 1)[0], 'UserTransformer.transform') + }) + + test('point to the transformer of a nested item', async ({ assert }) => { + const error = await captureError(() => + apiSerializer.serialize( + UserTransformer.transform(createUser(1, false)), + container.createResolver() + ) + ) + + assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) + assert.deepEqual(topFrames(error, 2), [ + 'ProfileTransformer.transform', + 'UserTransformer.toObject', + ]) + }) + + test('point to the transformer of a nested item inside a collection', async ({ assert }) => { + const error = await captureError(() => + apiSerializer.serialize( + UserTransformer.transform([createUser(1, true), createUser(2, false)]), + container.createResolver() + ) + ) + + assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) + assert.deepEqual(topFrames(error, 2), [ + 'ProfileTransformer.transform', + 'UserTransformer.toObject', + ]) + }) + + test('point to the transformer of a nested item inside a paginator', async ({ assert }) => { + const error = await captureError(() => + apiSerializer.serialize( + UserTransformer.paginate([createUser(1, true), createUser(2, false)], { total: 2 }), + container.createResolver() + ) + ) + + assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) + assert.deepEqual(topFrames(error, 2), [ + 'ProfileTransformer.transform', + 'UserTransformer.toObject', + ]) + }) + + test('point to the transformer of an item inside a nested collection', async ({ assert }) => { + const comment = new Comment() + comment.id = 1 + + const post = new Post() + post.id = 1 + post.comments = [comment] + + const error = await captureError(() => + apiSerializer.serialize(PostTransformer.transform(post), container.createResolver()) + ) + + assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) + assert.deepEqual(topFrames(error, 2), [ + 'UserTransformer.transform', + 'CommentTransformer.toObject', + ]) + }) + + test('point to the variant that transforms a nested item', async ({ assert }) => { + const error = await captureError(() => + apiSerializer.serialize( + UserTransformer.transform(createUser(1, false)).useVariant('withProfileSummary'), + container.createResolver() + ) + ) + + assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) + assert.deepEqual(topFrames(error, 2), [ + 'ProfileTransformer.transform', + 'UserTransformer.withProfileSummary', + ]) + }) + + test('throw runtime exception for an item created without a debugging error', async ({ + assert, + }) => { + const item = new Item([undefined, []], UserTransformer, 1, 'toObject') + const error = await captureError(() => + apiSerializer.serialize(item, container.createResolver()) + ) + + assert.instanceOf(error, RuntimeException) + assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) + assert.equal(topFrames(error, 1)[0], 'Item.resolve') + }) +}) From 35693801f4110c9393cb2431591581465daaf353 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 15 Oct 2026 14:26:51 +0530 Subject: [PATCH 3/4] test: make undefined value stack trace tests self-contained Define classes and data inside each test and assert on the stack with an inline try/catch, matching the other spec files. --- tests/debugging_error.spec.ts | 467 ++++++++++++++++++++++------------ 1 file changed, 306 insertions(+), 161 deletions(-) diff --git a/tests/debugging_error.spec.ts b/tests/debugging_error.spec.ts index 3cac90b..dffa8ee 100644 --- a/tests/debugging_error.spec.ts +++ b/tests/debugging_error.spec.ts @@ -13,167 +13,240 @@ import { Item } from '../src/resource/item.ts' import { BaseTransformer } from '../src/base_transformer.ts' import { apiSerializer, container } from './helpers.ts' -const UNDEFINED_VALUE_MESSAGE = - 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' - -/** - * Runs the callback and returns the error it throws. Serializing a top-level - * item throws synchronously, hence the callback instead of a promise - */ -async function captureError(callback: () => unknown): Promise { - try { - await callback() - } catch (error) { - return error as Error - } - throw new Error('Expected callback to throw') -} - -/** - * Returns the function names of the top stack frames of an error - */ -function topFrames(error: Error, count: number) { - return error - .stack!.split('\n') - .map((line) => line.trim()) - .filter((line) => line.startsWith('at ')) - .slice(0, count) - .map((line) => line.replace(/^at (async )?/, '').split(' ')[0]) -} - -class Profile { - declare bio: string -} - -class User { - declare id: number - declare profile: Profile -} - -class Comment { - declare id: number - declare author: User -} - -class Post { - declare id: number - declare comments: Comment[] -} - -class ProfileTransformer extends BaseTransformer { - toObject() { - return { bio: this.resource.bio } - } - - summary() { - return { bio: this.resource.bio.slice(0, 10) } - } -} - -class UserTransformer extends BaseTransformer { - toObject() { - return { - id: this.resource.id, - profile: ProfileTransformer.transform(this.resource.profile), - } - } - - withProfileSummary() { - return { - id: this.resource.id, - profile: ProfileTransformer.transform(this.resource.profile).useVariant('summary').depth(2), - } - } -} - -class CommentTransformer extends BaseTransformer { - toObject() { - return { - id: this.resource.id, - author: UserTransformer.transform(this.resource.author), - } - } -} - -class PostTransformer extends BaseTransformer { - toObject() { - return { - id: this.resource.id, - comments: CommentTransformer.transform(this.resource.comments).depth(2), - } - } -} - -function createUser(id: number, withProfile: boolean) { - const user = new User() - user.id = id - if (withProfile) { - user.profile = new Profile() - user.profile.bio = 'Hello world from AdonisJS' - } - return user -} - test.group('Transformer | undefined value stack trace', () => { test('point to the caller of a top-level transform call', async ({ assert }) => { - const error = await captureError(() => - apiSerializer.serialize( + assert.plan(3) + + class User { + declare id: number + } + + class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + } + } + } + + try { + await apiSerializer.serialize( UserTransformer.transform(undefined as any), container.createResolver() ) - ) + } catch (error: any) { + const frames = error.stack.split('\n') - assert.instanceOf(error, RuntimeException) - assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) - assert.equal(topFrames(error, 1)[0], 'UserTransformer.transform') + assert.instanceOf(error, RuntimeException) + assert.equal( + error.message, + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + ) + assert.match(frames[1], /^\s+at UserTransformer\.transform /) + } }) test('point to the transformer of a nested item', async ({ assert }) => { - const error = await captureError(() => - apiSerializer.serialize( - UserTransformer.transform(createUser(1, false)), - container.createResolver() - ) - ) + assert.plan(3) + + class Profile { + declare bio: string + } + + class User { + declare id: number + declare profile: Profile + } + + class ProfileTransformer extends BaseTransformer { + toObject() { + return { + bio: this.resource.bio, + } + } + } + + class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + profile: ProfileTransformer.transform(this.resource.profile), + } + } + } + + const user = new User() + user.id = 1 + + try { + await apiSerializer.serialize(UserTransformer.transform(user), container.createResolver()) + } catch (error: any) { + const frames = error.stack.split('\n') - assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) - assert.deepEqual(topFrames(error, 2), [ - 'ProfileTransformer.transform', - 'UserTransformer.toObject', - ]) + assert.equal( + error.message, + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + ) + assert.match(frames[1], /^\s+at ProfileTransformer\.transform /) + assert.match(frames[2], /^\s+at UserTransformer\.toObject /) + } }) test('point to the transformer of a nested item inside a collection', async ({ assert }) => { - const error = await captureError(() => - apiSerializer.serialize( - UserTransformer.transform([createUser(1, true), createUser(2, false)]), + assert.plan(3) + + class Profile { + declare bio: string + } + + class User { + declare id: number + declare profile: Profile + } + + class ProfileTransformer extends BaseTransformer { + toObject() { + return { + bio: this.resource.bio, + } + } + } + + class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + profile: ProfileTransformer.transform(this.resource.profile), + } + } + } + + const profile = new Profile() + profile.bio = 'Hello world' + + const user = new User() + user.id = 1 + user.profile = profile + + const userWithoutProfile = new User() + userWithoutProfile.id = 2 + + try { + await apiSerializer.serialize( + UserTransformer.transform([user, userWithoutProfile]), container.createResolver() ) - ) + } catch (error: any) { + const frames = error.stack.split('\n') - assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) - assert.deepEqual(topFrames(error, 2), [ - 'ProfileTransformer.transform', - 'UserTransformer.toObject', - ]) + assert.equal( + error.message, + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + ) + assert.match(frames[1], /^\s+at ProfileTransformer\.transform /) + assert.match(frames[2], /^\s+at UserTransformer\.toObject /) + } }) test('point to the transformer of a nested item inside a paginator', async ({ assert }) => { - const error = await captureError(() => - apiSerializer.serialize( - UserTransformer.paginate([createUser(1, true), createUser(2, false)], { total: 2 }), + assert.plan(3) + + class Profile { + declare bio: string + } + + class User { + declare id: number + declare profile: Profile + } + + class ProfileTransformer extends BaseTransformer { + toObject() { + return { + bio: this.resource.bio, + } + } + } + + class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + profile: ProfileTransformer.transform(this.resource.profile), + } + } + } + + const profile = new Profile() + profile.bio = 'Hello world' + + const user = new User() + user.id = 1 + user.profile = profile + + const userWithoutProfile = new User() + userWithoutProfile.id = 2 + + try { + await apiSerializer.serialize( + UserTransformer.paginate([user, userWithoutProfile], { total: 2, currentPage: 1 }), container.createResolver() ) - ) + } catch (error: any) { + const frames = error.stack.split('\n') - assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) - assert.deepEqual(topFrames(error, 2), [ - 'ProfileTransformer.transform', - 'UserTransformer.toObject', - ]) + assert.equal( + error.message, + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + ) + assert.match(frames[1], /^\s+at ProfileTransformer\.transform /) + assert.match(frames[2], /^\s+at UserTransformer\.toObject /) + } }) test('point to the transformer of an item inside a nested collection', async ({ assert }) => { + assert.plan(3) + + class User { + declare id: number + } + + class Comment { + declare id: number + declare author: User + } + + class Post { + declare id: number + declare comments: Comment[] + } + + class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + } + } + } + + class CommentTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + author: UserTransformer.transform(this.resource.author), + } + } + } + + class PostTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + comments: CommentTransformer.transform(this.resource.comments).depth(2), + } + } + } + const comment = new Comment() comment.id = 1 @@ -181,42 +254,114 @@ test.group('Transformer | undefined value stack trace', () => { post.id = 1 post.comments = [comment] - const error = await captureError(() => - apiSerializer.serialize(PostTransformer.transform(post), container.createResolver()) - ) + try { + await apiSerializer.serialize(PostTransformer.transform(post), container.createResolver()) + } catch (error: any) { + const frames = error.stack.split('\n') - assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) - assert.deepEqual(topFrames(error, 2), [ - 'UserTransformer.transform', - 'CommentTransformer.toObject', - ]) + assert.equal( + error.message, + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + ) + assert.match(frames[1], /^\s+at UserTransformer\.transform /) + assert.match(frames[2], /^\s+at CommentTransformer\.toObject /) + } }) test('point to the variant that transforms a nested item', async ({ assert }) => { - const error = await captureError(() => - apiSerializer.serialize( - UserTransformer.transform(createUser(1, false)).useVariant('withProfileSummary'), + assert.plan(3) + + class Profile { + declare bio: string + } + + class User { + declare id: number + declare profile: Profile + } + + class ProfileTransformer extends BaseTransformer { + toObject() { + return { + bio: this.resource.bio, + } + } + + summary() { + return { + bio: this.resource.bio.slice(0, 10), + } + } + } + + class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + } + } + + withProfileSummary() { + return { + id: this.resource.id, + profile: ProfileTransformer.transform(this.resource.profile) + .useVariant('summary') + .depth(2), + } + } + } + + const user = new User() + user.id = 1 + + try { + await apiSerializer.serialize( + UserTransformer.transform(user).useVariant('withProfileSummary'), container.createResolver() ) - ) + } catch (error: any) { + const frames = error.stack.split('\n') - assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) - assert.deepEqual(topFrames(error, 2), [ - 'ProfileTransformer.transform', - 'UserTransformer.withProfileSummary', - ]) + assert.equal( + error.message, + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + ) + assert.match(frames[1], /^\s+at ProfileTransformer\.transform /) + assert.match(frames[2], /^\s+at UserTransformer\.withProfileSummary /) + } }) test('throw runtime exception for an item created without a debugging error', async ({ assert, }) => { - const item = new Item([undefined, []], UserTransformer, 1, 'toObject') - const error = await captureError(() => - apiSerializer.serialize(item, container.createResolver()) - ) - - assert.instanceOf(error, RuntimeException) - assert.equal(error.message, UNDEFINED_VALUE_MESSAGE) - assert.equal(topFrames(error, 1)[0], 'Item.resolve') + assert.plan(3) + + class User { + declare id: number + } + + class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + } + } + } + + try { + await apiSerializer.serialize( + new Item([undefined, []], UserTransformer, 1, 'toObject'), + container.createResolver() + ) + } catch (error: any) { + const frames = error.stack.split('\n') + + assert.instanceOf(error, RuntimeException) + assert.equal( + error.message, + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + ) + assert.match(frames[1], /^\s+at Item\.resolve /) + } }) }) From bdaf9e4f34a6db38000faadf19f82c6ee8155f27 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Thu, 15 Oct 2026 14:28:01 +0530 Subject: [PATCH 4/4] test: assert stack trace for a deeply nested undefined item Cover user -> posts -> comments -> author -> profile, where the author is missing a profile four levels down. --- tests/debugging_error.spec.ts | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tests/debugging_error.spec.ts b/tests/debugging_error.spec.ts index dffa8ee..dcbee5f 100644 --- a/tests/debugging_error.spec.ts +++ b/tests/debugging_error.spec.ts @@ -268,6 +268,99 @@ test.group('Transformer | undefined value stack trace', () => { } }) + test('point to the transformer of a deeply nested item', async ({ assert }) => { + assert.plan(3) + + class Profile { + declare bio: string + } + + class User { + declare id: number + declare profile: Profile + declare posts: Post[] + } + + class Comment { + declare id: number + declare author: User + } + + class Post { + declare id: number + declare comments: Comment[] + } + + class ProfileTransformer extends BaseTransformer { + toObject() { + return { + bio: this.resource.bio, + } + } + } + + class UserTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + profile: ProfileTransformer.transform(this.resource.profile), + posts: PostTransformer.transform(this.resource.posts).depth(4), + } + } + } + + class CommentTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + author: UserTransformer.transform(this.resource.author), + } + } + } + + class PostTransformer extends BaseTransformer { + toObject() { + return { + id: this.resource.id, + comments: CommentTransformer.transform(this.resource.comments), + } + } + } + + const profile = new Profile() + profile.bio = 'Hello world' + + const authorWithoutProfile = new User() + authorWithoutProfile.id = 2 + authorWithoutProfile.posts = [] + + const comment = new Comment() + comment.id = 1 + comment.author = authorWithoutProfile + + const post = new Post() + post.id = 1 + post.comments = [comment] + + const user = new User() + user.id = 1 + user.profile = profile + user.posts = [post] + + try { + await apiSerializer.serialize(UserTransformer.transform(user), container.createResolver()) + } catch (error: any) { + const frames = error.stack.split('\n') + + assert.equal( + error.message, + 'Cannot transform undefined value. Use "this.whenLoaded(value)" to allow undefined values' + ) + assert.match(frames[1], /^\s+at ProfileTransformer\.transform /) + assert.match(frames[2], /^\s+at UserTransformer\.toObject /) + } + }) + test('point to the variant that transforms a nested item', async ({ assert }) => { assert.plan(3)