rip-lang 3.14.5 → 3.15.1

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 (43) hide show
  1. package/README.md +9 -5
  2. package/bin/rip +5 -0
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-LANG.md +17 -5
  5. package/docs/RIP-SCHEMA.md +4 -4
  6. package/docs/demo/README.md +43 -0
  7. package/docs/demo/components/_layout.rip +28 -0
  8. package/docs/demo/components/about.rip +36 -0
  9. package/docs/demo/components/card.rip +10 -0
  10. package/docs/demo/components/counter.rip +33 -0
  11. package/docs/demo/components/index.rip +30 -0
  12. package/docs/demo/components/todos.rip +48 -0
  13. package/docs/demo/css/styles.css +472 -0
  14. package/docs/dist/rip.js +3211 -4619
  15. package/docs/dist/rip.min.js +270 -683
  16. package/docs/dist/rip.min.js.br +0 -0
  17. package/docs/example/index.json +6 -6
  18. package/docs/extensions/duckdb/index.html +7 -5
  19. package/docs/extensions/duckdb/manifest.json +1 -1
  20. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  21. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  22. package/package.json +8 -3
  23. package/src/AGENTS.md +107 -9
  24. package/src/{ui.rip → app.rip} +24 -2
  25. package/src/browser.js +154 -37
  26. package/src/compiler.js +87 -9
  27. package/src/grammar/grammar.rip +1 -1
  28. package/src/grammar/solar.rip +0 -1
  29. package/src/lexer.js +25 -3
  30. package/src/parser.js +4 -4
  31. package/src/schema/dts-emit.js +329 -0
  32. package/src/schema/loader-browser.js +55 -0
  33. package/src/schema/loader-server.js +65 -0
  34. package/src/schema/runtime-browser-stubs.js +51 -0
  35. package/src/schema/runtime-db-naming.js +34 -0
  36. package/src/schema/runtime-ddl.js +124 -0
  37. package/src/schema/runtime-orm.js +294 -0
  38. package/src/schema/runtime-validate.js +816 -0
  39. package/src/schema/runtime.generated.js +1315 -0
  40. package/src/{schema.js → schema/schema.js} +43 -1627
  41. package/src/typecheck.js +3 -2
  42. package/src/types-emit.js +1021 -0
  43. package/src/types.js +11 -1035
@@ -1,4 +1,17 @@
1
- import { parser } from './parser.js';
1
+ // Schema reaches sideways to the host's parser table to re-parse @ensure
2
+ // predicate bodies. This is the one host coupling point — the host's lexer
3
+ // and compiler import `installSchemaSupport` from us, and we import the
4
+ // parser back from them. Same compilation unit, no package boundary.
5
+ import { parser } from '../parser.js';
6
+
7
+ // Runtime-string composition is delegated to a registered provider so the
8
+ // bundler can tree-shake server-only fragments out of the browser bundle.
9
+ // One of `./loader-server.js` or `./loader-browser.js` must be
10
+ // side-effect-imported before any compileToJS call that emits schemas.
11
+ // (`src/browser.js` imports loader-browser; CLI / typecheck / test runner
12
+ // import loader-server.)
13
+ let _schemaRuntimeProvider = null;
14
+ export function setSchemaRuntimeProvider(fn) { _schemaRuntimeProvider = fn; }
2
15
 
3
16
  // Schema System — inline `schema` declarations compile to runtime validator
4
17
  // and ORM plans.
@@ -100,7 +113,12 @@ export function installSchemaSupport(Lexer, CodeEmitter) {
100
113
  return emitSchemaNode(this, head, rest, context);
101
114
  };
102
115
  CodeEmitter.prototype.getSchemaRuntime = function() {
103
- return getSchemaRuntime();
116
+ // Compiler-controlled mode. Defaults to 'migration' (everything) for
117
+ // compatibility with existing CLI / Node compilation, where the user
118
+ // might invoke any schema feature including .toSQL(). Browser-bundle
119
+ // build overrides to 'browser' for size reduction — see Phase 2 step 3.
120
+ const mode = this.options?.schemaMode || 'migration';
121
+ return getSchemaRuntime({ mode });
104
122
  };
105
123
  }
106
124
  }
