uql-orm 0.60.0 → 0.62.0
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/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/dialect/abstractSqlDialect.d.ts +12 -7
- package/dist/dialect/abstractSqlDialect.js +51 -50
- package/dist/dialect/mysqlLikeSqlDialect.js +1 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
- package/dist/dialect/pgLikeSqlDialect.js +8 -7
- package/dist/dialect/queryContext.d.ts +5 -6
- package/dist/dialect/queryContext.js +8 -8
- package/dist/entity/decorator/entity.d.ts +3 -3
- package/dist/entity/decorator/entity.js +2 -2
- package/dist/entity/decorator/members.d.ts +3 -2
- package/dist/entity/decorator/members.js +1 -0
- package/dist/entity/metadata/definition.js +12 -6
- package/dist/http/contract.d.ts +1 -1
- package/dist/http/contract.js +16 -4
- package/dist/migrate/builder/migrationBuilder.js +3 -5
- package/dist/migrate/builder/tableBuilder.js +2 -4
- package/dist/migrate/builder/types.d.ts +11 -3
- package/dist/migrate/codegen/indexDecoratorSource.js +3 -1
- package/dist/migrate/generator/definitionToNode.d.ts +8 -5
- package/dist/migrate/generator/definitionToNode.js +13 -4
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +6 -1
- package/dist/migrate/schemaGenerator.d.ts +5 -3
- package/dist/migrate/schemaGenerator.js +9 -2
- package/dist/mongo/mongoDialect.js +1 -1
- package/dist/mssql/mssqlDialect.js +3 -5
- package/dist/querier/queryError.d.ts +15 -12
- package/dist/querier/queryError.js +67 -8
- package/dist/schema/schemaASTBuilder.d.ts +6 -1
- package/dist/schema/schemaASTBuilder.js +17 -16
- package/dist/type/dialect.d.ts +15 -6
- package/dist/type/entity.d.ts +94 -34
- package/dist/type/migration.d.ts +9 -2
- package/dist/type/query.d.ts +3 -3
- package/dist/type/queryLock.d.ts +7 -7
- package/dist/type/queryLock.js +8 -6
- package/dist/type/queryRaw.d.ts +22 -30
- package/dist/type/queryRaw.js +3 -9
- package/dist/util/ddlExpression.util.d.ts +8 -13
- package/dist/util/ddlExpression.util.js +17 -19
- package/dist/util/dialect.util.d.ts +1 -1
- package/dist/util/dialect.util.js +3 -4
- package/dist/util/field.util.d.ts +1 -1
- package/dist/util/raw.d.ts +19 -17
- package/dist/util/raw.js +43 -17
- package/package.json +1 -1
|
@@ -126,7 +126,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
126
126
|
.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])))
|
|
127
127
|
.join(` || ' ' || `);
|
|
128
128
|
// The config is bound once and its numbered placeholder reused by both calls.
|
|
129
|
-
const config = search.$config ? `${this.addValue(ctx
|
|
129
|
+
const config = search.$config ? `${this.addValue(ctx, search.$config)}::regconfig, ` : '';
|
|
130
130
|
ctx.append(`TO_TSVECTOR(${config}${fields}) @@ WEBSEARCH_TO_TSQUERY(${config}`);
|
|
131
131
|
ctx.addValue(search.$value);
|
|
132
132
|
ctx.append(')');
|
|
@@ -158,10 +158,11 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
158
158
|
get neOp() {
|
|
159
159
|
return 'IS DISTINCT FROM';
|
|
160
160
|
}
|
|
161
|
+
/** One array parameter, which a context that inlines values has none of: it lists them instead. */
|
|
161
162
|
formatIn(ctx, values, negate) {
|
|
162
|
-
if (values.length === 0)
|
|
163
|
-
return
|
|
164
|
-
const ph = this.addValue(ctx
|
|
163
|
+
if (values.length === 0 || ctx.inlineValues)
|
|
164
|
+
return super.formatIn(ctx, values, negate);
|
|
165
|
+
const ph = this.addValue(ctx, values);
|
|
165
166
|
return negate ? ` <> ALL(${ph})` : ` = ANY(${ph})`;
|
|
166
167
|
}
|
|
167
168
|
numericCast(expr) {
|
|
@@ -210,7 +211,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
210
211
|
}, expr);
|
|
211
212
|
}
|
|
212
213
|
jsonUnset(ctx, expr, unset) {
|
|
213
|
-
return `(${expr}) - ${this.addValue(ctx
|
|
214
|
+
return `(${expr}) - ${this.addValue(ctx, [...unset])}::text[]`;
|
|
214
215
|
}
|
|
215
216
|
/** Postgres binds JSON with a numbered placeholder and an explicit cast, per driver capability. */
|
|
216
217
|
jsonScalarParam(ctx, value) {
|
|
@@ -223,9 +224,9 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
223
224
|
if (value instanceof QueryRaw)
|
|
224
225
|
return this.rawFragment(ctx, value);
|
|
225
226
|
if (value == null)
|
|
226
|
-
return `${this.addValue(ctx
|
|
227
|
+
return `${this.addValue(ctx, null)}::${type}`;
|
|
227
228
|
const json = JSON.stringify(value);
|
|
228
|
-
const ph = this.addValue(ctx
|
|
229
|
+
const ph = this.addValue(ctx, json);
|
|
229
230
|
return this.features.explicitJsonCast ? `(${ph}::text)::${type}` : `${ph}::${type}`;
|
|
230
231
|
}
|
|
231
232
|
}
|
|
@@ -9,6 +9,7 @@ import type { QueryContext, QueryDialect } from '../type/index.js';
|
|
|
9
9
|
export declare class SqlQueryContext implements QueryContext {
|
|
10
10
|
readonly dialect: QueryDialect;
|
|
11
11
|
private readonly statement?;
|
|
12
|
+
readonly inlineValues: boolean;
|
|
12
13
|
private readonly sqlChunks;
|
|
13
14
|
private readonly params;
|
|
14
15
|
private readonly tableAliases;
|
|
@@ -20,8 +21,9 @@ export declare class SqlQueryContext implements QueryContext {
|
|
|
20
21
|
* than needing to be reconciled after the fact.
|
|
21
22
|
* @param statement The context this one renders a fragment of, which owns the claimed aliases: a
|
|
22
23
|
* fragment is part of one statement, so its aliases have to be unique across the whole of it.
|
|
24
|
+
* @param inlineValues See {@link QueryContext.inlineValues}; a fragment takes its statement's.
|
|
23
25
|
*/
|
|
24
|
-
constructor(dialect: QueryDialect, params?: unknown[], statement?: SqlQueryContext | undefined);
|
|
26
|
+
constructor(dialect: QueryDialect, params?: unknown[], statement?: SqlQueryContext | undefined, inlineValues?: boolean);
|
|
25
27
|
createFragment(): QueryContext;
|
|
26
28
|
/**
|
|
27
29
|
* Appends raw SQL string fragments to the query.
|
|
@@ -31,11 +33,8 @@ export declare class SqlQueryContext implements QueryContext {
|
|
|
31
33
|
*/
|
|
32
34
|
append(sql: string): this;
|
|
33
35
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* @param value The value to be parameterized.
|
|
38
|
-
* @returns The current context instance for method chaining.
|
|
36
|
+
* Appends the SQL the dialect writes for `value`: a placeholder for the value bound, or its literal
|
|
37
|
+
* where this context inlines values.
|
|
39
38
|
*/
|
|
40
39
|
addValue(value: unknown): this;
|
|
41
40
|
/**
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
export class SqlQueryContext {
|
|
9
9
|
dialect;
|
|
10
10
|
statement;
|
|
11
|
+
inlineValues;
|
|
11
12
|
sqlChunks = [];
|
|
12
13
|
params;
|
|
13
14
|
tableAliases = new Set();
|
|
@@ -19,14 +20,16 @@ export class SqlQueryContext {
|
|
|
19
20
|
* than needing to be reconciled after the fact.
|
|
20
21
|
* @param statement The context this one renders a fragment of, which owns the claimed aliases: a
|
|
21
22
|
* fragment is part of one statement, so its aliases have to be unique across the whole of it.
|
|
23
|
+
* @param inlineValues See {@link QueryContext.inlineValues}; a fragment takes its statement's.
|
|
22
24
|
*/
|
|
23
|
-
constructor(dialect, params = [], statement) {
|
|
25
|
+
constructor(dialect, params = [], statement, inlineValues = false) {
|
|
24
26
|
this.dialect = dialect;
|
|
25
27
|
this.statement = statement;
|
|
28
|
+
this.inlineValues = inlineValues;
|
|
26
29
|
this.params = params;
|
|
27
30
|
}
|
|
28
31
|
createFragment() {
|
|
29
|
-
return new SqlQueryContext(this.dialect, this.params, this.statement ?? this);
|
|
32
|
+
return new SqlQueryContext(this.dialect, this.params, this.statement ?? this, this.inlineValues);
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
35
|
* Appends raw SQL string fragments to the query.
|
|
@@ -41,14 +44,11 @@ export class SqlQueryContext {
|
|
|
41
44
|
return this;
|
|
42
45
|
}
|
|
43
46
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* @param value The value to be parameterized.
|
|
48
|
-
* @returns The current context instance for method chaining.
|
|
47
|
+
* Appends the SQL the dialect writes for `value`: a placeholder for the value bound, or its literal
|
|
48
|
+
* where this context inlines values.
|
|
49
49
|
*/
|
|
50
50
|
addValue(value) {
|
|
51
|
-
this.sqlChunks.push(this.dialect.addValue(this
|
|
51
|
+
this.sqlChunks.push(this.dialect.addValue(this, value));
|
|
52
52
|
return this;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
@@ -12,7 +12,7 @@ export declare function Entity<E>(opts?: EntityOptions<E>): (entity: Type<E>, co
|
|
|
12
12
|
/**
|
|
13
13
|
* Registers a named `$where` filter, applied to every query unless bypassed via `QueryOptions.filters`.
|
|
14
14
|
*
|
|
15
|
-
* @example `@Filter('active', {
|
|
15
|
+
* @example `@Filter('active', { where: { status: 'active' }, default: false })`
|
|
16
16
|
*/
|
|
17
17
|
export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity: Type<E>) => void;
|
|
18
18
|
/**
|
|
@@ -22,6 +22,6 @@ export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity
|
|
|
22
22
|
*
|
|
23
23
|
* @example `@Index((user) => [user.lastName, user.firstName], { name: 'users_fullname_idx' })`
|
|
24
24
|
* @example `@Index((user) => [user.email], { unique: true })`
|
|
25
|
-
* @example `@Index((user) => [user.
|
|
25
|
+
* @example `@Index((user) => [user.email], { unique: true, where: { deletedAt: null } })`
|
|
26
26
|
*/
|
|
27
|
-
export declare function Index<E>(columns: (keys: KeyMap<E>) => readonly IndexColumnInput<FieldKey<
|
|
27
|
+
export declare function Index<E>(columns: (keys: KeyMap<E>) => readonly IndexColumnInput<FieldKey<E>, E>[], options?: EntityIndexOptions<E>): (entity: Type<E>) => void;
|
|
@@ -20,7 +20,7 @@ export function Entity(opts) {
|
|
|
20
20
|
/**
|
|
21
21
|
* Registers a named `$where` filter, applied to every query unless bypassed via `QueryOptions.filters`.
|
|
22
22
|
*
|
|
23
|
-
* @example `@Filter('active', {
|
|
23
|
+
* @example `@Filter('active', { where: { status: 'active' }, default: false })`
|
|
24
24
|
*/
|
|
25
25
|
export function Filter(name, opts) {
|
|
26
26
|
return (entity) => {
|
|
@@ -34,7 +34,7 @@ export function Filter(name, opts) {
|
|
|
34
34
|
*
|
|
35
35
|
* @example `@Index((user) => [user.lastName, user.firstName], { name: 'users_fullname_idx' })`
|
|
36
36
|
* @example `@Index((user) => [user.email], { unique: true })`
|
|
37
|
-
* @example `@Index((user) => [user.
|
|
37
|
+
* @example `@Index((user) => [user.email], { unique: true, where: { deletedAt: null } })`
|
|
38
38
|
*/
|
|
39
39
|
export function Index(columns, options = {}) {
|
|
40
40
|
return (entity) => {
|
|
@@ -40,12 +40,13 @@ type EnumValue<Members, Declared> = Declared extends Members ? {
|
|
|
40
40
|
*
|
|
41
41
|
* @example `@Field({ type: String }) name?: string;`
|
|
42
42
|
* @example `@Field({ references: () => User }) userId?: string;` (where `User.id` is a `uuid`)
|
|
43
|
+
* @example `@Field({ type: Number, computed: (line) => raw`${line.qty} * ${line.price}` }) total?: number;`
|
|
43
44
|
*/
|
|
44
|
-
export declare function Field<O extends FieldOptions<DeclaredValue<O
|
|
45
|
+
export declare function Field<This, O extends FieldOptions<DeclaredValue<O>, This> & ({
|
|
45
46
|
type: FieldType;
|
|
46
47
|
} | {
|
|
47
48
|
references: EntityGetter;
|
|
48
|
-
}) & RejectUnknown<O, FieldOptions> & RejectIncompatible<O>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
|
|
49
|
+
}) & RejectUnknown<O, FieldOptions> & RejectIncompatible<O>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined, This>;
|
|
49
50
|
/**
|
|
50
51
|
* A key the type level cannot name, reported on each `@Id` that leaves it unnamed. Where no `idKey`
|
|
51
52
|
* brand and no conventional name applies, `IdKey` falls back to every field, and `IdValue`,
|
|
@@ -8,6 +8,7 @@ import { memberRegistrations } from './bag.js';
|
|
|
8
8
|
*
|
|
9
9
|
* @example `@Field({ type: String }) name?: string;`
|
|
10
10
|
* @example `@Field({ references: () => User }) userId?: string;` (where `User.id` is a `uuid`)
|
|
11
|
+
* @example `@Field({ type: Number, computed: (line) => raw`${line.qty} * ${line.price}` }) total?: number;`
|
|
11
12
|
*/
|
|
12
13
|
export function Field(opts) {
|
|
13
14
|
return (_value, context) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
2
|
import { isInlinedExpression } from '../../util/field.util.js';
|
|
3
|
-
import { entityName, fieldOptionConflict, getKeys,
|
|
3
|
+
import { entityName, entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, definedEntries, } from '../../util/index.js';
|
|
4
4
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
5
5
|
/**
|
|
6
6
|
* A map held on `globalThis` through the global symbol registry, so a single one survives multiple
|
|
@@ -31,8 +31,14 @@ export function defineField(entity, key, opts = {}) {
|
|
|
31
31
|
// Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
|
|
32
32
|
// column from the referenced primary key (picking up its `columnType`, length and chained keys)
|
|
33
33
|
// instead of treating whatever ends up in `type` as deliberate.
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const { computed, ...rest } = opts;
|
|
35
|
+
const resolved = rest.type ? rest : { ...rest, typeFromReference: true };
|
|
36
|
+
meta.fields[fieldKey] = {
|
|
37
|
+
...meta.fields[fieldKey],
|
|
38
|
+
name: key,
|
|
39
|
+
...resolved,
|
|
40
|
+
...(computed && { computed: entitySql(computed) }),
|
|
41
|
+
};
|
|
36
42
|
return meta;
|
|
37
43
|
}
|
|
38
44
|
export function defineId(entity, key, opts) {
|
|
@@ -94,7 +100,7 @@ export function defineIndex(entity, index) {
|
|
|
94
100
|
(meta.indexes ??= []).push({
|
|
95
101
|
...index,
|
|
96
102
|
unique: index.unique ?? false,
|
|
97
|
-
where:
|
|
103
|
+
where: index.where && entityWhere(index.where),
|
|
98
104
|
columns: index.columns(keys).map(normalizeIndexColumn),
|
|
99
105
|
include: index.include?.(keys),
|
|
100
106
|
});
|
|
@@ -161,7 +167,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
161
167
|
});
|
|
162
168
|
// Unnamed checks are named by the generator, as unnamed indexes are.
|
|
163
169
|
for (const check of opts.checks ?? []) {
|
|
164
|
-
(meta.checks ??= []).push({ name: check.name,
|
|
170
|
+
(meta.checks ??= []).push({ name: check.name, where: entityWhere(check.where) });
|
|
165
171
|
}
|
|
166
172
|
for (const index of opts.indexes ?? []) {
|
|
167
173
|
defineIndex(entity, index);
|
|
@@ -204,7 +210,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
204
210
|
meta.softDelete = softDeleteKeys[0];
|
|
205
211
|
if (!meta.filters)
|
|
206
212
|
meta.filters = {};
|
|
207
|
-
meta.filters[SOFT_DELETE_FILTER] = {
|
|
213
|
+
meta.filters[SOFT_DELETE_FILTER] = { where: { [meta.softDelete]: null }, default: true };
|
|
208
214
|
}
|
|
209
215
|
const ids = getIdKeys(meta);
|
|
210
216
|
if (!ids.length) {
|
package/dist/http/contract.d.ts
CHANGED
|
@@ -86,7 +86,7 @@ export type RequestErrorResponse = {
|
|
|
86
86
|
};
|
|
87
87
|
/**
|
|
88
88
|
* Map a thrown error to the wire error envelope. Honors a numeric `status` on the error
|
|
89
|
-
* (e.g. hooks throwing 403), defaults to 500; `code` mirrors the HTTP status.
|
|
89
|
+
* (e.g. hooks throwing 403), then a constraint violation (409/400), defaults to 500; `code` mirrors the HTTP status.
|
|
90
90
|
*/
|
|
91
91
|
export declare function toErrorResponse(err: unknown): {
|
|
92
92
|
status: number;
|
package/dist/http/contract.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { queryErrorKind } from '../querier/queryError.js';
|
|
1
2
|
// the specific util modules, not the barrel, so the browser bundle does not pull in entity metadata
|
|
2
3
|
import { getKeys } from '../util/object.util.js';
|
|
3
4
|
import { kebabCase } from '../util/string.util.js';
|
|
@@ -59,12 +60,23 @@ export function matchRoute(method, subPath) {
|
|
|
59
60
|
}
|
|
60
61
|
return idOp && subPath !== undefined ? { op: idOp, method: CRUD_ROUTES[idOp].method, id: subPath } : undefined;
|
|
61
62
|
}
|
|
63
|
+
/** Generic on purpose: a driver's constraint message names tables and constraints, and Postgres echoes the value. */
|
|
64
|
+
const CONSTRAINT_ERRORS = new Map([
|
|
65
|
+
['uniqueViolation', { message: 'Conflict', code: 409 }],
|
|
66
|
+
['foreignKeyViolation', { message: 'Conflict', code: 409 }],
|
|
67
|
+
['notNullViolation', { message: 'Bad Request', code: 400 }],
|
|
68
|
+
['checkViolation', { message: 'Bad Request', code: 400 }],
|
|
69
|
+
]);
|
|
62
70
|
/**
|
|
63
71
|
* Map a thrown error to the wire error envelope. Honors a numeric `status` on the error
|
|
64
|
-
* (e.g. hooks throwing 403), defaults to 500; `code` mirrors the HTTP status.
|
|
72
|
+
* (e.g. hooks throwing 403), then a constraint violation (409/400), defaults to 500; `code` mirrors the HTTP status.
|
|
65
73
|
*/
|
|
66
74
|
export function toErrorResponse(err) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
const error = err instanceof Error && 'status' in err && typeof err.status === 'number'
|
|
76
|
+
? { message: err.message, code: err.status }
|
|
77
|
+
: (CONSTRAINT_ERRORS.get(queryErrorKind(err)) ?? {
|
|
78
|
+
message: err instanceof Error ? err.message : 'Internal Server Error',
|
|
79
|
+
code: 500,
|
|
80
|
+
});
|
|
81
|
+
return { status: error.code, body: { error } };
|
|
70
82
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - OperationRecorder: Record operations only (for code generation)
|
|
6
6
|
* - MigrationBuilder: Execute DDL operations (for integration tests/runtime)
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { indexNameParts, normalizeIndexColumn } from '../../util/index.js';
|
|
9
9
|
import { derivedIndexName } from '../../util/sql.util.js';
|
|
10
10
|
import { createSchemaGenerator } from '../schemaGenerator.js';
|
|
11
11
|
import { splitSqlStatements } from './splitSqlStatements.js';
|
|
@@ -23,9 +23,7 @@ function createIndexOperation(tableName, columns, options = {}) {
|
|
|
23
23
|
tableName,
|
|
24
24
|
index: {
|
|
25
25
|
...index,
|
|
26
|
-
name: name ??
|
|
27
|
-
derivedIndexName(tableName, entries.map((entry) => entry.column)),
|
|
28
|
-
where: ddlText(index.where, 'a partial-index predicate'),
|
|
26
|
+
name: name ?? derivedIndexName(tableName, indexNameParts(entries)),
|
|
29
27
|
entries,
|
|
30
28
|
unique: unique ?? false,
|
|
31
29
|
},
|
|
@@ -310,7 +308,7 @@ export class MigrationBuilder extends OperationRecorder {
|
|
|
310
308
|
case 'alterColumn':
|
|
311
309
|
return this.sqlGenerator.generateAlterColumnSql(operation.tableName, operation.columnName, operation.changes);
|
|
312
310
|
case 'createIndex':
|
|
313
|
-
return this.sqlGenerator.
|
|
311
|
+
return this.sqlGenerator.generateCreateIndexFromDefinition(operation.tableName, operation.index);
|
|
314
312
|
case 'dropIndex':
|
|
315
313
|
return this.sqlGenerator.generateDropIndex(operation.tableName, operation.indexName);
|
|
316
314
|
case 'addForeignKey':
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Fluent API for defining tables in migrations.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { indexNameParts, normalizeIndexColumn } from '../../util/index.js';
|
|
7
7
|
import { derivedIndexName } from '../../util/sql.util.js';
|
|
8
8
|
import { columnForeignKey, columnIndex } from '../generator/definitionToNode.js';
|
|
9
9
|
import { ColumnBuilder } from './columnBuilder.js';
|
|
@@ -171,9 +171,7 @@ export class TableBuilder {
|
|
|
171
171
|
const entries = columns.map(normalizeIndexColumn);
|
|
172
172
|
this._indexes.push({
|
|
173
173
|
...rest,
|
|
174
|
-
name: name ??
|
|
175
|
-
derivedIndexName(this._name, entries.map((entry) => entry.column), unique),
|
|
176
|
-
where: ddlText(rest.where, 'a partial-index predicate'),
|
|
174
|
+
name: name ?? derivedIndexName(this._name, indexNameParts(entries), unique),
|
|
177
175
|
entries,
|
|
178
176
|
unique,
|
|
179
177
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Enables type-safe migrations without raw SQL.
|
|
6
6
|
*/
|
|
7
7
|
import type { ColumnNode, EnumValues, ForeignKeyAction } from '../../schema/types.js';
|
|
8
|
-
import type { IndexColumnInput, IndexOptions, IndexSchema } from '../../type/index.js';
|
|
8
|
+
import type { EntityIndexColumn, Except, IndexColumnInput, IndexOptions, IndexSchema, QueryRaw } from '../../type/index.js';
|
|
9
9
|
import type { ForeignKeySchema } from '../../type/migration.js';
|
|
10
10
|
/**
|
|
11
11
|
* Foreign key reference options.
|
|
@@ -91,6 +91,14 @@ export interface FullColumnDefinition extends ColumnDefinition {
|
|
|
91
91
|
/** Index name (if indexed) */
|
|
92
92
|
index?: string | boolean;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* An index as the builder records it, with the entries and options `@Index` takes: its SQL kept as `raw`
|
|
96
|
+
* until a generator renders it for the engine the migration runs on.
|
|
97
|
+
*/
|
|
98
|
+
export type IndexDefinition = Except<IndexSchema, 'entries' | 'where'> & {
|
|
99
|
+
readonly entries: readonly EntityIndexColumn[];
|
|
100
|
+
readonly where?: QueryRaw;
|
|
101
|
+
};
|
|
94
102
|
/**
|
|
95
103
|
* Complete table definition.
|
|
96
104
|
*/
|
|
@@ -102,7 +110,7 @@ export interface TableDefinition {
|
|
|
102
110
|
/** Primary key columns (for composite keys) */
|
|
103
111
|
primaryKey?: string[];
|
|
104
112
|
/** Index definitions */
|
|
105
|
-
indexes:
|
|
113
|
+
indexes: IndexDefinition[];
|
|
106
114
|
/** Foreign key definitions at table level */
|
|
107
115
|
foreignKeys: ForeignKeySchema[];
|
|
108
116
|
/** Table comment */
|
|
@@ -183,7 +191,7 @@ export interface RenameColumnOperation extends MigrationOperation {
|
|
|
183
191
|
export interface CreateIndexOperation extends MigrationOperation {
|
|
184
192
|
type: 'createIndex';
|
|
185
193
|
tableName: string;
|
|
186
|
-
index:
|
|
194
|
+
index: IndexDefinition;
|
|
187
195
|
ifNotExists?: boolean;
|
|
188
196
|
}
|
|
189
197
|
/**
|
|
@@ -89,7 +89,9 @@ export function indexNeedsRaw(index) {
|
|
|
89
89
|
}
|
|
90
90
|
function indexEntrySource(entry, propertyName, param) {
|
|
91
91
|
if (entry.expression) {
|
|
92
|
-
|
|
92
|
+
const expression = `() => ${rawTag(entry.column)}`;
|
|
93
|
+
const modifiers = significantModifiers(entry);
|
|
94
|
+
return modifiers.length === 0 ? expression : `{ column: ${expression}, ${modifiers.join(', ')} }`;
|
|
93
95
|
}
|
|
94
96
|
const column = memberSource(param, propertyName(entry.column));
|
|
95
97
|
const modifiers = significantModifiers(entry);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { ColumnNode, TableNode } from '../../schema/types.js';
|
|
2
2
|
import type { ForeignKeySchema, IndexSchema } from '../../type/migration.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { QueryRaw } from '../../type/queryRaw.js';
|
|
4
|
+
import type { FullColumnDefinition, IndexDefinition, TableDefinition } from '../builder/types.js';
|
|
4
5
|
/**
|
|
5
6
|
* A migration builder's table definition as the AST nodes the generators render from, so a hand-written
|
|
6
|
-
* `createTable` and an entity reach `generateCreateTableFromNode` in the same shape
|
|
7
|
-
* not generator methods:
|
|
7
|
+
* `createTable` and an entity reach `generateCreateTableFromNode` in the same shape, its SQL rendered by
|
|
8
|
+
* `render`. Free functions and not generator methods: the dialect reaches them only through `render`.
|
|
8
9
|
*/
|
|
9
|
-
export declare function tableDefinitionToNode(def: TableDefinition): TableNode;
|
|
10
|
+
export declare function tableDefinitionToNode(def: TableDefinition, render: (sql: QueryRaw) => string): TableNode;
|
|
10
11
|
/**
|
|
11
12
|
* A builder's column as the AST node the generators render from.
|
|
12
13
|
*
|
|
@@ -25,6 +26,8 @@ export declare function fullColumnDefinitionToNode(col: FullColumnDefinition, ta
|
|
|
25
26
|
* Shared with `TableBuilder.build`, which lifts these into the table it is creating: written twice,
|
|
26
27
|
* `addColumn` had no lift at all and silently emitted a column with no index.
|
|
27
28
|
*/
|
|
28
|
-
export declare function columnIndex(tableName: string, col: FullColumnDefinition): IndexSchema | undefined;
|
|
29
|
+
export declare function columnIndex(tableName: string, col: FullColumnDefinition): Pick<IndexSchema, 'name' | 'entries' | 'unique'> | undefined;
|
|
30
|
+
/** An index the builder recorded, its SQL rendered by `render` into the text the schema holds. */
|
|
31
|
+
export declare function renderIndexDefinition(index: IndexDefinition, render: (sql: QueryRaw) => string): IndexSchema;
|
|
29
32
|
/** The foreign key a column-level `references` declares, or nothing. Shared for the same reason. */
|
|
30
33
|
export declare function columnForeignKey(col: FullColumnDefinition): ForeignKeySchema | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderIndexColumn } from '../../util/ddlExpression.util.js';
|
|
1
2
|
import { derivedForeignKeyName, derivedIndexName } from '../../util/sql.util.js';
|
|
2
3
|
/**
|
|
3
4
|
* A table the builder names but has not seen.
|
|
@@ -12,10 +13,10 @@ function unresolvedTable(name) {
|
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
15
|
* A migration builder's table definition as the AST nodes the generators render from, so a hand-written
|
|
15
|
-
* `createTable` and an entity reach `generateCreateTableFromNode` in the same shape
|
|
16
|
-
* not generator methods:
|
|
16
|
+
* `createTable` and an entity reach `generateCreateTableFromNode` in the same shape, its SQL rendered by
|
|
17
|
+
* `render`. Free functions and not generator methods: the dialect reaches them only through `render`.
|
|
17
18
|
*/
|
|
18
|
-
export function tableDefinitionToNode(def) {
|
|
19
|
+
export function tableDefinitionToNode(def, render) {
|
|
19
20
|
const columns = new Map();
|
|
20
21
|
const pkNodes = [];
|
|
21
22
|
const table = {
|
|
@@ -40,7 +41,7 @@ export function tableDefinitionToNode(def) {
|
|
|
40
41
|
: pkNodes;
|
|
41
42
|
table.primaryKey = finalPrimaryKey;
|
|
42
43
|
for (const idxDef of def.indexes) {
|
|
43
|
-
table.indexes.push({ ...idxDef, table });
|
|
44
|
+
table.indexes.push({ ...renderIndexDefinition(idxDef, render), table });
|
|
44
45
|
}
|
|
45
46
|
for (const fkDef of def.foreignKeys) {
|
|
46
47
|
const relNode = {
|
|
@@ -92,6 +93,14 @@ export function columnIndex(tableName, col) {
|
|
|
92
93
|
unique: col.isUnique,
|
|
93
94
|
};
|
|
94
95
|
}
|
|
96
|
+
/** An index the builder recorded, its SQL rendered by `render` into the text the schema holds. */
|
|
97
|
+
export function renderIndexDefinition(index, render) {
|
|
98
|
+
return {
|
|
99
|
+
...index,
|
|
100
|
+
entries: index.entries.map((entry) => renderIndexColumn(entry, render)),
|
|
101
|
+
where: index.where && render(index.where),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
95
104
|
/** The foreign key a column-level `references` declares, or nothing. Shared for the same reason. */
|
|
96
105
|
export function columnForeignKey(col) {
|
|
97
106
|
if (!col.foreignKey) {
|
|
@@ -8,6 +8,8 @@ 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
|
+
/** A collection has no SQL to render a check, a computed column or an index expression into. */
|
|
12
|
+
compileDdl(): string;
|
|
11
13
|
/**
|
|
12
14
|
* A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
|
|
13
15
|
* defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
|
|
@@ -3,6 +3,7 @@ import { getMeta } from '../../entity/index.js';
|
|
|
3
3
|
import { mongoDialectFeatures } from '../../mongo/mongoDialect.js';
|
|
4
4
|
import { getKeys } from '../../util/index.js';
|
|
5
5
|
import { derivedIndexName } from '../../util/sql.util.js';
|
|
6
|
+
import { renderIndexDefinition } from './definitionToNode.js';
|
|
6
7
|
import { indexNodeToSchema } from './indexNodeToSchema.js';
|
|
7
8
|
import { serializeMongoCommand } from './mongoCommand.js';
|
|
8
9
|
export class MongoSchemaGenerator extends AbstractDialect {
|
|
@@ -14,6 +15,10 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
14
15
|
super({ namingStrategy });
|
|
15
16
|
this.defaultForeignKeyAction = defaultForeignKeyAction;
|
|
16
17
|
}
|
|
18
|
+
/** A collection has no SQL to render a check, a computed column or an index expression into. */
|
|
19
|
+
compileDdl() {
|
|
20
|
+
throw new TypeError('mongodb has no SQL to render a check, a computed column or an index expression into');
|
|
21
|
+
}
|
|
17
22
|
/**
|
|
18
23
|
* A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
|
|
19
24
|
* defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
|
|
@@ -135,7 +140,7 @@ export class MongoSchemaGenerator extends AbstractDialect {
|
|
|
135
140
|
generateCreateTableFromDefinition(table, _options) {
|
|
136
141
|
return [
|
|
137
142
|
serializeMongoCommand({ action: 'createCollection', name: table.name }),
|
|
138
|
-
...table.indexes.map((index) => this.generateCreateIndex(table.name, index)),
|
|
143
|
+
...table.indexes.map((index) => this.generateCreateIndex(table.name, renderIndexDefinition(index, () => this.compileDdl()))),
|
|
139
144
|
];
|
|
140
145
|
}
|
|
141
146
|
generateRenameTableSql(oldName, newName) {
|
|
@@ -2,8 +2,8 @@ import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
|
2
2
|
import type { SchemaAST } from '../schema/schemaAST.js';
|
|
3
3
|
import { type DiffOptions } from '../schema/schemaASTDiffer.js';
|
|
4
4
|
import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
|
|
5
|
-
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, MigratorDialect, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
|
|
6
|
-
import type { FullColumnDefinition, TableDefinition } from './builder/types.js';
|
|
5
|
+
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, EntityWhereMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, MigratorDialect, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
|
|
6
|
+
import type { FullColumnDefinition, IndexDefinition, TableDefinition } from './builder/types.js';
|
|
7
7
|
import { type IndexDdl, type TableDdl } from './ddl/index.js';
|
|
8
8
|
/**
|
|
9
9
|
* Unified SQL schema generator.
|
|
@@ -23,6 +23,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
23
23
|
resolveTableAlias<E>(meta: EntityMeta<E>): string;
|
|
24
24
|
resolveSchema<E>(meta: EntityMeta<E>): string | undefined;
|
|
25
25
|
resolveColumnName(key: string, field: FieldOptions): string;
|
|
26
|
+
compileDdl(sql: EntityWhereMeta<object>, entity: Type<object>): string;
|
|
26
27
|
/** Escape an identifier (table name, column name, etc.) */
|
|
27
28
|
protected escapeId(identifier: string): string;
|
|
28
29
|
/**
|
|
@@ -165,6 +166,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
165
166
|
generateCreateTableFromDefinition(table: TableDefinition, options?: {
|
|
166
167
|
ifNotExists?: boolean;
|
|
167
168
|
}): string[];
|
|
169
|
+
generateCreateIndexFromDefinition(tableName: string, index: IndexDefinition): string;
|
|
168
170
|
generateRenameTableSql(oldName: string, newName: string): string;
|
|
169
171
|
/**
|
|
170
172
|
* `ADD COLUMN`, plus the constraint and index the column declares.
|
|
@@ -217,7 +219,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
217
219
|
* other way and the table is created under one name and compared under another, which reports every
|
|
218
220
|
* table of a project using a naming strategy as both missing and unexpected.
|
|
219
221
|
*/
|
|
220
|
-
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName'>, entities: readonly Type<object>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
|
|
222
|
+
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName' | 'compileDdl'>, entities: readonly Type<object>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
|
|
221
223
|
/**
|
|
222
224
|
* Synchronous factory for SQL schema generators only.
|
|
223
225
|
* For MongoDB, use `createSchemaGeneratorAsync` from `./schemaGeneratorAsync.js` so the optional `mongodb` peer is not loaded at import time.
|
|
@@ -8,7 +8,7 @@ import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '
|
|
|
8
8
|
import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
|
|
9
9
|
import { indexDdlFor, tableDdlFor } from './ddl/index.js';
|
|
10
10
|
import { sizedType } from './ddl/tableDdl.js';
|
|
11
|
-
import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, tableDefinitionToNode, } from './generator/definitionToNode.js';
|
|
11
|
+
import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, renderIndexDefinition, tableDefinitionToNode, } from './generator/definitionToNode.js';
|
|
12
12
|
import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
|
|
13
13
|
/**
|
|
14
14
|
* Unified SQL schema generator.
|
|
@@ -45,6 +45,9 @@ export class SqlSchemaGenerator {
|
|
|
45
45
|
resolveColumnName(key, field) {
|
|
46
46
|
return this.dialect.resolveColumnName(key, field);
|
|
47
47
|
}
|
|
48
|
+
compileDdl(sql, entity) {
|
|
49
|
+
return this.dialect.compileDdl(sql, entity);
|
|
50
|
+
}
|
|
48
51
|
/** Escape an identifier (table name, column name, etc.) */
|
|
49
52
|
escapeId(identifier) {
|
|
50
53
|
return this.dialect.escapeId(identifier);
|
|
@@ -565,9 +568,12 @@ export class SqlSchemaGenerator {
|
|
|
565
568
|
return this.generateCreateIndex(qualifyName(index.table.name, index.table.schema), indexNodeToSchema(index), options);
|
|
566
569
|
}
|
|
567
570
|
generateCreateTableFromDefinition(table, options = {}) {
|
|
568
|
-
const tableNode = tableDefinitionToNode(table);
|
|
571
|
+
const tableNode = tableDefinitionToNode(table, (sql) => this.dialect.compileDdl(sql));
|
|
569
572
|
return this.generateCreateTableFromNode(tableNode, options);
|
|
570
573
|
}
|
|
574
|
+
generateCreateIndexFromDefinition(tableName, index) {
|
|
575
|
+
return this.generateCreateIndex(tableName, renderIndexDefinition(index, (sql) => this.dialect.compileDdl(sql)));
|
|
576
|
+
}
|
|
571
577
|
generateRenameTableSql(oldName, newName) {
|
|
572
578
|
return this.tableDdl.renameTable(oldName, newName);
|
|
573
579
|
}
|
|
@@ -719,6 +725,7 @@ export function buildEntityAST(generator, entities, defaultForeignKeyAction) {
|
|
|
719
725
|
resolveTableName: (meta) => generator.resolveTableAlias(meta),
|
|
720
726
|
resolveSchema: (meta) => generator.resolveSchema(meta),
|
|
721
727
|
resolveColumnName: (key, field) => generator.resolveColumnName(key, field),
|
|
728
|
+
compileDdl: (sql, entity) => generator.compileDdl(sql, entity),
|
|
722
729
|
defaultForeignKeyAction,
|
|
723
730
|
});
|
|
724
731
|
}
|
|
@@ -263,7 +263,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
263
263
|
* lock silently removes the mutual exclusion the caller asked for.
|
|
264
264
|
*/
|
|
265
265
|
assertNoLock(q) {
|
|
266
|
-
if (q.$lock
|
|
266
|
+
if (q.$lock) {
|
|
267
267
|
throw new TypeError('$lock (row-level locking) is not supported on MongoDB');
|
|
268
268
|
}
|
|
269
269
|
}
|
|
@@ -273,8 +273,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
273
273
|
* scalar - which is why reading and writing need the two different binders here.
|
|
274
274
|
*/
|
|
275
275
|
jsonScalarParam(ctx, value) {
|
|
276
|
-
return (this.#jsonCompound(ctx, value) ??
|
|
277
|
-
this.addValue(ctx.values, typeof value === 'boolean' ? JSON.stringify(value) : value));
|
|
276
|
+
return (this.#jsonCompound(ctx, value) ?? this.addValue(ctx, typeof value === 'boolean' ? JSON.stringify(value) : value));
|
|
278
277
|
}
|
|
279
278
|
/**
|
|
280
279
|
* A value being *written* to a JSON path, which takes the type the driver sent: a `BIT` becomes a
|
|
@@ -286,7 +285,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
286
285
|
if (compound) {
|
|
287
286
|
return compound;
|
|
288
287
|
}
|
|
289
|
-
const placeholder = this.addValue(ctx
|
|
288
|
+
const placeholder = this.addValue(ctx, value);
|
|
290
289
|
return typeof value === 'boolean' ? `CAST(${placeholder} AS BIT)` : placeholder;
|
|
291
290
|
}
|
|
292
291
|
/**
|
|
@@ -300,8 +299,7 @@ export class MsSqlDialect extends MergeSqlDialect {
|
|
|
300
299
|
if (value === null || typeof value !== 'object') {
|
|
301
300
|
return undefined;
|
|
302
301
|
}
|
|
303
|
-
|
|
304
|
-
return `JSON_QUERY(${this.placeholder(ctx.values.length)})`;
|
|
302
|
+
return `JSON_QUERY(${this.addValue(ctx, JSON.stringify(value))})`;
|
|
305
303
|
}
|
|
306
304
|
/** An exploded element compares as text here, so `$elemMatch` always expands per field. */
|
|
307
305
|
jsonContainmentIsPartial = false;
|