@soda-gql/codegen 0.11.16 → 0.11.18

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.
package/dist/index.cjs CHANGED
@@ -48,6 +48,7 @@ const emitOperation = (operation, options) => {
48
48
  const emitFragment = (fragment, options) => {
49
49
  const lines = [];
50
50
  const schema = options.schemaDocument ? require_generator.createSchemaIndex(options.schemaDocument) : null;
51
+ const hasVariables = fragment.variables.length > 0;
51
52
  lines.push(`import { gql } from "${options.graphqlSystemPath}";`);
52
53
  if (fragment.fragmentDependencies.length > 0 && options.fragmentImports) {
53
54
  for (const fragName of fragment.fragmentDependencies) {
@@ -59,10 +60,15 @@ const emitFragment = (fragment, options) => {
59
60
  }
60
61
  lines.push("");
61
62
  const exportName = `${fragment.name}Fragment`;
62
- lines.push(`export const ${exportName} = gql.${options.schemaName}(({ fragment }) =>`);
63
+ const destructure = hasVariables ? "fragment, $var" : "fragment";
64
+ lines.push(`export const ${exportName} = gql.${options.schemaName}(({ ${destructure} }) =>`);
63
65
  lines.push(` fragment.${fragment.onType}({`);
64
- lines.push(` fields: ({ f }) => ({`);
65
- const fieldLinesResult = emitSelections(fragment.selections, 3, [], schema);
66
+ if (hasVariables) {
67
+ lines.push(` variables: { ${emitVariables(fragment.variables)} },`);
68
+ }
69
+ const fieldsContext = hasVariables ? "{ f, $ }" : "{ f }";
70
+ lines.push(` fields: (${fieldsContext}) => ({`);
71
+ const fieldLinesResult = emitSelections(fragment.selections, 3, fragment.variables, schema);
66
72
  if (fieldLinesResult.isErr()) {
67
73
  return (0, neverthrow.err)(fieldLinesResult.error);
68
74
  }
@@ -80,7 +86,7 @@ const emitVariables = (variables) => {
80
86
  };
81
87
  /**
82
88
  * Emit field selections (public API).
83
- * Converts EnrichedVariable[] to Set<string> and delegates to internal implementation.
89
+ * Converts variable array to Set<string> and delegates to internal implementation.
84
90
  */
85
91
  const emitSelections = (selections, indent, variables, schema) => {
86
92
  const variableNames = new Set(variables.map((v) => v.name));
@@ -556,36 +562,399 @@ const builtinScalarTypes = new Set([
556
562
  "Boolean"
557
563
  ]);
558
564
  /**
565
+ * Parse a modifier string into its structural components.
566
+ * @param modifier - Modifier string like "!", "?", "![]!", "?[]?[]!"
567
+ * @returns Parsed structure with inner nullability and list modifiers
568
+ */
569
+ const parseModifierStructure = (modifier) => {
570
+ const inner = modifier[0] === "!" ? "!" : "?";
571
+ const lists = [];
572
+ const listPattern = /\[\]([!?])/g;
573
+ let match;
574
+ while ((match = listPattern.exec(modifier)) !== null) {
575
+ lists.push(`[]${match[1]}`);
576
+ }
577
+ return {
578
+ inner,
579
+ lists
580
+ };
581
+ };
582
+ /**
583
+ * Rebuild modifier string from structure.
584
+ */
585
+ const buildModifier = (structure) => {
586
+ return structure.inner + structure.lists.join("");
587
+ };
588
+ /**
589
+ * Merge two modifiers by taking the stricter constraint at each level.
590
+ * - Non-null (!) is stricter than nullable (?)
591
+ * - List depths must match
592
+ *
593
+ * @param a - First modifier
594
+ * @param b - Second modifier
595
+ * @returns Merged modifier or error if incompatible
596
+ */
597
+ const mergeModifiers = (a, b) => {
598
+ const structA = parseModifierStructure(a);
599
+ const structB = parseModifierStructure(b);
600
+ if (structA.lists.length !== structB.lists.length) {
601
+ return {
602
+ ok: false,
603
+ reason: `Incompatible list depths: "${a}" has ${structA.lists.length} list level(s), "${b}" has ${structB.lists.length}`
604
+ };
605
+ }
606
+ const mergedInner = structA.inner === "!" || structB.inner === "!" ? "!" : "?";
607
+ const mergedLists = [];
608
+ for (let i = 0; i < structA.lists.length; i++) {
609
+ const listA = structA.lists[i];
610
+ const listB = structB.lists[i];
611
+ mergedLists.push(listA === "[]!" || listB === "[]!" ? "[]!" : "[]?");
612
+ }
613
+ return {
614
+ ok: true,
615
+ value: buildModifier({
616
+ inner: mergedInner,
617
+ lists: mergedLists
618
+ })
619
+ };
620
+ };
621
+ /**
622
+ * Get the expected type for a field argument from the schema.
623
+ * Returns null if the field or argument is not found.
624
+ */
625
+ const getArgumentType = (schema, parentTypeName, fieldName, argumentName) => {
626
+ const objectRecord = schema.objects.get(parentTypeName);
627
+ if (!objectRecord) return null;
628
+ const fieldDef = objectRecord.fields.get(fieldName);
629
+ if (!fieldDef) return null;
630
+ const argDef = fieldDef.arguments?.find((arg) => arg.name.value === argumentName);
631
+ if (!argDef) return null;
632
+ return parseTypeNode(argDef.type);
633
+ };
634
+ /**
635
+ * Get the expected type for an input object field from the schema.
636
+ */
637
+ const getInputFieldType = (schema, inputTypeName, fieldName) => {
638
+ const inputRecord = schema.inputs.get(inputTypeName);
639
+ if (!inputRecord) return null;
640
+ const fieldDef = inputRecord.fields.get(fieldName);
641
+ if (!fieldDef) return null;
642
+ return parseTypeNode(fieldDef.type);
643
+ };
644
+ /**
645
+ * Resolve the type kind for a type name.
646
+ */
647
+ const resolveTypeKindFromName = (schema, typeName) => {
648
+ if (isScalarName(schema, typeName)) return "scalar";
649
+ if (isEnumName(schema, typeName)) return "enum";
650
+ if (schema.inputs.has(typeName)) return "input";
651
+ return null;
652
+ };
653
+ /**
654
+ * Extract variable usages from a parsed value, given the expected type.
655
+ * Handles nested input objects recursively.
656
+ */
657
+ const collectVariablesFromValue = (value, expectedTypeName, expectedModifier, schema, usages) => {
658
+ if (value.kind === "variable") {
659
+ const typeKind = resolveTypeKindFromName(schema, expectedTypeName);
660
+ if (!typeKind) {
661
+ return {
662
+ code: "GRAPHQL_UNKNOWN_TYPE",
663
+ message: `Unknown type "${expectedTypeName}" for variable "$${value.name}"`,
664
+ typeName: expectedTypeName
665
+ };
666
+ }
667
+ usages.push({
668
+ name: value.name,
669
+ typeName: expectedTypeName,
670
+ modifier: expectedModifier,
671
+ typeKind
672
+ });
673
+ return null;
674
+ }
675
+ if (value.kind === "object") {
676
+ for (const field of value.fields) {
677
+ const fieldType = getInputFieldType(schema, expectedTypeName, field.name);
678
+ if (!fieldType) {
679
+ return {
680
+ code: "GRAPHQL_UNKNOWN_FIELD",
681
+ message: `Unknown field "${field.name}" on input type "${expectedTypeName}"`,
682
+ typeName: expectedTypeName,
683
+ fieldName: field.name
684
+ };
685
+ }
686
+ const error = collectVariablesFromValue(field.value, fieldType.typeName, fieldType.modifier, schema, usages);
687
+ if (error) return error;
688
+ }
689
+ return null;
690
+ }
691
+ if (value.kind === "list") {
692
+ const struct = parseModifierStructure(expectedModifier);
693
+ if (struct.lists.length > 0) {
694
+ const innerModifier = buildModifier({
695
+ inner: struct.inner,
696
+ lists: struct.lists.slice(1)
697
+ });
698
+ for (const item of value.values) {
699
+ const error = collectVariablesFromValue(item, expectedTypeName, innerModifier, schema, usages);
700
+ if (error) return error;
701
+ }
702
+ }
703
+ }
704
+ return null;
705
+ };
706
+ /**
707
+ * Collect variable usages from field arguments.
708
+ */
709
+ const collectVariablesFromArguments = (args, parentTypeName, fieldName, schema, usages) => {
710
+ for (const arg of args) {
711
+ const argType = getArgumentType(schema, parentTypeName, fieldName, arg.name);
712
+ if (!argType) {
713
+ return {
714
+ code: "GRAPHQL_UNKNOWN_ARGUMENT",
715
+ message: `Unknown argument "${arg.name}" on field "${fieldName}"`,
716
+ fieldName,
717
+ argumentName: arg.name
718
+ };
719
+ }
720
+ const error = collectVariablesFromValue(arg.value, argType.typeName, argType.modifier, schema, usages);
721
+ if (error) return error;
722
+ }
723
+ return null;
724
+ };
725
+ /**
726
+ * Recursively collect all variable usages from selections.
727
+ */
728
+ const collectVariableUsages = (selections, parentTypeName, schema) => {
729
+ const usages = [];
730
+ const collect = (sels, parentType) => {
731
+ for (const sel of sels) {
732
+ switch (sel.kind) {
733
+ case "field": {
734
+ if (sel.arguments && sel.arguments.length > 0) {
735
+ const error$1 = collectVariablesFromArguments(sel.arguments, parentType, sel.name, schema, usages);
736
+ if (error$1) return error$1;
737
+ }
738
+ if (sel.selections && sel.selections.length > 0) {
739
+ const fieldReturnType = getFieldReturnType(schema, parentType, sel.name);
740
+ if (!fieldReturnType) {
741
+ return {
742
+ code: "GRAPHQL_UNKNOWN_FIELD",
743
+ message: `Unknown field "${sel.name}" on type "${parentType}"`,
744
+ typeName: parentType,
745
+ fieldName: sel.name
746
+ };
747
+ }
748
+ const error$1 = collect(sel.selections, fieldReturnType);
749
+ if (error$1) return error$1;
750
+ }
751
+ break;
752
+ }
753
+ case "inlineFragment": {
754
+ const error$1 = collect(sel.selections, sel.onType);
755
+ if (error$1) return error$1;
756
+ break;
757
+ }
758
+ case "fragmentSpread": break;
759
+ }
760
+ }
761
+ return null;
762
+ };
763
+ const error = collect(selections, parentTypeName);
764
+ if (error) return (0, neverthrow.err)(error);
765
+ return (0, neverthrow.ok)(usages);
766
+ };
767
+ /**
768
+ * Get the return type of a field (unwrapped from modifiers).
769
+ */
770
+ const getFieldReturnType = (schema, parentTypeName, fieldName) => {
771
+ const objectRecord = schema.objects.get(parentTypeName);
772
+ if (!objectRecord) return null;
773
+ const fieldDef = objectRecord.fields.get(fieldName);
774
+ if (!fieldDef) return null;
775
+ const { typeName } = parseTypeNode(fieldDef.type);
776
+ return typeName;
777
+ };
778
+ /**
779
+ * Merge multiple variable usages into a single InferredVariable.
780
+ * Validates type compatibility and merges modifiers using stricter constraint.
781
+ */
782
+ const mergeVariableUsages = (variableName, usages) => {
783
+ if (usages.length === 0) {
784
+ return (0, neverthrow.err)({
785
+ code: "GRAPHQL_UNDECLARED_VARIABLE",
786
+ message: `No usages found for variable "${variableName}"`,
787
+ variableName
788
+ });
789
+ }
790
+ const first = usages[0];
791
+ for (const usage of usages) {
792
+ if (usage.typeName !== first.typeName) {
793
+ return (0, neverthrow.err)({
794
+ code: "GRAPHQL_VARIABLE_TYPE_MISMATCH",
795
+ message: `Variable "$${variableName}" has conflicting types: "${first.typeName}" and "${usage.typeName}"`,
796
+ variableName
797
+ });
798
+ }
799
+ }
800
+ let mergedModifier = first.modifier;
801
+ for (let i = 1; i < usages.length; i++) {
802
+ const result = mergeModifiers(mergedModifier, usages[i].modifier);
803
+ if (!result.ok) {
804
+ return (0, neverthrow.err)({
805
+ code: "GRAPHQL_VARIABLE_MODIFIER_INCOMPATIBLE",
806
+ message: `Variable "$${variableName}" has incompatible modifiers: ${result.reason}`,
807
+ variableName
808
+ });
809
+ }
810
+ mergedModifier = result.value;
811
+ }
812
+ return (0, neverthrow.ok)({
813
+ name: variableName,
814
+ typeName: first.typeName,
815
+ modifier: mergedModifier,
816
+ typeKind: first.typeKind
817
+ });
818
+ };
819
+ /**
820
+ * Infer variables from collected usages.
821
+ * Groups by variable name and merges each group.
822
+ */
823
+ const inferVariablesFromUsages = (usages) => {
824
+ const byName = new Map();
825
+ for (const usage of usages) {
826
+ const existing = byName.get(usage.name);
827
+ if (existing) {
828
+ existing.push(usage);
829
+ } else {
830
+ byName.set(usage.name, [usage]);
831
+ }
832
+ }
833
+ const variables = [];
834
+ for (const [name, group] of byName) {
835
+ const result = mergeVariableUsages(name, group);
836
+ if (result.isErr()) return (0, neverthrow.err)(result.error);
837
+ variables.push(result.value);
838
+ }
839
+ variables.sort((a, b) => a.name.localeCompare(b.name));
840
+ return (0, neverthrow.ok)(variables);
841
+ };
842
+ /**
559
843
  * Check if a type name is a scalar type.
560
844
  */
561
845
  const isScalarName = (schema, name) => builtinScalarTypes.has(name) || schema.scalars.has(name);
562
846
  /**
847
+ * Topologically sort fragments so dependencies come before dependents.
848
+ * Detects circular dependencies.
849
+ *
850
+ * Note: Uses the existing collectFragmentDependencies function defined below.
851
+ */
852
+ const sortFragmentsByDependency = (fragments) => {
853
+ const graph = new Map();
854
+ for (const fragment of fragments) {
855
+ const deps = collectFragmentDependenciesSet(fragment.selections);
856
+ graph.set(fragment.name, deps);
857
+ }
858
+ const fragmentByName = new Map();
859
+ for (const f of fragments) {
860
+ fragmentByName.set(f.name, f);
861
+ }
862
+ const sorted = [];
863
+ const visited = new Set();
864
+ const visiting = new Set();
865
+ const visit = (name, path) => {
866
+ if (visited.has(name)) return null;
867
+ if (visiting.has(name)) {
868
+ const cycleStart = path.indexOf(name);
869
+ const cycle = path.slice(cycleStart).concat(name);
870
+ return {
871
+ code: "GRAPHQL_FRAGMENT_CIRCULAR_DEPENDENCY",
872
+ message: `Circular dependency detected in fragments: ${cycle.join(" -> ")}`,
873
+ fragmentNames: cycle
874
+ };
875
+ }
876
+ const fragment = fragmentByName.get(name);
877
+ if (!fragment) {
878
+ visited.add(name);
879
+ return null;
880
+ }
881
+ visiting.add(name);
882
+ const deps = graph.get(name) ?? new Set();
883
+ for (const dep of deps) {
884
+ const error = visit(dep, [...path, name]);
885
+ if (error) return error;
886
+ }
887
+ visiting.delete(name);
888
+ visited.add(name);
889
+ sorted.push(fragment);
890
+ return null;
891
+ };
892
+ for (const fragment of fragments) {
893
+ const error = visit(fragment.name, []);
894
+ if (error) return (0, neverthrow.err)(error);
895
+ }
896
+ return (0, neverthrow.ok)(sorted);
897
+ };
898
+ /**
899
+ * Recursively collect fragment spread names from selections into a Set.
900
+ * Internal helper for sortFragmentsByDependency.
901
+ */
902
+ const collectFragmentDependenciesSet = (selections) => {
903
+ const deps = new Set();
904
+ const collect = (sels) => {
905
+ for (const sel of sels) {
906
+ switch (sel.kind) {
907
+ case "fragmentSpread":
908
+ deps.add(sel.name);
909
+ break;
910
+ case "field":
911
+ if (sel.selections) {
912
+ collect(sel.selections);
913
+ }
914
+ break;
915
+ case "inlineFragment":
916
+ collect(sel.selections);
917
+ break;
918
+ }
919
+ }
920
+ };
921
+ collect(selections);
922
+ return deps;
923
+ };
924
+ /**
563
925
  * Check if a type name is an enum type.
564
926
  */
565
927
  const isEnumName = (schema, name) => schema.enums.has(name);
566
928
  /**
567
929
  * Transform parsed operations/fragments by enriching them with schema information.
568
930
  *
569
- * This resolves variable type kinds (scalar, enum, input) and collects
570
- * fragment dependencies.
931
+ * This resolves variable type kinds (scalar, enum, input), collects
932
+ * fragment dependencies, and infers variables for fragments.
571
933
  */
572
934
  const transformParsedGraphql = (parsed, options) => {
573
935
  const schema = require_generator.createSchemaIndex(options.schemaDocument);
574
- const operations = [];
575
- for (const op of parsed.operations) {
576
- const result = transformOperation(op, schema);
936
+ const sortResult = sortFragmentsByDependency(parsed.fragments);
937
+ if (sortResult.isErr()) {
938
+ return (0, neverthrow.err)(sortResult.error);
939
+ }
940
+ const sortedFragments = sortResult.value;
941
+ const resolvedFragmentVariables = new Map();
942
+ const fragments = [];
943
+ for (const frag of sortedFragments) {
944
+ const result = transformFragment(frag, schema, resolvedFragmentVariables);
577
945
  if (result.isErr()) {
578
946
  return (0, neverthrow.err)(result.error);
579
947
  }
580
- operations.push(result.value);
948
+ resolvedFragmentVariables.set(frag.name, result.value.variables);
949
+ fragments.push(result.value);
581
950
  }
582
- const fragments = [];
583
- for (const frag of parsed.fragments) {
584
- const result = transformFragment(frag, schema);
951
+ const operations = [];
952
+ for (const op of parsed.operations) {
953
+ const result = transformOperation(op, schema);
585
954
  if (result.isErr()) {
586
955
  return (0, neverthrow.err)(result.error);
587
956
  }
588
- fragments.push(result.value);
957
+ operations.push(result.value);
589
958
  }
590
959
  return (0, neverthrow.ok)({
591
960
  operations,
@@ -620,12 +989,36 @@ const transformOperation = (op, schema) => {
620
989
  };
621
990
  /**
622
991
  * Transform a single fragment.
992
+ * Infers variables from field arguments and propagates variables from spread fragments.
623
993
  */
624
- const transformFragment = (frag, _schema) => {
994
+ const transformFragment = (frag, schema, resolvedFragmentVariables) => {
625
995
  const fragmentDependencies = collectFragmentDependencies(frag.selections);
996
+ const directUsagesResult = collectVariableUsages(frag.selections, frag.onType, schema);
997
+ if (directUsagesResult.isErr()) {
998
+ return (0, neverthrow.err)(directUsagesResult.error);
999
+ }
1000
+ const directUsages = directUsagesResult.value;
1001
+ const spreadVariables = [];
1002
+ for (const depName of fragmentDependencies) {
1003
+ const depVariables = resolvedFragmentVariables.get(depName);
1004
+ if (depVariables) {
1005
+ spreadVariables.push(...depVariables);
1006
+ }
1007
+ }
1008
+ const allUsages = [...directUsages, ...spreadVariables.map((v) => ({
1009
+ name: v.name,
1010
+ typeName: v.typeName,
1011
+ modifier: v.modifier,
1012
+ typeKind: v.typeKind
1013
+ }))];
1014
+ const variablesResult = inferVariablesFromUsages(allUsages);
1015
+ if (variablesResult.isErr()) {
1016
+ return (0, neverthrow.err)(variablesResult.error);
1017
+ }
626
1018
  return (0, neverthrow.ok)({
627
1019
  ...frag,
628
- fragmentDependencies
1020
+ fragmentDependencies,
1021
+ variables: variablesResult.value
629
1022
  });
630
1023
  };
631
1024
  /**
@@ -1149,14 +1542,19 @@ export * from "./_internal";
1149
1542
  };
1150
1543
 
1151
1544
  //#endregion
1545
+ exports.collectVariableUsages = collectVariableUsages;
1152
1546
  exports.emitFragment = emitFragment;
1153
1547
  exports.emitOperation = emitOperation;
1154
1548
  exports.hashSchema = hashSchema;
1549
+ exports.inferVariablesFromUsages = inferVariablesFromUsages;
1155
1550
  exports.loadSchema = loadSchema;
1551
+ exports.mergeModifiers = mergeModifiers;
1552
+ exports.mergeVariableUsages = mergeVariableUsages;
1156
1553
  exports.parseGraphqlFile = parseGraphqlFile;
1157
1554
  exports.parseGraphqlSource = parseGraphqlSource;
1158
1555
  exports.parseTypeNode = parseTypeNode;
1159
1556
  exports.runCodegen = runCodegen;
1557
+ exports.sortFragmentsByDependency = sortFragmentsByDependency;
1160
1558
  exports.transformParsedGraphql = transformParsedGraphql;
1161
1559
  exports.writeInjectTemplate = writeInjectTemplate;
1162
1560
  //# sourceMappingURL=index.cjs.map