@soda-gql/builder 0.9.0 → 0.10.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.
package/dist/index.cjs CHANGED
@@ -42,6 +42,7 @@ __soda_gql_core_runtime = __toESM(__soda_gql_core_runtime);
42
42
  let __soda_gql_runtime = require("@soda-gql/runtime");
43
43
  __soda_gql_runtime = __toESM(__soda_gql_runtime);
44
44
  let __swc_core = require("@swc/core");
45
+ let graphql = require("graphql");
45
46
  let typescript = require("typescript");
46
47
  typescript = __toESM(typescript);
47
48
  let fast_glob = require("fast-glob");
@@ -63,6 +64,7 @@ const BuilderArtifactOperationSchema = zod.z.object({
63
64
  "subscription"
64
65
  ]),
65
66
  operationName: zod.z.string(),
67
+ schemaLabel: zod.z.string(),
66
68
  document: zod.z.unknown(),
67
69
  variableNames: zod.z.array(zod.z.string())
68
70
  })
@@ -71,7 +73,11 @@ const BuilderArtifactFragmentSchema = zod.z.object({
71
73
  id: zod.z.string(),
72
74
  type: zod.z.literal("fragment"),
73
75
  metadata: BuilderArtifactElementMetadataSchema,
74
- prebuild: zod.z.object({ typename: zod.z.string() })
76
+ prebuild: zod.z.object({
77
+ typename: zod.z.string(),
78
+ key: zod.z.string().optional(),
79
+ schemaLabel: zod.z.string()
80
+ })
75
81
  });
76
82
  const BuilderArtifactElementSchema = zod.z.discriminatedUnion("type", [BuilderArtifactOperationSchema, BuilderArtifactFragmentSchema]);
77
83
  const BuilderArtifactMetaSchema = zod.z.object({
@@ -387,6 +393,12 @@ const builderErrors = {
387
393
  message: `Internal invariant violated: ${message}`,
388
394
  context,
389
395
  cause
396
+ }),
397
+ schemaNotFound: (schemaLabel, canonicalId) => ({
398
+ code: "SCHEMA_NOT_FOUND",
399
+ message: `Schema not found for label "${schemaLabel}" (element: ${canonicalId})`,
400
+ schemaLabel,
401
+ canonicalId
390
402
  })
391
403
  };
