@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/model",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Declare a table's schema, validation, and authorization once — compiled to migrations, RLS, and handler config",
5
5
  "license": "AGPL-3.0-only",
6
6
  "publishConfig": {
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.