@mikro-orm/knex 6.0.0-rc.0 → 6.0.0-rc.2
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/AbstractSqlConnection.d.ts +14 -0
- package/AbstractSqlConnection.js +18 -0
- package/AbstractSqlDriver.d.ts +15 -5
- package/AbstractSqlDriver.js +214 -73
- package/SqlEntityRepository.js +1 -1
- package/package.json +2 -2
- package/query/ArrayCriteriaNode.d.ts +3 -2
- package/query/ArrayCriteriaNode.js +7 -2
- package/query/CriteriaNode.d.ts +5 -2
- package/query/CriteriaNode.js +11 -2
- package/query/CriteriaNodeFactory.js +17 -4
- package/query/ObjectCriteriaNode.d.ts +3 -2
- package/query/ObjectCriteriaNode.js +51 -15
- package/query/QueryBuilder.d.ts +13 -5
- package/query/QueryBuilder.js +83 -34
- package/query/QueryBuilderHelper.d.ts +3 -3
- package/query/QueryBuilderHelper.js +16 -15
- package/query/ScalarCriteriaNode.d.ts +2 -2
- package/query/ScalarCriteriaNode.js +3 -3
- package/schema/DatabaseSchema.js +1 -1
- package/schema/DatabaseTable.d.ts +10 -4
- package/schema/DatabaseTable.js +320 -30
- package/schema/SchemaComparator.d.ts +2 -2
- package/schema/SchemaComparator.js +11 -7
- package/schema/SchemaHelper.d.ts +1 -1
- package/schema/SchemaHelper.js +7 -4
- package/schema/SqlSchemaGenerator.js +2 -2
- package/typings.d.ts +15 -3
|
@@ -300,7 +300,7 @@ class QueryBuilderHelper {
|
|
|
300
300
|
}
|
|
301
301
|
isOneToOneInverse(field, meta) {
|
|
302
302
|
meta ??= this.metadata.find(this.entityName);
|
|
303
|
-
const prop = meta.properties[field];
|
|
303
|
+
const prop = meta.properties[field.replace(/:ref$/, '')];
|
|
304
304
|
return prop && prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
305
305
|
}
|
|
306
306
|
getTableName(entityName) {
|
|
@@ -462,24 +462,21 @@ class QueryBuilderHelper {
|
|
|
462
462
|
}
|
|
463
463
|
getQueryOrder(type, orderBy, populate) {
|
|
464
464
|
if (Array.isArray(orderBy)) {
|
|
465
|
-
return orderBy
|
|
466
|
-
.map(o => this.getQueryOrder(type, o, populate))
|
|
467
|
-
.filter(o => o)
|
|
468
|
-
.join(', ');
|
|
465
|
+
return orderBy.flatMap(o => this.getQueryOrder(type, o, populate));
|
|
469
466
|
}
|
|
470
467
|
return this.getQueryOrderFromObject(type, orderBy, populate);
|
|
471
468
|
}
|
|
472
469
|
getQueryOrderFromObject(type, orderBy, populate) {
|
|
473
470
|
const ret = [];
|
|
474
|
-
Object.keys(orderBy)
|
|
471
|
+
for (const key of Object.keys(orderBy)) {
|
|
475
472
|
const direction = orderBy[key];
|
|
476
473
|
const order = core_1.Utils.isNumber(direction) ? core_1.QueryOrderNumeric[direction] : direction;
|
|
477
474
|
const raw = core_1.RawQueryFragment.getKnownFragment(key);
|
|
478
475
|
if (raw) {
|
|
479
476
|
ret.push(`${this.platform.formatQuery(raw.sql, raw.params)} ${order.toLowerCase()}`);
|
|
480
|
-
|
|
477
|
+
continue;
|
|
481
478
|
}
|
|
482
|
-
core_1.Utils.splitPrimaryKeys(key)
|
|
479
|
+
for (const f of core_1.Utils.splitPrimaryKeys(key)) {
|
|
483
480
|
// eslint-disable-next-line prefer-const
|
|
484
481
|
let [alias, field] = this.splitField(f, true);
|
|
485
482
|
alias = populate[alias] || alias;
|
|
@@ -496,14 +493,14 @@ class QueryBuilderHelper {
|
|
|
496
493
|
colPart = this.platform.formatQuery(colPart.sql, colPart.params);
|
|
497
494
|
}
|
|
498
495
|
if (Array.isArray(order)) {
|
|
499
|
-
order.forEach(part => ret.push(this.getQueryOrderFromObject(type, part, populate)));
|
|
496
|
+
order.forEach(part => ret.push(...this.getQueryOrderFromObject(type, part, populate)));
|
|
500
497
|
}
|
|
501
498
|
else {
|
|
502
499
|
ret.push(`${colPart} ${order.toLowerCase()}`);
|
|
503
500
|
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
return ret
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return ret;
|
|
507
504
|
}
|
|
508
505
|
finalize(type, qb, meta, data, returning) {
|
|
509
506
|
if (!meta || !data || !this.platform.usesReturningStatement()) {
|
|
@@ -532,17 +529,21 @@ class QueryBuilderHelper {
|
|
|
532
529
|
}
|
|
533
530
|
splitField(field, greedyAlias = false) {
|
|
534
531
|
const parts = field.split('.');
|
|
532
|
+
const ref = parts[parts.length - 1].split(':')[1];
|
|
533
|
+
if (ref) {
|
|
534
|
+
parts[parts.length - 1] = parts[parts.length - 1].substring(0, parts[parts.length - 1].indexOf(':'));
|
|
535
|
+
}
|
|
535
536
|
if (parts.length === 1) {
|
|
536
|
-
return [this.alias, parts[0]];
|
|
537
|
+
return [this.alias, parts[0], ref];
|
|
537
538
|
}
|
|
538
539
|
if (greedyAlias) {
|
|
539
540
|
const fromField = parts.pop();
|
|
540
541
|
const fromAlias = parts.join('.');
|
|
541
|
-
return [fromAlias, fromField];
|
|
542
|
+
return [fromAlias, fromField, ref];
|
|
542
543
|
}
|
|
543
544
|
const fromAlias = parts.shift();
|
|
544
545
|
const fromField = parts.join('.');
|
|
545
|
-
return [fromAlias, fromField];
|
|
546
|
+
return [fromAlias, fromField, ref];
|
|
546
547
|
}
|
|
547
548
|
getLockSQL(qb, lockMode, lockTables = []) {
|
|
548
549
|
const meta = this.metadata.find(this.entityName);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CriteriaNode } from './CriteriaNode';
|
|
2
|
-
import type { IQueryBuilder } from '../typings';
|
|
2
|
+
import type { IQueryBuilder, ICriteriaNodeProcessOptions } from '../typings';
|
|
3
3
|
/**
|
|
4
4
|
* @internal
|
|
5
5
|
*/
|
|
6
6
|
export declare class ScalarCriteriaNode<T extends object> extends CriteriaNode<T> {
|
|
7
|
-
process(qb: IQueryBuilder<T>,
|
|
7
|
+
process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any;
|
|
8
8
|
willAutoJoin<T>(qb: IQueryBuilder<T>, alias?: string): boolean;
|
|
9
9
|
shouldJoin(): boolean;
|
|
10
10
|
}
|
|
@@ -8,16 +8,16 @@ const enums_1 = require("./enums");
|
|
|
8
8
|
* @internal
|
|
9
9
|
*/
|
|
10
10
|
class ScalarCriteriaNode extends CriteriaNode_1.CriteriaNode {
|
|
11
|
-
process(qb,
|
|
11
|
+
process(qb, options) {
|
|
12
12
|
if (this.shouldJoin()) {
|
|
13
13
|
const path = this.getPath();
|
|
14
14
|
const parentPath = this.parent.getPath(); // the parent is always there, otherwise `shouldJoin` would return `false`
|
|
15
15
|
const nestedAlias = qb.getAliasForJoinPath(path) || qb.getNextAlias(this.prop?.pivotTable ?? this.entityName);
|
|
16
|
-
const field =
|
|
16
|
+
const field = this.aliased(this.prop.name, options?.alias);
|
|
17
17
|
const type = this.prop.kind === core_1.ReferenceKind.MANY_TO_MANY ? enums_1.JoinType.pivotJoin : enums_1.JoinType.leftJoin;
|
|
18
18
|
qb.join(field, nestedAlias, undefined, type, path);
|
|
19
19
|
// select the owner as virtual property when joining from 1:1 inverse side, but only if the parent is root entity
|
|
20
|
-
if (this.prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !parentPath.includes('.')) {
|
|
20
|
+
if (this.prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !parentPath.includes('.') && !qb._fields?.includes(field)) {
|
|
21
21
|
qb.addSelect(field);
|
|
22
22
|
}
|
|
23
23
|
}
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -73,7 +73,7 @@ class DatabaseSchema {
|
|
|
73
73
|
table.comment = meta.comment;
|
|
74
74
|
meta.props
|
|
75
75
|
.filter(prop => this.shouldHaveColumn(meta, prop))
|
|
76
|
-
.forEach(prop => table.addColumnFromProperty(prop, meta));
|
|
76
|
+
.forEach(prop => table.addColumnFromProperty(prop, meta, config));
|
|
77
77
|
meta.indexes.forEach(index => table.addIndex(meta, index, 'index'));
|
|
78
78
|
meta.uniques.forEach(index => table.addIndex(meta, index, 'unique'));
|
|
79
79
|
table.addIndex(meta, { properties: meta.props.filter(prop => prop.primary).map(prop => prop.name) }, 'primary');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Dictionary, type EntityMetadata, type EntityProperty, type NamingStrategy } from '@mikro-orm/core';
|
|
1
|
+
import { type Configuration, type Dictionary, type EntityMetadata, type EntityProperty, type MikroORMOptions, type NamingStrategy } from '@mikro-orm/core';
|
|
2
2
|
import type { SchemaHelper } from './SchemaHelper';
|
|
3
3
|
import type { CheckDef, Column, ForeignKey, IndexDef } from '../typings';
|
|
4
4
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform';
|
|
@@ -23,9 +23,13 @@ export declare class DatabaseTable {
|
|
|
23
23
|
getChecks(): CheckDef[];
|
|
24
24
|
init(cols: Column[], indexes: IndexDef[] | undefined, checks: CheckDef<unknown>[] | undefined, pks: string[], fks?: Dictionary<ForeignKey>, enums?: Dictionary<string[]>): void;
|
|
25
25
|
addColumn(column: Column): void;
|
|
26
|
-
addColumnFromProperty(prop: EntityProperty, meta: EntityMetadata): void;
|
|
26
|
+
addColumnFromProperty(prop: EntityProperty, meta: EntityMetadata, config: Configuration): void;
|
|
27
27
|
private getIndexName;
|
|
28
|
-
getEntityDeclaration(namingStrategy: NamingStrategy, schemaHelper: SchemaHelper): EntityMetadata;
|
|
28
|
+
getEntityDeclaration(namingStrategy: NamingStrategy, schemaHelper: SchemaHelper, scalarPropertiesForRelations: NonNullable<MikroORMOptions['entityGenerator']['scalarPropertiesForRelations']>): EntityMetadata;
|
|
29
|
+
private foreignKeysToProps;
|
|
30
|
+
private findFkIndex;
|
|
31
|
+
private getIndexProperties;
|
|
32
|
+
private getSafeBaseNameForFkProp;
|
|
29
33
|
/**
|
|
30
34
|
* The shortest name is stripped of the default namespace. All other namespaced elements are returned as full-qualified names.
|
|
31
35
|
*/
|
|
@@ -38,10 +42,12 @@ export declare class DatabaseTable {
|
|
|
38
42
|
hasCheck(checkName: string): boolean;
|
|
39
43
|
getPrimaryKey(): IndexDef | undefined;
|
|
40
44
|
hasPrimaryKey(): boolean;
|
|
45
|
+
private getForeignKeyDeclaration;
|
|
41
46
|
private getPropertyDeclaration;
|
|
42
47
|
private getReferenceKind;
|
|
43
48
|
private getPropertyName;
|
|
44
|
-
private
|
|
49
|
+
private getPropertyTypeForForeignKey;
|
|
50
|
+
private getPropertyTypeForColumn;
|
|
45
51
|
private getPropertyDefaultValue;
|
|
46
52
|
addIndex(meta: EntityMetadata, index: {
|
|
47
53
|
properties: string | string[];
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -57,7 +57,7 @@ class DatabaseTable {
|
|
|
57
57
|
addColumn(column) {
|
|
58
58
|
this.columns[column.name] = column;
|
|
59
59
|
}
|
|
60
|
-
addColumnFromProperty(prop, meta) {
|
|
60
|
+
addColumnFromProperty(prop, meta, config) {
|
|
61
61
|
prop.fieldNames.forEach((field, idx) => {
|
|
62
62
|
const type = prop.enum ? 'enum' : prop.columnTypes[idx];
|
|
63
63
|
const mappedType = this.platform.getMappedType(type);
|
|
@@ -105,7 +105,7 @@ class DatabaseTable {
|
|
|
105
105
|
});
|
|
106
106
|
if ([core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
107
107
|
const constraintName = this.getIndexName(true, prop.fieldNames, 'foreign');
|
|
108
|
-
let schema = prop.targetMeta.schema === '*' ? this.schema : prop.targetMeta.schema ?? this.
|
|
108
|
+
let schema = prop.targetMeta.root.schema === '*' ? this.schema : (prop.targetMeta.root.schema ?? config.get('schema', this.platform.getDefaultSchemaName()));
|
|
109
109
|
if (prop.referencedTableName.includes('.')) {
|
|
110
110
|
schema = undefined;
|
|
111
111
|
}
|
|
@@ -151,40 +151,285 @@ class DatabaseTable {
|
|
|
151
151
|
}
|
|
152
152
|
return this.platform.getIndexName(this.name, columnNames, type);
|
|
153
153
|
}
|
|
154
|
-
getEntityDeclaration(namingStrategy, schemaHelper) {
|
|
154
|
+
getEntityDeclaration(namingStrategy, schemaHelper, scalarPropertiesForRelations) {
|
|
155
|
+
const { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames, } = this.foreignKeysToProps(namingStrategy, scalarPropertiesForRelations);
|
|
155
156
|
let name = namingStrategy.getClassName(this.name, '_');
|
|
156
157
|
name = name.match(/^\d/) ? 'E' + name : name;
|
|
157
158
|
const schema = new core_1.EntitySchema({ name, collection: this.name, schema: this.schema });
|
|
158
159
|
const compositeFkIndexes = {};
|
|
159
160
|
const compositeFkUniques = {};
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
const potentiallyUnmappedIndexes = this.indexes.filter(index => !index.primary // Skip primary index. Whether it's in use by scalar column or FK, it's already mapped.
|
|
162
|
+
&& (index.columnNames.length > 1 // All composite indexes are to be mapped to entity decorators or FK props.
|
|
163
|
+
|| !(index.columnNames[0] in columnFks) // Non-composite indexes for scalar props are to be mapped to the column.
|
|
164
|
+
|| skippedColumnNames.includes(index.columnNames[0]) // Non-composite indexes for skipped columns are to be mapped as entity decorators.
|
|
165
|
+
)
|
|
166
|
+
// ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions)
|
|
167
|
+
&& !(index.columnNames.some(col => !col) && !index.expression));
|
|
168
|
+
for (const index of potentiallyUnmappedIndexes) {
|
|
169
|
+
const ret = { name: index.keyName };
|
|
170
|
+
// Index is for FK. Map to the FK prop and move on.
|
|
171
|
+
const fkForIndex = fkIndexes.get(index);
|
|
172
|
+
if (fkForIndex) {
|
|
173
|
+
ret.properties = [this.getPropertyName(namingStrategy, fkForIndex.baseName, fkForIndex.fk)];
|
|
164
174
|
const map = index.unique ? compositeFkUniques : compositeFkIndexes;
|
|
165
175
|
map[ret.properties[0]] = { keyName: index.keyName };
|
|
166
176
|
continue;
|
|
167
177
|
}
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
const properties = this.getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy);
|
|
179
|
+
// If there is a column that cannot be unambiguously mapped to a prop, render an expression.
|
|
180
|
+
if (index.expression) {
|
|
181
|
+
ret.expression = index.expression;
|
|
170
182
|
}
|
|
171
|
-
else if (
|
|
172
|
-
|
|
183
|
+
else if (typeof properties === 'undefined') {
|
|
184
|
+
ret.expression = schemaHelper.getCreateIndexSQL(this.name, index);
|
|
173
185
|
}
|
|
174
186
|
else {
|
|
175
|
-
|
|
187
|
+
ret.properties = properties;
|
|
188
|
+
// If the index is for one property that is not a FK prop, map to the column prop and move on.
|
|
189
|
+
if (properties.length === 1 && !fksOnStandaloneProps.has(properties[0])) {
|
|
190
|
+
const map = index.unique ? compositeFkUniques : compositeFkIndexes;
|
|
191
|
+
map[properties[0]] = { keyName: index.keyName };
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Composite indexes that aren't exclusively mapped to FK props get an entity decorator.
|
|
196
|
+
if (index.unique) {
|
|
197
|
+
schema.addUnique(ret);
|
|
198
|
+
continue;
|
|
176
199
|
}
|
|
200
|
+
schema.addIndex(ret);
|
|
177
201
|
}
|
|
178
|
-
|
|
179
|
-
|
|
202
|
+
const addedStandaloneFkPropsBasedOnColumn = new Set;
|
|
203
|
+
const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name));
|
|
204
|
+
for (const column of nonSkippedColumns) {
|
|
205
|
+
const columnName = column.name;
|
|
206
|
+
const standaloneFkPropBasedOnColumn = fksOnStandaloneProps.get(columnName);
|
|
207
|
+
if (standaloneFkPropBasedOnColumn && !fksOnColumnProps.get(columnName)) {
|
|
208
|
+
addedStandaloneFkPropsBasedOnColumn.add(columnName);
|
|
209
|
+
const [fkIndex, currentFk] = standaloneFkPropBasedOnColumn;
|
|
210
|
+
const prop = this.getForeignKeyDeclaration(currentFk, namingStrategy, schemaHelper, fkIndex, nullableForeignKeys.has(currentFk), columnName);
|
|
211
|
+
schema.addProperty(prop.name, prop.type, prop);
|
|
212
|
+
}
|
|
213
|
+
const prop = this.getPropertyDeclaration(column, namingStrategy, schemaHelper, compositeFkIndexes, compositeFkUniques, columnFks, fksOnColumnProps.get(columnName));
|
|
214
|
+
schema.addProperty(prop.name, prop.type, prop);
|
|
215
|
+
}
|
|
216
|
+
for (const [propBaseName, [fkIndex, currentFk]] of fksOnStandaloneProps.entries()) {
|
|
217
|
+
if (addedStandaloneFkPropsBasedOnColumn.has(propBaseName)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const prop = this.getForeignKeyDeclaration(currentFk, namingStrategy, schemaHelper, fkIndex, nullableForeignKeys.has(currentFk), propBaseName);
|
|
180
221
|
schema.addProperty(prop.name, prop.type, prop);
|
|
181
222
|
}
|
|
182
223
|
const meta = schema.init().meta;
|
|
183
|
-
meta.relations
|
|
184
|
-
.filter(prop => prop.primary && prop.kind === core_1.ReferenceKind.MANY_TO_ONE
|
|
185
|
-
|
|
224
|
+
const oneToOneCandidateProperties = meta.relations
|
|
225
|
+
.filter(prop => prop.primary && prop.kind === core_1.ReferenceKind.MANY_TO_ONE);
|
|
226
|
+
if (oneToOneCandidateProperties.length === 1
|
|
227
|
+
&& oneToOneCandidateProperties[0].fieldNames.length === (new Set(meta.getPrimaryProps().flatMap(prop => prop.fieldNames))).size) {
|
|
228
|
+
oneToOneCandidateProperties[0].kind = core_1.ReferenceKind.ONE_TO_ONE;
|
|
229
|
+
}
|
|
186
230
|
return meta;
|
|
187
231
|
}
|
|
232
|
+
foreignKeysToProps(namingStrategy, scalarPropertiesForRelations) {
|
|
233
|
+
const fks = Object.values(this.getForeignKeys());
|
|
234
|
+
const fksOnColumnProps = new Map();
|
|
235
|
+
const fksOnStandaloneProps = new Map();
|
|
236
|
+
const columnFks = {};
|
|
237
|
+
const fkIndexes = new Map();
|
|
238
|
+
const nullableForeignKeys = new Set();
|
|
239
|
+
for (const currentFk of fks) {
|
|
240
|
+
const fkIndex = this.findFkIndex(currentFk);
|
|
241
|
+
if (currentFk.columnNames.length === 1 && !fks.some(fk => fk !== currentFk && fk.columnNames.length === 1 && currentFk.columnNames[0] === fk.columnNames[0])) {
|
|
242
|
+
// Non-composite FK is the only possible one for a column. Render the column with it.
|
|
243
|
+
const columnName = currentFk.columnNames[0];
|
|
244
|
+
columnFks[columnName] ??= [];
|
|
245
|
+
columnFks[columnName].push(currentFk);
|
|
246
|
+
if (this.getColumn(columnName)?.nullable) {
|
|
247
|
+
nullableForeignKeys.add(currentFk);
|
|
248
|
+
}
|
|
249
|
+
if (scalarPropertiesForRelations === 'always') {
|
|
250
|
+
const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
|
|
251
|
+
fksOnStandaloneProps.set(baseName, [fkIndex, currentFk]);
|
|
252
|
+
fkIndexes.set(fkIndex, { fk: currentFk, baseName });
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
fksOnColumnProps.set(columnName, currentFk);
|
|
256
|
+
fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const specificColumnNames = [];
|
|
261
|
+
const nullableColumnsInFk = [];
|
|
262
|
+
for (const columnName of currentFk.columnNames) {
|
|
263
|
+
columnFks[columnName] ??= [];
|
|
264
|
+
columnFks[columnName].push(currentFk);
|
|
265
|
+
if (!fks.some(fk => fk !== currentFk && fk.columnNames.includes(columnName))) {
|
|
266
|
+
specificColumnNames.push(columnName);
|
|
267
|
+
}
|
|
268
|
+
if (this.getColumn(columnName)?.nullable) {
|
|
269
|
+
nullableColumnsInFk.push(columnName);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (nullableColumnsInFk.length > 0) {
|
|
273
|
+
nullableForeignKeys.add(currentFk);
|
|
274
|
+
}
|
|
275
|
+
if (specificColumnNames.length === 1 && ((nullableColumnsInFk.length === currentFk.columnNames.length || nullableColumnsInFk.length === 0) || (nullableColumnsInFk.length === 1 && nullableColumnsInFk[0] === specificColumnNames[0]))) {
|
|
276
|
+
// Composite FK has exactly one column which is not used in any other FK.
|
|
277
|
+
// The FK also doesn't have a mix of nullable and non-nullable columns,
|
|
278
|
+
// or its only nullable column is this very one.
|
|
279
|
+
// It is safe to just render this FK attached to the specific column.
|
|
280
|
+
const columnName = specificColumnNames[0];
|
|
281
|
+
if (scalarPropertiesForRelations === 'always') {
|
|
282
|
+
const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
|
|
283
|
+
fksOnStandaloneProps.set(baseName, [fkIndex, currentFk]);
|
|
284
|
+
fkIndexes.set(fkIndex, { fk: currentFk, baseName });
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
fksOnColumnProps.set(columnName, currentFk);
|
|
288
|
+
fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (specificColumnNames.length === currentFk.columnNames.length) {
|
|
293
|
+
// All columns involved with this FK are only covered by this one FK.
|
|
294
|
+
if (nullableColumnsInFk.length <= 1) {
|
|
295
|
+
// Also, this FK is either not nullable, or has only one nullable column.
|
|
296
|
+
// It is safe to name the FK after the nullable column, or any non-nullable one (the first one is picked).
|
|
297
|
+
const columnName = nullableColumnsInFk.at(0) ?? currentFk.columnNames[0];
|
|
298
|
+
if (scalarPropertiesForRelations === 'always') {
|
|
299
|
+
const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
|
|
300
|
+
fksOnStandaloneProps.set(baseName, [fkIndex, currentFk]);
|
|
301
|
+
fkIndexes.set(fkIndex, { fk: currentFk, baseName });
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
fksOnColumnProps.set(columnName, currentFk);
|
|
305
|
+
fkIndexes.set(fkIndex, { fk: currentFk, baseName: columnName });
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
// If the first nullable column's name with FK is different from the name without FK,
|
|
310
|
+
// name a standalone prop after the column, but treat the column prop itself as not having FK.
|
|
311
|
+
const columnName = nullableColumnsInFk[0];
|
|
312
|
+
const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName);
|
|
313
|
+
fksOnStandaloneProps.set(baseName, [fkIndex, currentFk]);
|
|
314
|
+
fkIndexes.set(fkIndex, { fk: currentFk, baseName });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// FK is not unambiguously mappable to a column. Pick another name for a standalone FK prop.
|
|
318
|
+
const baseName = this.getSafeBaseNameForFkProp(namingStrategy, currentFk, fks);
|
|
319
|
+
fksOnStandaloneProps.set(baseName, [fkIndex, currentFk]);
|
|
320
|
+
fkIndexes.set(fkIndex, { fk: currentFk, baseName });
|
|
321
|
+
}
|
|
322
|
+
const columnsInFks = Object.keys(columnFks);
|
|
323
|
+
const skippingHandlers = {
|
|
324
|
+
// Never generate scalar props for composite keys,
|
|
325
|
+
// i.e. always skip columns if they are covered by foreign keys.
|
|
326
|
+
never: (column) => columnsInFks.includes(column.name) && !fksOnColumnProps.has(column.name),
|
|
327
|
+
// Always generate scalar props for composite keys,
|
|
328
|
+
// i.e. do not skip columns, even if they are covered by foreign keys.
|
|
329
|
+
always: (column) => false,
|
|
330
|
+
// Smart scalar props generation.
|
|
331
|
+
// Skips columns if they are covered by foreign keys.
|
|
332
|
+
// But also does not skip if the column is not nullable, and yet all involved FKs are nullable,
|
|
333
|
+
// or if one or more FKs involved has multiple nullable columns.
|
|
334
|
+
smart: (column) => {
|
|
335
|
+
return columnsInFks.includes(column.name)
|
|
336
|
+
&& !fksOnColumnProps.has(column.name)
|
|
337
|
+
&& (column.nullable
|
|
338
|
+
? columnFks[column.name].some(fk => !fk.columnNames.some(fkColumnName => fkColumnName !== column.name && this.getColumn(fkColumnName)?.nullable))
|
|
339
|
+
: columnFks[column.name].some(fk => !nullableForeignKeys.has(fk)));
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
const skippedColumnNames = this.getColumns().filter(skippingHandlers[scalarPropertiesForRelations]).map(column => column.name);
|
|
343
|
+
return { fksOnColumnProps, fksOnStandaloneProps, columnFks, fkIndexes, nullableForeignKeys, skippedColumnNames };
|
|
344
|
+
}
|
|
345
|
+
findFkIndex(currentFk) {
|
|
346
|
+
const fkColumnsLength = currentFk.columnNames.length;
|
|
347
|
+
const possibleIndexes = this.indexes.filter(index => {
|
|
348
|
+
return index.columnNames.length >= fkColumnsLength && !currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName);
|
|
349
|
+
});
|
|
350
|
+
possibleIndexes.sort((a, b) => {
|
|
351
|
+
if (a.primary !== b.primary) {
|
|
352
|
+
return a.primary ? -1 : 1;
|
|
353
|
+
}
|
|
354
|
+
if (a.unique !== b.unique) {
|
|
355
|
+
return a.unique ? -1 : 1;
|
|
356
|
+
}
|
|
357
|
+
if (a.columnNames.length !== b.columnNames.length) {
|
|
358
|
+
return a.columnNames.length < b.columnNames.length ? -1 : 1;
|
|
359
|
+
}
|
|
360
|
+
return a.keyName.localeCompare(b.keyName);
|
|
361
|
+
});
|
|
362
|
+
return possibleIndexes[0];
|
|
363
|
+
}
|
|
364
|
+
getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) {
|
|
365
|
+
const propBaseNames = new Set();
|
|
366
|
+
const columnNames = index.columnNames;
|
|
367
|
+
const l = columnNames.length;
|
|
368
|
+
if (columnNames.some(col => !col)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
for (let i = 0; i < l; ++i) {
|
|
372
|
+
const columnName = columnNames[i];
|
|
373
|
+
// The column is not involved with FKs.
|
|
374
|
+
// It has a prop named after it.
|
|
375
|
+
// Add it and move on.
|
|
376
|
+
if (!(columnName in columnFks)) {
|
|
377
|
+
propBaseNames.add(columnName);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// If the prop named after the column has a FK and the FK's columns are a subset of this index,
|
|
381
|
+
// include this prop and move on.
|
|
382
|
+
const columnPropFk = fksOnColumnProps.get(columnName);
|
|
383
|
+
if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
|
|
384
|
+
propBaseNames.add(columnName);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
// If there is at least one standalone FK featuring this column,
|
|
388
|
+
// and all of its columns are a subset of this index,
|
|
389
|
+
// include that FK, and consider mapping of this column to a prop a success.
|
|
390
|
+
let propAdded = false;
|
|
391
|
+
for (const [propName, [, fk]] of fksOnStandaloneProps) {
|
|
392
|
+
if (!columnFks[columnName].includes(fk)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
|
|
396
|
+
propBaseNames.add(propName);
|
|
397
|
+
propAdded = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (propAdded) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// If we have reached this point, it means the column is not mappable to a prop name.
|
|
404
|
+
// Break the whole prop creation.
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
return Array.from(propBaseNames).map(baseName => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
|
|
408
|
+
}
|
|
409
|
+
getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) {
|
|
410
|
+
if (columnName && this.getPropertyName(namingStrategy, columnName, currentFk) !== this.getPropertyName(namingStrategy, columnName)) {
|
|
411
|
+
// The eligible scalar column name is different from the name of the FK prop of the same column.
|
|
412
|
+
// Both can be safely rendered.
|
|
413
|
+
// Use the column name as a base for the FK prop.
|
|
414
|
+
return columnName;
|
|
415
|
+
}
|
|
416
|
+
if (!fks.some(fk => fk !== currentFk && fk.referencedTableName === currentFk.referencedTableName) && !this.getColumn(currentFk.referencedTableName)) {
|
|
417
|
+
// FK is the only one in this table that references this other table.
|
|
418
|
+
// The name of the referenced table is not shared with a column in this table,
|
|
419
|
+
// so it is safe to output prop name based on the referenced entity.
|
|
420
|
+
return currentFk.referencedTableName;
|
|
421
|
+
}
|
|
422
|
+
// Any ambiguous FK is rendered with a name based on the FK constraint name
|
|
423
|
+
let finalPropBaseName = currentFk.constraintName;
|
|
424
|
+
while (this.getColumn(finalPropBaseName)) {
|
|
425
|
+
// In the unlikely event that the FK constraint name is shared by a column name, generate a name by
|
|
426
|
+
// continuously prefixing with "fk_", until a non-existent column is hit.
|
|
427
|
+
// The worst case scenario is a very long name with several repeated "fk_"
|
|
428
|
+
// that is not really a valid DB identifier but a valid JS variable name.
|
|
429
|
+
finalPropBaseName = `fk_${finalPropBaseName}`;
|
|
430
|
+
}
|
|
431
|
+
return finalPropBaseName;
|
|
432
|
+
}
|
|
188
433
|
/**
|
|
189
434
|
* The shortest name is stripped of the default namespace. All other namespaced elements are returned as full-qualified names.
|
|
190
435
|
*/
|
|
@@ -218,13 +463,48 @@ class DatabaseTable {
|
|
|
218
463
|
hasPrimaryKey() {
|
|
219
464
|
return !!this.getPrimaryKey();
|
|
220
465
|
}
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const
|
|
466
|
+
getForeignKeyDeclaration(fk, namingStrategy, schemaHelper, fkIndex, nullable, propNameBase) {
|
|
467
|
+
const prop = this.getPropertyName(namingStrategy, propNameBase, fk);
|
|
468
|
+
const kind = (fkIndex.unique && !fkIndex.primary) ? this.getReferenceKind(fk, fkIndex) : this.getReferenceKind(fk);
|
|
469
|
+
const type = this.getPropertyTypeForForeignKey(namingStrategy, fk);
|
|
470
|
+
const fkOptions = {};
|
|
471
|
+
fkOptions.fieldNames = fk.columnNames;
|
|
472
|
+
fkOptions.referencedTableName = fk.referencedTableName;
|
|
473
|
+
fkOptions.referencedColumnNames = fk.referencedColumnNames;
|
|
474
|
+
fkOptions.updateRule = fk.updateRule?.toLowerCase();
|
|
475
|
+
fkOptions.deleteRule = fk.deleteRule?.toLowerCase();
|
|
476
|
+
const columnOptions = {};
|
|
477
|
+
if (fk.columnNames.length === 1) {
|
|
478
|
+
const column = this.getColumn(fk.columnNames[0]);
|
|
479
|
+
columnOptions.default = this.getPropertyDefaultValue(schemaHelper, column, type);
|
|
480
|
+
columnOptions.defaultRaw = this.getPropertyDefaultValue(schemaHelper, column, type, true);
|
|
481
|
+
columnOptions.nullable = column.nullable;
|
|
482
|
+
columnOptions.primary = column.primary;
|
|
483
|
+
columnOptions.length = column.length;
|
|
484
|
+
columnOptions.precision = column.precision;
|
|
485
|
+
columnOptions.scale = column.scale;
|
|
486
|
+
columnOptions.enum = !!column.enumItems?.length;
|
|
487
|
+
columnOptions.items = column.enumItems;
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
name: prop,
|
|
491
|
+
type,
|
|
492
|
+
kind,
|
|
493
|
+
...columnOptions,
|
|
494
|
+
nullable,
|
|
495
|
+
primary: fkIndex.primary || !fk.columnNames.some(columnName => !this.getPrimaryKey()?.columnNames.includes(columnName)),
|
|
496
|
+
index: !fkIndex.unique ? fkIndex.keyName : undefined,
|
|
497
|
+
unique: (fkIndex.unique && !fkIndex.primary) ? fkIndex.keyName : undefined,
|
|
498
|
+
...fkOptions,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
getPropertyDeclaration(column, namingStrategy, schemaHelper, compositeFkIndexes, compositeFkUniques, columnFks, fk) {
|
|
502
|
+
const prop = this.getPropertyName(namingStrategy, column.name, fk);
|
|
503
|
+
const persist = !(column.name in columnFks && typeof fk === 'undefined');
|
|
224
504
|
const index = compositeFkIndexes[prop] || this.indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && !idx.unique && !idx.primary);
|
|
225
505
|
const unique = compositeFkUniques[prop] || this.indexes.find(idx => idx.columnNames[0] === column.name && !idx.composite && idx.unique && !idx.primary);
|
|
226
506
|
const kind = this.getReferenceKind(fk, unique);
|
|
227
|
-
const type = this.
|
|
507
|
+
const type = this.getPropertyTypeForColumn(namingStrategy, column, fk);
|
|
228
508
|
const fkOptions = {};
|
|
229
509
|
if (fk) {
|
|
230
510
|
fkOptions.fieldNames = fk.columnNames;
|
|
@@ -241,7 +521,7 @@ class DatabaseTable {
|
|
|
241
521
|
default: this.getPropertyDefaultValue(schemaHelper, column, type),
|
|
242
522
|
defaultRaw: this.getPropertyDefaultValue(schemaHelper, column, type, true),
|
|
243
523
|
nullable: column.nullable,
|
|
244
|
-
primary: column.primary,
|
|
524
|
+
primary: column.primary && persist,
|
|
245
525
|
fieldName: column.name,
|
|
246
526
|
length: column.length,
|
|
247
527
|
precision: column.precision,
|
|
@@ -250,6 +530,7 @@ class DatabaseTable {
|
|
|
250
530
|
unique: unique ? unique.keyName : undefined,
|
|
251
531
|
enum: !!column.enumItems?.length,
|
|
252
532
|
items: column.enumItems,
|
|
533
|
+
persist,
|
|
253
534
|
...fkOptions,
|
|
254
535
|
};
|
|
255
536
|
}
|
|
@@ -262,19 +543,28 @@ class DatabaseTable {
|
|
|
262
543
|
}
|
|
263
544
|
return core_1.ReferenceKind.SCALAR;
|
|
264
545
|
}
|
|
265
|
-
getPropertyName(namingStrategy,
|
|
266
|
-
|
|
267
|
-
let field = column.name;
|
|
546
|
+
getPropertyName(namingStrategy, baseName, fk) {
|
|
547
|
+
let field = baseName;
|
|
268
548
|
if (fk) {
|
|
269
|
-
const idx = fk.columnNames.indexOf(
|
|
270
|
-
|
|
549
|
+
const idx = fk.columnNames.indexOf(baseName);
|
|
550
|
+
let replacedFieldName = field.replace(new RegExp(`_${fk.referencedColumnNames[idx]}$`), '');
|
|
551
|
+
if (replacedFieldName === field) {
|
|
552
|
+
replacedFieldName = field.replace(new RegExp(`_${namingStrategy.referenceColumnName()}$`), '');
|
|
553
|
+
}
|
|
554
|
+
field = replacedFieldName;
|
|
555
|
+
}
|
|
556
|
+
if (field.startsWith('_')) {
|
|
557
|
+
return field;
|
|
271
558
|
}
|
|
272
559
|
return namingStrategy.columnNameToProperty(field);
|
|
273
560
|
}
|
|
274
|
-
|
|
561
|
+
getPropertyTypeForForeignKey(namingStrategy, fk) {
|
|
562
|
+
const parts = fk.referencedTableName.split('.', 2);
|
|
563
|
+
return namingStrategy.getClassName(parts.length > 1 ? parts[1] : parts[0], '_');
|
|
564
|
+
}
|
|
565
|
+
getPropertyTypeForColumn(namingStrategy, column, fk) {
|
|
275
566
|
if (fk) {
|
|
276
|
-
|
|
277
|
-
return namingStrategy.getClassName(parts.length > 1 ? parts[1] : parts[0], '_');
|
|
567
|
+
return this.getPropertyTypeForForeignKey(namingStrategy, fk);
|
|
278
568
|
}
|
|
279
569
|
// If this column is using an enum.
|
|
280
570
|
if (column.enumItems?.length) {
|
|
@@ -17,12 +17,12 @@ export declare class SchemaComparator {
|
|
|
17
17
|
* operations to change the schema stored in fromSchema to the schema that is
|
|
18
18
|
* stored in toSchema.
|
|
19
19
|
*/
|
|
20
|
-
compare(fromSchema: DatabaseSchema, toSchema: DatabaseSchema): SchemaDifference;
|
|
20
|
+
compare(fromSchema: DatabaseSchema, toSchema: DatabaseSchema, inverseDiff?: SchemaDifference): SchemaDifference;
|
|
21
21
|
/**
|
|
22
22
|
* Returns the difference between the tables fromTable and toTable.
|
|
23
23
|
* If there are no differences this method returns the boolean false.
|
|
24
24
|
*/
|
|
25
|
-
diffTable(fromTable: DatabaseTable, toTable: DatabaseTable): TableDifference | false;
|
|
25
|
+
diffTable(fromTable: DatabaseTable, toTable: DatabaseTable, inverseTableDiff?: TableDifference): TableDifference | false;
|
|
26
26
|
/**
|
|
27
27
|
* Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
|
|
28
28
|
* however ambiguities between different possibilities should not lead to renaming at all.
|