@constructive-io/graphql-codegen 2.27.0 → 2.27.2

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.
@@ -1,6 +1,8 @@
1
1
  import * as t from '@babel/types';
2
2
  import { generateCode, commentBlock } from '../babel-ast';
3
3
  import { getTableNames, lcFirst, getGeneratedFileHeader } from '../utils';
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
4
6
  /**
5
7
  * Generate the main client.ts file (OrmClient class)
6
8
  * This is the runtime client that handles GraphQL execution
@@ -128,627 +130,46 @@ export class OrmClient {
128
130
  }
129
131
  /**
130
132
  * Generate the query-builder.ts file (runtime query builder)
133
+ *
134
+ * Reads from the actual TypeScript file in the source directory,
135
+ * which enables proper type checking and testability.
131
136
  */
132
137
  export function generateQueryBuilderFile() {
133
- const content = `/**
138
+ // Read the query-builder.ts source file
139
+ // Handle both development (src/) and production (dist/) scenarios
140
+ let sourceFilePath = path.join(__dirname, 'query-builder.ts');
141
+ // If running from dist/, look for the source in src/ instead
142
+ if (!fs.existsSync(sourceFilePath)) {
143
+ // Navigate from dist/cli/codegen/orm/ to src/cli/codegen/orm/
144
+ sourceFilePath = path.resolve(__dirname, '../../../../src/cli/codegen/orm/query-builder.ts');
145
+ }
146
+ // If still not found, try relative to package root
147
+ if (!fs.existsSync(sourceFilePath)) {
148
+ // For installed packages, the file should be adjacent in the same dir
149
+ throw new Error(`Could not find query-builder.ts source file. ` +
150
+ `Searched in: ${path.join(__dirname, 'query-builder.ts')} and ` +
151
+ `${path.resolve(__dirname, '../../../../src/cli/codegen/orm/query-builder.ts')}`);
152
+ }
153
+ let sourceContent = fs.readFileSync(sourceFilePath, 'utf-8');
154
+ // Replace the source file header comment with the generated file header
155
+ const headerComment = `/**
156
+ * Query Builder - Builds and executes GraphQL operations
157
+ *
158
+ * This is the RUNTIME code that gets copied to generated output.
159
+ * It uses gql-ast to build GraphQL documents programmatically.
160
+ *
161
+ * NOTE: This file is read at codegen time and written to output.
162
+ * Any changes here will affect all generated ORM clients.
163
+ */`;
164
+ const generatedHeader = `/**
134
165
  * Query Builder - Builds and executes GraphQL operations
135
166
  * @generated by @constructive-io/graphql-codegen
136
167
  * DO NOT EDIT - changes will be overwritten
137
- */
138
-
139
- import * as t from 'gql-ast';
140
- import { parseType, print } from 'graphql';
141
- import type {
142
- ArgumentNode,
143
- FieldNode,
144
- VariableDefinitionNode,
145
- EnumValueNode,
146
- } from 'graphql';
147
- import { OrmClient, QueryResult, GraphQLRequestError } from './client';
148
-
149
- export interface QueryBuilderConfig {
150
- client: OrmClient;
151
- operation: 'query' | 'mutation';
152
- operationName: string;
153
- fieldName: string;
154
- document: string;
155
- variables?: Record<string, unknown>;
156
- }
157
-
158
- export class QueryBuilder<TResult> {
159
- private config: QueryBuilderConfig;
160
-
161
- constructor(config: QueryBuilderConfig) {
162
- this.config = config;
163
- }
164
-
165
- /**
166
- * Execute the query and return a discriminated union result
167
- * Use result.ok to check success, or .unwrap() to throw on error
168
- */
169
- async execute(): Promise<QueryResult<TResult>> {
170
- return this.config.client.execute<TResult>(
171
- this.config.document,
172
- this.config.variables
173
- );
174
- }
175
-
176
- /**
177
- * Execute and unwrap the result, throwing GraphQLRequestError on failure
178
- * @throws {GraphQLRequestError} If the query returns errors
179
- */
180
- async unwrap(): Promise<TResult> {
181
- const result = await this.execute();
182
- if (!result.ok) {
183
- throw new GraphQLRequestError(result.errors, result.data);
184
- }
185
- return result.data;
186
- }
187
-
188
- /**
189
- * Execute and unwrap, returning defaultValue on error instead of throwing
190
- */
191
- async unwrapOr<D>(defaultValue: D): Promise<TResult | D> {
192
- const result = await this.execute();
193
- if (!result.ok) {
194
- return defaultValue;
195
- }
196
- return result.data;
197
- }
198
-
199
- /**
200
- * Execute and unwrap, calling onError callback on failure
201
- */
202
- async unwrapOrElse<D>(onError: (errors: import('./client').GraphQLError[]) => D): Promise<TResult | D> {
203
- const result = await this.execute();
204
- if (!result.ok) {
205
- return onError(result.errors);
206
- }
207
- return result.data;
208
- }
209
-
210
- toGraphQL(): string {
211
- return this.config.document;
212
- }
213
-
214
- getVariables(): Record<string, unknown> | undefined {
215
- return this.config.variables;
216
- }
217
- }
218
-
219
- // ============================================================================
220
- // Selection Builders
221
- // ============================================================================
222
-
223
- export function buildSelections(
224
- select: Record<string, unknown> | undefined
225
- ): FieldNode[] {
226
- if (!select) {
227
- return [];
228
- }
229
-
230
- const fields: FieldNode[] = [];
231
-
232
- for (const [key, value] of Object.entries(select)) {
233
- if (value === false || value === undefined) {
234
- continue;
235
- }
236
-
237
- if (value === true) {
238
- fields.push(t.field({ name: key }));
239
- continue;
240
- }
241
-
242
- if (typeof value === 'object' && value !== null) {
243
- const nested = value as {
244
- select?: Record<string, unknown>;
245
- first?: number;
246
- filter?: Record<string, unknown>;
247
- orderBy?: string[];
248
- connection?: boolean;
249
- };
250
-
251
- if (nested.select) {
252
- const nestedSelections = buildSelections(nested.select);
253
- const isConnection =
254
- nested.connection === true ||
255
- nested.first !== undefined ||
256
- nested.filter !== undefined;
257
- const args = buildArgs([
258
- buildOptionalArg('first', nested.first),
259
- nested.filter
260
- ? t.argument({ name: 'filter', value: buildValueAst(nested.filter) })
261
- : null,
262
- buildEnumListArg('orderBy', nested.orderBy),
263
- ]);
264
-
265
- if (isConnection) {
266
- fields.push(
267
- t.field({
268
- name: key,
269
- args,
270
- selectionSet: t.selectionSet({
271
- selections: buildConnectionSelections(nestedSelections),
272
- }),
273
- })
274
- );
275
- } else {
276
- fields.push(
277
- t.field({
278
- name: key,
279
- args,
280
- selectionSet: t.selectionSet({ selections: nestedSelections }),
281
- })
282
- );
283
- }
284
- }
285
- }
286
- }
287
-
288
- return fields;
289
- }
290
-
291
- // ============================================================================
292
- // Document Builders
293
- // ============================================================================
294
-
295
- export function buildFindManyDocument<TSelect, TWhere>(
296
- operationName: string,
297
- queryField: string,
298
- select: TSelect,
299
- args: {
300
- where?: TWhere;
301
- orderBy?: string[];
302
- first?: number;
303
- last?: number;
304
- after?: string;
305
- before?: string;
306
- offset?: number;
307
- },
308
- filterTypeName: string,
309
- orderByTypeName: string
310
- ): { document: string; variables: Record<string, unknown> } {
311
- const selections = select
312
- ? buildSelections(select as Record<string, unknown>)
313
- : [t.field({ name: 'id' })];
314
-
315
- const variableDefinitions: VariableDefinitionNode[] = [];
316
- const queryArgs: ArgumentNode[] = [];
317
- const variables: Record<string, unknown> = {};
318
-
319
- addVariable({ varName: 'where', argName: 'filter', typeName: filterTypeName, value: args.where }, variableDefinitions, queryArgs, variables);
320
- addVariable({ varName: 'orderBy', typeName: '[' + orderByTypeName + '!]', value: args.orderBy?.length ? args.orderBy : undefined }, variableDefinitions, queryArgs, variables);
321
- addVariable({ varName: 'first', typeName: 'Int', value: args.first }, variableDefinitions, queryArgs, variables);
322
- addVariable({ varName: 'last', typeName: 'Int', value: args.last }, variableDefinitions, queryArgs, variables);
323
- addVariable({ varName: 'after', typeName: 'Cursor', value: args.after }, variableDefinitions, queryArgs, variables);
324
- addVariable({ varName: 'before', typeName: 'Cursor', value: args.before }, variableDefinitions, queryArgs, variables);
325
- addVariable({ varName: 'offset', typeName: 'Int', value: args.offset }, variableDefinitions, queryArgs, variables);
326
-
327
- const document = t.document({
328
- definitions: [
329
- t.operationDefinition({
330
- operation: 'query',
331
- name: operationName + 'Query',
332
- variableDefinitions: variableDefinitions.length ? variableDefinitions : undefined,
333
- selectionSet: t.selectionSet({
334
- selections: [
335
- t.field({
336
- name: queryField,
337
- args: queryArgs.length ? queryArgs : undefined,
338
- selectionSet: t.selectionSet({
339
- selections: buildConnectionSelections(selections),
340
- }),
341
- }),
342
- ],
343
- }),
344
- }),
345
- ],
346
- });
347
-
348
- return { document: print(document), variables };
349
- }
350
-
351
- export function buildFindFirstDocument<TSelect, TWhere>(
352
- operationName: string,
353
- queryField: string,
354
- select: TSelect,
355
- args: { where?: TWhere },
356
- filterTypeName: string
357
- ): { document: string; variables: Record<string, unknown> } {
358
- const selections = select
359
- ? buildSelections(select as Record<string, unknown>)
360
- : [t.field({ name: 'id' })];
361
-
362
- const variableDefinitions: VariableDefinitionNode[] = [];
363
- const queryArgs: ArgumentNode[] = [];
364
- const variables: Record<string, unknown> = {};
365
-
366
- // Always add first: 1 for findFirst
367
- addVariable({ varName: 'first', typeName: 'Int', value: 1 }, variableDefinitions, queryArgs, variables);
368
- addVariable({ varName: 'where', argName: 'filter', typeName: filterTypeName, value: args.where }, variableDefinitions, queryArgs, variables);
369
-
370
- const document = t.document({
371
- definitions: [
372
- t.operationDefinition({
373
- operation: 'query',
374
- name: operationName + 'Query',
375
- variableDefinitions,
376
- selectionSet: t.selectionSet({
377
- selections: [
378
- t.field({
379
- name: queryField,
380
- args: queryArgs,
381
- selectionSet: t.selectionSet({
382
- selections: [
383
- t.field({
384
- name: 'nodes',
385
- selectionSet: t.selectionSet({ selections }),
386
- }),
387
- ],
388
- }),
389
- }),
390
- ],
391
- }),
392
- }),
393
- ],
394
- });
395
-
396
- return { document: print(document), variables };
397
- }
398
-
399
- export function buildCreateDocument<TSelect, TData>(
400
- operationName: string,
401
- mutationField: string,
402
- entityField: string,
403
- select: TSelect,
404
- data: TData,
405
- inputTypeName: string
406
- ): { document: string; variables: Record<string, unknown> } {
407
- const selections = select
408
- ? buildSelections(select as Record<string, unknown>)
409
- : [t.field({ name: 'id' })];
410
-
411
- return {
412
- document: buildInputMutationDocument({
413
- operationName,
414
- mutationField,
415
- inputTypeName,
416
- resultSelections: [
417
- t.field({
418
- name: entityField,
419
- selectionSet: t.selectionSet({ selections }),
420
- }),
421
- ],
422
- }),
423
- variables: {
424
- input: {
425
- [entityField]: data,
426
- },
427
- },
428
- };
429
- }
430
-
431
- export function buildUpdateDocument<TSelect, TWhere extends { id: string }, TData>(
432
- operationName: string,
433
- mutationField: string,
434
- entityField: string,
435
- select: TSelect,
436
- where: TWhere,
437
- data: TData,
438
- inputTypeName: string
439
- ): { document: string; variables: Record<string, unknown> } {
440
- const selections = select
441
- ? buildSelections(select as Record<string, unknown>)
442
- : [t.field({ name: 'id' })];
443
-
444
- return {
445
- document: buildInputMutationDocument({
446
- operationName,
447
- mutationField,
448
- inputTypeName,
449
- resultSelections: [
450
- t.field({
451
- name: entityField,
452
- selectionSet: t.selectionSet({ selections }),
453
- }),
454
- ],
455
- }),
456
- variables: {
457
- input: {
458
- id: where.id,
459
- patch: data,
460
- },
461
- },
462
- };
463
- }
464
-
465
- export function buildDeleteDocument<TWhere extends { id: string }>(
466
- operationName: string,
467
- mutationField: string,
468
- entityField: string,
469
- where: TWhere,
470
- inputTypeName: string
471
- ): { document: string; variables: Record<string, unknown> } {
472
- return {
473
- document: buildInputMutationDocument({
474
- operationName,
475
- mutationField,
476
- inputTypeName,
477
- resultSelections: [
478
- t.field({
479
- name: entityField,
480
- selectionSet: t.selectionSet({
481
- selections: [t.field({ name: 'id' })],
482
- }),
483
- }),
484
- ],
485
- }),
486
- variables: {
487
- input: {
488
- id: where.id,
489
- },
490
- },
491
- };
492
- }
493
-
494
- export function buildCustomDocument<TSelect, TArgs>(
495
- operationType: 'query' | 'mutation',
496
- operationName: string,
497
- fieldName: string,
498
- select: TSelect,
499
- args: TArgs,
500
- variableDefinitions: Array<{ name: string; type: string }>
501
- ): { document: string; variables: Record<string, unknown> } {
502
- let actualSelect = select;
503
- let isConnection = false;
504
-
505
- if (select && typeof select === 'object' && 'select' in select) {
506
- const wrapper = select as { select?: TSelect; connection?: boolean };
507
- if (wrapper.select) {
508
- actualSelect = wrapper.select;
509
- isConnection = wrapper.connection === true;
510
- }
511
- }
512
-
513
- const selections = actualSelect
514
- ? buildSelections(actualSelect as Record<string, unknown>)
515
- : [];
516
-
517
- const variableDefs = variableDefinitions.map((definition) =>
518
- t.variableDefinition({
519
- variable: t.variable({ name: definition.name }),
520
- type: parseType(definition.type),
521
- })
522
- );
523
- const fieldArgs = variableDefinitions.map((definition) =>
524
- t.argument({
525
- name: definition.name,
526
- value: t.variable({ name: definition.name }),
527
- })
528
- );
529
-
530
- const fieldSelections = isConnection
531
- ? buildConnectionSelections(selections)
532
- : selections;
533
-
534
- const document = t.document({
535
- definitions: [
536
- t.operationDefinition({
537
- operation: operationType,
538
- name: operationName,
539
- variableDefinitions: variableDefs.length ? variableDefs : undefined,
540
- selectionSet: t.selectionSet({
541
- selections: [
542
- t.field({
543
- name: fieldName,
544
- args: fieldArgs.length ? fieldArgs : undefined,
545
- selectionSet: fieldSelections.length
546
- ? t.selectionSet({ selections: fieldSelections })
547
- : undefined,
548
- }),
549
- ],
550
- }),
551
- }),
552
- ],
553
- });
554
-
555
- return {
556
- document: print(document),
557
- variables: (args ?? {}) as Record<string, unknown>,
558
- };
559
- }
560
-
561
- // ============================================================================
562
- // Helper Functions
563
- // ============================================================================
564
-
565
- function buildArgs(args: Array<ArgumentNode | null>): ArgumentNode[] {
566
- return args.filter((arg): arg is ArgumentNode => arg !== null);
567
- }
568
-
569
- function buildOptionalArg(
570
- name: string,
571
- value: number | string | undefined
572
- ): ArgumentNode | null {
573
- if (value === undefined) {
574
- return null;
575
- }
576
- const valueNode =
577
- typeof value === 'number'
578
- ? t.intValue({ value: value.toString() })
579
- : t.stringValue({ value });
580
- return t.argument({ name, value: valueNode });
581
- }
582
-
583
- function buildEnumListArg(
584
- name: string,
585
- values: string[] | undefined
586
- ): ArgumentNode | null {
587
- if (!values || values.length === 0) {
588
- return null;
589
- }
590
- return t.argument({
591
- name,
592
- value: t.listValue({
593
- values: values.map((value) => buildEnumValue(value)),
594
- }),
595
- });
596
- }
597
-
598
- function buildEnumValue(value: string): EnumValueNode {
599
- return {
600
- kind: 'EnumValue',
601
- value,
602
- };
603
- }
604
-
605
- function buildPageInfoSelections(): FieldNode[] {
606
- return [
607
- t.field({ name: 'hasNextPage' }),
608
- t.field({ name: 'hasPreviousPage' }),
609
- t.field({ name: 'startCursor' }),
610
- t.field({ name: 'endCursor' }),
611
- ];
612
- }
613
-
614
- function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] {
615
- return [
616
- t.field({
617
- name: 'nodes',
618
- selectionSet: t.selectionSet({ selections: nodeSelections }),
619
- }),
620
- t.field({ name: 'totalCount' }),
621
- t.field({
622
- name: 'pageInfo',
623
- selectionSet: t.selectionSet({ selections: buildPageInfoSelections() }),
624
- }),
625
- ];
626
- }
627
-
628
- interface VariableSpec {
629
- varName: string;
630
- argName?: string;
631
- typeName: string;
632
- value: unknown;
633
- }
634
-
635
- interface InputMutationConfig {
636
- operationName: string;
637
- mutationField: string;
638
- inputTypeName: string;
639
- resultSelections: FieldNode[];
640
- }
641
-
642
- function buildInputMutationDocument(config: InputMutationConfig): string {
643
- const document = t.document({
644
- definitions: [
645
- t.operationDefinition({
646
- operation: 'mutation',
647
- name: config.operationName + 'Mutation',
648
- variableDefinitions: [
649
- t.variableDefinition({
650
- variable: t.variable({ name: 'input' }),
651
- type: parseType(config.inputTypeName + '!'),
652
- }),
653
- ],
654
- selectionSet: t.selectionSet({
655
- selections: [
656
- t.field({
657
- name: config.mutationField,
658
- args: [
659
- t.argument({
660
- name: 'input',
661
- value: t.variable({ name: 'input' }),
662
- }),
663
- ],
664
- selectionSet: t.selectionSet({
665
- selections: config.resultSelections,
666
- }),
667
- }),
668
- ],
669
- }),
670
- }),
671
- ],
672
- });
673
- return print(document);
674
- }
675
-
676
- function addVariable(
677
- spec: VariableSpec,
678
- definitions: VariableDefinitionNode[],
679
- args: ArgumentNode[],
680
- variables: Record<string, unknown>
681
- ): void {
682
- if (spec.value === undefined) return;
683
-
684
- definitions.push(
685
- t.variableDefinition({
686
- variable: t.variable({ name: spec.varName }),
687
- type: parseType(spec.typeName),
688
- })
689
- );
690
- args.push(
691
- t.argument({
692
- name: spec.argName ?? spec.varName,
693
- value: t.variable({ name: spec.varName }),
694
- })
695
- );
696
- variables[spec.varName] = spec.value;
697
- }
698
-
699
- function buildValueAst(
700
- value: unknown
701
- ):
702
- | ReturnType<typeof t.stringValue>
703
- | ReturnType<typeof t.intValue>
704
- | ReturnType<typeof t.floatValue>
705
- | ReturnType<typeof t.booleanValue>
706
- | ReturnType<typeof t.listValue>
707
- | ReturnType<typeof t.objectValue>
708
- | ReturnType<typeof t.nullValue>
709
- | EnumValueNode {
710
- if (value === null) {
711
- return t.nullValue();
712
- }
713
-
714
- if (typeof value === 'boolean') {
715
- return t.booleanValue({ value });
716
- }
717
-
718
- if (typeof value === 'number') {
719
- return Number.isInteger(value)
720
- ? t.intValue({ value: value.toString() })
721
- : t.floatValue({ value: value.toString() });
722
- }
723
-
724
- if (typeof value === 'string') {
725
- return t.stringValue({ value });
726
- }
727
-
728
- if (Array.isArray(value)) {
729
- return t.listValue({
730
- values: value.map((item) => buildValueAst(item)),
731
- });
732
- }
733
-
734
- if (typeof value === 'object' && value !== null) {
735
- const obj = value as Record<string, unknown>;
736
- return t.objectValue({
737
- fields: Object.entries(obj).map(([key, val]) =>
738
- t.objectField({
739
- name: key,
740
- value: buildValueAst(val),
741
- })
742
- ),
743
- });
744
- }
745
-
746
- throw new Error('Unsupported value type: ' + typeof value);
747
- }
748
- `;
168
+ */`;
169
+ sourceContent = sourceContent.replace(headerComment, generatedHeader);
749
170
  return {
750
171
  fileName: 'query-builder.ts',
751
- content,
172
+ content: sourceContent,
752
173
  };
