rip-lang 3.16.1 → 3.16.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.
Files changed (40) hide show
  1. package/README.md +2 -3
  2. package/bin/rip +39 -8
  3. package/bin/rip-schema +175 -0
  4. package/docs/RIP-APP.md +91 -2
  5. package/docs/RIP-DUCKDB.md +64 -1
  6. package/docs/RIP-INTRO.md +4 -4
  7. package/docs/RIP-LANG.md +32 -33
  8. package/docs/RIP-SCHEMA.md +1204 -364
  9. package/docs/dist/rip.js +3245 -611
  10. package/docs/dist/rip.min.js +1161 -289
  11. package/docs/dist/rip.min.js.br +0 -0
  12. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  13. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  14. package/docs/extensions/vscode/rip/index.html +2 -1
  15. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  16. package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
  17. package/docs/index.html +1 -1
  18. package/docs/ui/hljs-rip.js +1 -1
  19. package/package.json +7 -4
  20. package/src/AGENTS.md +39 -8
  21. package/src/compiler.js +220 -36
  22. package/src/components.js +315 -14
  23. package/src/dts.js +18 -3
  24. package/src/grammar/README.md +29 -170
  25. package/src/grammar/grammar.rip +17 -12
  26. package/src/grammar/solar.rip +4 -17
  27. package/src/lexer.js +24 -17
  28. package/src/parser.js +229 -229
  29. package/src/schema/dts.js +328 -54
  30. package/src/schema/loader-server.js +2 -1
  31. package/src/schema/runtime-browser-stubs.js +20 -9
  32. package/src/schema/runtime-ddl.js +161 -44
  33. package/src/schema/runtime-migrate.js +681 -0
  34. package/src/schema/runtime-orm.js +698 -54
  35. package/src/schema/runtime-validate.js +808 -24
  36. package/src/schema/runtime.generated.js +2395 -135
  37. package/src/schema/schema.js +1049 -89
  38. package/src/typecheck.js +283 -55
  39. package/src/types.js +5 -1
  40. package/src/grammar/lunar.rip +0 -2412
