firebase-contract 0.1.0-rc.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 (152) hide show
  1. package/README.md +560 -0
  2. package/dist/index.d.ts +15 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +12 -0
  5. package/dist/lib/_internal/compiler/compiler.d.ts +71 -0
  6. package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -0
  7. package/dist/lib/_internal/compiler/compiler.js +104 -0
  8. package/dist/lib/_internal/compiler/index.d.ts +3 -0
  9. package/dist/lib/_internal/compiler/index.d.ts.map +1 -0
  10. package/dist/lib/_internal/compiler/index.js +1 -0
  11. package/dist/lib/_internal/diagnostics.d.ts +29 -0
  12. package/dist/lib/_internal/diagnostics.d.ts.map +1 -0
  13. package/dist/lib/_internal/diagnostics.js +26 -0
  14. package/dist/lib/_internal/format-diagnostics.d.ts +8 -0
  15. package/dist/lib/_internal/format-diagnostics.d.ts.map +1 -0
  16. package/dist/lib/_internal/format-diagnostics.js +10 -0
  17. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts +8 -0
  18. package/dist/lib/_internal/generators/api/api-dto-generator.d.ts.map +1 -0
  19. package/dist/lib/_internal/generators/api/api-dto-generator.js +134 -0
  20. package/dist/lib/_internal/generators/api/api-types-generator.d.ts +8 -0
  21. package/dist/lib/_internal/generators/api/api-types-generator.d.ts.map +1 -0
  22. package/dist/lib/_internal/generators/api/api-types-generator.js +105 -0
  23. package/dist/lib/_internal/generators/api/api-validation-generator.d.ts +7 -0
  24. package/dist/lib/_internal/generators/api/api-validation-generator.d.ts.map +1 -0
  25. package/dist/lib/_internal/generators/api/api-validation-generator.js +73 -0
  26. package/dist/lib/_internal/generators/api/task-payload-generator.d.ts +9 -0
  27. package/dist/lib/_internal/generators/api/task-payload-generator.d.ts.map +1 -0
  28. package/dist/lib/_internal/generators/api/task-payload-generator.js +96 -0
  29. package/dist/lib/_internal/generators/config/config-generator.d.ts +9 -0
  30. package/dist/lib/_internal/generators/config/config-generator.d.ts.map +1 -0
  31. package/dist/lib/_internal/generators/config/config-generator.js +71 -0
  32. package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts +8 -0
  33. package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts.map +1 -0
  34. package/dist/lib/_internal/generators/data-connect/adapter-generator.js +142 -0
  35. package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts +15 -0
  36. package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts.map +1 -0
  37. package/dist/lib/_internal/generators/data-connect/graphql-generator.js +209 -0
  38. package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts +18 -0
  39. package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts.map +1 -0
  40. package/dist/lib/_internal/generators/data-connect/operations-generator.js +539 -0
  41. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts +11 -0
  42. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts.map +1 -0
  43. package/dist/lib/_internal/generators/firestore/firestore-projection-generator.js +112 -0
  44. package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts +11 -0
  45. package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts.map +1 -0
  46. package/dist/lib/_internal/generators/firestore/firestore-split-generator.js +375 -0
  47. package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts +12 -0
  48. package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts.map +1 -0
  49. package/dist/lib/_internal/generators/firestore/firestore-type-generator.js +81 -0
  50. package/dist/lib/_internal/generators/generator.d.ts +23 -0
  51. package/dist/lib/_internal/generators/generator.d.ts.map +1 -0
  52. package/dist/lib/_internal/generators/generator.js +1 -0
  53. package/dist/lib/_internal/generators/id/id-codec-generator.d.ts +16 -0
  54. package/dist/lib/_internal/generators/id/id-codec-generator.d.ts.map +1 -0
  55. package/dist/lib/_internal/generators/id/id-codec-generator.js +140 -0
  56. package/dist/lib/_internal/generators/index.d.ts +31 -0
  57. package/dist/lib/_internal/generators/index.d.ts.map +1 -0
  58. package/dist/lib/_internal/generators/index.js +64 -0
  59. package/dist/lib/_internal/generators/registry.d.ts +15 -0
  60. package/dist/lib/_internal/generators/registry.d.ts.map +1 -0
  61. package/dist/lib/_internal/generators/registry.js +24 -0
  62. package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts +9 -0
  63. package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts.map +1 -0
  64. package/dist/lib/_internal/generators/sql/sql-migration-generator.js +44 -0
  65. package/dist/lib/_internal/generators/support/constraints.d.ts +8 -0
  66. package/dist/lib/_internal/generators/support/constraints.d.ts.map +1 -0
  67. package/dist/lib/_internal/generators/support/constraints.js +39 -0
  68. package/dist/lib/_internal/generators/support/header.d.ts +13 -0
  69. package/dist/lib/_internal/generators/support/header.d.ts.map +1 -0
  70. package/dist/lib/_internal/generators/support/header.js +17 -0
  71. package/dist/lib/_internal/generators/support/naming.d.ts +10 -0
  72. package/dist/lib/_internal/generators/support/naming.d.ts.map +1 -0
  73. package/dist/lib/_internal/generators/support/naming.js +30 -0
  74. package/dist/lib/_internal/generators/support/relations.d.ts +12 -0
  75. package/dist/lib/_internal/generators/support/relations.d.ts.map +1 -0
  76. package/dist/lib/_internal/generators/support/relations.js +11 -0
  77. package/dist/lib/_internal/generators/support/split.d.ts +34 -0
  78. package/dist/lib/_internal/generators/support/split.d.ts.map +1 -0
  79. package/dist/lib/_internal/generators/support/split.js +75 -0
  80. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts +18 -0
  81. package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -0
  82. package/dist/lib/_internal/generators/typescript/typescript-generator.js +99 -0
  83. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts +10 -0
  84. package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -0
  85. package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +175 -0
  86. package/dist/lib/_internal/generators/union/union-generator.d.ts +8 -0
  87. package/dist/lib/_internal/generators/union/union-generator.d.ts.map +1 -0
  88. package/dist/lib/_internal/generators/union/union-generator.js +37 -0
  89. package/dist/lib/_internal/generators/zod/zod-generator.d.ts +7 -0
  90. package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -0
  91. package/dist/lib/_internal/generators/zod/zod-generator.js +68 -0
  92. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts +9 -0
  93. package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -0
  94. package/dist/lib/_internal/generators/zod/zod-split-generator.js +136 -0
  95. package/dist/lib/_internal/ir/build-ir.d.ts +18 -0
  96. package/dist/lib/_internal/ir/build-ir.d.ts.map +1 -0
  97. package/dist/lib/_internal/ir/build-ir.js +430 -0
  98. package/dist/lib/_internal/ir/ir.d.ts +393 -0
  99. package/dist/lib/_internal/ir/ir.d.ts.map +1 -0
  100. package/dist/lib/_internal/ir/ir.js +22 -0
  101. package/dist/lib/_internal/parser/parse.d.ts +9 -0
  102. package/dist/lib/_internal/parser/parse.d.ts.map +1 -0
  103. package/dist/lib/_internal/parser/parse.js +682 -0
  104. package/dist/lib/_internal/parser/raw-document.d.ts +230 -0
  105. package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -0
  106. package/dist/lib/_internal/parser/raw-document.js +6 -0
  107. package/dist/lib/_internal/resolver/import-resolver.d.ts +25 -0
  108. package/dist/lib/_internal/resolver/import-resolver.d.ts.map +1 -0
  109. package/dist/lib/_internal/resolver/import-resolver.js +73 -0
  110. package/dist/lib/_internal/resolver/module-loader.d.ts +25 -0
  111. package/dist/lib/_internal/resolver/module-loader.d.ts.map +1 -0
  112. package/dist/lib/_internal/resolver/module-loader.js +62 -0
  113. package/dist/lib/_internal/runtime/data-connect-adapter.d.ts +25 -0
  114. package/dist/lib/_internal/runtime/data-connect-adapter.d.ts.map +1 -0
  115. package/dist/lib/_internal/runtime/data-connect-adapter.js +24 -0
  116. package/dist/lib/_internal/runtime/index.d.ts +3 -0
  117. package/dist/lib/_internal/runtime/index.d.ts.map +1 -0
  118. package/dist/lib/_internal/runtime/index.js +1 -0
  119. package/dist/lib/_internal/validation/rules.d.ts +32 -0
  120. package/dist/lib/_internal/validation/rules.d.ts.map +1 -0
  121. package/dist/lib/_internal/validation/rules.js +282 -0
  122. package/dist/lib/_internal/validation/validate.d.ts +9 -0
  123. package/dist/lib/_internal/validation/validate.d.ts.map +1 -0
  124. package/dist/lib/_internal/validation/validate.js +6 -0
  125. package/dist/lib/cli/commands/generate.d.ts +3 -0
  126. package/dist/lib/cli/commands/generate.d.ts.map +1 -0
  127. package/dist/lib/cli/commands/generate.js +66 -0
  128. package/dist/lib/cli/commands/index.d.ts +4 -0
  129. package/dist/lib/cli/commands/index.d.ts.map +1 -0
  130. package/dist/lib/cli/commands/index.js +11 -0
  131. package/dist/lib/cli/commands/init.d.ts +3 -0
  132. package/dist/lib/cli/commands/init.d.ts.map +1 -0
  133. package/dist/lib/cli/commands/init.js +72 -0
  134. package/dist/lib/cli/commands/inspect.d.ts +3 -0
  135. package/dist/lib/cli/commands/inspect.d.ts.map +1 -0
  136. package/dist/lib/cli/commands/inspect.js +23 -0
  137. package/dist/lib/cli/commands/validate.d.ts +3 -0
  138. package/dist/lib/cli/commands/validate.d.ts.map +1 -0
  139. package/dist/lib/cli/commands/validate.js +23 -0
  140. package/dist/lib/cli/config.d.ts +12 -0
  141. package/dist/lib/cli/config.d.ts.map +1 -0
  142. package/dist/lib/cli/config.js +20 -0
  143. package/dist/lib/cli/index.d.ts +3 -0
  144. package/dist/lib/cli/index.d.ts.map +1 -0
  145. package/dist/lib/cli/index.js +13 -0
  146. package/dist/lib/cli/version.d.ts +3 -0
  147. package/dist/lib/cli/version.d.ts.map +1 -0
  148. package/dist/lib/cli/version.js +15 -0
  149. package/dist/lib/modules/runtime.d.ts +2 -0
  150. package/dist/lib/modules/runtime.d.ts.map +1 -0
  151. package/dist/lib/modules/runtime.js +3 -0
  152. package/package.json +60 -0
