@distilled.cloud/core 1.0.0-rc.11 → 1.0.0-rc.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/lib/api.d.ts +4 -4
  2. package/lib/api.d.ts.map +1 -1
  3. package/lib/api.js +1 -1
  4. package/lib/api.js.map +1 -1
  5. package/lib/category.js +1 -1
  6. package/lib/category.js.map +1 -1
  7. package/lib/codegen/boolean-string-enums.d.ts +8 -8
  8. package/lib/codegen/boolean-string-enums.d.ts.map +1 -1
  9. package/lib/codegen/boolean-string-enums.js +84 -27
  10. package/lib/codegen/boolean-string-enums.js.map +1 -1
  11. package/lib/codegen/boolean-string-enums.test.js +130 -0
  12. package/lib/codegen/boolean-string-enums.test.js.map +1 -1
  13. package/lib/codegen/cli.js +2 -2
  14. package/lib/codegen/cli.js.map +1 -1
  15. package/lib/codegen/generator.d.ts +20 -3
  16. package/lib/codegen/generator.d.ts.map +1 -1
  17. package/lib/codegen/generator.js +36 -9
  18. package/lib/codegen/generator.js.map +1 -1
  19. package/lib/codegen/generator.test.d.ts +2 -0
  20. package/lib/codegen/generator.test.d.ts.map +1 -0
  21. package/lib/codegen/generator.test.js +126 -0
  22. package/lib/codegen/generator.test.js.map +1 -0
  23. package/lib/codegen/graphql-client.d.ts +7 -1
  24. package/lib/codegen/graphql-client.d.ts.map +1 -1
  25. package/lib/codegen/graphql-client.js +262 -125
  26. package/lib/codegen/graphql-client.js.map +1 -1
  27. package/lib/codegen/graphql-client.test.js +104 -236
  28. package/lib/codegen/graphql-client.test.js.map +1 -1
  29. package/lib/codegen/openapi-binary.test.d.ts +2 -0
  30. package/lib/codegen/openapi-binary.test.d.ts.map +1 -0
  31. package/lib/codegen/openapi-binary.test.js +67 -0
  32. package/lib/codegen/openapi-binary.test.js.map +1 -0
  33. package/lib/codegen/openapi.d.ts +7 -1
  34. package/lib/codegen/openapi.d.ts.map +1 -1
  35. package/lib/codegen/openapi.js +15 -4
  36. package/lib/codegen/openapi.js.map +1 -1
  37. package/lib/codegen/patches.d.ts +16 -1
  38. package/lib/codegen/patches.d.ts.map +1 -1
  39. package/lib/codegen/patches.js +70 -4
  40. package/lib/codegen/patches.js.map +1 -1
  41. package/lib/codegen/patches.test.js +48 -2
  42. package/lib/codegen/patches.test.js.map +1 -1
  43. package/lib/codegen/untagged-unions.test.d.ts +2 -0
  44. package/lib/codegen/untagged-unions.test.d.ts.map +1 -0
  45. package/lib/codegen/untagged-unions.test.js +80 -0
  46. package/lib/codegen/untagged-unions.test.js.map +1 -0
  47. package/lib/errors.d.ts +2 -1
  48. package/lib/errors.d.ts.map +1 -1
  49. package/lib/errors.js +5 -2
  50. package/lib/errors.js.map +1 -1
  51. package/lib/fixtures/xml/upstream-validator.json +397 -0
  52. package/lib/graphql.d.ts +296 -205
  53. package/lib/graphql.d.ts.map +1 -1
  54. package/lib/graphql.js +968 -631
  55. package/lib/graphql.js.map +1 -1
  56. package/lib/graphql.test.js +134 -763
  57. package/lib/graphql.test.js.map +1 -1
  58. package/lib/graphql.types.d.ts +10 -0
  59. package/lib/graphql.types.d.ts.map +1 -1
  60. package/lib/graphql.types.js +4 -44
  61. package/lib/graphql.types.js.map +1 -1
  62. package/lib/index.d.ts +27 -0
  63. package/lib/index.d.ts.map +1 -0
  64. package/lib/index.js +27 -0
  65. package/lib/index.js.map +1 -0
  66. package/lib/pagination.d.ts +2 -1
  67. package/lib/pagination.d.ts.map +1 -1
  68. package/lib/pagination.js +24 -16
  69. package/lib/pagination.js.map +1 -1
  70. package/lib/pagination.test.d.ts +2 -0
  71. package/lib/pagination.test.d.ts.map +1 -0
  72. package/lib/pagination.test.js +134 -0
  73. package/lib/pagination.test.js.map +1 -0
  74. package/lib/protocol-http.d.ts +12 -17
  75. package/lib/protocol-http.d.ts.map +1 -1
  76. package/lib/protocol-http.js +100 -45
  77. package/lib/protocol-http.js.map +1 -1
  78. package/lib/protocol-http.test.js +130 -0
  79. package/lib/protocol-http.test.js.map +1 -1
  80. package/lib/protocol-rest.d.ts +18 -1
  81. package/lib/protocol-rest.d.ts.map +1 -1
  82. package/lib/protocol-rest.js +24 -4
  83. package/lib/protocol-rest.js.map +1 -1
  84. package/lib/query.d.ts +29 -0
  85. package/lib/query.d.ts.map +1 -0
  86. package/lib/query.js +27 -0
  87. package/lib/query.js.map +1 -0
  88. package/lib/response-validation.d.ts +67 -0
  89. package/lib/response-validation.d.ts.map +1 -0
  90. package/lib/response-validation.js +76 -0
  91. package/lib/response-validation.js.map +1 -0
  92. package/lib/response-validation.test.d.ts +2 -0
  93. package/lib/response-validation.test.d.ts.map +1 -0
  94. package/lib/response-validation.test.js +67 -0
  95. package/lib/response-validation.test.js.map +1 -0
  96. package/lib/testing.d.ts +39 -0
  97. package/lib/testing.d.ts.map +1 -0
  98. package/lib/testing.js +41 -0
  99. package/lib/testing.js.map +1 -0
  100. package/lib/trait.d.ts +29 -9
  101. package/lib/trait.d.ts.map +1 -1
  102. package/lib/trait.js +8 -0
  103. package/lib/trait.js.map +1 -1
  104. package/lib/xml.d.ts +44 -0
  105. package/lib/xml.d.ts.map +1 -0
  106. package/lib/xml.js +315 -0
  107. package/lib/xml.js.map +1 -0
  108. package/lib/xml.test.d.ts +2 -0
  109. package/lib/xml.test.d.ts.map +1 -0
  110. package/lib/xml.test.js +211 -0
  111. package/lib/xml.test.js.map +1 -0
  112. package/package.json +4 -4
  113. package/src/api.ts +4 -4
  114. package/src/category.ts +1 -1
  115. package/src/codegen/boolean-string-enums.test.ts +147 -0
  116. package/src/codegen/boolean-string-enums.ts +84 -27
  117. package/src/codegen/cli.ts +2 -2
  118. package/src/codegen/generator.test.ts +151 -0
  119. package/src/codegen/generator.ts +70 -12
  120. package/src/codegen/graphql-client.test.ts +116 -283
  121. package/src/codegen/graphql-client.ts +326 -169
  122. package/src/codegen/openapi-binary.test.ts +98 -0
  123. package/src/codegen/openapi.ts +26 -4
  124. package/src/codegen/patches.test.ts +66 -1
  125. package/src/codegen/patches.ts +84 -4
  126. package/src/codegen/untagged-unions.test.ts +89 -0
  127. package/src/errors.ts +5 -2
  128. package/src/fixtures/xml/upstream-validator.json +397 -0
  129. package/src/graphql.test.ts +174 -948
  130. package/src/graphql.ts +1592 -1167
  131. package/src/graphql.types.ts +92 -163
  132. package/src/index.ts +26 -0
  133. package/src/pagination.test.ts +191 -0
  134. package/src/pagination.ts +35 -25
  135. package/src/protocol-http.test.ts +163 -0
  136. package/src/protocol-http.ts +122 -46
  137. package/src/protocol-rest.ts +62 -13
  138. package/src/query.ts +39 -0
  139. package/src/response-validation.test.ts +95 -0
  140. package/src/response-validation.ts +116 -0
  141. package/src/testing.ts +78 -0
  142. package/src/trait.ts +39 -8
  143. package/src/xml.test.ts +269 -0
  144. package/src/xml.ts +339 -0
  145. package/lib/graphql.fixture.d.ts +0 -249
  146. package/lib/graphql.fixture.d.ts.map +0 -1
  147. package/lib/graphql.fixture.js +0 -240
  148. package/lib/graphql.fixture.js.map +0 -1
  149. package/src/graphql.fixture.ts +0 -371
