Skip to content

Commit 8c4daf5

Browse files
toiroakrclaude
andcommitted
test: add e2e tests for toResolverOutput with all field types
Test that toResolverOutput produces types matching TailorDB introspection: - Nested objects (userInfo, metadata) - File fields (avatar) - Forward relations (ownerID, owner) - Backward relations (detail from ProfileDetail, comments from ProfileComment) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent af18cb4 commit 8c4daf5

18 files changed

Lines changed: 301 additions & 4 deletions

example/e2e/resolver.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ describe("controlplane", async () => {
151151
// Verify field descriptions from TailorDBField
152152
const inputType = passThrough?.inputs?.find((i) => i.name === "input");
153153
expect(inputType?.type?.kind).toBe("UserDefined");
154+
expect(inputType?.type?.name).toBe("PassThroughInput");
154155

155156
// Verify response field descriptions
156157
const userInfoResponse = passThrough?.response?.type?.fields?.find(
@@ -385,6 +386,144 @@ describe("dataplane", () => {
385386
const result = await graphQLClient.rawRequest(query);
386387
expect(result.errors).toBeDefined();
387388
});
389+
390+
test("verifies output typeName via GraphQL introspection", async () => {
391+
const introspectionQuery = gql`
392+
query {
393+
__type(name: "Query") {
394+
fields {
395+
name
396+
type {
397+
name
398+
kind
399+
ofType {
400+
name
401+
}
402+
}
403+
}
404+
}
405+
}
406+
`;
407+
const result = await graphQLClient.rawRequest(introspectionQuery);
408+
expect(result.errors).toBeUndefined();
409+
410+
const queryType = result.data as {
411+
__type: {
412+
fields: {
413+
name: string;
414+
type: { name: string | null; kind: string; ofType: { name: string } | null };
415+
}[];
416+
};
417+
};
418+
const passThroughField = queryType.__type.fields.find((f) => f.name === "passThrough");
419+
expect(passThroughField).toBeDefined();
420+
421+
// Verify the output type name
422+
const outputTypeName = passThroughField?.type.name ?? passThroughField?.type.ofType?.name;
423+
expect(outputTypeName).toBe("NestedProfile");
424+
});
425+
426+
test("toResolverOutput produces types matching TailorDB introspection", async () => {
427+
// Helper to get field type name
428+
const getTypeName = (
429+
field: { type: { name: string | null; ofType: { name: string } | null } } | undefined,
430+
) => field?.type?.name ?? field?.type?.ofType?.name;
431+
432+
// Get the passThrough resolver's output type name
433+
const schemaQuery = gql`
434+
query {
435+
__schema {
436+
queryType {
437+
fields {
438+
name
439+
type {
440+
name
441+
ofType {
442+
name
443+
}
444+
}
445+
}
446+
}
447+
}
448+
}
449+
`;
450+
const schemaResult = await graphQLClient.rawRequest(schemaQuery);
451+
const queryFields = (
452+
schemaResult.data as {
453+
__schema: {
454+
queryType: {
455+
fields: {
456+
name: string;
457+
type: { name: string | null; ofType: { name: string } | null };
458+
}[];
459+
};
460+
};
461+
}
462+
).__schema.queryType.fields;
463+
const passThroughField = queryFields.find((f) => f.name === "passThrough");
464+
const passThroughTypeName = getTypeName(passThroughField);
465+
466+
// Get nested field types for passThrough output
467+
const typeQuery = gql`
468+
query GetType($name: String!) {
469+
__type(name: $name) {
470+
fields {
471+
name
472+
type {
473+
name
474+
ofType {
475+
name
476+
}
477+
}
478+
}
479+
}
480+
}
481+
`;
482+
const passThroughTypeResult = await graphQLClient.rawRequest(typeQuery, {
483+
name: passThroughTypeName,
484+
});
485+
const passThroughFields = (
486+
passThroughTypeResult.data as {
487+
__type: {
488+
fields: {
489+
name: string;
490+
type: { name: string | null; ofType: { name: string } | null };
491+
}[];
492+
};
493+
}
494+
).__type.fields;
495+
496+
// Get TailorDB NestedProfile type for comparison
497+
const tailorDbResult = await graphQLClient.rawRequest(typeQuery, { name: "NestedProfile" });
498+
const tailorDbFields = (
499+
tailorDbResult.data as {
500+
__type: {
501+
fields: {
502+
name: string;
503+
type: { name: string | null; ofType: { name: string } | null };
504+
}[];
505+
};
506+
}
507+
).__type.fields;
508+
509+
// Verify field types match (including backward relations from new types)
510+
const fieldsToCheck = [
511+
"userInfo", // nested object
512+
"metadata", // nested object
513+
"avatar", // file field
514+
"ownerID", // n-1 relation (foreign key)
515+
"owner", // n-1 relation (navigation property)
516+
"detail", // 1-1 backward relation (from ProfileDetail)
517+
"comments", // n-1 backward relation (from ProfileComment)
518+
];
519+
for (const fieldName of fieldsToCheck) {
520+
const passThroughFieldType = getTypeName(
521+
passThroughFields.find((f) => f.name === fieldName),
522+
);
523+
const tailorDbFieldType = getTypeName(tailorDbFields.find((f) => f.name === fieldName));
524+
expect(passThroughFieldType).toBe(tailorDbFieldType);
525+
}
526+
});
388527
});
389528

390529
test("env", async () => {

example/generated/files.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export interface TypeWithFiles {
2+
NestedProfile: {
3+
fields: "avatar";
4+
};
25
SalesOrder: {
36
fields: "receipt" | "form";
47
};
@@ -11,6 +14,7 @@ export interface TypeWithFiles {
1114
}
1215

1316
const namespaces: Record<keyof TypeWithFiles, string> = {
17+
NestedProfile: "tailordb",
1418
SalesOrder: "tailordb",
1519
User: "tailordb",
1620
Event: "analyticsdb",

example/generated/tailordb.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ export interface Namespace {
5858
version: number;
5959
};
6060
archived: boolean | null;
61+
ownerID: string | null;
62+
createdAt: Generated<Timestamp>;
63+
updatedAt: Timestamp | null;
64+
}
65+
66+
ProfileComment: {
67+
id: Generated<string>;
68+
content: string;
69+
profileID: string;
70+
createdAt: Generated<Timestamp>;
71+
updatedAt: Timestamp | null;
72+
}
73+
74+
ProfileDetail: {
75+
id: Generated<string>;
76+
bio: string | null;
77+
profileID: string;
6178
createdAt: Generated<Timestamp>;
6279
updatedAt: Timestamp | null;
6380
}

example/resolvers/passThrough.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createResolver, t } from "@tailor-platform/sdk";
1+
import { createResolver, t, toResolverOutput } from "@tailor-platform/sdk";
22
import { nestedProfile } from "../tailordb/nested";
33

44
const inputFields = {
@@ -11,12 +11,12 @@ export default createResolver({
1111
description: "Pass Through - Nested Profile Type(Create)",
1212
input: {
1313
id: t.uuid({ optional: true }),
14-
input: t.object(inputFields),
14+
input: t.object(inputFields).typeName("PassThroughInput"),
1515
},
1616
body: ({ input }) => ({
1717
...input.input,
1818
id: input.id ?? crypto.randomUUID(),
1919
createdAt: new Date(),
2020
}),
21-
output: nestedProfile.fields,
21+
output: toResolverOutput(nestedProfile),
2222
});

example/seed/config.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
entityDependencies:
22
Customer: []
33
Invoice: [SalesOrder]
4-
NestedProfile: []
4+
NestedProfile: [User]
5+
ProfileComment: [NestedProfile]
6+
ProfileDetail: [NestedProfile]
57
PurchaseOrder: [Supplier]
68
SalesOrder: [Customer]
79
SalesOrderCreated: []

example/seed/data/NestedProfile.schema.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ const hook = createTailorDBHook(nestedProfile);
1212

1313
export const schema = defineSchema(
1414
createStandardSchema(schemaType, hook),
15+
{
16+
foreignKeys: [
17+
{"column":"ownerID","references":{"table":"User","column":"id"}},
18+
],
19+
}
1520
);

example/seed/data/ProfileComment.jsonl

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { t } from "@tailor-platform/sdk";
2+
import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/test";
3+
import { defineSchema } from "@toiroakr/lines-db";
4+
import { profileComment } from "../../tailordb/profileReference";
5+
6+
const schemaType = t.object({
7+
...profileComment.pickFields(["id","createdAt"], { optional: true }),
8+
...profileComment.omitFields(["id","createdAt"]),
9+
});
10+
11+
const hook = createTailorDBHook(profileComment);
12+
13+
export const schema = defineSchema(
14+
createStandardSchema(schemaType, hook),
15+
{
16+
foreignKeys: [
17+
{"column":"profileID","references":{"table":"NestedProfile","column":"id"}},
18+
],
19+
}
20+
);

example/seed/data/ProfileDetail.jsonl

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { t } from "@tailor-platform/sdk";
2+
import { createTailorDBHook, createStandardSchema } from "@tailor-platform/sdk/test";
3+
import { defineSchema } from "@toiroakr/lines-db";
4+
import { profileDetail } from "../../tailordb/profileReference";
5+
6+
const schemaType = t.object({
7+
...profileDetail.pickFields(["id","createdAt"], { optional: true }),
8+
...profileDetail.omitFields(["id","createdAt"]),
9+
});
10+
11+
const hook = createTailorDBHook(profileDetail);
12+
13+
export const schema = defineSchema(
14+
createStandardSchema(schemaType, hook),
15+
{
16+
foreignKeys: [
17+
{"column":"profileID","references":{"table":"NestedProfile","column":"id"}},
18+
],
19+
indexes: [
20+
{"name":"profiledetail_profileID_unique_idx","columns":["profileID"],"unique":true},
21+
],
22+
}
23+
);

0 commit comments

Comments
 (0)