@@ -0,0 +1,539 @@
1
+ import { findModel } from '../../ir/ir.js';
2
+ import { camelCase, pluralize, snakeCase } from '../support/naming.js';
3
+ import { isRelation, relationFkName, relationFkType } from '../support/relations.js';
4
+ import { headerBlocks } from '../support/header.js';
5
+ const SCALAR_GQL = {
6
+ string: 'String',
7
+ int: 'Int',
8
+ int64: 'Int64',
9
+ float: 'Float',
10
+ boolean: 'Boolean',
11
+ timestamp: 'Timestamp',
12
+ date: 'Date',
13
+ json: 'Any',
14
+ id: 'UUID',
15
+ };
16
+ const SCALAR_TS = {
17
+ string: 'string',
18
+ int: 'number',
19
+ int64: 'number',
20
+ float: 'number',
21
+ boolean: 'boolean',
22
+ timestamp: 'string',
23
+ date: 'string',
24
+ json: 'unknown',
25
+ id: 'string',
26
+ };
27
+ const enumGqlName = (ir, name) => ir.enums.find(e => e.name === name)?.gqlName ?? name;
28
+ const fieldGqlType = (ir, field) => isRelation(field)
29
+ ? SCALAR_GQL[relationFkType(ir, field)]
30
+ : field.type.kind === 'enum'
31
+ ? enumGqlName(ir, field.type.name)
32
+ : field.type.kind === 'scalar'
33
+ ? SCALAR_GQL[field.type.name]
34
+ : 'Any';
35
+ const fieldTsType = (ir, field) => isRelation(field)
36
+ ? SCALAR_TS[relationFkType(ir, field)]
37
+ : field.type.kind === 'enum'
38
+ ? field.type.name
39
+ : field.type.kind === 'scalar'
40
+ ? SCALAR_TS[field.type.name]
41
+ : 'unknown';
42
+ const varName = (field) => (isRelation(field) ? relationFkName(field) : field.name);
43
+ const toVariable = (ir, field, required, comment) => ({
44
+ name: varName(field),
45
+ field,
46
+ gqlType: fieldGqlType(ir, field),
47
+ tsType: fieldTsType(ir, field),
48
+ required,
49
+ ...(comment !== undefined ? { comment } : {}),
50
+ });
51
+ const idField = (model) => model.fields.find(field => field.isId);
52
+ /** Primary-key fields: the composite `key` list, else the single `id: true` field. */
53
+ const keyFields = (model) => {
54
+ if (model.key.length > 0) {
55
+ return model.key.flatMap(name => {
56
+ const field = model.fields.find(f => f.name === name);
57
+ return field ? [field] : [];
58
+ });
59
+ }
60
+ const id = idField(model);
61
+ return id ? [id] : [];
62
+ };
63
+ /** Effective variable name for an entry: explicit override, else field/FK name. */
64
+ const entryVarName = (field, override) => override ?? varName(field);
65
+ const collectVariables = (ir, op, model) => {
66
+ const byName = new Map(model.fields.map(field => [field.name, field]));
67
+ const vars = [];
68
+ const seen = new Set();
69
+ const push = (field, required, comment, nameOverride) => {
70
+ const name = entryVarName(field, nameOverride);
71
+ if (seen.has(name))
72
+ return;
73
+ seen.add(name);
74
+ vars.push({ ...toVariable(ir, field, required, comment), name });
75
+ };
76
+ if (op.operationType === 'mutation') {
77
+ // update/delete need the primary key (single id or composite) as variables.
78
+ if (op.action === 'update' || op.action === 'delete') {
79
+ for (const field of keyFields(model))
80
+ push(field, true, undefined, op.keyVars?.[field.name]);
81
+ }
82
+ for (const input of op.inputs) {
83
+ if (input.literal !== undefined)
84
+ continue; // literal writes declare no variable
85
+ if (input.inc && !input.var)
86
+ continue; // fixed +1 declares no variable
87
+ const field = byName.get(input.field);
88
+ if (!field)
89
+ continue;
90
+ if (input.asKey && field.type.kind === 'model') {
91
+ // Key-object variable: `$catalogVersion: CatalogVersion_Key!`
92
+ const related = findModel(ir, field.type.name);
93
+ const keyType = `${related?.gqlName ?? field.type.name}_Key`;
94
+ const name = input.var ?? field.name;
95
+ if (!seen.has(name)) {
96
+ seen.add(name);
97
+ vars.push({ name, field, gqlType: keyType, tsType: 'unknown', required: input.required ?? !field.optional });
98
+ }
99
+ continue;
100
+ }
101
+ push(field, (input.inc ? true : input.required ?? !field.optional), input.description, input.var);
102
+ }
103
+ for (const entry of op.inc) {
104
+ if (!entry.var)
105
+ continue; // fixed +1 declares no variable
106
+ const field = byName.get(entry.field);
107
+ if (field)
108
+ push(field, true, undefined, entry.var);
109
+ }
110
+ }
111
+ else {
112
+ for (const where of op.where) {
113
+ if (where.literal !== undefined)
114
+ continue; // literal comparisons declare no variable
115
+ // Dotted paths (`provider.name`, `review.catalog.id`) type the variable from the
116
+ // final segment's field on the (transitively) related model.
117
+ if (where.field.includes('.')) {
118
+ const segs = where.field.split('.');
119
+ let m = model;
120
+ let sub;
121
+ for (let i = 0; i < segs.length; i++) {
122
+ sub = m?.fields.find(f => f.name === segs[i]);
123
+ if (!sub)
124
+ break;
125
+ if (i < segs.length - 1) {
126
+ m = sub.type.kind === 'model' ? findModel(ir, sub.type.name) : undefined;
127
+ }
128
+ }
129
+ if (sub)
130
+ push(sub, where.required ?? true, where.description, where.var ?? sub.name);
131
+ continue;
132
+ }
133
+ const field = byName.get(where.field);
134
+ if (field)
135
+ push(field, where.required ?? true, where.description, where.var);
136
+ }
137
+ if (op.limit !== undefined && typeof op.limit !== 'number') {
138
+ const lim = op.limit;
139
+ vars.push({
140
+ name: lim.var,
141
+ field: {
142
+ name: lim.var,
143
+ type: { kind: 'scalar', name: 'int' },
144
+ constraints: {},
145
+ optional: !(lim.required ?? false),
146
+ list: false,
147
+ isId: false,
148
+ unique: false,
149
+ relation: false,
150
+ },
151
+ gqlType: 'Int',
152
+ tsType: 'number',
153
+ required: lim.required ?? lim.default !== undefined,
154
+ });
155
+ }
156
+ }
157
+ return vars;
158
+ };
159
+ const dataEntryFor = (field, input) => {
160
+ if (input.asKey) {
161
+ return `${field.name}: $${input.var ?? field.name}`;
162
+ }
163
+ if (input.inc) {
164
+ return `${field.name}_update: { inc: ${input.var ? `$${input.var}` : '1'} }`;
165
+ }
166
+ if (input.literal !== undefined) {
167
+ const value = input.quote ? `"${input.literal}"` : input.literal;
168
+ return `${field.name}: ${value}`;
169
+ }
170
+ const v = input.var ?? varName(field);
171
+ if (isRelation(field)) {
172
+ return input.flat ? `${field.name}Id: $${v}` : `${field.name}: { id: $${v} }`;
173
+ }
174
+ return `${field.name}: $${v}`;
175
+ };
176
+ const authDirective = (op) => {
177
+ const reason = op.auth === 'PUBLIC' && op.authReason ? `, insecureReason: "${op.authReason}"` : '';
178
+ return `@auth(level: ${op.auth}${reason})`;
179
+ };
180
+ /**
181
+ * The `@auth` segment between the signature and the opening `{`. Matches the
182
+ * hand-written style: NO_ACCESS stays inline; PUBLIC with an insecureReason
183
+ * becomes a multi-line block on its own line.
184
+ */
185
+ const authSuffix = (op) => {
186
+ if (op.auth === 'PUBLIC' && op.authReason) {
187
+ return `\n@auth(\n level: PUBLIC\n insecureReason: "${op.authReason}"\n) {`;
188
+ }
189
+ if (op.style?.auth === 'newline') {
190
+ return `\n${authDirective(op)} {`;
191
+ }
192
+ return ` ${authDirective(op)} {`;
193
+ };
194
+ /** Render selection entries recursively with real-file indentation. */
195
+ const renderSelectLines = (entries, indent) => entries.flatMap(entry => {
196
+ const comments = entry.description ? entry.description.split('\n').map(l => `${indent}# ${l}`.trimEnd()) : [];
197
+ const name = `${entry.alias ? `${entry.alias}: ` : ''}${entry.field}`;
198
+ // Multi-line args (block scalar with newlines) render one argument per line.
199
+ const multiArgs = entry.args !== undefined && entry.args.includes('\n');
200
+ const head = multiArgs ? `${name}(` : `${name}${entry.args ? `(${entry.args})` : ''}`;
201
+ const argLines = multiArgs
202
+ ? [...entry.args.split('\n').map(l => `${indent} ${l}`), `${indent})`]
203
+ : [];
204
+ if (entry.select && entry.select.length > 0) {
205
+ const open = multiArgs
206
+ ? [...argLines.slice(0, -1), `${indent}) {`]
207
+ : [];
208
+ return multiArgs
209
+ ? [...comments, `${indent}${head}`, ...open.slice(0, -1), `${indent}) {`, ...renderSelectLines(entry.select, `${indent} `), `${indent}}`]
210
+ : [...comments, `${indent}${head} {`, ...renderSelectLines(entry.select, `${indent} `), `${indent}}`];
211
+ }
212
+ return [...comments, `${indent}${head}`, ...argLines];
213
+ });
214
+ const scalarFieldNames = (model) => model.fields.filter(field => !isRelation(field) && field.type.kind !== 'model').map(field => field.name);
215
+ const footerLines = (op) => op.footer ? '\n\n' + op.footer.split('\n').map(l => `# ${l}`.trimEnd()).join('\n') : '';
216
+ const renderOperationGql = (ir, op, model) => {
217
+ // Verbatim escape hatch: description + raw body + footer, untouched.
218
+ if (op.raw !== undefined) {
219
+ const rawHeader = op.description ? op.description.split('\n').map(l => `# ${l}`.trimEnd()).join('\n') + '\n' : '';
220
+ return `${rawHeader}${op.raw}${footerLines(op)}`;
221
+ }
222
+ const vars = collectVariables(ir, op, model);
223
+ // Multi-line signature (one variable per line) matching real connector files;
224
+ // single-variable operations stay inline unless a variable carries a comment.
225
+ const limitSpec = op.operationType === 'query' && op.limit !== undefined && typeof op.limit !== 'number' ? op.limit : undefined;
226
+ const varDecls = vars.map(v => {
227
+ const suffix = limitSpec && v.name === limitSpec.var && limitSpec.default !== undefined ? ` = ${limitSpec.default}` : '';
228
+ const decl = `$${v.name}: ${v.gqlType}${v.required ? '!' : ''}${suffix}`;
229
+ if (!v.comment)
230
+ return decl;
231
+ const comments = v.comment.split('\n').map(l => `# ${l}`.trimEnd());
232
+ return [...comments, decl].join('\n ');
233
+ });
234
+ const hasComments = vars.some(v => v.comment);
235
+ const sigStyle = op.style?.signature;
236
+ const inlineSig = sigStyle === 'inline' || (sigStyle !== 'multi' && varDecls.length === 1 && !hasComments);
237
+ const signature = varDecls.length === 0
238
+ ? ''
239
+ : inlineSig && !hasComments
240
+ ? `(${varDecls.join(', ')})`
241
+ : `(\n ${varDecls.join('\n ')}\n)`;
242
+ // Multi-line description (e.g. the real files' header comment blocks).
243
+ const header = op.description ? op.description.split('\n').map(l => `# ${l}`.trimEnd()).join('\n') + '\n' : '';
244
+ const base = camelCase(model.gqlName ?? model.name);
245
+ if (op.operationType === 'mutation') {
246
+ const keys = keyFields(model);
247
+ const keyNames = new Set(keys.map(f => f.name));
248
+ const isKeyed = op.action === 'update' || op.action === 'delete';
249
+ const entries = op.inputs
250
+ .map(input => ({ input, field: model.fields.find(f => f.name === input.field) }))
251
+ .filter((e) => !!e.field)
252
+ .filter(e => !(isKeyed && keyNames.has(e.field.name) && e.input.literal === undefined));
253
+ const dataParts = [
254
+ ...entries.map(e => dataEntryFor(e.field, e.input)),
255
+ ...op.inc.map(entry => `${entry.field}_update: { inc: ${entry.var ? `$${entry.var}` : '1'} }`),
256
+ ...op.exprs.map(e => `${e.field}_expr: "${e.expr}"`),
257
+ ];
258
+ const keyPairs = keys.map(f => {
259
+ const v = op.keyVars?.[f.name] ?? varName(f);
260
+ // Relation key fields use their FK column name in the key object (catalogId, not catalog).
261
+ return { field: varName(f), v };
262
+ });
263
+ const inlineData = op.style?.data === 'inline';
264
+ const compactData = op.style?.data === 'compact';
265
+ const dataBlock = dataParts.length > 0
266
+ ? inlineData || compactData
267
+ ? `data: { ${dataParts.join(', ')} }`
268
+ : `data: {\n ${dataParts.join('\n ')}\n }`
269
+ : '';
270
+ const argLines = [];
271
+ if (isKeyed) {
272
+ if (op.keyArg === 'id' && keyPairs.length === 1) {
273
+ argLines.push(`${keyPairs[0].field}: $${keyPairs[0].v}`);
274
+ }
275
+ else if (op.style?.key === 'multi') {
276
+ argLines.push(`key: {\n ${keyPairs.map(k => `${k.field}: $${k.v}`).join('\n ')}\n }`);
277
+ }
278
+ else {
279
+ argLines.push(`key: { ${keyPairs.map(k => `${k.field}: $${k.v}`).join(', ')} }`);
280
+ }
281
+ }
282
+ if (op.action !== 'delete' && dataBlock)
283
+ argLines.push(dataBlock);
284
+ const verb = op.action ?? 'insert';
285
+ const body = inlineData
286
+ ? `${base}_${verb}(${argLines.join(', ')})`
287
+ : `${base}_${verb}(\n ${argLines.join('\n ')}\n )`;
288
+ return `${header}mutation ${op.gqlName ?? op.name}${signature}${authSuffix(op)}\n ${body}\n}${footerLines(op)}`;
289
+ }
290
+ const fieldByName = new Map(model.fields.map(f => [f.name, f]));
291
+ // Selection lines (shared by list and single forms).
292
+ let selection;
293
+ if (op.aggregate) {
294
+ const aggFields = [...(op.aggregate.count ? ['_count'] : []), ...op.aggregate.sum.map(f => `${f}_sum`)];
295
+ selection = aggFields.map(f => ` ${f}`).join('\n');
296
+ }
297
+ else {
298
+ const lines = op.select.length > 0
299
+ ? renderSelectLines(op.select, ' ')
300
+ : scalarFieldNames(model).map(name => ` ${name}`);
301
+ selection = lines.join('\n');
302
+ }
303
+ // Single-row primary-key lookup: `x(key: { id: $id })` or `x(id: $id)`.
304
+ if (op.single) {
305
+ const pairs = op.where.map(w => {
306
+ const field = fieldByName.get(w.field);
307
+ const v = w.var ?? (field ? varName(field) : w.field);
308
+ return `${w.field}: $${v}`;
309
+ });
310
+ const arg = op.single === 'key' ? `key: { ${pairs.join(', ')} }` : pairs.join(', ');
311
+ return `${header}query ${op.gqlName ?? op.name}${signature}${authSuffix(op)}\n ${base}(${arg}) {\n${selection}\n }\n}${footerLines(op)}`;
312
+ }
313
+ const plural = `${base}s`;
314
+ // Group where conditions per field; relations use the canonical id-nested form.
315
+ const grouped = new Map();
316
+ for (const w of op.where) {
317
+ const dot = w.field.indexOf('.');
318
+ const topKey = dot > 0 ? w.field.slice(0, dot) : w.field;
319
+ const field = fieldByName.get(topKey);
320
+ if (!field)
321
+ continue;
322
+ let value;
323
+ if (w.literal !== undefined) {
324
+ value = w.literal;
325
+ }
326
+ else if (dot > 0) {
327
+ const related = field.type.kind === 'model' ? findModel(ir, field.type.name) : undefined;
328
+ const subField = related?.fields.find(f => f.name === w.field.slice(dot + 1));
329
+ value = `$${w.var ?? (subField ? subField.name : w.field.slice(dot + 1))}`;
330
+ }
331
+ else {
332
+ value = `$${w.var ?? varName(field)}`;
333
+ }
334
+ const group = grouped.get(topKey) ?? { conds: [], flat: false, sub: false };
335
+ if (dot > 0) {
336
+ // Nested relation condition; deeper paths nest recursively
337
+ // (`review.catalog.id` → `catalog: { id: { eq: $x } }` inside `review: { … }`).
338
+ const rest = w.field.slice(dot + 1).split('.');
339
+ let inner = `{ ${w.op}: ${value} }`;
340
+ for (let i = rest.length - 1; i >= 0; i--)
341
+ inner = `{ ${rest[i]}: ${inner} }`.replace(/^\{ /, '{ ').replace(/ \}$/, ' }');
342
+ // strip outermost braces since group renderer adds them
343
+ group.conds.push(inner.slice(2, -2));
344
+ group.sub = true;
345
+ }
346
+ else {
347
+ group.conds.push(`${w.op}: ${value}`);
348
+ }
349
+ group.flat = group.flat || (w.flat ?? false);
350
+ grouped.set(topKey, group);
351
+ }
352
+ const whereEntries = [...grouped.entries()].map(([name, group]) => {
353
+ const field = fieldByName.get(name);
354
+ const inner = `{ ${group.conds.join(', ')} }`;
355
+ if (field && isRelation(field)) {
356
+ if (group.sub)
357
+ return `${name}: ${inner}`; // nested sub-field conditions
358
+ return group.flat ? `${name}Id: ${inner}` : `${name}: { id: ${inner} }`;
359
+ }
360
+ return `${name}: ${inner}`;
361
+ });
362
+ const args = [];
363
+ const argCount = [whereEntries.length > 0, op.orderBy.length > 0, op.limit !== undefined].filter(Boolean).length;
364
+ const multiArgs = op.style?.args === 'inline' ? false : op.style?.args === 'multi' ? argCount > 0 : argCount > 1;
365
+ if (whereEntries.length > 0) {
366
+ // `_and` renders as an expanded array (one condition per line) in multi-line
367
+ // call form, matching the hand-written style.
368
+ const inlineAnd = op.style?.and === 'inline';
369
+ const andItems = whereEntries.map(e => `{ ${e} }`);
370
+ const whereBody = op.whereAnd
371
+ ? multiArgs
372
+ ? inlineAnd
373
+ ? `{\n _and: [${andItems.join(', ')}]\n }`
374
+ : `{\n _and: [\n ${andItems.join('\n ')}\n ]\n }`
375
+ : `{ _and: [${andItems.join(', ')}] }`
376
+ : op.style?.where === 'multi'
377
+ ? `{\n ${whereEntries.join('\n ')}\n }`
378
+ : `{ ${whereEntries.join(', ')} }`;
379
+ args.push(`where: ${whereBody}`);
380
+ }
381
+ if (op.orderBy.length > 0) {
382
+ const clauses = op.orderBy.map(o => `{ ${o.field}: ${o.dir} }`).join(', ');
383
+ const bare = op.style?.orderBy === 'bare' && op.orderBy.length === 1;
384
+ args.push(bare ? `orderBy: ${clauses}` : `orderBy: [${clauses}]`);
385
+ }
386
+ if (op.limit !== undefined) {
387
+ args.push(typeof op.limit === 'number' ? `limit: ${op.limit}` : `limit: $${op.limit.var}`);
388
+ }
389
+ // Multi-argument list calls wrap one argument per line, like the real files.
390
+ const call = multiArgs
391
+ ? `${plural}(\n ${args.join('\n ')}\n ) {`
392
+ : `${plural}(${args.join(', ')}) {`;
393
+ return `${header}query ${op.gqlName ?? op.name}${signature}${authSuffix(op)}\n ${call}\n${selection}\n }\n}${footerLines(op)}`;
394
+ };
395
+ const renderVariablesType = (ir, op, model) => {
396
+ const vars = collectVariables(ir, op, model);
397
+ if (vars.length === 0) {
398
+ return `export interface ${op.name}Variables {}`;
399
+ }
400
+ const fields = vars.map(v => ` ${v.name}${v.required ? '' : '?'}: ${v.tsType}`).join('\n');
401
+ return `export interface ${op.name}Variables {\n${fields}\n}`;
402
+ };
403
+ const renderResultType = (op, model) => {
404
+ const base = camelCase(model.name);
405
+ if (op.operationType === 'mutation') {
406
+ const id = idField(model);
407
+ const idName = id?.name ?? 'id';
408
+ const idTs = id && id.type.kind === 'scalar' ? SCALAR_TS[id.type.name] : 'string';
409
+ return `export interface ${op.name}Result {\n ${base}_${op.action ?? 'insert'}: { ${idName}: ${idTs} }\n}`;
410
+ }
411
+ const plural = `${base}s`;
412
+ if (op.aggregate) {
413
+ const parts = [
414
+ ...(op.aggregate.count ? ['_count: number'] : []),
415
+ ...op.aggregate.sum.map(f => `${f}_sum: number | null`),
416
+ ];
417
+ return `export interface ${op.name}Result {\n ${plural}: { ${parts.join('; ')} }[]\n}`;
418
+ }
419
+ const pickNames = op.select.map(entry => {
420
+ const field = model.fields.find(f => f.name === entry.field);
421
+ return field && isRelation(field) ? relationFkName(field) : entry.field;
422
+ });
423
+ const element = op.select.length > 0 ? `Pick<${model.name}, ${pickNames.map(n => `'${n}'`).join(' | ')}>` : model.name;
424
+ return `export interface ${op.name}Result {\n ${plural}: ${element}[]\n}`;
425
+ };
426
+ /** Group operations by connector; ops with no connector go to the '' (root) group. */
427
+ const groupByConnector = (resolvable) => {
428
+ const groups = new Map();
429
+ for (const entry of resolvable) {
430
+ const targets = entry.op.connectors.length > 0 ? entry.op.connectors : [''];
431
+ for (const connector of targets) {
432
+ const list = groups.get(connector) ?? [];
433
+ list.push(entry);
434
+ groups.set(connector, list);
435
+ }
436
+ }
437
+ return groups;
438
+ };
439
+ const renderGqlFile = (ir, entries, context) => {
440
+ const blocks = [...headerBlocks(context, header => header.replace(/^\/\/ ?/gm, '# '))];
441
+ for (const { op, model } of entries) {
442
+ blocks.push(renderOperationGql(ir, op, model));
443
+ }
444
+ return `${blocks.join('\n\n')}\n`;
445
+ };
446
+ const renderTsFile = (ir, entries, typesImport, context) => {
447
+ const usedTypes = new Set();
448
+ for (const { op, model } of entries) {
449
+ if (op.operationType === 'query' && !op.aggregate) {
450
+ usedTypes.add(model.name);
451
+ }
452
+ for (const variable of collectVariables(ir, op, model)) {
453
+ if (variable.field.type.kind === 'enum') {
454
+ usedTypes.add(variable.field.type.name);
455
+ }
456
+ }
457
+ }
458
+ const blocks = [...headerBlocks(context)];
459
+ if (usedTypes.size > 0) {
460
+ blocks.push(`import type { ${[...usedTypes].sort().join(', ')} } from '${typesImport}'`);
461
+ }
462
+ for (const { op, model } of entries) {
463
+ blocks.push(renderVariablesType(ir, op, model));
464
+ blocks.push(renderResultType(op, model));
465
+ }
466
+ return `${blocks.join('\n\n')}\n`;
467
+ };
468
+ /**
469
+ * Generates Data Connect operations: `.gql` files with `mutation`/`query`
470
+ * definitions (over the auto-generated `<table>_insert/update/upsert/delete`
471
+ * resolvers, with `@auth` directives) plus matching TS `Variables`/`Result`
472
+ * types. Operations are routed per **connector** — an operation may target
473
+ * several connectors (`connectors: [app, api]`) and is emitted into each, under
474
+ * `<connector>/operations.gql`. Operations without a connector go to the root.
475
+ */
476
+ export const createDataConnectOperationsGenerator = () => ({
477
+ name: 'data-connect-operations',
478
+ description: 'Data Connect query/mutation operations (.gql + TS types), routed per connector',
479
+ generate(ir, context) {
480
+ if (ir.operations.length === 0) {
481
+ return [];
482
+ }
483
+ const resolvable = ir.operations
484
+ .map(op => ({ op, model: findModel(ir, op.model) }))
485
+ .filter((entry) => !!entry.model);
486
+ const files = [];
487
+ for (const [connector, entries] of groupByConnector(resolvable)) {
488
+ const dir = connector ? `${connector}/` : '';
489
+ // types.ts lives at the output root; connector files sit one level deeper.
490
+ const typesImport = connector ? '../types.js' : './types.js';
491
+ files.push({ path: `${dir}operations.gql`, content: renderGqlFile(ir, entries, context) });
492
+ files.push({ path: `${dir}operations-types.ts`, content: renderTsFile(ir, entries, typesImport, context) });
493
+ }
494
+ return files;
495
+ },
496
+ });
497
+ /** Entity directory name for a model: kebab-case of its table name. */
498
+ const entityDir = (model) => (model.table ?? pluralize(snakeCase(model.name))).replace(/_/g, '-');
499
+ /**
500
+ * Split variant matching the real connector layout:
501
+ * `<connector>/operations/<entity>/{queries,mutations}.gql` (+ one
502
+ * `operations-types.ts` per connector). Selectable alongside the single-file
503
+ * `data-connect-operations` generator.
504
+ */
505
+ export const createDataConnectOperationsSplitGenerator = () => ({
506
+ name: 'data-connect-operations-split',
507
+ description: 'Data Connect operations, one file per entity per connector',
508
+ generate(ir, context) {
509
+ if (ir.operations.length === 0) {
510
+ return [];
511
+ }
512
+ const resolvable = ir.operations
513
+ .map(op => ({ op, model: findModel(ir, op.model) }))
514
+ .filter((entry) => !!entry.model);
515
+ const files = [];
516
+ for (const [connector, entries] of groupByConnector(resolvable)) {
517
+ const dir = connector ? `${connector}/` : '';
518
+ const byEntity = new Map();
519
+ for (const entry of entries) {
520
+ const key = entry.op.entityDir ?? entityDir(entry.model);
521
+ byEntity.set(key, [...(byEntity.get(key) ?? []), entry]);
522
+ }
523
+ for (const [entity, group] of byEntity) {
524
+ const queries = group.filter(e => e.op.operationType === 'query');
525
+ const mutations = group.filter(e => e.op.operationType === 'mutation');
526
+ if (queries.length > 0) {
527
+ files.push({ path: `${dir}operations/${entity}/queries.gql`, content: renderGqlFile(ir, queries, context) });
528
+ }
529
+ if (mutations.length > 0) {
530
+ files.push({ path: `${dir}operations/${entity}/mutations.gql`, content: renderGqlFile(ir, mutations, context) });
531
+ }
532
+ }
533
+ // No TS emitted here: Data Connect's own codegen generates the typed SDK
534
+ // from these .gql files; shared types are distributed via libs-level
535
+ // generators (typescript/zod/...), not into connector source dirs.
536
+ }
537
+ return files;
538
+ },
539
+ });
@@ -0,0 +1,11 @@
1
+ import { Generator } from '../generator.js';
2
+ /**
3
+ * Generates Firestore projection Zod schemas. Each projection derives from a
4
+ * Data Connect model (`from`) and applies the projection reality: relations
5
+ * become resolved string ids, timestamps become `z.date()`, optional fields use
6
+ * `.nullable()`, a `_meta_` consistency envelope is attached, and inline fields
7
+ * add denormalized data (e.g. flattened graph edges) — mirroring a
8
+ * hand-written Firestore schema library.
9
+ */
10
+ export declare const createFirestoreProjectionGenerator: () => Generator;
11
+ //# sourceMappingURL=firestore-projection-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firestore-projection-generator.d.ts","sourceRoot":"","sources":["../../../../../src/lib/_internal/generators/firestore/firestore-projection-generator.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,SAAS,EAAoB,MAAM,iBAAiB,CAAA;AAuF5E;;;;;;;GAOG;AACH,eAAO,MAAM,kCAAkC,QAAO,SAgBpD,CAAA"}
@@ -0,0 +1,112 @@
1
+ import { findModel } from '../../ir/ir.js';
2
+ import { singleQuote } from '../support/naming.js';
3
+ import { isRelation, relationFkName } from '../support/relations.js';
4
+ import { headerBlocks } from '../support/header.js';
5
+ // Firestore stores timestamps as dates and represents relations as resolved
6
+ // (hashids-encoded) string ids, regardless of the Data Connect id's type.
7
+ const SCALAR_ZOD = {
8
+ string: 'z.string()',
9
+ int: 'z.number()',
10
+ int64: 'z.number()',
11
+ float: 'z.number()',
12
+ boolean: 'z.boolean()',
13
+ timestamp: 'z.date()',
14
+ date: 'z.date()',
15
+ json: 'z.unknown()',
16
+ id: 'z.string()',
17
+ };
18
+ const META_BLOCK = [
19
+ 'export const _Meta_OperationSchema = z.object({',
20
+ ' id: z.string(),',
21
+ ' identifierId: z.string(),',
22
+ ' updatedAt: z.number(),',
23
+ '})',
24
+ 'export const _Meta_Schema = z.object({',
25
+ ' scope: z.number().nullable(),',
26
+ ' sys: z.number().nullable(),',
27
+ ' op: _Meta_OperationSchema.nullable(),',
28
+ '})',
29
+ 'export type _Meta_ = z.infer<typeof _Meta_Schema>',
30
+ ].join('\n');
31
+ const fsField = (ir, field) => {
32
+ const name = isRelation(field) ? relationFkName(field) : field.name;
33
+ let schema;
34
+ if (isRelation(field)) {
35
+ schema = 'z.string()'; // resolved, encoded foreign-key id
36
+ }
37
+ else if (field.type.kind === 'enum') {
38
+ const irEnum = ir.enums.find(e => e.name === field.type.name);
39
+ schema = irEnum ? `z.enum([${irEnum.values.map(singleQuote).join(', ')}])` : 'z.string()';
40
+ }
41
+ else if (field.type.kind === 'scalar') {
42
+ schema = SCALAR_ZOD[field.type.name];
43
+ }
44
+ else {
45
+ // embedded model or unresolved: kept opaque in the projection.
46
+ schema = 'z.unknown()';
47
+ }
48
+ if (field.list)
49
+ schema = `z.array(${schema})`;
50
+ if (field.optional)
51
+ schema = `${schema}.nullable()`;
52
+ return { name, schema };
53
+ };
54
+ const effectiveFields = (ir, doc) => {
55
+ const base = doc.from ? findModel(ir, doc.from) : undefined;
56
+ let baseFields = base?.fields ?? [];
57
+ if (doc.pick.length > 0) {
58
+ baseFields = baseFields.filter(field => doc.pick.includes(field.name));
59
+ }
60
+ else if (doc.omit.length > 0) {
61
+ baseFields = baseFields.filter(field => !doc.omit.includes(field.name));
62
+ }
63
+ // Insertion order = base fields, then added/overriding fields (override keeps position).
64
+ const entries = new Map();
65
+ for (const field of baseFields) {
66
+ const { name, schema } = fsField(ir, field);
67
+ entries.set(name, schema);
68
+ }
69
+ for (const field of doc.fields) {
70
+ const { name, schema } = fsField(ir, field);
71
+ entries.set(name, schema);
72
+ }
73
+ return [...entries].map(([name, schema]) => ({ name, schema }));
74
+ };
75
+ const renderDoc = (ir, doc) => {
76
+ const lines = [];
77
+ if (doc.collection)
78
+ lines.push(`// ${doc.collection}`);
79
+ else if (doc.description)
80
+ lines.push(`// ${doc.description}`);
81
+ const fields = effectiveFields(ir, doc).map(field => ` ${field.name}: ${field.schema},`);
82
+ if (doc.meta)
83
+ fields.push(' _meta_: _Meta_Schema,');
84
+ lines.push(`export const ${doc.name}Schema = z.object({\n${fields.join('\n')}\n})`);
85
+ lines.push(`export type ${doc.name} = z.infer<typeof ${doc.name}Schema>`);
86
+ return lines.join('\n');
87
+ };
88
+ /**
89
+ * Generates Firestore projection Zod schemas. Each projection derives from a
90
+ * Data Connect model (`from`) and applies the projection reality: relations
91
+ * become resolved string ids, timestamps become `z.date()`, optional fields use
92
+ * `.nullable()`, a `_meta_` consistency envelope is attached, and inline fields
93
+ * add denormalized data (e.g. flattened graph edges) — mirroring a
94
+ * hand-written Firestore schema library.
95
+ */
96
+ export const createFirestoreProjectionGenerator = () => ({
97
+ name: 'firestore',
98
+ description: 'Firestore projection Zod schemas (derived from Data Connect models)',
99
+ generate(ir, context) {
100
+ if (ir.firestore.length === 0) {
101
+ return [];
102
+ }
103
+ const blocks = [...headerBlocks(context), "import { z } from 'zod'"];
104
+ if (ir.firestore.some(doc => doc.meta)) {
105
+ blocks.push(META_BLOCK);
106
+ }
107
+ for (const doc of ir.firestore) {
108
+ blocks.push(renderDoc(ir, doc));
109
+ }
110
+ return [{ path: 'firestore.ts', content: `${blocks.join('\n\n')}\n` }];
111
+ },
112
+ });
@@ -0,0 +1,11 @@
1
+ import { Generator } from '../generator.js';
2
+ /**
3
+ * Split variant of the Firestore projection generator, mirroring the
4
+ * hand-maintained `libs/types/src/schema/firestore/*` layout: one file per
5
+ * projection at `firestore/<collectionName>.ts`, FS-only enums (frozen consts)
6
+ * and embedded value objects co-located at their first referencing projection,
7
+ * the `_meta_` envelope under `firestore/_/`, and a `firestore.ts` barrel that
8
+ * also carries the `FIRESTORE_DATABASES` constants.
9
+ */
10
+ export declare const createFirestoreSplitGenerator: () => Generator;
11
+ //# sourceMappingURL=firestore-split-generator.d.ts.map