@everystack/model 0.4.0 → 0.4.2
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/package.json +1 -1
- package/src/derived.ts +26 -0
- package/src/model.ts +16 -0
- package/src/module.ts +6 -0
package/package.json
CHANGED
package/src/derived.ts
CHANGED
|
@@ -27,6 +27,21 @@ import type { FieldBuilder } from './field';
|
|
|
27
27
|
/** Anything a derived object can declare a dependency on — order derives from these refs. */
|
|
28
28
|
export type DependsOnRef = ModelDescriptor | DerivedDescriptor;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The loud undefined gate. A missing named import under CJS interop evaluates to
|
|
32
|
+
* `undefined` and, unchecked, dies layers later as `'table' in undefined` with no
|
|
33
|
+
* context. Every ref list rejects the hole HERE, naming the owner, the list, the
|
|
34
|
+
* index, and the likely cause.
|
|
35
|
+
*/
|
|
36
|
+
export function rejectHoles(context: string, listName: string, list: readonly unknown[] | undefined): void {
|
|
37
|
+
if (!list) return;
|
|
38
|
+
list.forEach((entry, i) => {
|
|
39
|
+
if (entry == null) {
|
|
40
|
+
throw new Error(`${context}: ${listName}[${i}] is ${entry === undefined ? 'undefined' : 'null'} — renamed or missing import?`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
/**
|
|
31
46
|
* Relations (views/matviews) carry read-only authz in v1; functions carry execute.
|
|
32
47
|
* The row-scoped condition keys (`owner`, `via`, `where`, `sql`) are meaningless on
|
|
@@ -121,6 +136,9 @@ export interface TriggerSpec {
|
|
|
121
136
|
* the trigger attaches — defineModel / defineView.
|
|
122
137
|
*/
|
|
123
138
|
export function trigger(name: string, def: TriggerDefinition): TriggerSpec {
|
|
139
|
+
if (def.execute == null) {
|
|
140
|
+
throw new Error(`trigger '${name}': execute is ${def.execute === undefined ? 'undefined' : 'null'} — renamed or missing import?`);
|
|
141
|
+
}
|
|
124
142
|
const events = EVENT_ORDER.filter((e) => def.events.includes(e));
|
|
125
143
|
if (events.length === 0) throw new Error(`trigger '${name}': declare at least one event.`);
|
|
126
144
|
if (def.of && !events.includes('update')) {
|
|
@@ -151,6 +169,7 @@ export function trigger(name: string, def: TriggerDefinition): TriggerSpec {
|
|
|
151
169
|
|
|
152
170
|
/** The legality rules that depend on the trigger's TARGET (table vs view). */
|
|
153
171
|
export function validateTriggersFor(target: 'table' | 'view', owner: string, triggers: TriggerSpec[]): void {
|
|
172
|
+
rejectHoles(`${target} '${owner}'`, 'triggers', triggers);
|
|
154
173
|
for (const t of triggers) {
|
|
155
174
|
if (target === 'table' && t.timing === 'insteadOf') {
|
|
156
175
|
throw new Error(`table '${owner}', trigger '${t.name}': INSTEAD OF is a view trigger — attach it to the defineView it rewrites.`);
|
|
@@ -183,6 +202,9 @@ export interface SetofReturn {
|
|
|
183
202
|
}
|
|
184
203
|
|
|
185
204
|
export function setof(ref: SetofReturn['setof']): SetofReturn {
|
|
205
|
+
if (ref == null) {
|
|
206
|
+
throw new Error(`setof(): ref is ${ref === undefined ? 'undefined' : 'null'} — renamed or missing import?`);
|
|
207
|
+
}
|
|
186
208
|
return { setof: ref };
|
|
187
209
|
}
|
|
188
210
|
|
|
@@ -219,6 +241,7 @@ export interface FunctionDescriptor {
|
|
|
219
241
|
}
|
|
220
242
|
|
|
221
243
|
export function defineFunction(name: string, def: FunctionDefinition): FunctionDescriptor {
|
|
244
|
+
rejectHoles(`function '${name}'`, 'dependsOn', def.dependsOn);
|
|
222
245
|
const security = def.security ?? 'invoker';
|
|
223
246
|
if (security === 'definer' && (!def.searchPath || def.searchPath.length === 0)) {
|
|
224
247
|
throw new Error(
|
|
@@ -306,6 +329,7 @@ export interface ViewDescriptor {
|
|
|
306
329
|
}
|
|
307
330
|
|
|
308
331
|
export function defineView(name: string, def: ViewDefinition): ViewDescriptor {
|
|
332
|
+
rejectHoles(`view '${name}'`, 'dependsOn', def.dependsOn);
|
|
309
333
|
if (typeof def.securityInvoker !== 'boolean') {
|
|
310
334
|
throw new Error(
|
|
311
335
|
`view '${name}': securityInvoker is a required decision — true (readers' own RLS applies through the view) or false (definer semantics: the sealed-slice pattern; the body must not leak).`,
|
|
@@ -363,6 +387,7 @@ export interface MaterializedViewDescriptor {
|
|
|
363
387
|
}
|
|
364
388
|
|
|
365
389
|
export function defineMaterializedView(name: string, def: MaterializedViewDefinition): MaterializedViewDescriptor {
|
|
390
|
+
rejectHoles(`materialized view '${name}'`, 'dependsOn', def.dependsOn);
|
|
366
391
|
validateRelationAbilities('materialized view', name, def.abilities ?? []);
|
|
367
392
|
validatePrivateContradiction('materialized view', name, def.private, def.abilities);
|
|
368
393
|
validateRelationFields('materialized view', name, def.fields, def.private);
|
|
@@ -460,6 +485,7 @@ export interface SqlDescriptor {
|
|
|
460
485
|
* gates skip 'sql'-kind objects: what this hatch declares is on its author.
|
|
461
486
|
*/
|
|
462
487
|
export function defineSql(name: string, def: SqlDefinition): SqlDescriptor {
|
|
488
|
+
rejectHoles(`sql '${name}'`, 'dependsOn', def.dependsOn);
|
|
463
489
|
if (!def.kind || !def.kind.trim()) {
|
|
464
490
|
throw new Error(`defineSql '${name}': kind is required — the reconciler drops and joins objects by kind.`);
|
|
465
491
|
}
|
package/src/model.ts
CHANGED
|
@@ -124,6 +124,22 @@ export function defineModel(table: string, def: ModelDefinition): ModelDescripto
|
|
|
124
124
|
throw new Error(`model '${table}': can('execute') is a function ability — declare it on the defineFunction it gates.`);
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
// An array field's value default must be a JS array — `.default([])`, not the
|
|
129
|
+
// Postgres array literal `.default("{}")`. The literal is an implementation
|
|
130
|
+
// detail that leaks past the generator into drizzle's typed array `.default()`
|
|
131
|
+
// (which wants `string[] | SQL`) and fails tsc. Reject it at authoring time.
|
|
132
|
+
for (const [name, f] of Object.entries(def.fields)) {
|
|
133
|
+
const spec = f.spec;
|
|
134
|
+
if (spec.isArray && spec.defaultKind === 'value' && spec.default != null && !Array.isArray(spec.default)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`model '${table}': field '${name}' is an array — its .default() takes a JS array, ` +
|
|
137
|
+
`e.g. .default([]), not ${JSON.stringify(spec.default)}. ` +
|
|
138
|
+
`For a SQL expression default use sql\`\` via .defaultSql().`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
127
143
|
validateTriggersFor('table', table, def.triggers ?? []);
|
|
128
144
|
|
|
129
145
|
return {
|
package/src/module.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { ModelDescriptor } from './model';
|
|
23
23
|
import type { DerivedDescriptor, SequenceDescriptor } from './derived';
|
|
24
|
+
import { rejectHoles } from './derived';
|
|
24
25
|
|
|
25
26
|
export interface ModuleDefinition {
|
|
26
27
|
/** The package's Models (schema + authz). */
|
|
@@ -65,6 +66,11 @@ export interface Module {
|
|
|
65
66
|
|
|
66
67
|
/** Declare a package's database slice. Empty fields default to nothing — a models-only module is fine. */
|
|
67
68
|
export function defineModule(def: ModuleDefinition): Module {
|
|
69
|
+
// The loud undefined gate — a missing import composes as a hole; name it here,
|
|
70
|
+
// at the module boundary, before any list is flattened or iterated.
|
|
71
|
+
rejectHoles('defineModule', 'models', def.models);
|
|
72
|
+
rejectHoles('defineModule', 'derived', def.derived);
|
|
73
|
+
rejectHoles('defineModule', 'sequences', def.sequences);
|
|
68
74
|
// Derived identity is one name per kind: `DROP FUNCTION` renders no argument list, so
|
|
69
75
|
// v1 rejects function overloads outright, and same-name relations collide in the
|
|
70
76
|
// catalog anyway — caught here, at composition, before any SQL exists.
|