@constructive-io/graphql-codegen 2.24.0 → 2.26.0

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 (66) hide show
  1. package/README.md +403 -279
  2. package/cli/codegen/babel-ast.d.ts +7 -0
  3. package/cli/codegen/babel-ast.js +15 -0
  4. package/cli/codegen/barrel.js +43 -14
  5. package/cli/codegen/custom-mutations.js +4 -4
  6. package/cli/codegen/custom-queries.js +12 -22
  7. package/cli/codegen/gql-ast.js +22 -1
  8. package/cli/codegen/index.js +1 -0
  9. package/cli/codegen/mutations.d.ts +2 -0
  10. package/cli/codegen/mutations.js +26 -13
  11. package/cli/codegen/orm/client-generator.js +475 -136
  12. package/cli/codegen/orm/custom-ops-generator.js +8 -3
  13. package/cli/codegen/orm/input-types-generator.js +22 -0
  14. package/cli/codegen/orm/model-generator.js +18 -5
  15. package/cli/codegen/orm/select-types.d.ts +33 -0
  16. package/cli/codegen/queries.d.ts +1 -1
  17. package/cli/codegen/queries.js +112 -35
  18. package/cli/codegen/utils.d.ts +6 -0
  19. package/cli/codegen/utils.js +19 -0
  20. package/cli/commands/generate-orm.d.ts +14 -0
  21. package/cli/commands/generate-orm.js +160 -44
  22. package/cli/commands/generate.d.ts +22 -0
  23. package/cli/commands/generate.js +195 -55
  24. package/cli/commands/init.js +29 -9
  25. package/cli/index.js +133 -28
  26. package/cli/watch/orchestrator.d.ts +4 -0
  27. package/cli/watch/orchestrator.js +4 -0
  28. package/esm/cli/codegen/babel-ast.d.ts +7 -0
  29. package/esm/cli/codegen/babel-ast.js +14 -0
  30. package/esm/cli/codegen/barrel.js +44 -15
  31. package/esm/cli/codegen/custom-mutations.js +5 -5
  32. package/esm/cli/codegen/custom-queries.js +13 -23
  33. package/esm/cli/codegen/gql-ast.js +23 -2
  34. package/esm/cli/codegen/index.js +1 -0
  35. package/esm/cli/codegen/mutations.d.ts +2 -0
  36. package/esm/cli/codegen/mutations.js +27 -14
  37. package/esm/cli/codegen/orm/client-generator.js +475 -136
  38. package/esm/cli/codegen/orm/custom-ops-generator.js +8 -3
  39. package/esm/cli/codegen/orm/input-types-generator.js +22 -0
  40. package/esm/cli/codegen/orm/model-generator.js +18 -5
  41. package/esm/cli/codegen/orm/select-types.d.ts +33 -0
  42. package/esm/cli/codegen/queries.d.ts +1 -1
  43. package/esm/cli/codegen/queries.js +114 -37
  44. package/esm/cli/codegen/utils.d.ts +6 -0
  45. package/esm/cli/codegen/utils.js +18 -0
  46. package/esm/cli/commands/generate-orm.d.ts +14 -0
  47. package/esm/cli/commands/generate-orm.js +161 -45
  48. package/esm/cli/commands/generate.d.ts +22 -0
  49. package/esm/cli/commands/generate.js +195 -56
  50. package/esm/cli/commands/init.js +29 -9
  51. package/esm/cli/index.js +134 -29
  52. package/esm/cli/watch/orchestrator.d.ts +4 -0
  53. package/esm/cli/watch/orchestrator.js +5 -1
  54. package/esm/types/config.d.ts +39 -2
  55. package/esm/types/config.js +88 -4
  56. package/esm/types/index.d.ts +2 -2
  57. package/esm/types/index.js +1 -1
  58. package/package.json +10 -7
  59. package/types/config.d.ts +39 -2
  60. package/types/config.js +91 -4
  61. package/types/index.d.ts +2 -2
  62. package/types/index.js +2 -1
  63. package/cli/codegen/orm/query-builder.d.ts +0 -161
  64. package/cli/codegen/orm/query-builder.js +0 -366
  65. package/esm/cli/codegen/orm/query-builder.d.ts +0 -161
  66. package/esm/cli/codegen/orm/query-builder.js +0 -353