753
174
  }
754
175
  /**
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ORM Client - Type stub for compile-time type checking
3
+ *
4
+ * This is a stub file that provides type definitions for query-builder.ts
5
+ * during compilation. The actual client.ts is generated at codegen time
6
+ * from the generateOrmClientFile() function in client-generator.ts.
7
+ *
8
+ * @internal This file is NOT part of the generated output
9
+ */
10
+ export interface OrmClientConfig {
11
+ endpoint: string;
12
+ headers?: Record<string, string>;
13
+ }
14
+ export interface GraphQLError {
15
+ message: string;
16
+ locations?: {
17
+ line: number;
18
+ column: number;
19
+ }[];
20
+ path?: (string | number)[];
21
+ extensions?: Record<string, unknown>;
22
+ }
23
+ export declare class GraphQLRequestError extends Error {
24
+ readonly errors: GraphQLError[];
25
+ readonly data: unknown;
26
+ constructor(errors: GraphQLError[], data?: unknown);
27
+ }
28
+ export type QueryResult<T> = {
29
+ ok: true;
30
+ data: T;
31
+ errors: undefined;
32
+ } | {
33
+ ok: false;
34
+ data: null;
35
+ errors: GraphQLError[];
36
+ };
37
+ export declare class OrmClient {
38
+ private endpoint;
39
+ private headers;
40
+ constructor(config: OrmClientConfig);
41
+ execute<T>(document: string, variables?: Record<string, unknown>): Promise<QueryResult<T>>;
42
+ setHeaders(headers: Record<string, string>): void;
43
+ getEndpoint(): string;
44
+ }