@supatype/cli 0.1.12 → 0.1.13
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +138 -132
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/cli-version-embedded.js +1 -1
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +23 -1
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/doctor.d.ts +0 -7
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +26 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +15 -7
- package/dist/commands/push.js.map +1 -1
- package/dist/compose-local-server-image.d.ts +11 -0
- package/dist/compose-local-server-image.d.ts.map +1 -1
- package/dist/compose-local-server-image.js +18 -0
- package/dist/compose-local-server-image.js.map +1 -1
- package/dist/dev-compose.d.ts +1 -0
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +79 -10
- package/dist/dev-compose.js.map +1 -1
- package/dist/field-bounds.d.ts +68 -0
- package/dist/field-bounds.d.ts.map +1 -0
- package/dist/field-bounds.js +277 -0
- package/dist/field-bounds.js.map +1 -0
- package/dist/hooks-generator.d.ts +1 -1
- package/dist/hooks-generator.d.ts.map +1 -1
- package/dist/hooks-generator.js +78 -4
- package/dist/hooks-generator.js.map +1 -1
- package/dist/model-hooks.d.ts +44 -2
- package/dist/model-hooks.d.ts.map +1 -1
- package/dist/model-hooks.js +116 -12
- package/dist/model-hooks.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +38 -4
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +87 -4
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +309 -27
- package/dist/type-extractor.js.map +1 -1
- package/package.json +4 -3
- package/src/cli-version-embedded.ts +1 -1
- package/src/commands/db.ts +27 -1
- package/src/commands/doctor.ts +30 -0
- package/src/commands/push.ts +26 -6
- package/src/compose-local-server-image.ts +17 -0
- package/src/dev-compose.ts +96 -9
- package/src/field-bounds.ts +359 -0
- package/src/hooks-generator.ts +81 -4
- package/src/model-hooks.ts +158 -12
- package/src/schema-ast-v2.ts +114 -10
- package/src/type-extractor.ts +374 -39
- package/tests/field-bounds-matrix.test.ts +163 -0
- package/tests/field-bounds.test.ts +139 -0
- package/tests/field-validators.test.ts +139 -0
- package/tests/hooks-generator.test.ts +86 -0
- package/tests/local-server-image-env.test.ts +93 -0
- package/tests/model-constraints.test.ts +293 -0
- package/tests/model-hooks.test.ts +56 -0
- package/tests/type-extractor.test.ts +49 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a declared bound means for each field kind, and the SQL it compiles to.
|
|
3
|
+
*
|
|
4
|
+
* **This table is the mechanism.** Bounds used to be compiled inside the modifier cases of
|
|
5
|
+
* `type-extractor.ts`, which meant `MaxLength` became `char_length(col)` whatever the column turned
|
|
6
|
+
* out to be: `char_length(text[])` does not exist, so the RFC's own `tags: MaxLength<string[], 10>`
|
|
7
|
+
* produced SQL that fails `CREATE TABLE`. Worse, nine of twelve engine field structs had nowhere to
|
|
8
|
+
* put a `check`, so the constraint was dropped by serde with no error anywhere in the chain.
|
|
9
|
+
*
|
|
10
|
+
* A table keyed by kind fixes the class of bug rather than the instances: a kind absent from
|
|
11
|
+
* {@link BOUNDS_BY_KIND} throws, so a new field kind cannot be added without answering "what does a
|
|
12
|
+
* bound mean here", and every answer is either an expression or a refusal with a named alternative.
|
|
13
|
+
* There is no third outcome, which is what "no bound is ever silent" means in practice.
|
|
14
|
+
*/
|
|
15
|
+
import { FIELD_KINDS } from "./schema-ast-v2.js";
|
|
16
|
+
const TEXTUAL = {
|
|
17
|
+
length: "chars",
|
|
18
|
+
instead: {
|
|
19
|
+
items: "text has characters, not items; use MaxLength/MinLength",
|
|
20
|
+
range: "text is not ordered numerically; use a model-level constraint if you need a comparison",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const NUMERIC = {
|
|
24
|
+
range: "numeric",
|
|
25
|
+
instead: {
|
|
26
|
+
length: "a number has no length; use Between to bound its value",
|
|
27
|
+
items: "a number has no items; use Between to bound its value",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
const temporal = (range) => ({
|
|
31
|
+
range,
|
|
32
|
+
instead: {
|
|
33
|
+
length: "a date has no length; use Between with ISO-8601 string bounds",
|
|
34
|
+
items: "a date has no items; use Between with ISO-8601 string bounds",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const NO_BOUNDS = (why) => ({
|
|
38
|
+
instead: { length: why, items: why, range: why },
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* What each bound means for each kind.
|
|
42
|
+
*
|
|
43
|
+
* `Record<FieldKind, KindBounds>` is the whole mechanism: it is **exhaustive by the compiler**, so
|
|
44
|
+
* adding a kind to `FIELD_KINDS` fails the build here until someone says what `MaxLength`,
|
|
45
|
+
* `MaxItems` and `Between` do for it. Composite kinds (`timestamps`, `publishable`, `softDelete`)
|
|
46
|
+
* expand into real columns before a bound could apply, so they carry none, but they still have to
|
|
47
|
+
* say so.
|
|
48
|
+
*/
|
|
49
|
+
const BOUNDS_BY_KIND = {
|
|
50
|
+
text: TEXTUAL,
|
|
51
|
+
email: TEXTUAL,
|
|
52
|
+
url: TEXTUAL,
|
|
53
|
+
slug: TEXTUAL,
|
|
54
|
+
color: TEXTUAL,
|
|
55
|
+
xml: TEXTUAL,
|
|
56
|
+
ip: TEXTUAL,
|
|
57
|
+
cidr: TEXTUAL,
|
|
58
|
+
macaddr: TEXTUAL,
|
|
59
|
+
tsQuery: TEXTUAL,
|
|
60
|
+
tsVector: TEXTUAL,
|
|
61
|
+
richText: {
|
|
62
|
+
length: "richText",
|
|
63
|
+
instead: {
|
|
64
|
+
items: "rich text is measured in characters of plain text; use MaxLength/MinLength",
|
|
65
|
+
range: "rich text is not ordered; use MaxLength/MinLength",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
bytes: {
|
|
69
|
+
length: "octets",
|
|
70
|
+
instead: {
|
|
71
|
+
items: "a binary column has octets, not items; use MaxLength/MinLength",
|
|
72
|
+
range: "a binary column is not ordered; use MaxLength/MinLength",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
integer: NUMERIC,
|
|
76
|
+
smallInt: NUMERIC,
|
|
77
|
+
bigInt: NUMERIC,
|
|
78
|
+
float: NUMERIC,
|
|
79
|
+
serial: NUMERIC,
|
|
80
|
+
bigSerial: NUMERIC,
|
|
81
|
+
decimal: NUMERIC,
|
|
82
|
+
money: NUMERIC,
|
|
83
|
+
datetime: temporal("timestamptz"),
|
|
84
|
+
timestamp: temporal("timestamp"),
|
|
85
|
+
date: temporal("date"),
|
|
86
|
+
interval: temporal("interval"),
|
|
87
|
+
array: {
|
|
88
|
+
items: "array",
|
|
89
|
+
instead: {
|
|
90
|
+
length: "an array has items, not characters; use MaxItems/MinItems",
|
|
91
|
+
range: "an array is not ordered; use MaxItems/MinItems",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
blocks: {
|
|
95
|
+
items: "jsonbArray",
|
|
96
|
+
instead: {
|
|
97
|
+
length: "blocks are counted, not measured; use MaxItems/MinItems",
|
|
98
|
+
range: "blocks are not ordered; use MaxItems/MinItems",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
// `json` is decided per field, not per kind: `JSON<Item[]>` takes item bounds and `JSON<{...}>`
|
|
102
|
+
// takes none. {@link boundsForKind} applies that, which is why the entry here is the object case.
|
|
103
|
+
json: NO_BOUNDS("a JSON object has no single measure; bound a sub-field with a model-level constraint, " +
|
|
104
|
+
"or declare the field as JSON<T[]> to bound its element count"),
|
|
105
|
+
button: NO_BOUNDS("a button is a composite value; bound a sub-field with a model-level constraint"),
|
|
106
|
+
enum: NO_BOUNDS("the union already constrains the permitted values"),
|
|
107
|
+
boolean: NO_BOUNDS("a boolean has two values and needs no bound"),
|
|
108
|
+
uuid: NO_BOUNDS("a UUID is fixed width"),
|
|
109
|
+
image: NO_BOUNDS("size and type limits belong on the bucket: fileSizeLimit and allowedMimeTypes"),
|
|
110
|
+
file: NO_BOUNDS("size and type limits belong on the bucket: fileSizeLimit and allowedMimeTypes"),
|
|
111
|
+
geo: NO_BOUNDS("a geometry is not measured this way"),
|
|
112
|
+
vector: NO_BOUNDS("the dimension is already fixed by the type, as Vector<N>"),
|
|
113
|
+
relation: NO_BOUNDS("bound the column on the model this relation points at"),
|
|
114
|
+
custom: NO_BOUNDS("a plugin field declares its own storage; bounds are the plugin's to define"),
|
|
115
|
+
timestamps: NO_BOUNDS("a composite expands into columns before a bound could apply"),
|
|
116
|
+
publishable: NO_BOUNDS("a composite expands into columns before a bound could apply"),
|
|
117
|
+
softDelete: NO_BOUNDS("a composite expands into columns before a bound could apply"),
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Re-exported for tests. Completeness is now the compiler's job, not a test's: this exists so the
|
|
121
|
+
* matrix can assert it covers every kind, which is a different question from whether every kind is
|
|
122
|
+
* classified.
|
|
123
|
+
*/
|
|
124
|
+
export const CLASSIFIED_KINDS = FIELD_KINDS;
|
|
125
|
+
/** `JSON<T[]>` counts elements; `JSON<{...}>` takes no bound. Anything else follows its kind. */
|
|
126
|
+
function boundsForKind(kind, jsonIsArray) {
|
|
127
|
+
const entry = BOUNDS_BY_KIND[kind];
|
|
128
|
+
if (kind === "json" && jsonIsArray) {
|
|
129
|
+
return {
|
|
130
|
+
items: "jsonbArray",
|
|
131
|
+
instead: {
|
|
132
|
+
length: "a JSON array has items, not characters; use MaxItems/MinItems",
|
|
133
|
+
range: "a JSON array is not ordered; use MaxItems/MinItems",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return entry;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* How a kind is measured, for one measure family.
|
|
141
|
+
*
|
|
142
|
+
* **The single answer for both paths.** `MaxLength<T, N>` on a field and `Length<"col">` inside a
|
|
143
|
+
* model constraint have to agree about what "length" means for a given column, or the same schema
|
|
144
|
+
* gets `char_length` in one place and `cardinality` in the other. A second table for the constraint
|
|
145
|
+
* path is how `char_length(text[])` would come back, in a new file, having been fixed once already.
|
|
146
|
+
*/
|
|
147
|
+
export function measureFormFor(kind, measure, options = {}) {
|
|
148
|
+
const entry = boundsForKind(kind, options.jsonIsArray === true);
|
|
149
|
+
const form = measure === "length" ? entry.length : entry.items;
|
|
150
|
+
if (form !== undefined)
|
|
151
|
+
return { form };
|
|
152
|
+
return { instead: entry.instead?.[measure] ?? `a ${kind} field cannot be measured that way` };
|
|
153
|
+
}
|
|
154
|
+
const COLUMN = '"{name}"';
|
|
155
|
+
function lengthExpr(form) {
|
|
156
|
+
switch (form) {
|
|
157
|
+
case "chars":
|
|
158
|
+
return `char_length(${COLUMN})`;
|
|
159
|
+
case "octets":
|
|
160
|
+
return `octet_length(${COLUMN})`;
|
|
161
|
+
case "richText":
|
|
162
|
+
return `char_length(_supatype.richtext_text(${COLUMN}))`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* `jsonb_array_length` raises `cannot get array length of a non-array` at insert time, so the type
|
|
167
|
+
* guard is part of the constraint rather than an assumption about what callers send.
|
|
168
|
+
*/
|
|
169
|
+
function itemsClause(form, comparisons) {
|
|
170
|
+
if (form === "array") {
|
|
171
|
+
return comparisons.map((c) => `cardinality(${COLUMN}) ${c}`).join(" AND ");
|
|
172
|
+
}
|
|
173
|
+
const guarded = comparisons.map((c) => `jsonb_array_length(${COLUMN}) ${c}`).join(" AND ");
|
|
174
|
+
return `jsonb_typeof(${COLUMN}) = 'array' AND ${guarded}`;
|
|
175
|
+
}
|
|
176
|
+
function rangeLiteral(form, value) {
|
|
177
|
+
if (form === "numeric")
|
|
178
|
+
return String(value);
|
|
179
|
+
const cast = form === "timestamptz" ? "timestamptz" : form;
|
|
180
|
+
return `'${String(value).replace(/'/g, "''")}'::${cast}`;
|
|
181
|
+
}
|
|
182
|
+
/** ISO-8601 date, date-time or a Postgres interval. Validated here so a bad literal is a CLI error. */
|
|
183
|
+
function isTemporalLiteral(form, value) {
|
|
184
|
+
if (form === "interval")
|
|
185
|
+
return /^\s*\d+\s+[a-z]+(\s+\d+\s+[a-z]+)*\s*$/i.test(value);
|
|
186
|
+
return !Number.isNaN(Date.parse(value));
|
|
187
|
+
}
|
|
188
|
+
function refuse(field, modifier, kind, hint) {
|
|
189
|
+
const tail = hint ? ` ${hint}.` : "";
|
|
190
|
+
throw new Error(`Field "${field}": ${modifier} is not supported on a ${kind} field.${tail}`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Compile declared bounds against the kind they landed on.
|
|
194
|
+
*
|
|
195
|
+
* Throws rather than dropping. A bound that cannot be honoured is a mistake in the schema, and the
|
|
196
|
+
* failure it used to produce, silence, is the one failure this must not have.
|
|
197
|
+
*/
|
|
198
|
+
export function compileBounds(field, kind, bounds, options = {}) {
|
|
199
|
+
const entry = boundsForKind(kind, options.jsonIsArray === true);
|
|
200
|
+
const clauses = [];
|
|
201
|
+
const validation = {};
|
|
202
|
+
const { maxLength, minLength } = bounds;
|
|
203
|
+
if (maxLength !== undefined || minLength !== undefined) {
|
|
204
|
+
const resolved = measureFormFor(kind, "length", options);
|
|
205
|
+
if (resolved.form === undefined) {
|
|
206
|
+
refuse(field, maxLength !== undefined ? "MaxLength" : "MinLength", kind, resolved.instead);
|
|
207
|
+
}
|
|
208
|
+
const expr = lengthExpr(resolved.form);
|
|
209
|
+
if (maxLength !== undefined) {
|
|
210
|
+
clauses.push(`${expr} <= ${maxLength}`);
|
|
211
|
+
validation.maxLength = maxLength;
|
|
212
|
+
}
|
|
213
|
+
if (minLength !== undefined) {
|
|
214
|
+
clauses.push(`${expr} >= ${minLength}`);
|
|
215
|
+
validation.minLength = minLength;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const { maxItems, minItems } = bounds;
|
|
219
|
+
const itemsResolved = measureFormFor(kind, "items", options);
|
|
220
|
+
if (maxItems !== undefined || minItems !== undefined) {
|
|
221
|
+
if (itemsResolved.form === undefined) {
|
|
222
|
+
refuse(field, maxItems !== undefined ? "MaxItems" : "MinItems", kind, itemsResolved.instead);
|
|
223
|
+
}
|
|
224
|
+
const comparisons = [];
|
|
225
|
+
if (maxItems !== undefined) {
|
|
226
|
+
comparisons.push(`<= ${maxItems}`);
|
|
227
|
+
validation.maxItems = maxItems;
|
|
228
|
+
}
|
|
229
|
+
if (minItems !== undefined) {
|
|
230
|
+
comparisons.push(`>= ${minItems}`);
|
|
231
|
+
validation.minItems = minItems;
|
|
232
|
+
}
|
|
233
|
+
clauses.push(itemsClause(itemsResolved.form, comparisons));
|
|
234
|
+
}
|
|
235
|
+
const { min, max } = bounds;
|
|
236
|
+
if (min !== undefined || max !== undefined) {
|
|
237
|
+
if (!entry.range)
|
|
238
|
+
refuse(field, "Between", kind, entry.instead?.range);
|
|
239
|
+
for (const [bound, comparison] of [[min, ">="], [max, "<="]]) {
|
|
240
|
+
if (bound === undefined)
|
|
241
|
+
continue;
|
|
242
|
+
assertRangeShape(field, kind, entry.range, bound);
|
|
243
|
+
clauses.push(`${COLUMN} ${comparison} ${rangeLiteral(entry.range, bound)}`);
|
|
244
|
+
}
|
|
245
|
+
if (min !== undefined)
|
|
246
|
+
validation.min = min;
|
|
247
|
+
if (max !== undefined)
|
|
248
|
+
validation.max = max;
|
|
249
|
+
}
|
|
250
|
+
// Parenthesised only when there is something to bind, matching `mergeCheckConstraint`. Gratuitous
|
|
251
|
+
// parentheses are not cosmetic here: the differ compares this text against what Postgres hands
|
|
252
|
+
// back from `pg_get_constraintdef`, so every avoidable difference is a false "changed" on push.
|
|
253
|
+
return {
|
|
254
|
+
...(clauses.length > 0 && { check: joinClauses(clauses) }),
|
|
255
|
+
...(Object.keys(validation).length > 0 && { validation }),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function joinClauses(clauses) {
|
|
259
|
+
const [only] = clauses;
|
|
260
|
+
if (clauses.length === 1 && only !== undefined)
|
|
261
|
+
return only;
|
|
262
|
+
return clauses.map((c) => `(${c})`).join(" AND ");
|
|
263
|
+
}
|
|
264
|
+
/** A number bounds a number and a string bounds a date. Crossing them is a mistake, not a cast. */
|
|
265
|
+
function assertRangeShape(field, kind, form, bound) {
|
|
266
|
+
const isNumeric = form === "numeric";
|
|
267
|
+
if (isNumeric && typeof bound !== "number") {
|
|
268
|
+
throw new Error(`Field "${field}": Between on a ${kind} field takes numbers, but "${bound}" is a string.`);
|
|
269
|
+
}
|
|
270
|
+
if (!isNumeric && typeof bound !== "string") {
|
|
271
|
+
throw new Error(`Field "${field}": Between on a ${kind} field takes ISO-8601 string bounds, but ${bound} is a number.`);
|
|
272
|
+
}
|
|
273
|
+
if (!isNumeric && !isTemporalLiteral(form, bound)) {
|
|
274
|
+
throw new Error(`Field "${field}": Between bound "${bound}" is not a valid ${form === "interval" ? "interval" : "ISO-8601 date"}.`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=field-bounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"field-bounds.js","sourceRoot":"","sources":["../src/field-bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,WAAW,EAAwC,MAAM,oBAAoB,CAAA;AA6BtF,MAAM,OAAO,GAAe;IAC1B,MAAM,EAAE,OAAO;IACf,OAAO,EAAE;QACP,KAAK,EAAE,yDAAyD;QAChE,KAAK,EAAE,wFAAwF;KAChG;CACF,CAAA;AAED,MAAM,OAAO,GAAe;IAC1B,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE;QACP,MAAM,EAAE,wDAAwD;QAChE,KAAK,EAAE,uDAAuD;KAC/D;CACF,CAAA;AAED,MAAM,QAAQ,GAAG,CAAC,KAAgB,EAAc,EAAE,CAAC,CAAC;IAClD,KAAK;IACL,OAAO,EAAE;QACP,MAAM,EAAE,+DAA+D;QACvE,KAAK,EAAE,8DAA8D;KACtE;CACF,CAAC,CAAA;AAEF,MAAM,SAAS,GAAG,CAAC,GAAW,EAAc,EAAE,CAAC,CAAC;IAC9C,OAAO,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE;CACjD,CAAC,CAAA;AAEF;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAkC;IACpD,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,GAAG,EAAE,OAAO;IACZ,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,GAAG,EAAE,OAAO;IACZ,EAAE,EAAE,OAAO;IACX,IAAI,EAAE,OAAO;IACb,OAAO,EAAE,OAAO;IAChB,OAAO,EAAE,OAAO;IAChB,QAAQ,EAAE,OAAO;IAEjB,QAAQ,EAAE;QACR,MAAM,EAAE,UAAU;QAClB,OAAO,EAAE;YACP,KAAK,EAAE,4EAA4E;YACnF,KAAK,EAAE,mDAAmD;SAC3D;KACF;IAED,KAAK,EAAE;QACL,MAAM,EAAE,QAAQ;QAChB,OAAO,EAAE;YACP,KAAK,EAAE,gEAAgE;YACvE,KAAK,EAAE,yDAAyD;SACjE;KACF;IAED,OAAO,EAAE,OAAO;IAChB,QAAQ,EAAE,OAAO;IACjB,MAAM,EAAE,OAAO;IACf,KAAK,EAAE,OAAO;IACd,MAAM,EAAE,OAAO;IACf,SAAS,EAAE,OAAO;IAClB,OAAO,EAAE,OAAO;IAChB,KAAK,EAAE,OAAO;IAEd,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC;IACjC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC;IAChC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC;IACtB,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC;IAE9B,KAAK,EAAE;QACL,KAAK,EAAE,OAAO;QACd,OAAO,EAAE;YACP,MAAM,EAAE,2DAA2D;YACnE,KAAK,EAAE,gDAAgD;SACxD;KACF;IAED,MAAM,EAAE;QACN,KAAK,EAAE,YAAY;QACnB,OAAO,EAAE;YACP,MAAM,EAAE,yDAAyD;YACjE,KAAK,EAAE,+CAA+C;SACvD;KACF;IAED,gGAAgG;IAChG,kGAAkG;IAClG,IAAI,EAAE,SAAS,CACb,wFAAwF;QACtF,8DAA8D,CACjE;IACD,MAAM,EAAE,SAAS,CAAC,gFAAgF,CAAC;IAEnG,IAAI,EAAE,SAAS,CAAC,mDAAmD,CAAC;IACpE,OAAO,EAAE,SAAS,CAAC,6CAA6C,CAAC;IACjE,IAAI,EAAE,SAAS,CAAC,uBAAuB,CAAC;IACxC,KAAK,EAAE,SAAS,CAAC,+EAA+E,CAAC;IACjG,IAAI,EAAE,SAAS,CAAC,+EAA+E,CAAC;IAChG,GAAG,EAAE,SAAS,CAAC,qCAAqC,CAAC;IACrD,MAAM,EAAE,SAAS,CAAC,0DAA0D,CAAC;IAC7E,QAAQ,EAAE,SAAS,CAAC,uDAAuD,CAAC;IAC5E,MAAM,EAAE,SAAS,CAAC,4EAA4E,CAAC;IAE/F,UAAU,EAAE,SAAS,CAAC,6DAA6D,CAAC;IACpF,WAAW,EAAE,SAAS,CAAC,6DAA6D,CAAC;IACrF,UAAU,EAAE,SAAS,CAAC,6DAA6D,CAAC;CACrF,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAyB,WAAW,CAAA;AAEjE,iGAAiG;AACjG,SAAS,aAAa,CAAC,IAAe,EAAE,WAAoB;IAC1D,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,CAAA;IAClC,IAAI,IAAI,KAAK,MAAM,IAAI,WAAW,EAAE,CAAC;QACnC,OAAO;YACL,KAAK,EAAE,YAAY;YACnB,OAAO,EAAE;gBACP,MAAM,EAAE,+DAA+D;gBACvE,KAAK,EAAE,oDAAoD;aAC5D;SACF,CAAA;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAYD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAe,EACf,OAA2B,EAC3B,UAAqC,EAAE;IAEvC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,KAAK,IAAI,CAAC,CAAA;IAC/D,MAAM,IAAI,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAA;IAC9D,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IACvC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,IAAI,oCAAoC,EAAE,CAAA;AAC/F,CAAC;AAED,MAAM,MAAM,GAAG,UAAU,CAAA;AAEzB,SAAS,UAAU,CAAC,IAAgB;IAClC,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,OAAO;YACV,OAAO,eAAe,MAAM,GAAG,CAAA;QACjC,KAAK,QAAQ;YACX,OAAO,gBAAgB,MAAM,GAAG,CAAA;QAClC,KAAK,UAAU;YACb,OAAO,uCAAuC,MAAM,IAAI,CAAA;IAC5D,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,WAAW,CAAC,IAAe,EAAE,WAAqB;IACzD,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;QACrB,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,MAAM,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC5E,CAAC;IACD,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,MAAM,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC1F,OAAO,gBAAgB,MAAM,mBAAmB,OAAO,EAAE,CAAA;AAC3D,CAAC;AAED,SAAS,YAAY,CAAC,IAAe,EAAE,KAAsB;IAC3D,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IAC5C,MAAM,IAAI,GAAG,IAAI,KAAK,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1D,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE,CAAA;AAC1D,CAAC;AAED,uGAAuG;AACvG,SAAS,iBAAiB,CAAC,IAAe,EAAE,KAAa;IACvD,IAAI,IAAI,KAAK,UAAU;QAAE,OAAO,yCAAyC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACrF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;AACzC,CAAC;AAOD,SAAS,MAAM,CAAC,KAAa,EAAE,QAAgB,EAAE,IAAe,EAAE,IAAwB;IACxF,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAA;IACpC,MAAM,IAAI,KAAK,CACb,UAAU,KAAK,MAAM,QAAQ,0BAA0B,IAAI,UAAU,IAAI,EAAE,CAC5E,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,KAAa,EACb,IAAe,EACf,MAAsB,EACtB,UAAqC,EAAE;IAEvC,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,WAAW,KAAK,IAAI,CAAC,CAAA;IAC/D,MAAM,OAAO,GAAa,EAAE,CAAA;IAC5B,MAAM,UAAU,GAAoB,EAAE,CAAA;IAEtC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,MAAM,CAAA;IACvC,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;QACvD,MAAM,QAAQ,GAAG,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;QACxD,IAAI,QAAQ,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,CAAC,KAAK,EAAE,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QAC5F,CAAC;QACD,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,IAAkB,CAAC,CAAA;QACpD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,OAAO,SAAS,EAAE,CAAC,CAAA;YACvC,UAAU,CAAC,SAAS,GAAG,SAAS,CAAA;QAClC,CAAC;QACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,OAAO,SAAS,EAAE,CAAC,CAAA;YACvC,UAAU,CAAC,SAAS,GAAG,SAAS,CAAA;QAClC,CAAC;IACH,CAAC;IAED,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAA;IACrC,MAAM,aAAa,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IAC5D,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QACrD,IAAI,aAAa,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,CAAC,KAAK,EAAE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;QAC9F,CAAC;QACD,MAAM,WAAW,GAAa,EAAE,CAAA;QAChC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,WAAW,CAAC,IAAI,CAAC,MAAM,QAAQ,EAAE,CAAC,CAAA;YAClC,UAAU,CAAC,QAAQ,GAAG,QAAQ,CAAA;QAChC,CAAC;QACD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,WAAW,CAAC,IAAI,CAAC,MAAM,QAAQ,EAAE,CAAC,CAAA;YAClC,UAAU,CAAC,QAAQ,GAAG,QAAQ,CAAA;QAChC,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,IAAiB,EAAE,WAAW,CAAC,CAAC,CAAA;IACzE,CAAC;IAED,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,CAAA;IAC3B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QAC3C,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QACtE,KAAK,MAAM,CAAC,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,CAAU,EAAE,CAAC;YACtE,IAAI,KAAK,KAAK,SAAS;gBAAE,SAAQ;YACjC,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;YACjD,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,UAAU,IAAI,YAAY,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,CAAA;QAC7E,CAAC;QACD,IAAI,GAAG,KAAK,SAAS;YAAE,UAAU,CAAC,GAAG,GAAG,GAAG,CAAA;QAC3C,IAAI,GAAG,KAAK,SAAS;YAAE,UAAU,CAAC,GAAG,GAAG,GAAG,CAAA;IAC7C,CAAC;IAED,kGAAkG;IAClG,+FAA+F;IAC/F,gGAAgG;IAChG,OAAO;QACL,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1D,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC;KAC1D,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,OAAiB;IACpC,MAAM,CAAC,IAAI,CAAC,GAAG,OAAO,CAAA;IACtB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAC3D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;AACnD,CAAC;AAED,mGAAmG;AACnG,SAAS,gBAAgB,CAAC,KAAa,EAAE,IAAe,EAAE,IAAe,EAAE,KAAsB;IAC/F,MAAM,SAAS,GAAG,IAAI,KAAK,SAAS,CAAA;IACpC,IAAI,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,UAAU,KAAK,mBAAmB,IAAI,8BAA8B,KAAK,gBAAgB,CAC1F,CAAA;IACH,CAAC;IACD,IAAI,CAAC,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,UAAU,KAAK,mBAAmB,IAAI,4CAA4C,KAAK,eAAe,CACvG,CAAA;IACH,CAAC;IACD,IAAI,CAAC,SAAS,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE,KAAe,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CACb,UAAU,KAAK,qBAAqB,KAAK,oBAAoB,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,eAAe,GAAG,CACnH,CAAA;IACH,CAAC;AACH,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The module source, or `null` when no model declares a hook.
|
|
2
|
+
* The module source, or `null` when no model declares a hook or a field validator.
|
|
3
3
|
*
|
|
4
4
|
* Returning null rather than an empty module keeps a `_supatype/hooks.ts` from appearing in projects
|
|
5
5
|
* that have no hooks, a generated file nobody imports is a file somebody eventually edits.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks-generator.d.ts","sourceRoot":"","sources":["../src/hooks-generator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hooks-generator.d.ts","sourceRoot":"","sources":["../src/hooks-generator.ts"],"names":[],"mappings":"AA2CA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAkW/D"}
|
package/dist/hooks-generator.js
CHANGED
|
@@ -16,15 +16,17 @@ function hookedModels(ast) {
|
|
|
16
16
|
return [];
|
|
17
17
|
return models
|
|
18
18
|
.filter((model) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const platform = model.annotations?.platform;
|
|
20
|
+
const declares = (value) => typeof value === "object" && value !== null && Object.keys(value).length > 0;
|
|
21
|
+
// Validators need the same row types hooks do, so a model declaring only validators must
|
|
22
|
+
// still appear here or `FieldValidator<"products", ...>` would not compile.
|
|
23
|
+
return declares(platform?.hooks) || declares(platform?.validate);
|
|
22
24
|
})
|
|
23
25
|
.map((model) => ({ table: resolveTableName(model), fields: model.fields }))
|
|
24
26
|
.sort((a, b) => a.table.localeCompare(b.table));
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
|
-
* The module source, or `null` when no model declares a hook.
|
|
29
|
+
* The module source, or `null` when no model declares a hook or a field validator.
|
|
28
30
|
*
|
|
29
31
|
* Returning null rather than an empty module keeps a `_supatype/hooks.ts` from appearing in projects
|
|
30
32
|
* that have no hooks, a generated file nobody imports is a file somebody eventually edits.
|
|
@@ -153,6 +155,78 @@ export type AfterDelete<T extends HookedTable> = (
|
|
|
153
155
|
ctx: AfterDeleteContext<T>,
|
|
154
156
|
) => void | Promise<void>
|
|
155
157
|
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A per-field validator: one field's value, and a verdict about it.
|
|
161
|
+
*
|
|
162
|
+
* \`FieldValidator<"products", "setup_items">\` types \`value\` as that column's real type, so a
|
|
163
|
+
* validator cannot quietly be written against the wrong shape.
|
|
164
|
+
*
|
|
165
|
+
* Return \`true\` to accept, or a message to refuse. The message reaches the caller attached to the
|
|
166
|
+
* field name, which is what lets a form put it on the input rather than in a banner: the whole
|
|
167
|
+
* reason for declaring this instead of putting the same logic in a \`beforeChange\` hook.
|
|
168
|
+
*/
|
|
169
|
+
export type FieldValidatorContext<T extends HookedTable, F extends keyof HookTables[T]["Row"]> =
|
|
170
|
+
HookBase & {
|
|
171
|
+
readonly operation: "insert" | "update"
|
|
172
|
+
readonly field: F
|
|
173
|
+
readonly value: HookTables[T]["Row"][F]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type FieldVerdict = true | string
|
|
177
|
+
|
|
178
|
+
export type FieldValidator<T extends HookedTable, F extends keyof HookTables[T]["Row"]> = (
|
|
179
|
+
ctx: FieldValidatorContext<T, F>,
|
|
180
|
+
) => FieldVerdict | Promise<FieldVerdict>
|
|
181
|
+
|
|
182
|
+
interface WireValidatorContext {
|
|
183
|
+
table?: string
|
|
184
|
+
operation?: string
|
|
185
|
+
field?: string
|
|
186
|
+
value?: unknown
|
|
187
|
+
user?: unknown
|
|
188
|
+
requestId?: string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Wrap a typed validator as the request handler the worker expects.
|
|
193
|
+
*
|
|
194
|
+
* A refusal is **422 with the field named**, not a bare 400: the request was understood and one
|
|
195
|
+
* value was rejected. A thrown error is deliberately *not* turned into a refusal, because a broken
|
|
196
|
+
* validator has not decided anything: it becomes a 500, which the server reads as unavailable and
|
|
197
|
+
* refuses the write on, rather than as the validator saying no.
|
|
198
|
+
*/
|
|
199
|
+
export function fieldValidator(
|
|
200
|
+
handler: (ctx: never) => FieldVerdict | Promise<FieldVerdict>,
|
|
201
|
+
): (req: Request) => Promise<Response> {
|
|
202
|
+
return async (req: Request): Promise<Response> => {
|
|
203
|
+
if (req.method !== "POST") {
|
|
204
|
+
return json({ message: "A validator is invoked with POST" }, 405)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let wire: WireValidatorContext
|
|
208
|
+
try {
|
|
209
|
+
wire = (await req.json()) as WireValidatorContext
|
|
210
|
+
} catch {
|
|
211
|
+
return json({ message: "Validator payload was not JSON" }, 400)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const field = wire.field ?? ""
|
|
215
|
+
const ctx = {
|
|
216
|
+
table: wire.table ?? "",
|
|
217
|
+
operation: wire.operation === "update" ? "update" : "insert",
|
|
218
|
+
field,
|
|
219
|
+
value: wire.value,
|
|
220
|
+
user: (wire.user ?? null) as never,
|
|
221
|
+
requestId: wire.requestId ?? "",
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const verdict = await handler(ctx as never)
|
|
225
|
+
if (verdict === true) return json({}, 200)
|
|
226
|
+
return json({ field, message: String(verdict), error: String(verdict) }, 422)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
156
230
|
/** Every handler a single function may serve, when one function covers a model's whole lifecycle. */
|
|
157
231
|
export interface HookHandlers<T extends HookedTable> {
|
|
158
232
|
readonly beforeChange?: BeforeChange<T>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks-generator.js","sourceRoot":"","sources":["../src/hooks-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,gBAAgB,GAEjB,MAAM,6BAA6B,CAAA;AAOpC,uEAAuE;AACvE,SAAS,YAAY,CAAC,GAAY;IAChC,MAAM,MAAM,GAAI,GAA+B,EAAE,MAAM,CAAA;IACvD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAA;IAErC,OAAO,MAAM;SACV,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;QAChB,MAAM,
|
|
1
|
+
{"version":3,"file":"hooks-generator.js","sourceRoot":"","sources":["../src/hooks-generator.ts"],"names":[],"mappings":"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"}
|
package/dist/model-hooks.d.ts
CHANGED
|
@@ -37,15 +37,45 @@ export interface ManifestHookEntry {
|
|
|
37
37
|
* cannot disagree about the safe direction.
|
|
38
38
|
*/
|
|
39
39
|
export declare function manifestHooks(ast: unknown): Record<string, Record<string, ManifestHookEntry>>;
|
|
40
|
+
/** One declared per-field validator. `event` is the field it checks, so reporting reads uniformly. */
|
|
41
|
+
export interface DeclaredValidator {
|
|
42
|
+
model: string;
|
|
43
|
+
field: string;
|
|
44
|
+
function: string;
|
|
45
|
+
}
|
|
46
|
+
/** Every per-field validator declared across the schema, in a stable order for reporting. */
|
|
47
|
+
export declare function declaredValidators(ast: unknown): DeclaredValidator[];
|
|
48
|
+
/**
|
|
49
|
+
* The validator map for `.supatype/manifest.json`, keyed by **table** then **column**.
|
|
50
|
+
*
|
|
51
|
+
* `onUnavailable` is written explicitly as `reject` rather than left to the server's default. The
|
|
52
|
+
* server does default that way, but its policy matches exact event names, and a validator that
|
|
53
|
+
* silently accepted a value because a new event name was missing from a switch is precisely the
|
|
54
|
+
* failure found when that path was built. Saying it here means neither side has to be right alone.
|
|
55
|
+
*/
|
|
56
|
+
export declare function manifestValidators(ast: unknown): Record<string, Record<string, ManifestHookEntry>>;
|
|
57
|
+
/**
|
|
58
|
+
* Validators naming a function that does not exist, as lines for a push failure.
|
|
59
|
+
*
|
|
60
|
+
* Shares `availableFunctions` with the hook check, so "what counts as a function" cannot come to
|
|
61
|
+
* mean two things.
|
|
62
|
+
*/
|
|
63
|
+
export declare function validateModelValidators(ast: unknown, functionsDir: string, cwd: string): string[];
|
|
40
64
|
/** Well below the 10s edge-function ceiling, so a hung hook fails fast instead of holding a slot. */
|
|
41
65
|
export declare const DEFAULT_HOOK_TIMEOUT_MS = 2000;
|
|
42
66
|
/**
|
|
43
|
-
* Merge the hook
|
|
67
|
+
* Merge the hook and validator maps into an existing `.supatype/manifest.json`.
|
|
68
|
+
*
|
|
69
|
+
* Both keys are written here rather than in two functions, because they fail together and for the
|
|
70
|
+
* same reason: each is a map the server reads to decide what to call around a write, and a manifest
|
|
71
|
+
* carrying a stale one calls the wrong thing or nothing at all. A validator that is never called is
|
|
72
|
+
* the worse half of that: the schema says the field is checked, and no error appears anywhere,
|
|
73
|
+
* because the write simply succeeds.
|
|
44
74
|
*
|
|
45
75
|
* **Only updates a manifest that is already there.** Creating one from scratch here would be a
|
|
46
76
|
* hazard: `functions_enabled` is a plain bool on the server's side, so a manifest carrying only
|
|
47
77
|
* hooks would read as functions *disabled*, the exact defect this repo fixed a commit ago, arriving
|
|
48
|
-
* by a different door. The compose path owns creation; this owns
|
|
78
|
+
* by a different door. The compose path owns creation; this owns two keys.
|
|
49
79
|
*
|
|
50
80
|
* Returns true when the file was rewritten.
|
|
51
81
|
*/
|
|
@@ -58,6 +88,18 @@ export interface HooksReport {
|
|
|
58
88
|
functionsDisabled: boolean;
|
|
59
89
|
/** True when a manifest exists but carries no hook map, so the server has nothing to call. */
|
|
60
90
|
mapMissing: boolean;
|
|
91
|
+
/** Field validators declared across the schema. */
|
|
92
|
+
validators: DeclaredValidator[];
|
|
93
|
+
/**
|
|
94
|
+
* Validators whose function directory is missing.
|
|
95
|
+
*
|
|
96
|
+
* Reported apart from `missing` because the consequence is different and worth saying plainly: a
|
|
97
|
+
* missing hook is a lifecycle step that will not run, a missing validator is a field written
|
|
98
|
+
* unchecked.
|
|
99
|
+
*/
|
|
100
|
+
validatorsMissing: DeclaredValidator[];
|
|
101
|
+
/** True when validators are declared and the manifest carries no validator map. */
|
|
102
|
+
validatorMapMissing: boolean;
|
|
61
103
|
}
|
|
62
104
|
/**
|
|
63
105
|
* What `supatype doctor` needs to answer "will my hooks actually run?".
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-hooks.d.ts","sourceRoot":"","sources":["../src/model-hooks.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,EAAE,CAqB1D;AAeD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAoBV;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAA;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAoC7F;AAED,qGAAqG;AACrG,eAAO,MAAM,uBAAuB,OAAO,CAAA;AAE3C
|
|
1
|
+
{"version":3,"file":"model-hooks.d.ts","sourceRoot":"","sources":["../src/model-hooks.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,EAAE,CAqB1D;AAeD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAoBV;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAA;CACjC;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAoC7F;AAGD,sGAAsG;AACtG,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,6FAA6F;AAC7F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,EAAE,CAqBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CA8BlG;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,GACV,MAAM,EAAE,CAqBV;AAED,qGAAqG;AACrG,eAAO,MAAM,uBAAuB,OAAO,CAAA;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAkBpE;AAyBD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,EAAE,CAAA;IACxB,iDAAiD;IACjD,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,2EAA2E;IAC3E,iBAAiB,EAAE,OAAO,CAAA;IAC1B,8FAA8F;IAC9F,UAAU,EAAE,OAAO,CAAA;IACnB,mDAAmD;IACnD,UAAU,EAAE,iBAAiB,EAAE,CAAA;IAC/B;;;;;;OAMG;IACH,iBAAiB,EAAE,iBAAiB,EAAE,CAAA;IACtC,mFAAmF;IACnF,mBAAmB,EAAE,OAAO,CAAA;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,WAAW,CAwCxF"}
|