@everystack/model 0.3.5 → 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 CHANGED
@@ -72,7 +72,7 @@ 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)`, and `.renamedFrom('oldField')` (turns a drop+add into a data-preserving `RENAME COLUMN`).
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
76
 
77
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.
78
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/model",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
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/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
- const readonlyCols = fieldsWhere(model, (f) => f.spec.isReadonly);
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