@@ -8,8 +8,8 @@
8
8
  */
9
9
  import * as fs from 'node:fs';
10
10
  import * as path from 'node:path';
11
- import * as prettier from 'prettier';
12
- import { resolveConfig } from '../../types/config';
11
+ import { execSync } from 'node:child_process';
12
+ import { isMultiConfig, mergeConfig, resolveConfig } from '../../types/config';
13
13
  import { createSchemaSource, validateSourceOptions, } from '../introspect/source';
14
14
  import { runCodegenPipeline, validateTablesFound } from './shared';
15
15
  import { findConfigFile, loadConfigFile } from './init';
@@ -18,9 +18,9 @@ import { generate } from '../codegen';
18
18
  * Execute the generate command
19
19
  */
20
20
  export async function generateCommand(options = {}) {
21
- const log = options.verbose ? console.log : () => { };
22
- // 1. Load config
23
- log('Loading configuration...');
21
+ if (options.verbose) {
22
+ console.log('Loading configuration...');
23
+ }
24
24
  const configResult = await loadConfig(options);
25
25
  if (!configResult.success) {
26
26
  return {
@@ -28,24 +28,72 @@ export async function generateCommand(options = {}) {
28
28
  message: configResult.error,
29
29
  };
30
30
  }
31
- const config = configResult.config;
32
- // Log source
33
- if (config.schema) {
34
- log(` Schema: ${config.schema}`);
31
+ const targets = configResult.targets ?? [];
32
+ if (targets.length === 0) {
33
+ return {
34
+ success: false,
35
+ message: 'No targets resolved from configuration.',
36
+ };
37
+ }
38
+ const isMultiTarget = configResult.isMulti ?? targets.length > 1;
39
+ const results = [];
40
+ for (const target of targets) {
41
+ const result = await generateForTarget(target, options, isMultiTarget);
42
+ results.push(result);
43
+ }
44
+ if (!isMultiTarget) {
45
+ const [result] = results;
46
+ return {
47
+ success: result.success,
48
+ message: result.message,
49
+ targets: results,
50
+ tables: result.tables,
51
+ customQueries: result.customQueries,
52
+ customMutations: result.customMutations,
53
+ filesWritten: result.filesWritten,
54
+ errors: result.errors,
55
+ };
35
56
  }
36
- else {
37
- log(` Endpoint: ${config.endpoint}`);
57
+ const successCount = results.filter((result) => result.success).length;
58
+ const failedCount = results.length - successCount;
59
+ const summaryMessage = failedCount === 0
60
+ ? `Generated SDK for ${results.length} targets.`
61
+ : `Generated SDK for ${successCount} of ${results.length} targets.`;
62
+ return {
63
+ success: failedCount === 0,
64
+ message: summaryMessage,
65
+ targets: results,
66
+ errors: failedCount > 0
67
+ ? results.flatMap((result) => result.errors ?? [])
68
+ : undefined,
69
+ };
70
+ }
71
+ async function generateForTarget(target, options, isMultiTarget) {
72
+ const config = target.config;
73
+ const prefix = isMultiTarget ? `[${target.name}] ` : '';
74
+ const log = options.verbose
75
+ ? (message) => console.log(`${prefix}${message}`)
76
+ : () => { };
77
+ const formatMessage = (message) => isMultiTarget ? `Target "${target.name}": ${message}` : message;
78
+ if (isMultiTarget) {
79
+ console.log(`\nTarget "${target.name}"`);
80
+ const sourceLabel = config.schema
81
+ ? `schema: ${config.schema}`
82
+ : `endpoint: ${config.endpoint}`;
83
+ console.log(` Source: ${sourceLabel}`);
84
+ console.log(` Output: ${config.output}`);
38
85
  }
39
- log(` Output: ${config.output}`);
40
- // 2. Create schema source
86
+ // 1. Validate source
41
87
  const sourceValidation = validateSourceOptions({
42
88
  endpoint: config.endpoint || undefined,
43
89
  schema: config.schema || undefined,
44
90
  });
45
91
  if (!sourceValidation.valid) {
46
92
  return {
93
+ name: target.name,
94
+ output: config.output,
47
95
  success: false,
48
- message: sourceValidation.error,
96
+ message: formatMessage(sourceValidation.error),
49
97
  };
50
98
  }
51
99
  const source = createSchemaSource({
@@ -54,7 +102,7 @@ export async function generateCommand(options = {}) {
54
102
  authorization: options.authorization || config.headers['Authorization'],
55
103
  headers: config.headers,
56
104
  });
57
- // 3. Run the codegen pipeline
105
+ // 2. Run the codegen pipeline
58
106
  let pipelineResult;
59
107
  try {
60
108
  pipelineResult = await runCodegenPipeline({
@@ -66,21 +114,25 @@ export async function generateCommand(options = {}) {
66
114
  }
67
115
  catch (err) {
68
116
  return {
117
+ name: target.name,
118
+ output: config.output,
69
119
  success: false,
70
- message: `Failed to fetch schema: ${err instanceof Error ? err.message : 'Unknown error'}`,
120
+ message: formatMessage(`Failed to fetch schema: ${err instanceof Error ? err.message : 'Unknown error'}`),
71
121
  };
72
122
  }
73
123
  const { tables, customOperations, stats } = pipelineResult;
74
- // 4. Validate tables found
124
+ // 3. Validate tables found
75
125
  const tablesValidation = validateTablesFound(tables);
76
126
  if (!tablesValidation.valid) {
77
127
  return {
128
+ name: target.name,
129
+ output: config.output,
78
130
  success: false,
79
- message: tablesValidation.error,
131
+ message: formatMessage(tablesValidation.error),
80
132
  };
81
133
  }
82
- // 5. Generate code
83
- console.log('Generating code...');
134
+ // 4. Generate code
135
+ console.log(`${prefix}Generating code...`);
84
136
  const { files: generatedFiles, stats: genStats } = generate({
85
137
  tables,
86
138
  customOperations: {
@@ -90,7 +142,7 @@ export async function generateCommand(options = {}) {
90
142
  },
91
143
  config,
92
144
  });
93
- console.log(`Generated ${genStats.totalFiles} files`);
145
+ console.log(`${prefix}Generated ${genStats.totalFiles} files`);
94
146
  log(` ${genStats.queryHooks} table query hooks`);
95
147
  log(` ${genStats.mutationHooks} table mutation hooks`);
96
148
  log(` ${genStats.customQueryHooks} custom query hooks`);
@@ -99,15 +151,17 @@ export async function generateCommand(options = {}) {
99
151
  const customMutations = customOperations.mutations.map((m) => m.name);
100
152
  if (options.dryRun) {
101
153
  return {
154
+ name: target.name,
155
+ output: config.output,
102
156
  success: true,
103
- message: `Dry run complete. Would generate ${generatedFiles.length} files for ${tables.length} tables and ${stats.customQueries + stats.customMutations} custom operations.`,
157
+ message: formatMessage(`Dry run complete. Would generate ${generatedFiles.length} files for ${tables.length} tables and ${stats.customQueries + stats.customMutations} custom operations.`),
104
158
  tables: tables.map((t) => t.name),
105
159
  customQueries,
106
160
  customMutations,
107
161
  filesWritten: generatedFiles.map((f) => f.path),
108
162
  };
109
163
  }
110
- // 6. Write files
164
+ // 5. Write files
111
165
  log('Writing files...');
112
166
  const writeResult = await writeGeneratedFiles(generatedFiles, config.output, [
113
167
  'queries',
@@ -115,23 +169,48 @@ export async function generateCommand(options = {}) {
115
169
  ]);
116
170
  if (!writeResult.success) {
117
171
  return {
172
+ name: target.name,
173
+ output: config.output,
118
174
  success: false,
119
- message: `Failed to write files: ${writeResult.errors?.join(', ')}`,
175
+ message: formatMessage(`Failed to write files: ${writeResult.errors?.join(', ')}`),
120
176
  errors: writeResult.errors,
121
177
  };
122
178
  }
123
179
  const totalOps = customQueries.length + customMutations.length;
124
180
  const customOpsMsg = totalOps > 0 ? ` and ${totalOps} custom operations` : '';
125
181
  return {
182
+ name: target.name,
183
+ output: config.output,
126
184
  success: true,
127
- message: `Generated SDK for ${tables.length} tables${customOpsMsg}. Files written to ${config.output}`,
185
+ message: formatMessage(`Generated SDK for ${tables.length} tables${customOpsMsg}. Files written to ${config.output}`),
128
186
  tables: tables.map((t) => t.name),
129
187
  customQueries,
130
188
  customMutations,
131
189
  filesWritten: writeResult.filesWritten,
132
190
  };
133
191
  }
192
+ function buildTargetOverrides(options) {
193
+ const overrides = {};
194
+ if (options.endpoint) {
195
+ overrides.endpoint = options.endpoint;
196
+ overrides.schema = undefined;
197
+ }
198
+ if (options.schema) {
199
+ overrides.schema = options.schema;
200
+ overrides.endpoint = undefined;
201
+ }
202
+ if (options.output) {
203
+ overrides.output = options.output;
204
+ }
205
+ return overrides;
206
+ }
134
207
  async function loadConfig(options) {
208
+ if (options.endpoint && options.schema) {
209
+ return {
210
+ success: false,
211
+ error: 'Cannot use both --endpoint and --schema. Choose one source.',
212
+ };
213
+ }
135
214
  // Find config file
136
215
  let configPath = options.config;
137
216
  if (!configPath) {
@@ -145,31 +224,72 @@ async function loadConfig(options) {
145
224
  }
146
225
  baseConfig = loadResult.config;
147
226
  }
148
- // Override with CLI options
149
- const mergedConfig = {
150
- endpoint: options.endpoint || baseConfig.endpoint,
151
- schema: options.schema || baseConfig.schema,
152
- output: options.output || baseConfig.output,
153
- headers: baseConfig.headers,
154
- tables: baseConfig.tables,
155
- queries: baseConfig.queries,
156
- mutations: baseConfig.mutations,
157
- excludeFields: baseConfig.excludeFields,
158
- hooks: baseConfig.hooks,
159
- postgraphile: baseConfig.postgraphile,
160
- codegen: baseConfig.codegen,
161
- reactQuery: baseConfig.reactQuery,
162
- };
163
- // Validate at least one source is provided
227
+ const overrides = buildTargetOverrides(options);
228
+ if (isMultiConfig(baseConfig)) {
229
+ if (Object.keys(baseConfig.targets).length === 0) {
230
+ return {
231
+ success: false,
232
+ error: 'Config file defines no targets.',
233
+ };
234
+ }
235
+ if (!options.target &&
236
+ (options.endpoint || options.schema || options.output)) {
237
+ return {
238
+ success: false,
239
+ error: 'Multiple targets configured. Use --target with --endpoint, --schema, or --output.',
240
+ };
241
+ }
242
+ if (options.target && !baseConfig.targets[options.target]) {
243
+ return {
244
+ success: false,
245
+ error: `Target "${options.target}" not found in config file.`,
246
+ };
247
+ }
248
+ const selectedTargets = options.target
249
+ ? { [options.target]: baseConfig.targets[options.target] }
250
+ : baseConfig.targets;
251
+ const defaults = baseConfig.defaults ?? {};
252
+ const resolvedTargets = [];
253
+ for (const [name, target] of Object.entries(selectedTargets)) {
254
+ let mergedTarget = mergeConfig(defaults, target);
255
+ if (options.target && name === options.target) {
256
+ mergedTarget = mergeConfig(mergedTarget, overrides);
257
+ }
258
+ if (!mergedTarget.endpoint && !mergedTarget.schema) {
259
+ return {
260
+ success: false,
261
+ error: `Target "${name}" is missing an endpoint or schema.`,
262
+ };
263
+ }
264
+ resolvedTargets.push({
265
+ name,
266
+ config: resolveConfig(mergedTarget),
267
+ });
268
+ }
269
+ return {
270
+ success: true,
271
+ targets: resolvedTargets,
272
+ isMulti: true,
273
+ };
274
+ }
275
+ if (options.target) {
276
+ return {
277
+ success: false,
278
+ error: 'Config file does not define targets. Remove --target to continue.',
279
+ };
280
+ }
281
+ const mergedConfig = mergeConfig(baseConfig, overrides);
164
282
  if (!mergedConfig.endpoint && !mergedConfig.schema) {
165
283
  return {
166
284
  success: false,
167
285
  error: 'No source specified. Use --endpoint or --schema, or create a config file with "graphql-codegen init".',
168
286
  };
169
287
  }
170
- // Resolve with defaults
171
- const config = resolveConfig(mergedConfig);
172
- return { success: true, config };
288
+ return {
289
+ success: true,
290
+ targets: [{ name: 'default', config: resolveConfig(mergedConfig) }],
291
+ isMulti: false,
292
+ };
173
293
  }
174
294
  export async function writeGeneratedFiles(files, outputDir, subdirs, options = {}) {
175
295
  const { showProgress = true } = options;
@@ -225,9 +345,7 @@ export async function writeGeneratedFiles(files, outputDir, subdirs, options = {
225
345
  // Ignore if already exists
226
346
  }
227
347
  try {
228
- // Format with prettier
229
- const formattedContent = await formatCode(file.content);
230
- fs.writeFileSync(filePath, formattedContent, 'utf-8');
348
+ fs.writeFileSync(filePath, file.content, 'utf-8');
231
349
  written.push(filePath);
232
350
  }
233
351
  catch (err) {
@@ -239,23 +357,44 @@ export async function writeGeneratedFiles(files, outputDir, subdirs, options = {
239
357
  if (showProgress && isTTY) {
240
358
  process.stdout.write('\r' + ' '.repeat(40) + '\r');
241
359
  }
360
+ // Format all generated files with oxfmt
361
+ if (errors.length === 0) {
362
+ if (showProgress) {
363
+ console.log('Formatting generated files...');
364
+ }
365
+ const formatResult = formatOutput(outputDir);
366
+ if (!formatResult.success && showProgress) {
367
+ console.warn('Warning: Failed to format generated files:', formatResult.error);
368
+ }
369
+ }
242
370
  return {
243
371
  success: errors.length === 0,
244
372
  filesWritten: written,
245
373
  errors: errors.length > 0 ? errors : undefined,
246
374
  };
247
375
  }
248
- async function formatCode(code) {
376
+ /**
377
+ * Format generated files using oxfmt
378
+ * Runs oxfmt on the output directory after all files are written
379
+ */
380
+ export function formatOutput(outputDir) {
381
+ // Resolve to absolute path for reliable execution
382
+ const absoluteOutputDir = path.resolve(outputDir);
249
383
  try {
250
- return await prettier.format(code, {
251
- parser: 'typescript',
252
- singleQuote: true,
253
- trailingComma: 'es5',
254
- tabWidth: 2,
384
+ // Find oxfmt binary from this package's node_modules/.bin
385
+ // oxfmt is a dependency of @constructive-io/graphql-codegen
386
+ const oxfmtPkgPath = require.resolve('oxfmt/package.json');
387
+ const oxfmtDir = path.dirname(oxfmtPkgPath);
388
+ const oxfmtBin = path.join(oxfmtDir, 'bin', 'oxfmt');
389
+ execSync(`"${oxfmtBin}" "${absoluteOutputDir}"`, {
390
+ stdio: 'pipe',
391
+ encoding: 'utf-8',
255
392
  });
393
+ return { success: true };
256
394
  }
257
- catch {
258
- // If prettier fails, return unformatted code
259
- return code;
395
+ catch (err) {
396
+ // oxfmt may fail if files have syntax errors or if not installed
397
+ const message = err instanceof Error ? err.message : 'Unknown error';
398
+ return { success: false, error: message };
260
399
  }
261
400
  }
@@ -14,6 +14,15 @@ export default defineConfig({
14
14
  // Output directory for generated files
15
15
  output: '{{OUTPUT}}',
16
16
 
17
+ // Optional: Multi-target config (use instead of endpoint/output)
18
+ // defaults: {
19
+ // headers: { Authorization: 'Bearer YOUR_TOKEN' },
20
+ // },
21
+ // targets: {
22
+ // public: { endpoint: 'https://api.example.com/graphql', output: './generated/public' },
23
+ // admin: { schema: './admin.schema.graphql', output: './generated/admin' },
24
+ // },
25
+
17
26
  // Optional: Tables to include/exclude (supports glob patterns)
18
27
  // tables: {
19
28
  // include: ['*'],
@@ -38,7 +47,7 @@ export default defineConfig({
38
47
  * Execute the init command
39
48
  */
40
49
  export async function initCommand(options = {}) {
41
- const { directory = process.cwd(), force = false, endpoint = '', output = './generated' } = options;
50
+ const { directory = process.cwd(), force = false, endpoint = '', output = './generated', } = options;
42
51
  const configPath = path.join(directory, CONFIG_FILENAME);
43
52
  // Check if config already exists
44
53
  if (fs.existsSync(configPath) && !force) {
@@ -48,9 +57,7 @@ export async function initCommand(options = {}) {
48
57
  };
49
58
  }
50
59
  // Generate config content
51
- const content = CONFIG_TEMPLATE
52
- .replace('{{ENDPOINT}}', endpoint || 'http://localhost:5000/graphql')
53
- .replace('{{OUTPUT}}', output);
60
+ const content = CONFIG_TEMPLATE.replace('{{ENDPOINT}}', endpoint || 'http://localhost:5000/graphql').replace('{{OUTPUT}}', output);
54
61
  try {
55
62
  // Ensure directory exists
56
63
  fs.mkdirSync(directory, { recursive: true });
@@ -96,10 +103,11 @@ export function findConfigFile(startDir = process.cwd()) {
96
103
  * tsx or ts-node installed.
97
104
  */
98
105
  export async function loadConfigFile(configPath) {
99
- if (!fs.existsSync(configPath)) {
106
+ const resolvedPath = path.resolve(configPath);
107
+ if (!fs.existsSync(resolvedPath)) {
100
108
  return {
101
109
  success: false,
102
- error: `Config file not found: ${configPath}`,
110
+ error: `Config file not found: ${resolvedPath}`,
103
111
  };
104
112
  }
105
113
  try {
@@ -110,19 +118,31 @@ export async function loadConfigFile(configPath) {
110
118
  debug: process.env.JITI_DEBUG === '1',
111
119
  });
112
120
  // jiti.import() with { default: true } returns mod?.default ?? mod
113
- const config = await jiti.import(configPath, { default: true });
121
+ const config = await jiti.import(resolvedPath, { default: true });
114
122
  if (!config || typeof config !== 'object') {
115
123
  return {
116
124
  success: false,
117
125
  error: 'Config file must export a configuration object',
118
126
  };
119
127
  }
120
- if (!('endpoint' in config)) {
128
+ const hasEndpoint = 'endpoint' in config;
129
+ const hasSchema = 'schema' in config;
130
+ const hasTargets = 'targets' in config;
131
+ if (!hasEndpoint && !hasSchema && !hasTargets) {
121
132
  return {
122
133
  success: false,
123
- error: 'Config file missing required "endpoint" property',
134
+ error: 'Config file must define "endpoint", "schema", or "targets".',
124
135
  };
125
136
  }
137
+ if (hasTargets) {
138
+ const targets = config.targets;
139
+ if (!targets || typeof targets !== 'object' || Array.isArray(targets)) {
140
+ return {
141
+ success: false,
142
+ error: 'Config file "targets" must be an object of named configs.',
143
+ };
144
+ }
145
+ }
126
146
  return {
127
147
  success: true,
128
148
  config,