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.
- package/README.md +2 -3
- package/bin/rip +39 -8
- package/bin/rip-schema +175 -0
- package/docs/RIP-APP.md +91 -2
- package/docs/RIP-DUCKDB.md +64 -1
- package/docs/RIP-INTRO.md +4 -4
- package/docs/RIP-LANG.md +32 -33
- package/docs/RIP-SCHEMA.md +1204 -364
- package/docs/dist/rip.js +3245 -611
- package/docs/dist/rip.min.js +1161 -289
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/index.html +2 -1
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/vscode-rip-0.6.0.vsix +0 -0
- package/docs/index.html +1 -1
- package/docs/ui/hljs-rip.js +1 -1
- package/package.json +7 -4
- package/src/AGENTS.md +39 -8
- package/src/compiler.js +220 -36
- package/src/components.js +315 -14
- package/src/dts.js +18 -3
- package/src/grammar/README.md +29 -170
- package/src/grammar/grammar.rip +17 -12
- package/src/grammar/solar.rip +4 -17
- package/src/lexer.js +24 -17
- package/src/parser.js +229 -229
- package/src/schema/dts.js +328 -54
- package/src/schema/loader-server.js +2 -1
- package/src/schema/runtime-browser-stubs.js +20 -9
- package/src/schema/runtime-ddl.js +161 -44
- package/src/schema/runtime-migrate.js +681 -0
- package/src/schema/runtime-orm.js +698 -54
- package/src/schema/runtime-validate.js +808 -24
- package/src/schema/runtime.generated.js +2395 -135
- package/src/schema/schema.js +1049 -89
- package/src/typecheck.js +283 -55
- package/src/types.js +5 -1
- 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:
|
|
58
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
72
|
-
'
|
|
73
|
-
'
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'
|
|
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<
|
|
85
|
-
' create(data:
|
|
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
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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}:
|
|
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
|
-
|
|
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} = ${
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
lines.push(`${exp}
|
|
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
|
-
|
|
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 —
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
|
36
|
-
__SchemaDef.prototype.
|
|
37
|
-
__SchemaDef.prototype.
|
|
38
|
-
__SchemaDef.prototype.
|
|
39
|
-
__SchemaDef.prototype.
|
|
40
|
-
__SchemaDef.prototype.
|
|
41
|
-
__SchemaDef.prototype.
|
|
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()
|
|
48
|
-
function __schemaDestroy()
|
|
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
|