392
404
  /**
@@ -473,6 +485,10 @@ const formatBuilderError = (error) => {
473
485
  lines.push(` Context: ${error.context}`);
474
486
  }
475
487
  break;
488
+ case "SCHEMA_NOT_FOUND":
489
+ lines.push(` Schema label: ${error.schemaLabel}`);
490
+ lines.push(` Element: ${error.canonicalId}`);
491
+ break;
476
492
  }
477
493
  if ("cause" in error && error.cause && !["CONFIG_INVALID"].includes(error.code)) {
478
494
  lines.push(` Caused by: ${error.cause}`);
@@ -1378,6 +1394,363 @@ const createGraphqlSystemIdentifyHelper = (config) => {
1378
1394
  };
1379
1395
  };
1380
1396
 
1397
+ //#endregion
1398
+ //#region packages/builder/src/prebuilt/emitter.ts
1399
+ /**
1400
+ * Prebuilt types emitter.
1401
+ *
1402
+ * Generates TypeScript type definitions for PrebuiltTypes registry
1403
+ * from field selection data and schema.
1404
+ *
1405
+ * ## Error Handling Strategy
1406
+ *
1407
+ * The emitter uses a partial failure approach for type calculation errors:
1408
+ *
1409
+ * **Recoverable errors** (result in warnings, element skipped):
1410
+ * - Type calculation failures (e.g., `calculateFieldsType` throws)
1411
+ * - Input type generation failures (e.g., `generateInputType` throws)
1412
+ * - These are caught per-element, logged as warnings, and the element is omitted
1413
+ *
1414
+ * **Fatal errors** (result in error result):
1415
+ * - `SCHEMA_NOT_FOUND`: Selection references non-existent schema
1416
+ * - `WRITE_FAILED`: Cannot write output file to disk
1417
+ *
1418
+ * This allows builds to succeed with partial type coverage when some elements
1419
+ * have issues, while providing visibility into problems via warnings.
1420
+ *
1421
+ * @module
1422
+ */
1423
+ /**
1424
+ * Group field selections by schema.
1425
+ * Uses the schemaLabel from each selection to group them correctly.
1426
+ *
1427
+ * @returns Result containing grouped selections and warnings, or error if schema not found
1428
+ */
1429
+ const groupBySchema = (fieldSelections, schemas) => {
1430
+ const grouped = new Map();
1431
+ const warnings = [];
1432
+ for (const schemaName of Object.keys(schemas)) {
1433
+ grouped.set(schemaName, {
1434
+ fragments: [],
1435
+ operations: [],
1436
+ inputObjects: new Set()
1437
+ });
1438
+ }
1439
+ for (const [canonicalId, selection] of fieldSelections) {
1440
+ const schemaName = selection.schemaLabel;
1441
+ const schema = schemas[schemaName];
1442
+ const group = grouped.get(schemaName);
1443
+ if (!schema || !group) {
1444
+ return (0, neverthrow.err)(builderErrors.schemaNotFound(schemaName, canonicalId));
1445
+ }
1446
+ const outputFormatters = { scalarOutput: (name) => `ScalarOutput_${schemaName}<"${name}">` };
1447
+ const inputFormatters = {
1448
+ scalarInput: (name) => `ScalarInput_${schemaName}<"${name}">`,
1449
+ inputObject: (name) => `Input_${schemaName}_${name}`
1450
+ };
1451
+ if (selection.type === "fragment") {
1452
+ if (!selection.key) {
1453
+ continue;
1454
+ }
1455
+ try {
1456
+ const usedInputObjects = collectUsedInputObjectsFromSpecifiers(schema, selection.variableDefinitions);
1457
+ for (const inputName of usedInputObjects) {
1458
+ group.inputObjects.add(inputName);
1459
+ }
1460
+ const outputType = (0, __soda_gql_core.calculateFieldsType)(schema, selection.fields, outputFormatters);
1461
+ const hasVariables = Object.keys(selection.variableDefinitions).length > 0;
1462
+ const inputType = hasVariables ? (0, __soda_gql_core.generateInputTypeFromSpecifiers)(schema, selection.variableDefinitions, { formatters: inputFormatters }) : "void";
1463
+ group.fragments.push({
1464
+ key: selection.key,
1465
+ inputType,
1466
+ outputType
1467
+ });
1468
+ } catch (error) {
1469
+ warnings.push(`[prebuilt] Failed to calculate type for fragment "${selection.key}": ${error instanceof Error ? error.message : String(error)}`);
1470
+ }
1471
+ } else if (selection.type === "operation") {
1472
+ try {
1473
+ const usedInputObjects = collectUsedInputObjects(schema, selection.variableDefinitions);
1474
+ for (const inputName of usedInputObjects) {
1475
+ group.inputObjects.add(inputName);
1476
+ }
1477
+ const outputType = (0, __soda_gql_core.calculateFieldsType)(schema, selection.fields, outputFormatters);
1478
+ const inputType = (0, __soda_gql_core.generateInputType)(schema, selection.variableDefinitions, inputFormatters);
1479
+ group.operations.push({
1480
+ key: selection.operationName,
1481
+ inputType,
1482
+ outputType
1483
+ });
1484
+ } catch (error) {
1485
+ warnings.push(`[prebuilt] Failed to calculate type for operation "${selection.operationName}": ${error instanceof Error ? error.message : String(error)}`);
1486
+ }
1487
+ }
1488
+ }
1489
+ return (0, neverthrow.ok)({
1490
+ grouped,
1491
+ warnings
1492
+ });
1493
+ };
1494
+ /**
1495
+ * Calculate relative import path from one file to another.
1496
+ */
1497
+ const toImportSpecifier = (from, to) => {
1498
+ const fromDir = (0, node_path.dirname)(from);
1499
+ let relativePath = (0, node_path.relative)(fromDir, to);
1500
+ if (!relativePath.startsWith(".")) {
1501
+ relativePath = `./${relativePath}`;
1502
+ }
1503
+ return relativePath.replace(/\.ts$/, "");
1504
+ };
1505
+ /**
1506
+ * Extract input object names from a GraphQL TypeNode.
1507
+ */
1508
+ const extractInputObjectsFromType = (schema, typeNode, inputObjects) => {
1509
+ switch (typeNode.kind) {
1510
+ case graphql.Kind.NON_NULL_TYPE:
1511
+ extractInputObjectsFromType(schema, typeNode.type, inputObjects);
1512
+ break;
1513
+ case graphql.Kind.LIST_TYPE:
1514
+ extractInputObjectsFromType(schema, typeNode.type, inputObjects);
1515
+ break;
1516
+ case graphql.Kind.NAMED_TYPE: {
1517
+ const name = typeNode.name.value;
1518
+ if (!schema.scalar[name] && !schema.enum[name] && schema.input[name]) {
1519
+ inputObjects.add(name);
1520
+ }
1521
+ break;
1522
+ }
1523
+ }
1524
+ };
1525
+ /**
1526
+ * Recursively collect nested input objects from schema definitions.
1527
+ * Takes a set of initial input names and expands to include all nested inputs.
1528
+ */
1529
+ const collectNestedInputObjects = (schema, initialInputNames) => {
1530
+ const inputObjects = new Set(initialInputNames);
1531
+ const collectNested = (inputName, seen) => {
1532
+ if (seen.has(inputName)) {
1533
+ return;
1534
+ }
1535
+ seen.add(inputName);
1536
+ const inputDef = schema.input[inputName];
1537
+ if (!inputDef) {
1538
+ return;
1539
+ }
1540
+ for (const field of Object.values(inputDef.fields)) {
1541
+ if (field.kind === "input" && !inputObjects.has(field.name)) {
1542
+ inputObjects.add(field.name);
1543
+ collectNested(field.name, seen);
1544
+ }
1545
+ }
1546
+ };
1547
+ for (const inputName of Array.from(initialInputNames)) {
1548
+ collectNested(inputName, new Set());
1549
+ }
1550
+ return inputObjects;
1551
+ };
1552
+ /**
1553
+ * Collect all input object types used in variable definitions.
1554
+ * Recursively collects nested input objects from the schema.
1555
+ */
1556
+ const collectUsedInputObjects = (schema, variableDefinitions) => {
1557
+ const directInputs = new Set();
1558
+ for (const varDef of variableDefinitions) {
1559
+ extractInputObjectsFromType(schema, varDef.type, directInputs);
1560
+ }
1561
+ return collectNestedInputObjects(schema, directInputs);
1562
+ };
1563
+ /**
1564
+ * Collect all input object types used in InputTypeSpecifiers.
1565
+ * Recursively collects nested input objects from the schema.
1566
+ */
1567
+ const collectUsedInputObjectsFromSpecifiers = (schema, specifiers) => {
1568
+ const directInputs = new Set();
1569
+ for (const specifier of Object.values(specifiers)) {
1570
+ if (specifier.kind === "input" && schema.input[specifier.name]) {
1571
+ directInputs.add(specifier.name);
1572
+ }
1573
+ }
1574
+ return collectNestedInputObjects(schema, directInputs);
1575
+ };
1576
+ /**
1577
+ * Generate type definitions for input objects.
1578
+ */
1579
+ const generateInputObjectTypeDefinitions = (schema, schemaName, inputNames) => {
1580
+ const lines = [];
1581
+ const defaultDepth = schema.__defaultInputDepth ?? 3;
1582
+ const depthOverrides = schema.__inputDepthOverrides ?? {};
1583
+ const formatters = {
1584
+ scalarInput: (name) => `ScalarInput_${schemaName}<"${name}">`,
1585
+ inputObject: (name) => `Input_${schemaName}_${name}`
1586
+ };
1587
+ const sortedNames = Array.from(inputNames).sort();
1588
+ for (const inputName of sortedNames) {
1589
+ const typeString = (0, __soda_gql_core.generateInputObjectType)(schema, inputName, {
1590
+ defaultDepth,
1591
+ depthOverrides,
1592
+ formatters
1593
+ });
1594
+ lines.push(`type Input_${schemaName}_${inputName} = ${typeString};`);
1595
+ }
1596
+ return lines;
1597
+ };
1598
+ /**
1599
+ * Generate the TypeScript code for prebuilt types.
1600
+ */
1601
+ const generateTypesCode = (grouped, schemas, injects, outdir) => {
1602
+ const typesFilePath = (0, node_path.join)(outdir, "prebuilt", "types.ts");
1603
+ const lines = [
1604
+ "/**",
1605
+ " * Prebuilt type registry.",
1606
+ " *",
1607
+ " * This file is auto-generated by @soda-gql/builder.",
1608
+ " * Do not edit manually.",
1609
+ " *",
1610
+ " * @module",
1611
+ " * @generated",
1612
+ " */",
1613
+ "",
1614
+ "import type { PrebuiltTypeRegistry } from \"@soda-gql/core\";"
1615
+ ];
1616
+ for (const [schemaName, inject] of Object.entries(injects)) {
1617
+ const relativePath = toImportSpecifier(typesFilePath, inject.scalars);
1618
+ lines.push(`import type { scalar as scalar_${schemaName} } from "${relativePath}";`);
1619
+ }
1620
+ lines.push("");
1621
+ for (const schemaName of Object.keys(injects)) {
1622
+ lines.push(`type ScalarInput_${schemaName}<T extends keyof typeof scalar_${schemaName}> = ` + `typeof scalar_${schemaName}[T]["$type"]["input"];`);
1623
+ lines.push(`type ScalarOutput_${schemaName}<T extends keyof typeof scalar_${schemaName}> = ` + `typeof scalar_${schemaName}[T]["$type"]["output"];`);
1624
+ }
1625
+ lines.push("");
1626
+ for (const [schemaName, { fragments, operations, inputObjects }] of grouped) {
1627
+ const schema = schemas[schemaName];
1628
+ if (inputObjects.size > 0 && schema) {
1629
+ lines.push("// Input object types");
1630
+ const inputTypeLines = generateInputObjectTypeDefinitions(schema, schemaName, inputObjects);
1631
+ lines.push(...inputTypeLines);
1632
+ lines.push("");
1633
+ }
1634
+ const fragmentEntries = fragments.sort((a, b) => a.key.localeCompare(b.key)).map((f) => ` readonly "${f.key}": { readonly input: ${f.inputType}; readonly output: ${f.outputType} };`);
1635
+ const operationEntries = operations.sort((a, b) => a.key.localeCompare(b.key)).map((o) => ` readonly "${o.key}": { readonly input: ${o.inputType}; readonly output: ${o.outputType} };`);
1636
+ lines.push(`export type PrebuiltTypes_${schemaName} = {`);
1637
+ lines.push(" readonly fragments: {");
1638
+ if (fragmentEntries.length > 0) {
1639
+ lines.push(...fragmentEntries);
1640
+ }
1641
+ lines.push(" };");
1642
+ lines.push(" readonly operations: {");
1643
+ if (operationEntries.length > 0) {
1644
+ lines.push(...operationEntries);
1645
+ }
1646
+ lines.push(" };");
1647
+ lines.push("} satisfies PrebuiltTypeRegistry;");
1648
+ lines.push("");
1649
+ }
1650
+ return lines.join("\n");
1651
+ };
1652
+ /**
1653
+ * Emit prebuilt types to the prebuilt/types.ts file.
1654
+ *
1655
+ * This function uses a partial failure strategy: if type calculation fails for
1656
+ * individual elements (e.g., due to invalid field selections or missing schema
1657
+ * types), those elements are skipped and warnings are collected rather than
1658
+ * failing the entire emission. This allows builds to succeed even when some
1659
+ * elements have issues, while still reporting problems via warnings.
1660
+ *
1661
+ * @param options - Emitter options including schemas, field selections, and output directory
1662
+ * @returns Result containing output path and warnings, or error if a hard failure occurs
1663
+ *
1664
+ * @example
1665
+ * ```typescript
1666
+ * const result = await emitPrebuiltTypes({
1667
+ * schemas: { mySchema: schema },
1668
+ * fieldSelections,
1669
+ * outdir: "./generated",
1670
+ * injects: { mySchema: { scalars: "./scalars.ts" } },
1671
+ * });
1672
+ *
1673
+ * if (result.isOk()) {
1674
+ * console.log(`Generated: ${result.value.path}`);
1675
+ * if (result.value.warnings.length > 0) {
1676
+ * console.warn("Warnings:", result.value.warnings);
1677
+ * }
1678
+ * }
1679
+ * ```
1680
+ */
1681
+ const emitPrebuiltTypes = async (options) => {
1682
+ const { schemas, fieldSelections, outdir, injects } = options;
1683
+ const groupResult = groupBySchema(fieldSelections, schemas);
1684
+ if (groupResult.isErr()) {
1685
+ return (0, neverthrow.err)(groupResult.error);
1686
+ }
1687
+ const { grouped, warnings } = groupResult.value;
1688
+ const code = generateTypesCode(grouped, schemas, injects, outdir);
1689
+ const typesPath = (0, node_path.join)(outdir, "prebuilt", "types.ts");
1690
+ try {
1691
+ await (0, node_fs_promises.writeFile)(typesPath, code, "utf-8");
1692
+ return (0, neverthrow.ok)({
1693
+ path: typesPath,
1694
+ warnings
1695
+ });
1696
+ } catch (error) {
1697
+ return (0, neverthrow.err)(builderErrors.writeFailed(typesPath, `Failed to write prebuilt types: ${error instanceof Error ? error.message : String(error)}`, error));
1698
+ }
1699
+ };
1700
+
1701
+ //#endregion
1702
+ //#region packages/builder/src/prebuilt/extractor.ts
1703
+ /**
1704
+ * Extract field selections from evaluated intermediate elements.
1705
+ *
1706
+ * For fragments, calls `spread()` with empty/default variables to get field selections.
1707
+ * For operations, calls `documentSource()` to get field selections.
1708
+ *
1709
+ * @param elements - Record of canonical ID to intermediate artifact element
1710
+ * @returns Object containing selections map and any warnings encountered
1711
+ */
1712
+ const extractFieldSelections = (elements) => {
1713
+ const selections = new Map();
1714
+ const warnings = [];
1715
+ for (const [id, element] of Object.entries(elements)) {
1716
+ const canonicalId = id;
1717
+ try {
1718
+ if (element.type === "fragment") {
1719
+ const variableDefinitions = element.element.variableDefinitions;
1720
+ const varRefs = Object.fromEntries(Object.keys(variableDefinitions).map((k) => [k, (0, __soda_gql_core.createVarRefFromVariable)(k)]));
1721
+ const fields = element.element.spread(varRefs);
1722
+ selections.set(canonicalId, {
1723
+ type: "fragment",
1724
+ schemaLabel: element.element.schemaLabel,
1725
+ key: element.element.key,
1726
+ typename: element.element.typename,
1727
+ fields,
1728
+ variableDefinitions
1729
+ });
1730
+ } else if (element.type === "operation") {
1731
+ const fields = element.element.documentSource();
1732
+ const document = element.element.document;
1733
+ const operationDef = document.definitions.find((def) => def.kind === graphql.Kind.OPERATION_DEFINITION);
1734
+ const variableDefinitions = operationDef?.variableDefinitions ?? [];
1735
+ selections.set(canonicalId, {
1736
+ type: "operation",
1737
+ schemaLabel: element.element.schemaLabel,
1738
+ operationName: element.element.operationName,
1739
+ operationType: element.element.operationType,
1740
+ fields,
1741
+ variableDefinitions
1742
+ });
1743
+ }
1744
+ } catch (error) {
1745
+ warnings.push(`[prebuilt] Failed to extract field selections for ${canonicalId}: ${error instanceof Error ? error.message : String(error)}`);
1746
+ }
1747
+ }
1748
+ return {
1749
+ selections,
1750
+ warnings
1751
+ };
1752
+ };
1753
+
1381
1754
  //#endregion
