firebase-contract 0.1.0-rc.11 → 0.1.0-rc.12
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 +20 -1
- package/dist/lib/_internal/compiler/compiler.d.ts.map +1 -1
- package/dist/lib/_internal/compiler/compiler.js +42 -4
- package/dist/lib/_internal/validation/rules.d.ts +32 -0
- package/dist/lib/_internal/validation/rules.d.ts.map +1 -1
- package/dist/lib/_internal/validation/rules.js +119 -0
- package/dist/lib/cli/commands/index.d.ts.map +1 -1
- package/dist/lib/cli/commands/index.js +2 -0
- package/dist/lib/cli/commands/where.d.ts +3 -0
- package/dist/lib/cli/commands/where.d.ts.map +1 -0
- package/dist/lib/cli/commands/where.js +57 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ fbc init # scaffold contract.yml + firebase-contract.json
|
|
|
34
34
|
fbc validate # parse, resolve imports, semantically validate
|
|
35
35
|
fbc generate # run every generator declared across the contract graph
|
|
36
36
|
fbc inspect # print the normalized IR (debugging)
|
|
37
|
+
fbc where <Name> # locate the yml defining a type (logical / gqlName / fsName / table)
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
### One-command generation
|
|
@@ -103,7 +104,13 @@ generators:
|
|
|
103
104
|
and errors for generators without one.
|
|
104
105
|
- **`options`** — free-form string map passed through to the generator.
|
|
105
106
|
Currently: `typesImport` (api-types, task-payloads, data-connect-operations)
|
|
106
|
-
overrides the
|
|
107
|
+
overrides the types import path. For api-types and task-payloads this is
|
|
108
|
+
normally unnecessary: when the option is absent and a `typescript` generator
|
|
109
|
+
is declared (nearest-first: same yml → root), the import specifier is derived
|
|
110
|
+
automatically as the relative path from the output file to the declared
|
|
111
|
+
barrel — moving or renaming the barrel (`out` / `file`) re-derives it. An
|
|
112
|
+
explicit `typesImport` always wins; without a typescript declaration the
|
|
113
|
+
generator's built-in default (`./types`) applies.
|
|
107
114
|
- **`header`** — per-generator banner override: `default` for the built-in
|
|
108
115
|
AUTO-GENERATED banner, any text for a custom comment, `""` to suppress the
|
|
109
116
|
banner for this generator. Wins over the contract-level `header:` and the
|
|
@@ -174,6 +181,18 @@ in other contracts (relative paths or npm packages). Imports are resolved
|
|
|
174
181
|
transitively with cycle/duplicate detection and diamond dedup, so you can split
|
|
175
182
|
a large contract across files that mirror your repo layout.
|
|
176
183
|
|
|
184
|
+
**Type names are globally unique.** The namespace is flat across the whole
|
|
185
|
+
import graph: defining the same name twice — same kind (`DUPLICATE_DEFINITION`)
|
|
186
|
+
or across kinds/derived identifiers, e.g. an enum and a model both named
|
|
187
|
+
`Status`, or two enums whose `CONSTANT_CASE` consts collide (`NAME_COLLISION`)
|
|
188
|
+
— aborts generation. Uniqueness is enforced per output namespace with its
|
|
189
|
+
*effective* names, so `fsName`/`gqlName` renames legitimately reuse a name in a
|
|
190
|
+
different output: GraphQL names are checked per `data-connect-graphql` scope
|
|
191
|
+
(services may map distinct logical names onto the same gqlName), and the
|
|
192
|
+
firestore module is checked under `fsName ?? name` alongside doc names. A bare
|
|
193
|
+
type reference therefore always resolves to exactly one definition — use
|
|
194
|
+
`fbc where <Name>` to find it.
|
|
195
|
+
|
|
177
196
|
```yaml
|
|
178
197
|
version: 1
|
|
179
198
|
imports:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/compiler/compiler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAoB,MAAM,mBAAmB,CAAA;AAEhE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAU,WAAW,EAAqC,MAAM,2BAA2B,CAAA;AAElG,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/compiler/compiler.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAoB,MAAM,mBAAmB,CAAA;AAEhE,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAU,WAAW,EAAqC,MAAM,2BAA2B,CAAA;AAElG,OAAO,EAAkB,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAI3E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAI5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC7D,OAAO,EAEL,cAAc,EAGf,MAAM,kBAAkB,CAAA;AAEzB,MAAM,WAAW,cAAc;IAC7B,6DAA6D;IAC7D,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,EAAE,CAAA;IACN,SAAS,EAAE,WAAW,EAAE,CAAA;IACxB,WAAW,EAAE,UAAU,EAAE,CAAA;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,WAAW,MAAM,EAAE,UAAS,cAAmB,KAAG,aAkBzE,CAAA;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,oDAAoD;IACpD,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,oFAAoF;IACpF,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,mFAAmF;IACnF,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;AAED,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,KAAK,EAAE,aAAa,EAAE,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,oBAAoB,EAAE,CAAA;IAC/B,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,EAAE,EAAE,OAAO,CAAA;CACZ;AAyOD;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GACtB,WAAW,MAAM,EACjB,UAAS;IACP,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,GAAG,CAAC,EAAE,IAAI,CAAA;CACN,KACL,iBA0FF,CAAA;AAED,MAAM,WAAW,SAAS;IACxB,sEAAsE;IACtE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,aAAa,EAAE,KAAG,SAUlD,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,EAAE,CAAA;IACN,WAAW,EAAE,UAAU,EAAE,CAAA;IACzB,oFAAoF;IACpF,KAAK,EAAE,aAAa,EAAE,CAAA;IACtB,0DAA0D;IAC1D,EAAE,EAAE,OAAO,CAAA;CACZ;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,EAAE,SAAS,eAAe,KAAG,cAgEtE,CAAA"}
|
|
@@ -4,6 +4,7 @@ import { error, hasErrors } from '../diagnostics.js';
|
|
|
4
4
|
import { buildIr } from '../ir/build-ir.js';
|
|
5
5
|
import { resolveImports } from '../resolver/import-resolver.js';
|
|
6
6
|
import { createFsLoader } from '../resolver/module-loader.js';
|
|
7
|
+
import { graphqlNameCollisions } from '../validation/rules.js';
|
|
7
8
|
import { validateIr } from '../validation/validate.js';
|
|
8
9
|
import { createDefaultRegistry } from '../generators/index.js';
|
|
9
10
|
import { toHeaderComment } from '../generators/support/header.js';
|
|
@@ -20,10 +21,18 @@ export const compile = (entryPath, options = {}) => {
|
|
|
20
21
|
const { documents, diagnostics: importDiagnostics } = resolveImports(entryPath, loader);
|
|
21
22
|
const { ir, diagnostics: buildDiagnostics } = buildIr(documents);
|
|
22
23
|
const validationDiagnostics = validateIr(ir);
|
|
24
|
+
// A GraphQL schema is generated per data-connect-graphql declaration from the
|
|
25
|
+
// declaring yml's subtree, so its name uniqueness only holds at that scope —
|
|
26
|
+
// distinct services may reuse a gqlName. generateAll re-compiles each target
|
|
27
|
+
// with the declaring yml as entry, which is exactly when this check applies.
|
|
28
|
+
const entryDoc = documents[documents.length - 1];
|
|
29
|
+
const graphqlDiagnostics = entryDoc?.generatorDecls?.some(decl => decl.generator === 'data-connect-graphql')
|
|
30
|
+
? graphqlNameCollisions(ir)
|
|
31
|
+
: [];
|
|
23
32
|
return {
|
|
24
33
|
ir,
|
|
25
34
|
documents,
|
|
26
|
-
diagnostics: [...importDiagnostics, ...buildDiagnostics, ...validationDiagnostics],
|
|
35
|
+
diagnostics: [...importDiagnostics, ...buildDiagnostics, ...validationDiagnostics, ...graphqlDiagnostics],
|
|
27
36
|
};
|
|
28
37
|
};
|
|
29
38
|
/** `apis:/tasks:/events:` section a raw api belongs to (drives the section defaults lookup). */
|
|
@@ -74,6 +83,30 @@ const resolveOutDir = (out, declaringDoc, rootDoc, diagnostics) => {
|
|
|
74
83
|
/** Find a generator declaration by name — nearest first: the declaring yml, then the root yml. */
|
|
75
84
|
const findGeneratorDecl = (name, doc, rootDoc) => doc.generatorDecls?.find(decl => decl.generator === name) ??
|
|
76
85
|
rootDoc.generatorDecls?.find(decl => decl.generator === name);
|
|
86
|
+
/** Generators whose output imports the shared types barrel via `options.typesImport`. */
|
|
87
|
+
const TYPES_IMPORT_GENERATORS = new Set(['api-types', 'task-payloads']);
|
|
88
|
+
/**
|
|
89
|
+
* Derive the default `typesImport` specifier from the typescript declaration
|
|
90
|
+
* itself: the relative path from the job's output directory to the declared
|
|
91
|
+
* barrel (`out` + `file`, default `types.ts`). The declaration resolves
|
|
92
|
+
* nearest-first (same yml → root), mirroring how entries resolve declarations,
|
|
93
|
+
* so both outputs stay in sync when the barrel is moved or renamed. An explicit
|
|
94
|
+
* `options.typesImport` always wins; without a typescript declaration in scope
|
|
95
|
+
* the generator's built-in default applies.
|
|
96
|
+
*/
|
|
97
|
+
const deriveTypesImport = (outDir, doc, rootDoc) => {
|
|
98
|
+
const decl = findGeneratorDecl('typescript', doc, rootDoc);
|
|
99
|
+
if (!decl)
|
|
100
|
+
return undefined;
|
|
101
|
+
const declaringDoc = doc.generatorDecls?.includes(decl) ? doc : rootDoc;
|
|
102
|
+
// Alias problems are reported when the typescript job itself is built.
|
|
103
|
+
const barrelDir = resolveOutDir(decl.out, declaringDoc, rootDoc, []);
|
|
104
|
+
if (barrelDir === undefined)
|
|
105
|
+
return undefined;
|
|
106
|
+
const barrel = join(barrelDir, (decl.file ?? 'types.ts').replace(/\.ts$/, ''));
|
|
107
|
+
const specifier = relative(outDir, barrel);
|
|
108
|
+
return specifier.startsWith('.') ? specifier : `./${specifier}`;
|
|
109
|
+
};
|
|
77
110
|
/**
|
|
78
111
|
* Build generator jobs from the declaration/application DSL:
|
|
79
112
|
* - api-scoped generators run for entries that opt in (section `defaults` →
|
|
@@ -98,14 +131,12 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
|
|
|
98
131
|
const output = {};
|
|
99
132
|
const file = use.file ?? decl?.file;
|
|
100
133
|
const split = use.split ?? decl?.split ?? generator?.defaultOutput?.split;
|
|
101
|
-
|
|
134
|
+
let options = use.options ?? decl?.options;
|
|
102
135
|
const header = use.header ?? decl?.header;
|
|
103
136
|
if (file !== undefined)
|
|
104
137
|
output.file = file;
|
|
105
138
|
if (split !== undefined)
|
|
106
139
|
output.split = split;
|
|
107
|
-
if (options !== undefined)
|
|
108
|
-
output.options = options;
|
|
109
140
|
// Split mode needs a per-api file name; bundled mode cannot expand one.
|
|
110
141
|
const effectiveFile = file ?? generator?.defaultOutput?.file;
|
|
111
142
|
if (effectiveFile !== undefined && split === false && API_PLACEHOLDER_RE.test(effectiveFile)) {
|
|
@@ -123,6 +154,13 @@ const buildGeneratorJobs = (doc, rootDoc, registry, diagnostics) => {
|
|
|
123
154
|
const outDir = resolveOutDir(expanded, doc, rootDoc, diagnostics);
|
|
124
155
|
if (outDir === undefined)
|
|
125
156
|
continue;
|
|
157
|
+
if (TYPES_IMPORT_GENERATORS.has(use.generator) && options?.['typesImport'] === undefined) {
|
|
158
|
+
const derived = deriveTypesImport(outDir, doc, rootDoc);
|
|
159
|
+
if (derived !== undefined)
|
|
160
|
+
options = { ...(options ?? {}), typesImport: derived };
|
|
161
|
+
}
|
|
162
|
+
if (options !== undefined)
|
|
163
|
+
output.options = options;
|
|
126
164
|
const key = [use.generator, outDir, file ?? '', String(split ?? ''), JSON.stringify(options ?? null), header ?? '\u0001'].join('\u0000');
|
|
127
165
|
const existing = grouped.get(key);
|
|
128
166
|
if (existing) {
|
|
@@ -28,5 +28,37 @@ export declare const apis: ValidationRule;
|
|
|
28
28
|
export declare const firestore: ValidationRule;
|
|
29
29
|
/** Union variants must be known models, each carrying the discriminant field. */
|
|
30
30
|
export declare const unions: ValidationRule;
|
|
31
|
+
/**
|
|
32
|
+
* Type names must be unique within every output namespace — the export surface
|
|
33
|
+
* where generated identifiers converge (a split layout still merges into one
|
|
34
|
+
* barrel, so physical files are irrelevant). Renames (`gqlName` / `fsName`)
|
|
35
|
+
* deliberately move a type between namespaces, so each namespace is checked
|
|
36
|
+
* independently against its *effective* names:
|
|
37
|
+
*
|
|
38
|
+
* - TypeScript types module: model/enum/union logical names, enum const
|
|
39
|
+
* (`CONSTANT_CASE`) and `…Key` companions, plus the reserved `Json` type
|
|
40
|
+
* - zod schemas module: logical names and their `…Schema` companions
|
|
41
|
+
* - unions module: union names/schemas plus the variant schemas it imports
|
|
42
|
+
* - firestore module: doc names, co-located enums/embedded models under
|
|
43
|
+
* `fsName ?? name`, plus the `_Meta_…` / `FIRESTORE_DATABASES` runtime
|
|
44
|
+
*
|
|
45
|
+
* GraphQL names are checked separately ({@link graphqlNameCollisions}) because
|
|
46
|
+
* that namespace is scoped per data-connect-graphql declaration, not global.
|
|
47
|
+
*
|
|
48
|
+
* Same-kind duplicates are already DUPLICATE_DEFINITION at merge time; this
|
|
49
|
+
* rule closes every cross-kind and derived-identifier hole. Each colliding
|
|
50
|
+
* definition pair is reported once (the first namespace that trips).
|
|
51
|
+
*/
|
|
52
|
+
export declare const nameCollisions: ValidationRule;
|
|
53
|
+
/**
|
|
54
|
+
* GraphQL name uniqueness under `gqlName ?? name`. Unlike {@link nameCollisions}
|
|
55
|
+
* this is NOT a default rule: a GraphQL schema exists per `data-connect-graphql`
|
|
56
|
+
* declaration, scoped to the declaring yml's import subtree — distinct services
|
|
57
|
+
* may intentionally reuse a gqlName (e.g. a `T2…` logical name mapped back to
|
|
58
|
+
* the plain name inside its own service). The compiler runs this check only
|
|
59
|
+
* when the entry document itself declares a GraphQL-emitting generator, i.e.
|
|
60
|
+
* exactly at the scope the schema is generated from.
|
|
61
|
+
*/
|
|
62
|
+
export declare const graphqlNameCollisions: ValidationRule;
|
|
31
63
|
export declare const DEFAULT_RULES: ValidationRule[];
|
|
32
64
|
//# sourceMappingURL=rules.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/validation/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../../../src/lib/_internal/validation/rules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,mBAAmB,CAAA;AAG9D,OAAO,EAAa,EAAE,EAAgB,MAAM,aAAa,CAAA;AAEzD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,CAAA;AAIrD,uEAAuE;AACvE,eAAO,MAAM,eAAe,EAAE,cAU3B,CAAA;AAEH,6CAA6C;AAC7C,eAAO,MAAM,UAAU,EAAE,cAQpB,CAAA;AAEL,oCAAoC;AACpC,eAAO,MAAM,mBAAmB,EAAE,cAgB9B,CAAA;AAEJ,kFAAkF;AAClF,eAAO,MAAM,UAAU,EAAE,cAiCxB,CAAA;AAED,iFAAiF;AACjF,eAAO,MAAM,QAAQ,EAAE,cA+BnB,CAAA;AAEJ,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,EAAE,cAkC9B,CAAA;AAEJ,mEAAmE;AACnE,eAAO,MAAM,cAAc,EAAE,cAU1B,CAAA;AAEH,mGAAmG;AACnG,eAAO,MAAM,UAAU,EAAE,cA+ErB,CAAA;AAEJ,uFAAuF;AACvF,eAAO,MAAM,IAAI,EAAE,cAYf,CAAA;AAEJ,wFAAwF;AACxF,eAAO,MAAM,SAAS,EAAE,cA+BpB,CAAA;AAEJ,iFAAiF;AACjF,eAAO,MAAM,MAAM,EAAE,cAoBjB,CAAA;AAaJ;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,cAAc,EAAE,cA+E5B,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,cAmBnC,CAAA;AAED,eAAO,MAAM,aAAa,EAAE,cAAc,EAazC,CAAA"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { error, warning } from '../diagnostics.js';
|
|
2
|
+
import { constantCase } from '../generators/support/naming.js';
|
|
3
|
+
import { isTable } from '../generators/support/split.js';
|
|
2
4
|
import { findModel } from '../ir/ir.js';
|
|
3
5
|
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4
6
|
/** Every field type must resolve to a known scalar, enum, or model. */
|
|
@@ -267,6 +269,122 @@ export const unions = ir => ir.unions.flatMap(union => {
|
|
|
267
269
|
}
|
|
268
270
|
return diagnostics;
|
|
269
271
|
});
|
|
272
|
+
const RESERVED_OWNER = 'the generated runtime';
|
|
273
|
+
/**
|
|
274
|
+
* Type names must be unique within every output namespace — the export surface
|
|
275
|
+
* where generated identifiers converge (a split layout still merges into one
|
|
276
|
+
* barrel, so physical files are irrelevant). Renames (`gqlName` / `fsName`)
|
|
277
|
+
* deliberately move a type between namespaces, so each namespace is checked
|
|
278
|
+
* independently against its *effective* names:
|
|
279
|
+
*
|
|
280
|
+
* - TypeScript types module: model/enum/union logical names, enum const
|
|
281
|
+
* (`CONSTANT_CASE`) and `…Key` companions, plus the reserved `Json` type
|
|
282
|
+
* - zod schemas module: logical names and their `…Schema` companions
|
|
283
|
+
* - unions module: union names/schemas plus the variant schemas it imports
|
|
284
|
+
* - firestore module: doc names, co-located enums/embedded models under
|
|
285
|
+
* `fsName ?? name`, plus the `_Meta_…` / `FIRESTORE_DATABASES` runtime
|
|
286
|
+
*
|
|
287
|
+
* GraphQL names are checked separately ({@link graphqlNameCollisions}) because
|
|
288
|
+
* that namespace is scoped per data-connect-graphql declaration, not global.
|
|
289
|
+
*
|
|
290
|
+
* Same-kind duplicates are already DUPLICATE_DEFINITION at merge time; this
|
|
291
|
+
* rule closes every cross-kind and derived-identifier hole. Each colliding
|
|
292
|
+
* definition pair is reported once (the first namespace that trips).
|
|
293
|
+
*/
|
|
294
|
+
export const nameCollisions = ir => {
|
|
295
|
+
const diagnostics = [];
|
|
296
|
+
const reportedPairs = new Set();
|
|
297
|
+
const check = (space, claims) => {
|
|
298
|
+
const first = new Map();
|
|
299
|
+
for (const claim of claims) {
|
|
300
|
+
const prev = first.get(claim.identifier);
|
|
301
|
+
if (!prev) {
|
|
302
|
+
first.set(claim.identifier, claim);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (prev.owner === claim.owner)
|
|
306
|
+
continue;
|
|
307
|
+
const pair = [prev.owner, claim.owner].sort().join(' vs ');
|
|
308
|
+
if (reportedPairs.has(pair))
|
|
309
|
+
continue;
|
|
310
|
+
reportedPairs.add(pair);
|
|
311
|
+
diagnostics.push(error('NAME_COLLISION', `${prev.owner} and ${claim.owner} both emit "${claim.identifier}" in the ${space} output`, {
|
|
312
|
+
file: claim.file ?? prev.file,
|
|
313
|
+
path: claim.path,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const claim = (owner, file, path, identifiers) => identifiers.map(identifier => ({ owner, identifier, file, path }));
|
|
318
|
+
// TypeScript types module: the logical namespace every other output derives from.
|
|
319
|
+
check('TypeScript types', [
|
|
320
|
+
{ owner: RESERVED_OWNER, identifier: 'Json', path: '(generated)' },
|
|
321
|
+
...ir.models.flatMap(m => claim(`model "${m.name}"`, m.sourceFile, `models.${m.name}`, [m.name])),
|
|
322
|
+
...ir.enums.flatMap(e => claim(`enum "${e.name}"`, e.sourceFile, `enums.${e.name}`, [e.name, `${e.name}Key`, constantCase(e.name)])),
|
|
323
|
+
...ir.unions.flatMap(u => claim(`union "${u.name}"`, u.sourceFile, `unions.${u.name}`, [u.name])),
|
|
324
|
+
]);
|
|
325
|
+
// zod schemas module: logical names plus their `…Schema` consts.
|
|
326
|
+
check('zod schemas', [
|
|
327
|
+
...ir.models.flatMap(m => claim(`model "${m.name}"`, m.sourceFile, `models.${m.name}`, [m.name, `${m.name}Schema`])),
|
|
328
|
+
...ir.enums.flatMap(e => claim(`enum "${e.name}"`, e.sourceFile, `enums.${e.name}`, [e.name, `${e.name}Schema`])),
|
|
329
|
+
]);
|
|
330
|
+
// unions module: union declarations plus the variant schemas imported into it.
|
|
331
|
+
const variantOwners = new Map();
|
|
332
|
+
for (const union of ir.unions)
|
|
333
|
+
for (const variant of union.variants)
|
|
334
|
+
variantOwners.set(variant, `model "${variant}"`);
|
|
335
|
+
check('unions', [
|
|
336
|
+
...ir.unions.flatMap(u => claim(`union "${u.name}"`, u.sourceFile, `unions.${u.name}`, [u.name, `${u.name}Schema`])),
|
|
337
|
+
...[...variantOwners.entries()].map(([variant, owner]) => {
|
|
338
|
+
const model = findModel(ir, variant);
|
|
339
|
+
return { owner, identifier: `${variant}Schema`, file: model?.sourceFile, path: `models.${variant}` };
|
|
340
|
+
}),
|
|
341
|
+
]);
|
|
342
|
+
// firestore module: doc schemas + co-located enums/embedded models under fsName-effective names.
|
|
343
|
+
// Tables are imported as `Dc…Schema` aliases, so they don't claim names here.
|
|
344
|
+
check('firestore', [
|
|
345
|
+
...['_Meta_', '_Meta_Schema', '_Meta_Operation', '_Meta_OperationSchema', 'FIRESTORE_DATABASES', 'FirestoreDatabaseKey', 'FirestoreDatabaseId'].map(identifier => ({ owner: RESERVED_OWNER, identifier, path: '(generated)' })),
|
|
346
|
+
...ir.firestore.flatMap(d => claim(`firestore doc "${d.name}"`, d.sourceFile, `firestore.${d.name}`, [d.name, `${d.name}Schema`])),
|
|
347
|
+
...ir.enums.flatMap(e => {
|
|
348
|
+
const fs = e.fsName ?? e.name;
|
|
349
|
+
return claim(`enum "${e.name}"`, e.sourceFile, `enums.${e.name}`, [fs, `${fs}Key`, constantCase(fs), `${fs}Schema`]);
|
|
350
|
+
}),
|
|
351
|
+
...ir.models
|
|
352
|
+
.filter(m => !isTable(m))
|
|
353
|
+
.flatMap(m => {
|
|
354
|
+
const fs = m.fsName ?? m.name;
|
|
355
|
+
return claim(`model "${m.name}"`, m.sourceFile, `models.${m.name}`, [fs, `${fs}Schema`]);
|
|
356
|
+
}),
|
|
357
|
+
]);
|
|
358
|
+
return diagnostics;
|
|
359
|
+
};
|
|
360
|
+
/**
|
|
361
|
+
* GraphQL name uniqueness under `gqlName ?? name`. Unlike {@link nameCollisions}
|
|
362
|
+
* this is NOT a default rule: a GraphQL schema exists per `data-connect-graphql`
|
|
363
|
+
* declaration, scoped to the declaring yml's import subtree — distinct services
|
|
364
|
+
* may intentionally reuse a gqlName (e.g. a `T2…` logical name mapped back to
|
|
365
|
+
* the plain name inside its own service). The compiler runs this check only
|
|
366
|
+
* when the entry document itself declares a GraphQL-emitting generator, i.e.
|
|
367
|
+
* exactly at the scope the schema is generated from.
|
|
368
|
+
*/
|
|
369
|
+
export const graphqlNameCollisions = ir => {
|
|
370
|
+
const diagnostics = [];
|
|
371
|
+
const first = new Map();
|
|
372
|
+
const claims = [
|
|
373
|
+
...ir.models.map(m => ({ owner: `model "${m.name}"`, name: m.gqlName ?? m.name, file: m.sourceFile, path: `models.${m.name}` })),
|
|
374
|
+
...ir.enums.map(e => ({ owner: `enum "${e.name}"`, name: e.gqlName ?? e.name, file: e.sourceFile, path: `enums.${e.name}` })),
|
|
375
|
+
];
|
|
376
|
+
for (const { owner, name, file, path } of claims) {
|
|
377
|
+
const prev = first.get(name);
|
|
378
|
+
if (!prev) {
|
|
379
|
+
first.set(name, { owner, file, path });
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (prev.owner === owner)
|
|
383
|
+
continue;
|
|
384
|
+
diagnostics.push(error('NAME_COLLISION', `${prev.owner} and ${owner} both emit "${name}" in the GraphQL schema output`, { file: file ?? prev.file, path }));
|
|
385
|
+
}
|
|
386
|
+
return diagnostics;
|
|
387
|
+
};
|
|
270
388
|
export const DEFAULT_RULES = [
|
|
271
389
|
unresolvedTypes,
|
|
272
390
|
emptyEnums,
|
|
@@ -279,4 +397,5 @@ export const DEFAULT_RULES = [
|
|
|
279
397
|
apis,
|
|
280
398
|
firestore,
|
|
281
399
|
unions,
|
|
400
|
+
nameCollisions,
|
|
282
401
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/cli/commands/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/cli/commands/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAQnC,iDAAiD;AACjD,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,KAAG,IAMnD,CAAA"}
|
|
@@ -2,10 +2,12 @@ import { registerGenerate } from './generate.js';
|
|
|
2
2
|
import { registerInit } from './init.js';
|
|
3
3
|
import { registerInspect } from './inspect.js';
|
|
4
4
|
import { registerValidate } from './validate.js';
|
|
5
|
+
import { registerWhere } from './where.js';
|
|
5
6
|
/** Register every CLI command on the program. */
|
|
6
7
|
export const registerCommands = (program) => {
|
|
7
8
|
registerInit(program);
|
|
8
9
|
registerValidate(program);
|
|
9
10
|
registerGenerate(program);
|
|
10
11
|
registerInspect(program);
|
|
12
|
+
registerWhere(program);
|
|
11
13
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"where.d.ts","sourceRoot":"","sources":["../../../../src/lib/cli/commands/where.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA+CnC,eAAO,MAAM,aAAa,GAAI,SAAS,OAAO,KAAG,IA2BhD,CAAA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { compile } from '../../_internal/compiler/compiler.js';
|
|
2
|
+
import { hasErrors } from '../../_internal/diagnostics.js';
|
|
3
|
+
import { formatDiagnostics } from '../../_internal/format-diagnostics.js';
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
const definitionsOf = (ir) => [
|
|
6
|
+
...ir.models.map(m => ({
|
|
7
|
+
kind: 'model',
|
|
8
|
+
name: m.name,
|
|
9
|
+
sourceFile: m.sourceFile,
|
|
10
|
+
yamlPath: `models.${m.name}`,
|
|
11
|
+
renames: [
|
|
12
|
+
...(m.gqlName ? [`gqlName: ${m.gqlName}`] : []),
|
|
13
|
+
...(m.fsName ? [`fsName: ${m.fsName}`] : []),
|
|
14
|
+
...(m.table ? [`table: ${m.table}`] : []),
|
|
15
|
+
],
|
|
16
|
+
})),
|
|
17
|
+
...ir.enums.map(e => ({
|
|
18
|
+
kind: 'enum',
|
|
19
|
+
name: e.name,
|
|
20
|
+
sourceFile: e.sourceFile,
|
|
21
|
+
yamlPath: `enums.${e.name}`,
|
|
22
|
+
renames: [...(e.gqlName ? [`gqlName: ${e.gqlName}`] : []), ...(e.fsName ? [`fsName: ${e.fsName}`] : [])],
|
|
23
|
+
})),
|
|
24
|
+
...ir.unions.map(u => ({ kind: 'union', name: u.name, sourceFile: u.sourceFile, yamlPath: `unions.${u.name}`, renames: [] })),
|
|
25
|
+
...ir.firestore.map(d => ({ kind: 'firestore doc', name: d.name, sourceFile: d.sourceFile, yamlPath: `firestore.${d.name}`, renames: [] })),
|
|
26
|
+
...ir.envelopes.map(e => ({ kind: 'envelope', name: e.name, sourceFile: e.sourceFile, yamlPath: `envelopes.${e.name}`, renames: [] })),
|
|
27
|
+
...ir.apis.map(a => ({ kind: 'api', name: a.name, sourceFile: a.sourceFile, yamlPath: `apis.${a.name}`, renames: [] })),
|
|
28
|
+
...ir.operations.map(o => ({ kind: 'operation', name: o.name, sourceFile: o.sourceFile, yamlPath: `operations.${o.name}`, renames: [] })),
|
|
29
|
+
];
|
|
30
|
+
/** True when the definition is known by `query` under any of its names. */
|
|
31
|
+
const matches = (definition, query) => definition.name === query || definition.renames.some(rename => rename.endsWith(`: ${query}`));
|
|
32
|
+
export const registerWhere = (program) => {
|
|
33
|
+
program
|
|
34
|
+
.command('where')
|
|
35
|
+
.description('Locate the yml that defines a type (matches logical, gqlName, fsName, and table names)')
|
|
36
|
+
.argument('<name>', 'type name to look up')
|
|
37
|
+
.argument('[entry]', 'entry contract file')
|
|
38
|
+
.action((name, entryArg) => {
|
|
39
|
+
const config = loadConfig();
|
|
40
|
+
const entry = entryArg ?? config.entry ?? 'contract.yml';
|
|
41
|
+
const { ir, diagnostics } = compile(entry);
|
|
42
|
+
if (hasErrors(diagnostics)) {
|
|
43
|
+
console.error(formatDiagnostics(diagnostics));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const found = definitionsOf(ir).filter(definition => matches(definition, name));
|
|
47
|
+
if (found.length === 0) {
|
|
48
|
+
console.error(`"${name}" is not defined in ${entry} or its imports`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
for (const definition of found) {
|
|
52
|
+
const location = definition.sourceFile ?? '(unknown file)';
|
|
53
|
+
const renames = definition.renames.length > 0 ? ` [${definition.renames.join(', ')}]` : '';
|
|
54
|
+
console.log(`${definition.kind} ${definition.name} ${location} (${definition.yamlPath})${renames}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|