@fabricorg/gen-graphql 4.0.6 → 4.0.7

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.js CHANGED
@@ -1,239 +1,271 @@
1
- // index.ts
2
- var pascal = (value) => value.split(/[-._/]/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join("");
3
- var identifier = (value) => {
4
- const normalized = value.replace(/[^_0-9A-Za-z]/g, "_");
5
- return /^[_A-Za-z]/.test(normalized) ? normalized : `field_${normalized}`;
1
+ //#region index.ts
2
+ const pascal = (value) => value.split(/[-._/]/).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join("");
3
+ const identifier = (value) => {
4
+ const normalized = value.replace(/[^_0-9A-Za-z]/g, "_");
5
+ return /^[_A-Za-z]/.test(normalized) ? normalized : `field_${normalized}`;
6
+ };
7
+ const SCALARS = {
8
+ string: "String",
9
+ integer: "Int",
10
+ number: "Float",
11
+ boolean: "Boolean"
6
12
  };
7
- var SCALARS = { string: "String", integer: "Int", number: "Float", boolean: "Boolean" };
8
13
  function unsupported(projection, path, reason) {
9
- throw new Error(`${projection.subject}${path} ${reason}. Give the view or object an explicit GraphQL-projectable shape.`);
14
+ throw new Error(`${projection.subject}${path} ${reason}. Give the view or object an explicit GraphQL-projectable shape.`);
10
15
  }
16
+ /** Take a unique type name before projecting children, so a recursive `$ref` back to this type resolves to the same name. */
11
17
  function reserve(projection, preferred) {
12
- let name = preferred;
13
- for (let suffix = 2; projection.types.has(name); suffix += 1) name = `${preferred}${suffix}`;
14
- projection.types.set(name, "");
15
- return name;
18
+ let name = preferred;
19
+ for (let suffix = 2; projection.types.has(name); suffix += 1) name = `${preferred}${suffix}`;
20
+ projection.types.set(name, "");
21
+ return name;
16
22
  }
23
+ /** Resolve `#/$defs/<key>` only; any other pointer is not portable across generators. */
17
24
  function resolveRef(projection, ref, path) {
18
- const match = /^#\/\$defs\/([^/]+)$/.exec(ref);
19
- if (!match) unsupported(projection, path, `uses $ref "${ref}", which is not a local "#/$defs/<name>" pointer`);
20
- const key = decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~"));
21
- const target = projection.root.$defs?.[key];
22
- if (!target) unsupported(projection, path, `references missing definition "#/$defs/${key}"`);
23
- return { schema: target, key };
25
+ const match = /^#\/\$defs\/([^/]+)$/.exec(ref);
26
+ if (!match) unsupported(projection, path, `uses $ref "${ref}", which is not a local "#/$defs/<name>" pointer`);
27
+ const key = decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~"));
28
+ const target = projection.root.$defs?.[key];
29
+ if (!target) unsupported(projection, path, `references missing definition "#/$defs/${key}"`);
30
+ return {
31
+ schema: target,
32
+ key
33
+ };
24
34
  }
35
+ /** A shape that becomes a named GraphQL object type rather than a scalar, list or JSON. */
25
36
  function isObjectShape(schema) {
26
- return schema.$ref === void 0 && (schema.type === void 0 || schema.type === "object") && !Array.isArray(schema.enum) && Object.keys(schema.properties ?? {}).length > 0;
37
+ return schema.$ref === void 0 && (schema.type === void 0 || schema.type === "object") && !Array.isArray(schema.enum) && Object.keys(schema.properties ?? {}).length > 0;
27
38
  }
39
+ /**
40
+ * Whether a value may legitimately be null, following local refs. GraphQL
41
+ * non-null is a promise to the client, so a required field whose definition
42
+ * admits null must still be nullable.
43
+ */
28
44
  function admitsNull(schema, projection, seen = /* @__PURE__ */ new Set()) {
29
- if (schema.$ref !== void 0) {
30
- const match = /^#\/\$defs\/([^/]+)$/.exec(schema.$ref);
31
- if (!match) return false;
32
- const key = decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~"));
33
- if (seen.has(key)) return false;
34
- seen.add(key);
35
- const target = projection.root.$defs?.[key];
36
- return target ? admitsNull(target, projection, seen) : false;
37
- }
38
- if (Array.isArray(schema.type)) return schema.type.includes("null");
39
- if (schema.type === "null") return true;
40
- return Array.isArray(schema.enum) ? schema.enum.includes(null) : false;
45
+ if (schema.$ref !== void 0) {
46
+ const match = /^#\/\$defs\/([^/]+)$/.exec(schema.$ref);
47
+ if (!match) return false;
48
+ const key = decodeURIComponent(match[1].replace(/~1/g, "/").replace(/~0/g, "~"));
49
+ if (seen.has(key)) return false;
50
+ seen.add(key);
51
+ const target = projection.root.$defs?.[key];
52
+ return target ? admitsNull(target, projection, seen) : false;
53
+ }
54
+ if (Array.isArray(schema.type)) return schema.type.includes("null");
55
+ if (schema.type === "null") return true;
56
+ return Array.isArray(schema.enum) ? schema.enum.includes(null) : false;
41
57
  }
42
58
  function enumType(projection, schema, preferred) {
43
- const values = schema.enum;
44
- if (!Array.isArray(values) || values.length === 0) return void 0;
45
- const members = values.filter((value) => value !== null);
46
- if (members.length === 0) return void 0;
47
- if (!members.every((value) => typeof value === "string" && /^[_A-Za-z][_0-9A-Za-z]*$/.test(value))) return void 0;
48
- const unique = [...new Set(members)];
49
- const name = reserve(projection, preferred);
50
- projection.types.set(name, `enum ${name} {
51
- ${unique.map((member) => ` ${member}`).join("\n")}
52
- }`);
53
- return name;
59
+ const values = schema.enum;
60
+ if (!Array.isArray(values) || values.length === 0) return void 0;
61
+ const members = values.filter((value) => value !== null);
62
+ if (members.length === 0) return void 0;
63
+ if (!members.every((value) => typeof value === "string" && /^[_A-Za-z][_0-9A-Za-z]*$/.test(value))) return void 0;
64
+ const unique = [...new Set(members)];
65
+ const name = reserve(projection, preferred);
66
+ projection.types.set(name, `enum ${name} {\n${unique.map((member) => ` ${member}`).join("\n")}\n}`);
67
+ return name;
54
68
  }
69
+ /** Returns a GraphQL type reference without nullability; callers append `!`. */
55
70
  function project(schema, preferred, projection, path) {
56
- for (const keyword of ["oneOf", "anyOf", "allOf", "not", "if"]) {
57
- if (schema[keyword] !== void 0) unsupported(projection, path, `uses "${keyword}", which has no GraphQL projection`);
58
- }
59
- if (schema.$ref !== void 0) {
60
- const { schema: target, key } = resolveRef(projection, schema.$ref, path);
61
- const claimed = projection.refs.get(key);
62
- if (claimed) return claimed;
63
- const preferredRefName = `${projection.rootName}${pascal(key)}`;
64
- if (isObjectShape(target)) {
65
- const name = reserve(projection, preferredRefName);
66
- projection.refs.set(key, name);
67
- projection.types.set(name, objectSdl(name, Object.entries(target.properties ?? {}), target, projection, `${path} -> $defs.${key}`));
68
- return name;
69
- }
70
- const projected = project(target, preferredRefName, projection, `${path} -> $defs.${key}`);
71
- projection.refs.set(key, projected);
72
- return projected;
73
- }
74
- const declared = Array.isArray(schema.type) ? schema.type.filter((entry) => entry !== "null") : schema.type === void 0 ? [] : [schema.type];
75
- if (Array.isArray(schema.type) && declared.length > 1) {
76
- unsupported(projection, path, `declares the union type [${schema.type.join(", ")}], which has no GraphQL projection`);
77
- }
78
- if (Array.isArray(schema.type) && declared.length === 0) {
79
- unsupported(projection, path, `declares only the null type, which has no GraphQL projection`);
80
- }
81
- const type = declared[0];
82
- if (type === "null") unsupported(projection, path, `declares only the null type, which has no GraphQL projection`);
83
- if (type === void 0 || type === "object") {
84
- const named2 = enumType(projection, schema, preferred);
85
- if (named2) return named2;
86
- const properties = Object.entries(schema.properties ?? {});
87
- if (properties.length === 0) {
88
- for (const keyword of ["patternProperties", "propertyNames"]) {
89
- if (schema[keyword] !== void 0) unsupported(projection, path, `uses "${keyword}" with no declared properties, which has no GraphQL projection`);
90
- }
91
- if (schema.additionalProperties !== void 0 && typeof schema.additionalProperties !== "boolean") {
92
- unsupported(projection, path, `constrains "additionalProperties" with no declared properties, which has no GraphQL projection`);
93
- }
94
- return "JSON";
95
- }
96
- if (schema.patternProperties !== void 0) unsupported(projection, path, `uses "patternProperties" alongside "properties", which has no GraphQL projection`);
97
- const name = reserve(projection, preferred);
98
- projection.types.set(name, objectSdl(name, properties, schema, projection, path));
99
- return name;
100
- }
101
- if (type === "array") {
102
- if (schema.prefixItems !== void 0) unsupported(projection, path, `uses "prefixItems", which has no GraphQL projection`);
103
- if (Array.isArray(schema.items)) unsupported(projection, path, `uses tuple "items", which has no GraphQL projection`);
104
- if (schema.items === void 0) unsupported(projection, path, `declares an array without "items", so its element type has no GraphQL projection`);
105
- const item = schema.items;
106
- const reference = project(item, `${preferred}Item`, projection, `${path}[]`);
107
- return `[${reference}${admitsNull(item, projection) ? "" : "!"}]`;
108
- }
109
- const named = enumType(projection, schema, preferred);
110
- if (named) return named;
111
- const scalar = SCALARS[type];
112
- if (!scalar) unsupported(projection, path, `declares unsupported type "${type}"`);
113
- return scalar;
71
+ for (const keyword of [
72
+ "oneOf",
73
+ "anyOf",
74
+ "allOf",
75
+ "not",
76
+ "if"
77
+ ]) if (schema[keyword] !== void 0) unsupported(projection, path, `uses "${keyword}", which has no GraphQL projection`);
78
+ if (schema.$ref !== void 0) {
79
+ const { schema: target, key } = resolveRef(projection, schema.$ref, path);
80
+ const claimed = projection.refs.get(key);
81
+ if (claimed) return claimed;
82
+ const preferredRefName = `${projection.rootName}${pascal(key)}`;
83
+ if (isObjectShape(target)) {
84
+ const name = reserve(projection, preferredRefName);
85
+ projection.refs.set(key, name);
86
+ projection.types.set(name, objectSdl(name, Object.entries(target.properties ?? {}), target, projection, `${path} -> $defs.${key}`));
87
+ return name;
88
+ }
89
+ const projected = project(target, preferredRefName, projection, `${path} -> $defs.${key}`);
90
+ projection.refs.set(key, projected);
91
+ return projected;
92
+ }
93
+ const declared = Array.isArray(schema.type) ? schema.type.filter((entry) => entry !== "null") : schema.type === void 0 ? [] : [schema.type];
94
+ if (Array.isArray(schema.type) && declared.length > 1) unsupported(projection, path, `declares the union type [${schema.type.join(", ")}], which has no GraphQL projection`);
95
+ if (Array.isArray(schema.type) && declared.length === 0) unsupported(projection, path, `declares only the null type, which has no GraphQL projection`);
96
+ const type = declared[0];
97
+ if (type === "null") unsupported(projection, path, `declares only the null type, which has no GraphQL projection`);
98
+ if (type === void 0 || type === "object") {
99
+ const named = enumType(projection, schema, preferred);
100
+ if (named) return named;
101
+ const properties = Object.entries(schema.properties ?? {});
102
+ if (properties.length === 0) {
103
+ for (const keyword of ["patternProperties", "propertyNames"]) if (schema[keyword] !== void 0) unsupported(projection, path, `uses "${keyword}" with no declared properties, which has no GraphQL projection`);
104
+ if (schema.additionalProperties !== void 0 && typeof schema.additionalProperties !== "boolean") unsupported(projection, path, `constrains "additionalProperties" with no declared properties, which has no GraphQL projection`);
105
+ return "JSON";
106
+ }
107
+ if (schema.patternProperties !== void 0) unsupported(projection, path, `uses "patternProperties" alongside "properties", which has no GraphQL projection`);
108
+ const name = reserve(projection, preferred);
109
+ projection.types.set(name, objectSdl(name, properties, schema, projection, path));
110
+ return name;
111
+ }
112
+ if (type === "array") {
113
+ if (schema.prefixItems !== void 0) unsupported(projection, path, `uses "prefixItems", which has no GraphQL projection`);
114
+ if (Array.isArray(schema.items)) unsupported(projection, path, `uses tuple "items", which has no GraphQL projection`);
115
+ if (schema.items === void 0) unsupported(projection, path, `declares an array without "items", so its element type has no GraphQL projection`);
116
+ const item = schema.items;
117
+ return `[${project(item, `${preferred}Item`, projection, `${path}[]`)}${admitsNull(item, projection) ? "" : "!"}]`;
118
+ }
119
+ const named = enumType(projection, schema, preferred);
120
+ if (named) return named;
121
+ const scalar = SCALARS[type];
122
+ if (!scalar) unsupported(projection, path, `declares unsupported type "${type}"`);
123
+ return scalar;
114
124
  }
115
125
  function objectSdl(name, properties, schema, projection, path) {
116
- const required = new Set(schema.required ?? []);
117
- const fieldNames = properties.map(([property]) => identifier(property));
118
- if (new Set(fieldNames).size !== fieldNames.length) {
119
- throw new Error(`${projection.subject}${path} produces colliding GraphQL field names.`);
120
- }
121
- const fields = properties.map(([property, child], index) => {
122
- const reference = project(child, `${name}${pascal(property)}`, projection, `${path}.${property}`);
123
- const nonNull = required.has(property) && !admitsNull(child, projection);
124
- return ` ${fieldNames[index]}: ${reference}${nonNull ? "!" : ""}`;
125
- });
126
- return `type ${name} {
127
- ${fields.join("\n")}
128
- }`;
126
+ const required = new Set(schema.required ?? []);
127
+ const fieldNames = properties.map(([property]) => identifier(property));
128
+ if (new Set(fieldNames).size !== fieldNames.length) throw new Error(`${projection.subject}${path} produces colliding GraphQL field names.`);
129
+ return `type ${name} {\n${properties.map(([property, child], index) => {
130
+ const reference = project(child, `${name}${pascal(property)}`, projection, `${path}.${property}`);
131
+ const nonNull = required.has(property) && !admitsNull(child, projection);
132
+ return ` ${fieldNames[index]}: ${reference}${nonNull ? "!" : ""}`;
133
+ }).join("\n")}\n}`;
129
134
  }
130
- var graphqlGenerator = {
131
- name: "@fabricorg/gen-graphql",
132
- version: "4.0.6",
133
- generate(ir) {
134
- return ir.modules.flatMap((module) => {
135
- const namespaceType = pascal(module.namespace);
136
- const types = /* @__PURE__ */ new Map();
137
- const projectionFor = (root, rootName, subject) => ({ types, refs: /* @__PURE__ */ new Map(), root, rootName, subject });
138
- for (const objectType of module.objectTypes) {
139
- const subject = `Module "${module.namespace}" object "${objectType.type}"`;
140
- const schema = objectType.schema ?? {};
141
- const required = new Set(schema.required ?? []);
142
- const properties = Object.entries(schema.properties ?? {}).filter(([name]) => !["id", "tenantId", "spaceId"].includes(name));
143
- const fieldNames = properties.map(([name]) => identifier(name));
144
- if (new Set(fieldNames).size !== fieldNames.length) throw new Error(`Object "${module.namespace}.${objectType.type}" produces colliding GraphQL field names.`);
145
- const entityName = `${namespaceType}${objectType.type}`;
146
- if (types.has(entityName)) throw new Error(`Module "${module.namespace}" produces two GraphQL types named "${entityName}".`);
147
- types.set(entityName, "");
148
- const projection = projectionFor(schema, entityName, subject);
149
- for (const keyword of ["oneOf", "anyOf", "allOf", "not", "if"]) {
150
- if (schema[keyword] !== void 0) unsupported(projection, "", `uses "${keyword}", which has no GraphQL projection`);
151
- }
152
- const fields = properties.map(([name, child], index) => {
153
- const reference = project(child, `${entityName}${pascal(name)}`, projection, `.${name}`);
154
- const nonNull = required.has(name) && !admitsNull(child, projection);
155
- return ` ${fieldNames[index]}: ${reference}${nonNull ? "!" : ""}`;
156
- });
157
- types.set(entityName, [`type ${entityName} @key(fields: "id") {`, " id: ID!", " tenantId: ID!", " spaceId: ID!", ...fields, "}"].join("\n"));
158
- }
159
- const fieldName = (value) => {
160
- const name = pascal(value);
161
- return `${name[0]?.toLowerCase() ?? ""}${name.slice(1)}`;
162
- };
163
- const viewPayloads = module.views.map((view) => {
164
- const rootName = `${pascal(view.name)}V${view.version.replace(/[^0-9A-Za-z]/g, "_")}`;
165
- const projection = projectionFor(view.shape, rootName, `Module "${module.namespace}" view "${view.name}@${view.version}" shape`);
166
- const reference = project(view.shape, rootName, projection, "");
167
- return { field: fieldName(view.name), reference, nonNull: !admitsNull(view.shape, projection), parameterized: Boolean(view.parameterSchema) };
168
- });
169
- const queryFields = viewPayloads.map((view) => ` ${view.field}${view.parameterized ? "(parameters: JSON!)" : ""}: ${view.reference}${view.nonNull ? "!" : ""}`);
170
- const mutations = module.actions.map((action) => ` ${fieldName(action.actionId)}(parameters: JSON!): GovernedActionSubmission!`);
171
- const operationNames = [...viewPayloads.map((view) => view.field), ...module.actions.map((action) => fieldName(action.actionId))];
172
- if (new Set(operationNames).size !== operationNames.length) throw new Error(`Module "${module.namespace}" produces colliding GraphQL operation names.`);
173
- const content = [
174
- `# Generated by ${graphqlGenerator.name}@${graphqlGenerator.version}`,
175
- `# Manifest digest: ${ir.manifestDigest}`,
176
- 'extend schema @link(url: "https://specs.apollo.dev/federation/v2.7", import: ["@key"])',
177
- "scalar JSON",
178
- "type GovernedActionSubmission { actionInvocationId: ID!, status: String! }",
179
- ...[...types.values()].filter(Boolean),
180
- `type Query {
181
- ${queryFields.length ? queryFields.join("\n") : " _fabric: Boolean!"}
182
- }`,
183
- `type Mutation {
184
- ${mutations.length ? mutations.join("\n") : " _fabric: Boolean!"}
185
- }`,
186
- ""
187
- ].join("\n\n");
188
- const actionIds = Object.fromEntries(module.actions.map((action) => [fieldName(action.actionId), action.actionId]));
189
- const viewIds = Object.fromEntries(module.views.map((view) => [fieldName(view.name), { name: view.name, version: view.version }]));
190
- const resolvers = [
191
- 'import type { ProjectionHost, ProjectionQuery } from "@fabricorg/projection-host";',
192
- 'import type { GovernedActionHost, SubmitActionInput, SubmitActionResult } from "@fabricorg/platform-host";',
193
- "",
194
- "// Generated resolver seam. Authenticate first; derive actor and tenant server-side.",
195
- `export const actionIds = ${JSON.stringify(actionIds, null, 2)} as const;`,
196
- `export const viewIds = ${JSON.stringify(viewIds, null, 2)} as const;`,
197
- "",
198
- 'export async function resolveProjection(host: Pick<ProjectionHost, "project">, field: keyof typeof viewIds, context: Omit<ProjectionQuery, "view">): Promise<unknown> {',
199
- " const result = await host.project({ ...context, view: viewIds[field] });",
200
- " return result.state;",
201
- "}",
202
- 'export type GovernedMutationContext = Omit<SubmitActionInput, "actionId" | "parameters">;',
203
- 'export async function submitMutation(host: Pick<GovernedActionHost, "submitAction">, field: keyof typeof actionIds, parameters: Record<string, unknown>, context: GovernedMutationContext): Promise<SubmitActionResult> {',
204
- " return await host.submitAction({ ...context, actionId: actionIds[field], parameters });",
205
- "}",
206
- "",
207
- "export interface ResolverWiring<TContext> {",
208
- ' projectionHost: Pick<ProjectionHost, "project">;',
209
- ' actionHost: Pick<GovernedActionHost, "submitAction">;',
210
- " /** Derive tenant, space and actor from the authenticated request. Never from client input. */",
211
- ' projectionContext: (context: TContext) => Omit<ProjectionQuery, "view"> | Promise<Omit<ProjectionQuery, "view">>;',
212
- " mutationContext: (context: TContext) => GovernedMutationContext | Promise<GovernedMutationContext>;",
213
- "}",
214
- "",
215
- "/** Executable resolver map. Query fields read through ProjectionHost; Mutation fields submit through the governed Host. */",
216
- "export function createResolvers<TContext>(wiring: ResolverWiring<TContext>) {",
217
- " return {",
218
- " Query: {",
219
- ...viewPayloads.map(
220
- (view) => view.parameterized ? ` ${JSON.stringify(view.field)}: async (_parent: unknown, args: { parameters: unknown }, context: TContext) => resolveProjection(wiring.projectionHost, ${JSON.stringify(view.field)}, { ...await wiring.projectionContext(context), parameters: args.parameters }),` : ` ${JSON.stringify(view.field)}: async (_parent: unknown, _args: unknown, context: TContext) => resolveProjection(wiring.projectionHost, ${JSON.stringify(view.field)}, await wiring.projectionContext(context)),`
221
- ),
222
- " },",
223
- " Mutation: {",
224
- ...Object.keys(actionIds).map(
225
- (field) => ` ${JSON.stringify(field)}: async (_parent: unknown, args: { parameters: Record<string, unknown> }, context: TContext) => submitMutation(wiring.actionHost, ${JSON.stringify(field)}, args.parameters, await wiring.mutationContext(context)),`
226
- ),
227
- " },",
228
- " };",
229
- "}",
230
- ""
231
- ].join("\n");
232
- return [{ path: `${module.namespace}/schema.graphql`, content }, { path: `${module.namespace}/resolver-contract.ts`, content: resolvers }];
233
- });
234
- }
235
- };
236
- export {
237
- graphqlGenerator
135
+ const graphqlGenerator = {
136
+ name: "@fabricorg/gen-graphql",
137
+ version: "4.0.7",
138
+ generate(ir) {
139
+ return ir.modules.flatMap((module) => {
140
+ const namespaceType = pascal(module.namespace);
141
+ const types = /* @__PURE__ */ new Map();
142
+ const projectionFor = (root, rootName, subject) => ({
143
+ types,
144
+ refs: /* @__PURE__ */ new Map(),
145
+ root,
146
+ rootName,
147
+ subject
148
+ });
149
+ for (const objectType of module.objectTypes) {
150
+ const subject = `Module "${module.namespace}" object "${objectType.type}"`;
151
+ const schema = objectType.schema ?? {};
152
+ const required = new Set(schema.required ?? []);
153
+ const properties = Object.entries(schema.properties ?? {}).filter(([name]) => ![
154
+ "id",
155
+ "tenantId",
156
+ "spaceId"
157
+ ].includes(name));
158
+ const fieldNames = properties.map(([name]) => identifier(name));
159
+ if (new Set(fieldNames).size !== fieldNames.length) throw new Error(`Object "${module.namespace}.${objectType.type}" produces colliding GraphQL field names.`);
160
+ const entityName = `${namespaceType}${objectType.type}`;
161
+ if (types.has(entityName)) throw new Error(`Module "${module.namespace}" produces two GraphQL types named "${entityName}".`);
162
+ types.set(entityName, "");
163
+ const projection = projectionFor(schema, entityName, subject);
164
+ for (const keyword of [
165
+ "oneOf",
166
+ "anyOf",
167
+ "allOf",
168
+ "not",
169
+ "if"
170
+ ]) if (schema[keyword] !== void 0) unsupported(projection, "", `uses "${keyword}", which has no GraphQL projection`);
171
+ const fields = properties.map(([name, child], index) => {
172
+ const reference = project(child, `${entityName}${pascal(name)}`, projection, `.${name}`);
173
+ const nonNull = required.has(name) && !admitsNull(child, projection);
174
+ return ` ${fieldNames[index]}: ${reference}${nonNull ? "!" : ""}`;
175
+ });
176
+ types.set(entityName, [
177
+ `type ${entityName} @key(fields: "id") {`,
178
+ " id: ID!",
179
+ " tenantId: ID!",
180
+ " spaceId: ID!",
181
+ ...fields,
182
+ "}"
183
+ ].join("\n"));
184
+ }
185
+ const fieldName = (value) => {
186
+ const name = pascal(value);
187
+ return `${name[0]?.toLowerCase() ?? ""}${name.slice(1)}`;
188
+ };
189
+ const viewPayloads = module.views.map((view) => {
190
+ const rootName = `${pascal(view.name)}V${view.version.replace(/[^0-9A-Za-z]/g, "_")}`;
191
+ const projection = projectionFor(view.shape, rootName, `Module "${module.namespace}" view "${view.name}@${view.version}" shape`);
192
+ const reference = project(view.shape, rootName, projection, "");
193
+ return {
194
+ field: fieldName(view.name),
195
+ reference,
196
+ nonNull: !admitsNull(view.shape, projection),
197
+ parameterized: Boolean(view.parameterSchema)
198
+ };
199
+ });
200
+ const queryFields = viewPayloads.map((view) => ` ${view.field}${view.parameterized ? "(parameters: JSON!)" : ""}: ${view.reference}${view.nonNull ? "!" : ""}`);
201
+ const mutations = module.actions.map((action) => ` ${fieldName(action.actionId)}(parameters: JSON!): GovernedActionSubmission!`);
202
+ const operationNames = [...viewPayloads.map((view) => view.field), ...module.actions.map((action) => fieldName(action.actionId))];
203
+ if (new Set(operationNames).size !== operationNames.length) throw new Error(`Module "${module.namespace}" produces colliding GraphQL operation names.`);
204
+ const content = [
205
+ `# Generated by ${graphqlGenerator.name}@${graphqlGenerator.version}`,
206
+ `# Manifest digest: ${ir.manifestDigest}`,
207
+ "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.7\", import: [\"@key\"])",
208
+ "scalar JSON",
209
+ "type GovernedActionSubmission { actionInvocationId: ID!, status: String! }",
210
+ ...[...types.values()].filter(Boolean),
211
+ `type Query {\n${queryFields.length ? queryFields.join("\n") : " _fabric: Boolean!"}\n}`,
212
+ `type Mutation {\n${mutations.length ? mutations.join("\n") : " _fabric: Boolean!"}\n}`,
213
+ ""
214
+ ].join("\n\n");
215
+ const actionIds = Object.fromEntries(module.actions.map((action) => [fieldName(action.actionId), action.actionId]));
216
+ const viewIds = Object.fromEntries(module.views.map((view) => [fieldName(view.name), {
217
+ name: view.name,
218
+ version: view.version
219
+ }]));
220
+ const resolvers = [
221
+ "import type { ProjectionHost, ProjectionQuery } from \"@fabricorg/projection-host\";",
222
+ "import type { GovernedActionHost, SubmitActionInput, SubmitActionResult } from \"@fabricorg/platform-host\";",
223
+ "",
224
+ "// Generated resolver seam. Authenticate first; derive actor and tenant server-side.",
225
+ `export const actionIds = ${JSON.stringify(actionIds, null, 2)} as const;`,
226
+ `export const viewIds = ${JSON.stringify(viewIds, null, 2)} as const;`,
227
+ "",
228
+ "export async function resolveProjection(host: Pick<ProjectionHost, \"project\">, field: keyof typeof viewIds, context: Omit<ProjectionQuery, \"view\">): Promise<unknown> {",
229
+ " const result = await host.project({ ...context, view: viewIds[field] });",
230
+ " return result.state;",
231
+ "}",
232
+ "export type GovernedMutationContext = Omit<SubmitActionInput, \"actionId\" | \"parameters\">;",
233
+ "export async function submitMutation(host: Pick<GovernedActionHost, \"submitAction\">, field: keyof typeof actionIds, parameters: Record<string, unknown>, context: GovernedMutationContext): Promise<SubmitActionResult> {",
234
+ " return await host.submitAction({ ...context, actionId: actionIds[field], parameters });",
235
+ "}",
236
+ "",
237
+ "export interface ResolverWiring<TContext> {",
238
+ " projectionHost: Pick<ProjectionHost, \"project\">;",
239
+ " actionHost: Pick<GovernedActionHost, \"submitAction\">;",
240
+ " /** Derive tenant, space and actor from the authenticated request. Never from client input. */",
241
+ " projectionContext: (context: TContext) => Omit<ProjectionQuery, \"view\"> | Promise<Omit<ProjectionQuery, \"view\">>;",
242
+ " mutationContext: (context: TContext) => GovernedMutationContext | Promise<GovernedMutationContext>;",
243
+ "}",
244
+ "",
245
+ "/** Executable resolver map. Query fields read through ProjectionHost; Mutation fields submit through the governed Host. */",
246
+ "export function createResolvers<TContext>(wiring: ResolverWiring<TContext>) {",
247
+ " return {",
248
+ " Query: {",
249
+ ...viewPayloads.map((view) => view.parameterized ? ` ${JSON.stringify(view.field)}: async (_parent: unknown, args: { parameters: unknown }, context: TContext) => resolveProjection(wiring.projectionHost, ${JSON.stringify(view.field)}, { ...await wiring.projectionContext(context), parameters: args.parameters }),` : ` ${JSON.stringify(view.field)}: async (_parent: unknown, _args: unknown, context: TContext) => resolveProjection(wiring.projectionHost, ${JSON.stringify(view.field)}, await wiring.projectionContext(context)),`),
250
+ " },",
251
+ " Mutation: {",
252
+ ...Object.keys(actionIds).map((field) => ` ${JSON.stringify(field)}: async (_parent: unknown, args: { parameters: Record<string, unknown> }, context: TContext) => submitMutation(wiring.actionHost, ${JSON.stringify(field)}, args.parameters, await wiring.mutationContext(context)),`),
253
+ " },",
254
+ " };",
255
+ "}",
256
+ ""
257
+ ].join("\n");
258
+ return [{
259
+ path: `${module.namespace}/schema.graphql`,
260
+ content
261
+ }, {
262
+ path: `${module.namespace}/resolver-contract.ts`,
263
+ content: resolvers
264
+ }];
265
+ });
266
+ }
238
267
  };
268
+ //#endregion
269
+ export { graphqlGenerator };
270
+
239
271
  //# sourceMappingURL=index.js.map