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.
- package/README.md +560 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/lib/_internal/compiler/compiler.d.ts +71 -0
- package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -0
- package/dist/lib/_internal/compiler/compiler.js +104 -0
- package/dist/lib/_internal/compiler/index.d.ts +3 -0
- package/dist/lib/_internal/compiler/index.d.ts.map +1 -0
- package/dist/lib/_internal/compiler/index.js +1 -0
- package/dist/lib/_internal/diagnostics.d.ts +29 -0
- package/dist/lib/_internal/diagnostics.d.ts.map +1 -0
- package/dist/lib/_internal/diagnostics.js +26 -0
- package/dist/lib/_internal/format-diagnostics.d.ts +8 -0
- package/dist/lib/_internal/format-diagnostics.d.ts.map +1 -0
- package/dist/lib/_internal/format-diagnostics.js +10 -0
- package/dist/lib/_internal/generators/api/api-dto-generator.d.ts +8 -0
- package/dist/lib/_internal/generators/api/api-dto-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/api/api-dto-generator.js +134 -0
- package/dist/lib/_internal/generators/api/api-types-generator.d.ts +8 -0
- package/dist/lib/_internal/generators/api/api-types-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/api/api-types-generator.js +105 -0
- package/dist/lib/_internal/generators/api/api-validation-generator.d.ts +7 -0
- package/dist/lib/_internal/generators/api/api-validation-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/api/api-validation-generator.js +73 -0
- package/dist/lib/_internal/generators/api/task-payload-generator.d.ts +9 -0
- package/dist/lib/_internal/generators/api/task-payload-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/api/task-payload-generator.js +96 -0
- package/dist/lib/_internal/generators/config/config-generator.d.ts +9 -0
- package/dist/lib/_internal/generators/config/config-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/config/config-generator.js +71 -0
- package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts +8 -0
- package/dist/lib/_internal/generators/data-connect/adapter-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/data-connect/adapter-generator.js +142 -0
- package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts +15 -0
- package/dist/lib/_internal/generators/data-connect/graphql-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/data-connect/graphql-generator.js +209 -0
- package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts +18 -0
- package/dist/lib/_internal/generators/data-connect/operations-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/data-connect/operations-generator.js +539 -0
- package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts +11 -0
- package/dist/lib/_internal/generators/firestore/firestore-projection-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/firestore/firestore-projection-generator.js +112 -0
- package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts +11 -0
- package/dist/lib/_internal/generators/firestore/firestore-split-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/firestore/firestore-split-generator.js +375 -0
- package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts +12 -0
- package/dist/lib/_internal/generators/firestore/firestore-type-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/firestore/firestore-type-generator.js +81 -0
- package/dist/lib/_internal/generators/generator.d.ts +23 -0
- package/dist/lib/_internal/generators/generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/generator.js +1 -0
- package/dist/lib/_internal/generators/id/id-codec-generator.d.ts +16 -0
- package/dist/lib/_internal/generators/id/id-codec-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/id/id-codec-generator.js +140 -0
- package/dist/lib/_internal/generators/index.d.ts +31 -0
- package/dist/lib/_internal/generators/index.d.ts.map +1 -0
- package/dist/lib/_internal/generators/index.js +64 -0
- package/dist/lib/_internal/generators/registry.d.ts +15 -0
- package/dist/lib/_internal/generators/registry.d.ts.map +1 -0
- package/dist/lib/_internal/generators/registry.js +24 -0
- package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts +9 -0
- package/dist/lib/_internal/generators/sql/sql-migration-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/sql/sql-migration-generator.js +44 -0
- package/dist/lib/_internal/generators/support/constraints.d.ts +8 -0
- package/dist/lib/_internal/generators/support/constraints.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/constraints.js +39 -0
- package/dist/lib/_internal/generators/support/header.d.ts +13 -0
- package/dist/lib/_internal/generators/support/header.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/header.js +17 -0
- package/dist/lib/_internal/generators/support/naming.d.ts +10 -0
- package/dist/lib/_internal/generators/support/naming.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/naming.js +30 -0
- package/dist/lib/_internal/generators/support/relations.d.ts +12 -0
- package/dist/lib/_internal/generators/support/relations.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/relations.js +11 -0
- package/dist/lib/_internal/generators/support/split.d.ts +34 -0
- package/dist/lib/_internal/generators/support/split.d.ts.map +1 -0
- package/dist/lib/_internal/generators/support/split.js +75 -0
- package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts +18 -0
- package/dist/lib/_internal/generators/typescript/typescript-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/typescript/typescript-generator.js +99 -0
- package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts +10 -0
- package/dist/lib/_internal/generators/typescript/typescript-split-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/typescript/typescript-split-generator.js +175 -0
- package/dist/lib/_internal/generators/union/union-generator.d.ts +8 -0
- package/dist/lib/_internal/generators/union/union-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/union/union-generator.js +37 -0
- package/dist/lib/_internal/generators/zod/zod-generator.d.ts +7 -0
- package/dist/lib/_internal/generators/zod/zod-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/zod/zod-generator.js +68 -0
- package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts +9 -0
- package/dist/lib/_internal/generators/zod/zod-split-generator.d.ts.map +1 -0
- package/dist/lib/_internal/generators/zod/zod-split-generator.js +136 -0
- package/dist/lib/_internal/ir/build-ir.d.ts +18 -0
- package/dist/lib/_internal/ir/build-ir.d.ts.map +1 -0
- package/dist/lib/_internal/ir/build-ir.js +430 -0
- package/dist/lib/_internal/ir/ir.d.ts +393 -0
- package/dist/lib/_internal/ir/ir.d.ts.map +1 -0
- package/dist/lib/_internal/ir/ir.js +22 -0
- package/dist/lib/_internal/parser/parse.d.ts +9 -0
- package/dist/lib/_internal/parser/parse.d.ts.map +1 -0
- package/dist/lib/_internal/parser/parse.js +682 -0
- package/dist/lib/_internal/parser/raw-document.d.ts +230 -0
- package/dist/lib/_internal/parser/raw-document.d.ts.map +1 -0
- package/dist/lib/_internal/parser/raw-document.js +6 -0
- package/dist/lib/_internal/resolver/import-resolver.d.ts +25 -0
- package/dist/lib/_internal/resolver/import-resolver.d.ts.map +1 -0
- package/dist/lib/_internal/resolver/import-resolver.js +73 -0
- package/dist/lib/_internal/resolver/module-loader.d.ts +25 -0
- package/dist/lib/_internal/resolver/module-loader.d.ts.map +1 -0
- package/dist/lib/_internal/resolver/module-loader.js +62 -0
- package/dist/lib/_internal/runtime/data-connect-adapter.d.ts +25 -0
- package/dist/lib/_internal/runtime/data-connect-adapter.d.ts.map +1 -0
- package/dist/lib/_internal/runtime/data-connect-adapter.js +24 -0
- package/dist/lib/_internal/runtime/index.d.ts +3 -0
- package/dist/lib/_internal/runtime/index.d.ts.map +1 -0
- package/dist/lib/_internal/runtime/index.js +1 -0
- package/dist/lib/_internal/validation/rules.d.ts +32 -0
- package/dist/lib/_internal/validation/rules.d.ts.map +1 -0
- package/dist/lib/_internal/validation/rules.js +282 -0
- package/dist/lib/_internal/validation/validate.d.ts +9 -0
- package/dist/lib/_internal/validation/validate.d.ts.map +1 -0
- package/dist/lib/_internal/validation/validate.js +6 -0
- package/dist/lib/cli/commands/generate.d.ts +3 -0
- package/dist/lib/cli/commands/generate.d.ts.map +1 -0
- package/dist/lib/cli/commands/generate.js +66 -0
- package/dist/lib/cli/commands/index.d.ts +4 -0
- package/dist/lib/cli/commands/index.d.ts.map +1 -0
- package/dist/lib/cli/commands/index.js +11 -0
- package/dist/lib/cli/commands/init.d.ts +3 -0
- package/dist/lib/cli/commands/init.d.ts.map +1 -0
- package/dist/lib/cli/commands/init.js +72 -0
- package/dist/lib/cli/commands/inspect.d.ts +3 -0
- package/dist/lib/cli/commands/inspect.d.ts.map +1 -0
- package/dist/lib/cli/commands/inspect.js +23 -0
- package/dist/lib/cli/commands/validate.d.ts +3 -0
- package/dist/lib/cli/commands/validate.d.ts.map +1 -0
- package/dist/lib/cli/commands/validate.js +23 -0
- package/dist/lib/cli/config.d.ts +12 -0
- package/dist/lib/cli/config.d.ts.map +1 -0
- package/dist/lib/cli/config.js +20 -0
- package/dist/lib/cli/index.d.ts +3 -0
- package/dist/lib/cli/index.d.ts.map +1 -0
- package/dist/lib/cli/index.js +13 -0
- package/dist/lib/cli/version.d.ts +3 -0
- package/dist/lib/cli/version.d.ts.map +1 -0
- package/dist/lib/cli/version.js +15 -0
- package/dist/lib/modules/runtime.d.ts +2 -0
- package/dist/lib/modules/runtime.d.ts.map +1 -0
- package/dist/lib/modules/runtime.js +3 -0
- 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
|