@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
package/src/model-hooks.ts
CHANGED
|
@@ -164,16 +164,127 @@ export function manifestHooks(ast: unknown): Record<string, Record<string, Manif
|
|
|
164
164
|
return out
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
|
|
168
|
+
/** One declared per-field validator. `event` is the field it checks, so reporting reads uniformly. */
|
|
169
|
+
export interface DeclaredValidator {
|
|
170
|
+
model: string
|
|
171
|
+
field: string
|
|
172
|
+
function: string
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Every per-field validator declared across the schema, in a stable order for reporting. */
|
|
176
|
+
export function declaredValidators(ast: unknown): DeclaredValidator[] {
|
|
177
|
+
const models = (ast as { models?: unknown[] })?.models
|
|
178
|
+
if (!Array.isArray(models)) return []
|
|
179
|
+
|
|
180
|
+
const out: DeclaredValidator[] = []
|
|
181
|
+
for (const model of models) {
|
|
182
|
+
const shaped = model as {
|
|
183
|
+
name?: string
|
|
184
|
+
annotations?: { platform?: { validate?: Record<string, unknown> } }
|
|
185
|
+
}
|
|
186
|
+
const validators = shaped.annotations?.platform?.validate
|
|
187
|
+
if (typeof validators !== "object" || validators === null) continue
|
|
188
|
+
|
|
189
|
+
for (const [field, value] of Object.entries(validators)) {
|
|
190
|
+
const fn = (value as { function?: unknown })?.function
|
|
191
|
+
if (typeof fn === "string" && fn.length > 0) {
|
|
192
|
+
out.push({ model: shaped.name ?? "?", field, function: fn })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out.sort((a, b) => `${a.model}.${a.field}`.localeCompare(`${b.model}.${b.field}`))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The validator map for `.supatype/manifest.json`, keyed by **table** then **column**.
|
|
201
|
+
*
|
|
202
|
+
* `onUnavailable` is written explicitly as `reject` rather than left to the server's default. The
|
|
203
|
+
* server does default that way, but its policy matches exact event names, and a validator that
|
|
204
|
+
* silently accepted a value because a new event name was missing from a switch is precisely the
|
|
205
|
+
* failure found when that path was built. Saying it here means neither side has to be right alone.
|
|
206
|
+
*/
|
|
207
|
+
export function manifestValidators(ast: unknown): Record<string, Record<string, ManifestHookEntry>> {
|
|
208
|
+
const models = (ast as { models?: unknown[] })?.models
|
|
209
|
+
if (!Array.isArray(models)) return {}
|
|
210
|
+
|
|
211
|
+
const out: Record<string, Record<string, ManifestHookEntry>> = {}
|
|
212
|
+
for (const model of models) {
|
|
213
|
+
const shaped = model as {
|
|
214
|
+
annotations?: {
|
|
215
|
+
db?: { tableName?: string }
|
|
216
|
+
platform?: { validate?: Record<string, { function?: string; timeout?: number }> }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const table = shaped.annotations?.db?.tableName
|
|
220
|
+
const validators = shaped.annotations?.platform?.validate
|
|
221
|
+
if (typeof table !== "string" || table.length === 0) continue
|
|
222
|
+
if (typeof validators !== "object" || validators === null) continue
|
|
223
|
+
|
|
224
|
+
const entries: Record<string, ManifestHookEntry> = {}
|
|
225
|
+
for (const [field, value] of Object.entries(validators)) {
|
|
226
|
+
const fn = value?.function
|
|
227
|
+
if (typeof fn !== "string" || fn.length === 0) continue
|
|
228
|
+
entries[field] = {
|
|
229
|
+
function: fn,
|
|
230
|
+
timeout: typeof value.timeout === "number" ? value.timeout : DEFAULT_HOOK_TIMEOUT_MS,
|
|
231
|
+
onUnavailable: "reject",
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (Object.keys(entries).length > 0) out[table] = entries
|
|
235
|
+
}
|
|
236
|
+
return out
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Validators naming a function that does not exist, as lines for a push failure.
|
|
241
|
+
*
|
|
242
|
+
* Shares `availableFunctions` with the hook check, so "what counts as a function" cannot come to
|
|
243
|
+
* mean two things.
|
|
244
|
+
*/
|
|
245
|
+
export function validateModelValidators(
|
|
246
|
+
ast: unknown,
|
|
247
|
+
functionsDir: string,
|
|
248
|
+
cwd: string,
|
|
249
|
+
): string[] {
|
|
250
|
+
const validators = declaredValidators(ast)
|
|
251
|
+
if (validators.length === 0) return []
|
|
252
|
+
|
|
253
|
+
const available = availableFunctions(functionsDir)
|
|
254
|
+
const known = new Set(available)
|
|
255
|
+
const missing = validators.filter((entry) => !known.has(entry.function))
|
|
256
|
+
if (missing.length === 0) return []
|
|
257
|
+
|
|
258
|
+
const where = relative(cwd, functionsDir) || functionsDir
|
|
259
|
+
const lines = missing.map(
|
|
260
|
+
(entry) =>
|
|
261
|
+
` ${entry.model}.${entry.field} → "${entry.function}" (no ${where}/${entry.function}/index.ts)`,
|
|
262
|
+
)
|
|
263
|
+
lines.push("")
|
|
264
|
+
lines.push(
|
|
265
|
+
available.length > 0
|
|
266
|
+
? `Functions found in ${where}: ${available.join(", ")}`
|
|
267
|
+
: `No functions found in ${where}. Create one with: supatype hooks new <name>`,
|
|
268
|
+
)
|
|
269
|
+
return lines
|
|
270
|
+
}
|
|
271
|
+
|
|
167
272
|
/** Well below the 10s edge-function ceiling, so a hung hook fails fast instead of holding a slot. */
|
|
168
273
|
export const DEFAULT_HOOK_TIMEOUT_MS = 2000
|
|
169
274
|
|
|
170
275
|
/**
|
|
171
|
-
* Merge the hook
|
|
276
|
+
* Merge the hook and validator maps into an existing `.supatype/manifest.json`.
|
|
277
|
+
*
|
|
278
|
+
* Both keys are written here rather than in two functions, because they fail together and for the
|
|
279
|
+
* same reason: each is a map the server reads to decide what to call around a write, and a manifest
|
|
280
|
+
* carrying a stale one calls the wrong thing or nothing at all. A validator that is never called is
|
|
281
|
+
* the worse half of that: the schema says the field is checked, and no error appears anywhere,
|
|
282
|
+
* because the write simply succeeds.
|
|
172
283
|
*
|
|
173
284
|
* **Only updates a manifest that is already there.** Creating one from scratch here would be a
|
|
174
285
|
* hazard: `functions_enabled` is a plain bool on the server's side, so a manifest carrying only
|
|
175
286
|
* hooks would read as functions *disabled*, the exact defect this repo fixed a commit ago, arriving
|
|
176
|
-
* by a different door. The compose path owns creation; this owns
|
|
287
|
+
* by a different door. The compose path owns creation; this owns two keys.
|
|
177
288
|
*
|
|
178
289
|
* Returns true when the file was rewritten.
|
|
179
290
|
*/
|
|
@@ -189,17 +300,34 @@ export function syncManifestHooks(cwd: string, ast: unknown): boolean {
|
|
|
189
300
|
}
|
|
190
301
|
if (typeof parsed !== "object" || parsed === null) return false
|
|
191
302
|
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
303
|
+
const changedHooks = applyManifestMap(parsed, "hooks", manifestHooks(ast))
|
|
304
|
+
const changedValidators = applyManifestMap(parsed, "validators", manifestValidators(ast))
|
|
305
|
+
if (!changedHooks && !changedValidators) return false
|
|
306
|
+
|
|
307
|
+
writeFileSync(manifestPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8")
|
|
308
|
+
return true
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Set one manifest key to `map`, or remove it when the schema declares none.
|
|
313
|
+
*
|
|
314
|
+
* Removing rather than writing `{}` matters: the server distinguishes "no map" from "an empty map"
|
|
315
|
+
* when it decides whether the manifest predates the feature, and an empty object left behind by a
|
|
316
|
+
* schema that no longer declares any is not the same statement.
|
|
317
|
+
*/
|
|
318
|
+
function applyManifestMap(
|
|
319
|
+
manifest: Record<string, unknown>,
|
|
320
|
+
key: string,
|
|
321
|
+
map: Record<string, Record<string, ManifestHookEntry>>,
|
|
322
|
+
): boolean {
|
|
323
|
+
const next = JSON.stringify(map)
|
|
324
|
+
if (next === JSON.stringify(manifest[key] ?? {})) return false
|
|
196
325
|
|
|
197
|
-
if (Object.keys(
|
|
198
|
-
delete
|
|
326
|
+
if (Object.keys(map).length === 0) {
|
|
327
|
+
delete manifest[key]
|
|
199
328
|
} else {
|
|
200
|
-
|
|
329
|
+
manifest[key] = map
|
|
201
330
|
}
|
|
202
|
-
writeFileSync(manifestPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8")
|
|
203
331
|
return true
|
|
204
332
|
}
|
|
205
333
|
|
|
@@ -211,6 +339,18 @@ export interface HooksReport {
|
|
|
211
339
|
functionsDisabled: boolean
|
|
212
340
|
/** True when a manifest exists but carries no hook map, so the server has nothing to call. */
|
|
213
341
|
mapMissing: boolean
|
|
342
|
+
/** Field validators declared across the schema. */
|
|
343
|
+
validators: DeclaredValidator[]
|
|
344
|
+
/**
|
|
345
|
+
* Validators whose function directory is missing.
|
|
346
|
+
*
|
|
347
|
+
* Reported apart from `missing` because the consequence is different and worth saying plainly: a
|
|
348
|
+
* missing hook is a lifecycle step that will not run, a missing validator is a field written
|
|
349
|
+
* unchecked.
|
|
350
|
+
*/
|
|
351
|
+
validatorsMissing: DeclaredValidator[]
|
|
352
|
+
/** True when validators are declared and the manifest carries no validator map. */
|
|
353
|
+
validatorMapMissing: boolean
|
|
214
354
|
}
|
|
215
355
|
|
|
216
356
|
/**
|
|
@@ -225,15 +365,18 @@ export interface HooksReport {
|
|
|
225
365
|
*/
|
|
226
366
|
export function hooksReport(cwd: string, functionsDir: string, ast: unknown): HooksReport {
|
|
227
367
|
const declared = declaredHooks(ast)
|
|
368
|
+
const validators = declaredValidators(ast)
|
|
228
369
|
const manifestPath = join(cwd, ".supatype", "manifest.json")
|
|
229
370
|
|
|
230
371
|
let functionsDisabled = false
|
|
231
372
|
let mapMissing = false
|
|
232
|
-
|
|
373
|
+
let validatorMapMissing = false
|
|
374
|
+
if ((declared.length > 0 || validators.length > 0) && existsSync(manifestPath)) {
|
|
233
375
|
try {
|
|
234
376
|
const parsed = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>
|
|
235
377
|
functionsDisabled = parsed["functions_enabled"] === false
|
|
236
|
-
mapMissing = parsed["hooks"] === undefined
|
|
378
|
+
mapMissing = declared.length > 0 && parsed["hooks"] === undefined
|
|
379
|
+
validatorMapMissing = validators.length > 0 && parsed["validators"] === undefined
|
|
237
380
|
} catch {
|
|
238
381
|
// Unparseable: the server reports that far better than a doctor line could.
|
|
239
382
|
}
|
|
@@ -256,5 +399,8 @@ export function hooksReport(cwd: string, functionsDir: string, ast: unknown): Ho
|
|
|
256
399
|
missing: declared.filter((hook) => !known.has(hook.function)),
|
|
257
400
|
functionsDisabled,
|
|
258
401
|
mapMissing,
|
|
402
|
+
validators,
|
|
403
|
+
validatorsMissing: validators.filter((entry) => !known.has(entry.function)),
|
|
404
|
+
validatorMapMissing,
|
|
259
405
|
}
|
|
260
406
|
}
|
package/src/schema-ast-v2.ts
CHANGED
|
@@ -3,8 +3,80 @@
|
|
|
3
3
|
* Parsers build {@link ParsedField}; only `emitField` / `emitModel` / `emitSchema` produce JSON.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { FieldValidation } from "@supatype/types"
|
|
7
|
+
|
|
8
|
+
// Re-exported so consumers of these AST types need no second import for the one field that is
|
|
9
|
+
// declared elsewhere: the bound contract lives beside the modifiers that compile into it.
|
|
10
|
+
export type { FieldValidation }
|
|
11
|
+
|
|
6
12
|
export const AST_VERSION = 2 as const
|
|
7
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Every field kind the extractor can produce. **The registry, not a list.**
|
|
16
|
+
*
|
|
17
|
+
* A kind used to exist the moment someone wrote `scalar("newThing")` in the extractor's type switch,
|
|
18
|
+
* with nothing anywhere enumerating the set. That is how nine kinds came to accept a declared bound
|
|
19
|
+
* and silently enforce nothing: no code and no test could ask "what are all the kinds", so each was
|
|
20
|
+
* handled wherever someone happened to look.
|
|
21
|
+
*
|
|
22
|
+
* Naming a kind here is now the only way to create one, because {@link scalar} takes a `FieldKind`.
|
|
23
|
+
* Anything keyed by `Record<FieldKind, T>` is then exhaustive by the compiler rather than by
|
|
24
|
+
* somebody remembering: `BOUNDS_BY_KIND` in `field-bounds.ts` is the first such consumer, so adding
|
|
25
|
+
* a kind here fails the build until it is classified there.
|
|
26
|
+
*/
|
|
27
|
+
export const FIELD_KINDS = [
|
|
28
|
+
// Text-shaped
|
|
29
|
+
"text",
|
|
30
|
+
"email",
|
|
31
|
+
"url",
|
|
32
|
+
"slug",
|
|
33
|
+
"color",
|
|
34
|
+
"xml",
|
|
35
|
+
"ip",
|
|
36
|
+
"cidr",
|
|
37
|
+
"macaddr",
|
|
38
|
+
"tsQuery",
|
|
39
|
+
"tsVector",
|
|
40
|
+
"richText",
|
|
41
|
+
"bytes",
|
|
42
|
+
// Numeric
|
|
43
|
+
"integer",
|
|
44
|
+
"smallInt",
|
|
45
|
+
"bigInt",
|
|
46
|
+
"float",
|
|
47
|
+
"serial",
|
|
48
|
+
"bigSerial",
|
|
49
|
+
"decimal",
|
|
50
|
+
"money",
|
|
51
|
+
// Temporal
|
|
52
|
+
"datetime",
|
|
53
|
+
"timestamp",
|
|
54
|
+
"date",
|
|
55
|
+
"interval",
|
|
56
|
+
// Collections and structured values
|
|
57
|
+
"array",
|
|
58
|
+
"blocks",
|
|
59
|
+
"json",
|
|
60
|
+
"button",
|
|
61
|
+
"enum",
|
|
62
|
+
// Fixed-shape scalars
|
|
63
|
+
"boolean",
|
|
64
|
+
"uuid",
|
|
65
|
+
// Storage, spatial, plugin
|
|
66
|
+
"image",
|
|
67
|
+
"file",
|
|
68
|
+
"geo",
|
|
69
|
+
"vector",
|
|
70
|
+
"relation",
|
|
71
|
+
"custom",
|
|
72
|
+
// Composites, expanded into real columns before they reach a table
|
|
73
|
+
"timestamps",
|
|
74
|
+
"publishable",
|
|
75
|
+
"softDelete",
|
|
76
|
+
] as const
|
|
77
|
+
|
|
78
|
+
export type FieldKind = (typeof FIELD_KINDS)[number]
|
|
79
|
+
|
|
8
80
|
export type DefaultAst =
|
|
9
81
|
| { kind: "value"; value: string | number | boolean | null }
|
|
10
82
|
| { kind: "now" }
|
|
@@ -49,6 +121,15 @@ export interface KernelFieldFacts {
|
|
|
49
121
|
dimensions?: number
|
|
50
122
|
blocks?: BlockDefinitionAst[]
|
|
51
123
|
check?: string
|
|
124
|
+
validation?: FieldValidation
|
|
125
|
+
/**
|
|
126
|
+
* `JSON<T[]>` rather than `JSON<{...}>`, read from the declared type argument.
|
|
127
|
+
*
|
|
128
|
+
* Persisted because a model constraint naming this column with `ItemCount<>` has to resolve the
|
|
129
|
+
* same measure the field's own `MaxItems` would, and by then the type node is long gone. The
|
|
130
|
+
* engine never reads it: it receives the measure already resolved.
|
|
131
|
+
*/
|
|
132
|
+
jsonArray?: boolean
|
|
52
133
|
precision?: number
|
|
53
134
|
scale?: number
|
|
54
135
|
references?: string
|
|
@@ -64,7 +145,7 @@ export interface KernelFieldFacts {
|
|
|
64
145
|
|
|
65
146
|
/** Internal parse result: not serialized. */
|
|
66
147
|
export interface ParsedField {
|
|
67
|
-
kind:
|
|
148
|
+
kind: FieldKind
|
|
68
149
|
kernel: KernelFieldFacts
|
|
69
150
|
db: DbFieldAnnotations
|
|
70
151
|
platform: PlatformFieldAnnotations
|
|
@@ -87,8 +168,12 @@ export interface ModelAstV2 {
|
|
|
87
168
|
fields: Record<string, FieldAstV2>
|
|
88
169
|
options: Record<string, unknown>
|
|
89
170
|
annotations: {
|
|
90
|
-
db: { tableName: string; indexes: unknown[] }
|
|
91
|
-
platform: {
|
|
171
|
+
db: { tableName: string; indexes: unknown[]; constraints?: unknown[] }
|
|
172
|
+
platform: {
|
|
173
|
+
access: Record<string, unknown>
|
|
174
|
+
hooks?: Record<string, unknown>
|
|
175
|
+
validate?: Record<string, unknown>
|
|
176
|
+
}
|
|
92
177
|
}
|
|
93
178
|
}
|
|
94
179
|
|
|
@@ -110,7 +195,7 @@ export interface ExtractedSchemaAstV2 {
|
|
|
110
195
|
defaultLocale?: string
|
|
111
196
|
}
|
|
112
197
|
|
|
113
|
-
const DEFAULT_DB_BY_KIND: Record<
|
|
198
|
+
const DEFAULT_DB_BY_KIND: Partial<Record<FieldKind, Partial<DbFieldAnnotations>>> = {
|
|
114
199
|
text: { pgType: "TEXT" },
|
|
115
200
|
richText: { pgType: "JSONB" },
|
|
116
201
|
integer: { pgType: "INTEGER" },
|
|
@@ -147,7 +232,7 @@ const DEFAULT_DB_BY_KIND: Record<string, Partial<DbFieldAnnotations>> = {
|
|
|
147
232
|
blocks: { pgType: "JSONB" },
|
|
148
233
|
}
|
|
149
234
|
|
|
150
|
-
const DEFAULT_PLATFORM_BY_KIND: Record<
|
|
235
|
+
const DEFAULT_PLATFORM_BY_KIND: Partial<Record<FieldKind, Partial<PlatformFieldAnnotations>>> = {
|
|
151
236
|
richText: { editor: "rich" },
|
|
152
237
|
}
|
|
153
238
|
|
|
@@ -165,9 +250,15 @@ function stripUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
|
|
165
250
|
return out as Partial<T>
|
|
166
251
|
}
|
|
167
252
|
|
|
168
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* Start a parsed field with kind defaults for db/platform namespaces.
|
|
255
|
+
*
|
|
256
|
+
* Takes a plain `string` rather than a `FieldKind` because it is also called with a kind read back
|
|
257
|
+
* off the wire, where the value came from JSON and carries no compile-time guarantee. The optional
|
|
258
|
+
* lookup plus the fallback is what makes an unrecognised one safe.
|
|
259
|
+
*/
|
|
169
260
|
export function defaultPgTypeForKind(kind: string): string {
|
|
170
|
-
return DEFAULT_DB_BY_KIND[kind]?.pgType ?? "TEXT"
|
|
261
|
+
return DEFAULT_DB_BY_KIND[kind as FieldKind]?.pgType ?? "TEXT"
|
|
171
262
|
}
|
|
172
263
|
|
|
173
264
|
/** Flat wire shape for fields nested inside `blocks` definitions (engine FieldAst serde). */
|
|
@@ -195,6 +286,7 @@ export function emitBlockNestedField(field: FieldAstV2): FieldAstV2 {
|
|
|
195
286
|
if (field.srid !== undefined) wire.srid = field.srid
|
|
196
287
|
if (field.dimensions !== undefined) wire.dimensions = field.dimensions
|
|
197
288
|
if (field.check !== undefined) wire.check = field.check
|
|
289
|
+
if (field.validation !== undefined) wire.validation = field.validation
|
|
198
290
|
if (field.precision !== undefined) wire.precision = field.precision
|
|
199
291
|
if (field.scale !== undefined) wire.scale = field.scale
|
|
200
292
|
if (field.sources !== undefined) wire.sources = field.sources
|
|
@@ -207,7 +299,7 @@ export function emitBlockNestedField(field: FieldAstV2): FieldAstV2 {
|
|
|
207
299
|
}
|
|
208
300
|
|
|
209
301
|
export function scalar(
|
|
210
|
-
kind:
|
|
302
|
+
kind: FieldKind,
|
|
211
303
|
extra?: {
|
|
212
304
|
kernel?: Partial<KernelFieldFacts>
|
|
213
305
|
db?: Partial<DbFieldAnnotations>
|
|
@@ -267,6 +359,8 @@ export function emitField(parsed: ParsedField): FieldAstV2 {
|
|
|
267
359
|
}))
|
|
268
360
|
}
|
|
269
361
|
if (kernel.check !== undefined) wire.check = kernel.check
|
|
362
|
+
if (kernel.validation !== undefined) wire.validation = kernel.validation
|
|
363
|
+
if (kernel.jsonArray === true) wire.jsonArray = true
|
|
270
364
|
if (kernel.precision !== undefined) wire.precision = kernel.precision
|
|
271
365
|
if (kernel.scale !== undefined) wire.scale = kernel.scale
|
|
272
366
|
if (kernel.references !== undefined) wire.references = kernel.references
|
|
@@ -295,16 +389,26 @@ export function emitModel(
|
|
|
295
389
|
access: Record<string, unknown>,
|
|
296
390
|
indexes: unknown[] = [],
|
|
297
391
|
hooks: Record<string, unknown> = {},
|
|
392
|
+
constraints: unknown[] = [],
|
|
393
|
+
validators: Record<string, unknown> = {},
|
|
298
394
|
): ModelAstV2 {
|
|
299
395
|
return {
|
|
300
396
|
name,
|
|
301
397
|
fields,
|
|
302
398
|
options,
|
|
303
399
|
annotations: {
|
|
304
|
-
db
|
|
400
|
+
// Constraints sit in `db` beside `indexes`: both are things Postgres holds, unlike `access`
|
|
401
|
+
// and `hooks`, which the API layer enforces.
|
|
402
|
+
db: { tableName, indexes, ...(constraints.length > 0 && { constraints }) },
|
|
305
403
|
// Hooks sit in `platform` beside `access`: they are an API-layer concern, not a column one,
|
|
306
404
|
// supatype-server reads them, Postgres never sees them.
|
|
307
|
-
platform
|
|
405
|
+
// Validators sit in `platform` beside `hooks`: both are enforced by the API layer on the
|
|
406
|
+
// write path, and neither is something Postgres knows about.
|
|
407
|
+
platform: {
|
|
408
|
+
access,
|
|
409
|
+
...(Object.keys(hooks).length > 0 && { hooks }),
|
|
410
|
+
...(Object.keys(validators).length > 0 && { validate: validators }),
|
|
411
|
+
},
|
|
308
412
|
},
|
|
309
413
|
}
|
|
310
414
|
}
|