uql-orm 0.24.7 → 0.25.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/README.md +1 -1
- package/dist/dialect/abstractSqlDialect.js +5 -1
- package/dist/migrate/bin.js +0 -0
- package/dist/migrate/codegen/entityCodeGenerator.js +10 -2
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +9 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +18 -0
- package/dist/migrate/migrator.d.ts +9 -0
- package/dist/migrate/migrator.js +32 -21
- package/dist/migrate/schemaGenerator.d.ts +19 -8
- package/dist/migrate/schemaGenerator.js +48 -5
- package/dist/querier/abstractSqlQuerier.js +3 -1
- package/dist/schema/schemaASTBuilder.d.ts +1 -1
- package/dist/schema/schemaASTBuilder.js +7 -2
- package/dist/sqlite/nodeSqliteQuerierPool.js +1 -0
- package/dist/sqlite/sqliteQuerierPool.d.ts +5 -0
- package/dist/sqlite/sqliteQuerierPool.js +7 -0
- package/dist/turso/tursoLocalQuerierPool.js +1 -0
- package/dist/type/entity.d.ts +18 -2
- package/dist/type/migration.d.ts +30 -9
- package/package.json +5 -6
- package/LICENSE.md +0 -22
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ from the browser to the server. The same object runs on every supported database
|
|
|
51
51
|
## Why UQL?
|
|
52
52
|
|
|
53
53
|
- **The fastest.** Wins [all 8 categories](https://uql-orm.dev/benchmark) of our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), beating even query builders like Knex and Kysely: ~2.4× faster than the runner-up on average, reaching over 4.6M ops/s on simple SELECTs.
|
|
54
|
-
- **Light.** Zero runtime dependencies,
|
|
54
|
+
- **Light.** Zero runtime dependencies, 305 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
|
|
55
55
|
- **Queries are data (JSON), not method chains.** Plain JSON in, typed rows out. There's no DSL to learn and nothing to compile.
|
|
56
56
|
- **Type-safe to the leaf.** Every key is autocompleted and checked against your entity, down to the fields of a populated relation. Operators are gated per field type, and [JSON/JSONB](https://uql-orm.dev/querying/json) dot-paths resolve each path's value type, so `$like` on a numeric column, or a typo'd path, is a compile error instead of a runtime surprise.
|
|
57
57
|
- **No codegen, no build step.** Entities are TypeScript classes, so your code *is* the schema. There's no `.prisma` file to regenerate and no generated client to keep in sync.
|
|
@@ -534,7 +534,11 @@ export class AbstractSqlDialect extends IndexSqlDialect {
|
|
|
534
534
|
}
|
|
535
535
|
}
|
|
536
536
|
appendInNin(ctx, field, op, val) {
|
|
537
|
-
|
|
537
|
+
if (!Array.isArray(val)) {
|
|
538
|
+
// Not covered by the types: `/http` casts client JSON straight to `Query`, so this arrives untyped.
|
|
539
|
+
throw TypeError(`${op} expects an array, got ${val === null ? 'null' : typeof val}`);
|
|
540
|
+
}
|
|
541
|
+
this.appendFieldSql(ctx, field, this.formatIn(ctx, val, op === '$nin'));
|
|
538
542
|
}
|
|
539
543
|
/**
|
|
540
544
|
* Build a comparison condition for a JSON field.
|
package/dist/migrate/bin.js
CHANGED
|
File without changes
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - JSDoc comments for sync-added fields
|
|
11
11
|
*/
|
|
12
12
|
import { canonicalToTypeScript } from '../../schema/canonicalType.js';
|
|
13
|
+
import { DEFAULT_FOREIGN_KEY_ACTION, } from '../../schema/types.js';
|
|
13
14
|
import { camelCase, pascalCase, singularize } from '../../util/string.util.js';
|
|
14
15
|
import { buildFieldOptionsSource } from './fieldOptionsSource.js';
|
|
15
16
|
/**
|
|
@@ -234,8 +235,15 @@ export class EntityCodeGenerator {
|
|
|
234
235
|
}
|
|
235
236
|
lines.push(' */');
|
|
236
237
|
}
|
|
237
|
-
// Decorator
|
|
238
|
-
|
|
238
|
+
// Decorator. `onDelete`/`onUpdate` only when introspection found a real referential action, so a
|
|
239
|
+
// round-trip through an unconstrained column stays as terse as before.
|
|
240
|
+
const fkActions = [];
|
|
241
|
+
if (rel.onDelete && rel.onDelete !== DEFAULT_FOREIGN_KEY_ACTION)
|
|
242
|
+
fkActions.push(`onDelete: '${rel.onDelete}'`);
|
|
243
|
+
if (rel.onUpdate && rel.onUpdate !== DEFAULT_FOREIGN_KEY_ACTION)
|
|
244
|
+
fkActions.push(`onUpdate: '${rel.onUpdate}'`);
|
|
245
|
+
const fkActionsSource = fkActions.length ? `, ${fkActions.join(', ')}` : '';
|
|
246
|
+
lines.push(` @${decoratorName}({ entity: () => ${relatedClassName}${fkActionsSource} })`);
|
|
239
247
|
// Property
|
|
240
248
|
lines.push(` ${propertyName}?: ${relatedClassName};`);
|
|
241
249
|
return lines.join('\n');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AbstractDialect } from '../../dialect/abstractDialect.js';
|
|
2
2
|
import type { ForeignKeyAction, IndexNode, TableNode } from '../../schema/types.js';
|
|
3
|
-
import type { FieldOptions, IndexSchema, InsertIdSource, NamingStrategy, SchemaDiff, SchemaGenerator, Type } from '../../type/index.js';
|
|
3
|
+
import type { CreateSchemaOptions, FieldOptions, IndexSchema, InsertIdSource, NamingStrategy, SchemaDiff, SchemaGenerator, Type } from '../../type/index.js';
|
|
4
4
|
import type { TableDefinition } from '../builder/types.js';
|
|
5
5
|
export declare class MongoSchemaGenerator extends AbstractDialect implements SchemaGenerator {
|
|
6
6
|
protected readonly defaultForeignKeyAction?: ForeignKeyAction | undefined;
|
|
@@ -8,6 +8,14 @@ export declare class MongoSchemaGenerator extends AbstractDialect implements Sch
|
|
|
8
8
|
protected readonly featureDefaults: import("../../type/dialect.js").DialectFeatures;
|
|
9
9
|
readonly insertIdSource: InsertIdSource;
|
|
10
10
|
constructor(namingStrategy?: NamingStrategy, defaultForeignKeyAction?: ForeignKeyAction | undefined);
|
|
11
|
+
/**
|
|
12
|
+
* A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
|
|
13
|
+
* defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
|
|
14
|
+
* and ignored for the same reason.
|
|
15
|
+
*/
|
|
16
|
+
generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
|
|
17
|
+
generateDropSchema(entities: readonly Type<unknown>[]): string[];
|
|
18
|
+
private selected;
|
|
11
19
|
generateCreateTable<E>(entity: Type<E>, _options?: {
|
|
12
20
|
ifNotExists?: boolean;
|
|
13
21
|
}): string[];
|
|
@@ -13,6 +13,24 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
13
13
|
super({ namingStrategy });
|
|
14
14
|
this.defaultForeignKeyAction = defaultForeignKeyAction;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
|
|
18
|
+
* defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
|
|
19
|
+
* and ignored for the same reason.
|
|
20
|
+
*/
|
|
21
|
+
generateCreateSchema(entities, options) {
|
|
22
|
+
return this.selected(entities, options?.only).flatMap((entity) => this.generateCreateTable(entity, options));
|
|
23
|
+
}
|
|
24
|
+
generateDropSchema(entities) {
|
|
25
|
+
return this.selected(entities).map((entity) => this.generateDropTable(this.resolveTableName(entity, getMeta(entity))));
|
|
26
|
+
}
|
|
27
|
+
selected(entities, only) {
|
|
28
|
+
if (!only) {
|
|
29
|
+
return entities;
|
|
30
|
+
}
|
|
31
|
+
const wanted = new Set(only);
|
|
32
|
+
return entities.filter((entity) => wanted.has(this.resolveTableName(entity, getMeta(entity))));
|
|
33
|
+
}
|
|
16
34
|
generateCreateTable(entity, _options) {
|
|
17
35
|
const meta = getMeta(entity);
|
|
18
36
|
const collectionName = this.resolveTableName(entity, meta);
|
|
@@ -104,6 +104,15 @@ export declare class Migrator {
|
|
|
104
104
|
safe?: boolean;
|
|
105
105
|
drop?: boolean;
|
|
106
106
|
}): Promise<string[]>;
|
|
107
|
+
/**
|
|
108
|
+
* New tables are emitted together, never one at a time: a single-entity AST has no other table for a
|
|
109
|
+
* relation to resolve against, so every cross-entity foreign key was dropped and generated schemas
|
|
110
|
+
* carried none. Spanning the graph is also what lets a cyclic relation (any `createdBy`
|
|
111
|
+
* back-reference) be created at all.
|
|
112
|
+
*
|
|
113
|
+
* Empty in, empty out, so a diff with no new tables does not build an AST for the whole graph.
|
|
114
|
+
*/
|
|
115
|
+
private createSchema;
|
|
107
116
|
/**
|
|
108
117
|
* Each pending diff with the entity it came from, since resolving that is async and every caller
|
|
109
118
|
* needs it. What to emit stays with the caller: a sync narrows the forward direction to what the
|
package/dist/migrate/migrator.js
CHANGED
|
@@ -234,20 +234,22 @@ export class Migrator {
|
|
|
234
234
|
* Generate a migration based on entity schema differences
|
|
235
235
|
*/
|
|
236
236
|
async generateFromEntities(name) {
|
|
237
|
-
const
|
|
237
|
+
const creating = [];
|
|
238
|
+
const altering = [];
|
|
238
239
|
const downStatements = [];
|
|
239
240
|
for (const { diff, entity } of await this.pendingDiffs()) {
|
|
240
241
|
if (diff.type === 'create') {
|
|
241
242
|
if (entity) {
|
|
242
|
-
|
|
243
|
+
creating.push(diff.tableName);
|
|
243
244
|
downStatements.push(this.generator.generateDropTable(diff.tableName, { ifExists: true }));
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
else if (diff.type === 'alter') {
|
|
247
|
-
|
|
248
|
+
altering.push(...this.generator.generateAlterTable(diff));
|
|
248
249
|
downStatements.push(...this.generator.generateAlterTableDown(diff));
|
|
249
250
|
}
|
|
250
251
|
}
|
|
252
|
+
const upStatements = [...this.createSchema(creating), ...altering];
|
|
251
253
|
if (upStatements.length === 0) {
|
|
252
254
|
this.logger.logInfo('No schema changes detected.');
|
|
253
255
|
return '';
|
|
@@ -317,21 +319,18 @@ export class Migrator {
|
|
|
317
319
|
*/
|
|
318
320
|
async syncForce() {
|
|
319
321
|
await this.ensureSchemaGenerator();
|
|
322
|
+
// Both directions span the whole entity set rather than looping an entity at a time. A per-entity
|
|
323
|
+
// AST cannot resolve a cross-entity foreign key, so the old create loop silently produced a schema
|
|
324
|
+
// with no referential integrity; and the old drop loop went in reverse *declaration* order, which
|
|
325
|
+
// says nothing about the relation graph and is rejected as soon as the constraints are really there.
|
|
326
|
+
const statements = [
|
|
327
|
+
...this.generator.generateDropSchema(this.entities, { ifExists: true, cascade: true }),
|
|
328
|
+
...this.generator.generateCreateSchema(this.entities),
|
|
329
|
+
];
|
|
320
330
|
await withSqlQuerierForMigrations(this.pool, 'Migrator', (querier) => querier.transaction(async () => {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const dropSql = this.generator.generateDropTable(tableName, { ifExists: true });
|
|
325
|
-
this.logger.logSchema(`Executing: ${dropSql}`);
|
|
326
|
-
await querier.run(dropSql);
|
|
327
|
-
}
|
|
328
|
-
// Create all tables
|
|
329
|
-
for (const entity of this.entities) {
|
|
330
|
-
const createStmts = this.generator.generateCreateTable(entity);
|
|
331
|
-
for (const createSql of createStmts) {
|
|
332
|
-
this.logger.logSchema(`Executing: ${createSql}`);
|
|
333
|
-
await querier.run(createSql);
|
|
334
|
-
}
|
|
331
|
+
for (const sql of statements) {
|
|
332
|
+
this.logger.logSchema(`Executing: ${sql}`);
|
|
333
|
+
await querier.run(sql);
|
|
335
334
|
}
|
|
336
335
|
}));
|
|
337
336
|
this.logger.logSchema('Schema sync (force) completed');
|
|
@@ -353,17 +352,29 @@ export class Migrator {
|
|
|
353
352
|
* statements rather than a summary of a second, differently-computed diff.
|
|
354
353
|
*/
|
|
355
354
|
async planSync(options = {}) {
|
|
356
|
-
const
|
|
355
|
+
const creating = [];
|
|
356
|
+
const altering = [];
|
|
357
357
|
for (const { diff, entity } of await this.pendingDiffs()) {
|
|
358
358
|
if (diff.type === 'create') {
|
|
359
359
|
if (entity)
|
|
360
|
-
|
|
360
|
+
creating.push(diff.tableName);
|
|
361
361
|
}
|
|
362
362
|
else if (diff.type === 'alter') {
|
|
363
|
-
|
|
363
|
+
altering.push(...this.generator.generateAlterTable(this.filterDiff(diff, options)));
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
|
-
return
|
|
366
|
+
return [...this.createSchema(creating), ...altering];
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* New tables are emitted together, never one at a time: a single-entity AST has no other table for a
|
|
370
|
+
* relation to resolve against, so every cross-entity foreign key was dropped and generated schemas
|
|
371
|
+
* carried none. Spanning the graph is also what lets a cyclic relation (any `createdBy`
|
|
372
|
+
* back-reference) be created at all.
|
|
373
|
+
*
|
|
374
|
+
* Empty in, empty out, so a diff with no new tables does not build an AST for the whole graph.
|
|
375
|
+
*/
|
|
376
|
+
createSchema(tableNames) {
|
|
377
|
+
return tableNames.length ? this.generator.generateCreateSchema(this.entities, { only: tableNames }) : [];
|
|
367
378
|
}
|
|
368
379
|
/**
|
|
369
380
|
* Each pending diff with the entity it came from, since resolving that is async and every caller
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
|
|
3
|
-
import type { ColumnSchema, DialectFeatures, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SqlDdlGenerator, Type } from '../type/index.js';
|
|
3
|
+
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SqlDdlGenerator, Type } from '../type/index.js';
|
|
4
4
|
import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
|
|
5
5
|
/**
|
|
6
6
|
* Unified SQL schema generator.
|
|
@@ -27,13 +27,24 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
27
27
|
*/
|
|
28
28
|
protected getCanonicalType(field: FieldOptions, fieldType?: unknown): CanonicalType;
|
|
29
29
|
protected canonicalTypeToSql(type: CanonicalType): string;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Every `CREATE TABLE` for `entities`, then their foreign keys.
|
|
32
|
+
*
|
|
33
|
+
* Two phases rather than inline constraints, because a relation graph is routinely cyclic: any
|
|
34
|
+
* `createdBy`-style back-reference makes `A` reference `B` while `B` references `A`, and no create
|
|
35
|
+
* order satisfies that. TypeORM's schema builder splits for the same reason (`createNewTables()`
|
|
36
|
+
* then `createForeignKeys()`). SQLite is the exception and keeps them inline: it cannot `ALTER` a
|
|
37
|
+
* foreign key in, but it resolves targets lazily, so a forward reference is fine there.
|
|
38
|
+
*/
|
|
39
|
+
generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
|
|
40
|
+
generateDropSchema(entities: readonly Type<unknown>[], options?: DropSchemaOptions): string[];
|
|
41
|
+
/**
|
|
42
|
+
* The tables of `entities` in dependency order, optionally narrowed to `only`. The AST always spans
|
|
43
|
+
* every entity even when narrowed, so a relation pointing at a table outside the subset still
|
|
44
|
+
* resolves instead of being silently dropped.
|
|
45
|
+
*/
|
|
46
|
+
private orderedTables;
|
|
47
|
+
generateDropTable(tableName: string, options?: DropSchemaOptions): string;
|
|
37
48
|
generateAlterTable(diff: SchemaDiff): string[];
|
|
38
49
|
generateAlterTableDown(diff: SchemaDiff): string[];
|
|
39
50
|
generateCreateIndex(tableName: string, index: IndexSchema, options?: {
|
|
@@ -49,11 +49,54 @@ export class SqlSchemaGenerator {
|
|
|
49
49
|
canonicalTypeToSql(type) {
|
|
50
50
|
return canonicalToSql(type, this.dialect);
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Every `CREATE TABLE` for `entities`, then their foreign keys.
|
|
54
|
+
*
|
|
55
|
+
* Two phases rather than inline constraints, because a relation graph is routinely cyclic: any
|
|
56
|
+
* `createdBy`-style back-reference makes `A` reference `B` while `B` references `A`, and no create
|
|
57
|
+
* order satisfies that. TypeORM's schema builder splits for the same reason (`createNewTables()`
|
|
58
|
+
* then `createForeignKeys()`). SQLite is the exception and keeps them inline: it cannot `ALTER` a
|
|
59
|
+
* foreign key in, but it resolves targets lazily, so a forward reference is fine there.
|
|
60
|
+
*/
|
|
61
|
+
generateCreateSchema(entities, options = {}) {
|
|
62
|
+
const tables = this.orderedTables(entities, 'create', options.only);
|
|
63
|
+
const withForeignKeys = options.foreignKeys ?? true;
|
|
64
|
+
// Inline only where a constraint cannot be added afterwards, which is what makes the cyclic case
|
|
65
|
+
// work everywhere else.
|
|
66
|
+
const inline = withForeignKeys && !this.features.foreignKeyAlter;
|
|
67
|
+
const statements = tables.flatMap((table) => this.generateCreateTableFromNode(inline ? table : { ...table, outgoingRelations: [] }, options));
|
|
68
|
+
if (withForeignKeys && !inline) {
|
|
69
|
+
for (const table of tables) {
|
|
70
|
+
for (const rel of table.outgoingRelations) {
|
|
71
|
+
statements.push(this.generateAddForeignKeySql(table.name, {
|
|
72
|
+
name: rel.name,
|
|
73
|
+
columns: rel.from.columns.map((c) => c.name),
|
|
74
|
+
referencesTable: rel.to.table.name,
|
|
75
|
+
referencesColumns: rel.to.columns.map((c) => c.name),
|
|
76
|
+
onDelete: rel.onDelete ?? this.defaultForeignKeyAction,
|
|
77
|
+
onUpdate: rel.onUpdate ?? this.defaultForeignKeyAction,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return statements;
|
|
83
|
+
}
|
|
84
|
+
generateDropSchema(entities, options = {}) {
|
|
85
|
+
return this.orderedTables(entities, 'drop').map((table) => this.generateDropTable(table.name, options));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The tables of `entities` in dependency order, optionally narrowed to `only`. The AST always spans
|
|
89
|
+
* every entity even when narrowed, so a relation pointing at a table outside the subset still
|
|
90
|
+
* resolves instead of being silently dropped.
|
|
91
|
+
*/
|
|
92
|
+
orderedTables(entities, direction, only) {
|
|
93
|
+
const ast = new SchemaASTBuilder(this.dialect.namingStrategy).fromEntities(entities);
|
|
94
|
+
const tables = direction === 'create' ? ast.getCreateOrder() : ast.getDropOrder();
|
|
95
|
+
if (!only) {
|
|
96
|
+
return tables;
|
|
97
|
+
}
|
|
98
|
+
const wanted = new Set(only);
|
|
99
|
+
return tables.filter((table) => wanted.has(table.name));
|
|
57
100
|
}
|
|
58
101
|
generateDropTable(tableName, options = {}) {
|
|
59
102
|
const ifExists = options.ifExists ? 'IF EXISTS ' : '';
|
|
@@ -231,10 +231,12 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
231
231
|
return 0;
|
|
232
232
|
}
|
|
233
233
|
const ids = founds.map((it) => it[meta.id]);
|
|
234
|
+
// Children first: they hold the foreign key, so deleting the parent ahead of them is rejected
|
|
235
|
+
// outright by any schema that declares the constraint without `ON DELETE CASCADE`.
|
|
236
|
+
await this.deleteRelations(entity, ids, opts);
|
|
234
237
|
const deleteCtx = this.dialect.createContext();
|
|
235
238
|
this.dialect.delete(deleteCtx, entity, { $where: ids }, opts);
|
|
236
239
|
const { changes = 0 } = await this.run(deleteCtx.sql, deleteCtx.values);
|
|
237
|
-
await this.deleteRelations(entity, ids, opts);
|
|
238
240
|
return changes;
|
|
239
241
|
}
|
|
240
242
|
get hasOpenTransaction() {
|
|
@@ -41,7 +41,7 @@ export declare class SchemaASTBuilder {
|
|
|
41
41
|
/**
|
|
42
42
|
* Build AST from entity classes (decorated with @Entity, @Field, etc.)
|
|
43
43
|
*/
|
|
44
|
-
fromEntities(entities: Type<unknown>[], options?: BuildFromEntitiesOptions): SchemaAST;
|
|
44
|
+
fromEntities(entities: readonly Type<unknown>[], options?: BuildFromEntitiesOptions): SchemaAST;
|
|
45
45
|
/**
|
|
46
46
|
* Resolve the canonical type for a field, inheriting from the referenced
|
|
47
47
|
* entity's primary key when the field is a foreign-key reference
|
|
@@ -181,8 +181,13 @@ export class SchemaASTBuilder {
|
|
|
181
181
|
type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
|
|
182
182
|
from: { table, columns: [localColumn] },
|
|
183
183
|
to: { table: relatedTable, columns: [foreignColumn] },
|
|
184
|
-
onDelete
|
|
185
|
-
|
|
184
|
+
// Falls back to the FK column's own `onDelete`, which is what makes a bare `@Field({
|
|
185
|
+
// references, onDelete })` work with no relation declared at all.
|
|
186
|
+
onDelete: relation.onDelete ??
|
|
187
|
+
localField.onDelete ??
|
|
188
|
+
options.defaultForeignKeyAction ??
|
|
189
|
+
this.defaultForeignKeyAction,
|
|
190
|
+
onUpdate: relation.onUpdate ?? options.defaultForeignKeyAction ?? this.defaultForeignKeyAction,
|
|
186
191
|
confidence: 1.0,
|
|
187
192
|
inferredFrom: 'entity_decorator',
|
|
188
193
|
};
|
|
@@ -24,6 +24,7 @@ export class NodeSqliteQuerierPool extends AbstractLocalSqliteQuerierPool {
|
|
|
24
24
|
...(extensions?.length ? { allowExtension: true } : undefined),
|
|
25
25
|
});
|
|
26
26
|
nodeDb.exec('PRAGMA journal_mode = WAL');
|
|
27
|
+
nodeDb.exec('PRAGMA foreign_keys = ON');
|
|
27
28
|
return adaptNodeSqlite(nodeDb);
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -11,5 +11,10 @@ export type Sqlite3PoolOptions = Options & LocalSqlitePoolOptions;
|
|
|
11
11
|
export declare class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool<Sqlite3PoolOptions> {
|
|
12
12
|
readonly filename: string | Buffer;
|
|
13
13
|
constructor(filename?: string | Buffer, opts?: Sqlite3PoolOptions, extra?: ExtraOptions);
|
|
14
|
+
/**
|
|
15
|
+
* SQLite ships with foreign keys unenforced, per connection, for backward compatibility. UQL emits the
|
|
16
|
+
* constraints in its DDL, so leaving them off means a declared `onDelete: 'CASCADE'` silently does
|
|
17
|
+
* nothing and a dangling reference is accepted. Enabled here on every driver, as TypeORM also does.
|
|
18
|
+
*/
|
|
14
19
|
protected createDb(): Promise<SqliteDatabase>;
|
|
15
20
|
}
|
|
@@ -9,6 +9,11 @@ export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
|
|
|
9
9
|
super(opts, extra);
|
|
10
10
|
this.filename = filename;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* SQLite ships with foreign keys unenforced, per connection, for backward compatibility. UQL emits the
|
|
14
|
+
* constraints in its DDL, so leaving them off means a declared `onDelete: 'CASCADE'` silently does
|
|
15
|
+
* nothing and a dangling reference is accepted. Enabled here on every driver, as TypeORM also does.
|
|
16
|
+
*/
|
|
12
17
|
async createDb() {
|
|
13
18
|
// `bun:sqlite` rejects option keys it does not know, and rejects an options object carrying no
|
|
14
19
|
// open flags, so `extensions` is stripped out and what remains of it collapses back to nothing.
|
|
@@ -19,11 +24,13 @@ export class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool {
|
|
|
19
24
|
const { adaptBunSqlite } = await import('./bunSqliteAdapter.bun.js');
|
|
20
25
|
const bunDb = new BunDatabase(this.filename, opts);
|
|
21
26
|
bunDb.run('PRAGMA journal_mode = WAL');
|
|
27
|
+
bunDb.run('PRAGMA foreign_keys = ON');
|
|
22
28
|
return adaptBunSqlite(bunDb);
|
|
23
29
|
}
|
|
24
30
|
const { default: BetterSqlite3 } = await import('better-sqlite3');
|
|
25
31
|
const db = new BetterSqlite3(this.filename, opts);
|
|
26
32
|
db.pragma('journal_mode = WAL');
|
|
33
|
+
db.pragma('foreign_keys = ON');
|
|
27
34
|
return db;
|
|
28
35
|
}
|
|
29
36
|
}
|
|
@@ -30,6 +30,7 @@ export class TursoLocalQuerierPool extends AbstractSqlQuerierPool {
|
|
|
30
30
|
// Annotated rather than cast, so the structural contract is checked against the real driver.
|
|
31
31
|
const db = await connect(this.filename, this.opts);
|
|
32
32
|
await db.pragma('journal_mode = WAL');
|
|
33
|
+
await db.pragma('foreign_keys = ON');
|
|
33
34
|
return db;
|
|
34
35
|
}
|
|
35
36
|
async end() {
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IndexType } from '../schema/types.js';
|
|
1
|
+
import type { ForeignKeyAction, IndexType } from '../schema/types.js';
|
|
2
2
|
import type { FilterOptions } from './query.js';
|
|
3
3
|
import type { QueryRaw } from './queryRaw.js';
|
|
4
4
|
import type { DistributiveOmit, Json, Scalar, Type, Unpacked } from './utility.js';
|
|
@@ -237,6 +237,13 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
|
|
|
237
237
|
* Entity that this field references (for foreign keys).
|
|
238
238
|
*/
|
|
239
239
|
readonly references?: EntityGetter;
|
|
240
|
+
/**
|
|
241
|
+
* Referential action for the generated foreign key. Delete side only: `onUpdate` below already means
|
|
242
|
+
* a value callback. Reach for `@ManyToOne({ onDelete, onUpdate })` when the update side matters too, or
|
|
243
|
+
* when this disagrees with a relation also declared on the same column (the relation wins).
|
|
244
|
+
* @example `@Field({ references: () => Company, onDelete: 'CASCADE' }) companyId?: string;`
|
|
245
|
+
*/
|
|
246
|
+
readonly onDelete?: ForeignKeyAction;
|
|
240
247
|
readonly virtual?: QueryRaw;
|
|
241
248
|
readonly updatable?: boolean;
|
|
242
249
|
readonly eager?: boolean;
|
|
@@ -362,6 +369,15 @@ export type RelationOptions<E = any> = {
|
|
|
362
369
|
entity: EntityGetter<E>;
|
|
363
370
|
cardinality: RelationCardinality;
|
|
364
371
|
readonly cascade?: boolean | CascadeType;
|
|
372
|
+
/**
|
|
373
|
+
* Referential actions for the generated foreign key, letting the database cascade instead of the ORM's
|
|
374
|
+
* `cascade` (pick one; declaring both leaves the FK nothing to do). Read from the owning side
|
|
375
|
+
* (`@ManyToOne`, or a `@OneToOne` without `mappedBy`). `onDelete` falls back to the FK field's own
|
|
376
|
+
* `@Field({ onDelete })` when unset here; `onUpdate` has no such fallback since that key already means
|
|
377
|
+
* a value callback on `FieldOptions`.
|
|
378
|
+
*/
|
|
379
|
+
readonly onDelete?: ForeignKeyAction;
|
|
380
|
+
readonly onUpdate?: ForeignKeyAction;
|
|
365
381
|
mappedBy?: RelationMappedBy<E>;
|
|
366
382
|
/**
|
|
367
383
|
* The pivot entity of a many-to-many. Unconstrained by `E`: a pivot holds foreign keys to both
|
|
@@ -392,7 +408,7 @@ type RelationOwnerJoin<E> = Required<Pick<RelationOptions<E>, 'through'>> | Requ
|
|
|
392
408
|
* without one of the three, resolution has no columns to join on and throws.
|
|
393
409
|
*/
|
|
394
410
|
type RelationJoin<E> = RelationOwnerJoin<E> | Required<Pick<RelationOptions<E>, 'mappedBy'>>;
|
|
395
|
-
type RelationOptionsOwner<E> = Pick<RelationOptions<E>, 'entity' | 'references' | 'cascade'>;
|
|
411
|
+
type RelationOptionsOwner<E> = Pick<RelationOptions<E>, 'entity' | 'references' | 'cascade' | 'onDelete' | 'onUpdate'>;
|
|
396
412
|
type RelationOptionsInverseSide<E> = Pick<RelationOptions<E>, 'entity' | 'cascade'> & Required<Pick<RelationOptions<E>, 'mappedBy'>>;
|
|
397
413
|
type RelationOptionsThroughOwner<E> = Pick<RelationOptions<E>, 'entity' | 'cascade'> & RelationOwnerJoin<E>;
|
|
398
414
|
/**
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -174,22 +174,43 @@ export interface SchemaDiff {
|
|
|
174
174
|
readonly foreignKeysToAdd?: ForeignKeySchema[];
|
|
175
175
|
readonly foreignKeysToDrop?: string[];
|
|
176
176
|
}
|
|
177
|
+
export interface CreateSchemaOptions {
|
|
178
|
+
readonly ifNotExists?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Restrict which tables are created, for an incremental migration adding one table to a schema that
|
|
181
|
+
* already exists. Constraints still resolve against the full entity graph.
|
|
182
|
+
*/
|
|
183
|
+
readonly only?: readonly string[];
|
|
184
|
+
/**
|
|
185
|
+
* Emit the tables without their foreign keys. Only the integration fixtures want this, and only until
|
|
186
|
+
* their data stops relying on dangling references; a migration always wants the constraints.
|
|
187
|
+
*/
|
|
188
|
+
readonly foreignKeys?: boolean;
|
|
189
|
+
}
|
|
190
|
+
export interface DropSchemaOptions {
|
|
191
|
+
readonly ifExists?: boolean;
|
|
192
|
+
readonly cascade?: boolean;
|
|
193
|
+
}
|
|
177
194
|
/**
|
|
178
195
|
* Interface for generating DDL statements from entity metadata
|
|
179
196
|
*/
|
|
180
197
|
export interface SchemaGenerator {
|
|
181
198
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
199
|
+
* The whole schema for `entities`: every table, then the foreign keys between them.
|
|
200
|
+
*
|
|
201
|
+
* There is deliberately no per-entity counterpart. One entity means an AST holding one table, so every
|
|
202
|
+
* cross-entity foreign key has nothing to resolve against and is dropped: all three call sites that
|
|
203
|
+
* used to work that way emitted schemas with no referential integrity.
|
|
184
204
|
*/
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
205
|
+
generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
|
|
206
|
+
/**
|
|
207
|
+
* Every `DROP TABLE` for `entities`, dependents first. The inverse of {@link generateCreateSchema},
|
|
208
|
+
* and the reason it takes the whole set: dropping in any order that ignores the relation graph is
|
|
209
|
+
* rejected once the foreign keys are really there.
|
|
210
|
+
*/
|
|
211
|
+
generateDropSchema(entities: readonly Type<unknown>[], options?: DropSchemaOptions): string[];
|
|
188
212
|
/** Generate DROP TABLE statement. */
|
|
189
|
-
generateDropTable(tableName: string, options?:
|
|
190
|
-
ifExists?: boolean;
|
|
191
|
-
cascade?: boolean;
|
|
192
|
-
}): string;
|
|
213
|
+
generateDropTable(tableName: string, options?: DropSchemaOptions): string;
|
|
193
214
|
/**
|
|
194
215
|
* Generate ALTER TABLE statements based on schema diff
|
|
195
216
|
*/
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "Extremely fast, type-safe TypeScript ORM - one API for every database",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.25.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
],
|
|
59
59
|
"scripts": {
|
|
60
60
|
"prepack": "bun run verify-dist.ts && cp ../../README.md .",
|
|
61
|
-
"postpack": "rm README.md",
|
|
61
|
+
"postpack": "rm README.md && npm pkg delete gitHead",
|
|
62
62
|
"compile.browser": "bun build src/browser/index.ts --minify --sourcemap=linked --format=esm --target=browser --outdir=dist/browser --entry-naming 'uql-browser.min.[ext]'",
|
|
63
63
|
"build": "bun run clean && tsc -b tsconfig.build.json && bun run compile.browser && bun run verify-dist.ts",
|
|
64
64
|
"start": "tsc --watch",
|
|
@@ -141,8 +141,8 @@
|
|
|
141
141
|
"mariadb": "^3.5.3",
|
|
142
142
|
"mongodb": "^7.5.0",
|
|
143
143
|
"mysql2": "^3.23.2",
|
|
144
|
-
"pg": "^8.
|
|
145
|
-
"pg-query-stream": "^4.
|
|
144
|
+
"pg": "^8.23.0",
|
|
145
|
+
"pg-query-stream": "^4.17.0",
|
|
146
146
|
"rxjs": "^7.8.2",
|
|
147
147
|
"sqlite-vec": "^0.1.9",
|
|
148
148
|
"ws": "^8.21.3"
|
|
@@ -197,6 +197,5 @@
|
|
|
197
197
|
],
|
|
198
198
|
"publishConfig": {
|
|
199
199
|
"access": "public"
|
|
200
|
-
}
|
|
201
|
-
"gitHead": "26ffc7e2968761bc95a0bb1ffe0aeb37600ba1f1"
|
|
200
|
+
}
|
|
202
201
|
}
|
package/LICENSE.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2015-present UQL Contributors
|
|
2
|
-
|
|
3
|
-
MIT License
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
-
a copy of this software and associated documentation files (the
|
|
7
|
-
"Software"), to deal in the Software without restriction, including
|
|
8
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
-
the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
|
14
|
-
included in all copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|