uql-orm 0.64.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.
- package/dist/bunSql/bunSqlQuerier.d.ts +3 -10
- package/dist/bunSql/bunSqlQuerier.js +2 -16
- package/dist/bunSql/bunSqlQuerierPool.d.ts +0 -2
- package/dist/bunSql/bunSqlQuerierPool.js +14 -16
- package/dist/d1/d1Querier.d.ts +13 -34
- package/dist/d1/d1Querier.js +1 -1
- package/dist/d1/d1QuerierPool.d.ts +3 -3
- package/dist/dialect/abstractDialect.d.ts +4 -9
- package/dist/dialect/abstractDialect.js +4 -5
- package/dist/dialect/abstractSqlDialect.d.ts +2 -2
- package/dist/dialect/index.d.ts +0 -1
- package/dist/dialect/index.js +2 -3
- package/dist/dialect/mysqlLikeSqlDialect.js +0 -3
- package/dist/dialect/pgLikeSqlDialect.d.ts +9 -1
- package/dist/dialect/pgLikeSqlDialect.js +8 -5
- package/dist/dialect/queryContext.d.ts +3 -3
- package/dist/entity/metadata/definition.d.ts +6 -0
- package/dist/entity/metadata/definition.js +139 -94
- package/dist/migrate/cli.d.ts +1 -1
- package/dist/migrate/cli.js +20 -34
- package/dist/migrate/codegen/entityCodeGenerator.d.ts +5 -0
- package/dist/migrate/codegen/entityCodeGenerator.js +29 -7
- package/dist/migrate/codegen/indexDecoratorSource.js +1 -5
- package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
- package/dist/migrate/codegen/sourceLiteral.js +4 -0
- package/dist/migrate/index.d.ts +1 -1
- package/dist/migrate/index.js +1 -1
- package/dist/migrate/introspection/registry.d.ts +2 -2
- package/dist/migrate/introspection/registry.js +6 -11
- package/dist/migrate/migrationTarget.d.ts +6 -6
- package/dist/migrate/migrationTarget.js +18 -16
- package/dist/migrate/migrator.d.ts +3 -7
- package/dist/migrate/migrator.js +10 -31
- package/dist/migrate/schemaGenerator.d.ts +1 -3
- package/dist/migrate/schemaGenerator.js +0 -4
- package/dist/mongo/mongoDialect.d.ts +1 -1
- package/dist/mongo/mongoDialect.js +1 -4
- package/dist/mssql/mssqlDialect.js +0 -3
- package/dist/pglite/pgliteQuerier.d.ts +2 -6
- package/dist/pglite/pgliteQuerier.js +2 -9
- package/dist/postgres/index.d.ts +0 -1
- package/dist/postgres/index.js +0 -1
- package/dist/querier/abstractSharedHandleQuerierPool.d.ts +2 -5
- package/dist/querier/abstractSharedHandleQuerierPool.js +2 -5
- package/dist/querier/abstractSqlQuerier.d.ts +2 -3
- package/dist/querier/abstractSqlQuerier.js +8 -4
- package/dist/querier/cursorStream.d.ts +13 -0
- package/dist/{postgres/pgCursorStream.js → querier/cursorStream.js} +4 -13
- package/dist/schema/canonicalType.js +1 -1
- package/dist/schema/schemaASTBuilder.js +34 -40
- package/dist/sqlite/abstractSqliteQuerier.d.ts +7 -3
- package/dist/sqlite/abstractSqliteQuerier.js +18 -4
- package/dist/sqlite/hranaQuerier.d.ts +1 -1
- package/dist/sqlite/localSqliteQuerierPool.d.ts +7 -0
- package/dist/sqlite/localSqliteQuerierPool.js +19 -0
- package/dist/sqlite/nodeSqliteQuerierPool.js +2 -3
- package/dist/sqlite/sqliteDialect.js +0 -3
- package/dist/sqlite/sqliteQuerier.d.ts +8 -5
- package/dist/sqlite/sqliteQuerier.js +4 -13
- package/dist/sqlite/sqliteQuerierPool.js +4 -4
- package/dist/turso/tursoSessionQuerier.d.ts +2 -2
- package/dist/turso/tursoSessionQuerier.js +16 -18
- package/dist/type/dialect.d.ts +25 -35
- package/dist/type/entity.d.ts +29 -32
- package/dist/type/migratorDialect.d.ts +0 -9
- package/dist/type/migratorDialect.js +1 -16
- package/dist/type/querierPool.d.ts +0 -4
- package/dist/type/queryRaw.d.ts +2 -2
- package/dist/type/universalQuerier.d.ts +2 -2
- package/package.json +1 -3
- package/dist/postgres/pgCursorStream.d.ts +0 -20
- package/dist/postgres/postgresWireDriverCapabilities.d.ts +0 -21
- package/dist/postgres/postgresWireDriverCapabilities.js +0 -21
- package/dist/sqlite/bunSqliteAdapter.bun.d.ts +0 -27
- package/dist/sqlite/bunSqliteAdapter.bun.js +0 -25
- package/dist/sqlite/nodeSqliteAdapter.d.ts +0 -33
- package/dist/sqlite/nodeSqliteAdapter.js +0 -25
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
2
|
import { isInlinedExpression } from '../../util/field.util.js';
|
|
3
|
-
import {
|
|
3
|
+
import { entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, lowerFirst, memberRefs, normalizeIndexColumn, upperFirst, definedEntries, } from '../../util/index.js';
|
|
4
4
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
5
5
|
/**
|
|
6
6
|
* A map held on `globalThis` through the global symbol registry, so a single one survives multiple
|
|
@@ -54,10 +54,11 @@ export function defineRelation(entity, key, opts) {
|
|
|
54
54
|
* exist, and the registry holds data alone.
|
|
55
55
|
*/
|
|
56
56
|
export function relationRegistration({ mappedBy, references, ...opts }) {
|
|
57
|
+
const joined = references?.(keyMap(), keyMap());
|
|
57
58
|
return {
|
|
58
59
|
...opts,
|
|
59
60
|
...(mappedBy && { mappedBy: mappedBy(keyMap()) }),
|
|
60
|
-
...(
|
|
61
|
+
...(joined && { references: typeof joined === 'string' ? joined : [...joined] }),
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
/** Every entity's key map: a callback only reads one property off it, and that property is its own key. */
|
|
@@ -70,8 +71,8 @@ function addRelation(entity, key, registration) {
|
|
|
70
71
|
throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
|
|
71
72
|
}
|
|
72
73
|
if (registration.through && registration.references) {
|
|
73
|
-
throw new TypeError(`'${entity.name}.${key}' joins through a junction, whose
|
|
74
|
-
"pairs the declaring entity's columns with the target's instead.");
|
|
74
|
+
throw new TypeError(`'${entity.name}.${key}' joins through a junction, whose column referencing each side is the join; ` +
|
|
75
|
+
"'references' pairs the declaring entity's columns with the target's instead.");
|
|
75
76
|
}
|
|
76
77
|
const meta = ensureWritableMeta(entity);
|
|
77
78
|
// Registration writes into a map declared as resolved: `getMeta` runs `fillRelations`, which settles
|
|
@@ -305,77 +306,109 @@ function ensureMeta(entity) {
|
|
|
305
306
|
return meta;
|
|
306
307
|
}
|
|
307
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) {
|
|
308
320
|
const meta = metas.get(entity);
|
|
309
321
|
if (!meta) {
|
|
310
322
|
throw TypeError(`'${entity.name}' is not an entity`);
|
|
311
323
|
}
|
|
312
|
-
|
|
313
|
-
return meta;
|
|
314
|
-
}
|
|
315
|
-
// Stamped before finalizing, not after: `fillInverseSide` reads the other side through `getMeta`,
|
|
316
|
-
// and with each side mapped by the other that recursion has to find this half-filled meta rather
|
|
317
|
-
// than run again. Finalizing twice is harmless anyway - every step of it skips what it settled.
|
|
318
|
-
meta.processedAt = meta.revision;
|
|
319
|
-
return fillRelations(meta);
|
|
324
|
+
return meta;
|
|
320
325
|
}
|
|
321
326
|
function fillRelations(meta) {
|
|
322
327
|
for (const [relKey, relation] of definedEntries(meta.relations)) {
|
|
323
|
-
// The registered view: `references` may be unset until this settles it.
|
|
324
|
-
const relOpts = relation;
|
|
325
328
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
326
|
-
if (
|
|
327
|
-
fillInverseSide(at, meta, relOpts, relOpts.mappedBy);
|
|
328
|
-
}
|
|
329
|
-
else if (!relOpts.references) {
|
|
330
|
-
fillOwningSide(at, meta, relKey, relOpts);
|
|
331
|
-
}
|
|
332
|
-
if (!relOpts.references?.length) {
|
|
329
|
+
if (!settledReferences(at, meta, relKey, relation).length) {
|
|
333
330
|
throw new TypeError(`${at} has no columns to join on.`);
|
|
334
331
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
332
|
+
}
|
|
333
|
+
// A column `references` names is a foreign key with or without a relation over it, and one cannot point
|
|
334
|
+
// at a composite key: refused on first read, as a relation that cannot join is, not at the schema build.
|
|
335
|
+
foreignKeysOf(meta);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
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.
|
|
341
|
+
*/
|
|
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 }].');
|
|
344
349
|
}
|
|
350
|
+
relOpts.references = [{ local: references, foreign: soleIdOf(target, 'a foreign key') }];
|
|
351
|
+
return relOpts.references;
|
|
345
352
|
}
|
|
346
|
-
|
|
347
|
-
|
|
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);
|
|
348
360
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
];
|
|
359
|
-
return;
|
|
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
|
+
}
|
|
360
370
|
}
|
|
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
|
+
];
|
|
383
|
+
return relOpts.references;
|
|
384
|
+
}
|
|
385
|
+
function fillOwningSide(at, meta, relKey, relOpts) {
|
|
361
386
|
if (isToManyRelation(relOpts)) {
|
|
362
387
|
throw new TypeError(`${at} is a to-many relation with no way to join: it needs 'mappedBy' (the field on the other side), ` +
|
|
363
388
|
"'through' (a junction entity), or 'references' (the columns).");
|
|
364
389
|
}
|
|
390
|
+
const relMeta = ensureMeta(relOpts.entity());
|
|
365
391
|
// `<rel>Id` for the one-key case it has always been; `<rel><Key>` per column otherwise. Both name a
|
|
366
392
|
// property, so both are spelled from the referenced *property* - a column name is what the naming
|
|
367
393
|
// strategy makes of this afterwards.
|
|
368
394
|
const sole = relMeta.ids.length === 1;
|
|
369
|
-
|
|
395
|
+
const references = relMeta.ids.map((key) => ({
|
|
370
396
|
local: sole ? `${relKey}Id` : `${relKey}${upperFirst(key)}`,
|
|
371
397
|
foreign: key,
|
|
372
398
|
}));
|
|
399
|
+
// A column the entity declares would be joined by its name alone, so renaming either one would leave
|
|
400
|
+
// the other behind, still compiling.
|
|
401
|
+
const fields = meta.fields;
|
|
402
|
+
if (references.some(({ local }) => fields[local])) {
|
|
403
|
+
const own = lowerFirst(meta.entity.name);
|
|
404
|
+
throw new TypeError(`${at} joins ${references.map(({ local }) => `'${local}'`).join(', ')} by name, which a rename does not ` +
|
|
405
|
+
`follow: link them with ${sole ? `'references: (${own}) => ${own}.${references[0].local}'` : "'references' pairs"}.`);
|
|
406
|
+
}
|
|
373
407
|
// `typeFromReference` so schema generation resolves the referenced primary key's exact type
|
|
374
408
|
// (columnType, length, chained keys) rather than trusting the fallback, as it does for an
|
|
375
409
|
// explicit `@Field({ references })`.
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
fields[local] ??= {
|
|
410
|
+
for (const { local, foreign } of references) {
|
|
411
|
+
fields[local] = {
|
|
379
412
|
name: local,
|
|
380
413
|
type: fieldOf(relMeta, foreign).type ?? Number,
|
|
381
414
|
references: relOpts.entity,
|
|
@@ -383,80 +416,87 @@ function fillOwningSide(at, meta, relKey, relOpts) {
|
|
|
383
416
|
typeFromReference: true,
|
|
384
417
|
};
|
|
385
418
|
}
|
|
419
|
+
relOpts.references = references;
|
|
420
|
+
return references;
|
|
386
421
|
}
|
|
387
422
|
function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
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);
|
|
392
427
|
if (relMeta.fields[mappedBy]) {
|
|
393
428
|
if (meta.ids.length > 1) {
|
|
394
|
-
throw new TypeError(`${at} is mapped by
|
|
429
|
+
throw new TypeError(`${at} is mapped by ${other}, one column, but the primary key of ` +
|
|
395
430
|
`'${meta.entity.name}' is composite (${meta.ids.join(', ')}). Map it by the relation on the other side ` +
|
|
396
431
|
'instead, which joins every column of the key.');
|
|
397
432
|
}
|
|
398
433
|
// `local` is this entity's own key, as in every other pair.
|
|
399
434
|
relOpts.references = [{ local: meta.ids[0], foreign: mappedBy }];
|
|
400
|
-
return;
|
|
435
|
+
return relOpts.references;
|
|
401
436
|
}
|
|
402
|
-
// Authored view again: with each side mapped by the other, the target is still mid-resolution here and
|
|
403
|
-
// its own `references` are unset, which is what the second throw reports.
|
|
404
437
|
const owner = relMeta.relations[mappedBy];
|
|
405
438
|
if (!owner) {
|
|
406
|
-
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}'.`);
|
|
407
440
|
}
|
|
408
|
-
if (
|
|
409
|
-
throw new TypeError(`${at} is mapped by
|
|
441
|
+
if (owner.mappedBy) {
|
|
442
|
+
throw new TypeError(`${at} is mapped by ${other}, an inverse side too, so neither owns the foreign key.`);
|
|
410
443
|
}
|
|
444
|
+
const ownerReferences = settledReferences(other, relMeta, mappedBy, owner);
|
|
411
445
|
// Two different flips: a junction's pairs are the owner's group followed by ours, so the two groups
|
|
412
446
|
// swap - `toReversed` would also reverse each group, pairing a composite's columns crosswise. A
|
|
413
447
|
// plain foreign key is one pair per key whose ends swap.
|
|
414
448
|
relOpts.references =
|
|
415
449
|
relOpts.cardinality === 'm1' || relOpts.cardinality === 'mm'
|
|
416
|
-
? [...
|
|
417
|
-
:
|
|
450
|
+
? [...ownerReferences.slice(relMeta.ids.length), ...ownerReferences.slice(0, relMeta.ids.length)]
|
|
451
|
+
: ownerReferences.map(({ local, foreign }) => ({ local: foreign, foreign: local }));
|
|
418
452
|
relOpts.through = owner.through;
|
|
453
|
+
return relOpts.references;
|
|
419
454
|
}
|
|
420
455
|
/**
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* only because some *other* entity pointed `through` at it. Gaps only, so a declared relation keeps its
|
|
425
|
-
* own cardinality and `cascade`.
|
|
456
|
+
* The foreign keys an entity holds: each owning to-one's columns, and each `@Field({ references })` no
|
|
457
|
+
* relation joins on, as the many-to-one it describes, once its target has registered a key. What the
|
|
458
|
+
* schema build constrains and a junction joins by, read once the relations holding them are settled.
|
|
426
459
|
*/
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
460
|
+
export function foreignKeysOf(meta) {
|
|
461
|
+
settleOwnColumns(meta);
|
|
462
|
+
const owning = definedEntries(meta.relations)
|
|
463
|
+
.map(([, relation]) => relation)
|
|
464
|
+
.filter((relation) => !relation.mappedBy && !relation.through && !isToManyRelation(relation));
|
|
465
|
+
const joined = new Set(owning.flatMap(({ references }) => references.map(({ local }) => local)));
|
|
466
|
+
const columns = definedEntries(meta.fields).flatMap(([key, field]) => {
|
|
467
|
+
if (!field.references || joined.has(key))
|
|
468
|
+
return [];
|
|
469
|
+
const target = ensureMeta(field.references());
|
|
434
470
|
if (!target.ids.length)
|
|
435
|
-
|
|
471
|
+
return [];
|
|
436
472
|
if (target.ids.length > 1) {
|
|
437
|
-
throw new TypeError(`'${meta.entity.name}.${
|
|
473
|
+
throw new TypeError(`'${meta.entity.name}.${key}' cannot reference '${target.entity.name}', whose primary key is composite ` +
|
|
438
474
|
`(${target.ids.join(', ')}): a column points at one. Use ` +
|
|
439
475
|
`'@ManyToOne({ entity: () => ${target.entity.name} })', which declares one column per key.`);
|
|
440
476
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
477
|
+
return [{ entity: field.references, cardinality: 'm1', references: [{ local: key, foreign: target.ids[0] }] }];
|
|
478
|
+
});
|
|
479
|
+
return [...owning, ...columns];
|
|
480
|
+
}
|
|
481
|
+
/** Each key of `side`, paired with the one column of `junction` referencing it: a rename of either follows. */
|
|
482
|
+
function junctionReferences(at, junction, side) {
|
|
483
|
+
const pairs = foreignKeysOf(junction).flatMap(({ entity, references }) => entity() === side.entity ? references : []);
|
|
484
|
+
return side.ids.map((key) => {
|
|
485
|
+
const [pair, ...others] = pairs.filter(({ foreign }) => foreign === key);
|
|
486
|
+
const referenced = `'${side.entity.name}.${key}'`;
|
|
487
|
+
if (!pair) {
|
|
488
|
+
const declare = side.ids.length > 1
|
|
489
|
+
? `@ManyToOne({ entity: () => ${side.entity.name} })`
|
|
490
|
+
: `@Field({ references: () => ${side.entity.name} })`;
|
|
491
|
+
throw new TypeError(`${at} joins through '${junction.entity.name}', which has no column referencing ${referenced}: declare one, '${declare}'.`);
|
|
492
|
+
}
|
|
493
|
+
if (others.length) {
|
|
494
|
+
const columns = [pair, ...others].map(({ local }) => `'${local}'`).join(' and ');
|
|
495
|
+
throw new TypeError(`${at} joins through '${junction.entity.name}', where ${columns} each reference ${referenced}: a junction ` +
|
|
496
|
+
'needs exactly one column per key of each side.');
|
|
497
|
+
}
|
|
498
|
+
return { local: pair.local, foreign: key };
|
|
499
|
+
});
|
|
460
500
|
}
|
|
461
501
|
/** Every key the entity marks, in declaration order. More than one is a composite primary key. */
|
|
462
502
|
function getIdKeys(meta) {
|
|
@@ -490,7 +530,12 @@ function extendMeta(target, source) {
|
|
|
490
530
|
}
|
|
491
531
|
}
|
|
492
532
|
target.fields = { ...sourceFields, ...target.fields };
|
|
493
|
-
|
|
533
|
+
// A copy of each relation per entity: resolving one writes the columns it joins on into it, and those
|
|
534
|
+
// columns are the resolving entity's, so one shared object left every later entity without them.
|
|
535
|
+
target.relations = {
|
|
536
|
+
...Object.fromEntries(definedEntries(source.relations).map(([key, relation]) => [key, { ...relation }])),
|
|
537
|
+
...target.relations,
|
|
538
|
+
};
|
|
494
539
|
// Inherit user-defined filters from the parent (child overrides by name). The built-in soft-delete
|
|
495
540
|
// filter + `meta.softDelete` are (re)derived from the merged fields in `defineEntity`.
|
|
496
541
|
if (source.filters) {
|
package/dist/migrate/cli.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ export declare function runGenerateFromEntities(migrator: Migrator, args: string
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function runTypes(migrator: Migrator, args: string[]): void;
|
|
16
16
|
export declare function runSync(migrator: Migrator, args: string[], config: Partial<Config>): Promise<void>;
|
|
17
|
-
export declare function runGenerateFromDb(migrator: Migrator, args: string[]
|
|
17
|
+
export declare function runGenerateFromDb(migrator: Migrator, args: string[]): Promise<void>;
|
|
18
18
|
export declare function runDriftCheck(migrator: Migrator, config: Partial<Config>): Promise<void>;
|
package/dist/migrate/cli.js
CHANGED
|
@@ -56,7 +56,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
56
56
|
break;
|
|
57
57
|
case 'generate:from-db':
|
|
58
58
|
case 'generate-from-db':
|
|
59
|
-
await runGenerateFromDb(migrator, filteredArgs.slice(1)
|
|
59
|
+
await runGenerateFromDb(migrator, filteredArgs.slice(1));
|
|
60
60
|
break;
|
|
61
61
|
case 'sync':
|
|
62
62
|
await runSync(migrator, filteredArgs.slice(1), config);
|
|
@@ -197,7 +197,7 @@ function readOutput(args) {
|
|
|
197
197
|
export async function runSync(migrator, args, config) {
|
|
198
198
|
// Pulling the database into entity files is what `generate:from-db` does; one implementation.
|
|
199
199
|
if (args.includes('--pull')) {
|
|
200
|
-
return runGenerateFromDb(migrator, args
|
|
200
|
+
return runGenerateFromDb(migrator, args);
|
|
201
201
|
}
|
|
202
202
|
const force = args.includes('--force');
|
|
203
203
|
const safe = !args.includes('--unsafe');
|
|
@@ -216,46 +216,32 @@ export async function runSync(migrator, args, config) {
|
|
|
216
216
|
await migrator.sync({ ...options, logging: true });
|
|
217
217
|
console.log('\nSchema sync completed.');
|
|
218
218
|
}
|
|
219
|
-
export async function runGenerateFromDb(migrator, args
|
|
219
|
+
export async function runGenerateFromDb(migrator, args) {
|
|
220
220
|
const outputDir = readOutput(args) ?? './src/entities';
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
// Ensure output directory exists
|
|
238
|
-
if (!fs.existsSync(outputDir)) {
|
|
239
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
240
|
-
}
|
|
241
|
-
// Write entity files
|
|
242
|
-
for (const entity of entities) {
|
|
243
|
-
const filePath = path.join(outputDir, entity.fileName);
|
|
244
|
-
fs.writeFileSync(filePath, entity.code, 'utf-8');
|
|
245
|
-
console.log(` ✓ ${entity.className} -> ${filePath}`);
|
|
246
|
-
}
|
|
247
|
-
console.log(`\nGenerated ${entities.length} entities to ${outputDir}`);
|
|
221
|
+
console.log('\nAnalyzing database schema...');
|
|
222
|
+
const ast = await migrator.schemaIntrospector.introspect();
|
|
223
|
+
const tableCount = ast.tables.size;
|
|
224
|
+
console.log(`Found ${tableCount} table(s): ${Array.from(ast.tables.keys()).join(', ')}`);
|
|
225
|
+
console.log('\nGenerating entities...');
|
|
226
|
+
const generator = createEntityCodeGenerator(ast, {
|
|
227
|
+
addSyncComments: true,
|
|
228
|
+
includeRelations: true,
|
|
229
|
+
includeIndexes: true,
|
|
230
|
+
});
|
|
231
|
+
const entities = generator.generateAll();
|
|
232
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
233
|
+
for (const entity of entities) {
|
|
234
|
+
const filePath = path.join(outputDir, entity.fileName);
|
|
235
|
+
fs.writeFileSync(filePath, entity.code, 'utf-8');
|
|
236
|
+
console.log(` ✓ ${entity.className} -> ${filePath}`);
|
|
248
237
|
}
|
|
238
|
+
console.log(`\nGenerated ${entities.length} entities to ${outputDir}`);
|
|
249
239
|
}
|
|
250
240
|
export async function runDriftCheck(migrator, config) {
|
|
251
241
|
if (!config.entities || config.entities.length === 0) {
|
|
252
242
|
console.error('No entities configured. Add entities to your uql config.');
|
|
253
243
|
process.exit(1);
|
|
254
244
|
}
|
|
255
|
-
else if (!migrator.schemaIntrospector) {
|
|
256
|
-
console.error('No introspector available. Check your pool configuration.');
|
|
257
|
-
process.exit(1);
|
|
258
|
-
}
|
|
259
245
|
else {
|
|
260
246
|
console.log('\nChecking for schema drift...');
|
|
261
247
|
const expectedAST = buildEntityAST(await migrator.getSchemaGenerator(), config.entities);
|
|
@@ -98,6 +98,11 @@ export declare class EntityCodeGenerator {
|
|
|
98
98
|
* Build outgoing relation (ManyToOne or OneToOne where this table has FK).
|
|
99
99
|
*/
|
|
100
100
|
private buildOutgoingRelation;
|
|
101
|
+
/**
|
|
102
|
+
* The `references` callback of a to-one: its foreign key column where that is the target's whole primary
|
|
103
|
+
* key, column pairs otherwise, and nothing when the columns do not pair up.
|
|
104
|
+
*/
|
|
105
|
+
private referencesSource;
|
|
101
106
|
/**
|
|
102
107
|
* Build incoming relation (OneToMany where other tables have FK to this).
|
|
103
108
|
*/
|
|
@@ -14,6 +14,7 @@ import { DEFAULT_FOREIGN_KEY_ACTION, } from '../../schema/types.js';
|
|
|
14
14
|
import { camelCase, lowerFirst, pascalCase, singularize } from '../../util/string.util.js';
|
|
15
15
|
import { buildFieldOptionsSource, fieldNeedsRaw } from './fieldOptionsSource.js';
|
|
16
16
|
import { buildIndexDecoratorSource, indexNeedsRaw, isPlainFieldIndex } from './indexDecoratorSource.js';
|
|
17
|
+
import { memberSource } from './sourceLiteral.js';
|
|
17
18
|
/**
|
|
18
19
|
* Generates TypeScript entity code from SchemaAST.
|
|
19
20
|
*/
|
|
@@ -234,17 +235,38 @@ export class EntityCodeGenerator {
|
|
|
234
235
|
}
|
|
235
236
|
// Decorator. `onDelete`/`onUpdate` only when introspection found a real referential action, so a
|
|
236
237
|
// round-trip through an unconstrained column stays as terse as before.
|
|
237
|
-
const
|
|
238
|
+
const options = [`entity: () => ${relatedClassName}`];
|
|
239
|
+
const references = this.referencesSource(rel, relatedClassName);
|
|
240
|
+
if (references)
|
|
241
|
+
options.push(`references: ${references}`);
|
|
238
242
|
if (rel.onDelete && rel.onDelete !== DEFAULT_FOREIGN_KEY_ACTION)
|
|
239
|
-
|
|
243
|
+
options.push(`onDelete: '${rel.onDelete}'`);
|
|
240
244
|
if (rel.onUpdate && rel.onUpdate !== DEFAULT_FOREIGN_KEY_ACTION)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
lines.push(` @${decoratorName}({ entity: () => ${relatedClassName}${fkActionsSource} })`);
|
|
245
|
+
options.push(`onUpdate: '${rel.onUpdate}'`);
|
|
246
|
+
lines.push(` @${decoratorName}({ ${options.join(', ')} })`);
|
|
244
247
|
// Property
|
|
245
248
|
lines.push(` ${propertyName}?: ${relatedClassName};`);
|
|
246
249
|
return lines.join('\n');
|
|
247
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The `references` callback of a to-one: its foreign key column where that is the target's whole primary
|
|
253
|
+
* key, column pairs otherwise, and nothing when the columns do not pair up.
|
|
254
|
+
*/
|
|
255
|
+
referencesSource(rel, relatedClassName) {
|
|
256
|
+
if (!rel.from.columns.length || rel.from.columns.length !== rel.to.columns.length) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
const member = (param, column) => memberSource(param, this.options.propertyNameTransformer(column.name));
|
|
260
|
+
const own = lowerFirst(this.options.classNameTransformer(rel.from.table.name));
|
|
261
|
+
const key = rel.to.table.primaryKey;
|
|
262
|
+
if (rel.from.columns.length === 1 && key.length === 1 && rel.to.columns[0].name === key[0].name) {
|
|
263
|
+
return `(${own}) => ${member(own, rel.from.columns[0])}`;
|
|
264
|
+
}
|
|
265
|
+
const target = lowerFirst(relatedClassName);
|
|
266
|
+
const [local, foreign] = own === target ? ['local', 'foreign'] : [own, target];
|
|
267
|
+
const pairs = rel.from.columns.map((column, i) => `{ local: ${member(local, column)}, foreign: ${member(foreign, rel.to.columns[i])} }`);
|
|
268
|
+
return `(${local}, ${foreign}) => [${pairs.join(', ')}]`;
|
|
269
|
+
}
|
|
248
270
|
/**
|
|
249
271
|
* Build incoming relation (OneToMany where other tables have FK to this).
|
|
250
272
|
*/
|
|
@@ -262,9 +284,9 @@ export class EntityCodeGenerator {
|
|
|
262
284
|
lines.push(' */');
|
|
263
285
|
}
|
|
264
286
|
// The inverse side, mapped by the related class's property that points back at this one.
|
|
265
|
-
const inverseProp = this.options.propertyNameTransformer(this.options.singularize(table.name));
|
|
266
287
|
const param = lowerFirst(relatedClassName);
|
|
267
|
-
|
|
288
|
+
const inverse = memberSource(param, this.options.propertyNameTransformer(this.options.singularize(table.name)));
|
|
289
|
+
lines.push(` @${decoratorName}({ entity: () => ${relatedClassName}, mappedBy: (${param}) => ${inverse} })`);
|
|
268
290
|
// Property
|
|
269
291
|
if (inverseType === 'OneToMany' || inverseType === 'ManyToMany') {
|
|
270
292
|
lines.push(` ${propertyName}?: ${relatedClassName}[];`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isVectorIndexType } from '../../type/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { memberSource, rawTag } from './sourceLiteral.js';
|
|
3
3
|
/**
|
|
4
4
|
* A vector index carries its metric in the operator class pgvector names after it
|
|
5
5
|
* (`vector_cosine_ops`), which is the only place introspection can recover it from. `@Index` requires
|
|
@@ -93,7 +93,3 @@ function indexEntrySource(entry, propertyName, param) {
|
|
|
93
93
|
const modifiers = significantModifiers(entry);
|
|
94
94
|
return modifiers.length === 0 ? column : `{ column: ${column}, ${modifiers.join(', ')} }`;
|
|
95
95
|
}
|
|
96
|
-
/** `user.email`, or `user['first-name']` for a property name that is no identifier. */
|
|
97
|
-
function memberSource(param, property) {
|
|
98
|
-
return isIdentifierName(property) ? `${param}.${property}` : `${param}[${quoted(property)}]`;
|
|
99
|
-
}
|
|
@@ -6,6 +6,8 @@ export declare function isIdentifierName(text: string): boolean;
|
|
|
6
6
|
* parsing.
|
|
7
7
|
*/
|
|
8
8
|
export declare function quoted(text: string): string;
|
|
9
|
+
/** `user.email`, or `user['first-name']` for a property name that is no identifier. */
|
|
10
|
+
export declare function memberSource(param: string, property: string): string;
|
|
9
11
|
/**
|
|
10
12
|
* SQL as a `raw` tagged template. A database reprints an expression as arbitrary text, and exactly
|
|
11
13
|
* three sequences can end or interpolate a template literal, so escaping those is the whole job.
|
|
@@ -10,6 +10,10 @@ export function isIdentifierName(text) {
|
|
|
10
10
|
export function quoted(text) {
|
|
11
11
|
return `'${text.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
12
12
|
}
|
|
13
|
+
/** `user.email`, or `user['first-name']` for a property name that is no identifier. */
|
|
14
|
+
export function memberSource(param, property) {
|
|
15
|
+
return isIdentifierName(property) ? `${param}.${property}` : `${param}[${quoted(property)}]`;
|
|
16
|
+
}
|
|
13
17
|
/**
|
|
14
18
|
* SQL as a `raw` tagged template. A database reprints an expression as arbitrary text, and exactly
|
|
15
19
|
* three sequences can end or interpolate a template literal, so escaping those is the whole job.
|
package/dist/migrate/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export * from './drift/index.js';
|
|
|
10
10
|
export * from './introspection/index.js';
|
|
11
11
|
export { migrationBuilderFor } from './migrationTarget.js';
|
|
12
12
|
export { type BuilderMigrationDefinition, defineBuilderMigration, defineMigration, Migrator } from './migrator.js';
|
|
13
|
-
export {
|
|
13
|
+
export { SqlSchemaGenerator } from './schemaGenerator.js';
|
|
14
14
|
export { DatabaseMigrationStorage } from './storage/databaseStorage.js';
|
|
15
15
|
export { JsonMigrationStorage } from './storage/jsonStorage.js';
|
|
16
16
|
export { MongoMigrationStorage } from './storage/mongoStorage.js';
|
package/dist/migrate/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export * from './introspection/index.js';
|
|
|
16
16
|
export { migrationBuilderFor } from './migrationTarget.js';
|
|
17
17
|
export { defineBuilderMigration, defineMigration, Migrator } from './migrator.js';
|
|
18
18
|
// Schema generators
|
|
19
|
-
export {
|
|
19
|
+
export { SqlSchemaGenerator } from './schemaGenerator.js';
|
|
20
20
|
// Storage implementations
|
|
21
21
|
export { DatabaseMigrationStorage } from './storage/databaseStorage.js';
|
|
22
22
|
export { JsonMigrationStorage } from './storage/jsonStorage.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { QuerierPool, SchemaIntrospector } from '../../type/index.js';
|
|
2
|
-
/** The introspector for `
|
|
3
|
-
export declare function introspectorFor(
|
|
2
|
+
/** The introspector for the engine `pool` runs on, reading `schema` where the engine has schemas to read. */
|
|
3
|
+
export declare function introspectorFor(pool: QuerierPool, schema?: string): SchemaIntrospector;
|
|
@@ -4,12 +4,9 @@ import { MariadbSchemaIntrospector, MysqlSchemaIntrospector } from './mysqlIntro
|
|
|
4
4
|
import { CockroachSchemaIntrospector, PostgresSchemaIntrospector } from './postgresIntrospector.js';
|
|
5
5
|
import { SqliteSchemaIntrospector } from './sqliteIntrospector.js';
|
|
6
6
|
/**
|
|
7
|
-
* Which introspector each engine gets
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Every entry is statically imported, so `uql-orm/migrate` still carries all of them; making the
|
|
11
|
-
* table's values dynamic imports would shrink that entry, at the cost of an async
|
|
12
|
-
* `createIntrospector` the constructor cannot await.
|
|
7
|
+
* Which introspector each engine gets; SQLite and MongoDB have no schemas, so they ignore the argument.
|
|
8
|
+
* Statically imported, so `uql-orm/migrate` carries every one: dynamic imports would shrink that entry,
|
|
9
|
+
* but `Migrator` builds its introspector in its constructor, which cannot await.
|
|
13
10
|
*/
|
|
14
11
|
const INTROSPECTORS = {
|
|
15
12
|
postgres: (pool, schema) => new PostgresSchemaIntrospector(pool, schema),
|
|
@@ -17,12 +14,10 @@ const INTROSPECTORS = {
|
|
|
17
14
|
mysql: (pool, schema) => new MysqlSchemaIntrospector(pool, schema),
|
|
18
15
|
mariadb: (pool, schema) => new MariadbSchemaIntrospector(pool, schema),
|
|
19
16
|
mssql: (pool, schema) => new MsSqlSchemaIntrospector(pool, schema),
|
|
20
|
-
// Neither has schemas to read: SQLite attaches database files and MongoDB takes its database from
|
|
21
|
-
// the connection, so both ignore the argument rather than filtering on it.
|
|
22
17
|
sqlite: (pool) => new SqliteSchemaIntrospector(pool),
|
|
23
18
|
mongodb: (pool) => new MongoSchemaIntrospector(pool),
|
|
24
19
|
};
|
|
25
|
-
/** The introspector for `
|
|
26
|
-
export function introspectorFor(
|
|
27
|
-
return INTROSPECTORS[dialectName]
|
|
20
|
+
/** The introspector for the engine `pool` runs on, reading `schema` where the engine has schemas to read. */
|
|
21
|
+
export function introspectorFor(pool, schema) {
|
|
22
|
+
return INTROSPECTORS[pool.dialect.dialectName](pool, schema);
|
|
28
23
|
}
|
|
@@ -10,14 +10,14 @@ export type MigrationSession = {
|
|
|
10
10
|
/** `work` in one transaction where the engine takes DDL in one; MongoDB creates collections outside any. */
|
|
11
11
|
transaction(work: () => Promise<void>): Promise<void>;
|
|
12
12
|
};
|
|
13
|
-
/** Everything a migrator does differently per engine family, chosen once from its
|
|
13
|
+
/** Everything a migrator does differently per engine family, chosen once from its pool. */
|
|
14
14
|
export type MigrationTarget = {
|
|
15
15
|
readonly source: MigrationSource;
|
|
16
|
-
storage(
|
|
17
|
-
/** The schema generator
|
|
18
|
-
generator(
|
|
19
|
-
withSession<T>(
|
|
16
|
+
storage(tableName: string | undefined): MigrationStorage;
|
|
17
|
+
/** The dialect's schema generator. Async because MongoDB's loads its optional peer. */
|
|
18
|
+
generator(): Promise<SchemaGenerator>;
|
|
19
|
+
withSession<T>(task: (session: MigrationSession) => Promise<T>): Promise<T>;
|
|
20
20
|
};
|
|
21
|
-
export declare function migrationTargetFor(
|
|
21
|
+
export declare function migrationTargetFor(pool: QuerierPool<Querier, MigratorDialect>, defaultForeignKeyAction?: ForeignKeyAction): MigrationTarget;
|
|
22
22
|
/** A builder running each operation on `querier`, as SQL or as MongoDB driver commands. */
|
|
23
23
|
export declare function migrationBuilderFor(querier: Querier): Promise<MigrationBuilder>;
|