@@ -1,7 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { buildSchema, introspectionFromSchema, parse, validate } from "graphql";
3
- import { compile } from "../graphql.ts";
4
- import { fixtureSDL } from "../graphql.fixture.ts";
2
+ import { buildSchema, introspectionFromSchema } from "graphql";
5
3
  import { applyOperation } from "../json-patch.ts";
6
4
  import {
7
5
  convertGraphQLClient,
@@ -12,35 +10,18 @@ import {
12
10
 
13
11
  const named = (name: string, kind = "OBJECT") => ({ kind, name });
14
12
  const required = (ofType: object) => ({ kind: "NON_NULL", ofType });
15
- const list = (ofType: object) => ({ kind: "LIST", ofType });
16
13
  const field = (name: string, type: object, args: object[] = []) => ({
17
14
  name,
18
15
  type,
19
16
  args,
20
17
  });
18
+
21
19
  const fixture = {
22
20
  queryType: { name: "Query" },
23
21
  mutationType: { name: "Mutation" },
24
22
  types: [
25
23
  { kind: "SCALAR", name: "String" },
26
24
  { kind: "SCALAR", name: "Int" },
27
- { kind: "SCALAR", name: "JSON" },
28
- {
29
- kind: "ENUM",
30
- name: "Status",
31
- enumValues: [{ name: "ACTIVE" }, { name: "OLD", isDeprecated: true }],
32
- },
33
- {
34
- kind: "INPUT_OBJECT",
35
- name: "Filter",
36
- inputFields: [
37
- {
38
- name: "status",
39
- type: named("Status", "ENUM"),
40
- defaultValue: "ACTIVE",
41
- },
42
- ],
43
- },
44
25
  {
45
26
  kind: "OBJECT",
46
27
  name: "Query",
@@ -48,7 +29,13 @@ const fixture = {
48
29
  field("project", required(named("Project")), [
49
30
  { name: "id", type: required(named("String", "SCALAR")) },
50
31
  ]),
51
- field("search", list(named("Search", "UNION"))),
32
+ field("me", required(named("User"))),
33
+ field("projects", required(named("ProjectConnection")), [
34
+ {
35
+ name: "first",
36
+ type: named("Int", "SCALAR"),
37
+ },
38
+ ]),
52
39
  ],
53
40
  },
54
41
  {
@@ -56,47 +43,57 @@ const fixture = {
56
43
  name: "Mutation",
57
44
  fields: [
58
45
  field("projectCreate", named("Project"), [
59
- { name: "filter", type: named("Filter", "INPUT_OBJECT") },
46
+ { name: "name", type: required(named("String", "SCALAR")) },
60
47
  ]),
61
48
  ],
62
49
  },
63
50
  {
64
51
  kind: "OBJECT",
65
52
  name: "Project",
66
- interfaces: [named("Node", "INTERFACE")],
67
53
  fields: [
68
54
  field("id", required(named("String", "SCALAR"))),
69
- field("children", required(list(required(named("Project")))), [
70
- {
71
- name: "first",
72
- type: required(named("Int", "SCALAR")),
73
- defaultValue: "20",
74
- },
75
- { name: "filter", type: named("Filter", "INPUT_OBJECT") },
55
+ field("name", required(named("String", "SCALAR"))),
56
+ ],
57
+ },
58
+ {
59
+ kind: "OBJECT",
60
+ name: "User",
61
+ fields: [
62
+ field("email", required(named("String", "SCALAR"))),
63
+ field("name", named("String", "SCALAR")),
64
+ field("projects", required(named("ProjectConnection")), [
65
+ { name: "first", type: named("Int", "SCALAR") },
76
66
  ]),
77
- {
78
- ...field("legacy", named("String", "SCALAR")),
79
- isDeprecated: true,
80
- deprecationReason: "Use id",
81
- },
82
67
  ],
83
68
  },
84
69
  {
85
70
  kind: "OBJECT",
86
- name: "Service",
87
- fields: [field("id", required(named("String", "SCALAR")))],
71
+ name: "ProjectConnection",
72
+ fields: [
73
+ field(
74
+ "edges",
75
+ required({ kind: "LIST", ofType: named("ProjectEdge") }),
76
+ ),
77
+ field("pageInfo", required(named("PageInfo"))),
78
+ ],
88
79
  },
89
80
  {
90
- kind: "INTERFACE",
91
- name: "Node",
92
- fields: [field("id", required(named("String", "SCALAR")))],
93
- possibleTypes: [named("Project")],
81
+ kind: "OBJECT",
82
+ name: "ProjectEdge",
83
+ fields: [
84
+ field("cursor", required(named("String", "SCALAR"))),
85
+ field("node", required(named("Project"))),
86
+ ],
94
87
  },
95
88
  {
96
- kind: "UNION",
97
- name: "Search",
98
- possibleTypes: [named("Project"), named("Service")],
89
+ kind: "OBJECT",
90
+ name: "PageInfo",
91
+ fields: [
92
+ field("endCursor", named("String", "SCALAR")),
93
+ field("hasNextPage", required(named("Boolean", "SCALAR"))),
94
+ ],
99
95
  },
96
+ { kind: "SCALAR", name: "Boolean" },
100
97
  ],
101
98
  };
102
99
 
@@ -104,283 +101,119 @@ const options = {
104
101
  transportImport: "./transport.ts",
105
102
  requirementsImport: "./transport.ts",
106
103
  requirementsType: "Requirements",
107
- operationAliases: { createProject: "projectCreate" },
104
+ sdkName: "Railway",
108
105
  };
109
106
 
110
- describe("GraphQL-native compiler", () => {
111
- test("root helpers allocate safe unique names without hiding schema operations", () => {
112
- const source = buildSchema(`
113
- type Query {
114
- query: String report: String schema: String transport: String G: String
115
- catchTags: String delete: String Scalars: String Selection: String
116
- same: String mutationSame: String
117
- }
118
- type Mutation { same: String }
119
- `);
120
- const model = convertGraphQLClient(introspectionFromSchema(source));
121
- const output = generateGraphQLClient(model, {
122
- ...options,
123
- operationAliases: {
124
- default: "delete",
125
- deleteField: "delete",
126
- transport: "report",
127
- readReport: "report",
128
- },
129
- });
130
- expect(output).toContain(
131
- 'export const queryQuery = client.operation("query", "query");',
132
- );
133
- expect(output).toContain(
134
- 'export const queryReport = client.operation("query", "report");',
135
- );
136
- expect(output).toContain(
137
- 'export const queryTransport = client.operation("query", "transport");',
138
- );
139
- expect(output).toContain(
140
- 'export const queryDelete = client.operation("query", "delete");',
141
- );
142
- expect(output).toContain(
143
- 'export const queryScalars = client.operation("query", "Scalars");',
144
- );
145
- expect(output).toContain(
146
- 'export const mutationSame2 = client.operation("mutation", "same");',
147
- );
148
- expect(output).toContain("export const deleteField = queryDelete;");
149
- expect(output).toContain("export const readReport = queryReport;");
150
- expect(output).not.toContain("export const default =");
151
- expect(
152
- [...output.matchAll(/export const ([A-Za-z_$][\w$]*) =/g)].map(
153
- (match) => match[1],
154
- ),
155
- ).toEqual(expect.arrayContaining(["query", "report", "queryCatchTags"]));
156
- const declarations = [
157
- ...output.matchAll(/export (?:const|class|type) ([A-Za-z_$][\w$]*)/g),
158
- ].map((match) => match[1]);
159
- expect(new Set(declarations).size).toBe(declarations.length);
160
- expect(output.match(/client\.operation\(/g)).toHaveLength(12);
161
- expect(() =>
162
- new Bun.Transpiler({ loader: "ts" }).transformSync(output),
163
- ).not.toThrow();
164
- expect(
165
- generateGraphQLClient(model, {
166
- ...options,
167
- operationAliases: {
168
- default: "delete",
169
- deleteField: "delete",
170
- transport: "report",
171
- readReport: "report",
172
- },
173
- }),
174
- ).toBe(output);
175
- });
176
-
177
- test("schema type names and patched error tags cannot shadow generated support symbols", () => {
178
- const model = convertGraphQLClient(
179
- introspectionFromSchema(
180
- buildSchema(`
181
- type Query { data: Schema }
182
- type Schema { selection: Selection }
183
- type Selection { id: ID! }
184
- `),
185
- ),
186
- );
187
- for (const tag of ["Schema", "catchTags", "class"]) {
188
- applyOperation(model, {
189
- op: "add",
190
- path: `/errors/${tag}`,
191
- value: { matchers: [{ code: tag }] },
192
- });
193
- applyOperation(model, {
194
- op: "add",
195
- path: "/types/Query/fields/data/errors/-",
196
- value: tag,
197
- });
198
- }
107
+ describe("GraphQL Query SDK generator", () => {
108
+ test("emits TypeMeta, interfaces, and named roots on the SDK object", () => {
109
+ const model = convertGraphQLClient(fixture);
199
110
  const output = generateGraphQLClient(model, options);
111
+ expect(output).toContain("export interface Project");
112
+ expect(output).toContain("export interface User");
200
113
  expect(output).toContain(
201
- 'export class SchemaError extends S.TaggedError<SchemaError>()("Schema", G.errorFields)',
114
+ 'export const Project: TypeMeta = { name: "Project", fields: {} };',
202
115
  );
116
+ expect(output).toContain("export const Railway = {");
203
117
  expect(output).toContain(
204
- 'export class catchTagsError extends S.TaggedError<catchTagsError>()("catchTags", G.errorFields)',
118
+ 'root("query", "me", User, undefined, undefined, globalErrors)',
205
119
  );
120
+ expect(output).toContain('root("query", "project", Project');
121
+ expect(output).toContain('rootConnection("query", "projects", Project');
206
122
  expect(output).toContain(
207
- 'export class classError extends S.TaggedError<classError>()("class", G.errorFields)',
123
+ "Query<ReadonlyArray<Project>, RailwayGlobalError>",
208
124
  );
209
- expect(output).toContain('"Schema": SchemaError;');
210
- expect(output).toContain('"Schema": {');
211
- expect(output).toContain('"Selection": {');
125
+ expect(output).toContain(
126
+ 'connectionField("projects", Project, { first: "Int" })',
127
+ );
128
+ expect(output).toContain('root("mutation", "projectCreate", Project');
129
+ // Nullable GraphQL types include null; non-null ones do not.
130
+ expect(output).toContain("readonly name: string | null;");
131
+ expect(output).toContain("readonly email: string;");
132
+ expect(output).toContain("Query<Project | null, RailwayGlobalError>");
133
+ expect(output).toContain("args?: { readonly first?: number | null }");
134
+ expect(output).toContain('from "@distilled.cloud/core/graphql"');
212
135
  expect(() =>
213
136
  new Bun.Transpiler({ loader: "ts" }).transformSync(output),
214
137
  ).not.toThrow();
215
138
  });
216
139
 
217
- test("real introspection round-trips abstract fragments, recursive selections and defaulted required arguments", () => {
218
- const schema = buildSchema(
219
- fixtureSDL.replaceAll("first: Int = 20", "first: Int! = 20"),
220
- );
221
- const model = convertGraphQLClient(introspectionFromSchema(schema));
222
- const compiled = compile(model, "query", {
223
- node: {
224
- where: { id: "node-id" },
225
- select: {
226
- id: true,
227
- __typename: true,
228
- __on: {
229
- Project: {
230
- services: { select: { edges: { node: { id: true } } } },
231
- },
232
- Service: { status: true, project: { parent: { name: true } } },
233
- },
140
+ test("emits error classes and scopes them to roots", () => {
141
+ const model = convertGraphQLClient(fixture);
142
+ for (const operation of [
143
+ {
144
+ op: "add" as const,
145
+ path: "/errors/RailwayNotFound",
146
+ value: {
147
+ description: "Missing.",
148
+ category: "notFound",
149
+ matchers: [{ code: "NOT_FOUND" }],
234
150
  },
235
151
  },
236
- search: {
237
- where: { text: "query" },
238
- select: {
239
- __typename: true,
240
- __on: { Project: { name: true }, Service: { status: true } },
152
+ {
153
+ op: "add" as const,
154
+ path: "/errors/RailwayRateLimited",
155
+ value: {
156
+ category: "throttling",
157
+ retryable: true,
158
+ matchers: [{ code: "RATE_LIMITED" }],
241
159
  },
242
160
  },
243
- });
244
- expect(validate(schema, parse(compiled.query))).toEqual([]);
245
- expect(compiled.query).not.toContain("first:");
246
- expect(model.types.Project!.fields!.services!.args.first).toEqual({
247
- type: "Int!",
248
- defaultValue: "20",
249
- });
250
- expect(model.types.ProjectFilter!.inputFields!.statuses).toEqual({
251
- type: "[Status!]",
252
- defaultValue: "[ACTIVE]",
253
- });
254
- const output = generateGraphQLClient(model, options);
255
- expect(output).toContain('"first"?: Scalars["Int"]');
256
- expect(output).toContain('"Node": "Project" | "Service"');
257
- expect(output).toContain('"SearchResult": "Project" | "Service"');
258
- });
259
-
260
- test("required input fields with defaults are omittable but other required fields stay required", () => {
261
- const schema = buildSchema(`
262
- input Paging { limit: Int! = 20, owner: ID!, statuses: [String!]! = [] }
263
- type Query { names(paging: Paging!): [String!]! }
264
- `);
265
- const model = convertGraphQLClient(introspectionFromSchema(schema));
266
- const output = generateGraphQLClient(model, options);
267
- expect(output).toContain('"limit"?: Scalars["Int"]');
268
- expect(output).toContain('"owner": Scalars["ID"]');
269
- expect(output).toContain('"statuses"?: ReadonlyArray<Scalars["String"]>');
270
- const compiled = compile(model, "query", {
271
- names: { where: { paging: { owner: "account" } } },
272
- });
273
- expect(Object.values(compiled.variables)).toEqual([{ owner: "account" }]);
274
- expect(validate(schema, parse(compiled.query))).toEqual([]);
275
- expect(() =>
276
- compile(model, "query", { names: { where: { paging: {} } } }),
277
- ).toThrow("owner is required");
278
- });
279
-
280
- test("provider scalar overrides stay aligned in runtime metadata and generated scalar types", () => {
281
- const schema = buildSchema(
282
- `scalar DateTime scalar JSON type Query { date: DateTime! blob: JSON! }`,
283
- );
284
- const model = convertGraphQLClient(introspectionFromSchema(schema), {
285
- scalars: { DateTime: "string" },
286
- });
287
- const output = generateGraphQLClient(model, options);
288
- expect(model.types.DateTime!.scalar).toBe("string");
289
- expect(model.types.JSON!.scalar).toBe("unknown");
290
- expect(output).toContain('"DateTime": string;');
291
- expect(output).toContain('"JSON": unknown;');
292
- expect(
293
- validate(
294
- schema,
295
- parse(compile(model, "query", { date: true, blob: true }).query),
296
- ),
297
- ).toEqual([]);
298
- });
299
-
300
- test("preserves nested required arguments, defaults, recursion, union/interface members and deprecated fields", () => {
301
- const model = convertGraphQLClient({ data: { __schema: fixture } });
302
- expect(model.types.Project!.fields!.children).toEqual({
303
- type: "[Project!]!",
304
- args: {
305
- first: { type: "Int!", defaultValue: "20" },
306
- filter: { type: "Filter" },
161
+ {
162
+ op: "add" as const,
163
+ path: "/globalErrors/-",
164
+ value: "RailwayRateLimited",
307
165
  },
308
- errors: [],
309
- });
310
- expect(model.types.Project!.interfaces).toEqual(["Node"]);
311
- expect(model.types.Node!.possibleTypes).toEqual(["Project"]);
312
- expect(model.types.Search!.possibleTypes).toEqual(["Project", "Service"]);
313
- expect(model.types.Project!.fields!.legacy!.deprecated).toBe("Use id");
314
- expect(model.types.Status!.enumValues).toEqual(["ACTIVE", "OLD"]);
315
- expect(model.types.Filter!.inputFields!.status!.defaultValue).toBe(
316
- "ACTIVE",
317
- );
318
- expect(model.types.JSON!.scalar).toBe("unknown");
319
- validateGraphQLModel(model);
320
- });
321
-
322
- test("field errors patch the one model used by emitted type unions and runtime classification", () => {
323
- const model = convertGraphQLClient(fixture);
324
- applyOperation(model, {
325
- op: "add",
326
- path: "/errors/ChildrenUnavailable",
327
- value: {
328
- category: "server",
329
- matchers: [{ code: "CHILDREN_UNAVAILABLE" }],
166
+ {
167
+ op: "add" as const,
168
+ path: "/types/Query/fields/project/errors/-",
169
+ value: "RailwayNotFound",
330
170
  },
331
- });
332
- applyOperation(model, {
333
- op: "add",
334
- path: "/types/Project/fields/children/errors/-",
335
- value: "ChildrenUnavailable",
336
- });
171
+ ])
172
+ applyOperation(model, operation);
337
173
  const output = generateGraphQLClient(model, options);
338
174
  expect(output).toContain(
339
- '"children": G.Field<{ "first"?: Scalars["Int"]; "filter"?: Inputs["Filter"] | null }, "[Project!]!", "ChildrenUnavailable">;',
175
+ 'export class RailwayNotFound extends S.TaggedError<RailwayNotFound>()("RailwayNotFound", errorFields).pipe(Category.withNotFoundError) {}',
340
176
  );
341
177
  expect(output).toContain(
342
- 'export class ChildrenUnavailable extends S.TaggedError<ChildrenUnavailable>()("ChildrenUnavailable", G.errorFields).pipe(Category.withServerError)',
178
+ "Category.withThrottlingError, Category.withRetryable({ throttling: true })",
343
179
  );
344
- expect(output).toContain('"errors":["ChildrenUnavailable"]');
345
- expect(output).toContain('"matchers":[{"code":"CHILDREN_UNAVAILABLE"}]');
346
180
  expect(output).toContain(
347
- 'export const projectCreate = client.operation("mutation", "projectCreate");',
181
+ 'errorSpec(RailwayRateLimited, "RailwayRateLimited", [{"code":"RATE_LIMITED"}], { retryable: true, global: true })',
348
182
  );
349
- expect(output).toContain("export const createProject = projectCreate;");
350
- });
351
-
352
- test("generation has no fixed depth projection or artificial output optionality", () => {
353
- const output = generateGraphQLClient(
354
- convertGraphQLClient(fixture),
355
- options,
183
+ expect(output).toContain(
184
+ "export type RailwayGlobalError = RailwayRateLimited;",
185
+ );
186
+ expect(output).toContain(
187
+ "Query<Project, RailwayNotFound | RailwayGlobalError>",
356
188
  );
357
- expect(output).toContain('"Project": {');
358
- expect(output).toContain('"Search": "Project" | "Service";');
359
- expect(output).toContain('"Node": "Project";');
360
- expect(output).toContain('"id": G.Field<{ }, "String!", never>;');
361
- expect(output).not.toContain('"id"?: G.Field');
362
- expect(output).not.toContain("maxDepth");
189
+ expect(output).toContain("[RailwayNotFoundSpec, ...globalErrors]");
190
+ expect(output).toContain("Query<User, RailwayGlobalError>");
191
+ expect(() =>
192
+ new Bun.Transpiler({ loader: "ts" }).transformSync(output),
193
+ ).not.toThrow();
363
194
  });
364
195
 
365
- test("unknown coordinates, incomplete references and stale patches fail immediately", () => {
196
+ test("unknown coordinates fail validateGraphQLModel", () => {
366
197
  const model = convertGraphQLClient(fixture);
367
198
  applyOperation(model, {
368
199
  op: "add",
369
- path: "/types/Project/fields/children/errors/-",
200
+ path: "/types/Project/fields/id/errors/-",
370
201
  value: "MissingError",
371
202
  });
372
203
  expect(() => validateGraphQLModel(model)).toThrow(
373
- "Project.children: unknown GraphQL error MissingError",
204
+ "Project.id: unknown GraphQL error MissingError",
374
205
  );
375
206
  expect(() => graphqlTypeString({ kind: "NON_NULL" })).toThrow(
376
207
  "Incomplete GraphQL NON_NULL",
377
208
  );
378
- expect(() =>
379
- applyOperation(model, {
380
- op: "replace",
381
- path: "/types/Project/fields/removed/errors",
382
- value: [],
383
- }),
384
- ).toThrow();
209
+ });
210
+
211
+ test("SDL round-trip through convert still produces a model", () => {
212
+ const source = buildSchema(`
213
+ type Query { ping: String }
214
+ `);
215
+ const model = convertGraphQLClient(introspectionFromSchema(source));
216
+ expect(model.queryType).toBe("Query");
217
+ expect(model.types.Query?.fields?.ping?.type).toBe("String");
385
218
  });
386
219
  });