uql-orm 0.65.0 → 0.65.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.
|
@@ -71,6 +71,6 @@ export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
|
|
|
71
71
|
/**
|
|
72
72
|
* The foreign keys an entity holds: each owning to-one's columns, and each `@Field({ references })` no
|
|
73
73
|
* relation joins on, as the many-to-one it describes, once its target has registered a key. What the
|
|
74
|
-
* schema build constrains and a junction joins by.
|
|
74
|
+
* schema build constrains and a junction joins by, read once the relations holding them are settled.
|
|
75
75
|
*/
|
|
76
76
|
export declare function foreignKeysOf<E>(meta: EntityMeta<E>): RelationMeta[];
|
|
@@ -306,74 +306,88 @@ function ensureMeta(entity) {
|
|
|
306
306
|
return meta;
|
|
307
307
|
}
|
|
308
308
|
export function getMeta(entity) {
|
|
309
|
+
const meta = registeredMeta(entity);
|
|
310
|
+
// Stamped once finalizing succeeds, so a read after a failure reports the same mistake again. Finalizing
|
|
311
|
+
// reads other entities without resolving them, so no entity is ever read half resolved.
|
|
312
|
+
if (meta.processedAt !== meta.revision) {
|
|
313
|
+
fillRelations(meta);
|
|
314
|
+
meta.processedAt = meta.revision;
|
|
315
|
+
}
|
|
316
|
+
return meta;
|
|
317
|
+
}
|
|
318
|
+
/** The metadata `entity` registered, however much of it is resolved. */
|
|
319
|
+
function registeredMeta(entity) {
|
|
309
320
|
const meta = metas.get(entity);
|
|
310
321
|
if (!meta) {
|
|
311
322
|
throw TypeError(`'${entity.name}' is not an entity`);
|
|
312
323
|
}
|
|
313
|
-
|
|
314
|
-
return meta;
|
|
315
|
-
}
|
|
316
|
-
// Stamped before finalizing: `fillInverseSide` reads the other side through `getMeta`, and with each
|
|
317
|
-
// side mapped by the other that recursion has to find this half-filled meta rather than run again.
|
|
318
|
-
// Unstamped when finalizing throws, so the next read reports the same mistake; running it again is
|
|
319
|
-
// harmless, since every step skips what it settled.
|
|
320
|
-
meta.processedAt = meta.revision;
|
|
321
|
-
try {
|
|
322
|
-
return fillRelations(meta);
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
meta.processedAt = undefined;
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
324
|
+
return meta;
|
|
328
325
|
}
|
|
329
326
|
function fillRelations(meta) {
|
|
330
327
|
for (const [relKey, relation] of definedEntries(meta.relations)) {
|
|
331
|
-
// The registered view: `references` may be unset, or the one column a to-one names, until this settles it.
|
|
332
|
-
const relOpts = relation;
|
|
333
328
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
334
|
-
|
|
335
|
-
? fillInverseSide(at, meta, relOpts, relOpts.mappedBy)
|
|
336
|
-
: (pairedReferences(at, relOpts) ?? fillOwningSide(at, meta, relKey, relOpts));
|
|
337
|
-
if (!references.length) {
|
|
329
|
+
if (!settledReferences(at, meta, relKey, relation).length) {
|
|
338
330
|
throw new TypeError(`${at} has no columns to join on.`);
|
|
339
331
|
}
|
|
340
332
|
}
|
|
341
333
|
// A column `references` names is a foreign key with or without a relation over it, and one cannot point
|
|
342
334
|
// at a composite key: refused on first read, as a relation that cannot join is, not at the schema build.
|
|
343
335
|
foreignKeysOf(meta);
|
|
344
|
-
return meta;
|
|
345
336
|
}
|
|
346
337
|
/**
|
|
347
|
-
* The pairs a relation joins on,
|
|
348
|
-
*
|
|
338
|
+
* The pairs a relation joins on, settled on first read, whether its own entity is being resolved or another
|
|
339
|
+
* needs it. Never resolving an entity is what keeps it from recursing, and the one column a to-one names is
|
|
340
|
+
* paired with its target's key only here, since registration can run before the target has one.
|
|
349
341
|
*/
|
|
350
|
-
function
|
|
351
|
-
const { references } = relOpts;
|
|
352
|
-
if (typeof references
|
|
353
|
-
|
|
342
|
+
function settledReferences(at, meta, relKey, relOpts) {
|
|
343
|
+
const { references, mappedBy, through } = relOpts;
|
|
344
|
+
if (typeof references === 'string') {
|
|
345
|
+
const target = ensureMeta(relOpts.entity());
|
|
346
|
+
if (mappedBy || isToManyRelation(relOpts) || target.ids.length > 1) {
|
|
347
|
+
throw new TypeError(`${at} names one column, '${references}', which only a to-one holding a foreign key to a one-column key ` +
|
|
348
|
+
'can: pair the columns, [{ local, foreign }].');
|
|
349
|
+
}
|
|
350
|
+
relOpts.references = [{ local: references, foreign: soleIdOf(target, 'a foreign key') }];
|
|
351
|
+
return relOpts.references;
|
|
354
352
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
353
|
+
if (references)
|
|
354
|
+
return references;
|
|
355
|
+
if (mappedBy)
|
|
356
|
+
return fillInverseSide(at, meta, relOpts, mappedBy);
|
|
357
|
+
if (through)
|
|
358
|
+
return fillThrough(at, meta, relOpts, through);
|
|
359
|
+
return fillOwningSide(at, meta, relKey, relOpts);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Settles each relation joining on its own entity's columns, every one but an inverse side and a `through`:
|
|
363
|
+
* the columns they create and the foreign keys they hold are what another entity reads off this one.
|
|
364
|
+
*/
|
|
365
|
+
function settleOwnColumns(meta) {
|
|
366
|
+
for (const [relKey, relation] of definedEntries(meta.relations)) {
|
|
367
|
+
if (!relation.mappedBy && !relation.through) {
|
|
368
|
+
settledReferences(`'${meta.entity.name}.${relKey}'`, meta, relKey, relation);
|
|
369
|
+
}
|
|
359
370
|
}
|
|
360
|
-
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Each key of this entity, then each of the target, paired with the junction's one column referencing it.
|
|
374
|
+
* Both groups live on the junction whatever the cardinality, as `deleteRelations` and every dialect read
|
|
375
|
+
* them, and a composite key gives a pair per column, which is what makes a join address a whole key.
|
|
376
|
+
*/
|
|
377
|
+
function fillThrough(at, meta, relOpts, through) {
|
|
378
|
+
const junction = registeredMeta(through());
|
|
379
|
+
relOpts.references = [
|
|
380
|
+
...junctionReferences(at, junction, meta),
|
|
381
|
+
...junctionReferences(at, junction, ensureMeta(relOpts.entity())),
|
|
382
|
+
];
|
|
361
383
|
return relOpts.references;
|
|
362
384
|
}
|
|
363
385
|
function fillOwningSide(at, meta, relKey, relOpts) {
|
|
364
|
-
const relMeta = ensureMeta(relOpts.entity());
|
|
365
|
-
if (relOpts.through) {
|
|
366
|
-
// Both columns live on the junction, whatever the cardinality: `deleteRelations` and every dialect
|
|
367
|
-
// read them as junction columns. A composite key contributes one pair per column of it, which is
|
|
368
|
-
// what makes the join address a whole key rather than part.
|
|
369
|
-
const junction = getMeta(relOpts.through());
|
|
370
|
-
relOpts.references = [...junctionReferences(at, junction, meta), ...junctionReferences(at, junction, relMeta)];
|
|
371
|
-
return relOpts.references;
|
|
372
|
-
}
|
|
373
386
|
if (isToManyRelation(relOpts)) {
|
|
374
387
|
throw new TypeError(`${at} is a to-many relation with no way to join: it needs 'mappedBy' (the field on the other side), ` +
|
|
375
388
|
"'through' (a junction entity), or 'references' (the columns).");
|
|
376
389
|
}
|
|
390
|
+
const relMeta = ensureMeta(relOpts.entity());
|
|
377
391
|
// `<rel>Id` for the one-key case it has always been; `<rel><Key>` per column otherwise. Both name a
|
|
378
392
|
// property, so both are spelled from the referenced *property* - a column name is what the naming
|
|
379
393
|
// strategy makes of this afterwards.
|
|
@@ -406,14 +420,13 @@ function fillOwningSide(at, meta, relKey, relOpts) {
|
|
|
406
420
|
return references;
|
|
407
421
|
}
|
|
408
422
|
function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return own;
|
|
423
|
+
const relMeta = registeredMeta(relOpts.entity());
|
|
424
|
+
const other = `'${relMeta.entity.name}.${mappedBy}'`;
|
|
425
|
+
// The other side's own columns first: they declare, or create, what this side is mapped by.
|
|
426
|
+
settleOwnColumns(relMeta);
|
|
414
427
|
if (relMeta.fields[mappedBy]) {
|
|
415
428
|
if (meta.ids.length > 1) {
|
|
416
|
-
throw new TypeError(`${at} is mapped by
|
|
429
|
+
throw new TypeError(`${at} is mapped by ${other}, one column, but the primary key of ` +
|
|
417
430
|
`'${meta.entity.name}' is composite (${meta.ids.join(', ')}). Map it by the relation on the other side ` +
|
|
418
431
|
'instead, which joins every column of the key.');
|
|
419
432
|
}
|
|
@@ -421,16 +434,14 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
|
421
434
|
relOpts.references = [{ local: meta.ids[0], foreign: mappedBy }];
|
|
422
435
|
return relOpts.references;
|
|
423
436
|
}
|
|
424
|
-
// Authored view again: with each side mapped by the other, the target is still mid-resolution here and
|
|
425
|
-
// its own `references` are unset, which is what the second throw reports.
|
|
426
437
|
const owner = relMeta.relations[mappedBy];
|
|
427
438
|
if (!owner) {
|
|
428
|
-
throw new TypeError(`${at} is mapped by '${mappedBy}', which is neither a field nor a relation of '${
|
|
439
|
+
throw new TypeError(`${at} is mapped by '${mappedBy}', which is neither a field nor a relation of '${relMeta.entity.name}'.`);
|
|
429
440
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', an inverse side too, so neither owns the foreign key.`);
|
|
441
|
+
if (owner.mappedBy) {
|
|
442
|
+
throw new TypeError(`${at} is mapped by ${other}, an inverse side too, so neither owns the foreign key.`);
|
|
433
443
|
}
|
|
444
|
+
const ownerReferences = settledReferences(other, relMeta, mappedBy, owner);
|
|
434
445
|
// Two different flips: a junction's pairs are the owner's group followed by ours, so the two groups
|
|
435
446
|
// swap - `toReversed` would also reverse each group, pairing a composite's columns crosswise. A
|
|
436
447
|
// plain foreign key is one pair per key whose ends swap.
|
|
@@ -444,12 +455,13 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
|
444
455
|
/**
|
|
445
456
|
* The foreign keys an entity holds: each owning to-one's columns, and each `@Field({ references })` no
|
|
446
457
|
* relation joins on, as the many-to-one it describes, once its target has registered a key. What the
|
|
447
|
-
* schema build constrains and a junction joins by.
|
|
458
|
+
* schema build constrains and a junction joins by, read once the relations holding them are settled.
|
|
448
459
|
*/
|
|
449
460
|
export function foreignKeysOf(meta) {
|
|
461
|
+
settleOwnColumns(meta);
|
|
450
462
|
const owning = definedEntries(meta.relations)
|
|
451
463
|
.map(([, relation]) => relation)
|
|
452
|
-
.filter((
|
|
464
|
+
.filter((relation) => !relation.mappedBy && !relation.through && !isToManyRelation(relation));
|
|
453
465
|
const joined = new Set(owning.flatMap(({ references }) => references.map(({ local }) => local)));
|
|
454
466
|
const columns = definedEntries(meta.fields).flatMap(([key, field]) => {
|
|
455
467
|
if (!field.references || joined.has(key))
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -558,10 +558,9 @@ type RelationReferencePairs<E, O> = (local: KeyMap<O>, foreign: KeyMap<E>) => re
|
|
|
558
558
|
* assertions - `fillRelations` establishes the invariant once, and throws where it cannot.
|
|
559
559
|
*
|
|
560
560
|
* `entity` and `through` stay {@link EntityGetter}s. Resolution could call them once and store the class,
|
|
561
|
-
* but only by keeping the authored relations in a second map: it
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
* still there to find. A phase-split metadata map costs more than the call parentheses it saves.
|
|
561
|
+
* but only by keeping the authored relations in a second map: it settles them in place, reading them across
|
|
562
|
+
* entities not resolved yet, so the authored and the settled shape have to be one object. A phase-split
|
|
563
|
+
* metadata map costs more than the call parentheses it saves.
|
|
565
564
|
*/
|
|
566
565
|
export type RelationMeta = Omit<RelationRegistration, 'references'> & {
|
|
567
566
|
references: RelationReferences;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.65.
|
|
6
|
+
"version": "0.65.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|