package/src/schema/dts.js CHANGED
@@ -53,10 +53,24 @@ export const SCHEMA_INTRINSIC_DECLS = [
53
53
  // correctly; when `In` defaults to unknown, `keyof In` is `never` and
54
54
  // algebra methods don't autocomplete — which is the right behavior
55
55
  // for :input schemas where the input shape isn't statically known.
56
+ 'interface ArraySchema<Out> {',
57
+ ' parse(data: unknown): Out[];',
58
+ ' safe(data: unknown): SchemaSafeResult<Out[]>;',
59
+ ' ok(data: unknown): boolean;',
60
+ ' parseAsync(data: unknown): Promise<Out[]>;',
61
+ ' safeAsync(data: unknown): Promise<SchemaSafeResult<Out[]>>;',
62
+ ' okAsync(data: unknown): Promise<boolean>;',
63
+ ' toJSONSchema(): Record<string, unknown>;',
64
+ '}',
56
65
  'interface Schema<Out, In = unknown> {',
57
- ' parse(data: In): Out;',
58
- ' safe(data: In): SchemaSafeResult<Out>;',
66
+ ' parse(data: unknown): Out;',
67
+ ' array: ArraySchema<Out>;',
68
+ ' safe(data: unknown): SchemaSafeResult<Out>;',
59
69
  ' ok(data: unknown): boolean;',
70
+ ' parseAsync(data: unknown): Promise<Out>;',
71
+ ' safeAsync(data: unknown): Promise<SchemaSafeResult<Out>>;',
72
+ ' okAsync(data: unknown): Promise<boolean>;',
73
+ ' toJSONSchema(): Record<string, unknown>;',
60
74
  ' pick<K extends keyof In>(...keys: K[]): Schema<Pick<In, K>, Pick<In, K>>;',
61
75
  ' omit<K extends keyof In>(...keys: K[]): Schema<Omit<In, K>, Omit<In, K>>;',
62
76
  ' partial(): Schema<Partial<In>, Partial<In>>;',
@@ -64,27 +78,52 @@ export const SCHEMA_INTRINSIC_DECLS = [
64
78
  ' extend<U>(other: Schema<U>): Schema<In & U, In & U>;',
65
79
  '}',
66
80
  // Chainable query builder for :model.
67
- 'interface SchemaQuery<T> {',
81
+ // `Data` carries the model's column shape so `.where({...})` keys are checked
82
+ // against the real fields; it defaults to a permissive map so a bare
83
+ // `SchemaQuery<T>` (no column type) keeps accepting any condition object.
84
+ 'interface SchemaQuery<T, Data = Record<string, unknown>> {',
68
85
  ' all(): Promise<T[]>;',
69
86
  ' first(): Promise<T | null>;',
70
87
  ' count(): Promise<number>;',
71
- ' limit(n: number): SchemaQuery<T>;',
72
- ' offset(n: number): SchemaQuery<T>;',
73
- ' order(spec: string): SchemaQuery<T>;',
88
+ ' where(cond: Partial<Record<keyof Data, unknown>> | string, ...params: unknown[]): SchemaQuery<T, Data>;',
89
+ ' limit(n: number): SchemaQuery<T, Data>;',
90
+ ' offset(n: number): SchemaQuery<T, Data>;',
91
+ ' order(spec: string): SchemaQuery<T, Data>;',
92
+ ' orderBy(spec: string): SchemaQuery<T, Data>;',
93
+ ' includes(...specs: unknown[]): SchemaQuery<T, Data>;',
94
+ ' withDeleted(): SchemaQuery<T, Data>;',
95
+ ' onlyDeleted(): SchemaQuery<T, Data>;',
96
+ ' updateAll(values: Partial<Record<keyof Data, unknown>>): Promise<number | null>;',
97
+ ' deleteAll(): Promise<number | null>;',
98
+ ' unscoped(): SchemaQuery<T, Data>;',
74
99
  '}',
75
100
  // ModelSchema extends the base schema surface with ORM methods. Algebra
76
101
  // over `Data` (not `Instance`) so derived shapes reflect runtime
77
- // behavior-dropping semantics.
78
- 'interface ModelSchema<Instance, Data = unknown> extends Schema<Instance, Data> {',
79
- ' find(id: unknown): Promise<Instance | null>;',
80
- ' findMany(ids: unknown[]): Promise<Instance[]>;',
81
- ' where(cond: Record<string, unknown> | string, ...params: unknown[]): SchemaQuery<Instance>;',
102
+ // behavior-dropping semantics. `Id` is the primary-key type (always `number`
103
+ // today `INTEGER PRIMARY KEY`); `Create` is the per-model create-input
104
+ // shape (required fields required, auto-managed columns omitted) that codegen
105
+ // threads in. Both default sanely so a bare `ModelSchema<I, D>` still works.
106
+ 'interface ModelSchema<Instance, Data = unknown, Id = number, Create = Partial<Data>> extends Schema<Instance, Data> {',
107
+ ' find(id: Id): Promise<Instance | null>;',
108
+ ' findMany(ids: Id[]): Promise<Instance[]>;',
109
+ ' where(cond: Partial<Record<keyof Data, unknown>> | string, ...params: unknown[]): SchemaQuery<Instance, Data>;',
110
+ ' includes(...specs: unknown[]): SchemaQuery<Instance, Data>;',
111
+ ' withDeleted(): SchemaQuery<Instance, Data>;',
112
+ ' onlyDeleted(): SchemaQuery<Instance, Data>;',
113
+ ' unscoped(): SchemaQuery<Instance, Data>;',
82
114
  ' all(limit?: number): Promise<Instance[]>;',
83
115
  ' first(): Promise<Instance | null>;',
84
- ' count(cond?: Record<string, unknown>): Promise<number>;',
85
- ' create(data: Partial<Data>): Promise<Instance>;',
116
+ ' count(cond?: Partial<Record<keyof Data, unknown>>): Promise<number>;',
117
+ ' create(data: Create): Promise<Instance>;',
118
+ ' upsert(data: Create, opts: { on: unknown }): Promise<Instance>;',
119
+ ' insertMany(rows: Create[]): Promise<Instance[]>;',
86
120
  ' toSQL(options?: { dropFirst?: boolean; header?: string; idStart?: number }): string;',
87
121
  '}',
122
+ // Runtime namespace for transaction control (schema.transaction! ->).
123
+ 'declare const schema: {',
124
+ ' transaction<T>(fn: () => T | Promise<T>): Promise<T>;',
125
+ ' transaction<T>(opts: Record<string, unknown>, fn: () => T | Promise<T>): Promise<T>;',
126
+ '};',
88
127
  ];
89
128
 
90
129
  const RIP_TYPE_TO_TS = {
@@ -104,6 +143,12 @@ const RIP_TYPE_TO_TS = {
104
143
  any: 'any',
105
144
  };
106
145
 
146
+ // Non-built-in type names emit as-is: every named schema now declares a
147
+ // bare `type Name`, so same-file nested references — including
148
+ // self-references (`Tree = schema :shape` with `children? Tree[]`) and
149
+ // mutual recursion — resolve natively as recursive TS type aliases.
150
+ // Unknown (cross-file) identifiers also emit as-is, matching the
151
+ // runtime's permissive resolution.
107
152
  function mapFieldType(entry) {
108
153
  if (entry.typeName === 'literal-union' && entry.literals?.length) {
109
154
  return entry.literals.map(l => JSON.stringify(l)).join(' | ');
@@ -112,29 +157,21 @@ function mapFieldType(entry) {
112
157
  return entry.array ? `${base}[]` : base;
113
158
  }
114
159
 
115
- // Extract descriptor from a SCHEMA_BODY s-expr node. Grammar reduces
116
- // `['schema', SCHEMA_BODY_VAL]` where the value is the String wrapper
117
- // carrying `.descriptor` via the metadata bridge.
118
- function descriptorFromSchemaNode(schemaNode) {
119
- if (!Array.isArray(schemaNode)) return null;
120
- let head = schemaNode[0]?.valueOf?.() ?? schemaNode[0];
121
- if (head !== 'schema') return null;
122
- let body = schemaNode[1];
123
- if (!body || typeof body !== 'object') return null;
124
- if (body.descriptor) return body.descriptor;
125
- if (body.data?.descriptor) return body.data.descriptor;
126
- return null;
127
- }
160
+ // Extract descriptor from a SCHEMA_BODY s-expr node the shared decoder
161
+ // lives in schema.js (the projection fold uses the same one, so the two
162
+ // passes can't drift on what counts as a schema node).
163
+ import { schemaNodeDescriptor as descriptorFromSchemaNode } from './schema.js';
128
164
 
129
165
  // Walk the parsed s-expression collecting every named schema declaration.
130
166
  // Mixins are emitted first so subsequent :shape/:model type aliases can
131
167
  // reference them in `& Timestamps`-style intersections. Within a group,
132
168
  // source order is preserved. Returns true when at least one schema was
133
169
  // found (drives intrinsic preamble injection).
134
- export function emitSchemaTypes(sexpr, lines) {
170
+ export function emitSchemaTypes(sexpr, lines, schemaBehavior = null, schemaAnon = null) {
135
171
  const collected = [];
136
172
  collectSchemas(sexpr, collected);
137
- if (!collected.length) return false;
173
+ const anon = schemaAnon || [];
174
+ if (!collected.length && !anon.length) return false;
138
175
 
139
176
  // Set of locally-known schema names (for relation-accessor type
140
177
  // resolution — same-file targets get typed, unknown targets degrade).
@@ -143,14 +180,61 @@ export function emitSchemaTypes(sexpr, lines) {
143
180
 
144
181
  // Mixin types first so type aliases down-file can reference them.
145
182
  for (const c of collected) {
146
- if (c.descriptor.kind === 'mixin') emitOneSchemaType(c, byName, known, lines);
183
+ if (c.descriptor?.kind === 'mixin') emitOneSchemaType(c, byName, known, lines, schemaBehavior);
147
184
  }
148
185
  for (const c of collected) {
149
- if (c.descriptor.kind !== 'mixin') emitOneSchemaType(c, byName, known, lines);
186
+ if (c.descriptor?.kind !== 'mixin') emitOneSchemaType(c, byName, known, lines, schemaBehavior);
187
+ }
188
+
189
+ // Anonymous (expression-position) schemas — codegen stashed each one's
190
+ // descriptor under a synthesized `__anon` name (shadow-TS mode only;
191
+ // field-bearing kinds only — emitSchemaNode gates the stamp). Emit a
192
+ // field-type alias plus a `__schema` overload keyed on that descriptor
193
+ // key, so e.g. `Base.extend(schema :shape …)` resolves to a precise
194
+ // `Schema<T, T>` instead of the `(d: any) => any` fallback — which made
195
+ // `extend` contribute nothing to the derived type.
196
+ for (const a of anon) {
197
+ const fieldProps = fieldPropList(a.descriptor);
198
+ const mixinRefs = mixinIntersections(a.descriptor, byName);
199
+ const dataBase = `{ ${fieldProps.join('; ')} }`;
200
+ const dataType = mixinRefs.length ? `${dataBase} & ${mixinRefs.join(' & ')}` : dataBase;
201
+ lines.push(`type ${a.name} = ${dataType};`);
202
+ lines.push(`declare function __schema(d: { __anon: ${JSON.stringify(a.name)}; [k: string]: any }): Schema<${a.name}, ${a.name}>;`);
150
203
  }
151
204
  return true;
152
205
  }
153
206
 
207
+ // Schema algebra methods that derive a fresh schema from an existing one.
208
+ // A `Name = Base.<method>(...)` assignment (possibly chained) is a derived
209
+ // schema and gets a bare `type Name` even though it has no `schema` body.
210
+ const SCHEMA_ALGEBRA = new Set(['pick', 'omit', 'partial', 'required', 'extend']);
211
+
212
+ // Given an assignment's RHS s-expr, decide whether it's a schema-algebra
213
+ // call chain (`Base.pick(...)`, `Base.pick(...).omit(...)`, …) and return the
214
+ // root base identifier — or null when it isn't one. The base lets the caller
215
+ // confirm it resolves to a known schema before emitting a type for it, so an
216
+ // unrelated `foo = bar.partial()` never gets a (spurious) schema type.
217
+ function derivedSchemaBase(rhs) {
218
+ if (!Array.isArray(rhs)) return null;
219
+ const callee = rhs[0];
220
+ if (!Array.isArray(callee)) return null;
221
+ const dot = callee[0]?.valueOf?.() ?? callee[0];
222
+ if (dot !== '.') return null;
223
+ const method = callee[2]?.valueOf?.() ?? callee[2];
224
+ if (!SCHEMA_ALGEBRA.has(method)) return null;
225
+ // Descend through member accesses and call nodes to the root identifier:
226
+ // `User.pick(...).omit(...)` → callee[1] is the inner `.pick(...)` call.
227
+ let obj = callee[1];
228
+ while (Array.isArray(obj)) {
229
+ const head = obj[0]?.valueOf?.() ?? obj[0];
230
+ if (head === '.') obj = obj[1]; // member access — descend the object
231
+ else if (Array.isArray(obj[0])) obj = obj[0]; // call node — descend the callee
232
+ else break;
233
+ }
234
+ const root = obj?.valueOf?.() ?? obj;
235
+ return typeof root === 'string' ? root : null;
236
+ }
237
+
154
238
  function collectSchemas(sexpr, out) {
155
239
  if (!Array.isArray(sexpr)) return;
156
240
  const head = sexpr[0]?.valueOf?.() ?? sexpr[0];
@@ -170,17 +254,44 @@ function collectSchemas(sexpr, out) {
170
254
  }
171
255
  if (assignNode && Array.isArray(assignNode[2])) {
172
256
  const name = assignNode[1]?.valueOf?.() ?? assignNode[1];
257
+ if (typeof name !== 'string') return;
173
258
  const descriptor = descriptorFromSchemaNode(assignNode[2]);
174
- if (typeof name === 'string' && descriptor) {
259
+ if (descriptor) {
175
260
  out.push({ name, descriptor, exported });
261
+ } else {
262
+ // A derived schema (`Name = Base.pick(...)`) has no `schema` body, so it
263
+ // carries no descriptor — record its base instead. emitSchemaTypes emits
264
+ // a bare `type Name` for it once the base is confirmed to be a schema.
265
+ const derivedBase = derivedSchemaBase(assignNode[2]);
266
+ if (derivedBase) out.push({ name, derivedBase, exported });
176
267
  }
177
268
  }
178
269
  }
179
270
 
180
- function emitOneSchemaType(collected, byName, known, lines) {
271
+ function emitOneSchemaType(collected, byName, known, lines, schemaBehavior) {
181
272
  const { name, descriptor, exported } = collected;
182
273
  const exp = exported ? 'export ' : '';
183
- const decl = exported ? '' : 'declare ';
274
+
275
+ // Derived schema (`Name = Base.pick(...)`): no body, so no descriptor. Give it
276
+ // a bare type so it can be annotated (`u:: UserView`) and re-exported under a
277
+ // clean name. The type is the source-free result
278
+ // of the algebra, which the `Schema<Out, In>` interface methods already model
279
+ // exactly; reading it back off the value's own `parse` return reuses that
280
+ // inference rather than re-deriving Pick/Omit/Partial here, and so handles
281
+ // every operator and chained projection for free. Gated on the base being a
282
+ // locally-known schema, so an unrelated `foo = bar.partial()` never gets a
283
+ // bogus schema type (its `parse` lookup would otherwise error).
284
+ if (collected.derivedBase) {
285
+ if (!known.has(collected.derivedBase)) return;
286
+ lines.push(`${exp}type ${name} = ReturnType<(typeof ${name})['parse']>;`);
287
+ return;
288
+ }
289
+ // Always `declare`: the value binding is provided by the compiled body
290
+ // (`const Name = __schema(...)`), so the type surface is ambient. Without
291
+ // `declare`, an exported `export const Name: T;` in a `.ts` shadow is an
292
+ // uninitialized const (TS1155). `export declare const` is valid in both
293
+ // the `.ts` shadow and a published `.d.ts`.
294
+ const decl = 'declare ';
184
295
 
185
296
  if (descriptor.kind === 'enum') {
186
297
  const members = [];
@@ -204,61 +315,163 @@ function emitOneSchemaType(collected, byName, known, lines) {
204
315
  return;
205
316
  }
206
317
 
318
+ if (descriptor.kind === 'union') {
319
+ // Discriminated union: the bare type is the TS union of the
320
+ // constituents' bare instance types, so narrowing via the
321
+ // discriminator works natively. The const exposes the validation
322
+ // surface only — unions have no fields, so no algebra methods.
323
+ const members = descriptor.entries.filter(e => e.tag === 'union-member').map(e => e.name);
324
+ const u = members.length ? members.join(' | ') : 'never';
325
+ lines.push(`${exp}type ${name} = ${u};`);
326
+ lines.push(`${exp}${decl}const ${name}: { ` +
327
+ `parse(data: unknown): ${name}; ` +
328
+ `safe(data: unknown): SchemaSafeResult<${name}>; ` +
329
+ `ok(data: unknown): boolean; ` +
330
+ `parseAsync(data: unknown): Promise<${name}>; ` +
331
+ `safeAsync(data: unknown): Promise<SchemaSafeResult<${name}>>; ` +
332
+ `okAsync(data: unknown): Promise<boolean>; };`);
333
+ return;
334
+ }
335
+
207
336
  const fieldProps = fieldPropList(descriptor);
208
337
  const mixinRefs = mixinIntersections(descriptor, byName);
338
+
339
+ // Shadow-TS only: codegen stashed the compiled `~>`/`!>` bodies, so a
340
+ // computed/derived member's type can be inferred from its body via
341
+ // `ReturnType<typeof __<Name>__behavior.field>` (gap 13) — `status` becomes
342
+ // `"Completed" | "Pending"` instead of `unknown`. Without the buffer (a plain
343
+ // `.d.ts` emit, where no behavior const exists), the value type stays
344
+ // `unknown`. The behavior const itself is emitted just below.
345
+ const behaviorList = (schemaBehavior && schemaBehavior.get(name)) || null;
346
+ const behaviorVar = `__${name}__behavior`;
347
+ const inferredFields = new Set((behaviorList || []).map(b => b.field));
348
+ const memberType = (field) =>
349
+ inferredFields.has(field) ? `ReturnType<typeof ${behaviorVar}.${field}>` : 'unknown';
350
+
209
351
  const methods = [];
210
352
  const computed = [];
353
+ const derived = [];
211
354
  for (const e of descriptor.entries) {
212
355
  if (e.tag === 'method') {
213
- methods.push(`${e.name}: (...args: any[]) => unknown`);
356
+ // A method whose params are all annotated rides the behavior
357
+ // buffer: `typeof` yields its full signature — typed params,
358
+ // `this`, and the inferred return. Unannotated methods keep the
359
+ // honest fallback.
360
+ methods.push(inferredFields.has(e.name)
361
+ ? `${e.name}: typeof ${behaviorVar}.${e.name}`
362
+ : `${e.name}: (...args: any[]) => unknown`);
214
363
  } else if (e.tag === 'computed') {
215
- computed.push(`readonly ${e.name}: unknown`);
364
+ computed.push(`readonly ${e.name}: ${memberType(e.name)}`);
365
+ } else if (e.tag === 'derived') {
366
+ // `!>` eager-derived: an own *enumerable* property materialized at
367
+ // parse/hydrate, so it's part of the instance (and serializes) but
368
+ // isn't an input/projectable field — it lives on the Out type, never
369
+ // in `<Name>Data`. Writable (unlike the `~>` getter), so not readonly.
370
+ derived.push(`${e.name}: ${memberType(e.name)}`);
216
371
  }
217
372
  // hooks are intentionally omitted — they fire automatically and
218
373
  // shouldn't appear in autocomplete.
219
374
  }
220
375
 
376
+ // Emit the behavior const that anchors the `ReturnType<…>` inferences above.
377
+ // It re-uses the already-compiled `function(this: <Name>) { … }` bodies as
378
+ // object properties; `typeof __<Name>__behavior.field` then yields each
379
+ // body's signature. Forward-references to the instance type resolve fine in
380
+ // TS type space. Only present in shadow-TS mode (when the buffer is set).
381
+ if (behaviorList && behaviorList.length) {
382
+ const props = behaviorList.map(b => `${b.field}: ${b.fnExpr}`).join(', ');
383
+ lines.push(`const ${behaviorVar} = { ${props} };`);
384
+ }
385
+
221
386
  const dataBase = `{ ${fieldProps.join('; ')} }`;
222
387
  const dataType = mixinRefs.length ? `${dataBase} & ${mixinRefs.join(' & ')}` : dataBase;
223
388
 
224
389
  if (descriptor.kind === 'model') {
225
390
  const dataName = `${name}Data`;
226
- const instName = `${name}Instance`;
391
+ // Class-style: the bare schema name IS the instance type (parse result),
392
+ // mirroring how a class names both its value and its instance type — and
393
+ // how :enum/:mixin already emit a bare type. `${name}Data` survives as the
394
+ // fields-only shape that algebra/`toJSON` derive from.
395
+ const instName = name;
227
396
  const relationAccessors = modelRelationAccessors(descriptor, known);
397
+ // `${name}Data` includes the columns a :model manages implicitly — the
398
+ // `id` primary key, `@timestamps`, `@softDelete`, and `@belongs_to` FKs —
399
+ // so they appear in `toJSON()` and are projectable via `.pick`/`.omit`,
400
+ // matching the runtime's projectable field set.
401
+ const implicitProps = modelImplicitProps(descriptor);
402
+ const modelDataType = implicitProps.length ? `${dataType} & { ${implicitProps.join('; ')} }` : dataType;
403
+ // Create-input type: required-declared fields without a default and
404
+ // non-null FKs are required; everything else is optional; `id` and
405
+ // timestamps are omitted (the DB manages them). Threaded into ModelSchema
406
+ // so `create({})` flags a missing required field at compile time.
407
+ const createName = `${name}Create`;
408
+ const createBase = modelCreateInputType(descriptor);
409
+ const createType = mixinRefs.length ? `${createBase} & ${mixinRefs.join(' & ')}` : createBase;
410
+ const softDelete = descriptor.entries.some(e => e.tag === 'directive' && e.name === 'softDelete');
228
411
  const instanceExtras = [
412
+ ...derived,
229
413
  ...computed,
230
414
  ...methods,
231
415
  ...relationAccessors,
232
416
  `save(): Promise<${instName}>`,
233
- `destroy(): Promise<${instName}>`,
417
+ `destroy(opts?: { hard?: boolean }): Promise<${instName}>`,
418
+ ...(softDelete ? [`restore(): Promise<${instName}>`] : []),
234
419
  `ok(): boolean`,
235
420
  `errors(): SchemaIssue[]`,
236
421
  `toJSON(): ${dataName}`,
237
422
  ];
238
- lines.push(`${exp}type ${dataName} = ${dataType};`);
423
+ lines.push(`${exp}type ${dataName} = ${modelDataType};`);
424
+ lines.push(`${exp}type ${createName} = ${createType};`);
239
425
  lines.push(`${exp}type ${instName} = ${dataName} & { ${instanceExtras.join('; ')} };`);
240
- lines.push(`${exp}${decl}const ${name}: ModelSchema<${instName}, ${dataName}>;`);
426
+ // @scope declarations surface as statics on the model const AND as
427
+ // chainable methods on a per-model query alias, so scope-first
428
+ // chains (`User.active().since(d).all()`) typecheck.
429
+ const scopeNames = descriptor.entries.filter(e => e.tag === 'scope').map(e => e.name);
430
+ const modelT = `ModelSchema<${instName}, ${dataName}, number, ${createName}>`;
431
+ if (scopeNames.length) {
432
+ const queryName = `${name}Query`;
433
+ const scopeSigs = scopeNames.map(s => `${s}(...args: any[]): ${queryName}`);
434
+ lines.push(`${exp}type ${queryName} = SchemaQuery<${instName}, ${dataName}> & { ${scopeSigs.join('; ')} };`);
435
+ lines.push(`${exp}${decl}const ${name}: ${modelT} & { ${scopeSigs.join('; ')} };`);
436
+ } else {
437
+ lines.push(`${exp}${decl}const ${name}: ${modelT};`);
438
+ }
241
439
  return;
242
440
  }
243
441
 
244
442
  if (descriptor.kind === 'shape') {
245
443
  const dataName = `${name}Data`;
246
- const instName = `${name}Instance`;
247
- const hasBehavior = methods.length + computed.length > 0;
248
- lines.push(`${exp}type ${dataName} = ${dataType};`);
249
- if (hasBehavior) {
250
- lines.push(`${exp}type ${instName} = ${dataName} & { ${[...computed, ...methods].join('; ')} };`);
251
- lines.push(`${exp}${decl}const ${name}: Schema<${instName}, ${dataName}>;`);
444
+ // `!>` derived own props, `~>` computed getters, and methods all attach to
445
+ // the instance (Out) but not the projectable `${name}Data` (In).
446
+ const extras = [...derived, ...computed, ...methods];
447
+ if (extras.length) {
448
+ // Behavior present: `${name}Data` = fields, bare `${name}` = instance.
449
+ lines.push(`${exp}type ${dataName} = ${dataType};`);
450
+ lines.push(`${exp}type ${name} = ${dataName} & { ${extras.join('; ')} };`);
451
+ lines.push(`${exp}${decl}const ${name}: Schema<${name}, ${dataName}>;`);
252
452
  } else {
253
- lines.push(`${exp}${decl}const ${name}: Schema<${dataName}, ${dataName}>;`);
453
+ // No behavior: instance === data, so collapse to a single bare `${name}`
454
+ // (matching :input). No `${name}Data` alias to learn.
455
+ lines.push(`${exp}type ${name} = ${dataType};`);
456
+ lines.push(`${exp}${decl}const ${name}: Schema<${name}, ${name}>;`);
254
457
  }
255
458
  return;
256
459
  }
257
460
 
258
- // :input — parse returns the Data shape directly (no behavior).
259
- const valueName = `${name}Value`;
260
- lines.push(`${exp}type ${valueName} = ${dataType};`);
261
- lines.push(`${exp}${decl}const ${name}: Schema<${valueName}, ${valueName}>;`);
461
+ // :input — fields-only, except `!>` eager-derived own properties (the one
462
+ // behavior :input permits; methods/computed are rejected). When present,
463
+ // they live on the instance (Out) but not the input shape (In), so split
464
+ // `${name}Data` from the bare name like a behavior-bearing :shape.
465
+ if (derived.length) {
466
+ const dataName = `${name}Data`;
467
+ lines.push(`${exp}type ${dataName} = ${dataType};`);
468
+ lines.push(`${exp}type ${name} = ${dataName} & { ${derived.join('; ')} };`);
469
+ lines.push(`${exp}${decl}const ${name}: Schema<${name}, ${dataName}>;`);
470
+ return;
471
+ }
472
+ // No behavior: the bare name IS the parsed value type.
473
+ lines.push(`${exp}type ${name} = ${dataType};`);
474
+ lines.push(`${exp}${decl}const ${name}: Schema<${name}, ${name}>;`);
262
475
  }
263
476
 
264
477
  // Return an array of mixin type-reference strings for `& Foo & Bar` joins.
@@ -277,6 +490,65 @@ function mixinIntersections(descriptor, byName) {
277
490
  return refs;
278
491
  }
279
492
 
493
+ // The TS property strings for a :model's implicitly-managed columns: the
494
+ // `id` PK, `@timestamps`, `@softDelete`, and `@belongs_to` FK columns. These
495
+ // aren't declared fields but are real columns on every row — so they belong
496
+ // in `<Name>Data` (what `toJSON()` returns and `.pick`/`.omit` project over).
497
+ function modelImplicitProps(descriptor) {
498
+ const props = ['id: number'];
499
+ let timestamps = false, softDelete = false;
500
+ for (const e of descriptor.entries) {
501
+ if (e.tag !== 'directive') continue;
502
+ if (e.name === 'timestamps') timestamps = true;
503
+ else if (e.name === 'softDelete') softDelete = true;
504
+ else if (e.name === 'belongs_to') {
505
+ const target = e.args && e.args[0] && e.args[0].target;
506
+ if (target) {
507
+ const optional = e.args[0].optional === true;
508
+ const fk = target[0].toLowerCase() + target.slice(1) + 'Id';
509
+ props.push(`${fk}: number${optional ? ' | null' : ''}`);
510
+ }
511
+ }
512
+ }
513
+ if (timestamps) { props.push('createdAt: Date'); props.push('updatedAt: Date'); }
514
+ if (softDelete) props.push('deletedAt: Date | null');
515
+ return props;
516
+ }
517
+
518
+ // The create-input shape for a :model: what `create(data)` must be given.
519
+ // A declared field is REQUIRED iff it's marked `!` AND has no default
520
+ // (`[value]` bracket) — a required field with a default is effectively
521
+ // optional at insert time. Everything else (optional fields, defaulted
522
+ // fields) is optional. `@belongs_to` adds its FK column: required when the
523
+ // relation is non-null, optional (`| null`) otherwise. `id` and the
524
+ // auto-managed timestamp/softDelete columns are omitted — the DB fills them.
525
+ //
526
+ // `@mixin` fields are folded in by the caller via `& <Mixin>` and keep their
527
+ // declared optionality, so a mixin's `!`-required fields are required at
528
+ // create too (the runtime requires them as well). That's correct for ordinary
529
+ // shared fields; if a mixin models auto-managed columns, prefer the
530
+ // `@timestamps`/`@softDelete` directives (which are omitted here) over a mixin.
531
+ function modelCreateInputType(descriptor) {
532
+ const props = [];
533
+ for (const e of descriptor.entries) {
534
+ if (e.tag !== 'field') continue;
535
+ // `constraintTokens` is the `[default]` bracket; its presence means the
536
+ // field has a default, so it's optional at create even when marked `!`.
537
+ const requiredAtCreate = e.modifiers.includes('!') && !e.constraintTokens;
538
+ const mark = requiredAtCreate ? '' : '?';
539
+ props.push(`${e.name}${mark}: ${mapFieldType(e)}`);
540
+ }
541
+ for (const e of descriptor.entries) {
542
+ if (e.tag !== 'directive' || e.name !== 'belongs_to') continue;
543
+ const target = e.args && e.args[0] && e.args[0].target;
544
+ if (!target) continue;
545
+ const optional = e.args[0].optional === true;
546
+ const fk = target[0].toLowerCase() + target.slice(1) + 'Id';
547
+ props.push(`${fk}${optional ? '?' : ''}: number${optional ? ' | null' : ''}`);
548
+ }
549
+ return `{ ${props.join('; ')} }`;
550
+ }
551
+
280
552
  // Emit relation accessor type declarations for :model instances. For
281
553
  // targets declared in the same file we emit a typed Promise; for
282
554
  // unknown (cross-file) targets we degrade to `Promise<unknown>` rather
@@ -291,18 +563,20 @@ function modelRelationAccessors(descriptor, known) {
291
563
  if (!target) continue;
292
564
  const optional = args[0].optional === true;
293
565
  const targetLc = target[0].toLowerCase() + target.slice(1);
294
- const instName = `${target}Instance`;
566
+ // Class-style: the target's bare name IS its instance type.
567
+ const instName = target;
295
568
  const isKnown = known && known.has(target);
569
+ const optsT = 'opts?: { reload?: boolean }';
296
570
  if (e.name === 'belongs_to') {
297
571
  const retT = isKnown ? (optional ? `${instName} | null` : `${instName} | null`) : 'unknown';
298
- out.push(`${targetLc}(): Promise<${retT}>`);
572
+ out.push(`${targetLc}(${optsT}): Promise<${retT}>`);
299
573
  } else if (e.name === 'has_one' || e.name === 'one') {
300
574
  const retT = isKnown ? `${instName} | null` : 'unknown';
301
- out.push(`${targetLc}(): Promise<${retT}>`);
575
+ out.push(`${targetLc}(${optsT}): Promise<${retT}>`);
302
576
  } else if (e.name === 'has_many' || e.name === 'many') {
303
577
  const retT = isKnown ? `${instName}[]` : 'unknown[]';
304
578
  const pluralLc = __schemaClientPluralize(targetLc);
305
- out.push(`${pluralLc}(): Promise<${retT}>`);
579
+ out.push(`${pluralLc}(${optsT}): Promise<${retT}>`);
306
580
  }
307
581
  }
308
582
  return out;
@@ -30,6 +30,7 @@ import {
30
30
  SCHEMA_DB_NAMING_RUNTIME,
31
31
  SCHEMA_ORM_RUNTIME,
32
32
  SCHEMA_DDL_RUNTIME,
33
+ SCHEMA_MIGRATE_RUNTIME,
33
34
  SCHEMA_BROWSER_STUBS_RUNTIME,
34
35
  } from './runtime.generated.js';
35
36
  import { setSchemaRuntimeProvider } from './schema.js';
@@ -47,7 +48,7 @@ function provider({ mode = 'migration' } = {}) {
47
48
  body = SCHEMA_VALIDATE_RUNTIME + '\n' + SCHEMA_DB_NAMING_RUNTIME + '\n' + SCHEMA_ORM_RUNTIME;
48
49
  break;
49
50
  case 'migration':
50
- body = SCHEMA_VALIDATE_RUNTIME + '\n' + SCHEMA_DB_NAMING_RUNTIME + '\n' + SCHEMA_ORM_RUNTIME + '\n' + SCHEMA_DDL_RUNTIME;
51
+ body = SCHEMA_VALIDATE_RUNTIME + '\n' + SCHEMA_DB_NAMING_RUNTIME + '\n' + SCHEMA_ORM_RUNTIME + '\n' + SCHEMA_DDL_RUNTIME + '\n' + SCHEMA_MIGRATE_RUNTIME;
51
52
  break;
52
53
  default:
53
54
  throw new Error(`unknown schema runtime mode: ${mode}`);
@@ -32,20 +32,31 @@ const __schemaBrowserStub = (api) => function() {
32
32
  };
33
33
 
34
34
  // Static / class-level methods on __SchemaDef
35
- __SchemaDef.prototype.find = __schemaBrowserStub('find');
36
- __SchemaDef.prototype.where = __schemaBrowserStub('where');
37
- __SchemaDef.prototype.all = __schemaBrowserStub('all');
38
- __SchemaDef.prototype.first = __schemaBrowserStub('first');
39
- __SchemaDef.prototype.count = __schemaBrowserStub('count');
40
- __SchemaDef.prototype.create = __schemaBrowserStub('create');
41
- __SchemaDef.prototype.toSQL = __schemaBrowserStub('toSQL');
35
+ __SchemaDef.prototype.find = __schemaBrowserStub('find');
36
+ __SchemaDef.prototype.findMany = __schemaBrowserStub('findMany');
37
+ __SchemaDef.prototype.where = __schemaBrowserStub('where');
38
+ __SchemaDef.prototype.includes = __schemaBrowserStub('includes');
39
+ __SchemaDef.prototype.withDeleted = __schemaBrowserStub('withDeleted');
40
+ __SchemaDef.prototype.onlyDeleted = __schemaBrowserStub('onlyDeleted');
41
+ __SchemaDef.prototype.unscoped = __schemaBrowserStub('unscoped');
42
+ __SchemaDef.prototype.all = __schemaBrowserStub('all');
43
+ __SchemaDef.prototype.first = __schemaBrowserStub('first');
44
+ __SchemaDef.prototype.count = __schemaBrowserStub('count');
45
+ __SchemaDef.prototype.create = __schemaBrowserStub('create');
46
+ __SchemaDef.prototype.upsert = __schemaBrowserStub('upsert');
47
+ __SchemaDef.prototype.insertMany = __schemaBrowserStub('insertMany');
48
+ __SchemaDef.prototype.toSQL = __schemaBrowserStub('toSQL');
42
49
 
43
50
  // Helpers referenced by the validate fragment that are otherwise
44
51
  // defined in db-naming / orm fragments. Kept inert (return safe
45
52
  // defaults or throw on use) so validate's _makeClass / _normalize
46
53
  // can run end-to-end in browser context.
47
- function __schemaSave() { throw new Error("schema instance.save() is not available in the browser. Import @rip-lang/db on the server."); }
48
- function __schemaDestroy() { throw new Error("schema instance.destroy() is not available in the browser. Import @rip-lang/db on the server."); }
54
+ function __schemaSave() { throw new Error("schema instance.save() is not available in the browser. Import @rip-lang/db on the server."); }
55
+ function __schemaDestroy() { throw new Error("schema instance.destroy() is not available in the browser. Import @rip-lang/db on the server."); }
56
+ function __schemaRestore() { throw new Error("schema instance.restore() is not available in the browser. Import @rip-lang/db on the server."); }
57
+ function __schemaResolveRelation() { throw new Error("schema relation accessors are not available in the browser. Import @rip-lang/db on the server."); }
58
+ function __schemaTransaction() { throw new Error("schema.transaction() is not available in the browser. Import @rip-lang/db on the server."); }
59
+ function __schemaInvokeScope() { throw new Error("schema query scopes are not available in the browser. Import @rip-lang/db on the server."); }
49
60
  function __schemaTableName(m) { return null; } // returned only for :model normalize; never used downstream in browser
50
61
  function __schemaPluralize(w) { return w; } // identity — relations work for type-resolution but never query
51
62
  function __schemaFkName(m) { return ''; } // ditto