rip-lang 3.15.0 → 3.15.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 -2
- package/bin/rip +1 -1
- package/docs/RIP-SCHEMA.md +4 -4
- package/docs/dist/rip.js +6 -6
- package/docs/dist/rip.min.js +83 -83
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/extensions/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/package.json +6 -8
- package/scripts/postinstall.js +27 -0
- package/src/AGENTS.md +27 -25
- package/src/browser.js +1 -1
- package/src/compiler.js +1 -1
- package/src/grammar/grammar.rip +1 -1
- package/src/lexer.js +1 -1
- package/src/schema/dts-emit.js +329 -0
- package/src/schema/loader-browser.js +55 -0
- package/src/schema/loader-server.js +65 -0
- package/src/schema/runtime-browser-stubs.js +51 -0
- package/src/schema/runtime-db-naming.js +34 -0
- package/src/schema/runtime-ddl.js +124 -0
- package/src/schema/runtime-orm.js +294 -0
- package/src/schema/runtime-validate.js +816 -0
- package/src/schema/runtime.generated.js +1315 -0
- package/src/schema/schema.js +1805 -0
- package/src/typecheck.js +2 -2
- package/src/types-emit.js +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Schema runtime loader — server / CLI / migration variant.
|
|
2
|
+
//
|
|
3
|
+
// Why this file exists (peer-review: "are loaders just picking which
|
|
4
|
+
// fragments to concatenate, or doing real work?"):
|
|
5
|
+
//
|
|
6
|
+
// The loader is the IMPORT BOUNDARY. By having loader-server.js
|
|
7
|
+
// import all five fragments and loader-browser.js import only
|
|
8
|
+
// validate + browser-stubs, Bun's tree-shaker can statically
|
|
9
|
+
// determine which fragment string constants reach which entry
|
|
10
|
+
// point. If the mode-switch logic lived in src/schema.js (which is
|
|
11
|
+
// reachable from BOTH browser and server entries), every fragment
|
|
12
|
+
// constant would be statically reachable from every entry, and
|
|
13
|
+
// tree-shaking couldn't remove anything. The two-loader split is
|
|
14
|
+
// what makes the bundle savings real.
|
|
15
|
+
//
|
|
16
|
+
// Side-effect import. Adds a runtime provider that supports all four
|
|
17
|
+
// modes (validate / browser / server / migration) and eagerly installs
|
|
18
|
+
// the migration runtime on globalThis at module load.
|
|
19
|
+
//
|
|
20
|
+
// Used by every Node-side caller of the compiler:
|
|
21
|
+
// - bin/rip (CLI)
|
|
22
|
+
// - test/runner.js, test/types/... (test runner)
|
|
23
|
+
// - src/typecheck.js (LSP / rip check)
|
|
24
|
+
// - any server-side code that imports src/compiler.js
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
SCHEMA_RUNTIME_WRAPPER_HEAD,
|
|
28
|
+
SCHEMA_RUNTIME_WRAPPER_TAIL,
|
|
29
|
+
SCHEMA_VALIDATE_RUNTIME,
|
|
30
|
+
SCHEMA_DB_NAMING_RUNTIME,
|
|
31
|
+
SCHEMA_ORM_RUNTIME,
|
|
32
|
+
SCHEMA_DDL_RUNTIME,
|
|
33
|
+
SCHEMA_BROWSER_STUBS_RUNTIME,
|
|
34
|
+
} from './runtime.generated.js';
|
|
35
|
+
import { setSchemaRuntimeProvider } from './schema.js';
|
|
36
|
+
|
|
37
|
+
function provider({ mode = 'migration' } = {}) {
|
|
38
|
+
let body;
|
|
39
|
+
switch (mode) {
|
|
40
|
+
case 'validate':
|
|
41
|
+
body = SCHEMA_VALIDATE_RUNTIME;
|
|
42
|
+
break;
|
|
43
|
+
case 'browser':
|
|
44
|
+
body = SCHEMA_VALIDATE_RUNTIME + '\n' + SCHEMA_BROWSER_STUBS_RUNTIME;
|
|
45
|
+
break;
|
|
46
|
+
case 'server':
|
|
47
|
+
body = SCHEMA_VALIDATE_RUNTIME + '\n' + SCHEMA_DB_NAMING_RUNTIME + '\n' + SCHEMA_ORM_RUNTIME;
|
|
48
|
+
break;
|
|
49
|
+
case 'migration':
|
|
50
|
+
body = SCHEMA_VALIDATE_RUNTIME + '\n' + SCHEMA_DB_NAMING_RUNTIME + '\n' + SCHEMA_ORM_RUNTIME + '\n' + SCHEMA_DDL_RUNTIME;
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`unknown schema runtime mode: ${mode}`);
|
|
54
|
+
}
|
|
55
|
+
return (SCHEMA_RUNTIME_WRAPPER_HEAD + body + SCHEMA_RUNTIME_WRAPPER_TAIL).trimStart();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setSchemaRuntimeProvider(provider);
|
|
59
|
+
|
|
60
|
+
// Eagerly install migration runtime so test harnesses that emit with
|
|
61
|
+
// skipRuntimes: true find {__schema, SchemaError} on globalThis.
|
|
62
|
+
export const SCHEMA_RUNTIME = provider({ mode: 'migration' });
|
|
63
|
+
if (typeof globalThis !== 'undefined' && !globalThis.__ripSchema) {
|
|
64
|
+
try { (0, eval)(SCHEMA_RUNTIME); } catch {}
|
|
65
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Schema runtime fragment: browser-stubs (browser only)
|
|
2
|
+
//
|
|
3
|
+
// This file is the source of truth for one slice of the schema runtime.
|
|
4
|
+
// Edit here, then run `bun run build:schema-runtime` to regenerate
|
|
5
|
+
// `src/schema/runtime.generated.js`. Tests pin the public surface via
|
|
6
|
+
// test/schema/errors.test.js, test/schema/modes.test.js, and the source
|
|
7
|
+
// schema test suite.
|
|
8
|
+
//
|
|
9
|
+
// Fragments are concatenated INSIDE one shared IIFE wrapper at build time.
|
|
10
|
+
// They share scope; references like `__SchemaRegistry` resolve to bindings
|
|
11
|
+
// defined in earlier-included fragments. Editor tooling (LSP / lint) may
|
|
12
|
+
// not recognize cross-fragment references — that is expected; behavior is
|
|
13
|
+
// pinned by the test suite.
|
|
14
|
+
|
|
15
|
+
/* eslint-disable no-undef, no-unused-vars */
|
|
16
|
+
// Browser stubs — throwing replacements for every ORM / DDL helper that
|
|
17
|
+
// the validate fragment references but doesn't implement. Loaded ONLY
|
|
18
|
+
// in browser mode.
|
|
19
|
+
//
|
|
20
|
+
// The validate fragment's `_makeClass`, `_normalize`, and
|
|
21
|
+
// `__schemaNormalizeDirectiveRelation` reference helpers that live in
|
|
22
|
+
// db-naming, orm, and ddl fragments at runtime. Browser mode doesn't
|
|
23
|
+
// include those fragments, so we provide thin throwing stubs here so
|
|
24
|
+
// browser-side schema declarations parse and validate cleanly while
|
|
25
|
+
// any attempt to use server-only behavior fails with a helpful message.
|
|
26
|
+
|
|
27
|
+
const __schemaBrowserStub = (api) => function() {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"schema." + api + "() is not available in the browser. " +
|
|
30
|
+
"Import @rip-lang/db on the server."
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Static / class-level methods on __SchemaDef
|
|
35
|
+
__SchemaDef.prototype.find = __schemaBrowserStub('find');
|
|
36
|
+
__SchemaDef.prototype.where = __schemaBrowserStub('where');
|
|
37
|
+
__SchemaDef.prototype.all = __schemaBrowserStub('all');
|
|
38
|
+
__SchemaDef.prototype.first = __schemaBrowserStub('first');
|
|
39
|
+
__SchemaDef.prototype.count = __schemaBrowserStub('count');
|
|
40
|
+
__SchemaDef.prototype.create = __schemaBrowserStub('create');
|
|
41
|
+
__SchemaDef.prototype.toSQL = __schemaBrowserStub('toSQL');
|
|
42
|
+
|
|
43
|
+
// Helpers referenced by the validate fragment that are otherwise
|
|
44
|
+
// defined in db-naming / orm fragments. Kept inert (return safe
|
|
45
|
+
// defaults or throw on use) so validate's _makeClass / _normalize
|
|
46
|
+
// can run end-to-end in browser context.
|
|
47
|
+
function __schemaSave() { throw new Error("schema instance.save() is not available in the browser. Import @rip-lang/db on the server."); }
|
|
48
|
+
function __schemaDestroy() { throw new Error("schema instance.destroy() is not available in the browser. Import @rip-lang/db on the server."); }
|
|
49
|
+
function __schemaTableName(m) { return null; } // returned only for :model normalize; never used downstream in browser
|
|
50
|
+
function __schemaPluralize(w) { return w; } // identity — relations work for type-resolution but never query
|
|
51
|
+
function __schemaFkName(m) { return ''; } // ditto
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Schema runtime fragment: db-naming (server + migration)
|
|
2
|
+
//
|
|
3
|
+
// This file is the source of truth for one slice of the schema runtime.
|
|
4
|
+
// Edit here, then run `bun run build:schema-runtime` to regenerate
|
|
5
|
+
// `src/schema/runtime.generated.js`. Tests pin the public surface via
|
|
6
|
+
// test/schema/errors.test.js, test/schema/modes.test.js, and the source
|
|
7
|
+
// schema test suite.
|
|
8
|
+
//
|
|
9
|
+
// Fragments are concatenated INSIDE one shared IIFE wrapper at build time.
|
|
10
|
+
// They share scope; references like `__SchemaRegistry` resolve to bindings
|
|
11
|
+
// defined in earlier-included fragments. Editor tooling (LSP / lint) may
|
|
12
|
+
// not recognize cross-fragment references — that is expected; behavior is
|
|
13
|
+
// pinned by the test suite.
|
|
14
|
+
|
|
15
|
+
/* eslint-disable no-undef, no-unused-vars */
|
|
16
|
+
const __SCHEMA_UNCOUNTABLE = new Set(['equipment','information','rice','money','species','series','fish','sheep','data']);
|
|
17
|
+
|
|
18
|
+
const __SCHEMA_IRREGULAR = new Map([['person','people'],['man','men'],['woman','women'],['child','children'],['tooth','teeth'],['foot','feet'],['mouse','mice']]);
|
|
19
|
+
|
|
20
|
+
function __schemaPluralize(w) {
|
|
21
|
+
const lw = w.toLowerCase();
|
|
22
|
+
if (__SCHEMA_UNCOUNTABLE.has(lw)) return w;
|
|
23
|
+
if (__SCHEMA_IRREGULAR.has(lw)) return __SCHEMA_IRREGULAR.get(lw);
|
|
24
|
+
// Preserve case of the input — pluralizer operates on the trailing form
|
|
25
|
+
// but keeps the rest unchanged, so orderItem becomes orderItems
|
|
26
|
+
// and User becomes Users.
|
|
27
|
+
if (/[^aeiouy]y$/i.test(w)) return w.slice(0, -1) + 'ies';
|
|
28
|
+
if (/(s|x|z|ch|sh)$/i.test(w)) return w + 'es';
|
|
29
|
+
return w + 's';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function __schemaTableName(model) { return __schemaPluralize(__schemaSnake(model)); }
|
|
33
|
+
|
|
34
|
+
function __schemaFkName(model) { return __schemaSnake(model) + '_id'; }
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Schema runtime fragment: ddl (migration only)
|
|
2
|
+
//
|
|
3
|
+
// This file is the source of truth for one slice of the schema runtime.
|
|
4
|
+
// Edit here, then run `bun run build:schema-runtime` to regenerate
|
|
5
|
+
// `src/schema/runtime.generated.js`. Tests pin the public surface via
|
|
6
|
+
// test/schema/errors.test.js, test/schema/modes.test.js, and the source
|
|
7
|
+
// schema test suite.
|
|
8
|
+
//
|
|
9
|
+
// Fragments are concatenated INSIDE one shared IIFE wrapper at build time.
|
|
10
|
+
// They share scope; references like `__SchemaRegistry` resolve to bindings
|
|
11
|
+
// defined in earlier-included fragments. Editor tooling (LSP / lint) may
|
|
12
|
+
// not recognize cross-fragment references — that is expected; behavior is
|
|
13
|
+
// pinned by the test suite.
|
|
14
|
+
|
|
15
|
+
/* eslint-disable no-undef, no-unused-vars */
|
|
16
|
+
const __SCHEMA_SQL_TYPES = {
|
|
17
|
+
string: 'VARCHAR', text: 'TEXT', integer: 'INTEGER', number: 'DOUBLE',
|
|
18
|
+
boolean: 'BOOLEAN', date: 'DATE', datetime: 'TIMESTAMP', email: 'VARCHAR',
|
|
19
|
+
url: 'VARCHAR', uuid: 'UUID', phone: 'VARCHAR', zip: 'VARCHAR', json: 'JSON', any: 'JSON',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function __schemaToSQL(def, options) {
|
|
23
|
+
const opts = options || {};
|
|
24
|
+
const { dropFirst = false, header } = opts;
|
|
25
|
+
const norm = def._normalize();
|
|
26
|
+
const blocks = [];
|
|
27
|
+
if (header) blocks.push(header);
|
|
28
|
+
|
|
29
|
+
const table = norm.tableName;
|
|
30
|
+
const seq = table + '_seq';
|
|
31
|
+
if (dropFirst) {
|
|
32
|
+
blocks.push('DROP TABLE IF EXISTS ' + table + ' CASCADE;\nDROP SEQUENCE IF EXISTS ' + seq + ';');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Sequence seed: explicit option wins over @idStart directive wins over 1.
|
|
36
|
+
// DuckDB 1.5.2 does not implement ALTER SEQUENCE ... RESTART WITH N, so the
|
|
37
|
+
// baseline has to be set at creation — hence the knob lives here, not in a
|
|
38
|
+
// post-create migration.
|
|
39
|
+
let idStart = 1;
|
|
40
|
+
for (const d of norm.directives) {
|
|
41
|
+
if (d.name === 'idStart' && d.args?.[0] && Number.isInteger(d.args[0].value)) {
|
|
42
|
+
idStart = d.args[0].value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (opts.idStart !== undefined) {
|
|
46
|
+
if (!Number.isInteger(opts.idStart)) {
|
|
47
|
+
throw new Error('schema.toSQL(): idStart must be an integer; got ' + String(opts.idStart));
|
|
48
|
+
}
|
|
49
|
+
idStart = opts.idStart;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const columns = [];
|
|
53
|
+
const indexes = [];
|
|
54
|
+
columns.push(' ' + norm.primaryKey + " INTEGER PRIMARY KEY DEFAULT nextval('" + seq + "')");
|
|
55
|
+
|
|
56
|
+
for (const [n, f] of norm.fields) {
|
|
57
|
+
columns.push(__schemaColumnDDL(n, f));
|
|
58
|
+
if (f.unique) {
|
|
59
|
+
indexes.push('CREATE UNIQUE INDEX idx_' + table + '_' + __schemaSnake(n) + ' ON ' + table + ' ("' + __schemaSnake(n) + '");');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [, rel] of norm.relations) {
|
|
64
|
+
if (rel.kind !== 'belongsTo') continue;
|
|
65
|
+
const refTable = __schemaTableName(rel.target);
|
|
66
|
+
const notNull = rel.optional ? '' : ' NOT NULL';
|
|
67
|
+
columns.push(' ' + rel.foreignKey + ' INTEGER' + notNull + ' REFERENCES ' + refTable + '(id)');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (norm.timestamps) {
|
|
71
|
+
columns.push(' created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
|
|
72
|
+
columns.push(' updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
|
|
73
|
+
}
|
|
74
|
+
if (norm.softDelete) {
|
|
75
|
+
columns.push(' deleted_at TIMESTAMP');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// @index directives
|
|
79
|
+
for (const d of norm.directives) {
|
|
80
|
+
if (d.name !== 'index') continue;
|
|
81
|
+
const ixArgs = d.args?.[0] || {};
|
|
82
|
+
const fields = (ixArgs.fields || []).map(__schemaSnake);
|
|
83
|
+
if (!fields.length) continue;
|
|
84
|
+
const u = ixArgs.unique ? 'UNIQUE ' : '';
|
|
85
|
+
indexes.push('CREATE ' + u + 'INDEX idx_' + table + '_' + fields.join('_') + ' ON ' + table + ' (' + fields.map(f => '"' + f + '"').join(', ') + ');');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
blocks.push('CREATE SEQUENCE ' + seq + ' START ' + idStart + ';');
|
|
89
|
+
blocks.push('CREATE TABLE ' + table + ' (\n' + columns.join(',\n') + '\n);');
|
|
90
|
+
if (indexes.length) blocks.push(indexes.join('\n'));
|
|
91
|
+
|
|
92
|
+
return blocks.join('\n\n') + '\n';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function __schemaColumnDDL(name, field) {
|
|
96
|
+
let base = __SCHEMA_SQL_TYPES[field.typeName] || 'VARCHAR';
|
|
97
|
+
if (field.array) base = 'JSON';
|
|
98
|
+
if (base === 'VARCHAR' && field.constraints?.max != null) {
|
|
99
|
+
base = 'VARCHAR(' + field.constraints.max + ')';
|
|
100
|
+
}
|
|
101
|
+
const parts = [' ' + __schemaSnake(name) + ' ' + base];
|
|
102
|
+
if (field.required) parts.push('NOT NULL');
|
|
103
|
+
if (field.unique) parts.push('UNIQUE');
|
|
104
|
+
if (field.constraints?.default !== undefined) {
|
|
105
|
+
parts.push('DEFAULT ' + __schemaSQLDefault(field.constraints.default));
|
|
106
|
+
}
|
|
107
|
+
return parts.join(' ');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function __schemaSQLDefault(v) {
|
|
111
|
+
if (v === true) return 'true';
|
|
112
|
+
if (v === false) return 'false';
|
|
113
|
+
if (v === null) return 'NULL';
|
|
114
|
+
if (typeof v === 'number') return String(v);
|
|
115
|
+
if (typeof v === 'string') return "'" + v.replace(/'/g, "''") + "'";
|
|
116
|
+
return "'" + String(v).replace(/'/g, "''") + "'";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// DDL prototype augmentation — added to __SchemaDef
|
|
120
|
+
|
|
121
|
+
__SchemaDef.prototype.toSQL = function (options) {
|
|
122
|
+
this._assertModel('toSQL');
|
|
123
|
+
return __schemaToSQL(this, options);
|
|
124
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Schema runtime fragment: orm (server + migration)
|
|
2
|
+
//
|
|
3
|
+
// This file is the source of truth for one slice of the schema runtime.
|
|
4
|
+
// Edit here, then run `bun run build:schema-runtime` to regenerate
|
|
5
|
+
// `src/schema/runtime.generated.js`. Tests pin the public surface via
|
|
6
|
+
// test/schema/errors.test.js, test/schema/modes.test.js, and the source
|
|
7
|
+
// schema test suite.
|
|
8
|
+
//
|
|
9
|
+
// Fragments are concatenated INSIDE one shared IIFE wrapper at build time.
|
|
10
|
+
// They share scope; references like `__SchemaRegistry` resolve to bindings
|
|
11
|
+
// defined in earlier-included fragments. Editor tooling (LSP / lint) may
|
|
12
|
+
// not recognize cross-fragment references — that is expected; behavior is
|
|
13
|
+
// pinned by the test suite.
|
|
14
|
+
|
|
15
|
+
/* eslint-disable no-undef, no-unused-vars */
|
|
16
|
+
function __schemaDefaultAdapter() {
|
|
17
|
+
const url = (typeof process !== 'undefined' && process.env?.DB_URL) || 'http://localhost:4213';
|
|
18
|
+
return {
|
|
19
|
+
async query(sql, params) {
|
|
20
|
+
const body = params && params.length ? { sql, params } : { sql };
|
|
21
|
+
const res = await fetch(url + '/sql', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify(body),
|
|
25
|
+
});
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
if (data.error) throw new Error(data.error);
|
|
28
|
+
return data;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let __schemaAdapter = __schemaDefaultAdapter();
|
|
34
|
+
|
|
35
|
+
function __schemaSetAdapter(a) { __schemaAdapter = a; }
|
|
36
|
+
|
|
37
|
+
class __SchemaQuery {
|
|
38
|
+
constructor(def, opts = {}) {
|
|
39
|
+
this._def = def;
|
|
40
|
+
this._clauses = [];
|
|
41
|
+
this._params = [];
|
|
42
|
+
this._limit = null;
|
|
43
|
+
this._offset = null;
|
|
44
|
+
this._order = null;
|
|
45
|
+
this._includeDeleted = opts.includeDeleted === true;
|
|
46
|
+
}
|
|
47
|
+
where(cond, ...params) {
|
|
48
|
+
if (typeof cond === 'string') {
|
|
49
|
+
this._clauses.push(cond);
|
|
50
|
+
this._params.push(...params);
|
|
51
|
+
} else if (cond && typeof cond === 'object') {
|
|
52
|
+
for (const [k, v] of Object.entries(cond)) {
|
|
53
|
+
const col = __schemaSnake(k);
|
|
54
|
+
if (v === null || v === undefined) {
|
|
55
|
+
this._clauses.push('"' + col + '" IS NULL');
|
|
56
|
+
} else {
|
|
57
|
+
this._clauses.push('"' + col + '" = ?');
|
|
58
|
+
this._params.push(v);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
limit(n) { this._limit = n; return this; }
|
|
65
|
+
offset(n) { this._offset = n; return this; }
|
|
66
|
+
order(spec) { this._order = spec; return this; }
|
|
67
|
+
orderBy(spec) { return this.order(spec); }
|
|
68
|
+
_buildSQL() {
|
|
69
|
+
const n = this._def._normalize();
|
|
70
|
+
const table = n.tableName;
|
|
71
|
+
const parts = ['SELECT * FROM "' + table + '"'];
|
|
72
|
+
const where = [...this._clauses];
|
|
73
|
+
if (!this._includeDeleted && n.softDelete) where.push('"deleted_at" IS NULL');
|
|
74
|
+
if (where.length) parts.push('WHERE ' + where.join(' AND '));
|
|
75
|
+
if (this._order) parts.push('ORDER BY ' + this._order);
|
|
76
|
+
if (this._limit != null) parts.push('LIMIT ' + this._limit);
|
|
77
|
+
if (this._offset != null) parts.push('OFFSET ' + this._offset);
|
|
78
|
+
return parts.join(' ');
|
|
79
|
+
}
|
|
80
|
+
async all() {
|
|
81
|
+
const sql = this._buildSQL();
|
|
82
|
+
const res = await __schemaAdapter.query(sql, this._params);
|
|
83
|
+
return (res.data || []).map(row => this._def._hydrate(res.columns, row));
|
|
84
|
+
}
|
|
85
|
+
async first() {
|
|
86
|
+
this._limit = 1;
|
|
87
|
+
const arr = await this.all();
|
|
88
|
+
return arr[0] || null;
|
|
89
|
+
}
|
|
90
|
+
async count() {
|
|
91
|
+
const n = this._def._normalize();
|
|
92
|
+
const parts = ['SELECT COUNT(*) FROM "' + n.tableName + '"'];
|
|
93
|
+
const where = [...this._clauses];
|
|
94
|
+
if (!this._includeDeleted && n.softDelete) where.push('"deleted_at" IS NULL');
|
|
95
|
+
if (where.length) parts.push('WHERE ' + where.join(' AND '));
|
|
96
|
+
const res = await __schemaAdapter.query(parts.join(' '), this._params);
|
|
97
|
+
return res.data?.[0]?.[0] || 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function __schemaResolveRelation(def, inst, rel) {
|
|
102
|
+
const target = __SchemaRegistry.get(rel.target);
|
|
103
|
+
if (!target) throw new Error('schema: unknown relation target "' + rel.target + '" from ' + (def.name || 'anon'));
|
|
104
|
+
const pk = def._normalize().primaryKey;
|
|
105
|
+
if (rel.kind === 'belongsTo') {
|
|
106
|
+
const fk = inst[__schemaCamel(rel.foreignKey)];
|
|
107
|
+
return fk != null ? await target.find(fk) : null;
|
|
108
|
+
}
|
|
109
|
+
if (rel.kind === 'hasOne') {
|
|
110
|
+
return await target.where({ [rel.foreignKey]: inst[pk] }).first();
|
|
111
|
+
}
|
|
112
|
+
if (rel.kind === 'hasMany') {
|
|
113
|
+
return await target.where({ [rel.foreignKey]: inst[pk] }).all();
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function __schemaRunHook(def, inst, name) {
|
|
119
|
+
const fn = def._normalize().hooks.get(name);
|
|
120
|
+
if (fn) await fn.call(inst);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function __schemaSave(def, inst) {
|
|
124
|
+
const norm = def._normalize();
|
|
125
|
+
const isNew = !inst._persisted;
|
|
126
|
+
|
|
127
|
+
await __schemaRunHook(def, inst, 'beforeValidation');
|
|
128
|
+
const errs = def._validateFields(inst, true);
|
|
129
|
+
if (errs.length) throw new SchemaError(errs, def.name, def.kind);
|
|
130
|
+
await __schemaRunHook(def, inst, 'afterValidation');
|
|
131
|
+
|
|
132
|
+
await __schemaRunHook(def, inst, 'beforeSave');
|
|
133
|
+
if (isNew) await __schemaRunHook(def, inst, 'beforeCreate');
|
|
134
|
+
else await __schemaRunHook(def, inst, 'beforeUpdate');
|
|
135
|
+
|
|
136
|
+
if (isNew) {
|
|
137
|
+
const cols = [], placeholders = [], values = [];
|
|
138
|
+
for (const [n, f] of norm.fields) {
|
|
139
|
+
const v = inst[n];
|
|
140
|
+
if (v == null) continue;
|
|
141
|
+
cols.push('"' + __schemaSnake(n) + '"');
|
|
142
|
+
placeholders.push('?');
|
|
143
|
+
values.push(__schemaSerialize(v, f));
|
|
144
|
+
}
|
|
145
|
+
// Include relation FKs. belongsTo FKs are camelCase properties on
|
|
146
|
+
// the instance (e.g. organizationId for organization_id).
|
|
147
|
+
for (const [, rel] of norm.relations) {
|
|
148
|
+
if (rel.kind !== 'belongsTo') continue;
|
|
149
|
+
const fkCamel = __schemaCamel(rel.foreignKey);
|
|
150
|
+
const v = inst[fkCamel];
|
|
151
|
+
if (v != null) {
|
|
152
|
+
cols.push('"' + rel.foreignKey + '"');
|
|
153
|
+
placeholders.push('?');
|
|
154
|
+
values.push(v);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const sql = 'INSERT INTO "' + norm.tableName + '" (' + cols.join(', ') + ') VALUES (' + placeholders.join(', ') + ') RETURNING *';
|
|
158
|
+
const res = await __schemaAdapter.query(sql, values);
|
|
159
|
+
if (res.data?.[0] && res.columns) {
|
|
160
|
+
for (let i = 0; i < res.columns.length; i++) {
|
|
161
|
+
const snake = res.columns[i].name;
|
|
162
|
+
const key = __schemaCamel(snake);
|
|
163
|
+
if (!(key in inst)) {
|
|
164
|
+
Object.defineProperty(inst, key, { value: res.data[0][i], enumerable: true, writable: true, configurable: true });
|
|
165
|
+
} else {
|
|
166
|
+
inst[key] = res.data[0][i];
|
|
167
|
+
}
|
|
168
|
+
if (snake !== key && !(snake in inst)) {
|
|
169
|
+
Object.defineProperty(inst, snake, {
|
|
170
|
+
enumerable: false, configurable: true,
|
|
171
|
+
get() { return this[key]; },
|
|
172
|
+
set(v) { this[key] = v; },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Now that the RETURNING columns (id, @timestamps, FKs) are on the
|
|
178
|
+
// instance, !> eager-derived fields can see them. Mirrors the hydrate
|
|
179
|
+
// path, which runs _applyEagerDerived once all declared fields are
|
|
180
|
+
// populated. Per-docs semantics ("materialize once, not reactive")
|
|
181
|
+
// still hold — we're firing once, at end of construction, not on
|
|
182
|
+
// subsequent mutations.
|
|
183
|
+
def._applyEagerDerived(inst);
|
|
184
|
+
inst._persisted = true;
|
|
185
|
+
} else {
|
|
186
|
+
const sets = [], values = [];
|
|
187
|
+
for (const [n, f] of norm.fields) {
|
|
188
|
+
sets.push('"' + __schemaSnake(n) + '" = ?');
|
|
189
|
+
values.push(__schemaSerialize(inst[n], f));
|
|
190
|
+
}
|
|
191
|
+
if (sets.length) {
|
|
192
|
+
const pk = norm.primaryKey;
|
|
193
|
+
values.push(inst[pk]);
|
|
194
|
+
const sql = 'UPDATE "' + norm.tableName + '" SET ' + sets.join(', ') + ' WHERE "' + pk + '" = ?';
|
|
195
|
+
await __schemaAdapter.query(sql, values);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
inst._dirty.clear();
|
|
199
|
+
|
|
200
|
+
if (isNew) await __schemaRunHook(def, inst, 'afterCreate');
|
|
201
|
+
else await __schemaRunHook(def, inst, 'afterUpdate');
|
|
202
|
+
await __schemaRunHook(def, inst, 'afterSave');
|
|
203
|
+
return inst;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function __schemaDestroy(def, inst) {
|
|
207
|
+
if (!inst._persisted) return inst;
|
|
208
|
+
const norm = def._normalize();
|
|
209
|
+
await __schemaRunHook(def, inst, 'beforeDestroy');
|
|
210
|
+
if (norm.softDelete) {
|
|
211
|
+
const now = new Date().toISOString();
|
|
212
|
+
await __schemaAdapter.query('UPDATE "' + norm.tableName + '" SET "deleted_at" = ? WHERE "' + norm.primaryKey + '" = ?', [now, inst[norm.primaryKey]]);
|
|
213
|
+
inst.deletedAt = now;
|
|
214
|
+
} else {
|
|
215
|
+
await __schemaAdapter.query('DELETE FROM "' + norm.tableName + '" WHERE "' + norm.primaryKey + '" = ?', [inst[norm.primaryKey]]);
|
|
216
|
+
inst._persisted = false;
|
|
217
|
+
}
|
|
218
|
+
await __schemaRunHook(def, inst, 'afterDestroy');
|
|
219
|
+
return inst;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function __schemaSerialize(v, field) {
|
|
223
|
+
if (field && field.typeName === 'json' && v != null && typeof v === 'object') {
|
|
224
|
+
return JSON.stringify(v);
|
|
225
|
+
}
|
|
226
|
+
return v;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ORM prototype augmentations — added to __SchemaDef
|
|
230
|
+
|
|
231
|
+
__SchemaDef.prototype.find = async function (id) {
|
|
232
|
+
this._assertModel('find');
|
|
233
|
+
const norm = this._normalize();
|
|
234
|
+
const soft = norm.softDelete ? ' AND "deleted_at" IS NULL' : '';
|
|
235
|
+
const sql = 'SELECT * FROM "' + norm.tableName + '" WHERE "' + norm.primaryKey + '" = ?' + soft + ' LIMIT 1';
|
|
236
|
+
const res = await __schemaAdapter.query(sql, [id]);
|
|
237
|
+
if (!res.rows) return null;
|
|
238
|
+
return this._hydrate(res.columns, res.data[0]);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
__SchemaDef.prototype.where = function (cond, ...params) {
|
|
242
|
+
this._assertModel('where');
|
|
243
|
+
return new __SchemaQuery(this).where(cond, ...params);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
__SchemaDef.prototype.all = function () {
|
|
247
|
+
this._assertModel('all');
|
|
248
|
+
return new __SchemaQuery(this).all();
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
__SchemaDef.prototype.first = function () {
|
|
252
|
+
this._assertModel('first');
|
|
253
|
+
return new __SchemaQuery(this).first();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
__SchemaDef.prototype.count = function () {
|
|
257
|
+
this._assertModel('count');
|
|
258
|
+
return new __SchemaQuery(this).count();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
__SchemaDef.prototype.create = async function (data) {
|
|
262
|
+
this._assertModel('create');
|
|
263
|
+
// Input keys may be snake_case or camelCase; the runtime
|
|
264
|
+
// canonicalizes to camelCase so instance properties line up with
|
|
265
|
+
// declared field names.
|
|
266
|
+
const klass = this._getClass();
|
|
267
|
+
const canonical = {};
|
|
268
|
+
if (data && typeof data === 'object') {
|
|
269
|
+
for (const k of Object.keys(data)) canonical[__schemaCamel(k)] = data[k];
|
|
270
|
+
}
|
|
271
|
+
const inst = new klass(this._applyDefaults(canonical), false);
|
|
272
|
+
// FK columns like user_id canonicalize to userId and need to
|
|
273
|
+
// round-trip through the INSERT path, so attach them as own
|
|
274
|
+
// properties even though they aren't declared fields.
|
|
275
|
+
for (const [k, v] of Object.entries(canonical)) {
|
|
276
|
+
if (!(k in inst)) {
|
|
277
|
+
Object.defineProperty(inst, k, { value: v, enumerable: true, writable: true, configurable: true });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
await __schemaSave(this, inst);
|
|
281
|
+
return inst;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
__SchemaDef.prototype._assertModel = function (api) {
|
|
285
|
+
if (this.kind !== 'model') {
|
|
286
|
+
throw new Error('schema: .' + api + '() is :model-only (got :' + this.kind + ')');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---- Schema algebra (Phase 6) --------------------------------------------
|
|
291
|
+
// Invariant: every algebra operation returns a :shape. Model algebra
|
|
292
|
+
// strips ORM; :shape algebra drops behavior. Derived shapes preserve
|
|
293
|
+
// field metadata (constraints, defaults, modifiers) from the source
|
|
294
|
+
// normalized descriptor.
|