@everystack/model 0.4.13 → 0.4.14
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/model.ts +50 -0
package/package.json
CHANGED
package/src/model.ts
CHANGED
|
@@ -174,6 +174,23 @@ export interface ModelDefinition {
|
|
|
174
174
|
* just not exposed. Named `private` to avoid colliding with `schema: 'public'`.
|
|
175
175
|
*/
|
|
176
176
|
private?: boolean;
|
|
177
|
+
/**
|
|
178
|
+
* `rls: false` declares this table's row security OFF — authorization is grants-only,
|
|
179
|
+
* exactly as a brownfield database holds it (`db:pull` writes this line when the live
|
|
180
|
+
* table has RLS disabled and no ability renders). Without it the compiler declares RLS
|
|
181
|
+
* ENABLED, and the first plan after adopting such a table proposes
|
|
182
|
+
* `ENABLE ROW LEVEL SECURITY` — which, with zero policies, denies every non-owner role
|
|
183
|
+
* the table it already uses. Declaring the live truth is the fix; forcing the flag on
|
|
184
|
+
* was a silent lockdown nobody asked for.
|
|
185
|
+
*
|
|
186
|
+
* `false` is the only value: RLS-on is the default and has one spelling (omit this key).
|
|
187
|
+
* Refused with abilities (a policy under disabled RLS is a dead policy — declare
|
|
188
|
+
* `privileges` for grants-only access) and with `writtenBy: 'app'` (the app principal
|
|
189
|
+
* writes through its policies; there are none). Turning security OFF on a live table is
|
|
190
|
+
* a widening: the emitted `DISABLE ROW LEVEL SECURITY` carries the destructive-plan
|
|
191
|
+
* ceremony.
|
|
192
|
+
*/
|
|
193
|
+
rls?: false;
|
|
177
194
|
/**
|
|
178
195
|
* Triggers on this table — declared HERE because a trigger is existentially bound
|
|
179
196
|
* to its table and the table's owner must see what runs on every write (owner
|
|
@@ -205,6 +222,8 @@ export interface ModelDescriptor {
|
|
|
205
222
|
schema: string;
|
|
206
223
|
/** `true` removes the table from the generic data API (not in `exposedTables`). Default `false`. */
|
|
207
224
|
private: boolean;
|
|
225
|
+
/** Row-security posture — `false` = grants-only, no RLS (see {@link ModelDefinition.rls}). Default `true`. */
|
|
226
|
+
rls: boolean;
|
|
208
227
|
/** One-shot rename hint: this table used to be named `renamedFrom` (same schema). */
|
|
209
228
|
renamedFrom?: string;
|
|
210
229
|
/** One-shot move hint: this table used to live in schema `movedFrom` (same name) — emits SET SCHEMA. */
|
|
@@ -369,6 +388,36 @@ export function defineModel(table: string, def: ModelDefinition): ModelDescripto
|
|
|
369
388
|
}
|
|
370
389
|
}
|
|
371
390
|
|
|
391
|
+
// `rls: false` is the ONLY spelling — RLS-on is the default and one meaning gets one
|
|
392
|
+
// spelling, so `rls: true` (redundant today, load-bearing the day the default moves) is
|
|
393
|
+
// refused rather than accepted as a synonym.
|
|
394
|
+
if (def.rls !== undefined && def.rls !== false) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`model '${table}': rls: ${JSON.stringify(def.rls)} — RLS enabled is the default; omit the key. ` +
|
|
397
|
+
`The only declaration is rls: false (grants-only authorization, row security off).`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (def.rls === false) {
|
|
401
|
+
// An ability compiles to a policy, and a policy under disabled RLS is a dead policy —
|
|
402
|
+
// it filters nothing while looking like it does. The grants-only spelling is `privileges`.
|
|
403
|
+
if ((def.abilities ?? []).length) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`model '${table}': rls: false with abilities — an ability compiles to an RLS policy, and a ` +
|
|
406
|
+
`policy under disabled row security is dead: it filters nothing while reading as though it does. ` +
|
|
407
|
+
`Either drop rls: false (policies need RLS) or declare grants-only access via privileges.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
// `writtenBy: 'app'` means the app writes THROUGH its policies (RLS FORCEd). With RLS
|
|
411
|
+
// off there are no policies to write through — the combination is a contradiction.
|
|
412
|
+
if ((def.writtenBy ?? 'app') === 'app') {
|
|
413
|
+
throw new Error(
|
|
414
|
+
`model '${table}': rls: false with writtenBy: 'app' (the default) — 'app' writes through RLS ` +
|
|
415
|
+
`policies, and rls: false declares there are none. Declare writtenBy: 'worker' or 'functions' ` +
|
|
416
|
+
`(owner-connection writes), which is how a grants-only table is actually written.`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
372
421
|
validateTriggersFor('table', table, def.triggers ?? []);
|
|
373
422
|
|
|
374
423
|
// A rename and a move in one increment can't be composed from two independent markers
|
|
@@ -396,6 +445,7 @@ export function defineModel(table: string, def: ModelDefinition): ModelDescripto
|
|
|
396
445
|
softDelete: def.softDelete ?? false,
|
|
397
446
|
schema: def.schema ?? 'public',
|
|
398
447
|
private: def.private ?? false,
|
|
448
|
+
rls: def.rls ?? true,
|
|
399
449
|
...(def.renamedFrom ? { renamedFrom: def.renamedFrom } : {}),
|
|
400
450
|
...(def.movedFrom ? { movedFrom: def.movedFrom } : {}),
|
|
401
451
|
triggers: def.triggers ?? [],
|