@everystack/model 0.3.4 → 0.3.6
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/README.md +3 -1
- package/package.json +1 -1
- package/src/derive.ts +4 -2
- package/src/field.ts +21 -0
- package/src/model.ts +12 -0
package/README.md
CHANGED
|
@@ -72,7 +72,9 @@ export const models = [Account, Note];
|
|
|
72
72
|
| `field.jsonb()` | `jsonb` |
|
|
73
73
|
| `field.enum('name', [...])` | a `CREATE TYPE … AS ENUM` (name is explicit, so `db:pull` round-trips it) |
|
|
74
74
|
|
|
75
|
-
Modifiers: `.primaryKey()`, `.notNull()` / `.nullable()`, `.unique()`, `.default(value)` / `.defaultNow()` / `.defaultRandom()`, `.references(() => Other, { onDelete, onUpdate })`, `.private()` (never serialized), `.readonly()` (never accepted on write), `.validate(zodSchema)`,
|
|
75
|
+
Modifiers: `.primaryKey()`, `.notNull()` / `.nullable()`, `.unique()`, `.default(value)` / `.defaultNow()` / `.defaultRandom()`, `.references(() => Other, { onDelete, onUpdate })`, `.private()` (never serialized), `.readonly()` (never accepted on write), `.validate(zodSchema)`, `.renamedFrom('oldField')` (turns a drop+add into a data-preserving `RENAME COLUMN`), and `.deprecated()` (the contract phase of contraction: the column stays in the database and stays readable — old clients keep working — while new writes are rejected and generated types strike it through; deleting the field later is the actual drop, destructive and gated).
|
|
76
|
+
|
|
77
|
+
Renaming a whole table works the same way, at the model level: `defineModel('articles', { renamedFrom: 'posts', ... })` compiles to one `ALTER TABLE … RENAME TO …` (data preserved, policies ride along) instead of CREATE plus a held DROP. Both markers are one-shot — remove them once every environment is past the state; a satisfied rename is an inert notice.
|
|
76
78
|
|
|
77
79
|
`.validate(...)` does double duty: the SQL-expressible refinements (`min`/`max`/`length`/range/enum) become a Postgres `CHECK` — the backstop for the SSR-direct write path that never runs app-layer Zod — while richer refinements stay app-layer only.
|
|
78
80
|
|
package/package.json
CHANGED
package/src/derive.ts
CHANGED
|
@@ -66,8 +66,10 @@ export function deriveHandlerConfig(models: ModelDescriptor[]): DerivedHandlerCo
|
|
|
66
66
|
const privateCols = fieldsWhere(model, (f) => f.spec.isPrivate);
|
|
67
67
|
if (privateCols.length) hiddenColumns[table] = privateCols;
|
|
68
68
|
|
|
69
|
-
// .readonly() → protectedFields (mass-assignment guard).
|
|
70
|
-
|
|
69
|
+
// .readonly() → protectedFields (mass-assignment guard). A deprecated field
|
|
70
|
+
// joins it — the contract phase rejects new writes but keeps reads serving
|
|
71
|
+
// (stale clients read for weeks; hiding it would break them before the drop).
|
|
72
|
+
const readonlyCols = fieldsWhere(model, (f) => f.spec.isReadonly || f.spec.isDeprecated);
|
|
71
73
|
if (readonlyCols.length) protectedFields[table] = readonlyCols;
|
|
72
74
|
|
|
73
75
|
// The first owner-shaped ability → the handler's rowOwnership IDOR backstop.
|
package/src/field.ts
CHANGED
|
@@ -55,6 +55,14 @@ export interface FieldSpec {
|
|
|
55
55
|
isPrivate: boolean;
|
|
56
56
|
/** `.readonly()` — never accepted on write (→ `protectedFields`). */
|
|
57
57
|
isReadonly: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* `.deprecated()` — the contract phase of contraction. The column stays in the
|
|
60
|
+
* database and stays READABLE (stale clients keep working); it leaves the write
|
|
61
|
+
* surface (→ `protectedFields`) and is struck through in generated types
|
|
62
|
+
* (`@deprecated` in the drizzle schema). Deleting the field from the model is
|
|
63
|
+
* the physical drop — a destructive, role-gated act.
|
|
64
|
+
*/
|
|
65
|
+
isDeprecated: boolean;
|
|
58
66
|
/** `.references(() => Other)` — FK target, a model thunk. Used by relation/ownership work. */
|
|
59
67
|
references?: () => unknown;
|
|
60
68
|
/** `ON DELETE` action for the FK. Omitted (or `'no action'`) is the Postgres default. */
|
|
@@ -97,6 +105,7 @@ export class FieldBuilder {
|
|
|
97
105
|
isArray: false,
|
|
98
106
|
isPrivate: false,
|
|
99
107
|
isReadonly: false,
|
|
108
|
+
isDeprecated: false,
|
|
100
109
|
};
|
|
101
110
|
}
|
|
102
111
|
|
|
@@ -139,6 +148,18 @@ export class FieldBuilder {
|
|
|
139
148
|
return this;
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Mark the contract phase of contraction (expand → deprecate → drop). The column
|
|
153
|
+
* stays in the database and stays readable — old clients keep working — but new
|
|
154
|
+
* writes are rejected (`protectedFields`) and generated types strike it through.
|
|
155
|
+
* The physical drop is deleting the field from the model: a destructive plan,
|
|
156
|
+
* confirmed, snapshotted, and approver-gated on protected stages.
|
|
157
|
+
*/
|
|
158
|
+
deprecated(): this {
|
|
159
|
+
this.spec.isDeprecated = true;
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
142
163
|
/**
|
|
143
164
|
* Declare a foreign key. The target is a model thunk (forward-reference safe). Optional
|
|
144
165
|
* `onDelete`/`onUpdate` referential actions are column-scoped, so they live here, not on
|
package/src/model.ts
CHANGED
|
@@ -63,6 +63,15 @@ export interface ModelDefinition {
|
|
|
63
63
|
* can span schemas — the model's schema always wins over a generate call's default.
|
|
64
64
|
*/
|
|
65
65
|
schema?: string;
|
|
66
|
+
/**
|
|
67
|
+
* `renamedFrom: 'old_table'` — a one-shot migration hint, the table-level
|
|
68
|
+
* analog of `field.renamedFrom()`. A diff cannot distinguish RENAME from
|
|
69
|
+
* DROP+ADD; the marker carries the intent, so the diff emits one
|
|
70
|
+
* `ALTER TABLE … RENAME TO …` (data preserved) instead of CREATE plus a
|
|
71
|
+
* held DROP. Same schema as the model. Remove it once every environment
|
|
72
|
+
* is past the state — a satisfied rename is an inert notice.
|
|
73
|
+
*/
|
|
74
|
+
renamedFrom?: string;
|
|
66
75
|
/**
|
|
67
76
|
* `true` removes the table from the generic data API — `deriveHandlerConfig`
|
|
68
77
|
* skips it, so it never enters `exposedTables`. Default `false`. Package-owned
|
|
@@ -90,6 +99,8 @@ export interface ModelDescriptor {
|
|
|
90
99
|
schema: string;
|
|
91
100
|
/** `true` removes the table from the generic data API (not in `exposedTables`). Default `false`. */
|
|
92
101
|
private: boolean;
|
|
102
|
+
/** One-shot rename hint: this table used to be named `renamedFrom` (same schema). */
|
|
103
|
+
renamedFrom?: string;
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
export function defineModel(table: string, def: ModelDefinition): ModelDescriptor {
|
|
@@ -108,5 +119,6 @@ export function defineModel(table: string, def: ModelDefinition): ModelDescripto
|
|
|
108
119
|
writtenBy: def.writtenBy ?? 'app',
|
|
109
120
|
schema: def.schema ?? 'public',
|
|
110
121
|
private: def.private ?? false,
|
|
122
|
+
...(def.renamedFrom ? { renamedFrom: def.renamedFrom } : {}),
|
|
111
123
|
};
|
|
112
124
|
}
|