uql-orm 0.41.1 → 0.42.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/browser/querier/httpQuerier.d.ts +7 -7
- package/dist/browser/querier/httpQuerier.js +21 -6
- package/dist/browser/type/clientQuerier.d.ts +3 -3
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/dialect/abstractSqlDialect.d.ts +32 -3
- package/dist/dialect/abstractSqlDialect.js +58 -22
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +5 -3
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +2 -1
- package/dist/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/decorator/entity.js +1 -1
- package/dist/entity/decorator/members.d.ts +10 -2
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +18 -1
- package/dist/entity/metadata/definition.js +91 -36
- package/dist/http/handler.js +4 -4
- package/dist/maria/mariaDialect.d.ts +2 -2
- package/dist/maria/mariaDialect.js +3 -2
- package/dist/migrate/builder/tableBuilder.js +5 -4
- package/dist/migrate/drift/driftDetector.js +16 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +7 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +24 -28
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
- package/dist/migrate/introspection/postgresIntrospector.js +1 -1
- package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
- package/dist/migrate/migrator.js +6 -0
- package/dist/migrate/schemaGenerator.d.ts +44 -34
- package/dist/migrate/schemaGenerator.js +158 -141
- package/dist/mongo/mongoDialect.js +39 -14
- package/dist/mongo/mongodbQuerier.js +6 -3
- package/dist/postgres/postgresDialect.js +1 -1
- package/dist/querier/abstractQuerier.d.ts +16 -12
- package/dist/querier/abstractQuerier.js +88 -40
- package/dist/querier/abstractQuerierPool.d.ts +8 -8
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +12 -7
- package/dist/querier/relationCount.js +44 -33
- package/dist/schema/indexDifferences.d.ts +28 -0
- package/dist/schema/indexDifferences.js +46 -0
- package/dist/schema/schemaASTBuilder.js +38 -31
- package/dist/schema/schemaASTDiffer.d.ts +27 -1
- package/dist/schema/schemaASTDiffer.js +54 -18
- package/dist/schema/types.d.ts +46 -7
- package/dist/sqlite/sqliteDialect.d.ts +2 -1
- package/dist/sqlite/sqliteDialect.js +4 -1
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +57 -14
- package/dist/type/migration.d.ts +19 -0
- package/dist/type/queryWhere.d.ts +7 -2
- package/dist/type/universalQuerier.d.ts +8 -8
- package/dist/util/dialect.util.js +15 -14
- package/dist/util/field.util.d.ts +11 -1
- package/dist/util/field.util.js +12 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/object.util.d.ts +6 -0
- package/dist/util/object.util.js +18 -0
- package/dist/util/relationQuery.util.d.ts +48 -7
- package/dist/util/relationQuery.util.js +70 -9
- package/dist/util/rowKey.util.d.ts +9 -0
- package/dist/util/rowKey.util.js +24 -0
- package/dist/util/sql.util.d.ts +24 -7
- package/dist/util/sql.util.js +75 -10
- package/package.json +1 -1
|
@@ -31,6 +31,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
31
31
|
dropTableCascade: false,
|
|
32
32
|
renameColumn: true,
|
|
33
33
|
foreignKeyAlter: true,
|
|
34
|
+
primaryKeyAlter: true,
|
|
34
35
|
columnComment: true,
|
|
35
36
|
vectorIndexRequiresNotNull: false,
|
|
36
37
|
vectorSupportsLength: false,
|
|
@@ -62,7 +63,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
62
63
|
}
|
|
63
64
|
super.pager(ctx, opts);
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
+
serialType = 'BIGINT UNSIGNED AUTO_INCREMENT';
|
|
66
67
|
escapeIdChar = '`';
|
|
67
68
|
tableOptions = 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
|
|
68
69
|
beginTransactionCommand = 'START TRANSACTION';
|
|
@@ -70,6 +71,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
70
71
|
rollbackTransactionCommand = 'ROLLBACK';
|
|
71
72
|
isolationLevelStrategy = 'set-before';
|
|
72
73
|
dropForeignKeySyntax = 'DROP FOREIGN KEY';
|
|
74
|
+
dropPrimaryKeySyntax = 'DROP PRIMARY KEY';
|
|
73
75
|
dropIndexSyntax = 'on-table';
|
|
74
76
|
renameTableSyntax = 'rename-table';
|
|
75
77
|
alterColumnSyntax = 'MODIFY COLUMN';
|
|
@@ -91,7 +93,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
91
93
|
const alias = this.upsertNewRowAlias && this.escapeId(this.upsertNewRowAlias, true);
|
|
92
94
|
const updateCtx = this.createContext();
|
|
93
95
|
const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, (name) => alias ? `${alias}.${name}` : `VALUE(${name})`);
|
|
94
|
-
const returning = this.upsertReturning(
|
|
96
|
+
const returning = this.upsertReturning(meta);
|
|
95
97
|
if (update) {
|
|
96
98
|
this.appendInsertValues(ctx, entity, payload);
|
|
97
99
|
ctx.append(`${alias ? ` AS ${alias}` : ''} ON DUPLICATE KEY UPDATE ${update}${returning}`);
|
|
@@ -108,7 +110,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
108
110
|
* Appended to both branches above. Empty on MySQL, which has no `INSERT ... RETURNING`; MariaDB
|
|
109
111
|
* 10.5+ has it, and used to restate this whole method just to add it.
|
|
110
112
|
*/
|
|
111
|
-
upsertReturning(
|
|
113
|
+
upsertReturning(_meta) {
|
|
112
114
|
return '';
|
|
113
115
|
}
|
|
114
116
|
/**
|
|
@@ -15,7 +15,7 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
15
15
|
/** Default {@link DialectFeatures} for Postgres-wire dialects. */
|
|
16
16
|
protected readonly featureDefaults: DialectFeatures;
|
|
17
17
|
readonly escapeIdChar = "\"";
|
|
18
|
-
readonly
|
|
18
|
+
readonly serialType: string;
|
|
19
19
|
readonly tableOptions = "";
|
|
20
20
|
readonly beginTransactionCommand = "BEGIN";
|
|
21
21
|
readonly commitTransactionCommand = "COMMIT";
|
|
@@ -28,6 +28,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
28
28
|
dropTableCascade: true,
|
|
29
29
|
renameColumn: true,
|
|
30
30
|
foreignKeyAlter: true,
|
|
31
|
+
primaryKeyAlter: true,
|
|
31
32
|
columnComment: false,
|
|
32
33
|
vectorIndexRequiresNotNull: false,
|
|
33
34
|
vectorSupportsLength: true,
|
|
@@ -39,7 +40,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
39
40
|
// under heavy concurrent insert load (writes concentrate on one range); this default still beats
|
|
40
41
|
// `SERIAL` (CockroachDB's `unique_rowid()`, a ~64-bit value that overflows JS's safe-integer
|
|
41
42
|
// range). High-throughput CockroachDB users should override this per-entity with a UUID PK.
|
|
42
|
-
|
|
43
|
+
serialType = 'BIGINT GENERATED BY DEFAULT AS IDENTITY';
|
|
43
44
|
tableOptions = '';
|
|
44
45
|
beginTransactionCommand = 'BEGIN';
|
|
45
46
|
commitTransactionCommand = 'COMMIT';
|
|
@@ -21,7 +21,7 @@ export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity
|
|
|
21
21
|
* `E` is inferred from the class the returned decorator is applied to, which is what lets the column
|
|
22
22
|
* names be checked against it: `@Index(['nope'])` does not compile.
|
|
23
23
|
*
|
|
24
|
-
* @example `@Index(['lastName', 'firstName'], { name: '
|
|
24
|
+
* @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
|
|
25
25
|
* @example `@Index(['email'], { unique: true })`
|
|
26
26
|
* @example `@Index(['status'], { where: "status = 'active'" })`
|
|
27
27
|
*/
|
|
@@ -33,7 +33,7 @@ export function Filter(name, opts) {
|
|
|
33
33
|
* `E` is inferred from the class the returned decorator is applied to, which is what lets the column
|
|
34
34
|
* names be checked against it: `@Index(['nope'])` does not compile.
|
|
35
35
|
*
|
|
36
|
-
* @example `@Index(['lastName', 'firstName'], { name: '
|
|
36
|
+
* @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
|
|
37
37
|
* @example `@Index(['email'], { unique: true })`
|
|
38
38
|
* @example `@Index(['status'], { where: "status = 'active'" })`
|
|
39
39
|
*/
|
|
@@ -7,6 +7,14 @@ type MemberDecorator<V> = (value: undefined, context: ClassFieldDecoratorContext
|
|
|
7
7
|
* primary key's own type. Which is what makes `@Field({ references: () => User })` on a `number`, where
|
|
8
8
|
* `User.id` is a `uuid`, a compile error rather than a column that disagrees with its property.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* Maps any option the type does not declare to `never`, turning a typo into a compile error.
|
|
12
|
+
*
|
|
13
|
+
* Needed because the decorators capture their options as a naked type parameter, and TypeScript
|
|
14
|
+
* skips excess-property checking on one of those: `@Field({ nulable: true })` compiled and was
|
|
15
|
+
* silently ignored. Resolves to `unknown` - an inert intersection member - when there are none.
|
|
16
|
+
*/
|
|
17
|
+
type RejectUnknown<O, Known> = [Exclude<keyof O, keyof Known>] extends [never] ? unknown : Record<Exclude<keyof O, keyof Known> & string, never>;
|
|
10
18
|
/**
|
|
11
19
|
* The value type the options declare, which the decorated property is then checked against.
|
|
12
20
|
*
|
|
@@ -43,7 +51,7 @@ export declare function Field<O extends FieldOptions<DeclaredValue<O>> & ({
|
|
|
43
51
|
type: FieldType;
|
|
44
52
|
} | {
|
|
45
53
|
references: EntityGetter;
|
|
46
|
-
})
|
|
54
|
+
}) & RejectUnknown<O, FieldOptions>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
|
|
47
55
|
/**
|
|
48
56
|
* Declares the primary key, checked the same way as `@Field`.
|
|
49
57
|
*
|
|
@@ -52,7 +60,7 @@ export declare function Field<O extends FieldOptions<DeclaredValue<O>> & ({
|
|
|
52
60
|
*/
|
|
53
61
|
export declare function Id<O extends FieldOptions<DeclaredValue<O>> & {
|
|
54
62
|
type: FieldType;
|
|
55
|
-
}
|
|
63
|
+
} & RejectUnknown<O, FieldOptions>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
|
|
56
64
|
/**
|
|
57
65
|
* `E` comes from the mandatory `entity` getter, so the context can insist the property really holds that
|
|
58
66
|
* entity: `@ManyToOne({ entity: () => Other })` on a `Company` field stops compiling, and a to-many
|
package/dist/entity/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export * from './decorator/entity.js';
|
|
2
2
|
export * from './decorator/members.js';
|
|
3
|
-
export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, } from './metadata/definition.js';
|
|
3
|
+
export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, assertSoleId, idOf, soleIdOf, } from './metadata/definition.js';
|
package/dist/entity/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export * from './decorator/entity.js';
|
|
2
2
|
export * from './decorator/members.js';
|
|
3
|
-
export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, } from './metadata/definition.js';
|
|
3
|
+
export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, assertSoleId, idOf, soleIdOf, } from './metadata/definition.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityIndexInput, EntityMeta, EntityOptions, FieldKey, FieldOptions, FilterOptions, HookEvent, RelationOptions, Type } from '../../type/index.js';
|
|
1
|
+
import type { EntityId, EntityIndexInput, EntityMeta, EntityOptions, FieldKey, FieldOptions, FilterOptions, HookEvent, IdKey, RelationOptions, Type } from '../../type/index.js';
|
|
2
2
|
export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
|
|
3
3
|
export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
|
|
4
4
|
export declare function defineRelation<E>(entity: Type<E>, key: string, opts: RelationOptions): EntityMeta<E>;
|
|
@@ -25,6 +25,23 @@ type MemberSpecs = {
|
|
|
25
25
|
*/
|
|
26
26
|
export declare function applyMembers<E>(entity: Type<E>, specs: MemberSpecs | undefined): void;
|
|
27
27
|
export declare function defineEntity<E>(entity: Type<E>, opts?: EntityOptions<E>): EntityMeta<E>;
|
|
28
|
+
/**
|
|
29
|
+
* Refuses an entity whose primary key is not one column, naming the path that cannot express it.
|
|
30
|
+
*
|
|
31
|
+
* Refusing rather than taking the first of several is the whole guarantee: pairing one column of a
|
|
32
|
+
* two-column key is how a statement silently addresses rows that merely agree on it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assertSoleId<E>(meta: EntityMeta<E>, what: string): void;
|
|
35
|
+
/** The entity's one primary key, for a path that cannot express a composite. See {@link assertSoleId}. */
|
|
36
|
+
export declare function soleIdOf<E>(meta: EntityMeta<E>, what: string): IdKey<E>;
|
|
37
|
+
/**
|
|
38
|
+
* A row's primary key in the shape a `$where` takes: the value itself for a single key, an object
|
|
39
|
+
* carrying every key for a composite - which is exactly {@link EntityId}.
|
|
40
|
+
*
|
|
41
|
+
* What a settled write names its rows by, and what an insert hands back. Naming a composite row by
|
|
42
|
+
* one of its columns would address every row agreeing on that one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function idOf<E>(meta: EntityMeta<E>, row: E): EntityId<E>;
|
|
28
45
|
export declare function getEntities(): Type<unknown>[];
|
|
29
46
|
export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
|
|
30
47
|
export {};
|
|
@@ -24,12 +24,6 @@ export function defineField(entity, key, opts = {}) {
|
|
|
24
24
|
return meta;
|
|
25
25
|
}
|
|
26
26
|
export function defineId(entity, key, opts) {
|
|
27
|
-
const meta = ensureMeta(entity);
|
|
28
|
-
const id = getIdKey(meta);
|
|
29
|
-
if (id) {
|
|
30
|
-
// A subclass narrowing the inherited primary key: drop the old one so exactly one stays marked.
|
|
31
|
-
delete meta.fields[id];
|
|
32
|
-
}
|
|
33
27
|
return defineField(entity, key, { ...opts, isId: true });
|
|
34
28
|
}
|
|
35
29
|
// `RelationOptions` is parameterized by the *target* entity, which is independent of the owner `E`, so it
|
|
@@ -167,16 +161,51 @@ export function defineEntity(entity, opts = {}) {
|
|
|
167
161
|
meta.filters = {};
|
|
168
162
|
meta.filters[SOFT_DELETE_FILTER] = { condition: { [meta.softDelete]: null }, default: true };
|
|
169
163
|
}
|
|
170
|
-
const
|
|
171
|
-
if (!
|
|
172
|
-
throw TypeError(`'${entity.name}' must have
|
|
164
|
+
const ids = getIdKeys(meta);
|
|
165
|
+
if (!ids.length) {
|
|
166
|
+
throw TypeError(`'${entity.name}' must have at least one id field (use @Id, defineId, or defineEntity({ fields: { ..., isId: true } }))`);
|
|
173
167
|
}
|
|
174
|
-
meta.
|
|
168
|
+
meta.ids = ids;
|
|
175
169
|
return meta;
|
|
176
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Refuses an entity whose primary key is not one column, naming the path that cannot express it.
|
|
173
|
+
*
|
|
174
|
+
* Refusing rather than taking the first of several is the whole guarantee: pairing one column of a
|
|
175
|
+
* two-column key is how a statement silently addresses rows that merely agree on it.
|
|
176
|
+
*/
|
|
177
|
+
export function assertSoleId(meta, what) {
|
|
178
|
+
const { ids } = meta;
|
|
179
|
+
if (ids.length === 1) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
throw new TypeError(ids.length
|
|
183
|
+
? `'${meta.entity.name}' has a composite primary key (${ids.join(', ')}), which ${what} does not support yet.`
|
|
184
|
+
: // An entity registered with `@Field` but no `@Entity` never ran the check in `defineEntity`.
|
|
185
|
+
`'${meta.entity.name}' has no primary key, which ${what} needs.`);
|
|
186
|
+
}
|
|
187
|
+
/** The entity's one primary key, for a path that cannot express a composite. See {@link assertSoleId}. */
|
|
188
|
+
export function soleIdOf(meta, what) {
|
|
189
|
+
assertSoleId(meta, what);
|
|
190
|
+
return meta.ids[0];
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* A row's primary key in the shape a `$where` takes: the value itself for a single key, an object
|
|
194
|
+
* carrying every key for a composite - which is exactly {@link EntityId}.
|
|
195
|
+
*
|
|
196
|
+
* What a settled write names its rows by, and what an insert hands back. Naming a composite row by
|
|
197
|
+
* one of its columns would address every row agreeing on that one.
|
|
198
|
+
*/
|
|
199
|
+
export function idOf(meta, row) {
|
|
200
|
+
const { ids } = meta;
|
|
201
|
+
if (ids.length === 1) {
|
|
202
|
+
return row[ids[0]];
|
|
203
|
+
}
|
|
204
|
+
return Object.fromEntries(ids.map((key) => [key, row[key]]));
|
|
205
|
+
}
|
|
177
206
|
export function getEntities() {
|
|
178
207
|
return [...metas.entries()].reduce((acc, [key, val]) => {
|
|
179
|
-
if (val.
|
|
208
|
+
if (val.ids.length) {
|
|
180
209
|
acc.push(key);
|
|
181
210
|
}
|
|
182
211
|
return acc;
|
|
@@ -187,7 +216,7 @@ function ensureMeta(entity) {
|
|
|
187
216
|
if (meta) {
|
|
188
217
|
return meta;
|
|
189
218
|
}
|
|
190
|
-
meta = { entity,
|
|
219
|
+
meta = { entity, ids: [], fields: {}, relations: {} };
|
|
191
220
|
metas.set(entity, meta);
|
|
192
221
|
return meta;
|
|
193
222
|
}
|
|
@@ -210,7 +239,7 @@ function fillRelations(meta) {
|
|
|
210
239
|
continue;
|
|
211
240
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
212
241
|
if (relOpts.mappedBy) {
|
|
213
|
-
fillInverseSide(at, relOpts);
|
|
242
|
+
fillInverseSide(at, meta, relOpts);
|
|
214
243
|
}
|
|
215
244
|
else if (!relOpts.references) {
|
|
216
245
|
fillOwningSide(at, meta, relKey, relOpts);
|
|
@@ -234,13 +263,13 @@ function fillRelations(meta) {
|
|
|
234
263
|
}
|
|
235
264
|
function fillOwningSide(at, meta, relKey, relOpts) {
|
|
236
265
|
const relMeta = ensureMeta(relOpts.entity());
|
|
237
|
-
const relIdKey = relMeta.id;
|
|
238
266
|
if (relOpts.through) {
|
|
239
267
|
// Both columns live on the junction, whatever the cardinality: `fillToManyThroughRelation`,
|
|
240
|
-
// `deleteRelations` and every dialect read them as junction columns.
|
|
268
|
+
// `deleteRelations` and every dialect read them as junction columns. A composite key contributes
|
|
269
|
+
// one pair per column of it, which is what makes the join address a whole key rather than part.
|
|
241
270
|
relOpts.references = [
|
|
242
|
-
{ local: junctionColumn(meta,
|
|
243
|
-
{ local: junctionColumn(relMeta,
|
|
271
|
+
...meta.ids.map((key) => ({ local: junctionColumn(meta, key), foreign: key })),
|
|
272
|
+
...relMeta.ids.map((key) => ({ local: junctionColumn(relMeta, key), foreign: key })),
|
|
244
273
|
];
|
|
245
274
|
return;
|
|
246
275
|
}
|
|
@@ -248,21 +277,29 @@ function fillOwningSide(at, meta, relKey, relOpts) {
|
|
|
248
277
|
throw new TypeError(`${at} is a to-many relation with no way to join: it needs 'mappedBy' (the field on the other side), ` +
|
|
249
278
|
"'through' (a junction entity), or 'references' (the columns).");
|
|
250
279
|
}
|
|
251
|
-
|
|
252
|
-
|
|
280
|
+
// `<rel>Id` for the one-key case it has always been; `<rel><Key>` per column otherwise. Both name a
|
|
281
|
+
// property, so both are spelled from the referenced *property* - a column name is what the naming
|
|
282
|
+
// strategy makes of this afterwards.
|
|
283
|
+
const sole = relMeta.ids.length === 1;
|
|
284
|
+
relOpts.references = relMeta.ids.map((key) => ({
|
|
285
|
+
local: sole ? `${relKey}Id` : `${relKey}${upperFirst(key)}`,
|
|
286
|
+
foreign: key,
|
|
287
|
+
}));
|
|
253
288
|
// `typeFromReference` so schema generation resolves the referenced primary key's exact type
|
|
254
289
|
// (columnType, length, chained keys) rather than trusting the fallback, as it does for an
|
|
255
290
|
// explicit `@Field({ references })`.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
291
|
+
const fields = meta.fields;
|
|
292
|
+
for (const { local, foreign } of relOpts.references) {
|
|
293
|
+
fields[local] ??= {
|
|
294
|
+
name: local,
|
|
295
|
+
type: relMeta.fields[foreign]?.type ?? Number,
|
|
260
296
|
references: relOpts.entity,
|
|
297
|
+
referencedKey: foreign,
|
|
261
298
|
typeFromReference: true,
|
|
262
299
|
};
|
|
263
300
|
}
|
|
264
301
|
}
|
|
265
|
-
function fillInverseSide(at, relOpts) {
|
|
302
|
+
function fillInverseSide(at, meta, relOpts) {
|
|
266
303
|
const relEntity = relOpts.entity();
|
|
267
304
|
const relMeta = getMeta(relEntity);
|
|
268
305
|
const mappedBy = getMappedByKey(relOpts);
|
|
@@ -270,7 +307,13 @@ function fillInverseSide(at, relOpts) {
|
|
|
270
307
|
if (relOpts.references)
|
|
271
308
|
return;
|
|
272
309
|
if (relMeta.fields[mappedBy]) {
|
|
273
|
-
|
|
310
|
+
if (meta.ids.length > 1) {
|
|
311
|
+
throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', one column, but the primary key of ` +
|
|
312
|
+
`'${meta.entity.name}' is composite (${meta.ids.join(', ')}). Map it by the relation on the other side ` +
|
|
313
|
+
'instead, which joins every column of the key.');
|
|
314
|
+
}
|
|
315
|
+
// `local` is this entity's own key, as in every other pair.
|
|
316
|
+
relOpts.references = [{ local: meta.ids[0], foreign: mappedBy }];
|
|
274
317
|
return;
|
|
275
318
|
}
|
|
276
319
|
// Authored view again: with each side mapped by the other, the target is still mid-resolution here and
|
|
@@ -282,11 +325,12 @@ function fillInverseSide(at, relOpts) {
|
|
|
282
325
|
if (!owner.references?.length) {
|
|
283
326
|
throw new TypeError(`${at} is mapped by '${relEntity.name}.${mappedBy}', an inverse side too, so neither owns the foreign key.`);
|
|
284
327
|
}
|
|
285
|
-
// Two different flips: a junction
|
|
286
|
-
//
|
|
328
|
+
// Two different flips: a junction's pairs are the owner's group followed by ours, so the two groups
|
|
329
|
+
// swap - `toReversed` would also reverse each group, pairing a composite's columns crosswise. A
|
|
330
|
+
// plain foreign key is one pair per key whose ends swap.
|
|
287
331
|
relOpts.references =
|
|
288
332
|
relOpts.cardinality === 'm1' || relOpts.cardinality === 'mm'
|
|
289
|
-
? owner.references.
|
|
333
|
+
? [...owner.references.slice(relMeta.ids.length), ...owner.references.slice(0, relMeta.ids.length)]
|
|
290
334
|
: owner.references.map(({ local, foreign }) => ({ local: foreign, foreign: local }));
|
|
291
335
|
relOpts.through = owner.through;
|
|
292
336
|
}
|
|
@@ -303,7 +347,16 @@ function fillForeignKeyRelations(meta) {
|
|
|
303
347
|
const references = meta.fields[fieldKey]?.references;
|
|
304
348
|
if (!references || joined.has(fieldKey))
|
|
305
349
|
continue;
|
|
306
|
-
const
|
|
350
|
+
const target = ensureMeta(references());
|
|
351
|
+
// Nothing to derive from an entity that has not registered its own fields yet.
|
|
352
|
+
if (!target.ids.length)
|
|
353
|
+
continue;
|
|
354
|
+
if (target.ids.length > 1) {
|
|
355
|
+
throw new TypeError(`'${meta.entity.name}.${fieldKey}' cannot reference '${target.entity.name}', whose primary key is composite ` +
|
|
356
|
+
`(${target.ids.join(', ')}): a column points at one. Use ` +
|
|
357
|
+
`'@ManyToOne({ entity: () => ${target.entity.name} })', which declares one column per key.`);
|
|
358
|
+
}
|
|
359
|
+
const [foreign] = target.ids;
|
|
307
360
|
// The relation takes the column's name minus the key it points at (`itemId` -> `item`); a column
|
|
308
361
|
// named anything else has no name to take, so it stays a plain foreign key.
|
|
309
362
|
const suffix = upperFirst(foreign);
|
|
@@ -330,16 +383,18 @@ function getMappedByKey(relOpts) {
|
|
|
330
383
|
? relOpts.mappedBy(RELATION_KEY_MAP)
|
|
331
384
|
: relOpts.mappedBy;
|
|
332
385
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return
|
|
386
|
+
/** Every key the entity marks, in declaration order. More than one is a composite primary key. */
|
|
387
|
+
function getIdKeys(meta) {
|
|
388
|
+
return getKeys(meta.fields).filter((key) => meta.fields[key]?.isId);
|
|
336
389
|
}
|
|
337
390
|
function extendMeta(target, source) {
|
|
338
391
|
const sourceFields = { ...source.fields };
|
|
339
|
-
|
|
340
|
-
//
|
|
341
|
-
if (
|
|
342
|
-
|
|
392
|
+
// A subclass declaring its own primary key drops the parent's - every column of it, or a composite
|
|
393
|
+
// parent would leave its remaining keys marked and silently widen the child's key.
|
|
394
|
+
if (getIdKeys(target).length) {
|
|
395
|
+
for (const key of getIdKeys(source)) {
|
|
396
|
+
delete sourceFields[key];
|
|
397
|
+
}
|
|
343
398
|
}
|
|
344
399
|
target.fields = { ...sourceFields, ...target.fields };
|
|
345
400
|
target.relations = { ...source.relations, ...target.relations };
|
package/dist/http/handler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { withContext } from '../context/context.js';
|
|
2
|
-
import { getEntities, getMeta } from '../entity/index.js';
|
|
2
|
+
import { getEntities, getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { entityPath, matchRoute } from './contract.js';
|
|
4
4
|
import { parseQueryParams } from './query.js';
|
|
5
5
|
/** `Company (crm.Company)`: the class, and the table it maps, which is what tells two apart. */
|
|
@@ -142,8 +142,8 @@ export function createRequestHandler(opts) {
|
|
|
142
142
|
const founds = await querier.findMany(entity, query);
|
|
143
143
|
let ids = [];
|
|
144
144
|
let count = 0;
|
|
145
|
-
if (founds.length
|
|
146
|
-
const idKey = meta
|
|
145
|
+
if (founds.length) {
|
|
146
|
+
const idKey = soleIdOf(meta, 'the HTTP handler');
|
|
147
147
|
ids = founds.map((found) => found[idKey]);
|
|
148
148
|
count = await querier.deleteMany(entity, { $where: ids }, { hardDelete });
|
|
149
149
|
}
|
|
@@ -157,7 +157,7 @@ function ok(body) {
|
|
|
157
157
|
return { status: 200, body };
|
|
158
158
|
}
|
|
159
159
|
function buildIdQuery(meta, id, query) {
|
|
160
|
-
const idKey = meta
|
|
160
|
+
const idKey = soleIdOf(meta, 'the HTTP handler');
|
|
161
161
|
const where = query.$where;
|
|
162
162
|
if (Array.isArray(where)) {
|
|
163
163
|
query.$where = { $and: [{ [idKey]: { $in: where } }, { [idKey]: id }] };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
|
|
2
|
-
import type { DialectFeatures, FieldOptions, Query, QueryContext, QueryOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
2
|
+
import type { DialectFeatures, EntityMeta, FieldOptions, Query, QueryContext, QueryOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
3
3
|
export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
4
4
|
readonly dialectName = "mariadb";
|
|
5
5
|
readonly insertIdSource = "returning";
|
|
@@ -10,7 +10,7 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
10
10
|
* and `CREATE INDEX` takes `IF NOT EXISTS` - which MySQL's grammar has no place for.
|
|
11
11
|
*/
|
|
12
12
|
protected readonly featureOverrides: Partial<DialectFeatures>;
|
|
13
|
-
protected upsertReturning<E>(
|
|
13
|
+
protected upsertReturning<E>(meta: EntityMeta<E>): string;
|
|
14
14
|
/**
|
|
15
15
|
* MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
|
|
16
16
|
* reads a scalar and `JSON_EXTRACT` the subtree that the array operators need.
|
|
@@ -18,8 +18,9 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
18
18
|
vectorIndexRequiresNotNull: true,
|
|
19
19
|
indexIfNotExists: true,
|
|
20
20
|
};
|
|
21
|
-
upsertReturning(
|
|
22
|
-
|
|
21
|
+
upsertReturning(meta) {
|
|
22
|
+
const returning = this.returningId(meta);
|
|
23
|
+
return returning ? ` ${returning}` : '';
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
|
|
@@ -154,10 +154,10 @@ export class TableBuilder {
|
|
|
154
154
|
return this;
|
|
155
155
|
}
|
|
156
156
|
unique(columns, options) {
|
|
157
|
-
return this.addIndex(
|
|
157
|
+
return this.addIndex(columns, options, true);
|
|
158
158
|
}
|
|
159
159
|
index(columns, options) {
|
|
160
|
-
return this.addIndex(
|
|
160
|
+
return this.addIndex(columns, options, false);
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
163
|
* `@Index` and `table.index(...)` differ only in how the name is defaulted, so both normalize their
|
|
@@ -165,12 +165,13 @@ export class TableBuilder {
|
|
|
165
165
|
* form, and anything left as a bare string would reach it as a column literally named `[object
|
|
166
166
|
* Object]`.
|
|
167
167
|
*/
|
|
168
|
-
addIndex(
|
|
168
|
+
addIndex(columns, options, unique) {
|
|
169
169
|
const { name, ...rest } = typeof options === 'string' ? { name: options } : (options ?? {});
|
|
170
170
|
const entries = columns.map(normalizeIndexColumn);
|
|
171
171
|
this._indexes.push({
|
|
172
172
|
...rest,
|
|
173
|
-
name: name ??
|
|
173
|
+
name: name ??
|
|
174
|
+
derivedIndexName(this._name, entries.map((entry) => entry.column), unique),
|
|
174
175
|
where: ddlText(rest.where, 'a partial-index predicate'),
|
|
175
176
|
entries,
|
|
176
177
|
unique,
|
|
@@ -34,6 +34,7 @@ export function detectDrift(expectedAST, actualAST, options = {}) {
|
|
|
34
34
|
...detectTableDrifts(diff),
|
|
35
35
|
...detectColumnDrifts(diff, opts),
|
|
36
36
|
...detectIndexDrifts(diff),
|
|
37
|
+
...detectPrimaryKeyDrifts(diff),
|
|
37
38
|
...detectRelationshipDrifts(diff),
|
|
38
39
|
];
|
|
39
40
|
return {
|
|
@@ -43,6 +44,21 @@ export function detectDrift(expectedAST, actualAST, options = {}) {
|
|
|
43
44
|
generatedAt: new Date(),
|
|
44
45
|
};
|
|
45
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A table whose key holds different columns than the entity declares.
|
|
49
|
+
*
|
|
50
|
+
* Critical, and reported as a `constraint_mismatch` like any other: rows the database will accept
|
|
51
|
+
* are not the rows the ORM believes are unique, so it addresses by a key nothing enforces.
|
|
52
|
+
*/
|
|
53
|
+
function detectPrimaryKeyDrifts(diff) {
|
|
54
|
+
return diff.primaryKeyDiffs.map((pkDiff) => ({
|
|
55
|
+
type: 'constraint_mismatch',
|
|
56
|
+
severity: 'critical',
|
|
57
|
+
table: pkDiff.table,
|
|
58
|
+
details: `Primary key of "${pkDiff.table}" is (${pkDiff.actual.join(', ') || 'none'}) in the database but (${pkDiff.expected.join(', ') || 'none'}) in the entity`,
|
|
59
|
+
suggestion: 'Generate a migration to change the primary key',
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
46
62
|
/**
|
|
47
63
|
* Detect table-level drifts (missing/unexpected tables).
|
|
48
64
|
*/
|
|
@@ -16,6 +16,13 @@ export declare class MongoSchemaGenerator extends AbstractDialect implements Sch
|
|
|
16
16
|
generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
|
|
17
17
|
generateDropSchema(entities: readonly Type<unknown>[]): string[];
|
|
18
18
|
private selected;
|
|
19
|
+
/**
|
|
20
|
+
* The indexes `@Field({ index })` declares, as the collection would hold them.
|
|
21
|
+
*
|
|
22
|
+
* One owner because two paths need it: creating a collection, and working out which of its indexes
|
|
23
|
+
* are missing. Derived twice, they drifted the moment either changed how a name is settled.
|
|
24
|
+
*/
|
|
25
|
+
private fieldIndexes;
|
|
19
26
|
generateCreateTable<E>(entity: Type<E>, _options?: {
|
|
20
27
|
ifNotExists?: boolean;
|
|
21
28
|
}): string[];
|
|
@@ -32,22 +32,32 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
32
32
|
const wanted = new Set(only);
|
|
33
33
|
return entities.filter((entity) => wanted.has(this.resolveTableName(getMeta(entity))));
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The indexes `@Field({ index })` declares, as the collection would hold them.
|
|
37
|
+
*
|
|
38
|
+
* One owner because two paths need it: creating a collection, and working out which of its indexes
|
|
39
|
+
* are missing. Derived twice, they drifted the moment either changed how a name is settled.
|
|
40
|
+
*/
|
|
41
|
+
fieldIndexes(meta, collectionName) {
|
|
42
|
+
return getKeys(meta.fields).flatMap((key) => {
|
|
40
43
|
const field = meta.fields[key];
|
|
41
|
-
if (field?.index) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if (!field?.index) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const columnName = this.resolveColumnName(key, field);
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
name: typeof field.index === 'string' ? field.index : derivedIndexName(collectionName, [columnName]),
|
|
46
51
|
entries: [{ column: columnName }],
|
|
47
52
|
unique: !!field.unique,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
}
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
generateCreateTable(entity, _options) {
|
|
58
|
+
const meta = getMeta(entity);
|
|
59
|
+
const collectionName = this.resolveTableName(meta);
|
|
60
|
+
const indexes = this.fieldIndexes(meta, collectionName);
|
|
51
61
|
// One `createIndex` command each, mirroring the SQL generator's `[CREATE TABLE, ...CREATE INDEX]`,
|
|
52
62
|
// so the key spec is built here and the migrator only executes it.
|
|
53
63
|
return [
|
|
@@ -137,22 +147,8 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
137
147
|
if (!currentTable) {
|
|
138
148
|
return { tableName: collectionName, type: 'create' };
|
|
139
149
|
}
|
|
140
|
-
const indexesToAdd = [];
|
|
141
150
|
const existingIndexes = new Set(currentTable.indexes?.map((i) => i.name) ?? []);
|
|
142
|
-
|
|
143
|
-
const field = meta.fields[key];
|
|
144
|
-
if (field?.index) {
|
|
145
|
-
const columnName = this.resolveColumnName(key, field);
|
|
146
|
-
const indexName = typeof field.index === 'string' ? field.index : derivedIndexName(collectionName, [columnName]);
|
|
147
|
-
if (!existingIndexes.has(indexName)) {
|
|
148
|
-
indexesToAdd.push({
|
|
149
|
-
name: indexName,
|
|
150
|
-
entries: [{ column: columnName }],
|
|
151
|
-
unique: !!field.unique,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
151
|
+
const indexesToAdd = this.fieldIndexes(meta, collectionName).filter((index) => !existingIndexes.has(index.name));
|
|
156
152
|
if (indexesToAdd.length === 0) {
|
|
157
153
|
return undefined;
|
|
158
154
|
}
|
|
@@ -60,7 +60,10 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
|
|
|
60
60
|
protected getColumns(read: TableRowReader, tableName: string): Promise<ColumnSchema[]>;
|
|
61
61
|
protected getIndexes(read: TableRowReader, tableName: string): Promise<IndexSchema[]>;
|
|
62
62
|
protected getForeignKeys(read: TableRowReader, tableName: string): Promise<ForeignKeySchema[]>;
|
|
63
|
-
protected getPrimaryKey(read: TableRowReader, tableName: string): Promise<
|
|
63
|
+
protected getPrimaryKey(read: TableRowReader, tableName: string): Promise<{
|
|
64
|
+
columns?: string[];
|
|
65
|
+
name?: string;
|
|
66
|
+
}>;
|
|
64
67
|
protected tableExistsParams(tableName: string): unknown[];
|
|
65
68
|
protected getColumnsParams(tableName: string): unknown[];
|
|
66
69
|
protected getIndexesParams(tableName: string): unknown[];
|
|
@@ -108,6 +111,11 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
|
|
|
108
111
|
* overrides this.
|
|
109
112
|
*/
|
|
110
113
|
protected mapPrimaryKeyResult(results: RawRow[]): string[] | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* What the engine calls the key's constraint, where the query reported one. Only a `DROP` needs it,
|
|
116
|
+
* and only the reported name will do - see {@link TableSchema.primaryKeyName}.
|
|
117
|
+
*/
|
|
118
|
+
protected mapPrimaryKeyName(results: RawRow[]): string | undefined;
|
|
111
119
|
/** Parse default value string to appropriate type. */
|
|
112
120
|
protected abstract parseDefaultValue(defaultValue: string | null): unknown;
|
|
113
121
|
}
|
|
@@ -55,7 +55,8 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
|
|
|
55
55
|
return {
|
|
56
56
|
name: tableName,
|
|
57
57
|
columns,
|
|
58
|
-
primaryKey,
|
|
58
|
+
primaryKey: primaryKey.columns,
|
|
59
|
+
primaryKeyName: primaryKey.name,
|
|
59
60
|
indexes,
|
|
60
61
|
foreignKeys,
|
|
61
62
|
};
|
|
@@ -100,7 +101,7 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
|
|
|
100
101
|
}
|
|
101
102
|
async getPrimaryKey(read, tableName) {
|
|
102
103
|
const results = await read(this.getPrimaryKeyQuery(tableName), this.getPrimaryKeyParams(tableName));
|
|
103
|
-
return this.mapPrimaryKeyResult(results);
|
|
104
|
+
return { columns: this.mapPrimaryKeyResult(results), name: this.mapPrimaryKeyName(results) };
|
|
104
105
|
}
|
|
105
106
|
tableExistsParams(tableName) {
|
|
106
107
|
return [tableName];
|
|
@@ -162,6 +163,14 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
|
|
|
162
163
|
const columns = results.map((row) => String(row['column_name']));
|
|
163
164
|
return columns.length ? columns : undefined;
|
|
164
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* What the engine calls the key's constraint, where the query reported one. Only a `DROP` needs it,
|
|
168
|
+
* and only the reported name will do - see {@link TableSchema.primaryKeyName}.
|
|
169
|
+
*/
|
|
170
|
+
mapPrimaryKeyName(results) {
|
|
171
|
+
const name = results[0]?.['constraint_name'];
|
|
172
|
+
return name === undefined || name === null ? undefined : String(name);
|
|
173
|
+
}
|
|
165
174
|
}
|
|
166
175
|
/** A {@link TableRowReader} over one querier: the same statement is only ever sent once. */
|
|
167
176
|
function createTableRowReader(querier) {
|