@nexus-rpc/gen-core 0.1.0-alpha.3 → 0.1.0-alpha.4

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.
@@ -0,0 +1,5 @@
1
+ import type { DefinitionSchema } from "./definition-schema.js";
2
+ export interface DefinitionSource {
3
+ fileURI: URL;
4
+ schema: DefinitionSchema;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,12 +1,16 @@
1
1
  import { type LanguageName, type RendererOptions, type TargetLanguage } from "quicktype-core";
2
- import type { DefinitionSchema } from "./definition-schema";
2
+ import type { DefinitionSource } from "./definition-source.js";
3
3
  export interface GeneratorOptions<Lang extends LanguageName = LanguageName> {
4
4
  lang: TargetLanguage;
5
- schema: DefinitionSchema;
5
+ definitionSources: DefinitionSource[];
6
6
  rendererOptions: RendererOptions<Lang>;
7
7
  firstFilenameSansExtensions: string;
8
8
  }
9
+ export interface PreparedSchemaSources {
10
+ sources: PreparedSchema[];
11
+ }
9
12
  export interface PreparedSchema {
13
+ sourceURI: string;
10
14
  services: {
11
15
  [key: string]: PreparedService;
12
16
  };
@@ -37,8 +41,16 @@ export interface PreparedTypeReference {
37
41
  kind: "jsonSchema" | "existing";
38
42
  name: string;
39
43
  }
44
+ export interface MergedSources {
45
+ services: {
46
+ [key: string]: PreparedService;
47
+ };
48
+ topLevelJsonSchemaTypes: {
49
+ [key: string]: any;
50
+ };
51
+ }
40
52
  export interface NexusRendererOptions {
41
- nexusSchema: PreparedSchema;
53
+ nexusSchema: MergedSources;
42
54
  firstFilenameSansExtensions: string;
43
55
  }
44
56
  export declare function getNexusRendererOptions(rendererOptions: RendererOptions): NexusRendererOptions;
@@ -48,6 +60,6 @@ export declare class Generator {
48
60
  generate(): Promise<{
49
61
  [fileName: string]: string;
50
62
  }>;
51
- private prepareSchema;
63
+ private prepareSchemas;
52
64
  private prepareInOutType;
53
65
  }
package/dist/generator.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { InputData, JSONSchemaInput, JSONSchemaStore, quicktypeMultiFile, Ref, } from "quicktype-core";
2
2
  import { PathElementKind } from "quicktype-core/dist/input/PathElement.js";
3
- import { isPrimitiveTypeKind } from "quicktype-core/dist/Type/index.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import yaml from "yaml";
4
5
  export function getNexusRendererOptions(rendererOptions) {
5
6
  return rendererOptions.nexusOptions;
6
7
  }
@@ -11,73 +12,102 @@ export class Generator {
11
12
  }
12
13
  async generate() {
13
14
  // Prepare schema
14
- const schema = this.prepareSchema();
15
- // Build quicktype input
16
- const schemaInput = new JSONSchemaInput(new FetchDisabledSchemaStore());
17
- const jsonSchema = {
18
- ...schema.sharedJsonSchema,
19
- ...schema.topLevelJsonSchemaTypes,
15
+ const sources = this.prepareSchemas();
16
+ const mergedSources = {
17
+ services: {},
18
+ topLevelJsonSchemaTypes: {},
20
19
  };
21
- await schemaInput.addSource({
22
- // TODO(cretz): Give proper filename name here for proper cross-file referencing
23
- name: "__ALL_TYPES__",
24
- schema: JSON.stringify(jsonSchema),
25
- });
26
- // Set the top-level types
27
- for (const topLevel of Object.keys(schema.topLevelJsonSchemaTypes)) {
28
- schemaInput.addTopLevel(topLevel, Ref.parse(`__ALL_TYPES__#/${topLevel}`));
29
- }
30
- for (const [name, reference] of Object.entries(schema.topLevelJsonSchemaLocalRefs)) {
31
- schemaInput.addTopLevel(name, Ref.parse(`__ALL_TYPES__${reference}`));
20
+ // Build quicktype input
21
+ const schemaInput = new JSONSchemaInput(new LocalFetchingSchemaStore());
22
+ for (const schema of sources.sources) {
23
+ const jsonSchema = {
24
+ ...schema.sharedJsonSchema,
25
+ ...schema.topLevelJsonSchemaTypes,
26
+ };
27
+ await schemaInput.addSource({
28
+ // Append __ALL_TYPES__ to make it easier to filter out top level
29
+ // while rendering
30
+ name: schema.sourceURI.toString() + "/__ALL_TYPES__",
31
+ uris: [schema.sourceURI],
32
+ schema: JSON.stringify(jsonSchema),
33
+ });
34
+ // Set the top-level types
35
+ for (const topLevel of Object.keys(schema.topLevelJsonSchemaTypes)) {
36
+ schemaInput.addTopLevel(topLevel, Ref.parse(`${schema.sourceURI}#/${topLevel}`));
37
+ }
38
+ for (const [name, reference] of Object.entries(schema.topLevelJsonSchemaLocalRefs)) {
39
+ schemaInput.addTopLevel(name, Ref.parse(`${schema.sourceURI}${reference}`));
40
+ }
41
+ mergeSources(mergedSources, schema);
32
42
  }
33
43
  const inputData = new InputData();
34
44
  inputData.addInput(schemaInput);
35
45
  // Update renderer options with the prepared schema
36
46
  const rendererOptions = {
37
47
  nexusOptions: {
38
- nexusSchema: schema,
48
+ nexusSchema: mergedSources,
39
49
  firstFilenameSansExtensions: this.options.firstFilenameSansExtensions,
40
50
  },
41
51
  ...this.options.rendererOptions,
42
52
  };
43
53
  // Run quicktype and return
44
54
  const returnValue = {};
45
- const results = await quicktypeMultiFile({
46
- inputData,
47
- lang: this.options.lang,
48
- rendererOptions,
49
- });
50
- results.forEach((contents, fileName) => (returnValue[fileName] = contents.lines.join("\n")));
55
+ try {
56
+ const results = await quicktypeMultiFile({
57
+ inputData,
58
+ lang: this.options.lang,
59
+ rendererOptions,
60
+ });
61
+ results.forEach((contents, fileName) => (returnValue[fileName] = contents.lines.join("\n")));
62
+ }
63
+ catch (error) {
64
+ if (error instanceof Error &&
65
+ "properties" in error &&
66
+ error.messageName === "SchemaFetchError") {
67
+ // If there is an error with schema fetching, the message
68
+ // produced by quicktype will use an empty string for the address.
69
+ const { address, base } = error.properties;
70
+ throw new Error(`Could not fetch schema "${address}", referred to from ${base}`);
71
+ }
72
+ throw error;
73
+ }
51
74
  return returnValue;
52
75
  }
53
- prepareSchema() {
54
- const schema = {
55
- services: {},
56
- sharedJsonSchema: {
57
- types: this.options.schema.types ?? {},
58
- },
59
- topLevelJsonSchemaTypes: {},
60
- topLevelJsonSchemaLocalRefs: {},
76
+ prepareSchemas() {
77
+ const prepared = {
78
+ sources: [],
61
79
  };
62
- for (const [serviceName, service] of Object.entries(this.options.schema.services || {})) {
63
- schema.services[serviceName] = {
64
- description: service.description,
65
- operations: {},
80
+ for (const definitionSource of this.options.definitionSources) {
81
+ const schema = {
82
+ sourceURI: definitionSource.fileURI.toString(),
83
+ services: {},
84
+ sharedJsonSchema: {
85
+ types: definitionSource.schema.types ?? {},
86
+ },
87
+ topLevelJsonSchemaTypes: {},
88
+ topLevelJsonSchemaLocalRefs: {},
66
89
  };
67
- for (const [operationName, operation] of Object.entries(service.operations)) {
68
- const schemaOp = {
69
- description: operation.description,
90
+ for (const [serviceName, service] of Object.entries(definitionSource.schema.services ?? {})) {
91
+ schema.services[serviceName] = {
92
+ description: service.description,
93
+ operations: {},
70
94
  };
71
- schema.services[serviceName].operations[operationName] = schemaOp;
72
- if (operation.input) {
73
- schemaOp.input = this.prepareInOutType(schema, serviceName, operationName, operation.input, "Input");
74
- }
75
- if (operation.output) {
76
- schemaOp.output = this.prepareInOutType(schema, serviceName, operationName, operation.output, "Output");
95
+ for (const [operationName, operation] of Object.entries(service.operations)) {
96
+ const schemaOp = {
97
+ description: operation.description,
98
+ };
99
+ schema.services[serviceName].operations[operationName] = schemaOp;
100
+ if (operation.input) {
101
+ schemaOp.input = this.prepareInOutType(schema, serviceName, operationName, operation.input, "Input");
102
+ }
103
+ if (operation.output) {
104
+ schemaOp.output = this.prepareInOutType(schema, serviceName, operationName, operation.output, "Output");
105
+ }
77
106
  }
78
107
  }
108
+ prepared.sources.push(schema);
79
109
  }
80
- return schema;
110
+ return prepared;
81
111
  }
82
112
  prepareInOutType(schema, serviceName, operationName, opInOut, suffix) {
83
113
  // Check for an existing ref for this specific lang first
@@ -120,9 +150,23 @@ export class Generator {
120
150
  return { kind: "jsonSchema", name };
121
151
  }
122
152
  }
123
- class FetchDisabledSchemaStore extends JSONSchemaStore {
124
- fetch(_address) {
125
- // TODO(cretz): Support this?
126
- throw new Error("External $ref unsupported");
153
+ function mergeSources(destination, source) {
154
+ for (const name of Object.keys(source.topLevelJsonSchemaTypes)) {
155
+ if (name in destination.topLevelJsonSchemaTypes) {
156
+ throw new Error(`Duplicate type "${name}" defined across multiple definition files`);
157
+ }
158
+ }
159
+ for (const name of Object.keys(source.services)) {
160
+ if (name in destination.services) {
161
+ throw new Error(`Duplicate service "${name}" defined across multiple definition files`);
162
+ }
163
+ }
164
+ Object.assign(destination.topLevelJsonSchemaTypes, source.topLevelJsonSchemaTypes);
165
+ Object.assign(destination.services, source.services);
166
+ }
167
+ class LocalFetchingSchemaStore extends JSONSchemaStore {
168
+ async fetch(address) {
169
+ const content = await readFile(new URL(address), "utf8");
170
+ return yaml.parse(content);
127
171
  }
128
172
  }
package/dist/index.d.ts CHANGED
@@ -5,4 +5,5 @@ export { GoLanguageWithNexus } from "./language-go.js";
5
5
  export { JavaLanguageWithNexus } from "./language-java.js";
6
6
  export { PythonLanguageWithNexus } from "./language-python.js";
7
7
  export { TypeScriptLanguageWithNexus } from "./language-typescript.js";
8
+ export type { DefinitionSource } from "./definition-source.js";
8
9
  export { parseFiles } from "./parser.js";
@@ -28,7 +28,7 @@ export class GoLanguageWithNexus extends GoTargetLanguage {
28
28
  },
29
29
  emitTopLevel(original, t, name) {
30
30
  // Do not emit __ALL_TYPES__ placeholder
31
- if (name.firstProposedName(new Map()) == "__ALL_TYPES__") {
31
+ if (name.firstProposedName(new Map()).endsWith("/__ALL_TYPES__")) {
32
32
  return;
33
33
  }
34
34
  original(t, name);
@@ -132,7 +132,7 @@ class TypeScriptRenderAdapter extends RenderAdapter {
132
132
  // alterations.
133
133
  // Primitives
134
134
  this.render.forEachWithBlankLines(this.render.topLevels, "none", (t, name, pos) => {
135
- if (!t.isPrimitive() || name == "__ALL_TYPES__") {
135
+ if (!t.isPrimitive() || name.endsWith("/__ALL_TYPES__")) {
136
136
  return;
137
137
  }
138
138
  this.render.ensureBlankLine();
package/dist/parser.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import type { DefinitionSchema } from "./definition-schema.js";
2
- export declare function parseFiles(files: string[]): Promise<DefinitionSchema>;
1
+ import type { DefinitionSource } from "./definition-source.js";
2
+ export declare function parseFiles(files: string[]): Promise<DefinitionSource[]>;
package/dist/parser.js CHANGED
@@ -3,6 +3,7 @@ import Ajv from "ajv";
3
3
  import addFormats from "ajv-formats";
4
4
  import yaml from "yaml";
5
5
  import { readFile } from "node:fs/promises";
6
+ import { pathToFileURL } from "node:url";
6
7
  const ajv = new Ajv({
7
8
  allErrors: true,
8
9
  strict: false,
@@ -12,13 +13,8 @@ const ajv = new Ajv({
12
13
  },
13
14
  });
14
15
  addFormats(ajv);
15
- export async function parseFiles(files) {
16
- // TODO(cretz): Multi-file
17
- if (files.length != 1) {
18
- throw new Error("Must have only 1 file at this time");
19
- }
20
- // Parse YAML
21
- const document = yaml.parse(await readFile(files[0], "utf8"));
16
+ async function parseFile(file) {
17
+ const document = yaml.parse(await readFile(file, "utf8"));
22
18
  // Validate. We recreate the validator because it carries state.
23
19
  const valueFunction = await ajv.compileAsync(jsonSchema);
24
20
  if (!valueFunction(document)) {
@@ -30,5 +26,28 @@ export async function parseFiles(files) {
30
26
  ?.map((error) => `${error.instancePath || "(root)"}: ${error.message} (${JSON.stringify(error.params)})`)
31
27
  .join(", "));
32
28
  }
33
- return document;
29
+ return { fileURI: pathToFileURL(file), schema: document };
30
+ }
31
+ export async function parseFiles(files) {
32
+ if (files.length === 0) {
33
+ throw new Error("Must have at least 1 file");
34
+ }
35
+ const results = await Promise.allSettled(files.map(parseFile));
36
+ const errors = [];
37
+ const sources = [];
38
+ for (let index = 0; index < results.length; index++) {
39
+ const result = results[index];
40
+ if (result.status === "rejected") {
41
+ errors.push(new Error(`${files[index]}: ${result.reason}`, {
42
+ cause: result.reason,
43
+ }));
44
+ }
45
+ else {
46
+ sources.push(result.value);
47
+ }
48
+ }
49
+ if (errors.length > 0) {
50
+ throw new AggregateError(errors, `Failed to parse ${errors.length} file(s)`);
51
+ }
52
+ return sources;
34
53
  }
@@ -1,5 +1,5 @@
1
1
  import { ConvenienceRenderer, Name, Namer, type RendererOptions, type Sourcelike } from "quicktype-core";
2
- import { type NexusRendererOptions, type PreparedSchema } from "./generator.js";
2
+ import { type NexusRendererOptions, type MergedSources } from "./generator.js";
3
3
  import type { BlankLineConfig, ForEachPosition } from "quicktype-core/dist/Renderer.js";
4
4
  import type { ClassProperty, ClassType, EnumType, ObjectType, Type, TypeGraph, UnionType } from "quicktype-core/dist/Type/index.js";
5
5
  import type { ForbiddenWordsInfo } from "quicktype-core/dist/ConvenienceRenderer.js";
@@ -9,7 +9,7 @@ export declare abstract class RenderAdapter<AccessibleRenderer> {
9
9
  protected readonly nexusRendererOptions: NexusRendererOptions;
10
10
  private readonly serviceTypeNamesInUse;
11
11
  constructor(render: ConvenienceRenderer, rendererOptions: RendererOptions);
12
- get schema(): PreparedSchema;
12
+ get schema(): MergedSources;
13
13
  makeRenderer<T extends ConvenienceRenderer>(overrides: {
14
14
  [K in keyof AccessibleRenderer]?: AccessibleRenderer[K] extends AnyFunction ? (this: AccessibleRenderer, original: AccessibleRenderer[K], ...arguments_: Parameters<AccessibleRenderer[K]>) => ReturnType<AccessibleRenderer[K]> : never;
15
15
  }): T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexus-rpc/gen-core",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "Nexus code generation (core library)",
5
5
  "author": "Temporal Technologies Inc. <sdk@temporal.io>",
6
6
  "license": "MIT",
@@ -0,0 +1,6 @@
1
+ import type { DefinitionSchema } from "./definition-schema.js";
2
+
3
+ export interface DefinitionSource {
4
+ fileURI: URL;
5
+ schema: DefinitionSchema;
6
+ }
package/src/generator.ts CHANGED
@@ -9,19 +9,25 @@ import {
9
9
  type RendererOptions,
10
10
  type TargetLanguage,
11
11
  } from "quicktype-core";
12
- import type { DefinitionSchema } from "./definition-schema";
12
+ import type { DefinitionSource } from "./definition-source.js";
13
13
  import { PathElementKind } from "quicktype-core/dist/input/PathElement.js";
14
- import { isPrimitiveTypeKind } from "quicktype-core/dist/Type/index.js";
14
+ import { readFile } from "node:fs/promises";
15
+ import yaml from "yaml";
15
16
 
16
17
  export interface GeneratorOptions<Lang extends LanguageName = LanguageName> {
17
18
  lang: TargetLanguage;
18
- schema: DefinitionSchema;
19
+ definitionSources: DefinitionSource[];
19
20
  rendererOptions: RendererOptions<Lang>;
20
21
  // Used to help with ideal output filename
21
22
  firstFilenameSansExtensions: string;
22
23
  }
23
24
 
25
+ export interface PreparedSchemaSources {
26
+ sources: PreparedSchema[];
27
+ }
28
+
24
29
  export interface PreparedSchema {
30
+ sourceURI: string;
25
31
  services: { [key: string]: PreparedService };
26
32
  sharedJsonSchema: { types: { [key: string]: any } };
27
33
  topLevelJsonSchemaTypes: { [key: string]: any };
@@ -44,8 +50,13 @@ export interface PreparedTypeReference {
44
50
  name: string;
45
51
  }
46
52
 
53
+ export interface MergedSources {
54
+ services: { [key: string]: PreparedService };
55
+ topLevelJsonSchemaTypes: { [key: string]: any };
56
+ }
57
+
47
58
  export interface NexusRendererOptions {
48
- nexusSchema: PreparedSchema;
59
+ nexusSchema: MergedSources;
49
60
  firstFilenameSansExtensions: string;
50
61
  }
51
62
 
@@ -64,30 +75,42 @@ export class Generator {
64
75
 
65
76
  async generate(): Promise<{ [fileName: string]: string }> {
66
77
  // Prepare schema
67
- const schema = this.prepareSchema();
78
+ const sources = this.prepareSchemas();
79
+ const mergedSources: MergedSources = {
80
+ services: {},
81
+ topLevelJsonSchemaTypes: {},
82
+ };
68
83
 
69
84
  // Build quicktype input
70
- const schemaInput = new JSONSchemaInput(new FetchDisabledSchemaStore());
71
- const jsonSchema = {
72
- ...schema.sharedJsonSchema,
73
- ...schema.topLevelJsonSchemaTypes,
74
- };
75
- await schemaInput.addSource({
76
- // TODO(cretz): Give proper filename name here for proper cross-file referencing
77
- name: "__ALL_TYPES__",
78
- schema: JSON.stringify(jsonSchema),
79
- });
80
- // Set the top-level types
81
- for (const topLevel of Object.keys(schema.topLevelJsonSchemaTypes)) {
82
- schemaInput.addTopLevel(
83
- topLevel,
84
- Ref.parse(`__ALL_TYPES__#/${topLevel}`),
85
- );
86
- }
87
- for (const [name, reference] of Object.entries(
88
- schema.topLevelJsonSchemaLocalRefs,
89
- )) {
90
- schemaInput.addTopLevel(name, Ref.parse(`__ALL_TYPES__${reference}`));
85
+ const schemaInput = new JSONSchemaInput(new LocalFetchingSchemaStore());
86
+ for (const schema of sources.sources) {
87
+ const jsonSchema = {
88
+ ...schema.sharedJsonSchema,
89
+ ...schema.topLevelJsonSchemaTypes,
90
+ };
91
+ await schemaInput.addSource({
92
+ // Append __ALL_TYPES__ to make it easier to filter out top level
93
+ // while rendering
94
+ name: schema.sourceURI.toString() + "/__ALL_TYPES__",
95
+ uris: [schema.sourceURI],
96
+ schema: JSON.stringify(jsonSchema),
97
+ });
98
+ // Set the top-level types
99
+ for (const topLevel of Object.keys(schema.topLevelJsonSchemaTypes)) {
100
+ schemaInput.addTopLevel(
101
+ topLevel,
102
+ Ref.parse(`${schema.sourceURI}#/${topLevel}`),
103
+ );
104
+ }
105
+ for (const [name, reference] of Object.entries(
106
+ schema.topLevelJsonSchemaLocalRefs,
107
+ )) {
108
+ schemaInput.addTopLevel(
109
+ name,
110
+ Ref.parse(`${schema.sourceURI}${reference}`),
111
+ );
112
+ }
113
+ mergeSources(mergedSources, schema);
91
114
  }
92
115
  const inputData = new InputData();
93
116
  inputData.addInput(schemaInput);
@@ -95,7 +118,7 @@ export class Generator {
95
118
  // Update renderer options with the prepared schema
96
119
  const rendererOptions = {
97
120
  nexusOptions: {
98
- nexusSchema: schema,
121
+ nexusSchema: mergedSources,
99
122
  firstFilenameSansExtensions: this.options.firstFilenameSansExtensions,
100
123
  },
101
124
  ...this.options.rendererOptions,
@@ -103,62 +126,86 @@ export class Generator {
103
126
 
104
127
  // Run quicktype and return
105
128
  const returnValue: { [fileName: string]: string } = {};
106
- const results = await quicktypeMultiFile({
107
- inputData,
108
- lang: this.options.lang,
109
- rendererOptions,
110
- });
111
- results.forEach(
112
- (contents, fileName) =>
113
- (returnValue[fileName] = contents.lines.join("\n")),
114
- );
129
+ try {
130
+ const results = await quicktypeMultiFile({
131
+ inputData,
132
+ lang: this.options.lang,
133
+ rendererOptions,
134
+ });
135
+ results.forEach(
136
+ (contents, fileName) =>
137
+ (returnValue[fileName] = contents.lines.join("\n")),
138
+ );
139
+ } catch (error: unknown) {
140
+ if (
141
+ error instanceof Error &&
142
+ "properties" in error &&
143
+ (error as any).messageName === "SchemaFetchError"
144
+ ) {
145
+ // If there is an error with schema fetching, the message
146
+ // produced by quicktype will use an empty string for the address.
147
+ const { address, base } = (error as any).properties;
148
+ throw new Error(
149
+ `Could not fetch schema "${address}", referred to from ${base}`,
150
+ );
151
+ }
152
+ throw error;
153
+ }
115
154
  return returnValue;
116
155
  }
117
156
 
118
- private prepareSchema(): PreparedSchema {
119
- const schema: PreparedSchema = {
120
- services: {},
121
- sharedJsonSchema: {
122
- types: this.options.schema.types ?? {},
123
- },
124
- topLevelJsonSchemaTypes: {},
125
- topLevelJsonSchemaLocalRefs: {},
157
+ private prepareSchemas(): PreparedSchemaSources {
158
+ const prepared: PreparedSchemaSources = {
159
+ sources: [],
126
160
  };
127
- for (const [serviceName, service] of Object.entries(
128
- this.options.schema.services || {},
129
- )) {
130
- schema.services[serviceName] = {
131
- description: service.description,
132
- operations: {},
161
+
162
+ for (const definitionSource of this.options.definitionSources) {
163
+ const schema: PreparedSchema = {
164
+ sourceURI: definitionSource.fileURI.toString(),
165
+ services: {},
166
+ sharedJsonSchema: {
167
+ types: definitionSource.schema.types ?? {},
168
+ },
169
+ topLevelJsonSchemaTypes: {},
170
+ topLevelJsonSchemaLocalRefs: {},
133
171
  };
134
- for (const [operationName, operation] of Object.entries(
135
- service.operations,
172
+ for (const [serviceName, service] of Object.entries(
173
+ definitionSource.schema.services ?? {},
136
174
  )) {
137
- const schemaOp: PreparedOperation = {
138
- description: operation.description,
175
+ schema.services[serviceName] = {
176
+ description: service.description,
177
+ operations: {},
139
178
  };
140
- schema.services[serviceName].operations[operationName] = schemaOp;
141
- if (operation.input) {
142
- schemaOp.input = this.prepareInOutType(
143
- schema,
144
- serviceName,
145
- operationName,
146
- operation.input,
147
- "Input",
148
- );
149
- }
150
- if (operation.output) {
151
- schemaOp.output = this.prepareInOutType(
152
- schema,
153
- serviceName,
154
- operationName,
155
- operation.output,
156
- "Output",
157
- );
179
+ for (const [operationName, operation] of Object.entries(
180
+ service.operations,
181
+ )) {
182
+ const schemaOp: PreparedOperation = {
183
+ description: operation.description,
184
+ };
185
+ schema.services[serviceName].operations[operationName] = schemaOp;
186
+ if (operation.input) {
187
+ schemaOp.input = this.prepareInOutType(
188
+ schema,
189
+ serviceName,
190
+ operationName,
191
+ operation.input,
192
+ "Input",
193
+ );
194
+ }
195
+ if (operation.output) {
196
+ schemaOp.output = this.prepareInOutType(
197
+ schema,
198
+ serviceName,
199
+ operationName,
200
+ operation.output,
201
+ "Output",
202
+ );
203
+ }
158
204
  }
159
205
  }
206
+ prepared.sources.push(schema);
160
207
  }
161
- return schema;
208
+ return prepared;
162
209
  }
163
210
 
164
211
  private prepareInOutType(
@@ -214,9 +261,34 @@ export class Generator {
214
261
  }
215
262
  }
216
263
 
217
- class FetchDisabledSchemaStore extends JSONSchemaStore {
218
- fetch(_address: string): Promise<JSONSchema | undefined> {
219
- // TODO(cretz): Support this?
220
- throw new Error("External $ref unsupported");
264
+ function mergeSources(
265
+ destination: MergedSources,
266
+ source: PreparedSchema,
267
+ ): void {
268
+ for (const name of Object.keys(source.topLevelJsonSchemaTypes)) {
269
+ if (name in destination.topLevelJsonSchemaTypes) {
270
+ throw new Error(
271
+ `Duplicate type "${name}" defined across multiple definition files`,
272
+ );
273
+ }
274
+ }
275
+ for (const name of Object.keys(source.services)) {
276
+ if (name in destination.services) {
277
+ throw new Error(
278
+ `Duplicate service "${name}" defined across multiple definition files`,
279
+ );
280
+ }
281
+ }
282
+ Object.assign(
283
+ destination.topLevelJsonSchemaTypes,
284
+ source.topLevelJsonSchemaTypes,
285
+ );
286
+ Object.assign(destination.services, source.services);
287
+ }
288
+
289
+ class LocalFetchingSchemaStore extends JSONSchemaStore {
290
+ async fetch(address: string): Promise<JSONSchema | undefined> {
291
+ const content = await readFile(new URL(address), "utf8");
292
+ return yaml.parse(content);
221
293
  }
222
294
  }
package/src/index.ts CHANGED
@@ -5,4 +5,5 @@ export { GoLanguageWithNexus } from "./language-go.js";
5
5
  export { JavaLanguageWithNexus } from "./language-java.js";
6
6
  export { PythonLanguageWithNexus } from "./language-python.js";
7
7
  export { TypeScriptLanguageWithNexus } from "./language-typescript.js";
8
+ export type { DefinitionSource } from "./definition-source.js";
8
9
  export { parseFiles } from "./parser.js";
@@ -63,7 +63,7 @@ export class GoLanguageWithNexus extends GoTargetLanguage {
63
63
  },
64
64
  emitTopLevel(original, t, name) {
65
65
  // Do not emit __ALL_TYPES__ placeholder
66
- if (name.firstProposedName(new Map()) == "__ALL_TYPES__") {
66
+ if (name.firstProposedName(new Map()).endsWith("/__ALL_TYPES__")) {
67
67
  return;
68
68
  }
69
69
  original(t, name);
@@ -220,7 +220,7 @@ class TypeScriptRenderAdapter extends RenderAdapter<TypeScriptRenderAccessible>
220
220
  this.render.topLevels,
221
221
  "none",
222
222
  (t, name, pos) => {
223
- if (!t.isPrimitive() || name == "__ALL_TYPES__") {
223
+ if (!t.isPrimitive() || name.endsWith("/__ALL_TYPES__")) {
224
224
  return;
225
225
  }
226
226
 
package/src/parser.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import type { DefinitionSchema } from "./definition-schema.js";
2
+ import type { DefinitionSource } from "./definition-source.js";
2
3
 
3
4
  import jsonSchema from "../schemas/nexus-rpc-gen.json" with { type: "json" };
4
5
  import Ajv from "ajv";
5
6
  import addFormats from "ajv-formats";
6
7
  import yaml from "yaml";
7
8
  import { readFile } from "node:fs/promises";
9
+ import { pathToFileURL } from "node:url";
8
10
 
9
11
  const ajv = new Ajv({
10
12
  allErrors: true,
@@ -17,14 +19,8 @@ const ajv = new Ajv({
17
19
 
18
20
  addFormats(ajv);
19
21
 
20
- export async function parseFiles(files: string[]): Promise<DefinitionSchema> {
21
- // TODO(cretz): Multi-file
22
- if (files.length != 1) {
23
- throw new Error("Must have only 1 file at this time");
24
- }
25
-
26
- // Parse YAML
27
- const document = yaml.parse(await readFile(files[0], "utf8"));
22
+ async function parseFile(file: string): Promise<DefinitionSource> {
23
+ const document = yaml.parse(await readFile(file, "utf8"));
28
24
 
29
25
  // Validate. We recreate the validator because it carries state.
30
26
  const valueFunction = await ajv.compileAsync<DefinitionSchema>(jsonSchema);
@@ -42,6 +38,37 @@ export async function parseFiles(files: string[]): Promise<DefinitionSchema> {
42
38
  .join(", "),
43
39
  );
44
40
  }
41
+ return { fileURI: pathToFileURL(file), schema: document };
42
+ }
43
+
44
+ export async function parseFiles(files: string[]): Promise<DefinitionSource[]> {
45
+ if (files.length === 0) {
46
+ throw new Error("Must have at least 1 file");
47
+ }
48
+
49
+ const results = await Promise.allSettled(files.map(parseFile));
50
+ const errors: Error[] = [];
51
+ const sources: DefinitionSource[] = [];
52
+
53
+ for (let index = 0; index < results.length; index++) {
54
+ const result = results[index];
55
+ if (result.status === "rejected") {
56
+ errors.push(
57
+ new Error(`${files[index]}: ${result.reason}`, {
58
+ cause: result.reason,
59
+ }),
60
+ );
61
+ } else {
62
+ sources.push(result.value);
63
+ }
64
+ }
65
+
66
+ if (errors.length > 0) {
67
+ throw new AggregateError(
68
+ errors,
69
+ `Failed to parse ${errors.length} file(s)`,
70
+ );
71
+ }
45
72
 
46
- return document;
73
+ return sources;
47
74
  }
@@ -8,13 +8,12 @@ import {
8
8
  import {
9
9
  getNexusRendererOptions,
10
10
  type NexusRendererOptions,
11
- type PreparedSchema,
11
+ type MergedSources,
12
12
  } from "./generator.js";
13
13
  import { proxyWithOverrides } from "./utility.js";
14
14
  import type {
15
15
  BlankLineConfig,
16
16
  ForEachPosition,
17
- Renderer,
18
17
  } from "quicktype-core/dist/Renderer.js";
19
18
  import type {
20
19
  ClassProperty,
@@ -42,7 +41,7 @@ export abstract class RenderAdapter<AccessibleRenderer> {
42
41
  this.nexusRendererOptions = getNexusRendererOptions(rendererOptions);
43
42
  }
44
43
 
45
- public get schema(): PreparedSchema {
44
+ public get schema(): MergedSources {
46
45
  return this.nexusRendererOptions.nexusSchema;
47
46
  }
48
47