@@ -1759,1631 +1777,29 @@ function schemaError(tok, message) {
1759
1777
  // :mixin — non-instantiable; raises `Cannot parse :mixin`
1760
1778
  // :model — Phase 4 (the class additionally wires ORM methods)
1761
1779
 
1762
- // Schema runtime ABI version. Bump when the shape of a __schema({...})
1763
- // descriptor or any cross-bundle-visible runtime surface changes
1764
- // incompatibly. Two bundles that disagree on this number can't share
1765
- // one runtime, so a mismatch at load time throws rather than silently
1766
- // fragmenting. Tracks runtime contract — not the rip-lang product
1767
- // semver.
1768
- const SCHEMA_RUNTIME_ABI_VERSION = 1;
1769
-
1770
- const SCHEMA_RUNTIME = `
1771
- // ---- Rip Schema Runtime ----------------------------------------------------
1772
- // Four layers, lazy compilation:
1773
- // 1 (descriptor) object passed to __schema({...}). Raw metadata.
1774
- // 2 (normalized) fields/methods/computed/hooks/relations/constraints.
1775
- // Collision checks. Table name derivation. Built once.
1776
- // 3 (validator) compiled validator plan. Built on first .parse.
1777
- // 4a (ORM plan) built on first .find/.create/.save.
1778
- // 4b (DDL plan) built on first .toSQL(). Independent of 4a.
1780
+ // =============================================================================
1781
+ // Runtime composition (delegated to registered provider)
1782
+ // =============================================================================
1783
+ // Mode matrix:
1779
1784
  //
1780
- // Instance-singleton model:
1781
- // The runtime installs itself on globalThis.__ripSchema the first time a
1782
- // compiled bundle executes. Subsequent bundles that inject the same runtime
1783
- // template detect the existing installation and bind to it instead of
1784
- // re-running the body — giving every bundle a single shared registry,
1785
- // adapter, and class identity. The IIFE wrapper below enforces that.
1786
-
1787
- var { __schema, SchemaError, __SchemaRegistry, __schemaSetAdapter } = (function() {
1788
- if (typeof globalThis !== 'undefined' && globalThis.__ripSchema) {
1789
- if (globalThis.__ripSchema.__version !== ${SCHEMA_RUNTIME_ABI_VERSION}) {
1790
- throw new Error(
1791
- "rip-schema runtime version mismatch: loaded runtime is v" +
1792
- globalThis.__ripSchema.__version +
1793
- ", but this bundle expects v" + ${SCHEMA_RUNTIME_ABI_VERSION} +
1794
- ". Two compiled Rip bundles with incompatible schema runtimes are loaded in the same process."
1795
- );
1796
- }
1797
- return globalThis.__ripSchema;
1798
- }
1799
-
1800
- class SchemaError extends Error {
1801
- constructor(issues, schemaName, schemaKind) {
1802
- super(__schemaFormatIssues(issues, schemaName));
1803
- this.name = 'SchemaError';
1804
- this.issues = issues;
1805
- this.schemaName = schemaName || null;
1806
- this.schemaKind = schemaKind || null;
1807
- }
1808
- }
1809
-
1810
- function __schemaFormatIssues(issues, name) {
1811
- if (!issues || !issues.length) return 'SchemaError';
1812
- const head = name ? name + ': ' : '';
1813
- return head + issues.map(i => i.message || i.error || 'invalid').join('; ');
1814
- }
1815
-
1816
- // Reserved names are hoisted to module scope — they're pure data and
1817
- // rebuilding them per _normalize() call wastes allocations. Static: names
1818
- // that become class-level methods on :model (parse, find, toSQL, …).
1819
- // Instance: names that become instance methods (save, destroy, toJSON, …).
1820
- // A declared field, method, computed, or derived that collides with
1821
- // either set on a :model raises a collision error during normalize.
1822
- const __SCHEMA_RESERVED_STATIC = new Set([
1823
- 'parse','safe','ok','find','findMany','where','all','first','count','create','toSQL',
1824
- ]);
1825
- const __SCHEMA_RESERVED_INSTANCE = new Set([
1826
- 'save','destroy','reload','ok','errors','toJSON',
1827
- ]);
1828
- const __SCHEMA_RESERVED = new Set([...__SCHEMA_RESERVED_STATIC, ...__SCHEMA_RESERVED_INSTANCE]);
1829
-
1830
- const __schemaTypes = {
1831
- string: v => typeof v === 'string',
1832
- number: v => typeof v === 'number' && !Number.isNaN(v),
1833
- integer: v => Number.isInteger(v),
1834
- boolean: v => typeof v === 'boolean',
1835
- date: v => v instanceof Date && !Number.isNaN(v.getTime()),
1836
- datetime: v => v instanceof Date && !Number.isNaN(v.getTime()),
1837
- email: v => typeof v === 'string' && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v),
1838
- url: v => typeof v === 'string' && /^https?:\\/\\/.+/.test(v),
1839
- uuid: v => typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v),
1840
- phone: v => typeof v === 'string' && /^[\\d\\s\\-+()]+$/.test(v),
1841
- zip: v => typeof v === 'string' && /^\\d{5}(-\\d{4})?$/.test(v),
1842
- text: v => typeof v === 'string',
1843
- json: v => v !== undefined,
1844
- any: () => true,
1845
- };
1846
-
1847
- function __schemaCheckValue(v, typeName) {
1848
- const check = __schemaTypes[typeName];
1849
- return check ? check(v) : true;
1850
- }
1851
-
1852
- // Validate a single value against a typeName, returning either null (ok)
1853
- // or an array of issues relative to the value's own root. Primitive
1854
- // typenames dispatch through the __schemaTypes map; typenames that
1855
- // resolve to a registered :shape / :input / :model validate the value
1856
- // as a nested object; typenames that resolve to a :enum enforce
1857
- // membership. Unknown typenames stay permissive so forward-references
1858
- // and cross-module names do not hard-fail — matches pre-registry behavior.
1859
- function __schemaValidateValue(v, typeName) {
1860
- const prim = __schemaTypes[typeName];
1861
- if (prim) {
1862
- return prim(v) ? null : [{field: '', error: 'type', message: 'must be ' + typeName}];
1863
- }
1864
- const subDef = __SchemaRegistry.get(typeName);
1865
- if (!subDef) return null;
1866
- if (subDef.kind === 'enum') {
1867
- const errs = subDef._validateEnum(v, true);
1868
- return errs.length ? [{field: '', error: 'enum', message: errs[0].message}] : null;
1869
- }
1870
- if (subDef.kind === 'mixin') {
1871
- return [{field: '', error: 'type', message: ':mixin ' + typeName + ' is not usable as a field type'}];
1872
- }
1873
- if (v === null || typeof v !== 'object' || Array.isArray(v)) {
1874
- return [{field: '', error: 'type', message: 'must be a ' + typeName + ' object'}];
1875
- }
1876
- const subErrs = subDef._validateFields(v, true);
1877
- return subErrs.length ? subErrs : null;
1878
- }
1879
-
1880
- // Merge a child path segment into an existing field path. Produces
1881
- // 'addr.street' for object descent, 'items[0].name' for array descent.
1882
- function __schemaJoinField(head, child) {
1883
- if (!child) return head;
1884
- return head + (child.startsWith('[') ? child : '.' + child);
1885
- }
1886
-
1887
- // Rewrite a child issue's message so the leading "<childField> " token
1888
- // (present on most leaf messages: "name is required", "id must be
1889
- // integer") is replaced by the joined parent path — avoiding the
1890
- // duplicated "items[1].id id must be integer" reading.
1891
- function __schemaRewriteMessage(joinedField, childField, childMessage) {
1892
- if (!childField) return joinedField + ' ' + childMessage;
1893
- if (childMessage.startsWith(childField)) {
1894
- return joinedField + childMessage.slice(childField.length);
1895
- }
1896
- return joinedField + ': ' + childMessage;
1897
- }
1898
-
1899
- // Naming utilities (snake_case column/table names, irregular plurals).
1900
- function __schemaSnake(s) { return s.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase(); }
1901
- const __SCHEMA_UNCOUNTABLE = new Set(['equipment','information','rice','money','species','series','fish','sheep','data']);
1902
- const __SCHEMA_IRREGULAR = new Map([['person','people'],['man','men'],['woman','women'],['child','children'],['tooth','teeth'],['foot','feet'],['mouse','mice']]);
1903
- function __schemaPluralize(w) {
1904
- const lw = w.toLowerCase();
1905
- if (__SCHEMA_UNCOUNTABLE.has(lw)) return w;
1906
- if (__SCHEMA_IRREGULAR.has(lw)) return __SCHEMA_IRREGULAR.get(lw);
1907
- // Preserve case of the input — pluralizer operates on the trailing form
1908
- // but keeps the rest unchanged, so orderItem becomes orderItems
1909
- // and User becomes Users.
1910
- if (/[^aeiouy]y$/i.test(w)) return w.slice(0, -1) + 'ies';
1911
- if (/(s|x|z|ch|sh)$/i.test(w)) return w + 'es';
1912
- return w + 's';
1913
- }
1914
- function __schemaTableName(model) { return __schemaPluralize(__schemaSnake(model)); }
1915
- function __schemaFkName(model) { return __schemaSnake(model) + '_id'; }
1916
-
1917
- // ---- Registry ---------------------------------------------------------------
1918
- // Process-global, resettable, with placeholder state for forward/circular
1919
- // references. Duplicate registration of the same model name is a hard error.
1920
-
1921
- const __SchemaRegistry = {
1922
- _entries: new Map(),
1923
- register(def) {
1924
- // Named schemas of any kind land here. Relations look up :model,
1925
- // @mixin Name looks up :mixin. Algebra (.extend etc.) accepts :shape
1926
- // and derived shapes. Kind is checked at lookup time.
1927
- if (!def.name) return;
1928
- // Most recent registration wins. Recompilation produces a fresh
1929
- // __SchemaDef with the same name; the registry rebinds. Cross-
1930
- // module name collisions should be avoided — schema names are
1931
- // app-global identifiers for relation resolution.
1932
- this._entries.set(def.name, { def, kind: def.kind });
1933
- },
1934
- get(name) {
1935
- const entry = this._entries.get(name);
1936
- return entry ? entry.def : null;
1937
- },
1938
- getKind(name, kind) {
1939
- const entry = this._entries.get(name);
1940
- return entry && entry.kind === kind ? entry.def : null;
1941
- },
1942
- has(name) { return this._entries.has(name); },
1943
- reset() { this._entries.clear(); },
1944
- };
1945
-
1946
- // ---- DB adapter seam --------------------------------------------------------
1947
- // Default adapter uses fetch to rip-db /sql. Tests can swap with
1948
- // __schemaSetAdapter(...) before running queries.
1949
-
1950
- function __schemaDefaultAdapter() {
1951
- const url = (typeof process !== 'undefined' && process.env?.DB_URL) || 'http://localhost:4213';
1952
- return {
1953
- async query(sql, params) {
1954
- const body = params && params.length ? { sql, params } : { sql };
1955
- const res = await fetch(url + '/sql', {
1956
- method: 'POST',
1957
- headers: { 'Content-Type': 'application/json' },
1958
- body: JSON.stringify(body),
1959
- });
1960
- const data = await res.json();
1961
- if (data.error) throw new Error(data.error);
1962
- return data;
1963
- }
1964
- };
1965
- }
1966
-
1967
- let __schemaAdapter = __schemaDefaultAdapter();
1968
- function __schemaSetAdapter(a) { __schemaAdapter = a; }
1969
-
1970
- // ---- Query builder ----------------------------------------------------------
1971
-
1972
- class __SchemaQuery {
1973
- constructor(def, opts = {}) {
1974
- this._def = def;
1975
- this._clauses = [];
1976
- this._params = [];
1977
- this._limit = null;
1978
- this._offset = null;
1979
- this._order = null;
1980
- this._includeDeleted = opts.includeDeleted === true;
1981
- }
1982
- where(cond, ...params) {
1983
- if (typeof cond === 'string') {
1984
- this._clauses.push(cond);
1985
- this._params.push(...params);
1986
- } else if (cond && typeof cond === 'object') {
1987
- for (const [k, v] of Object.entries(cond)) {
1988
- const col = __schemaSnake(k);
1989
- if (v === null || v === undefined) {
1990
- this._clauses.push('"' + col + '" IS NULL');
1991
- } else {
1992
- this._clauses.push('"' + col + '" = ?');
1993
- this._params.push(v);
1994
- }
1995
- }
1996
- }
1997
- return this;
1998
- }
1999
- limit(n) { this._limit = n; return this; }
2000
- offset(n) { this._offset = n; return this; }
2001
- order(spec) { this._order = spec; return this; }
2002
- orderBy(spec) { return this.order(spec); }
2003
- _buildSQL() {
2004
- const n = this._def._normalize();
2005
- const table = n.tableName;
2006
- const parts = ['SELECT * FROM "' + table + '"'];
2007
- const where = [...this._clauses];
2008
- if (!this._includeDeleted && n.softDelete) where.push('"deleted_at" IS NULL');
2009
- if (where.length) parts.push('WHERE ' + where.join(' AND '));
2010
- if (this._order) parts.push('ORDER BY ' + this._order);
2011
- if (this._limit != null) parts.push('LIMIT ' + this._limit);
2012
- if (this._offset != null) parts.push('OFFSET ' + this._offset);
2013
- return parts.join(' ');
2014
- }
2015
- async all() {
2016
- const sql = this._buildSQL();
2017
- const res = await __schemaAdapter.query(sql, this._params);
2018
- return (res.data || []).map(row => this._def._hydrate(res.columns, row));
2019
- }
2020
- async first() {
2021
- this._limit = 1;
2022
- const arr = await this.all();
2023
- return arr[0] || null;
2024
- }
2025
- async count() {
2026
- const n = this._def._normalize();
2027
- const parts = ['SELECT COUNT(*) FROM "' + n.tableName + '"'];
2028
- const where = [...this._clauses];
2029
- if (!this._includeDeleted && n.softDelete) where.push('"deleted_at" IS NULL');
2030
- if (where.length) parts.push('WHERE ' + where.join(' AND '));
2031
- const res = await __schemaAdapter.query(parts.join(' '), this._params);
2032
- return res.data?.[0]?.[0] || 0;
2033
- }
2034
- }
2035
-
2036
- // ---- __SchemaDef ------------------------------------------------------------
2037
-
2038
- class __SchemaDef {
2039
- constructor(desc) {
2040
- this._desc = desc;
2041
- this.kind = desc.kind;
2042
- this.name = desc.name || null;
2043
- this._norm = null;
2044
- this._klass = null;
2045
- this._sourceModel = null;
2046
- }
2047
-
2048
- _normalize() {
2049
- if (this._norm) return this._norm;
2050
-
2051
- const fields = new Map();
2052
- const methods = new Map();
2053
- const computed = new Map();
2054
- const derived = new Map();
2055
- const hooks = new Map();
2056
- const directives = [];
2057
- const enumMembers = new Map();
2058
- const relations = new Map();
2059
- const ensures = [];
2060
- let timestamps = false;
2061
- let softDelete = false;
2062
-
2063
- const collision = (n, where) => {
2064
- throw new SchemaError(
2065
- [{field: n, error: 'collision', message: n + ' collides with ' + where}],
2066
- this.name, this.kind);
2067
- };
2068
- const noteCollision = (n) => {
2069
- if (fields.has(n)) collision(n, 'field');
2070
- if (methods.has(n)) collision(n, 'method');
2071
- if (computed.has(n)) collision(n, 'computed');
2072
- if (hooks.has(n)) collision(n, 'hook');
2073
- if (relations.has(n)) collision(n, 'relation');
2074
- if (this.kind === 'model' && __SCHEMA_RESERVED.has(n)) collision(n, 'reserved ORM name');
2075
- };
2076
-
2077
- for (const e of this._desc.entries) {
2078
- switch (e.tag) {
2079
- case 'field':
2080
- noteCollision(e.name);
2081
- fields.set(e.name, {
2082
- name: e.name,
2083
- required: e.modifiers.includes('!'),
2084
- unique: e.modifiers.includes('#'),
2085
- optional: e.modifiers.includes('?'),
2086
- typeName: e.typeName,
2087
- literals: e.literals || null,
2088
- array: e.array === true,
2089
- constraints: e.constraints || null,
2090
- transform: e.transform || null,
2091
- });
2092
- break;
2093
- case 'method':
2094
- noteCollision(e.name);
2095
- methods.set(e.name, e.fn);
2096
- break;
2097
- case 'computed':
2098
- noteCollision(e.name);
2099
- computed.set(e.name, e.fn);
2100
- break;
2101
- case 'derived':
2102
- noteCollision(e.name);
2103
- derived.set(e.name, e.fn);
2104
- break;
2105
- case 'hook':
2106
- if (hooks.has(e.name)) collision(e.name, 'duplicate hook');
2107
- hooks.set(e.name, e.fn);
2108
- break;
2109
- case 'directive': {
2110
- directives.push({ name: e.name, args: e.args || [] });
2111
- // @mixin is recorded but further handling is deferred to the
2112
- // post-pass so we can dedupe diamond includes and detect
2113
- // cycles with a full expansion stack. All other directives
2114
- // get their relation / timestamps / softDelete processing now.
2115
- if (e.name === 'mixin') break;
2116
- if (e.name === 'timestamps') timestamps = true;
2117
- if (e.name === 'softDelete') softDelete = true;
2118
- const rel = __schemaNormalizeDirectiveRelation(e, this.name);
2119
- if (rel) {
2120
- noteCollision(rel.accessor);
2121
- relations.set(rel.accessor, rel);
2122
- }
2123
- break;
2124
- }
2125
- case 'enum-member':
2126
- enumMembers.set(e.name, e.value !== undefined ? e.value : e.name);
2127
- break;
2128
- case 'ensure':
2129
- // @ensure entries are schema-level invariants (cross-field
2130
- // predicates). Declaration order is preserved so diagnostics
2131
- // come out in the order authored.
2132
- ensures.push({ message: e.message, fn: e.fn });
2133
- break;
2134
- }
2135
- }
2136
-
2137
- // @mixin expansion (Phase 5). Depth-first, dedupes diamond includes
2138
- // in the same host expansion, detects cycles with full chain.
2139
- if (this.kind === 'model' || this.kind === 'shape' || this.kind === 'input' ||
2140
- this.kind === 'mixin') {
2141
- __schemaExpandMixins(this, fields, directives, {
2142
- stack: [this.name || '<anon>'],
2143
- seen: new Set([this.name || '<anon>']),
2144
- onCollision: (name, src) => collision(name, 'mixin-included field from ' + src),
2145
- });
2146
- }
2147
-
2148
- // Add implicit primary key for :model unless a field already marked primary.
2149
- const primaryKey = 'id';
2150
- const tableName = this.kind === 'model' ? __schemaTableName(this.name) : null;
2151
-
2152
- this._norm = {
2153
- fields, methods, computed, derived, hooks, directives, enumMembers, relations,
2154
- ensures,
2155
- timestamps, softDelete, primaryKey, tableName,
2156
- };
2157
- return this._norm;
2158
- }
2159
-
2160
- // Run eager-derived entries (!>) — one pass, in declaration order.
2161
- //
2162
- // Invariants worth keeping in mind here:
2163
- // - Fires at parse/safe time AND at DB hydrate time (declared fields
2164
- // are populated by then in both paths).
2165
- // - NOT re-run on field mutation — the value is materialized once at
2166
- // instance creation and stays. Use ~> for live recomputation.
2167
- // - Stored as own enumerable properties, so they round-trip through
2168
- // Object.keys and JSON.stringify. Excluded from DB persistence by
2169
- // _getSaveableData (writes declared fields only).
2170
- // - Thrown errors propagate. parse() wraps them into SchemaError
2171
- // before surfacing; safe() captures into {error: 'derived'}
2172
- // issues; hydrate lets them crash fast as data-integrity signals.
2173
- _applyEagerDerived(inst) {
2174
- const norm = this._normalize();
2175
- if (!norm.derived.size) return;
2176
- for (const [n, fn] of norm.derived) {
2177
- const v = fn.call(inst);
2178
- Object.defineProperty(inst, n, {
2179
- value: v, enumerable: true, writable: true, configurable: true,
2180
- });
2181
- }
2182
- }
2183
-
2184
- // Run '@ensure' predicates — schema-level cross-field invariants —
2185
- // against a fully-typed, fully-defaulted data object. Returns [] if
2186
- // all pass, or an array of {field: '', error: 'ensure', message}
2187
- // issues for every failing predicate.
2188
- //
2189
- // Naming: '_applyEnsures' mirrors '_applyTransforms' and
2190
- // '_applyEagerDerived' — runtime method name matches the directive
2191
- // it services. The industry term for this pattern is 'refinement'
2192
- // (Zod's '.refine', design-by-contract postconditions); in Rip the
2193
- // user-visible name is '@ensure' and the code tracks that.
2194
- //
2195
- // Semantics:
2196
- // - Truthy return → pass; falsy → fail with the declared message.
2197
- // - Thrown exception → fail with the declared message (the thrown
2198
- // error's own message is used only if the @ensure declared no
2199
- // message, which can't happen via the parser since message is
2200
- // required — but downstream code-built defs might omit it).
2201
- // - All @ensures run; declaration order preserved in output.
2202
- // - Caller short-circuits: per-field validation errors skip this
2203
- // step entirely (predicates assume field types are correct).
2204
- // - Skipped on _hydrate — trusted DB data bypasses @ensures.
2205
- _applyEnsures(data) {
2206
- const norm = this._normalize();
2207
- if (!norm.ensures.length) return [];
2208
- const errs = [];
2209
- for (const r of norm.ensures) {
2210
- let ok = false;
2211
- try {
2212
- ok = !!r.fn(data);
2213
- } catch (e) {
2214
- errs.push({
2215
- field: '', error: 'ensure',
2216
- message: r.message || e?.message || 'ensure failed',
2217
- });
2218
- continue;
2219
- }
2220
- if (!ok) {
2221
- errs.push({
2222
- field: '', error: 'ensure',
2223
- message: r.message || 'ensure failed',
2224
- });
2225
- }
2226
- }
2227
- return errs;
2228
- }
2229
-
2230
- _getClass() {
2231
- if (this._klass) return this._klass;
2232
- const norm = this._normalize();
2233
- const name = this.name || 'Schema';
2234
- const def = this;
2235
-
2236
- const fieldNames = [...norm.fields.keys()];
2237
- const klass = ({[name]: class {
2238
- constructor(data, persisted = false) {
2239
- // Internal state is non-enumerable so Object.keys(inst) lists
2240
- // only declared fields that received a value.
2241
- Object.defineProperty(this, '_dirty', { value: new Set(), enumerable: false, writable: false, configurable: true });
2242
- Object.defineProperty(this, '_persisted', { value: persisted === true, enumerable: false, writable: true, configurable: true });
2243
- Object.defineProperty(this, '_snapshot', { value: null, enumerable: false, writable: true, configurable: true });
2244
- if (data && typeof data === 'object') {
2245
- for (const k of fieldNames) {
2246
- if (k in data && data[k] !== undefined) this[k] = data[k];
2247
- }
2248
- }
2249
- }
2250
- }})[name];
2251
-
2252
- for (const [n, fn] of norm.methods) {
2253
- Object.defineProperty(klass.prototype, n, {
2254
- value: fn, writable: true, enumerable: false, configurable: true,
2255
- });
2256
- }
2257
- for (const [n, fn] of norm.computed) {
2258
- Object.defineProperty(klass.prototype, n, {
2259
- get: fn, enumerable: false, configurable: true,
2260
- });
2261
- }
2262
-
2263
- // Relation methods: user.organization(). Accepts no args; returns
2264
- // a promise to a target-model instance (or array for has_many).
2265
- for (const [acc, rel] of norm.relations) {
2266
- Object.defineProperty(klass.prototype, acc, {
2267
- enumerable: false, configurable: true,
2268
- value: async function() { return __schemaResolveRelation(def, this, rel); },
2269
- });
2270
- }
2271
-
2272
- // Instance ORM methods — only for :model kind.
2273
- if (this.kind === 'model') {
2274
- Object.defineProperty(klass.prototype, 'save', {
2275
- enumerable: false, configurable: true, writable: true,
2276
- value: async function() { return __schemaSave(def, this); },
2277
- });
2278
- Object.defineProperty(klass.prototype, 'destroy', {
2279
- enumerable: false, configurable: true, writable: true,
2280
- value: async function() { return __schemaDestroy(def, this); },
2281
- });
2282
- Object.defineProperty(klass.prototype, 'ok', {
2283
- enumerable: false, configurable: true, writable: true,
2284
- value: function() { return def._validateFields(this, false); },
2285
- });
2286
- Object.defineProperty(klass.prototype, 'errors', {
2287
- enumerable: false, configurable: true, writable: true,
2288
- value: function() { return def._validateFields(this, true); },
2289
- });
2290
- // toJSON mirrors the instance's own enumerable properties, which by
2291
- // construction are: the primary key, declared fields, @timestamps
2292
- // columns, @softDelete timestamp, @belongs_to FK columns, and any
2293
- // !> eager-derived fields. Internal state (_dirty, _persisted,
2294
- // _snapshot) is defined non-enumerable; methods and ~> computed
2295
- // getters live on the prototype. So iterating own keys picks up
2296
- // exactly the user-facing wire shape without special-casing each
2297
- // category — and stays correct when new implicit columns get added
2298
- // to the runtime.
2299
- Object.defineProperty(klass.prototype, 'toJSON', {
2300
- enumerable: false, configurable: true, writable: true,
2301
- value: function() {
2302
- const out = {};
2303
- for (const k of Object.keys(this)) out[k] = this[k];
2304
- return out;
2305
- },
2306
- });
2307
- }
2308
-
2309
- this._klass = klass;
2310
- return klass;
2311
- }
2312
-
2313
- _hydrate(columns, row) {
2314
- // DB rows are trusted: hydrate into a class instance without
2315
- // revalidating. Column names arrive snake_case; declared fields live
2316
- // under their camelCase names, and implicit columns (id, created_at,
2317
- // updated_at, relation FKs) surface under their camelCase equivalents.
2318
- // Each snake_case column name also aliases the camelCase property via
2319
- // a non-enumerable accessor so order.user_id and order.userId read
2320
- // the same slot — useful when DB column names leak into user code
2321
- // via raw SQL helpers.
2322
- const data = {};
2323
- for (let i = 0; i < columns.length; i++) {
2324
- data[__schemaCamel(columns[i].name)] = row[i];
2325
- }
2326
- const k = this._getClass();
2327
- const inst = new k(data, true);
2328
- for (const key of Object.keys(data)) {
2329
- if (!(key in inst)) {
2330
- Object.defineProperty(inst, key, {
2331
- value: data[key], enumerable: true, writable: true, configurable: true,
2332
- });
2333
- }
2334
- }
2335
- for (let i = 0; i < columns.length; i++) {
2336
- const snake = columns[i].name;
2337
- const camel = __schemaCamel(snake);
2338
- if (snake !== camel && !(snake in inst)) {
2339
- Object.defineProperty(inst, snake, {
2340
- enumerable: false, configurable: true,
2341
- get() { return this[camel]; },
2342
- set(v) { this[camel] = v; },
2343
- });
2344
- }
2345
- }
2346
- // Eager-derived fields re-run on hydrate — they're not persisted
2347
- // and must be re-computed from the declared fields now present.
2348
- this._applyEagerDerived(inst);
2349
- return inst;
2350
- }
2351
-
2352
- _validateFields(data, collect) {
2353
- const norm = this._normalize();
2354
- const errors = collect ? [] : null;
2355
- for (const [n, f] of norm.fields) {
2356
- const v = data == null ? undefined : data[n];
2357
- if (v === undefined || v === null) {
2358
- if (f.required) {
2359
- if (!collect) return false;
2360
- errors.push({field: n, error: 'required', message: n + ' is required'});
2361
- }
2362
- continue;
2363
- }
2364
- if (f.array) {
2365
- if (!Array.isArray(v)) {
2366
- if (!collect) return false;
2367
- errors.push({field: n, error: 'type', message: n + ' must be an array'});
2368
- continue;
2369
- }
2370
- let bad = false;
2371
- for (let i = 0; i < v.length; i++) {
2372
- const issues = __schemaValidateValue(v[i], f.typeName);
2373
- if (issues) {
2374
- if (!collect) return false;
2375
- const head = n + '[' + i + ']';
2376
- for (const e of issues) {
2377
- const joined = __schemaJoinField(head, e.field);
2378
- errors.push({
2379
- field: joined,
2380
- error: e.error,
2381
- message: __schemaRewriteMessage(joined, e.field, e.message),
2382
- });
2383
- }
2384
- bad = true;
2385
- }
2386
- }
2387
- if (bad) continue;
2388
- } else if (f.typeName === 'literal-union') {
2389
- if (!f.literals.includes(v)) {
2390
- if (!collect) return false;
2391
- errors.push({field: n, error: 'enum', message: n + ' must be one of ' + f.literals.map(l => JSON.stringify(l)).join(', ')});
2392
- continue;
2393
- }
2394
- } else {
2395
- const issues = __schemaValidateValue(v, f.typeName);
2396
- if (issues) {
2397
- if (!collect) return false;
2398
- for (const e of issues) {
2399
- const joined = __schemaJoinField(n, e.field);
2400
- errors.push({
2401
- field: joined,
2402
- error: e.error,
2403
- message: __schemaRewriteMessage(joined, e.field, e.message),
2404
- });
2405
- }
2406
- continue;
2407
- }
2408
- }
2409
- // Apply constraint checks.
2410
- const c = f.constraints;
2411
- if (c) {
2412
- if (typeof v === 'string') {
2413
- if (c.min != null && v.length < c.min) { if (!collect) return false; errors.push({field: n, error: 'min', message: n + ' must be at least ' + c.min + ' chars'}); }
2414
- if (c.max != null && v.length > c.max) { if (!collect) return false; errors.push({field: n, error: 'max', message: n + ' must be at most ' + c.max + ' chars'}); }
2415
- if (c.regex && !c.regex.test(v)) { if (!collect) return false; errors.push({field: n, error: 'pattern', message: n + ' is invalid'}); }
2416
- } else if (typeof v === 'number') {
2417
- if (c.min != null && v < c.min) { if (!collect) return false; errors.push({field: n, error: 'min', message: n + ' must be >= ' + c.min}); }
2418
- if (c.max != null && v > c.max) { if (!collect) return false; errors.push({field: n, error: 'max', message: n + ' must be <= ' + c.max}); }
2419
- }
2420
- }
2421
- }
2422
- return collect ? errors : true;
2423
- }
2424
-
2425
- _applyDefaults(data) {
2426
- const norm = this._normalize();
2427
- for (const [n, f] of norm.fields) {
2428
- if ((data[n] === undefined || data[n] === null) && f.constraints?.default !== undefined) {
2429
- const d = f.constraints.default;
2430
- data[n] = (typeof d === 'object' && d !== null && !(d instanceof RegExp))
2431
- ? structuredClone(d) : d;
2432
- }
2433
- }
2434
- return data;
2435
- }
2436
-
2437
- // Inline field transforms run once during parse (and safe/ok), never
2438
- // during DB hydrate. Each transform receives the whole raw input
2439
- // object as 'it'; its return value becomes the field's candidate
2440
- // value before default + validation. Transform errors surface as
2441
- // {error: 'transform'} issues on the final result.
2442
- _applyTransforms(raw, working) {
2443
- const norm = this._normalize();
2444
- const errors = [];
2445
- for (const [n, f] of norm.fields) {
2446
- if (!f.transform) continue;
2447
- try {
2448
- working[n] = f.transform(raw);
2449
- } catch (e) {
2450
- errors.push({field: n, error: 'transform', message: e?.message || String(e)});
2451
- }
2452
- }
2453
- return errors;
2454
- }
2455
-
2456
- _validateEnum(data, collect) {
2457
- const norm = this._normalize();
2458
- for (const [n, v] of norm.enumMembers) {
2459
- if (data === n || data === v) return collect ? [] : true;
2460
- }
2461
- if (!collect) return false;
2462
- const members = [...norm.enumMembers.keys()].join(', ');
2463
- return [{field: '', error: 'enum', message: (this.name || 'enum') + ' expected one of: ' + members}];
2464
- }
2465
-
2466
- _materializeEnum(data) {
2467
- const norm = this._normalize();
2468
- for (const [n, v] of norm.enumMembers) {
2469
- if (data === n || data === v) return v;
2470
- }
2471
- return data;
2472
- }
2473
-
2474
- // Canonical field parse pipeline — run per-field in declaration order,
2475
- // then an after-fields pass for eager-derived. This is the SINGLE
2476
- // source of truth for parse-time field semantics; _hydrate bypasses
2477
- // steps 1-5 entirely (DB rows arrive canonical) and picks up at step 7.
2478
- //
2479
- // 1. Obtain raw candidate — transform(raw) if declared, else raw[name]
2480
- // 2. Apply default — if candidate missing/undefined
2481
- // 3. Required check — optional/required/nullability
2482
- // 4. Type validation — primitive / literal-union / array
2483
- // 5. Constraint checks — range, regex, attrs
2484
- // 6. Assign to instance — own enumerable property
2485
- // 7. Eager-derived pass — run !> entries in declaration order
2486
- //
2487
- // Transforms (step 1) run on parse/safe/ok only. Hydrate skips them
2488
- // because DB columns already hold the canonical values. Eager-derived
2489
- // (step 7) fires on BOTH paths so hydrated instances have the same
2490
- // shape as parsed ones.
2491
- parse(data) {
2492
- if (this.kind === 'mixin') {
2493
- throw new Error(":mixin schema '" + (this.name || 'anon') + "' is not instantiable");
2494
- }
2495
- if (this.kind === 'enum') {
2496
- const errs = this._validateEnum(data, true);
2497
- if (errs.length) throw new SchemaError(errs, this.name, this.kind);
2498
- return this._materializeEnum(data);
2499
- }
2500
- const raw = data || {};
2501
- const working = { ...raw };
2502
- const transformErrors = this._applyTransforms(raw, working);
2503
- this._applyDefaults(working);
2504
- const errs = transformErrors.concat(this._validateFields(working, true));
2505
- if (errs.length) throw new SchemaError(errs, this.name, this.kind);
2506
- // @ensure runs AFTER per-field validation so predicates can
2507
- // assume declared fields are typed and defaulted. A field-level
2508
- // failure short-circuits: we never reach this line with errs.
2509
- const ensureErrs = this._applyEnsures(working);
2510
- if (ensureErrs.length) throw new SchemaError(ensureErrs, this.name, this.kind);
2511
- const klass = this._getClass();
2512
- const inst = new klass(working, false);
2513
- this._applyEagerDerived(inst);
2514
- return inst;
2515
- }
2516
-
2517
- safe(data) {
2518
- if (this.kind === 'mixin') {
2519
- return {ok: false, value: null, errors: [{field: '', error: 'mixin', message: 'not instantiable'}]};
2520
- }
2521
- if (this.kind === 'enum') {
2522
- const errs = this._validateEnum(data, true);
2523
- if (errs.length) return {ok: false, value: null, errors: errs};
2524
- return {ok: true, value: this._materializeEnum(data), errors: null};
2525
- }
2526
- const raw = data || {};
2527
- const working = { ...raw };
2528
- const transformErrors = this._applyTransforms(raw, working);
2529
- this._applyDefaults(working);
2530
- const errs = transformErrors.concat(this._validateFields(working, true));
2531
- if (errs.length) return {ok: false, value: null, errors: errs};
2532
- const ensureErrs = this._applyEnsures(working);
2533
- if (ensureErrs.length) return {ok: false, value: null, errors: ensureErrs};
2534
- const klass = this._getClass();
2535
- const inst = new klass(working, false);
2536
- try { this._applyEagerDerived(inst); }
2537
- catch (e) {
2538
- return {ok: false, value: null, errors: [{field: '', error: 'derived', message: e?.message || String(e)}]};
2539
- }
2540
- return {ok: true, value: inst, errors: null};
2541
- }
2542
-
2543
- ok(data) {
2544
- if (this.kind === 'mixin') return false;
2545
- if (this.kind === 'enum') return this._validateEnum(data, false);
2546
- const raw = data || {};
2547
- const working = { ...raw };
2548
- const transformErrors = this._applyTransforms(raw, working);
2549
- if (transformErrors.length) return false;
2550
- this._applyDefaults(working);
2551
- if (!this._validateFields(working, false)) return false;
2552
- // Per-field validation passed — @ensure predicates are the final gate.
2553
- return this._applyEnsures(working).length === 0;
2554
- }
2555
-
2556
- // ---- :model static ORM methods --------------------------------------------
2557
-
2558
- async find(id) {
2559
- this._assertModel('find');
2560
- const norm = this._normalize();
2561
- const soft = norm.softDelete ? ' AND "deleted_at" IS NULL' : '';
2562
- const sql = 'SELECT * FROM "' + norm.tableName + '" WHERE "' + norm.primaryKey + '" = ?' + soft + ' LIMIT 1';
2563
- const res = await __schemaAdapter.query(sql, [id]);
2564
- if (!res.rows) return null;
2565
- return this._hydrate(res.columns, res.data[0]);
2566
- }
2567
-
2568
- where(cond, ...params) {
2569
- this._assertModel('where');
2570
- return new __SchemaQuery(this).where(cond, ...params);
2571
- }
2572
-
2573
- all() {
2574
- this._assertModel('all');
2575
- return new __SchemaQuery(this).all();
2576
- }
2577
-
2578
- first() {
2579
- this._assertModel('first');
2580
- return new __SchemaQuery(this).first();
2581
- }
2582
-
2583
- count() {
2584
- this._assertModel('count');
2585
- return new __SchemaQuery(this).count();
2586
- }
2587
-
2588
- async create(data) {
2589
- this._assertModel('create');
2590
- // Input keys may be snake_case or camelCase; the runtime
2591
- // canonicalizes to camelCase so instance properties line up with
2592
- // declared field names.
2593
- const klass = this._getClass();
2594
- const canonical = {};
2595
- if (data && typeof data === 'object') {
2596
- for (const k of Object.keys(data)) canonical[__schemaCamel(k)] = data[k];
2597
- }
2598
- const inst = new klass(this._applyDefaults(canonical), false);
2599
- // FK columns like user_id canonicalize to userId and need to
2600
- // round-trip through the INSERT path, so attach them as own
2601
- // properties even though they aren't declared fields.
2602
- for (const [k, v] of Object.entries(canonical)) {
2603
- if (!(k in inst)) {
2604
- Object.defineProperty(inst, k, { value: v, enumerable: true, writable: true, configurable: true });
2605
- }
2606
- }
2607
- await __schemaSave(this, inst);
2608
- return inst;
2609
- }
2610
-
2611
- toSQL(options) {
2612
- this._assertModel('toSQL');
2613
- return __schemaToSQL(this, options);
2614
- }
2615
-
2616
- _assertModel(api) {
2617
- if (this.kind !== 'model') {
2618
- throw new Error('schema: .' + api + '() is :model-only (got :' + this.kind + ')');
2619
- }
2620
- }
2621
-
2622
- // ---- Schema algebra (Phase 6) --------------------------------------------
2623
- // Invariant: every algebra operation returns a :shape. Model algebra
2624
- // strips ORM; :shape algebra drops behavior. Derived shapes preserve
2625
- // field metadata (constraints, defaults, modifiers) from the source
2626
- // normalized descriptor.
2627
-
2628
- pick(...keys) {
2629
- return __schemaDerive(this, (src) => {
2630
- const names = __schemaFlatten(keys);
2631
- const out = new Map();
2632
- for (const k of names) {
2633
- if (!src.has(k)) throw new Error("pick: unknown field '" + k + "' on " + (this.name || 'schema'));
2634
- out.set(k, src.get(k));
2635
- }
2636
- return out;
2637
- });
2638
- }
2639
-
2640
- omit(...keys) {
2641
- return __schemaDerive(this, (src) => {
2642
- const drop = new Set(__schemaFlatten(keys));
2643
- const out = new Map();
2644
- for (const [k, v] of src) if (!drop.has(k)) out.set(k, v);
2645
- return out;
2646
- });
2647
- }
2648
-
2649
- partial() {
2650
- return __schemaDerive(this, (src) => {
2651
- const out = new Map();
2652
- for (const [k, v] of src) out.set(k, { ...v, required: false });
2653
- return out;
2654
- });
2655
- }
2656
-
2657
- required(...keys) {
2658
- return __schemaDerive(this, (src) => {
2659
- const req = new Set(__schemaFlatten(keys));
2660
- const out = new Map();
2661
- for (const [k, v] of src) out.set(k, { ...v, required: req.has(k) ? true : v.required });
2662
- return out;
2663
- });
2664
- }
2665
-
2666
- extend(other) {
2667
- if (!(other instanceof __SchemaDef)) {
2668
- throw new Error('extend(): argument must be a schema value');
2669
- }
2670
- return __schemaDerive(this, (src) => {
2671
- const merged = new Map(src);
2672
- const otherFields = other._normalize().fields;
2673
- for (const [k, v] of otherFields) {
2674
- if (merged.has(k)) {
2675
- throw new Error("extend(): field '" + k + "' collides between " + (this.name || 'schema') + " and " + (other.name || 'other'));
2676
- }
2677
- merged.set(k, v);
2678
- }
2679
- return merged;
2680
- });
2681
- }
2682
- }
2683
-
2684
- function __schemaFlatten(keys) {
2685
- const out = [];
2686
- for (const k of keys) {
2687
- if (typeof k === 'symbol') out.push(Symbol.keyFor(k) || k.description);
2688
- else if (Array.isArray(k)) for (const kk of k) out.push(typeof kk === 'symbol' ? (Symbol.keyFor(kk) || kk.description) : kk);
2689
- else out.push(k);
2690
- }
2691
- return out;
2692
- }
2693
-
2694
- // Schema algebra — .pick / .omit / .partial / .required / .extend all
2695
- // land here. The v2 invariants encoded in this function:
1785
+ // validate = VALIDATE (pure)
1786
+ // browser = VALIDATE + BROWSER_STUBS (browser bundle)
1787
+ // server = VALIDATE + DB_NAMING + ORM (server runtime)
1788
+ // migration = VALIDATE + DB_NAMING + ORM + DDL (migration tool)
2696
1789
  //
2697
- // - Derived schemas are always kind: 'shape', regardless of source kind.
2698
- // ORM surface on :model is dropped.
2699
- // - Field semantics SURVIVE algebra: type, literals, constraints,
2700
- // inline transforms. Transforms-survive means a derived schema can
2701
- // still read raw-input keys that aren't in its declared output shape.
2702
- // - Instance behavior DOES NOT survive: methods, computed (~>), eager
2703
- // derived (!>), and hooks all get dropped because the rebuilt
2704
- // descriptor has no callable entries.
2705
- // - _sourceModel propagates through chained algebra so tooling can
2706
- // trace derived shapes back to the origin :model.
2707
- function __schemaDerive(source, transform) {
2708
- const src = source._normalize().fields;
2709
- const derivedFields = transform(src);
2710
- const entries = [];
2711
- for (const [, f] of derivedFields) {
2712
- const mods = [];
2713
- if (f.required) mods.push('!');
2714
- if (f.unique) mods.push('#');
2715
- if (f.optional && !f.required) mods.push('?');
2716
- entries.push({
2717
- tag: 'field', name: f.name, modifiers: mods,
2718
- typeName: f.typeName, array: f.array,
2719
- literals: f.literals || null,
2720
- constraints: f.constraints,
2721
- transform: f.transform || null,
2722
- });
2723
- }
2724
- const name = (source.name || 'Schema') + 'Derived';
2725
- const derived = new __SchemaDef({ kind: 'shape', name, entries });
2726
- // sourceModel propagates through chained algebra. Tooling can follow
2727
- // the chain back to the original :model for projection hints.
2728
- derived._sourceModel = source._sourceModel || (source.kind === 'model' ? source : null);
2729
- return derived;
1790
+ // The actual fragment imports + composition live in the loader files so
1791
+ // only the fragments needed by a given entry are bundled. Browser bundles
1792
+ // import loader-browser.js (validate + browser-stubs only); CLI / server
1793
+ // imports loader-server.js (all five fragments).
1794
+
1795
+ export function getSchemaRuntime(opts = {}) {
1796
+ if (!_schemaRuntimeProvider) {
1797
+ throw new Error(
1798
+ "schema runtime provider not registered. Side-effect-import either " +
1799
+ "'./schema/loader-server.js' (CLI / server / tests) or " +
1800
+ "'./schema/loader-browser.js' (browser bundle) before calling " +
1801
+ "any compileToJS that emits schemas."
1802
+ );
1803
+ }
1804
+ return _schemaRuntimeProvider(opts);
2730
1805
  }
2731
-
2732
- function __schemaCamel(col) { return String(col).replace(/_([a-z])/g, (_, c) => c.toUpperCase()); }
2733
-
2734
- function __schemaNormalizeDirectiveRelation(directive, ownerModel) {
2735
- const args = directive.args;
2736
- if (!args || !args.length) return null;
2737
- const a = args[0];
2738
- const name = directive.name;
2739
- if (name === 'belongs_to') {
2740
- const targetLc = a.target[0].toLowerCase() + a.target.slice(1);
2741
- return { kind: 'belongsTo', target: a.target, accessor: targetLc, foreignKey: __schemaFkName(a.target), optional: !!a.optional };
2742
- }
2743
- if (name === 'has_one' || name === 'one') {
2744
- const targetLc = a.target[0].toLowerCase() + a.target.slice(1);
2745
- return { kind: 'hasOne', target: a.target, accessor: targetLc, foreignKey: __schemaFkName(ownerModel), optional: !!a.optional };
2746
- }
2747
- if (name === 'has_many' || name === 'many') {
2748
- const targetLc = a.target[0].toLowerCase() + a.target.slice(1);
2749
- return { kind: 'hasMany', target: a.target, accessor: __schemaPluralize(targetLc), foreignKey: __schemaFkName(ownerModel), optional: !!a.optional };
2750
- }
2751
- return null;
2752
- }
2753
-
2754
- function __schemaExpandMixins(host, fields, directives, ctx) {
2755
- for (const d of directives) {
2756
- if (d.name !== 'mixin' || !d.args || !d.args[0]) continue;
2757
- const target = d.args[0].target;
2758
- if (!target) continue;
2759
- if (ctx.stack.includes(target)) {
2760
- throw new SchemaError(
2761
- [{field: '', error: 'mixin-cycle', message: 'mixin cycle: ' + ctx.stack.concat(target).join(' -> ')}],
2762
- host.name, host.kind);
2763
- }
2764
- if (ctx.seen.has(target)) continue;
2765
- const mx = __SchemaRegistry.getKind(target, 'mixin');
2766
- if (!mx) {
2767
- throw new SchemaError(
2768
- [{field: '', error: 'mixin-missing', message: 'unknown mixin: ' + target}],
2769
- host.name, host.kind);
2770
- }
2771
- ctx.seen.add(target);
2772
- ctx.stack.push(target);
2773
- // Recurse into nested mixins first (depth-first).
2774
- const childDirectives = mx._desc.entries.filter(e => e.tag === 'directive' && e.name === 'mixin')
2775
- .map(e => ({ name: e.name, args: e.args || [] }));
2776
- __schemaExpandMixins(host, fields, childDirectives, ctx);
2777
- // Then contribute the mixin's own fields.
2778
- for (const e of mx._desc.entries) {
2779
- if (e.tag !== 'field') continue;
2780
- if (fields.has(e.name)) {
2781
- throw new SchemaError(
2782
- [{field: e.name, error: 'mixin-collision', message: e.name + ' from mixin ' + target + ' collides with existing field'}],
2783
- host.name, host.kind);
2784
- }
2785
- fields.set(e.name, {
2786
- name: e.name,
2787
- required: e.modifiers.includes('!'),
2788
- unique: e.modifiers.includes('#'),
2789
- optional: e.modifiers.includes('?'),
2790
- typeName: e.typeName,
2791
- literals: e.literals || null,
2792
- array: e.array === true,
2793
- constraints: e.constraints || null,
2794
- transform: e.transform || null,
2795
- });
2796
- }
2797
- ctx.stack.pop();
2798
- }
2799
- }
2800
-
2801
- async function __schemaResolveRelation(def, inst, rel) {
2802
- const target = __SchemaRegistry.get(rel.target);
2803
- if (!target) throw new Error('schema: unknown relation target "' + rel.target + '" from ' + (def.name || 'anon'));
2804
- const pk = def._normalize().primaryKey;
2805
- if (rel.kind === 'belongsTo') {
2806
- const fk = inst[__schemaCamel(rel.foreignKey)];
2807
- return fk != null ? await target.find(fk) : null;
2808
- }
2809
- if (rel.kind === 'hasOne') {
2810
- return await target.where({ [rel.foreignKey]: inst[pk] }).first();
2811
- }
2812
- if (rel.kind === 'hasMany') {
2813
- return await target.where({ [rel.foreignKey]: inst[pk] }).all();
2814
- }
2815
- return null;
2816
- }
2817
-
2818
- // ---- Save / Destroy --------------------------------------------------------
2819
- // Rails-style lifecycle (D18):
2820
- // beforeValidation -> validate -> afterValidation ->
2821
- // beforeSave -> (beforeCreate|beforeUpdate) -> INSERT/UPDATE ->
2822
- // (afterCreate|afterUpdate) -> afterSave
2823
- // Destroy:
2824
- // beforeDestroy -> DELETE -> afterDestroy
2825
-
2826
- async function __schemaRunHook(def, inst, name) {
2827
- const fn = def._normalize().hooks.get(name);
2828
- if (fn) await fn.call(inst);
2829
- }
2830
-
2831
- async function __schemaSave(def, inst) {
2832
- const norm = def._normalize();
2833
- const isNew = !inst._persisted;
2834
-
2835
- await __schemaRunHook(def, inst, 'beforeValidation');
2836
- const errs = def._validateFields(inst, true);
2837
- if (errs.length) throw new SchemaError(errs, def.name, def.kind);
2838
- await __schemaRunHook(def, inst, 'afterValidation');
2839
-
2840
- await __schemaRunHook(def, inst, 'beforeSave');
2841
- if (isNew) await __schemaRunHook(def, inst, 'beforeCreate');
2842
- else await __schemaRunHook(def, inst, 'beforeUpdate');
2843
-
2844
- if (isNew) {
2845
- const cols = [], placeholders = [], values = [];
2846
- for (const [n, f] of norm.fields) {
2847
- const v = inst[n];
2848
- if (v == null) continue;
2849
- cols.push('"' + __schemaSnake(n) + '"');
2850
- placeholders.push('?');
2851
- values.push(__schemaSerialize(v, f));
2852
- }
2853
- // Include relation FKs. belongsTo FKs are camelCase properties on
2854
- // the instance (e.g. organizationId for organization_id).
2855
- for (const [, rel] of norm.relations) {
2856
- if (rel.kind !== 'belongsTo') continue;
2857
- const fkCamel = __schemaCamel(rel.foreignKey);
2858
- const v = inst[fkCamel];
2859
- if (v != null) {
2860
- cols.push('"' + rel.foreignKey + '"');
2861
- placeholders.push('?');
2862
- values.push(v);
2863
- }
2864
- }
2865
- const sql = 'INSERT INTO "' + norm.tableName + '" (' + cols.join(', ') + ') VALUES (' + placeholders.join(', ') + ') RETURNING *';
2866
- const res = await __schemaAdapter.query(sql, values);
2867
- if (res.data?.[0] && res.columns) {
2868
- for (let i = 0; i < res.columns.length; i++) {
2869
- const snake = res.columns[i].name;
2870
- const key = __schemaCamel(snake);
2871
- if (!(key in inst)) {
2872
- Object.defineProperty(inst, key, { value: res.data[0][i], enumerable: true, writable: true, configurable: true });
2873
- } else {
2874
- inst[key] = res.data[0][i];
2875
- }
2876
- if (snake !== key && !(snake in inst)) {
2877
- Object.defineProperty(inst, snake, {
2878
- enumerable: false, configurable: true,
2879
- get() { return this[key]; },
2880
- set(v) { this[key] = v; },
2881
- });
2882
- }
2883
- }
2884
- }
2885
- // Now that the RETURNING columns (id, @timestamps, FKs) are on the
2886
- // instance, !> eager-derived fields can see them. Mirrors the hydrate
2887
- // path, which runs _applyEagerDerived once all declared fields are
2888
- // populated. Per-docs semantics ("materialize once, not reactive")
2889
- // still hold — we're firing once, at end of construction, not on
2890
- // subsequent mutations.
2891
- def._applyEagerDerived(inst);
2892
- inst._persisted = true;
2893
- } else {
2894
- const sets = [], values = [];
2895
- for (const [n, f] of norm.fields) {
2896
- sets.push('"' + __schemaSnake(n) + '" = ?');
2897
- values.push(__schemaSerialize(inst[n], f));
2898
- }
2899
- if (sets.length) {
2900
- const pk = norm.primaryKey;
2901
- values.push(inst[pk]);
2902
- const sql = 'UPDATE "' + norm.tableName + '" SET ' + sets.join(', ') + ' WHERE "' + pk + '" = ?';
2903
- await __schemaAdapter.query(sql, values);
2904
- }
2905
- }
2906
- inst._dirty.clear();
2907
-
2908
- if (isNew) await __schemaRunHook(def, inst, 'afterCreate');
2909
- else await __schemaRunHook(def, inst, 'afterUpdate');
2910
- await __schemaRunHook(def, inst, 'afterSave');
2911
- return inst;
2912
- }
2913
-
2914
- async function __schemaDestroy(def, inst) {
2915
- if (!inst._persisted) return inst;
2916
- const norm = def._normalize();
2917
- await __schemaRunHook(def, inst, 'beforeDestroy');
2918
- if (norm.softDelete) {
2919
- const now = new Date().toISOString();
2920
- await __schemaAdapter.query('UPDATE "' + norm.tableName + '" SET "deleted_at" = ? WHERE "' + norm.primaryKey + '" = ?', [now, inst[norm.primaryKey]]);
2921
- inst.deletedAt = now;
2922
- } else {
2923
- await __schemaAdapter.query('DELETE FROM "' + norm.tableName + '" WHERE "' + norm.primaryKey + '" = ?', [inst[norm.primaryKey]]);
2924
- inst._persisted = false;
2925
- }
2926
- await __schemaRunHook(def, inst, 'afterDestroy');
2927
- return inst;
2928
- }
2929
-
2930
- function __schemaSerialize(v, field) {
2931
- if (field && field.typeName === 'json' && v != null && typeof v === 'object') {
2932
- return JSON.stringify(v);
2933
- }
2934
- return v;
2935
- }
2936
-
2937
- // ---- DDL emission (.toSQL) --------------------------------------------------
2938
- // Layer 4b: runs on first .toSQL() call. Independent of ORM — scripts
2939
- // that build schema from DDL never touch .find/.create.
2940
-
2941
- const __SCHEMA_SQL_TYPES = {
2942
- string: 'VARCHAR', text: 'TEXT', integer: 'INTEGER', number: 'DOUBLE',
2943
- boolean: 'BOOLEAN', date: 'DATE', datetime: 'TIMESTAMP', email: 'VARCHAR',
2944
- url: 'VARCHAR', uuid: 'UUID', phone: 'VARCHAR', zip: 'VARCHAR', json: 'JSON', any: 'JSON',
2945
- };
2946
-
2947
- function __schemaToSQL(def, options) {
2948
- const opts = options || {};
2949
- const { dropFirst = false, header } = opts;
2950
- const norm = def._normalize();
2951
- const blocks = [];
2952
- if (header) blocks.push(header);
2953
-
2954
- const table = norm.tableName;
2955
- const seq = table + '_seq';
2956
- if (dropFirst) {
2957
- blocks.push('DROP TABLE IF EXISTS ' + table + ' CASCADE;\\nDROP SEQUENCE IF EXISTS ' + seq + ';');
2958
- }
2959
-
2960
- // Sequence seed: explicit option wins over @idStart directive wins over 1.
2961
- // DuckDB 1.5.2 does not implement ALTER SEQUENCE ... RESTART WITH N, so the
2962
- // baseline has to be set at creation — hence the knob lives here, not in a
2963
- // post-create migration.
2964
- let idStart = 1;
2965
- for (const d of norm.directives) {
2966
- if (d.name === 'idStart' && d.args?.[0] && Number.isInteger(d.args[0].value)) {
2967
- idStart = d.args[0].value;
2968
- }
2969
- }
2970
- if (opts.idStart !== undefined) {
2971
- if (!Number.isInteger(opts.idStart)) {
2972
- throw new Error('schema.toSQL(): idStart must be an integer; got ' + String(opts.idStart));
2973
- }
2974
- idStart = opts.idStart;
2975
- }
2976
-
2977
- const columns = [];
2978
- const indexes = [];
2979
- columns.push(' ' + norm.primaryKey + " INTEGER PRIMARY KEY DEFAULT nextval('" + seq + "')");
2980
-
2981
- for (const [n, f] of norm.fields) {
2982
- columns.push(__schemaColumnDDL(n, f));
2983
- if (f.unique) {
2984
- indexes.push('CREATE UNIQUE INDEX idx_' + table + '_' + __schemaSnake(n) + ' ON ' + table + ' ("' + __schemaSnake(n) + '");');
2985
- }
2986
- }
2987
-
2988
- for (const [, rel] of norm.relations) {
2989
- if (rel.kind !== 'belongsTo') continue;
2990
- const refTable = __schemaTableName(rel.target);
2991
- const notNull = rel.optional ? '' : ' NOT NULL';
2992
- columns.push(' ' + rel.foreignKey + ' INTEGER' + notNull + ' REFERENCES ' + refTable + '(id)');
2993
- }
2994
-
2995
- if (norm.timestamps) {
2996
- columns.push(' created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
2997
- columns.push(' updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
2998
- }
2999
- if (norm.softDelete) {
3000
- columns.push(' deleted_at TIMESTAMP');
3001
- }
3002
-
3003
- // @index directives
3004
- for (const d of norm.directives) {
3005
- if (d.name !== 'index') continue;
3006
- const ixArgs = d.args?.[0] || {};
3007
- const fields = (ixArgs.fields || []).map(__schemaSnake);
3008
- if (!fields.length) continue;
3009
- const u = ixArgs.unique ? 'UNIQUE ' : '';
3010
- indexes.push('CREATE ' + u + 'INDEX idx_' + table + '_' + fields.join('_') + ' ON ' + table + ' (' + fields.map(f => '"' + f + '"').join(', ') + ');');
3011
- }
3012
-
3013
- blocks.push('CREATE SEQUENCE ' + seq + ' START ' + idStart + ';');
3014
- blocks.push('CREATE TABLE ' + table + ' (\\n' + columns.join(',\\n') + '\\n);');
3015
- if (indexes.length) blocks.push(indexes.join('\\n'));
3016
-
3017
- return blocks.join('\\n\\n') + '\\n';
3018
- }
3019
-
3020
- function __schemaColumnDDL(name, field) {
3021
- let base = __SCHEMA_SQL_TYPES[field.typeName] || 'VARCHAR';
3022
- if (field.array) base = 'JSON';
3023
- if (base === 'VARCHAR' && field.constraints?.max != null) {
3024
- base = 'VARCHAR(' + field.constraints.max + ')';
3025
- }
3026
- const parts = [' ' + __schemaSnake(name) + ' ' + base];
3027
- if (field.required) parts.push('NOT NULL');
3028
- if (field.unique) parts.push('UNIQUE');
3029
- if (field.constraints?.default !== undefined) {
3030
- parts.push('DEFAULT ' + __schemaSQLDefault(field.constraints.default));
3031
- }
3032
- return parts.join(' ');
3033
- }
3034
-
3035
- function __schemaSQLDefault(v) {
3036
- if (v === true) return 'true';
3037
- if (v === false) return 'false';
3038
- if (v === null) return 'NULL';
3039
- if (typeof v === 'number') return String(v);
3040
- if (typeof v === 'string') return "'" + v.replace(/'/g, "''") + "'";
3041
- return "'" + String(v).replace(/'/g, "''") + "'";
3042
- }
3043
-
3044
- function __schema(descriptor) {
3045
- const def = new __SchemaDef(descriptor);
3046
- // Every user-declared named schema lands in the registry so
3047
- // nested-typed fields (address! Address, items! OrderItem[],
3048
- // role! Role) can resolve their type reference at validate time.
3049
- // Algebra-derived schemas (.pick/.omit/.partial/…) bypass this
3050
- // factory so their synthetic names don't shadow the source.
3051
- if (def.name) __SchemaRegistry.register(def);
3052
- return def;
3053
- }
3054
-
3055
- const exports = {
3056
- __schema, SchemaError, __SchemaRegistry, __schemaSetAdapter,
3057
- __version: ${SCHEMA_RUNTIME_ABI_VERSION},
3058
- };
3059
- if (typeof globalThis !== 'undefined') globalThis.__ripSchema = exports;
3060
- return exports;
3061
- })();
3062
-
3063
- // === End Schema Runtime ===
3064
- `;
3065
-
3066
- function getSchemaRuntime() {
3067
- return SCHEMA_RUNTIME.trimStart();
3068
- }
3069
-
3070
- // ============================================================================
3071
- // Shadow TypeScript — Phase 3.5
3072
- // ============================================================================
3073
- //
3074
- // Emits virtual `.d.ts` / `.ts` declarations for :input, :shape, and :enum
3075
- // schemas so the TS language service can offer autocomplete and catch
3076
- // AST-shape mistakes before Phase 4 layers in :model/ORM/algebra. Written
3077
- // to mirror `emitComponentTypes()` in src/types.js — same prototype:
3078
- // `emitSchemaTypes(sexpr, lines)` returns true when any schema declaration
3079
- // was found (drives preamble injection), mutates `lines` with declarations.
3080
- //
3081
- // Type surface (locked with peer AI):
3082
- //
3083
- // interface Schema<T> {
3084
- // parse(data: unknown): T;
3085
- // safe(data: unknown): SchemaSafeResult<T>;
3086
- // ok(data: unknown): boolean;
3087
- // }
3088
- //
3089
- // `:input` emits declare const Foo: Schema<FooValue>;
3090
- // `:shape` emits declare const Foo: Schema<FooInstance>; where
3091
- // FooInstance = FooData & {methods/readonly getters}.
3092
- // `:enum` emits declare const Role: { parse(...): Role; ok(d): d is Role; ... }
3093
- //
3094
- // Methods are typed `(...args: any[]) => unknown`. Computed are
3095
- // `readonly name: unknown`. Body inference is out of scope for 3.5.
3096
-
3097
- export const SCHEMA_INTRINSIC_DECLS = [
3098
- 'interface SchemaIssue { field: string; error: string; message: string; }',
3099
- 'type SchemaSafeResult<T> = { ok: true; value: T; errors: null } | { ok: false; value: null; errors: SchemaIssue[] };',
3100
- // Base Schema interface. `Out` is the parsed value type; `In` is the
3101
- // data shape (defaults to unknown). Algebra methods are parameterized
3102
- // over `In` so chained operations on a typed :shape or :model derive
3103
- // correctly; when `In` defaults to unknown, `keyof In` is `never` and
3104
- // algebra methods don't autocomplete — which is the right behavior
3105
- // for :input schemas where the input shape isn't statically known.
3106
- 'interface Schema<Out, In = unknown> {',
3107
- ' parse(data: In): Out;',
3108
- ' safe(data: In): SchemaSafeResult<Out>;',
3109
- ' ok(data: unknown): boolean;',
3110
- ' pick<K extends keyof In>(...keys: K[]): Schema<Pick<In, K>, Pick<In, K>>;',
3111
- ' omit<K extends keyof In>(...keys: K[]): Schema<Omit<In, K>, Omit<In, K>>;',
3112
- ' partial(): Schema<Partial<In>, Partial<In>>;',
3113
- ' required<K extends keyof In>(...keys: K[]): Schema<Omit<In, K> & Required<Pick<In, K>>, Omit<In, K> & Required<Pick<In, K>>>;',
3114
- ' extend<U>(other: Schema<U>): Schema<In & U, In & U>;',
3115
- '}',
3116
- // Chainable query builder for :model.
3117
- 'interface SchemaQuery<T> {',
3118
- ' all(): Promise<T[]>;',
3119
- ' first(): Promise<T | null>;',
3120
- ' count(): Promise<number>;',
3121
- ' limit(n: number): SchemaQuery<T>;',
3122
- ' offset(n: number): SchemaQuery<T>;',
3123
- ' order(spec: string): SchemaQuery<T>;',
3124
- '}',
3125
- // ModelSchema extends the base schema surface with ORM methods. Algebra
3126
- // over `Data` (not `Instance`) so derived shapes reflect runtime
3127
- // behavior-dropping semantics.
3128
- 'interface ModelSchema<Instance, Data = unknown> extends Schema<Instance, Data> {',
3129
- ' find(id: unknown): Promise<Instance | null>;',
3130
- ' findMany(ids: unknown[]): Promise<Instance[]>;',
3131
- ' where(cond: Record<string, unknown> | string, ...params: unknown[]): SchemaQuery<Instance>;',
3132
- ' all(limit?: number): Promise<Instance[]>;',
3133
- ' first(): Promise<Instance | null>;',
3134
- ' count(cond?: Record<string, unknown>): Promise<number>;',
3135
- ' create(data: Partial<Data>): Promise<Instance>;',
3136
- ' toSQL(options?: { dropFirst?: boolean; header?: string; idStart?: number }): string;',
3137
- '}',
3138
- ];
3139
-
3140
- const RIP_TYPE_TO_TS = {
3141
- string: 'string',
3142
- text: 'string',
3143
- email: 'string',
3144
- url: 'string',
3145
- uuid: 'string',
3146
- phone: 'string',
3147
- zip: 'string',
3148
- number: 'number',
3149
- integer: 'number',
3150
- boolean: 'boolean',
3151
- date: 'Date',
3152
- datetime: 'Date',
3153
- json: 'unknown',
3154
- any: 'any',
3155
- };
3156
-
3157
- function mapFieldType(entry) {
3158
- if (entry.typeName === 'literal-union' && entry.literals?.length) {
3159
- return entry.literals.map(l => JSON.stringify(l)).join(' | ');
3160
- }
3161
- let base = RIP_TYPE_TO_TS[entry.typeName] ?? entry.typeName;
3162
- return entry.array ? `${base}[]` : base;
3163
- }
3164
-
3165
- // Extract descriptor from a SCHEMA_BODY s-expr node. Grammar reduces
3166
- // `['schema', SCHEMA_BODY_VAL]` where the value is the String wrapper
3167
- // carrying `.descriptor` via the metadata bridge.
3168
- function descriptorFromSchemaNode(schemaNode) {
3169
- if (!Array.isArray(schemaNode)) return null;
3170
- let head = schemaNode[0]?.valueOf?.() ?? schemaNode[0];
3171
- if (head !== 'schema') return null;
3172
- let body = schemaNode[1];
3173
- if (!body || typeof body !== 'object') return null;
3174
- if (body.descriptor) return body.descriptor;
3175
- if (body.data?.descriptor) return body.data.descriptor;
3176
- return null;
3177
- }
3178
-
3179
- // Walk the parsed s-expression collecting every named schema declaration.
3180
- // Mixins are emitted first so subsequent :shape/:model type aliases can
3181
- // reference them in `& Timestamps`-style intersections. Within a group,
3182
- // source order is preserved. Returns true when at least one schema was
3183
- // found (drives intrinsic preamble injection).
3184
- export function emitSchemaTypes(sexpr, lines) {
3185
- const collected = [];
3186
- collectSchemas(sexpr, collected);
3187
- if (!collected.length) return false;
3188
-
3189
- // Set of locally-known schema names (for relation-accessor type
3190
- // resolution — same-file targets get typed, unknown targets degrade).
3191
- const known = new Set(collected.map(c => c.name));
3192
- const byName = new Map(collected.map(c => [c.name, c]));
3193
-
3194
- // Mixin types first so type aliases down-file can reference them.
3195
- for (const c of collected) {
3196
- if (c.descriptor.kind === 'mixin') emitOneSchemaType(c, byName, known, lines);
3197
- }
3198
- for (const c of collected) {
3199
- if (c.descriptor.kind !== 'mixin') emitOneSchemaType(c, byName, known, lines);
3200
- }
3201
- return true;
3202
- }
3203
-
3204
- function collectSchemas(sexpr, out) {
3205
- if (!Array.isArray(sexpr)) return;
3206
- const head = sexpr[0]?.valueOf?.() ?? sexpr[0];
3207
- let exported = false;
3208
- let assignNode = null;
3209
- if (head === 'export' && Array.isArray(sexpr[1])) {
3210
- const inner = sexpr[1];
3211
- const innerHead = inner[0]?.valueOf?.() ?? inner[0];
3212
- if (innerHead === '=') { exported = true; assignNode = inner; }
3213
- else collectSchemas(sexpr[1], out);
3214
- } else if (head === '=') {
3215
- assignNode = sexpr;
3216
- } else if (head === 'program' || head === 'block') {
3217
- for (let i = 1; i < sexpr.length; i++) {
3218
- if (Array.isArray(sexpr[i])) collectSchemas(sexpr[i], out);
3219
- }
3220
- }
3221
- if (assignNode && Array.isArray(assignNode[2])) {
3222
- const name = assignNode[1]?.valueOf?.() ?? assignNode[1];
3223
- const descriptor = descriptorFromSchemaNode(assignNode[2]);
3224
- if (typeof name === 'string' && descriptor) {
3225
- out.push({ name, descriptor, exported });
3226
- }
3227
- }
3228
- }
3229
-
3230
- function emitOneSchemaType(collected, byName, known, lines) {
3231
- const { name, descriptor, exported } = collected;
3232
- const exp = exported ? 'export ' : '';
3233
- const decl = exported ? '' : 'declare ';
3234
-
3235
- if (descriptor.kind === 'enum') {
3236
- const members = [];
3237
- for (const e of descriptor.entries) {
3238
- if (e.tag !== 'enum-member') continue;
3239
- const v = e.value !== undefined ? e.value : e.name;
3240
- members.push(typeof v === 'string' ? JSON.stringify(v) : String(v));
3241
- }
3242
- const union = members.length ? members.join(' | ') : 'never';
3243
- lines.push(`${exp}type ${name} = ${union};`);
3244
- lines.push(`${exp}${decl}const ${name}: { parse(data: unknown): ${name}; safe(data: unknown): SchemaSafeResult<${name}>; ok(data: unknown): data is ${name}; };`);
3245
- return;
3246
- }
3247
-
3248
- if (descriptor.kind === 'mixin') {
3249
- // :mixin is declaration-time-only; expose it as a field type alias
3250
- // so hosts that `@mixin Foo` can intersect it into their Data type.
3251
- // No value declaration — mixins aren't user-facing runtime values.
3252
- const fieldProps = fieldPropList(descriptor);
3253
- lines.push(`${exp}type ${name} = { ${fieldProps.join('; ')} };`);
3254
- return;
3255
- }
3256
-
3257
- const fieldProps = fieldPropList(descriptor);
3258
- const mixinRefs = mixinIntersections(descriptor, byName);
3259
- const methods = [];
3260
- const computed = [];
3261
- for (const e of descriptor.entries) {
3262
- if (e.tag === 'method') {
3263
- methods.push(`${e.name}: (...args: any[]) => unknown`);
3264
- } else if (e.tag === 'computed') {
3265
- computed.push(`readonly ${e.name}: unknown`);
3266
- }
3267
- // hooks are intentionally omitted — they fire automatically and
3268
- // shouldn't appear in autocomplete.
3269
- }
3270
-
3271
- const dataBase = `{ ${fieldProps.join('; ')} }`;
3272
- const dataType = mixinRefs.length ? `${dataBase} & ${mixinRefs.join(' & ')}` : dataBase;
3273
-
3274
- if (descriptor.kind === 'model') {
3275
- const dataName = `${name}Data`;
3276
- const instName = `${name}Instance`;
3277
- const relationAccessors = modelRelationAccessors(descriptor, known);
3278
- const instanceExtras = [
3279
- ...computed,
3280
- ...methods,
3281
- ...relationAccessors,
3282
- `save(): Promise<${instName}>`,
3283
- `destroy(): Promise<${instName}>`,
3284
- `ok(): boolean`,
3285
- `errors(): SchemaIssue[]`,
3286
- `toJSON(): ${dataName}`,
3287
- ];
3288
- lines.push(`${exp}type ${dataName} = ${dataType};`);
3289
- lines.push(`${exp}type ${instName} = ${dataName} & { ${instanceExtras.join('; ')} };`);
3290
- lines.push(`${exp}${decl}const ${name}: ModelSchema<${instName}, ${dataName}>;`);
3291
- return;
3292
- }
3293
-
3294
- if (descriptor.kind === 'shape') {
3295
- const dataName = `${name}Data`;
3296
- const instName = `${name}Instance`;
3297
- const hasBehavior = methods.length + computed.length > 0;
3298
- lines.push(`${exp}type ${dataName} = ${dataType};`);
3299
- if (hasBehavior) {
3300
- lines.push(`${exp}type ${instName} = ${dataName} & { ${[...computed, ...methods].join('; ')} };`);
3301
- lines.push(`${exp}${decl}const ${name}: Schema<${instName}, ${dataName}>;`);
3302
- } else {
3303
- lines.push(`${exp}${decl}const ${name}: Schema<${dataName}, ${dataName}>;`);
3304
- }
3305
- return;
3306
- }
3307
-
3308
- // :input — parse returns the Data shape directly (no behavior).
3309
- const valueName = `${name}Value`;
3310
- lines.push(`${exp}type ${valueName} = ${dataType};`);
3311
- lines.push(`${exp}${decl}const ${name}: Schema<${valueName}, ${valueName}>;`);
3312
- }
3313
-
3314
- // Return an array of mixin type-reference strings for `& Foo & Bar` joins.
3315
- function mixinIntersections(descriptor, byName) {
3316
- const refs = [];
3317
- for (const e of descriptor.entries) {
3318
- if (e.tag !== 'directive' || e.name !== 'mixin') continue;
3319
- const args = e.args;
3320
- const target = args && args[0] && args[0].target;
3321
- if (!target) continue;
3322
- const known = byName && byName.get(target);
3323
- if (known && known.descriptor.kind === 'mixin') {
3324
- refs.push(target);
3325
- }
3326
- }
3327
- return refs;
3328
- }
3329
-
3330
- // Emit relation accessor type declarations for :model instances. For
3331
- // targets declared in the same file we emit a typed Promise; for
3332
- // unknown (cross-file) targets we degrade to `Promise<unknown>` rather
3333
- // than emit an unresolved bare name.
3334
- function modelRelationAccessors(descriptor, known) {
3335
- const out = [];
3336
- for (const e of descriptor.entries) {
3337
- if (e.tag !== 'directive') continue;
3338
- const args = e.args;
3339
- if (!args || !args[0]) continue;
3340
- const target = args[0].target;
3341
- if (!target) continue;
3342
- const optional = args[0].optional === true;
3343
- const targetLc = target[0].toLowerCase() + target.slice(1);
3344
- const instName = `${target}Instance`;
3345
- const isKnown = known && known.has(target);
3346
- if (e.name === 'belongs_to') {
3347
- const retT = isKnown ? (optional ? `${instName} | null` : `${instName} | null`) : 'unknown';
3348
- out.push(`${targetLc}(): Promise<${retT}>`);
3349
- } else if (e.name === 'has_one' || e.name === 'one') {
3350
- const retT = isKnown ? `${instName} | null` : 'unknown';
3351
- out.push(`${targetLc}(): Promise<${retT}>`);
3352
- } else if (e.name === 'has_many' || e.name === 'many') {
3353
- const retT = isKnown ? `${instName}[]` : 'unknown[]';
3354
- const pluralLc = __schemaClientPluralize(targetLc);
3355
- out.push(`${pluralLc}(): Promise<${retT}>`);
3356
- }
3357
- }
3358
- return out;
3359
- }
3360
-
3361
- // Minimal pluralizer for accessor names. Keep in sync with the runtime
3362
- // __schemaPluralize rules (same surface for declaration parity).
3363
- function __schemaClientPluralize(w) {
3364
- const lw = w.toLowerCase();
3365
- if (/[^aeiouy]y$/i.test(w)) return w.slice(0, -1) + 'ies';
3366
- if (/(s|x|z|ch|sh)$/i.test(w)) return w + 'es';
3367
- return w + 's';
3368
- }
3369
-
3370
- function fieldPropList(descriptor) {
3371
- const props = [];
3372
- for (const e of descriptor.entries) {
3373
- if (e.tag !== 'field') continue;
3374
- const required = e.modifiers.includes('!');
3375
- const mark = required ? '' : '?';
3376
- props.push(`${e.name}${mark}: ${mapFieldType(e)}`);
3377
- }
3378
- return props;
3379
- }
3380
-
3381
- // Eagerly install the runtime on globalThis at module load so downstream
3382
- // compilation units emitted with `skipRuntimes: true` (a common test-harness
3383
- // setting) can pick up `{__schema, SchemaError}` without a separate bootstrap
3384
- // step. The same pattern is used by the reactive and component runtimes.
3385
- if (typeof globalThis !== 'undefined' && !globalThis.__ripSchema) {
3386
- try { (0, eval)(SCHEMA_RUNTIME); } catch {}
3387
- }
3388
-
3389
- export { SCHEMA_RUNTIME };