@jarenjs/linq 0.49.2 → 0.56.0
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/ARCHITECTURE.md +217 -0
- package/README.md +559 -17
- package/docs/APP-PEN.md +1143 -0
- package/docs/CONTRACT-PEN.md +1217 -0
- package/docs/DB-CLIENT.md +814 -0
- package/docs/FLOW-PEN.md +1026 -0
- package/docs/FORMS-PEN.md +940 -0
- package/docs/JSLT-PEN.md +955 -0
- package/docs/LINQ-FORMAT.md +771 -383
- package/docs/MIGRATION-PEN.md +781 -0
- package/docs/MODEL-PEN.md +1083 -0
- package/docs/QUERY-PEN.md +1636 -0
- package/docs/SCHEMA-PEN.md +1218 -0
- package/package.json +57 -4
- package/src/app/action.js +255 -0
- package/src/app/capture.js +63 -0
- package/src/app/define.js +260 -0
- package/src/app/index.js +20 -0
- package/src/app/patch.js +277 -0
- package/src/app/sub.js +106 -0
- package/src/async.js +329 -75
- package/src/capture-root.js +82 -0
- package/src/concurrency.js +9 -4
- package/src/contract/define.js +269 -0
- package/src/contract/http.js +247 -0
- package/src/contract/index.js +23 -0
- package/src/contract/operation.js +342 -0
- package/src/db/handle.js +86 -0
- package/src/db/include.js +316 -0
- package/src/db/index.js +19 -0
- package/src/db/live.js +43 -0
- package/src/db/membership.js +37 -0
- package/src/db/open.js +82 -0
- package/src/document.js +143 -13
- package/src/effect.js +65 -0
- package/src/errors.js +69 -6
- package/src/expression.js +437 -36
- package/src/flow/capture.js +33 -0
- package/src/flow/dag.js +302 -0
- package/src/flow/fsm.js +328 -0
- package/src/flow/index.js +22 -0
- package/src/forms/index.js +43 -0
- package/src/forms/rules.js +170 -0
- package/src/forms/submit.js +177 -0
- package/src/index.js +4 -2
- package/src/jslt/body.js +226 -0
- package/src/jslt/index.js +18 -0
- package/src/jslt/rules.js +207 -0
- package/src/json-boundary.js +90 -0
- package/src/migration/define.js +323 -0
- package/src/migration/index.js +15 -0
- package/src/migration/steps.js +248 -0
- package/src/model/collection.js +171 -0
- package/src/model/define.js +125 -0
- package/src/model/entity.js +307 -0
- package/src/model/index.js +47 -0
- package/src/model/relation.js +85 -0
- package/src/provider.js +137 -20
- package/src/schema/brand.js +31 -0
- package/src/schema/builders.js +526 -0
- package/src/schema/check.js +29 -0
- package/src/schema/emit.js +394 -0
- package/src/schema/factories.js +239 -0
- package/src/schema/index.js +37 -0
- package/src/schema-of.js +24 -0
- package/src/sequence.js +233 -103
- package/src/sources.js +10 -3
- package/types/app.d.ts +293 -0
- package/types/contract.d.ts +371 -0
- package/types/db.d.ts +188 -0
- package/types/flow.d.ts +285 -0
- package/types/forms.d.ts +253 -0
- package/types/index.d.ts +231 -26
- package/types/jslt.d.ts +193 -0
- package/types/migration.d.ts +201 -0
- package/types/model.d.ts +493 -0
- package/types/schema.d.ts +494 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The step spellings of a `$migration` 0.1 document
|
|
4
|
+
* (MIGRATION-FORMAT §2): each function writes one step as plain JSON in
|
|
5
|
+
* the member order the format's examples use, and refuses only what it
|
|
6
|
+
* cannot spell (`JL0101`) or what the runner's own structural check would
|
|
7
|
+
* refuse later and the pen can see now (`JL0101`, mirrored from the
|
|
8
|
+
* runner's `JD0023` rules: a `sql` step without text, a `derive` without
|
|
9
|
+
* columns, a `rebuild` without its rendered parts, an unknown kind). A
|
|
10
|
+
* transform's callback is captured through the JSLT pen's `body()` — the
|
|
11
|
+
* capture the format's `jslt` step runs — as one root rule; an
|
|
12
|
+
* assertion's predicate through the chain's recording proxy, rooted at
|
|
13
|
+
* `$it` inside the `$for` the format's own example writes.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { captureExpression } from '../expression.js';
|
|
17
|
+
import { body } from '../jslt/body.js';
|
|
18
|
+
import { describeValue, requireJson } from '../json-boundary.js';
|
|
19
|
+
import { LinqBuildError } from '../errors.js';
|
|
20
|
+
|
|
21
|
+
/** The kinds the runner accepts, in the artifact's order. */
|
|
22
|
+
const STEP_KINDS = ['ddl', 'jslt', 'query', 'derive', 'sql', 'rebuild'];
|
|
23
|
+
const EXPECTS = ['empty', 'ebv'];
|
|
24
|
+
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25
|
+
/** An assertion evaluates with no externals: `p.x` cannot appear. */
|
|
26
|
+
const NO_PARAMS = new Set();
|
|
27
|
+
|
|
28
|
+
/** A JSON value, copied: the document is a value of its own. @param {any} v */
|
|
29
|
+
const copy = (v) => JSON.parse(JSON.stringify(v));
|
|
30
|
+
|
|
31
|
+
/** @param {any} value */
|
|
32
|
+
function isPlainObject(value) {
|
|
33
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A table name: an identifier, as the artifact's pattern admits.
|
|
38
|
+
* @param {any} name
|
|
39
|
+
* @param {string} what
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function requireName(name, what) {
|
|
43
|
+
if (typeof name !== 'string' || !NAME_RE.test(name)) {
|
|
44
|
+
throw new LinqBuildError('JL0101',
|
|
45
|
+
`${what} names an entity or collection by identifier ('users'), got ${describeValue(name)}`);
|
|
46
|
+
}
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {any} sql @param {string} what */
|
|
51
|
+
function requireSql(sql, what) {
|
|
52
|
+
if (typeof sql !== 'string' || sql.trim() === '') {
|
|
53
|
+
throw new LinqBuildError('JL0101',
|
|
54
|
+
`${what} takes one rendered SQL statement as a non-empty string, got ${describeValue(sql)}`);
|
|
55
|
+
}
|
|
56
|
+
return sql;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** @param {any} note @param {string} what */
|
|
60
|
+
function readNote(note, what) {
|
|
61
|
+
if (note === undefined) return undefined;
|
|
62
|
+
if (typeof note !== 'string') {
|
|
63
|
+
throw new LinqBuildError('JL0101', `${what} note is a string, got ${describeValue(note)}`);
|
|
64
|
+
}
|
|
65
|
+
return note;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The externals proxy an assertion's predicate sees: nothing. */
|
|
69
|
+
const NO_EXTERNALS = new Proxy(Object.freeze({}), {
|
|
70
|
+
get(_target, prop) {
|
|
71
|
+
if (typeof prop === 'symbol') return undefined;
|
|
72
|
+
throw new LinqBuildError('JL0104',
|
|
73
|
+
`an assert() predicate cannot bind '${String(prop)}' — a query assertion runs over the `
|
|
74
|
+
+ "table's rows with no externals (MIGRATION-FORMAT §2)");
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `{ kind: 'ddl', sql, note? }` — one rendered DDL statement.
|
|
80
|
+
* @param {string} sql @param {string} [note]
|
|
81
|
+
*/
|
|
82
|
+
export function ddlStep(sql, note = undefined) {
|
|
83
|
+
const out = { kind: 'ddl', sql: requireSql(sql, 'ddl()') };
|
|
84
|
+
const n = readNote(note, 'ddl()');
|
|
85
|
+
if (n !== undefined) out.note = n;
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* `{ kind: 'sql', sql, note? }` — one DATA statement spelled directly
|
|
91
|
+
* (§9.4); a dry run always prints it with its note.
|
|
92
|
+
* @param {string} sql @param {string} [note]
|
|
93
|
+
*/
|
|
94
|
+
export function sqlStep(sql, note = undefined) {
|
|
95
|
+
const out = { kind: 'sql', sql: requireSql(sql, 'sql()') };
|
|
96
|
+
const n = readNote(note, 'sql()');
|
|
97
|
+
if (n !== undefined) out.note = n;
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `{ kind: 'jslt', collection, stylesheet }` — the transform of one
|
|
103
|
+
* table's rows. The spelling is a callback (one root rule, `match: '$'`,
|
|
104
|
+
* its body captured over the whole row with `root`/`path` as the
|
|
105
|
+
* externals the engine binds), a `stylesheet(…)` envelope (its rules —
|
|
106
|
+
* the step carries the rules array, so a disposition or a mode table
|
|
107
|
+
* has no place in it, `JL0102`), or a rules array verbatim.
|
|
108
|
+
* @param {string} name
|
|
109
|
+
* @param {any} spelling
|
|
110
|
+
*/
|
|
111
|
+
export function transformStep(name, spelling) {
|
|
112
|
+
requireName(name, 'transform()');
|
|
113
|
+
let stylesheet;
|
|
114
|
+
if (typeof spelling === 'function') {
|
|
115
|
+
stylesheet = [{ match: '$', body: body(spelling) }];
|
|
116
|
+
}
|
|
117
|
+
else if (Array.isArray(spelling)) {
|
|
118
|
+
stylesheet = copy(requireJson(spelling, 'transform() rules'));
|
|
119
|
+
}
|
|
120
|
+
else if (isPlainObject(spelling) && spelling.$jslt === '0.1') {
|
|
121
|
+
for (const key of Object.keys(spelling)) {
|
|
122
|
+
if (key !== '$jslt' && key !== 'rules') {
|
|
123
|
+
throw new LinqBuildError('JL0102',
|
|
124
|
+
`transform() takes a stylesheet's rules — a jslt step carries the rules array `
|
|
125
|
+
+ `(MIGRATION-FORMAT §2), so '${key}' has no place in it; write the rules without it`,
|
|
126
|
+
`/${key}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
stylesheet = copy(requireJson(spelling.rules, 'transform() stylesheet rules'));
|
|
130
|
+
if (!Array.isArray(stylesheet)) {
|
|
131
|
+
throw new LinqBuildError('JL0101',
|
|
132
|
+
`transform() stylesheet rules are an array, got ${describeValue(spelling.rules)}`, '/rules');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
throw new LinqBuildError('JL0101',
|
|
137
|
+
'transform() takes a callback (row, x) => …, a stylesheet(…) document or a rules array, '
|
|
138
|
+
+ `got ${describeValue(spelling)}`);
|
|
139
|
+
}
|
|
140
|
+
return { kind: 'jslt', collection: name, stylesheet };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* `{ kind: 'query', collection, assert, expect? }` — an assertion over
|
|
145
|
+
* the table's rows. A callback names a predicate over one row, spelled
|
|
146
|
+
* as the format's own `$for` over the rows: with `expect: 'empty'` (the
|
|
147
|
+
* default, absent from the document) no row may satisfy it — the
|
|
148
|
+
* predicate names the VIOLATION; with `expect: 'ebv'` the matching rows
|
|
149
|
+
* are the witness. A document is taken verbatim.
|
|
150
|
+
* @param {string} name
|
|
151
|
+
* @param {any} spelling
|
|
152
|
+
* @param {{ expect?: 'empty' | 'ebv' }} [options]
|
|
153
|
+
*/
|
|
154
|
+
export function assertStep(name, spelling, options = undefined) {
|
|
155
|
+
requireName(name, 'assert()');
|
|
156
|
+
let expect;
|
|
157
|
+
if (options !== undefined) {
|
|
158
|
+
if (!isPlainObject(options)) {
|
|
159
|
+
throw new LinqBuildError('JL0101', `assert() options are { expect? }, got ${describeValue(options)}`);
|
|
160
|
+
}
|
|
161
|
+
for (const key of Object.keys(options)) {
|
|
162
|
+
if (key !== 'expect') throw new LinqBuildError('JL0101', `assert() does not take '${key}'`);
|
|
163
|
+
}
|
|
164
|
+
if (options.expect !== undefined) {
|
|
165
|
+
if (!EXPECTS.includes(options.expect)) {
|
|
166
|
+
throw new LinqBuildError('JL0101',
|
|
167
|
+
`assert() expect is 'empty' or 'ebv' (MIGRATION-FORMAT §2), got ${describeValue(options.expect)}`,
|
|
168
|
+
'/expect');
|
|
169
|
+
}
|
|
170
|
+
expect = options.expect;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let query;
|
|
174
|
+
if (typeof spelling === 'function') {
|
|
175
|
+
const predicate = captureExpression((it) => spelling(it, NO_EXTERNALS), ['it'], NO_PARAMS);
|
|
176
|
+
query = { $for: { it: '$[*]' }, $where: predicate, $return: '$it' };
|
|
177
|
+
}
|
|
178
|
+
else if (spelling !== undefined) {
|
|
179
|
+
query = copy(requireJson(spelling, 'assert() query'));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
throw new LinqBuildError('JL0101',
|
|
183
|
+
'assert() takes a predicate (row) => … or a query document over the rows');
|
|
184
|
+
}
|
|
185
|
+
const out = { kind: 'query', collection: name, assert: query };
|
|
186
|
+
if (expect === 'ebv') out.expect = 'ebv';
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* `{ kind: 'derive', collection, columns }` — recompute stored derived
|
|
192
|
+
* columns (§2.1); the columns ride verbatim, a non-empty array.
|
|
193
|
+
* @param {string} name
|
|
194
|
+
* @param {any} columns
|
|
195
|
+
*/
|
|
196
|
+
export function deriveStep(name, columns) {
|
|
197
|
+
requireName(name, 'derive()');
|
|
198
|
+
if (!Array.isArray(columns) || columns.length === 0) {
|
|
199
|
+
throw new LinqBuildError('JL0101',
|
|
200
|
+
`derive() takes a non-empty array of derived-column records ({ name, derive, segments }), got ${describeValue(columns)}`);
|
|
201
|
+
}
|
|
202
|
+
return { kind: 'derive', collection: name, columns: copy(requireJson(columns, 'derive() columns')) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Any planner-emitted step, verbatim — the escape that keeps `rebuild`
|
|
207
|
+
* authorable without the pen re-implementing §10. The structural rules
|
|
208
|
+
* are the runner's own (`JD0023`), seen here: a recognised kind and the
|
|
209
|
+
* members that kind requires. A `draft` flag rides untouched.
|
|
210
|
+
* @param {any} step
|
|
211
|
+
*/
|
|
212
|
+
export function rawStep(step) {
|
|
213
|
+
const raw = copy(requireJson(step, 'step()'));
|
|
214
|
+
if (!isPlainObject(raw) || !STEP_KINDS.includes(raw.kind)) {
|
|
215
|
+
throw new LinqBuildError('JL0101',
|
|
216
|
+
`step() takes a migration step with a recognised kind (${STEP_KINDS.join(', ')}), got `
|
|
217
|
+
+ `${isPlainObject(raw) ? `kind ${describeValue(raw.kind)}` : describeValue(step)}`);
|
|
218
|
+
}
|
|
219
|
+
const need = (member, ok) => {
|
|
220
|
+
if (!ok) {
|
|
221
|
+
throw new LinqBuildError('JL0101',
|
|
222
|
+
`step() '${raw.kind}' needs '${member}' (MIGRATION-FORMAT §2)`, `/${member}`);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
switch (raw.kind) {
|
|
226
|
+
case 'ddl': case 'sql':
|
|
227
|
+
need('sql', typeof raw.sql === 'string' && raw.sql !== '');
|
|
228
|
+
break;
|
|
229
|
+
case 'jslt':
|
|
230
|
+
need('collection', typeof raw.collection === 'string');
|
|
231
|
+
need('stylesheet', Array.isArray(raw.stylesheet));
|
|
232
|
+
break;
|
|
233
|
+
case 'query':
|
|
234
|
+
need('collection', typeof raw.collection === 'string');
|
|
235
|
+
need('assert', raw.assert !== undefined);
|
|
236
|
+
break;
|
|
237
|
+
case 'derive':
|
|
238
|
+
need('collection', typeof raw.collection === 'string');
|
|
239
|
+
need('columns', Array.isArray(raw.columns) && raw.columns.length > 0);
|
|
240
|
+
break;
|
|
241
|
+
default: // rebuild
|
|
242
|
+
need('table', typeof raw.table === 'string');
|
|
243
|
+
need('create', Array.isArray(raw.create) && raw.create.length > 0);
|
|
244
|
+
need('copy', typeof raw.copy === 'string');
|
|
245
|
+
need('indexes', Array.isArray(raw.indexes));
|
|
246
|
+
}
|
|
247
|
+
return raw;
|
|
248
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Collections and their indexes: `collection(builder, { key,
|
|
4
|
+
* indexes })` and `index(path, options)`. A key is an RFC 6901 pointer
|
|
5
|
+
* or a captured member path (`(d) => d.id` → `/id`); an index path is a
|
|
6
|
+
* captured lambda (`(p) => p.embedding` → `$.embedding`), a composite
|
|
7
|
+
* array of them, or a JSONPath string; the options ride verbatim, the
|
|
8
|
+
* store's model walk being the judge of `derive`/`precision`/`dims`/
|
|
9
|
+
* `physical`. The default index name is `by_<segments>`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LinqBuildError } from '../errors.js';
|
|
13
|
+
import { isSchemaBuilder } from '../schema/brand.js';
|
|
14
|
+
import { requireJson, requireName } from '../schema/builders.js';
|
|
15
|
+
import { captureQuery } from '../schema/check.js';
|
|
16
|
+
import { DOCUMENT_SCOPE, refuseStrandedRename } from './entity.js';
|
|
17
|
+
|
|
18
|
+
/** The index options the grammar names beside `name` and `path`. */
|
|
19
|
+
const INDEX_OPTIONS = new Set(['name', 'unique', 'derive', 'precision', 'dims', 'physical']);
|
|
20
|
+
|
|
21
|
+
/** The marker a collection spec carries so `defineModel` can tell it apart. */
|
|
22
|
+
export const COLLECTION = Symbol.for('@jarenjs/linq/model-collection');
|
|
23
|
+
|
|
24
|
+
/** A shorthand member path (`$.a.b`), as the capture spells one. */
|
|
25
|
+
const MEMBER_PATH = /^\$(\.[A-Za-z_][A-Za-z0-9_]*)+$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Capture one path lambda into a JSONPath string.
|
|
29
|
+
* @param {(doc: any) => any} lambda
|
|
30
|
+
* @param {string} what
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function capturePath(lambda, what) {
|
|
34
|
+
let captured;
|
|
35
|
+
try {
|
|
36
|
+
captured = captureQuery(what, [], lambda, { advice: () => DOCUMENT_SCOPE });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// a member named like a surface method (`at`, `get`, …) reads as the
|
|
40
|
+
// method, so the lambda answers a function; name the escape
|
|
41
|
+
if (error instanceof LinqBuildError && error.code === 'JL0005' && /function/.test(error.message)) {
|
|
42
|
+
throw new LinqBuildError('JL0102',
|
|
43
|
+
`${what} answered a function, not a path — a member named like a surface method `
|
|
44
|
+
+ "(`at`, `get`, `all`, …) is read with get('name'): (d) => d.get('at')", undefined, error);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
if (typeof captured !== 'string' || captured.charCodeAt(0) !== 0x24) {
|
|
49
|
+
throw new LinqBuildError('JL0102',
|
|
50
|
+
`${what} takes a member path ((d) => d.member); an operator result is not a path`);
|
|
51
|
+
}
|
|
52
|
+
return captured;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A path argument: a captured lambda, a JSONPath string, or an array of
|
|
57
|
+
* either (a composite).
|
|
58
|
+
* @param {any} path
|
|
59
|
+
* @param {string} what
|
|
60
|
+
* @returns {string | string[]}
|
|
61
|
+
*/
|
|
62
|
+
function indexPath(path, what) {
|
|
63
|
+
if (typeof path === 'function') return capturePath(path, what);
|
|
64
|
+
if (typeof path === 'string' && path.length > 0) return path;
|
|
65
|
+
if (Array.isArray(path) && path.length > 0) {
|
|
66
|
+
return path.map((one, i) => {
|
|
67
|
+
if (typeof one === 'function') return capturePath(one, `${what}[${i}]`);
|
|
68
|
+
if (typeof one === 'string' && one.length > 0) return one;
|
|
69
|
+
throw new LinqBuildError('JL0101', `${what}[${i}] is neither a path lambda nor a JSONPath string`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
throw new LinqBuildError('JL0101',
|
|
73
|
+
`${what} takes a path lambda, a JSONPath string, or a non-empty array of them`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The default name: `by_` + the member segments, identifier-safe. @param {string | string[]} path */
|
|
77
|
+
function defaultName(path) {
|
|
78
|
+
const segments = (Array.isArray(path) ? path : [path]).flatMap((one) =>
|
|
79
|
+
one.replace(/^\$\.?/, '').split(/[.[\]'"]+/).filter((s) => s !== '' && s !== '*'));
|
|
80
|
+
return 'by_' + segments.map((s) => s.replace(/[^A-Za-z0-9_]/g, '_')).join('_');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One index declaration.
|
|
85
|
+
* @param {((doc: any) => any) | string | readonly (((doc: any) => any) | string)[]} path
|
|
86
|
+
* @param {{ name?: string, unique?: boolean, derive?: 'geohash' | 'bbox' | 'vector',
|
|
87
|
+
* precision?: number, dims?: number, physical?: 'columns' | 'rtree' }} [options]
|
|
88
|
+
* @returns {any} the frozen index document
|
|
89
|
+
*/
|
|
90
|
+
export function index(path, options = {}) {
|
|
91
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
92
|
+
throw new LinqBuildError('JL0101', 'index() takes an options object');
|
|
93
|
+
}
|
|
94
|
+
for (const key of Object.keys(options)) {
|
|
95
|
+
if (!INDEX_OPTIONS.has(key)) {
|
|
96
|
+
throw new LinqBuildError('JL0101',
|
|
97
|
+
`index() does not take '${key}' — the options are name, unique, derive, precision, dims, physical`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const resolved = indexPath(path, 'index()');
|
|
101
|
+
const out = {
|
|
102
|
+
name: options.name === undefined ? defaultName(resolved) : requireName(options.name, 'index() name'),
|
|
103
|
+
path: resolved,
|
|
104
|
+
};
|
|
105
|
+
for (const key of ['unique', 'derive', 'precision', 'dims', 'physical']) {
|
|
106
|
+
if (options[key] !== undefined) out[key] = requireJson(options[key], `index() ${key}`);
|
|
107
|
+
}
|
|
108
|
+
return Object.freeze(out);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The key declaration: a pointer string, a captured member path, or
|
|
113
|
+
* `null` (the store allocates; `identity` says how).
|
|
114
|
+
* @param {any} key
|
|
115
|
+
* @returns {string | null}
|
|
116
|
+
*/
|
|
117
|
+
function keyPointer(key) {
|
|
118
|
+
if (key === null) return null;
|
|
119
|
+
if (typeof key === 'function') {
|
|
120
|
+
const path = capturePath(key, 'collection() key');
|
|
121
|
+
if (!MEMBER_PATH.test(path)) {
|
|
122
|
+
throw new LinqBuildError('JL0102',
|
|
123
|
+
`collection() key must select members by name ((d) => d.id), got the path ${path}`);
|
|
124
|
+
}
|
|
125
|
+
return '/' + path.slice(2).split('.').map((s) => s.replaceAll('~', '~0').replaceAll('/', '~1')).join('/');
|
|
126
|
+
}
|
|
127
|
+
if (typeof key === 'string' && key.startsWith('/')) return key;
|
|
128
|
+
throw new LinqBuildError('JL0101',
|
|
129
|
+
'collection() key is an RFC 6901 pointer, a member path lambda, or null');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* One collection declaration for `defineModel`.
|
|
134
|
+
* @param {any} builder - the document schema
|
|
135
|
+
* @param {{ key?: string | ((doc: any) => any) | null, identity?: 'caller' | 'uuid' | 'integer',
|
|
136
|
+
* indexes?: readonly any[], renamedFrom?: string }} [options]
|
|
137
|
+
* @returns {any} the frozen collection document
|
|
138
|
+
*/
|
|
139
|
+
export function collection(builder, options = {}) {
|
|
140
|
+
if (!isSchemaBuilder(builder)) {
|
|
141
|
+
throw new LinqBuildError('JL0101', 'collection() takes a schema builder as its document schema');
|
|
142
|
+
}
|
|
143
|
+
refuseStrandedRename(builder, 'collection()');
|
|
144
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
145
|
+
throw new LinqBuildError('JL0101', 'collection() takes an options object');
|
|
146
|
+
}
|
|
147
|
+
for (const key of Object.keys(options)) {
|
|
148
|
+
if (!['key', 'identity', 'indexes', 'renamedFrom'].includes(key)) {
|
|
149
|
+
throw new LinqBuildError('JL0101',
|
|
150
|
+
`collection() does not take '${key}' — the options are key, identity, indexes, renamedFrom`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const out = { schema: builder.schema };
|
|
154
|
+
if (options.key !== undefined) out.key = keyPointer(options.key);
|
|
155
|
+
if (options.identity !== undefined) out.identity = requireJson(options.identity, 'collection() identity');
|
|
156
|
+
if (options.indexes !== undefined) {
|
|
157
|
+
if (!Array.isArray(options.indexes)) {
|
|
158
|
+
throw new LinqBuildError('JL0101', 'collection() indexes is an array of index() entries');
|
|
159
|
+
}
|
|
160
|
+
out.indexes = options.indexes.map((one, i) => {
|
|
161
|
+
if (one === null || typeof one !== 'object' || typeof one.name !== 'string') {
|
|
162
|
+
throw new LinqBuildError('JL0101', `collection() indexes[${i}] is not an index() entry`);
|
|
163
|
+
}
|
|
164
|
+
return requireJson(one, `collection() indexes[${i}]`);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const renamed = options.renamedFrom ?? builder.state.renamedFrom;
|
|
168
|
+
if (renamed !== undefined) out['x-rename'] = requireName(renamed, 'collection() renamedFrom');
|
|
169
|
+
Object.defineProperty(out, COLLECTION, { value: true, enumerable: false });
|
|
170
|
+
return Object.freeze(out);
|
|
171
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `defineModel({ entities, collections })` — one `$model` 0.1
|
|
4
|
+
* document, deep-frozen, that `openStore` accepts unchanged. The pen
|
|
5
|
+
* refuses here only what it can see and the store's model walk would
|
|
6
|
+
* refuse anyway: a relation whose target is not a declared entity, a
|
|
7
|
+
* store-allocated default on a composite key, a collection spec where
|
|
8
|
+
* an entity was expected, and a `renamedFrom()` hint on a member, which
|
|
9
|
+
* the document has no place for. Inverse agreement, foreign-key types
|
|
10
|
+
* and the rest stay the engine's (`JD00xx`), never re-implemented.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
|
|
14
|
+
|
|
15
|
+
import { LinqBuildError } from '../errors.js';
|
|
16
|
+
import { requireNameMap } from '../json-boundary.js';
|
|
17
|
+
import { isSchemaBuilder } from '../schema/brand.js';
|
|
18
|
+
import { COLLECTION } from './collection.js';
|
|
19
|
+
import { refuseStrandedRename } from './entity.js';
|
|
20
|
+
|
|
21
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
22
|
+
|
|
23
|
+
/** @param {any} value @param {string} what @returns {Record<string, any>} */
|
|
24
|
+
function requireMap(value, what) {
|
|
25
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
26
|
+
throw new LinqBuildError('JL0101', `defineModel() ${what} is a plain object of declarations`);
|
|
27
|
+
}
|
|
28
|
+
requireNameMap(value, `defineModel() ${what}`, `/${what}`);
|
|
29
|
+
for (const name of Object.keys(value)) {
|
|
30
|
+
if (!NAME.test(name)) {
|
|
31
|
+
throw new LinqBuildError('JL0101',
|
|
32
|
+
`defineModel() ${what} names are identifiers, got '${name}'`, `/${what}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The `x-entity` blocks an entity declares, by member, read from the
|
|
40
|
+
* builder before its document is assembled.
|
|
41
|
+
* @param {any} builder
|
|
42
|
+
* @returns {[string, any][]}
|
|
43
|
+
*/
|
|
44
|
+
function entityBlocks(builder) {
|
|
45
|
+
const st = builder.state;
|
|
46
|
+
if (st.kind !== 'object') return [];
|
|
47
|
+
return st.props.map(([name, member]) => [name, member.annotation('x-entity') ?? {}]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the model document.
|
|
52
|
+
* @param {{ entities?: Record<string, any>, collections?: Record<string, any> }} spec
|
|
53
|
+
* @returns {any} the deep-frozen `$model` 0.1 document
|
|
54
|
+
*/
|
|
55
|
+
export function defineModel(spec) {
|
|
56
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
57
|
+
throw new LinqBuildError('JL0101', 'defineModel() takes { entities?, collections? }');
|
|
58
|
+
}
|
|
59
|
+
for (const key of Object.keys(spec)) {
|
|
60
|
+
if (key !== 'entities' && key !== 'collections') {
|
|
61
|
+
throw new LinqBuildError('JL0101', `defineModel() does not take '${key}'`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (spec.entities === undefined && spec.collections === undefined) {
|
|
65
|
+
throw new LinqBuildError('JL0101', 'defineModel() needs entities, collections, or both');
|
|
66
|
+
}
|
|
67
|
+
const out = { $model: '0.1' };
|
|
68
|
+
|
|
69
|
+
if (spec.collections !== undefined) {
|
|
70
|
+
const collections = requireMap(spec.collections, 'collections');
|
|
71
|
+
const emitted = {};
|
|
72
|
+
for (const name of Object.keys(collections)) {
|
|
73
|
+
const declared = collections[name];
|
|
74
|
+
if (declared === null || typeof declared !== 'object' || declared[COLLECTION] !== true) {
|
|
75
|
+
throw new LinqBuildError('JL0101',
|
|
76
|
+
`collections.${name} is not a collection() declaration`, `/collections/${name}`);
|
|
77
|
+
}
|
|
78
|
+
setObjectMember(emitted, name, declared);
|
|
79
|
+
}
|
|
80
|
+
out.collections = emitted;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (spec.entities !== undefined) {
|
|
84
|
+
const entities = requireMap(spec.entities, 'entities');
|
|
85
|
+
const names = Object.keys(entities);
|
|
86
|
+
const emitted = {};
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const at = `/entities/${name}`;
|
|
89
|
+
const builder = entities[name];
|
|
90
|
+
if (builder !== null && typeof builder === 'object' && builder[COLLECTION] === true) {
|
|
91
|
+
throw new LinqBuildError('JL0102',
|
|
92
|
+
`entities.${name} is a collection() declaration — an entity has no key pointer and no `
|
|
93
|
+
+ 'indexes option: its key is key() on a member and its indexes are unique()/index() '
|
|
94
|
+
+ 'per member (the vocabulary has no composite or derived entity index)', at);
|
|
95
|
+
}
|
|
96
|
+
if (!isSchemaBuilder(builder)) {
|
|
97
|
+
throw new LinqBuildError('JL0101', `entities.${name} is not a schema builder`, at);
|
|
98
|
+
}
|
|
99
|
+
refuseStrandedRename(builder, `entities.${name}`, at);
|
|
100
|
+
const blocks = entityBlocks(builder);
|
|
101
|
+
const keys = blocks.filter(([, block]) => block.key === true);
|
|
102
|
+
for (const [member, block] of blocks) {
|
|
103
|
+
const memberAt = `${at}/schema/properties/${member}/x-entity`;
|
|
104
|
+
const relation = block.relation;
|
|
105
|
+
if (relation !== undefined && !names.includes(relation.to)) {
|
|
106
|
+
throw new LinqBuildError('JL0102',
|
|
107
|
+
`relation target '${relation.to}' on ${name}.${member} is not a declared entity — `
|
|
108
|
+
+ `the model declares ${names.map((n) => `'${n}'`).join(', ')}`,
|
|
109
|
+
`${memberAt}/relation/to`);
|
|
110
|
+
}
|
|
111
|
+
if ((block.default === 'auto' || block.default === 'uuid') && keys.length > 1) {
|
|
112
|
+
throw new LinqBuildError('JL0102',
|
|
113
|
+
`identity('${block.default}') on ${name}.${member}: a store-allocated key is a SINGLE `
|
|
114
|
+
+ `key, and ${name} declares a composite one (${keys.map(([k]) => k).join(', ')})`,
|
|
115
|
+
`${memberAt}/default`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const entity = { schema: builder.schema };
|
|
119
|
+
if (builder.state.renamedFrom !== undefined) entity['x-rename'] = builder.state.renamedFrom;
|
|
120
|
+
setObjectMember(emitted, name, entity);
|
|
121
|
+
}
|
|
122
|
+
out.entities = emitted;
|
|
123
|
+
}
|
|
124
|
+
return deepFreeze(out);
|
|
125
|
+
}
|