@stonecrop/schema 0.25.0 → 0.27.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 +23 -0
- package/dist/cli.js +85 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.js +68 -61
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +256 -13
- package/dist/src/cli.js +53 -6
- package/dist/src/component-meta.d.ts.map +1 -1
- package/dist/src/component-meta.js +1 -0
- package/dist/src/converter/aggregate.d.ts +127 -0
- package/dist/src/converter/aggregate.d.ts.map +1 -0
- package/dist/src/converter/aggregate.js +235 -0
- package/dist/src/converter/authored.d.ts +43 -0
- package/dist/src/converter/authored.d.ts.map +1 -0
- package/dist/src/converter/authored.js +52 -0
- package/dist/src/converter/heuristics.d.ts +2 -2
- package/dist/src/converter/heuristics.d.ts.map +1 -1
- package/dist/src/converter/heuristics.js +43 -4
- package/dist/src/converter/index.d.ts +3 -1
- package/dist/src/converter/index.d.ts.map +1 -1
- package/dist/src/converter/index.js +2 -0
- package/dist/src/converter/merge.d.ts +25 -10
- package/dist/src/converter/merge.d.ts.map +1 -1
- package/dist/src/converter/merge.js +17 -26
- package/dist/src/doctype.d.ts +38 -0
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/doctype.js +55 -1
- package/dist/src/field.d.ts +59 -8
- package/dist/src/field.d.ts.map +1 -1
- package/dist/src/field.js +84 -14
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/dist/validation-BjRDR6sh.js +1000 -0
- package/dist/validation-BjRDR6sh.js.map +1 -0
- package/package.json +3 -1
- package/dist/validation-CQtfIFHQ.js +0 -583
- package/dist/validation-CQtfIFHQ.js.map +0 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregate doctype derivation.
|
|
3
|
+
*
|
|
4
|
+
* A table gets two generated doctypes: the entity itself, whose `fields` carry every column and
|
|
5
|
+
* which backs the record form, and an **aggregate** — the collection view over the same table.
|
|
6
|
+
* The aggregate starts with identity alone, because the useful default for a collection is the
|
|
7
|
+
* one column that lets a row be opened, not all forty. Widening it is curation, and curation
|
|
8
|
+
* survives regeneration (see `mergeIntrospectedDoctype`).
|
|
9
|
+
*
|
|
10
|
+
* The two are peers: each is a complete doctype with its own `name` and `slug`, and nothing here
|
|
11
|
+
* encodes a relationship between them. Deriving the aggregate's name from the entity's is a
|
|
12
|
+
* generated encoding, not a readable one — no consumer recovers the pair by parsing a slug.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
import pluralize from 'pluralize';
|
|
17
|
+
import { toSlug } from '../naming';
|
|
18
|
+
import { getDoctypeSlug } from '../doctype';
|
|
19
|
+
import { flattenFields, getPrimaryKeyField } from '../field';
|
|
20
|
+
/**
|
|
21
|
+
* The name an entity's aggregate doctype is generated under: the entity's name, pluralised.
|
|
22
|
+
*
|
|
23
|
+
* One definition, because the CLI writes the file under `toSlug` of this and any later caller
|
|
24
|
+
* (a scaffolder, a docs generator) must land on the same name or it silently addresses a
|
|
25
|
+
* different file.
|
|
26
|
+
*
|
|
27
|
+
* `pluralize` rather than appending `s`, because the irregulars are not rare in practice —
|
|
28
|
+
* measured against a consumer's 41 hand-authored aggregate doctypes, this rule reproduces every
|
|
29
|
+
* one of their names, slugs and filenames exactly, while `+ 's'` gets five wrong
|
|
30
|
+
* (`Currencys`, `JournalEntrys`, …).
|
|
31
|
+
*
|
|
32
|
+
* The rule is not total: an already-plural name pluralises to itself. Callers must handle that —
|
|
33
|
+
* see {@link buildAggregateDoctype}.
|
|
34
|
+
*
|
|
35
|
+
* @param doctypeName - the entity doctype's `name`
|
|
36
|
+
* @returns the aggregate doctype's `name`
|
|
37
|
+
* @public
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* aggregateDoctypeName('SalesOrder') // 'SalesOrders' -> slug 'sales-orders'
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function aggregateDoctypeName(doctypeName) {
|
|
45
|
+
return pluralize.plural(doctypeName);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Derive the aggregate doctype for a converted entity.
|
|
49
|
+
*
|
|
50
|
+
* Returns `undefined` when no identity column can be found — a natural-key table whose key the
|
|
51
|
+
* converter refuses to guess and whose author has not declared one, or a foreign PostGraphile
|
|
52
|
+
* endpoint that has left the Relay identifier occupying `id` (Stonecrop's own preset moves it to
|
|
53
|
+
* `nodeId`). That is deliberate: an aggregate with an empty `fields` array is a valid doctype that
|
|
54
|
+
* renders a table with no columns, which looks like a data problem rather than a generation one.
|
|
55
|
+
* Emitting nothing and saying so is the loud failure.
|
|
56
|
+
*
|
|
57
|
+
* Identity resolves the same way `getRecordIdField` resolves it — the declared `primaryKey`, then
|
|
58
|
+
* the conventional `id` — so an aggregate is always keyed on the column the client will later ask
|
|
59
|
+
* for. `declaredIdentity` overrides both: SDL cannot express which `UNIQUE` column is the key, so
|
|
60
|
+
* for a natural-key table the answer only exists in the authored file, and the caller that read it
|
|
61
|
+
* passes the fieldname back.
|
|
62
|
+
*
|
|
63
|
+
* @param doctype - a converted entity doctype, as returned by `convertGraphQLSchema`
|
|
64
|
+
* @param declaredIdentity - fieldname the authored doctype declares as its `primaryKey`, when the
|
|
65
|
+
* caller has read one. Must name a field the converter emitted; the caller checks that, because
|
|
66
|
+
* only it can say whether a missing one is a dropped column or a typo.
|
|
67
|
+
* @returns the aggregate doctype, or `undefined` when no identity column exists
|
|
68
|
+
* @public
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const [order] = convertGraphQLSchema(sdl, { include: ['Order'] })
|
|
73
|
+
* const aggregate = buildAggregateDoctype(order)
|
|
74
|
+
* // { name: 'Orders', slug: 'orders', fields: [ the id field ] }
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function buildAggregateDoctype(doctype, declaredIdentity) {
|
|
78
|
+
const identity = findIdentityField(doctype.fields, declaredIdentity);
|
|
79
|
+
if (!identity)
|
|
80
|
+
return undefined;
|
|
81
|
+
const name = aggregateDoctypeName(doctype.name);
|
|
82
|
+
// An already-plural name pluralises to itself, which would give the aggregate the entity's own
|
|
83
|
+
// `name` *and* its filename. Both write paths are silent about it: the CLI writes the file twice
|
|
84
|
+
// in one run, and the middleware's registry is a Map keyed by name, so the later read wins in
|
|
85
|
+
// whatever order `readdirSync` returns. Refusing is the only loud option.
|
|
86
|
+
if (name === doctype.name)
|
|
87
|
+
return undefined;
|
|
88
|
+
// `primaryKey` is stamped rather than copied through: a declared identity is not marked on the
|
|
89
|
+
// converter's own field, and an aggregate whose one column carries no marker resolves identity
|
|
90
|
+
// through `getRecordIdField`'s `id` fallback — a column it does not have, so every listed row is
|
|
91
|
+
// silently dropped. Rebuilt with `source` last so the key order matches an entity's identity
|
|
92
|
+
// field and both files stay byte-stable.
|
|
93
|
+
//
|
|
94
|
+
// A copy, not a reference: the two doctypes are written to separate files and an edit to one
|
|
95
|
+
// must not reach the other.
|
|
96
|
+
const { source, ...rest } = identity;
|
|
97
|
+
return {
|
|
98
|
+
name,
|
|
99
|
+
slug: toSlug(name),
|
|
100
|
+
fields: [{ ...rest, primaryKey: true, ...(source === undefined ? {} : { source }) }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The field an aggregate is keyed on: an identity the author declared, else the primary key the
|
|
105
|
+
* converter derived, else the conventional `id`.
|
|
106
|
+
*
|
|
107
|
+
* The author wins because the authored doctype is the source of truth — generation verifies it and
|
|
108
|
+
* never overwrites it (see `mergeIntrospectedDoctype`), and the divergence is already reported as
|
|
109
|
+
* identity drift.
|
|
110
|
+
*
|
|
111
|
+
* Calls `getPrimaryKeyField` for the derived half rather than restating it: a restatement drifted
|
|
112
|
+
* exactly as one does, staying top-level while the helper learned to descend into fieldsets.
|
|
113
|
+
*
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
function findIdentityField(fields, declared) {
|
|
117
|
+
if (declared !== undefined)
|
|
118
|
+
return fields.find(field => field.fieldname === declared);
|
|
119
|
+
return getPrimaryKeyField(fields) ?? fields.find(field => field.fieldname === 'id');
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The doctypes in a run that get a URL of their own.
|
|
123
|
+
*
|
|
124
|
+
* A child table has no page: its rows exist inside a parent and are edited there, so a route for
|
|
125
|
+
* it is an address nothing can link to. The declaration that says so is the parent's `links` entry
|
|
126
|
+
* with a to-many cardinality — which the server derives from the foreign keys it treats as owning,
|
|
127
|
+
* so this reads what the schema states rather than guessing from a name.
|
|
128
|
+
*
|
|
129
|
+
* The rule is *listed by something, referenced by nothing*. A single reference wins over any number
|
|
130
|
+
* of listings, and the asymmetry is deliberate: a doctype that is both a parent's rows and another
|
|
131
|
+
* doctype's link target — a recipe task, say, embedded in its recipe and pointed at by four other
|
|
132
|
+
* records — needs somewhere for those links to navigate to. Denying it leaves the arrow on an
|
|
133
|
+
* `AFormLink` dead, which fails silently; granting it leaves a URL nobody visits, which does not.
|
|
134
|
+
*
|
|
135
|
+
* Scoped to one run, so a partial generation sees a partial graph and grants more routes than a
|
|
136
|
+
* whole one would. That is the safe direction, and the extra routes are deletable — an authored
|
|
137
|
+
* file's keys survive regeneration untouched.
|
|
138
|
+
*
|
|
139
|
+
* @internal
|
|
140
|
+
*/
|
|
141
|
+
function routableDoctypes(entities) {
|
|
142
|
+
const listed = new Set();
|
|
143
|
+
const referenced = new Set();
|
|
144
|
+
for (const entity of entities) {
|
|
145
|
+
for (const link of Object.values(entity.links ?? {})) {
|
|
146
|
+
if (link.cardinality === 'noneOrMany' || link.cardinality === 'atLeastOne')
|
|
147
|
+
listed.add(link.target);
|
|
148
|
+
else
|
|
149
|
+
referenced.add(link.target);
|
|
150
|
+
}
|
|
151
|
+
// `flattenFields` rather than a top-level scan: a link inside a fieldset is still a reference,
|
|
152
|
+
// and the two ways to answer this question have already drifted apart once.
|
|
153
|
+
for (const field of flattenFields(entity.fields)) {
|
|
154
|
+
if ('doctype' in field && typeof field.doctype === 'string')
|
|
155
|
+
referenced.add(field.doctype);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return new Set(entities
|
|
159
|
+
.filter(entity => {
|
|
160
|
+
const slug = getDoctypeSlug(entity);
|
|
161
|
+
return referenced.has(slug) || !listed.has(slug);
|
|
162
|
+
})
|
|
163
|
+
.map(entity => entity.name));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Expand converted entities into the set of doctype files to write.
|
|
167
|
+
*
|
|
168
|
+
* Each table yields two: the entity, whose fields carry every column and which backs the record
|
|
169
|
+
* form, and its aggregate — the collection view. They are written as peers, one file each, with
|
|
170
|
+
* no key relating them.
|
|
171
|
+
*
|
|
172
|
+
* Separate from the CLI because the pairing of a file to its verification basis is the part that
|
|
173
|
+
* is easy to get wrong and impossible to notice: getting it wrong does not throw, it just reports
|
|
174
|
+
* drift that is not there, forever.
|
|
175
|
+
*
|
|
176
|
+
* @param entities - `convertGraphQLSchema` output
|
|
177
|
+
* @param options - see {@link GenerationPlanOptions}
|
|
178
|
+
* @returns one entry per file to write
|
|
179
|
+
* @public
|
|
180
|
+
*/
|
|
181
|
+
export function planGeneration(entities, options = {}) {
|
|
182
|
+
const entityNames = new Set(entities.map(entity => entity.name));
|
|
183
|
+
const claimed = new Set();
|
|
184
|
+
const routable = routableDoctypes(entities);
|
|
185
|
+
return entities.flatMap(entity => {
|
|
186
|
+
// Written out in full rather than as a segment the host assembles: the record parameter has
|
|
187
|
+
// to live somewhere, and a host given `/order` cannot know whether this doctype is the
|
|
188
|
+
// collection or the record without asking a second question. The pair shares the entity's
|
|
189
|
+
// slug, so no URL ever carries a plural.
|
|
190
|
+
const segment = `/${getDoctypeSlug(entity)}`;
|
|
191
|
+
const routed = routable.has(entity.name) ? { ...entity, route: `${segment}/:id` } : entity;
|
|
192
|
+
// `basis` is the same object as `generated` for an entity — it is verified against itself.
|
|
193
|
+
const self = { generated: routed, basis: routed, subset: false };
|
|
194
|
+
if (options.noAggregates)
|
|
195
|
+
return [self];
|
|
196
|
+
// Name collisions are checked here rather than in the builder because only this function
|
|
197
|
+
// holds the whole set. Reported before the identity check so each refusal names its own
|
|
198
|
+
// cause — the two are repaired differently.
|
|
199
|
+
const name = aggregateDoctypeName(entity.name);
|
|
200
|
+
if (name === entity.name) {
|
|
201
|
+
options.onWarning?.(`${entity.name} is already plural, so its aggregate would take the same name and the same ` +
|
|
202
|
+
`file. No aggregate was generated. Rename the doctype to its singular form, or author ` +
|
|
203
|
+
`${entity.slug}.json's collection view by hand.`);
|
|
204
|
+
return [self];
|
|
205
|
+
}
|
|
206
|
+
if (entityNames.has(name) || claimed.has(name)) {
|
|
207
|
+
options.onWarning?.(`${entity.name}'s aggregate would be named ${name}, which is already taken by another ` +
|
|
208
|
+
`doctype in this run. No aggregate was generated — one of the two needs an explicit ` +
|
|
209
|
+
`name via the doctypeNames option.`);
|
|
210
|
+
return [self];
|
|
211
|
+
}
|
|
212
|
+
// Checked here rather than in the builder because only the caller knows whether a name that
|
|
213
|
+
// matches nothing is a dropped column or a typo — and an aggregate built around a field the
|
|
214
|
+
// table has no column for renders a collection whose only column is absent from every row.
|
|
215
|
+
const declared = options.identity?.[entity.name];
|
|
216
|
+
if (declared !== undefined && !entity.fields.some(field => field.fieldname === declared)) {
|
|
217
|
+
options.onWarning?.(`${entity.name} declares its primaryKey on '${declared}', which the schema has no column for. ` +
|
|
218
|
+
`No aggregate was generated — correct the declaration in ${entity.slug}.json, or restore the ` +
|
|
219
|
+
`column to the table.`);
|
|
220
|
+
return [self];
|
|
221
|
+
}
|
|
222
|
+
const aggregate = buildAggregateDoctype(entity, declared);
|
|
223
|
+
if (!aggregate) {
|
|
224
|
+
options.onWarning?.(`${entity.name} has no derivable identity column, so no aggregate doctype was generated. ` +
|
|
225
|
+
`Declare a primaryKey on ${entity.slug}.json and re-run.`);
|
|
226
|
+
return [self];
|
|
227
|
+
}
|
|
228
|
+
claimed.add(name);
|
|
229
|
+
// The basis is the entity itself: an aggregate is verified against the table it curates from,
|
|
230
|
+
// not against its own one-field generation. Drift lines take their name from the authored file
|
|
231
|
+
// being checked, so they already name the file the reader has to edit.
|
|
232
|
+
const listed = routable.has(entity.name) ? { ...aggregate, route: segment } : aggregate;
|
|
233
|
+
return [self, { generated: listed, basis: entity, subset: true }];
|
|
234
|
+
});
|
|
235
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading an authored doctype — the JSON as it sits on disk, before any parsing.
|
|
3
|
+
*
|
|
4
|
+
* A separate reader from `@stonecrop/schema`'s `flattenFields`/`getPrimaryKeyField` because the two
|
|
5
|
+
* operate on different *shapes*, not different rules: those take parsed `DoctypeField`s and branch
|
|
6
|
+
* on the `kind` discriminant the Zod parser synthesizes, which authored JSON does not carry.
|
|
7
|
+
* `getPrimaryKeyField` on a raw file therefore returns `undefined` — indistinguishable from "no key
|
|
8
|
+
* declared", which is the exact condition its callers are testing.
|
|
9
|
+
*
|
|
10
|
+
* Every question about authored JSON is answered here once, so the rule cannot drift between the
|
|
11
|
+
* merge and the generation plan.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* A doctype as it exists on disk: a plain object that may carry keys this package does not model
|
|
17
|
+
* (`handler` on an action, `filterFunction` on a field, whatever an app has added). Typing it
|
|
18
|
+
* loosely is what lets the merge round-trip those keys untouched instead of dropping them.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
export type AuthoredDoctype = Record<string, unknown>;
|
|
23
|
+
/** @internal */
|
|
24
|
+
export declare function isAuthoredRecord(value: unknown): value is AuthoredDoctype;
|
|
25
|
+
/**
|
|
26
|
+
* Flatten authored fields, descending into fieldsets.
|
|
27
|
+
*
|
|
28
|
+
* A fieldset is a layout grouping, not a scope: a field inside one is still a field of the doctype,
|
|
29
|
+
* with a column of its own and a key it may declare.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export declare function flattenAuthored(fields: readonly AuthoredDoctype[]): AuthoredDoctype[];
|
|
34
|
+
/**
|
|
35
|
+
* The fieldname an authored doctype declares as its identity, or `undefined` when it declares none.
|
|
36
|
+
*
|
|
37
|
+
* Descends into fieldsets, because a nested `primaryKey` is a real declaration — ignoring one is
|
|
38
|
+
* what `getPrimaryKeyField` was fixed for.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export declare function authoredPrimaryKey(doctype: AuthoredDoctype): string | undefined;
|
|
43
|
+
//# sourceMappingURL=authored.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authored.d.ts","sourceRoot":"","sources":["../../../src/converter/authored.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAErD,gBAAgB;AAChB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAEzE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,GAAG,eAAe,EAAE,CAUrF;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAI/E"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading an authored doctype — the JSON as it sits on disk, before any parsing.
|
|
3
|
+
*
|
|
4
|
+
* A separate reader from `@stonecrop/schema`'s `flattenFields`/`getPrimaryKeyField` because the two
|
|
5
|
+
* operate on different *shapes*, not different rules: those take parsed `DoctypeField`s and branch
|
|
6
|
+
* on the `kind` discriminant the Zod parser synthesizes, which authored JSON does not carry.
|
|
7
|
+
* `getPrimaryKeyField` on a raw file therefore returns `undefined` — indistinguishable from "no key
|
|
8
|
+
* declared", which is the exact condition its callers are testing.
|
|
9
|
+
*
|
|
10
|
+
* Every question about authored JSON is answered here once, so the rule cannot drift between the
|
|
11
|
+
* merge and the generation plan.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
/** @internal */
|
|
16
|
+
export function isAuthoredRecord(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Flatten authored fields, descending into fieldsets.
|
|
21
|
+
*
|
|
22
|
+
* A fieldset is a layout grouping, not a scope: a field inside one is still a field of the doctype,
|
|
23
|
+
* with a column of its own and a key it may declare.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export function flattenAuthored(fields) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const field of fields) {
|
|
30
|
+
if (Array.isArray(field.schema)) {
|
|
31
|
+
out.push(...flattenAuthored(field.schema.filter(isAuthoredRecord)));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
out.push(field);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The fieldname an authored doctype declares as its identity, or `undefined` when it declares none.
|
|
41
|
+
*
|
|
42
|
+
* Descends into fieldsets, because a nested `primaryKey` is a real declaration — ignoring one is
|
|
43
|
+
* what `getPrimaryKeyField` was fixed for.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export function authoredPrimaryKey(doctype) {
|
|
48
|
+
if (!Array.isArray(doctype.fields))
|
|
49
|
+
return undefined;
|
|
50
|
+
const declared = flattenAuthored(doctype.fields.filter(isAuthoredRecord)).find(f => f.primaryKey === true);
|
|
51
|
+
return typeof declared?.fieldname === 'string' ? declared.fieldname : undefined;
|
|
52
|
+
}
|
|
@@ -33,11 +33,11 @@ export declare function defaultIsEntityType(typeName: string, type: GraphQLObjec
|
|
|
33
33
|
*
|
|
34
34
|
* @param fieldName - The GraphQL field name
|
|
35
35
|
* @param _field - The GraphQL field definition (unused in default implementation)
|
|
36
|
-
* @param
|
|
36
|
+
* @param parentType - The parent entity type, whose interfaces declare its Relay identifier
|
|
37
37
|
* @returns `true` if this field should be included
|
|
38
38
|
* @public
|
|
39
39
|
*/
|
|
40
|
-
export declare function defaultIsEntityField(fieldName: string, _field: GraphQLField<unknown, unknown>,
|
|
40
|
+
export declare function defaultIsEntityField(fieldName: string, _field: GraphQLField<unknown, unknown>, parentType: GraphQLObjectType): boolean;
|
|
41
41
|
/**
|
|
42
42
|
* Classify a single GraphQL field into a Stonecrop field definition.
|
|
43
43
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heuristics.d.ts","sourceRoot":"","sources":["../../../src/converter/heuristics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAON,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAGjB,MAAM,SAAS,CAAA;AAGhB,OAAO,KAAK,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AA+BnF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CA8BtF;
|
|
1
|
+
{"version":3,"file":"heuristics.d.ts","sourceRoot":"","sources":["../../../src/converter/heuristics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAON,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAGjB,MAAM,SAAS,CAAA;AAGhB,OAAO,KAAK,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AA+BnF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CA8BtF;AA+CD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CACnC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,EACtC,UAAU,EAAE,iBAAiB,GAC3B,OAAO,CAGT;AAyED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAChC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,EACrC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,EACxB,OAAO,GAAE,wBAA6B,GACpC,0BAA0B,CAkG5B"}
|
|
@@ -81,20 +81,59 @@ export function defaultIsEntityType(typeName, type) {
|
|
|
81
81
|
/**
|
|
82
82
|
* Fields to skip by default on entity types.
|
|
83
83
|
* These are internal to GraphQL servers and don't represent semantic data.
|
|
84
|
+
*
|
|
85
|
+
* Relay's global object identifier is deliberately absent: which field carries it is a
|
|
86
|
+
* declaration, not a name. See {@link relayNodeIdField}.
|
|
87
|
+
*/
|
|
88
|
+
const SKIP_FIELDS = new Set(['__typename', 'clientMutationId']);
|
|
89
|
+
/**
|
|
90
|
+
* The name Relay's Object Identification spec gives its marker interface.
|
|
91
|
+
*/
|
|
92
|
+
const RELAY_NODE_INTERFACE = 'Node';
|
|
93
|
+
/**
|
|
94
|
+
* The field carrying Relay's global object identifier on this type, or `undefined` for a type that
|
|
95
|
+
* declares none.
|
|
96
|
+
*
|
|
97
|
+
* Read off the interface rather than matched against a list of names, because the name is a server
|
|
98
|
+
* setting: PostGraphile exposes it as `nodeIdFieldName`, which is `id` under the un-overridden
|
|
99
|
+
* Amber preset, `nodeId` under Stonecrop's, and whatever a foreign host chose under theirs. A
|
|
100
|
+
* hardcoded name is a snapshot of one of those, and gets it wrong in both directions at once — it
|
|
101
|
+
* emits an opaque identifier as a column (whose every read then fails on a column that does not
|
|
102
|
+
* exist), and drops a real column that happens to share the name.
|
|
103
|
+
*
|
|
104
|
+
* The interface must be Relay's marker and not a domain interface that shares its name, so it has
|
|
105
|
+
* to declare exactly one field, a non-null `ID`, and nothing else — anything carrying domain fields
|
|
106
|
+
* is a different interface, and skipping against it would drop real columns.
|
|
107
|
+
*
|
|
108
|
+
* @internal
|
|
84
109
|
*/
|
|
85
|
-
|
|
110
|
+
function relayNodeIdField(type) {
|
|
111
|
+
for (const iface of type.getInterfaces()) {
|
|
112
|
+
if (iface.name !== RELAY_NODE_INTERFACE)
|
|
113
|
+
continue;
|
|
114
|
+
const declared = Object.values(iface.getFields());
|
|
115
|
+
if (declared.length !== 1)
|
|
116
|
+
continue;
|
|
117
|
+
const { namedType, required, isList } = unwrapType(declared[0].type);
|
|
118
|
+
if (required && !isList && namedType.name === 'ID')
|
|
119
|
+
return declared[0].name;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
86
123
|
/**
|
|
87
124
|
* Default heuristic to filter fields on entity types.
|
|
88
125
|
* Skips internal fields that don't represent meaningful data.
|
|
89
126
|
*
|
|
90
127
|
* @param fieldName - The GraphQL field name
|
|
91
128
|
* @param _field - The GraphQL field definition (unused in default implementation)
|
|
92
|
-
* @param
|
|
129
|
+
* @param parentType - The parent entity type, whose interfaces declare its Relay identifier
|
|
93
130
|
* @returns `true` if this field should be included
|
|
94
131
|
* @public
|
|
95
132
|
*/
|
|
96
|
-
export function defaultIsEntityField(fieldName, _field,
|
|
97
|
-
|
|
133
|
+
export function defaultIsEntityField(fieldName, _field, parentType) {
|
|
134
|
+
if (SKIP_FIELDS.has(fieldName))
|
|
135
|
+
return false;
|
|
136
|
+
return fieldName !== relayNodeIdField(parentType);
|
|
98
137
|
}
|
|
99
138
|
/**
|
|
100
139
|
* Unwrap NonNull and List wrappers from a GraphQL type, tracking nullability.
|
|
@@ -43,7 +43,9 @@ export { convertGraphQLSchema as default };
|
|
|
43
43
|
export type { IntrospectionSource, GraphQLConversionOptions, GraphQLConversionFieldMeta, ConvertedGraphQLDoctype, } from './types';
|
|
44
44
|
export { GQL_SCALAR_MAP, WELL_KNOWN_SCALARS, INTERNAL_SCALARS, buildScalarMap } from './scalars';
|
|
45
45
|
export { defaultIsEntityType, defaultIsEntityField, classifyFieldType } from './heuristics';
|
|
46
|
+
export { aggregateDoctypeName, buildAggregateDoctype, planGeneration } from './aggregate';
|
|
47
|
+
export type { GenerationPlanEntry, GenerationPlanOptions } from './aggregate';
|
|
46
48
|
export { mergeIntrospectedDoctype, formatDoctypeDrift } from './merge';
|
|
47
|
-
export type { AuthoredDoctype, DoctypeDrift, MergeResult } from './merge';
|
|
49
|
+
export type { AuthoredDoctype, DoctypeDrift, MergeOptions, MergeResult } from './merge';
|
|
48
50
|
export { toSlug, toPascalCase, pascalToSnake, snakeToCamel, camelToSnake, snakeToLabel, camelToLabel } from '../naming';
|
|
49
51
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/converter/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAIrG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,GAAE,wBAA6B,GACpC,uBAAuB,EAAE,CAkJ3B;AAwBD,OAAO,EAAE,oBAAoB,IAAI,OAAO,EAAE,CAAA;AAG1C,YAAY,EACX,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACvB,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAGhG,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAG3F,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/converter/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAIrG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,GAAE,wBAA6B,GACpC,uBAAuB,EAAE,CAkJ3B;AAwBD,OAAO,EAAE,oBAAoB,IAAI,OAAO,EAAE,CAAA;AAG1C,YAAY,EACX,mBAAmB,EACnB,wBAAwB,EACxB,0BAA0B,EAC1B,uBAAuB,GACvB,MAAM,SAAS,CAAA;AAGhB,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAGhG,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAG3F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACzF,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAG7E,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AACtE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAGvF,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA"}
|
|
@@ -187,6 +187,8 @@ export { convertGraphQLSchema as default };
|
|
|
187
187
|
export { GQL_SCALAR_MAP, WELL_KNOWN_SCALARS, INTERNAL_SCALARS, buildScalarMap } from './scalars';
|
|
188
188
|
// Heuristics
|
|
189
189
|
export { defaultIsEntityType, defaultIsEntityField, classifyFieldType } from './heuristics';
|
|
190
|
+
// Aggregate — the collection-view doctype derived from an entity, emitted as its own file
|
|
191
|
+
export { aggregateDoctypeName, buildAggregateDoctype, planGeneration } from './aggregate';
|
|
190
192
|
// Merge — verifies an authored doctype against the schema and stamps provenance
|
|
191
193
|
export { mergeIntrospectedDoctype, formatDoctypeDrift } from './merge';
|
|
192
194
|
// Naming utilities
|
|
@@ -13,15 +13,9 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @packageDocumentation
|
|
15
15
|
*/
|
|
16
|
+
import type { AuthoredDoctype } from './authored';
|
|
16
17
|
import type { ConvertedGraphQLDoctype } from './types';
|
|
17
|
-
|
|
18
|
-
* A doctype as it exists on disk: a plain object that may carry keys this package does not model
|
|
19
|
-
* (`handler` on an action, `filterFunction` on a field, whatever an app has added). Typing it
|
|
20
|
-
* loosely is what lets the merge round-trip those keys untouched instead of dropping them.
|
|
21
|
-
*
|
|
22
|
-
* @public
|
|
23
|
-
*/
|
|
24
|
-
export type AuthoredDoctype = Record<string, unknown>;
|
|
18
|
+
export type { AuthoredDoctype };
|
|
25
19
|
/**
|
|
26
20
|
* What generation found that the authored doctype does not agree with. Every bucket is advisory —
|
|
27
21
|
* nothing here is applied automatically.
|
|
@@ -51,6 +45,25 @@ export interface DoctypeDrift {
|
|
|
51
45
|
/** Identity properties that differ. These are the ones a human must adjudicate. */
|
|
52
46
|
identityDrift: string[];
|
|
53
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* How to verify the authored doctype against the schema.
|
|
50
|
+
*
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
export interface MergeOptions {
|
|
54
|
+
/**
|
|
55
|
+
* The authored doctype is a curated **subset** of the schema's columns rather than a model of
|
|
56
|
+
* all of them — an aggregate being the case this exists for.
|
|
57
|
+
*
|
|
58
|
+
* This changes what counts as drift in both directions, so `generated` must be passed the
|
|
59
|
+
* *entity's* full field set, not the subset's. A column the author added to an aggregate is
|
|
60
|
+
* then confirmed against the real table (so a genuinely dropped column still reports as an
|
|
61
|
+
* orphan), while the columns deliberately left out stop reporting as omissions. Without it an
|
|
62
|
+
* aggregate reports phantom drift on every run, which both spams `--check` and buries the one
|
|
63
|
+
* finding that matters.
|
|
64
|
+
*/
|
|
65
|
+
subset?: boolean;
|
|
66
|
+
}
|
|
54
67
|
/** Outcome of a merge: the doctype to write, plus what generation disagreed with. @public */
|
|
55
68
|
export interface MergeResult {
|
|
56
69
|
/** The authored doctype with `source` markers added and nothing else changed. */
|
|
@@ -62,7 +75,9 @@ export interface MergeResult {
|
|
|
62
75
|
* Verify an authored doctype against freshly generated output and stamp provenance.
|
|
63
76
|
*
|
|
64
77
|
* @param authored - the doctype as it exists on disk; every key not named below is preserved verbatim
|
|
65
|
-
* @param generated - `convertGraphQLSchema` output for the corresponding GraphQL type
|
|
78
|
+
* @param generated - `convertGraphQLSchema` output for the corresponding GraphQL type. For a
|
|
79
|
+
* `subset` merge this is the **entity**, whose fields are the set the subset is curated from
|
|
80
|
+
* @param options - see {@link MergeOptions}
|
|
66
81
|
* @returns the doctype to write, plus a drift report
|
|
67
82
|
*
|
|
68
83
|
* @example
|
|
@@ -74,7 +89,7 @@ export interface MergeResult {
|
|
|
74
89
|
*
|
|
75
90
|
* @public
|
|
76
91
|
*/
|
|
77
|
-
export declare function mergeIntrospectedDoctype(authored: AuthoredDoctype, generated: ConvertedGraphQLDoctype): MergeResult;
|
|
92
|
+
export declare function mergeIntrospectedDoctype(authored: AuthoredDoctype, generated: ConvertedGraphQLDoctype, options?: MergeOptions): MergeResult;
|
|
78
93
|
/**
|
|
79
94
|
* Render a drift report as human-readable lines. Empty when generation agrees with the doctype.
|
|
80
95
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../src/converter/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../../src/converter/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAEtD,YAAY,EAAE,eAAe,EAAE,CAAA;AAE/B;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC5B,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,IAAI,EAAE,OAAO,GAAG,SAAS,CAAA;IACzB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2DAA2D;IAC3D,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,mGAAmG;IACnG,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,qGAAqG;IACrG,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,mGAAmG;IACnG,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,oEAAoE;IACpE,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,mFAAmF;IACnF,aAAa,EAAE,MAAM,EAAE,CAAA;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,6FAA6F;AAC7F,MAAM,WAAW,WAAW;IAC3B,iFAAiF;IACjF,OAAO,EAAE,eAAe,CAAA;IACxB,sCAAsC;IACtC,KAAK,EAAE,YAAY,CAAA;CACnB;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,wBAAwB,CACvC,QAAQ,EAAE,eAAe,EACzB,SAAS,EAAE,uBAAuB,EAClC,OAAO,GAAE,YAAiB,GACxB,WAAW,CAgFb;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,EAAE,CAYhE"}
|
|
@@ -14,22 +14,7 @@
|
|
|
14
14
|
* @packageDocumentation
|
|
15
15
|
*/
|
|
16
16
|
import { INTROSPECTED_IDENTITY_PROPS } from '../field';
|
|
17
|
-
|
|
18
|
-
function flattenAuthored(fields) {
|
|
19
|
-
const out = [];
|
|
20
|
-
for (const f of fields) {
|
|
21
|
-
if (Array.isArray(f.schema)) {
|
|
22
|
-
out.push(...flattenAuthored(f.schema.filter(isRecord)));
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
out.push(f);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
function isRecord(value) {
|
|
31
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
32
|
-
}
|
|
17
|
+
import { authoredPrimaryKey, flattenAuthored, isAuthoredRecord } from './authored';
|
|
33
18
|
function describe(value) {
|
|
34
19
|
return value === undefined ? '—' : JSON.stringify(value);
|
|
35
20
|
}
|
|
@@ -37,7 +22,9 @@ function describe(value) {
|
|
|
37
22
|
* Verify an authored doctype against freshly generated output and stamp provenance.
|
|
38
23
|
*
|
|
39
24
|
* @param authored - the doctype as it exists on disk; every key not named below is preserved verbatim
|
|
40
|
-
* @param generated - `convertGraphQLSchema` output for the corresponding GraphQL type
|
|
25
|
+
* @param generated - `convertGraphQLSchema` output for the corresponding GraphQL type. For a
|
|
26
|
+
* `subset` merge this is the **entity**, whose fields are the set the subset is curated from
|
|
27
|
+
* @param options - see {@link MergeOptions}
|
|
41
28
|
* @returns the doctype to write, plus a drift report
|
|
42
29
|
*
|
|
43
30
|
* @example
|
|
@@ -49,8 +36,8 @@ function describe(value) {
|
|
|
49
36
|
*
|
|
50
37
|
* @public
|
|
51
38
|
*/
|
|
52
|
-
export function mergeIntrospectedDoctype(authored, generated) {
|
|
53
|
-
const authoredFields = Array.isArray(authored.fields) ? authored.fields.filter(
|
|
39
|
+
export function mergeIntrospectedDoctype(authored, generated, options = {}) {
|
|
40
|
+
const authoredFields = Array.isArray(authored.fields) ? authored.fields.filter(isAuthoredRecord) : [];
|
|
54
41
|
const generatedByName = new Map(generated.fields.map(f => [f.fieldname, f]));
|
|
55
42
|
// Expanding links live in `links`, not `fields`, so a field naming one is modelled, not orphaned.
|
|
56
43
|
const generatedLinkNames = new Set(Object.keys(generated.links ?? {}));
|
|
@@ -67,7 +54,7 @@ export function mergeIntrospectedDoctype(authored, generated) {
|
|
|
67
54
|
const tag = (field) => {
|
|
68
55
|
// Containers have no column of their own; recurse and leave the container itself alone.
|
|
69
56
|
if (Array.isArray(field.schema)) {
|
|
70
|
-
return { ...field, schema: field.schema.filter(
|
|
57
|
+
return { ...field, schema: field.schema.filter(isAuthoredRecord).map(tag) };
|
|
71
58
|
}
|
|
72
59
|
const name = typeof field.fieldname === 'string' ? field.fieldname : '';
|
|
73
60
|
const match = generatedByName.get(name);
|
|
@@ -101,18 +88,22 @@ export function mergeIntrospectedDoctype(authored, generated) {
|
|
|
101
88
|
return { ...field, source: 'introspected' };
|
|
102
89
|
};
|
|
103
90
|
const merged = { ...authored, fields: authoredFields.map(tag) };
|
|
104
|
-
|
|
105
|
-
|
|
91
|
+
// A curated subset omits columns by definition, so the bucket that reports omissions has
|
|
92
|
+
// nothing true to say about one.
|
|
93
|
+
if (!options.subset) {
|
|
94
|
+
const authoredNames = new Set(flattenAuthored(authoredFields).map(f => f.fieldname));
|
|
95
|
+
drift.omitted = generated.fields.map(f => f.fieldname).filter(n => !authoredNames.has(n));
|
|
96
|
+
}
|
|
106
97
|
// Classify identity last, once every field has been compared.
|
|
107
|
-
const authoredPk =
|
|
98
|
+
const authoredPk = authoredPrimaryKey(authored);
|
|
108
99
|
const generatedPk = generated.fields.find(f => f.primaryKey === true);
|
|
109
|
-
if (authoredPk && generatedPk && authoredPk
|
|
100
|
+
if (authoredPk && generatedPk && authoredPk !== generatedPk.fieldname) {
|
|
110
101
|
drift.mode = 'partial';
|
|
111
|
-
drift.reason = `authored primary key '${
|
|
102
|
+
drift.reason = `authored primary key '${authoredPk}' is not the derivable '${generatedPk.fieldname}' — left as authored`;
|
|
112
103
|
}
|
|
113
104
|
else if (authoredPk && !generatedPk) {
|
|
114
105
|
drift.mode = 'partial';
|
|
115
|
-
drift.reason = `authored primary key '${
|
|
106
|
+
drift.reason = `authored primary key '${authoredPk}' is not derivable from the schema — left as authored`;
|
|
116
107
|
}
|
|
117
108
|
else if (!authoredPk && generatedPk) {
|
|
118
109
|
drift.mode = 'partial';
|