@jarenjs/db 0.49.2 → 0.66.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.
- package/ARCHITECTURE.md +420 -71
- package/README.md +711 -79
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +309 -45
- package/docs/LIVE-FORMAT.md +156 -19
- package/docs/MIGRATION-FORMAT.md +247 -40
- package/docs/MODEL-FORMAT.md +968 -86
- package/package.json +21 -8
- package/schemas/jaren-migration.draft-07.schema.json +73 -0
- package/schemas/jaren-migration.schema.json +73 -0
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +255 -44
- package/src/cli.js +337 -50
- package/src/cursor.js +411 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +125 -11
- package/src/dialect.js +267 -112
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +245 -12
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +503 -69
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +18 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit-model.js +14 -0
- package/src/emit.js +349 -51
- package/src/entity.js +102 -59
- package/src/errors.js +430 -2
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -19
- package/src/introspect.js +583 -0
- package/src/jobs.js +870 -99
- package/src/json-bytes.js +58 -0
- package/src/live-time.js +12 -3
- package/src/live.js +11 -1
- package/src/maintenance.js +175 -0
- package/src/migrate.js +606 -333
- package/src/model.js +241 -8
- package/src/plan.js +1238 -160
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1748 -312
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1672 -276
- package/src/tracker.js +367 -68
- package/src/udf.js +88 -7
- package/types/index.d.ts +1246 -32
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +72 -3
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +81 -3
- package/types/wasm.d.ts +21 -0
- package/dist/types/algebra.d.ts +0 -230
- package/dist/types/app.d.ts +0 -49
- package/dist/types/capture.d.ts +0 -85
- package/dist/types/cli.d.ts +0 -2
- package/dist/types/dag-job.d.ts +0 -40
- package/dist/types/ddl.d.ts +0 -229
- package/dist/types/derive.d.ts +0 -250
- package/dist/types/dialect.d.ts +0 -154
- package/dist/types/dialects/sqlite.d.ts +0 -9
- package/dist/types/driver.d.ts +0 -110
- package/dist/types/drivers/bun.d.ts +0 -47
- package/dist/types/drivers/node.d.ts +0 -37
- package/dist/types/drivers/wasm.d.ts +0 -65
- package/dist/types/emit-model.d.ts +0 -44
- package/dist/types/emit.d.ts +0 -75
- package/dist/types/entity.d.ts +0 -23
- package/dist/types/errors.d.ts +0 -170
- package/dist/types/graph.d.ts +0 -28
- package/dist/types/index.d.ts +0 -37
- package/dist/types/jobs.d.ts +0 -140
- package/dist/types/knn.d.ts +0 -69
- package/dist/types/live-time.d.ts +0 -141
- package/dist/types/live.d.ts +0 -64
- package/dist/types/migrate.d.ts +0 -170
- package/dist/types/model.d.ts +0 -36
- package/dist/types/patch-sql.d.ts +0 -37
- package/dist/types/plan.d.ts +0 -142
- package/dist/types/profile.d.ts +0 -80
- package/dist/types/query.d.ts +0 -112
- package/dist/types/residual.d.ts +0 -64
- package/dist/types/series.d.ts +0 -227
- package/dist/types/store.d.ts +0 -60
- package/dist/types/tracker.d.ts +0 -43
- package/dist/types/typed.d.ts +0 -15
- package/dist/types/types.d.ts +0 -26
- package/dist/types/udf.d.ts +0 -75
- package/dist/types/window.d.ts +0 -52
package/src/model.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import {
|
|
25
25
|
collectSameDocumentAnchors, resolveSameDocumentRef,
|
|
26
26
|
} from '@jarenjs/validate/normalize';
|
|
27
|
+
import { compileJsonQuery } from '@jarenjs/json/query';
|
|
27
28
|
|
|
28
29
|
import { DbCompileError } from './errors.js';
|
|
29
30
|
|
|
@@ -123,12 +124,65 @@ function checkDefault(declared, docPath) {
|
|
|
123
124
|
|| declared === 'auto') return;
|
|
124
125
|
if (declared !== null && typeof declared === 'object' && !Array.isArray(declared)
|
|
125
126
|
&& (Object.hasOwn(declared, 'value') !== Object.hasOwn(declared, 'query'))
|
|
126
|
-
&& Object.keys(declared).length === 1)
|
|
127
|
+
&& Object.keys(declared).length === 1) {
|
|
128
|
+
if (Object.hasOwn(declared, 'query')) {
|
|
129
|
+
// compiled where the model is checked, not at the first
|
|
130
|
+
// `store.entity()` call — a default that cannot compile is a model
|
|
131
|
+
// defect, and the engine's reason travels with the model position
|
|
132
|
+
try {
|
|
133
|
+
compileJsonQuery(declared.query);
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
throw new DbCompileError('JD0005',
|
|
137
|
+
`the { query } default does not compile: ${/** @type {Error} */ (cause).message}`,
|
|
138
|
+
docPath, /** @type {Error} */ (cause));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
127
143
|
throw modelError(
|
|
128
144
|
"a default is 'now', 'updated', 'uuid', 'auto', { value: … } or { query: … }",
|
|
129
145
|
docPath);
|
|
130
146
|
}
|
|
131
147
|
|
|
148
|
+
/** The schema positions whose `x-entity` block the one-level walk READS:
|
|
149
|
+
* a top-level property, an `allOf` branch of one (shallow-merged), and
|
|
150
|
+
* a `$defs`/anchor target (a property's `$ref` resolves there). */
|
|
151
|
+
const READ_BLOCK_KEYS = new Set(['allOf', '$defs', 'definitions']);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Find an `x-entity` block the entity walk would never read — nested
|
|
155
|
+
* inside a property's `properties`, `items`, `anyOf`, … — so it fails
|
|
156
|
+
* the model instead of being ignored. §9.2's promise: a silently
|
|
157
|
+
* ignored mapping directive is a data-loss bug, so the vocabulary is
|
|
158
|
+
* closed in POSITION as well as in name.
|
|
159
|
+
* @param {any} node
|
|
160
|
+
* @param {string} path
|
|
161
|
+
* @param {boolean} read - whether a block AT this node is read
|
|
162
|
+
* @returns {string | null} the docPath of an unread block
|
|
163
|
+
*/
|
|
164
|
+
function unreadEntityBlock(node, path, read) {
|
|
165
|
+
if (node === null || typeof node !== 'object') return null;
|
|
166
|
+
if (Array.isArray(node)) {
|
|
167
|
+
for (let i = 0; i < node.length; i++) {
|
|
168
|
+
const found = unreadEntityBlock(node[i], `${path}/${i}`, read);
|
|
169
|
+
if (found !== null) return found;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
for (const key of Object.keys(node)) {
|
|
174
|
+
if (key === 'x-entity') {
|
|
175
|
+
if (!read) return `${path}/x-entity`;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// a block one level under a read position is read only through
|
|
179
|
+
// `allOf`/`$defs`; under anything else it is out of the walk
|
|
180
|
+
const found = unreadEntityBlock(node[key], `${path}/${key}`, read && READ_BLOCK_KEYS.has(key));
|
|
181
|
+
if (found !== null) return found;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
132
186
|
/**
|
|
133
187
|
* Normalize the `entities` member of a model document.
|
|
134
188
|
* @param {any} model
|
|
@@ -169,14 +223,54 @@ export function normalizeEntities(model) {
|
|
|
169
223
|
? raw['x-entity'] : undefined),
|
|
170
224
|
`${propertyPath}/x-entity`);
|
|
171
225
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
// a nullable scalar (`['string', 'null']`) is that scalar; a union
|
|
227
|
+
// of two or more scalar types has no one column type and lives in
|
|
228
|
+
// the document (§9.3) — mapping it by its FIRST member stored `5`
|
|
229
|
+
// as `"5.0"` in a text column
|
|
230
|
+
const scalarMembers = Array.isArray(effective.type)
|
|
231
|
+
? effective.type.filter((t) => t !== 'null')
|
|
232
|
+
: [effective.type];
|
|
233
|
+
const type = scalarMembers.length === 1 && typeof scalarMembers[0] === 'string'
|
|
234
|
+
? scalarMembers[0]
|
|
235
|
+
: undefined;
|
|
236
|
+
const union = scalarMembers.length > 1;
|
|
177
237
|
|
|
178
238
|
if (entityBlock.default !== undefined)
|
|
179
239
|
checkDefault(entityBlock.default, `${propertyPath}/x-entity/default`);
|
|
240
|
+
if (entityBlock.default !== undefined && entityBlock.relation !== undefined) {
|
|
241
|
+
throw modelError('a relation member takes no default — it is a projection, not stored state',
|
|
242
|
+
`${propertyPath}/x-entity/default`);
|
|
243
|
+
}
|
|
244
|
+
if (entityBlock.default === 'auto' && entityBlock.key !== true) {
|
|
245
|
+
throw modelError("default: 'auto' is allocated by the database for a single integer key only",
|
|
246
|
+
`${propertyPath}/x-entity/default`);
|
|
247
|
+
}
|
|
248
|
+
// the mapping directives that need a column of their own: on a
|
|
249
|
+
// property the document keeps — `column: 'json'`, a non-scalar, a
|
|
250
|
+
// union — they were accepted and never applied (a key with no key
|
|
251
|
+
// column let duplicates in and broke every read)
|
|
252
|
+
const columnMapped = SCALARS.has(type ?? '') && entityBlock.column !== 'json';
|
|
253
|
+
const needsColumn = ['key', 'unique', 'index'].filter((member) => entityBlock[member] === true);
|
|
254
|
+
if (!columnMapped && needsColumn.length > 0) {
|
|
255
|
+
const why = entityBlock.column === 'json'
|
|
256
|
+
? "column: 'json' keeps the property in the document"
|
|
257
|
+
: union ? 'a union of scalar types lives in the document'
|
|
258
|
+
: 'only a top-level scalar takes a column';
|
|
259
|
+
throw modelError(entityBlock.key === true
|
|
260
|
+
? `a key property must be a scalar with a column of its own — ${why}`
|
|
261
|
+
: `'${propertyName}' declares ${needsColumn.join('/')} but has no column — ${why}`,
|
|
262
|
+
propertyPath);
|
|
263
|
+
}
|
|
264
|
+
for (const key of Object.keys(raw !== null && typeof raw === 'object' ? raw : {})) {
|
|
265
|
+
if (key === 'x-entity') continue;
|
|
266
|
+
const unread = unreadEntityBlock(raw[key], `${propertyPath}/${key}`, READ_BLOCK_KEYS.has(key));
|
|
267
|
+
if (unread !== null) {
|
|
268
|
+
throw new DbCompileError('JD0030',
|
|
269
|
+
'x-entity applies to an entity\'s top-level properties (and their allOf/$ref '
|
|
270
|
+
+ 'targets) only — a nested block is never read, so it is refused rather than ignored',
|
|
271
|
+
unread);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
180
274
|
if (entityBlock.column !== undefined
|
|
181
275
|
&& entityBlock.column !== 'integer' && entityBlock.column !== 'json') {
|
|
182
276
|
throw modelError("x-entity.column is 'integer' (epoch date column) or 'json' (stay in the document)",
|
|
@@ -289,6 +383,16 @@ function resolveRelations(entities) {
|
|
|
289
383
|
// many-to-many: a join table
|
|
290
384
|
relation.kind = 'manyToMany';
|
|
291
385
|
const other = relation.to;
|
|
386
|
+
if (other === owner) {
|
|
387
|
+
throw modelError('a many-to-many relation to the entity itself is not supported — '
|
|
388
|
+
+ 'both endpoint columns would carry the same name', docPath);
|
|
389
|
+
}
|
|
390
|
+
for (const endpoint of [owner, other]) {
|
|
391
|
+
if (entities.get(endpoint).keys.length !== 1) {
|
|
392
|
+
throw modelError(`a many-to-many relation needs single-key endpoints; '${endpoint}' `
|
|
393
|
+
+ 'declares a composite key', docPath);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
292
396
|
relation.joinTable = typeof relation.through === 'string'
|
|
293
397
|
? relation.through
|
|
294
398
|
: [owner, other].sort().join('_');
|
|
@@ -313,6 +417,13 @@ function resolveRelations(entities) {
|
|
|
313
417
|
const holder = entities.get(relation.fkEntity);
|
|
314
418
|
const declaredVia = holder.properties.get(relation.via);
|
|
315
419
|
const targetEntity = entities.get(relation.fkTargets);
|
|
420
|
+
if (targetEntity.keys.length !== 1) {
|
|
421
|
+
// a foreign key references ONE column; a composite-key target
|
|
422
|
+
// rendered a reference to its first key alone, which SQLite
|
|
423
|
+
// refused at the first write with a raw "foreign key mismatch"
|
|
424
|
+
throw modelError(`a foreign-key relation must reference a single-key entity; `
|
|
425
|
+
+ `'${relation.fkTargets}' declares a composite key`, docPath);
|
|
426
|
+
}
|
|
316
427
|
const targetKeyType = targetEntity.properties.get(targetEntity.keys[0]).type;
|
|
317
428
|
if (declaredVia !== undefined) {
|
|
318
429
|
if (declaredVia.type !== targetKeyType || declaredVia.column === 'json') {
|
|
@@ -355,7 +466,8 @@ function resolveRelations(entities) {
|
|
|
355
466
|
if (relationA.kind !== relationB.kind) {
|
|
356
467
|
throw new DbCompileError('JD0031',
|
|
357
468
|
`relation '${a.owner}.${a.property.name}' and '${b.owner}.${b.property.name}' `
|
|
358
|
-
+ `disagree on the foreign key ('${relationA.via}' vs '${relationB.via}')
|
|
469
|
+
+ `disagree on the foreign key ('${relationA.via}' vs '${relationB.via}') `
|
|
470
|
+
+ '— two edges between the same pair declare their inverse on one side only (§9.4)',
|
|
359
471
|
`${a.property.docPath}/x-entity/relation`);
|
|
360
472
|
}
|
|
361
473
|
continue;
|
|
@@ -377,6 +489,59 @@ function resolveRelations(entities) {
|
|
|
377
489
|
}
|
|
378
490
|
}
|
|
379
491
|
|
|
492
|
+
/**
|
|
493
|
+
* The relation table a producer may read (§10.1): per entity, one
|
|
494
|
+
* frozen record per declared relation member — `to`, `kind`, and for a
|
|
495
|
+
* foreign-key relation `via`, `fkEntity`, `fkTargets` and `targetKey`
|
|
496
|
+
* (the key property the foreign key references on `fkTargets`, which
|
|
497
|
+
* is what a hop's equality compares `via` against); for a many-to-many
|
|
498
|
+
* `joinTable` and the target's `targetKey`. Plain data, keyed by
|
|
499
|
+
* entity name and then by member name, so a query producer can lower
|
|
500
|
+
* a relation hop to the phrases the translator runs without a second
|
|
501
|
+
* vocabulary and without importing this package.
|
|
502
|
+
* @param {Map<string, any>} entities - normalized entities
|
|
503
|
+
* @returns {Readonly<Record<string, Readonly<Record<string, any>>>>}
|
|
504
|
+
*/
|
|
505
|
+
export function relationTables(entities) {
|
|
506
|
+
/** @type {Record<string, any>} */
|
|
507
|
+
const tables = {};
|
|
508
|
+
for (const entity of entities.values()) {
|
|
509
|
+
/** @type {Record<string, any>} */
|
|
510
|
+
const table = {};
|
|
511
|
+
for (const property of entity.relations) {
|
|
512
|
+
const relation = property.relation;
|
|
513
|
+
const entry = relation.kind === 'manyToMany'
|
|
514
|
+
? {
|
|
515
|
+
to: relation.to,
|
|
516
|
+
kind: relation.kind,
|
|
517
|
+
joinTable: relation.joinTable,
|
|
518
|
+
targetKey: entities.get(relation.to).keys[0],
|
|
519
|
+
// the join row's two columns and the key each references, so a
|
|
520
|
+
// hop can lower through the join ROOT (§10.7) rather than
|
|
521
|
+
// refusing for want of one
|
|
522
|
+
ownColumn: `${entity.name}_key`,
|
|
523
|
+
ownKey: entity.keys[0],
|
|
524
|
+
targetColumn: `${relation.to}_key`,
|
|
525
|
+
}
|
|
526
|
+
: {
|
|
527
|
+
to: relation.to,
|
|
528
|
+
kind: relation.kind,
|
|
529
|
+
via: relation.via,
|
|
530
|
+
fkEntity: relation.fkEntity,
|
|
531
|
+
fkTargets: relation.fkTargets,
|
|
532
|
+
targetKey: entities.get(relation.fkTargets).keys[0],
|
|
533
|
+
};
|
|
534
|
+
// an own member, whatever the property is named: `__proto__` as
|
|
535
|
+
// a member name would otherwise rewrite the record's prototype
|
|
536
|
+
Object.defineProperty(table, property.name,
|
|
537
|
+
{ value: Object.freeze(entry), writable: true, enumerable: true, configurable: true });
|
|
538
|
+
}
|
|
539
|
+
Object.defineProperty(tables, entity.name,
|
|
540
|
+
{ value: Object.freeze(table), writable: true, enumerable: true, configurable: true });
|
|
541
|
+
}
|
|
542
|
+
return Object.freeze(tables);
|
|
543
|
+
}
|
|
544
|
+
|
|
380
545
|
/**
|
|
381
546
|
* The hybrid mapping, derived mechanically from §9.3's table and
|
|
382
547
|
* returned as DATA: per entity, the columns (name, type, source),
|
|
@@ -408,7 +573,13 @@ export function explainMapping(model) {
|
|
|
408
573
|
source: epoch ? 'epoch(document)' : 'document',
|
|
409
574
|
key: property.key,
|
|
410
575
|
};
|
|
411
|
-
if (property.enum !== undefined)
|
|
576
|
+
if (property.enum !== undefined) {
|
|
577
|
+
// `null` never fails a CHECK (NULL IN (…) is unknown, which
|
|
578
|
+
// passes), but rendered into the list it made the whole CHECK
|
|
579
|
+
// unknown for EVERY value — a nullable enum accepted anything
|
|
580
|
+
const values = property.enum.filter((value) => value !== null);
|
|
581
|
+
if (values.length > 0) column.check = values;
|
|
582
|
+
}
|
|
412
583
|
columns.push(column);
|
|
413
584
|
if (property.unique) indexes.push({ property: property.name, unique: true });
|
|
414
585
|
else if (property.index) indexes.push({ property: property.name, unique: false });
|
|
@@ -463,6 +634,13 @@ export function explainMapping(model) {
|
|
|
463
634
|
for (const declaring of entity.relations) {
|
|
464
635
|
const relation = declaring.relation;
|
|
465
636
|
if (relation.kind !== 'manyToMany' || seenJoins.has(relation.joinTable)) continue;
|
|
637
|
+
// a join table is a queryable ROOT (§10.7), so its name shares one
|
|
638
|
+
// namespace with the entities: a collision would make `$.X[*]`
|
|
639
|
+
// mean two things
|
|
640
|
+
if (entities.has(relation.joinTable)) {
|
|
641
|
+
throw modelError(`the join table '${relation.joinTable}' has the name of a declared `
|
|
642
|
+
+ 'entity, and both are query roots', declaring.docPath ?? entity.docPath);
|
|
643
|
+
}
|
|
466
644
|
seenJoins.add(relation.joinTable);
|
|
467
645
|
const [a, b] = [entity.name, relation.to].sort();
|
|
468
646
|
mapping.joinTables[relation.joinTable] = {
|
|
@@ -474,3 +652,58 @@ export function explainMapping(model) {
|
|
|
474
652
|
}
|
|
475
653
|
return mapping;
|
|
476
654
|
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* The read-only query ROOTS a model's join tables contribute (§10.7):
|
|
658
|
+
* one pseudo-entity per declared many-to-many join table, carrying
|
|
659
|
+
* exactly its two key columns and no document of its own. They are
|
|
660
|
+
* queryable — `$.<JoinTable>[*]` binds like any entity array — and they
|
|
661
|
+
* are NOT writable: `store.entity(name)` reads the model's own entity
|
|
662
|
+
* map, which these are deliberately not in, so a membership is still
|
|
663
|
+
* written through `link`/`unlink` and the tracker's join rows.
|
|
664
|
+
* @param {Map<string, any>} entities - the normalized entities
|
|
665
|
+
* @param {any} mapping - `explainMapping(...)`
|
|
666
|
+
* @returns {{ entities: Map<string, any>, mappings: Record<string, any> }}
|
|
667
|
+
*/
|
|
668
|
+
export function joinTableRoots(entities, mapping) {
|
|
669
|
+
/** @type {Map<string, any>} */
|
|
670
|
+
const roots = new Map();
|
|
671
|
+
/** @type {any} */
|
|
672
|
+
const mappings = {};
|
|
673
|
+
for (const [name, join] of Object.entries(mapping.joinTables ?? {})) {
|
|
674
|
+
const sides = [join.left, join.right];
|
|
675
|
+
/** @type {any} */
|
|
676
|
+
const properties = new Map();
|
|
677
|
+
/** @type {any} */
|
|
678
|
+
const schemaProperties = {};
|
|
679
|
+
const columns = [];
|
|
680
|
+
for (const side of sides) {
|
|
681
|
+
const referenced = entities.get(side.entity).properties.get(side.referencesKey);
|
|
682
|
+
properties.set(side.column, { name: side.column, type: referenced.type, key: true });
|
|
683
|
+
schemaProperties[side.column] = { type: referenced.type };
|
|
684
|
+
columns.push({ name: side.column, storage: referenced.type, source: 'column' });
|
|
685
|
+
}
|
|
686
|
+
roots.set(name, {
|
|
687
|
+
name,
|
|
688
|
+
docPath: `/entities/${sides[0].entity}` ,
|
|
689
|
+
schema: { type: 'object', required: sides.map((side) => side.column),
|
|
690
|
+
properties: schemaProperties },
|
|
691
|
+
properties,
|
|
692
|
+
keys: sides.map((side) => side.column),
|
|
693
|
+
relations: [],
|
|
694
|
+
version: null,
|
|
695
|
+
joinTable: true,
|
|
696
|
+
});
|
|
697
|
+
mappings[name] = {
|
|
698
|
+
table: name,
|
|
699
|
+
columns,
|
|
700
|
+
foreignKeys: [],
|
|
701
|
+
indexes: [],
|
|
702
|
+
keys: sides.map((side) => side.column),
|
|
703
|
+
// a join row IS its two keys: there is no document column to read,
|
|
704
|
+
// and the merge is handed an empty one
|
|
705
|
+
document: false,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
return { entities: roots, mappings };
|
|
709
|
+
}
|