1382
1755
  //#region packages/builder/src/artifact/aggregate.ts
1383
1756
  const canonicalToFilePath$1 = (canonicalId) => canonicalId.split("::")[0] ?? canonicalId;
@@ -1410,7 +1783,11 @@ const aggregate = ({ analyses, elements }) => {
1410
1783
  contentHash: ""
1411
1784
  };
1412
1785
  if (element.type === "fragment") {
1413
- const prebuild = { typename: element.element.typename };
1786
+ const prebuild = {
1787
+ typename: element.element.typename,
1788
+ key: element.element.key,
1789
+ schemaLabel: element.element.schemaLabel
1790
+ };
1414
1791
  registry.set(definition.canonicalId, {
1415
1792
  id: definition.canonicalId,
1416
1793
  type: "fragment",
@@ -1426,6 +1803,7 @@ const aggregate = ({ analyses, elements }) => {
1426
1803
  const prebuild = {
1427
1804
  operationType: element.element.operationType,
1428
1805
  operationName: element.element.operationName,
1806
+ schemaLabel: element.element.schemaLabel,
1429
1807
  document: element.element.document,
1430
1808
  variableNames: element.element.variableNames,
1431
1809
  metadata: element.element.metadata
@@ -3662,6 +4040,8 @@ exports.collectAffectedFiles = collectAffectedFiles;
3662
4040
  exports.createBuilderService = createBuilderService;
3663
4041
  exports.createBuilderSession = createBuilderSession;
3664
4042
  exports.createGraphqlSystemIdentifyHelper = createGraphqlSystemIdentifyHelper;
4043
+ exports.emitPrebuiltTypes = emitPrebuiltTypes;
4044
+ exports.extractFieldSelections = extractFieldSelections;
3665
4045
  exports.extractModuleAdjacency = extractModuleAdjacency;
3666
4046
  exports.formatBuilderErrorForCLI = formatBuilderErrorForCLI;
3667
4047
  exports.formatBuilderErrorStructured = formatBuilderErrorStructured;