uql-orm 0.57.0 → 0.58.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/README.md +6 -8
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/dialect/abstractSqlDialect.d.ts +2 -2
- package/dist/dialect/abstractSqlDialect.js +4 -4
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +1 -1
- package/dist/dialect/queryJoins.js +1 -0
- package/dist/entity/decorator/bag.d.ts +2 -2
- package/dist/entity/decorator/entity.d.ts +8 -9
- package/dist/entity/decorator/entity.js +6 -7
- package/dist/entity/decorator/members.d.ts +7 -6
- package/dist/entity/decorator/members.js +2 -1
- package/dist/entity/metadata/definition.d.ts +16 -11
- package/dist/entity/metadata/definition.js +51 -39
- package/dist/http/handler.d.ts +2 -2
- package/dist/http/handler.js +0 -1
- package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
- package/dist/migrate/codegen/entityTypes.d.ts +1 -1
- package/dist/migrate/codegen/entityTypes.js +4 -3
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
- package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
- package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
- package/dist/migrate/codegen/sourceLiteral.js +4 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
- package/dist/migrate/migrator.d.ts +2 -2
- package/dist/migrate/schemaGenerator.d.ts +5 -5
- package/dist/mongo/mongoDialect.d.ts +1 -1
- package/dist/mongo/mongoDialect.js +3 -3
- package/dist/mongo/mongodbQuerier.js +0 -1
- package/dist/querier/abstractSqlQuerier.js +6 -6
- package/dist/schema/schemaASTBuilder.d.ts +3 -3
- package/dist/schema/schemaASTBuilder.js +2 -2
- package/dist/type/config.d.ts +1 -1
- package/dist/type/entity.d.ts +108 -68
- package/dist/type/migration.d.ts +7 -7
- package/dist/type/querierPool.d.ts +2 -2
- package/dist/type/query.d.ts +19 -27
- package/dist/type/queryAggregate.d.ts +38 -29
- package/dist/type/queryWhere.d.ts +12 -9
- package/dist/util/dialect.util.d.ts +3 -3
- package/dist/util/dialect.util.js +24 -15
- package/dist/util/relationQuery.util.d.ts +1 -1
- package/package.json +1 -1
|
@@ -38,18 +38,41 @@ export function defineField(entity, key, opts = {}) {
|
|
|
38
38
|
export function defineId(entity, key, opts) {
|
|
39
39
|
return defineField(entity, key, { ...opts, isId: true });
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
// is left at its default here rather than tied to the class being registered.
|
|
41
|
+
/** `T` is the relation's target, independent of the owner `E`. */
|
|
43
42
|
export function defineRelation(entity, key, opts) {
|
|
44
|
-
|
|
43
|
+
return addRelation(entity, key, relationRegistration(opts));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `opts` as the registry takes them: `mappedBy` and `references` read off their key maps down to the
|
|
47
|
+
* names they give. The callbacks only read properties, so they run here, before any entity has to
|
|
48
|
+
* exist, and the registry holds data alone.
|
|
49
|
+
*/
|
|
50
|
+
export function relationRegistration({ mappedBy, references, ...opts }) {
|
|
51
|
+
return {
|
|
52
|
+
...opts,
|
|
53
|
+
...(mappedBy ? { mappedBy: mappedBy(keyMap()) } : {}),
|
|
54
|
+
...(references ? { references: [...references(keyMap(), keyMap())] } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Every entity's key map: a callback only reads one property off it, and that property is its own key. */
|
|
58
|
+
function keyMap() {
|
|
59
|
+
return KEY_MAP;
|
|
60
|
+
}
|
|
61
|
+
const KEY_MAP = new Proxy({}, { get: (_, key) => key });
|
|
62
|
+
function addRelation(entity, key, registration) {
|
|
63
|
+
if (!registration.entity) {
|
|
45
64
|
throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
|
|
46
65
|
}
|
|
66
|
+
if (registration.through && registration.references) {
|
|
67
|
+
throw new TypeError(`'${entity.name}.${key}' joins through a junction, whose columns follow the convention; 'references' ` +
|
|
68
|
+
"pairs the declaring entity's columns with the target's instead.");
|
|
69
|
+
}
|
|
47
70
|
const meta = ensureWritableMeta(entity);
|
|
48
|
-
// Registration writes
|
|
49
|
-
// `
|
|
50
|
-
//
|
|
71
|
+
// Registration writes into a map declared as resolved: `getMeta` runs `fillRelations`, which settles
|
|
72
|
+
// `references` or throws. Bridging the two shapes here is what lets every consumer read `RelationMeta`
|
|
73
|
+
// without asserting.
|
|
51
74
|
const relations = meta.relations;
|
|
52
|
-
relations[key] = { ...relations[key], ...
|
|
75
|
+
relations[key] = { ...relations[key], ...registration };
|
|
53
76
|
return meta;
|
|
54
77
|
}
|
|
55
78
|
export function defineHook(entity, methodName, event) {
|
|
@@ -62,18 +85,18 @@ export function defineHook(entity, methodName, event) {
|
|
|
62
85
|
return meta;
|
|
63
86
|
}
|
|
64
87
|
/**
|
|
65
|
-
* Declares a composite index. `unique` and the authored column sugar
|
|
66
|
-
* lets the dialects render one shape instead of re-parsing it.
|
|
88
|
+
* Declares a composite index, its columns read off the key map. `unique` and the authored column sugar
|
|
89
|
+
* are normalized here, which is what lets the dialects render one shape instead of re-parsing it.
|
|
67
90
|
*/
|
|
68
91
|
export function defineIndex(entity, index) {
|
|
69
92
|
const meta = ensureWritableMeta(entity);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
meta.indexes.push({
|
|
93
|
+
const keys = keyMap();
|
|
94
|
+
(meta.indexes ??= []).push({
|
|
73
95
|
...index,
|
|
74
96
|
unique: index.unique ?? false,
|
|
75
97
|
where: ddlText(index.where, 'a partial-index predicate'),
|
|
76
|
-
columns: index.columns.map(normalizeIndexColumn),
|
|
98
|
+
columns: index.columns(keys).map(normalizeIndexColumn),
|
|
99
|
+
include: index.include?.(keys),
|
|
77
100
|
});
|
|
78
101
|
return meta;
|
|
79
102
|
}
|
|
@@ -104,7 +127,7 @@ export function applyMembers(entity, specs) {
|
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
129
|
for (const [key, spec] of definedEntries(specs?.relations ?? {})) {
|
|
107
|
-
|
|
130
|
+
addRelation(entity, key, spec);
|
|
108
131
|
}
|
|
109
132
|
for (const [event, methodNames] of definedEntries(specs?.hooks ?? {})) {
|
|
110
133
|
for (const methodName of methodNames) {
|
|
@@ -113,10 +136,8 @@ export function applyMembers(entity, specs) {
|
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
138
|
/**
|
|
116
|
-
* Registers an entity
|
|
117
|
-
*
|
|
118
|
-
* column, and one assembled at runtime is the column bag it is. Pass `Row` to name a shape the spec
|
|
119
|
-
* cannot describe, such as the interface `uql-migrate types` generated for it.
|
|
139
|
+
* Registers a class as an entity from `opts` alone, the decorator-free counterpart of `@Entity()` with
|
|
140
|
+
* `@Field`/`@ManyToOne`/...
|
|
120
141
|
*/
|
|
121
142
|
export function defineEntity(entity, opts = {}) {
|
|
122
143
|
// Ahead of any registration, so a rejected definition leaves nothing half-written in the registry.
|
|
@@ -132,7 +153,12 @@ export function defineEntity(entity, opts = {}) {
|
|
|
132
153
|
// drains `context.metadata` itself, because TypeScript only attaches `Symbol.metadata` to the class
|
|
133
154
|
// after class decorators return; draining empties the bag, so whichever runs second is a no-op.
|
|
134
155
|
applyMembers(entity, ownRegistrations(entity));
|
|
135
|
-
|
|
156
|
+
const keys = keyMap();
|
|
157
|
+
applyMembers(entity, {
|
|
158
|
+
fields: opts.fields,
|
|
159
|
+
relations: Object.fromEntries(definedEntries(opts.relations ?? {}).map(([key, spec]) => [key, relationRegistration(spec)])),
|
|
160
|
+
hooks: Object.fromEntries(definedEntries(opts.hooks ?? {}).map(([event, methods]) => [event, methods(keys)])),
|
|
161
|
+
});
|
|
136
162
|
// Unnamed checks are named by the generator, as unnamed indexes are.
|
|
137
163
|
for (const check of opts.checks ?? []) {
|
|
138
164
|
(meta.checks ??= []).push({ name: check.name, expression: ddlText(check.expression, 'a check constraint') });
|
|
@@ -267,12 +293,7 @@ export function removeEntity(entity) {
|
|
|
267
293
|
return metas.delete(entity);
|
|
268
294
|
}
|
|
269
295
|
export function getEntities() {
|
|
270
|
-
return metas.
|
|
271
|
-
if (val.ids.length) {
|
|
272
|
-
acc.push(key);
|
|
273
|
-
}
|
|
274
|
-
return acc;
|
|
275
|
-
}, []);
|
|
296
|
+
return [...metas.values()].filter((meta) => meta.ids.length).map((meta) => meta.entity);
|
|
276
297
|
}
|
|
277
298
|
/**
|
|
278
299
|
* The metadata of `entity`, marked as changed. Every `define*` goes through this, and nothing outside
|
|
@@ -308,11 +329,11 @@ export function getMeta(entity) {
|
|
|
308
329
|
}
|
|
309
330
|
function fillRelations(meta) {
|
|
310
331
|
for (const [relKey, relation] of definedEntries(meta.relations)) {
|
|
311
|
-
// The
|
|
332
|
+
// The registered view: `references` may be unset until this settles it.
|
|
312
333
|
const relOpts = relation;
|
|
313
334
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
314
335
|
if (relOpts.mappedBy) {
|
|
315
|
-
fillInverseSide(at, meta, relOpts);
|
|
336
|
+
fillInverseSide(at, meta, relOpts, relOpts.mappedBy);
|
|
316
337
|
}
|
|
317
338
|
else if (!relOpts.references) {
|
|
318
339
|
fillOwningSide(at, meta, relKey, relOpts);
|
|
@@ -326,8 +347,8 @@ function fillRelations(meta) {
|
|
|
326
347
|
for (const { local } of relOpts.references) {
|
|
327
348
|
if (junction.fields[local])
|
|
328
349
|
continue;
|
|
329
|
-
throw new TypeError(`${at} joins through '${junction.entity.name}', which has no '${local}' field
|
|
330
|
-
|
|
350
|
+
throw new TypeError(`${at} joins through '${junction.entity.name}', which has no '${local}' field: a junction's ` +
|
|
351
|
+
'columns are named after the entities it joins. Declare it.');
|
|
331
352
|
}
|
|
332
353
|
}
|
|
333
354
|
}
|
|
@@ -372,11 +393,9 @@ function fillOwningSide(at, meta, relKey, relOpts) {
|
|
|
372
393
|
};
|
|
373
394
|
}
|
|
374
395
|
}
|
|
375
|
-
function fillInverseSide(at, meta, relOpts) {
|
|
396
|
+
function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
376
397
|
const relEntity = relOpts.entity();
|
|
377
398
|
const relMeta = getMeta(relEntity);
|
|
378
|
-
const mappedBy = getMappedByKey(relOpts);
|
|
379
|
-
relOpts.mappedBy = mappedBy;
|
|
380
399
|
if (relOpts.references)
|
|
381
400
|
return;
|
|
382
401
|
if (relMeta.fields[mappedBy]) {
|
|
@@ -448,13 +467,6 @@ function fillForeignKeyRelations(meta) {
|
|
|
448
467
|
function junctionColumn(meta, idKey) {
|
|
449
468
|
return lowerFirst(entityName(meta)) + upperFirst(fieldOf(meta, idKey).name ?? idKey);
|
|
450
469
|
}
|
|
451
|
-
/** A callback only reads one property off the key map, and that property is the key, so one serves every entity. */
|
|
452
|
-
const RELATION_KEY_MAP = new Proxy({}, { get: (_, key) => key });
|
|
453
|
-
function getMappedByKey(relOpts) {
|
|
454
|
-
return typeof relOpts.mappedBy === 'function'
|
|
455
|
-
? relOpts.mappedBy(RELATION_KEY_MAP)
|
|
456
|
-
: relOpts.mappedBy;
|
|
457
|
-
}
|
|
458
470
|
/** Every key the entity marks, in declaration order. More than one is a composite primary key. */
|
|
459
471
|
function getIdKeys(meta) {
|
|
460
472
|
return getKeys(meta.fields).filter((key) => meta.fields[key]?.isId);
|
package/dist/http/handler.d.ts
CHANGED
|
@@ -49,8 +49,8 @@ export type HookContext<E extends object, Ctx = unknown> = {
|
|
|
49
49
|
export type Hook<Ctx = unknown> = <E extends object>(ctx: HookContext<E, Ctx>) => void | Promise<void>;
|
|
50
50
|
export type ResponseHook<Ctx = unknown> = <E extends object>(ctx: HookContext<E, Ctx>, envelope: RequestSuccessResponse<unknown>) => void | Promise<void>;
|
|
51
51
|
export type RequestHandlerOptions<Ctx = unknown> = {
|
|
52
|
-
include?: Type<
|
|
53
|
-
exclude?: Type<
|
|
52
|
+
include?: Type<object>[];
|
|
53
|
+
exclude?: Type<object>[];
|
|
54
54
|
/**
|
|
55
55
|
* The URL segment an entity is addressed by, defaulting to its kebab-cased class name.
|
|
56
56
|
*
|
package/dist/http/handler.js
CHANGED
|
@@ -27,7 +27,6 @@ export function createRequestHandler(opts) {
|
|
|
27
27
|
"A route is the kebab-cased class name unless 'entityPath' says otherwise. Name them apart, " +
|
|
28
28
|
"pass an 'entityPath', or pass only one of them in 'include'.");
|
|
29
29
|
}
|
|
30
|
-
// oxlint-disable-next-line typescript/no-explicit-any -- heterogeneous entity map
|
|
31
30
|
const entityByPath = new Map([...byPath].map(([path, [entity]]) => [path, entity]));
|
|
32
31
|
return (req) => {
|
|
33
32
|
const entity = entityByPath.get(req.entityPath);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { canonicalToTypeScript } from '../../schema/canonicalType.js';
|
|
13
13
|
import { DEFAULT_FOREIGN_KEY_ACTION, } from '../../schema/types.js';
|
|
14
|
-
import { camelCase, pascalCase, singularize } from '../../util/string.util.js';
|
|
14
|
+
import { camelCase, lowerFirst, pascalCase, singularize } from '../../util/string.util.js';
|
|
15
15
|
import { buildFieldOptionsSource, fieldNeedsRaw } from './fieldOptionsSource.js';
|
|
16
16
|
import { buildIndexDecoratorSource, indexNeedsRaw, isPlainFieldIndex } from './indexDecoratorSource.js';
|
|
17
17
|
/**
|
|
@@ -116,8 +116,9 @@ export class EntityCodeGenerator {
|
|
|
116
116
|
buildEntityDecorators(table) {
|
|
117
117
|
const lines = [];
|
|
118
118
|
if (this.options.includeIndexes) {
|
|
119
|
+
const param = lowerFirst(this.options.classNameTransformer(table.name));
|
|
119
120
|
for (const index of this.declaredIndexes(table)) {
|
|
120
|
-
lines.push(buildIndexDecoratorSource(index, this.options.propertyNameTransformer));
|
|
121
|
+
lines.push(buildIndexDecoratorSource(index, this.options.propertyNameTransformer, param));
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
// Entity decorator
|
|
@@ -260,9 +261,10 @@ export class EntityCodeGenerator {
|
|
|
260
261
|
lines.push(` * Inverse relation from ${rel.from.table.name}`);
|
|
261
262
|
lines.push(' */');
|
|
262
263
|
}
|
|
263
|
-
//
|
|
264
|
+
// The inverse side, mapped by the related class's property that points back at this one.
|
|
264
265
|
const inverseProp = this.options.propertyNameTransformer(this.options.singularize(table.name));
|
|
265
|
-
|
|
266
|
+
const param = lowerFirst(relatedClassName);
|
|
267
|
+
lines.push(` @${decoratorName}({ entity: () => ${relatedClassName}, mappedBy: (${param}) => ${param}.${inverseProp} })`);
|
|
266
268
|
// Property
|
|
267
269
|
if (inverseType === 'OneToMany' || inverseType === 'ManyToMany') {
|
|
268
270
|
lines.push(` ${propertyName}?: ${relatedClassName}[];`);
|
|
@@ -4,4 +4,4 @@ import type { Type } from '../../type/index.js';
|
|
|
4
4
|
* compiler too. Interfaces, not the entity classes `generate:from-db` writes: those are the source of
|
|
5
5
|
* a schema, these describe one already defined elsewhere.
|
|
6
6
|
*/
|
|
7
|
-
export declare function entityTypesSource(entities: readonly Type<
|
|
7
|
+
export declare function entityTypesSource(entities: readonly Type<object>[]): string;
|
|
@@ -2,6 +2,7 @@ import { getMeta } from '../../entity/index.js';
|
|
|
2
2
|
import { canonicalToTypeScript } from '../../schema/canonicalType.js';
|
|
3
3
|
import { resolveColumnCanonicalType } from '../../schema/schemaASTBuilder.js';
|
|
4
4
|
import { isToManyRelation, upperFirst } from '../../util/index.js';
|
|
5
|
+
import { isIdentifierName } from './sourceLiteral.js';
|
|
5
6
|
/**
|
|
6
7
|
* A `.d.ts` for the entities as registered, so a shape that only exists at runtime reaches the
|
|
7
8
|
* compiler too. Interfaces, not the entity classes `generate:from-db` writes: those are the source of
|
|
@@ -41,12 +42,12 @@ function interfaceNames(metas) {
|
|
|
41
42
|
}
|
|
42
43
|
/** `text` as an identifier: what cannot be in one is dropped, and what cannot start one is prefixed. */
|
|
43
44
|
function identifier(text) {
|
|
44
|
-
const stripped = text.replace(/[
|
|
45
|
-
return
|
|
45
|
+
const stripped = text.replace(/[^\p{ID_Continue}$\u200C\u200D]/gu, '');
|
|
46
|
+
return isIdentifierName(stripped) ? stripped : `Entity${stripped}`;
|
|
46
47
|
}
|
|
47
48
|
/** A column name a property cannot hold - `hero-image` - is quoted rather than dropped. */
|
|
48
49
|
function member(key, type) {
|
|
49
|
-
const name =
|
|
50
|
+
const name = isIdentifierName(key) ? key : JSON.stringify(key);
|
|
50
51
|
return ` ${name}?: ${type};`;
|
|
51
52
|
}
|
|
52
53
|
/**
|
|
@@ -2,13 +2,14 @@ import type { IndexNode } from '../../schema/types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Whether `@Field({ index })` can carry the whole index. It says only "this column is indexed under
|
|
4
4
|
* this name", so anything else the index declares - an expression, a predicate, uniqueness, an access
|
|
5
|
-
* method, stored columns, a stored order - has to be written out as `@Index
|
|
5
|
+
* method, stored columns, a stored order - has to be written out as an `@Index` instead.
|
|
6
6
|
*/
|
|
7
7
|
export declare function isPlainFieldIndex(index: IndexNode): boolean;
|
|
8
8
|
/**
|
|
9
|
-
* One `@Index([...])` as source, for an index no `@Field` can express
|
|
10
|
-
* expression entry, so callers import `raw` when
|
|
9
|
+
* One `@Index((user) => [...])` as source, for an index no `@Field` can express, its columns read off the
|
|
10
|
+
* key map `param` names. Emits `raw(...)` for an expression entry, so callers import `raw` when
|
|
11
|
+
* {@link indexNeedsRaw} holds.
|
|
11
12
|
*/
|
|
12
|
-
export declare function buildIndexDecoratorSource(index: IndexNode, propertyName: (column: string) => string): string;
|
|
13
|
+
export declare function buildIndexDecoratorSource(index: IndexNode, propertyName: (column: string) => string, param: string): string;
|
|
13
14
|
/** Whether emitting this index needs `raw` imported alongside `Index`. */
|
|
14
15
|
export declare function indexNeedsRaw(index: IndexNode): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { rawTag } from './sourceLiteral.js';
|
|
1
|
+
import { isIdentifierName, quoted, rawTag } from './sourceLiteral.js';
|
|
2
2
|
/**
|
|
3
3
|
* A vector index carries its metric in the operator class pgvector names after it
|
|
4
4
|
* (`vector_cosine_ops`), which is the only place introspection can recover it from. `@Index` requires
|
|
@@ -37,7 +37,7 @@ function significantModifiers(entry) {
|
|
|
37
37
|
/**
|
|
38
38
|
* Whether `@Field({ index })` can carry the whole index. It says only "this column is indexed under
|
|
39
39
|
* this name", so anything else the index declares - an expression, a predicate, uniqueness, an access
|
|
40
|
-
* method, stored columns, a stored order - has to be written out as `@Index
|
|
40
|
+
* method, stored columns, a stored order - has to be written out as an `@Index` instead.
|
|
41
41
|
*/
|
|
42
42
|
export function isPlainFieldIndex(index) {
|
|
43
43
|
const entries = index.entries;
|
|
@@ -53,11 +53,12 @@ export function isPlainFieldIndex(index) {
|
|
|
53
53
|
significantModifiers(entry).length === 0);
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* One `@Index([...])` as source, for an index no `@Field` can express
|
|
57
|
-
* expression entry, so callers import `raw` when
|
|
56
|
+
* One `@Index((user) => [...])` as source, for an index no `@Field` can express, its columns read off the
|
|
57
|
+
* key map `param` names. Emits `raw(...)` for an expression entry, so callers import `raw` when
|
|
58
|
+
* {@link indexNeedsRaw} holds.
|
|
58
59
|
*/
|
|
59
|
-
export function buildIndexDecoratorSource(index, propertyName) {
|
|
60
|
-
const entries = index.entries.map((entry) => indexEntrySource(entry, propertyName)).join(', ');
|
|
60
|
+
export function buildIndexDecoratorSource(index, propertyName, param) {
|
|
61
|
+
const entries = index.entries.map((entry) => indexEntrySource(entry, propertyName, param)).join(', ');
|
|
61
62
|
const isVector = index.type === 'hnsw' || index.type === 'ivfflat';
|
|
62
63
|
const distance = isVector ? vectorDistance(index) : undefined;
|
|
63
64
|
const options = [];
|
|
@@ -77,21 +78,24 @@ export function buildIndexDecoratorSource(index, propertyName) {
|
|
|
77
78
|
if (index.where)
|
|
78
79
|
options.push(`where: ${rawTag(index.where)}`);
|
|
79
80
|
if (index.include?.length) {
|
|
80
|
-
|
|
81
|
+
const included = index.include.map((column) => memberSource(param, propertyName(column)));
|
|
82
|
+
options.push(`include: (${param}) => [${included.join(', ')}]`);
|
|
81
83
|
}
|
|
82
|
-
return `@Index([${entries}]${options.length > 0 ? `, { ${options.join(', ')} }` : ''})`;
|
|
84
|
+
return `@Index((${param}) => [${entries}]${options.length > 0 ? `, { ${options.join(', ')} }` : ''})`;
|
|
83
85
|
}
|
|
84
86
|
/** Whether emitting this index needs `raw` imported alongside `Index`. */
|
|
85
87
|
export function indexNeedsRaw(index) {
|
|
86
88
|
return Boolean(index.where) || index.entries.some((entry) => entry.expression);
|
|
87
89
|
}
|
|
88
|
-
function indexEntrySource(entry, propertyName) {
|
|
90
|
+
function indexEntrySource(entry, propertyName, param) {
|
|
89
91
|
if (entry.expression) {
|
|
90
92
|
return rawTag(entry.column);
|
|
91
93
|
}
|
|
94
|
+
const column = memberSource(param, propertyName(entry.column));
|
|
92
95
|
const modifiers = significantModifiers(entry);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
return modifiers.length === 0 ? column : `{ column: ${column}, ${modifiers.join(', ')} }`;
|
|
97
|
+
}
|
|
98
|
+
/** `user.email`, or `user['first-name']` for a property name that is no identifier. */
|
|
99
|
+
function memberSource(param, property) {
|
|
100
|
+
return isIdentifierName(property) ? `${param}.${property}` : `${param}[${quoted(property)}]`;
|
|
97
101
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Whether `text` can name a property unquoted, in any script: `dueño` can, `first-name` cannot. */
|
|
2
|
+
export declare function isIdentifierName(text: string): boolean;
|
|
1
3
|
/**
|
|
2
4
|
* A string as single-quoted source. Introspected text is arbitrary - a comment or a default
|
|
3
5
|
* expression can hold a quote or a backslash - and only escaping both keeps the generated file
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Whether `text` can name a property unquoted, in any script: `dueño` can, `first-name` cannot. */
|
|
2
|
+
export function isIdentifierName(text) {
|
|
3
|
+
return /^[\p{ID_Start}$_][\p{ID_Continue}$\u200C\u200D]*$/u.test(text);
|
|
4
|
+
}
|
|
1
5
|
/**
|
|
2
6
|
* A string as single-quoted source. Introspected text is arbitrary - a comment or a default
|
|
3
7
|
* expression can hold a quote or a backslash - and only escaping both keeps the generated file
|
|
@@ -13,8 +13,8 @@ export declare class MongoSchemaGenerator extends AbstractDialect implements Sch
|
|
|
13
13
|
* defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
|
|
14
14
|
* and ignored for the same reason.
|
|
15
15
|
*/
|
|
16
|
-
generateCreateSchema(entities: readonly Type<
|
|
17
|
-
generateDropSchema(entities: readonly Type<
|
|
16
|
+
generateCreateSchema(entities: readonly Type<object>[], options?: CreateSchemaOptions): string[];
|
|
17
|
+
generateDropSchema(entities: readonly Type<object>[]): string[];
|
|
18
18
|
private selected;
|
|
19
19
|
/**
|
|
20
20
|
* The indexes `@Field({ index })` declares, as the collection would hold them.
|
|
@@ -48,5 +48,5 @@ export declare class MongoSchemaGenerator extends AbstractDialect implements Sch
|
|
|
48
48
|
ifNotExists?: boolean;
|
|
49
49
|
}): string[];
|
|
50
50
|
generateRenameTableSql(oldName: string, newName: string): string;
|
|
51
|
-
diffSchema
|
|
51
|
+
diffSchema(entity: Type<object>, currentTable: TableNode | undefined): SchemaDiff | undefined;
|
|
52
52
|
}
|
|
@@ -12,7 +12,7 @@ export declare class Migrator {
|
|
|
12
12
|
get logger(): LoggerWrapper;
|
|
13
13
|
set logger(value: LoggingOptions);
|
|
14
14
|
private readonly _entities?;
|
|
15
|
-
get entities(): Type<
|
|
15
|
+
get entities(): Type<object>[];
|
|
16
16
|
readonly dialectName: DialectName;
|
|
17
17
|
schemaGenerator?: SchemaGenerator;
|
|
18
18
|
schemaIntrospector?: SchemaIntrospector;
|
|
@@ -89,7 +89,7 @@ export declare class Migrator {
|
|
|
89
89
|
* {@link schemaIntrospector} so a caller that replaced it still wins.
|
|
90
90
|
*/
|
|
91
91
|
private introspectClaimedSchemas;
|
|
92
|
-
findEntityForTable(tableName: string): Promise<Type<
|
|
92
|
+
findEntityForTable(tableName: string): Promise<Type<object> | undefined>;
|
|
93
93
|
/**
|
|
94
94
|
* Applies the entity schema to the database: every registered entity, or the one `entity` names.
|
|
95
95
|
*
|
|
@@ -46,7 +46,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
46
46
|
protected columnSqlType(col: ColumnNode): string;
|
|
47
47
|
protected canonicalTypeToSql(type: CanonicalType): string;
|
|
48
48
|
/** The entity side as an AST, carrying this generator's default referential action. */
|
|
49
|
-
buildAST(entities: readonly Type<
|
|
49
|
+
buildAST(entities: readonly Type<object>[]): SchemaAST;
|
|
50
50
|
/**
|
|
51
51
|
* Every `CREATE TABLE` for `entities`, then their foreign keys.
|
|
52
52
|
*
|
|
@@ -56,14 +56,14 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
56
56
|
* then `createForeignKeys()`). SQLite is the exception and keeps them inline: it cannot `ALTER` a
|
|
57
57
|
* foreign key in, but it resolves targets lazily, so a forward reference is fine there.
|
|
58
58
|
*/
|
|
59
|
-
generateCreateSchema(entities: readonly Type<
|
|
59
|
+
generateCreateSchema(entities: readonly Type<object>[], options?: CreateSchemaOptions): string[];
|
|
60
60
|
/**
|
|
61
61
|
* One statement per distinct schema the tables being created live in, in first-seen order. Only
|
|
62
62
|
* the tables actually being created, so a narrowed `only` does not declare namespaces it is not
|
|
63
63
|
* about to fill. Empty on an engine without schemas, whose tables are never qualified.
|
|
64
64
|
*/
|
|
65
65
|
private generateCreateSchemas;
|
|
66
|
-
generateDropSchema(entities: readonly Type<
|
|
66
|
+
generateDropSchema(entities: readonly Type<object>[], options?: DropSchemaOptions): string[];
|
|
67
67
|
/**
|
|
68
68
|
* The tables of `entities` in dependency order, optionally narrowed to `only`. The AST always spans
|
|
69
69
|
* every entity even when narrowed, so a relation pointing at a table outside the subset still
|
|
@@ -132,7 +132,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
132
132
|
* longer disagree about what has changed. Only two things are this side's own: the entity becomes a
|
|
133
133
|
* table node first, and types are compared as the *engine* would store them - see `normalizeType`.
|
|
134
134
|
*/
|
|
135
|
-
diffSchema
|
|
135
|
+
diffSchema(entity: Type<object>, currentTable: TableNode | undefined, desiredAst?: SchemaAST): SchemaDiff | undefined;
|
|
136
136
|
/**
|
|
137
137
|
* Indexes the entity declares that the table does not already have, in any shape.
|
|
138
138
|
*
|
|
@@ -217,7 +217,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
|
|
|
217
217
|
* other way and the table is created under one name and compared under another, which reports every
|
|
218
218
|
* table of a project using a naming strategy as both missing and unexpected.
|
|
219
219
|
*/
|
|
220
|
-
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName'>, entities: readonly Type<
|
|
220
|
+
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName'>, entities: readonly Type<object>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
|
|
221
221
|
/**
|
|
222
222
|
* Synchronous factory for SQL schema generators only.
|
|
223
223
|
* For MongoDB, use `createSchemaGeneratorAsync` from `./schemaGeneratorAsync.js` so the optional `mongodb` peer is not loaded at import time.
|
|
@@ -152,7 +152,7 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
152
152
|
/** Whether a `$sort` reads a relation, which is what forces the lookups to run before it. */
|
|
153
153
|
sortsRelations<E extends Document>(entity: Type<E>, sort: QuerySortMap<E> | undefined): boolean;
|
|
154
154
|
/**
|
|
155
|
-
* Aggregate results are keyed by `$group`/`$
|
|
155
|
+
* Aggregate results are keyed by `$group`/`$select` alias rather than by column, so an aggregate
|
|
156
156
|
* `$sort` addresses those aliases as-is - the same reason the SQL dialects sort by alias there.
|
|
157
157
|
*/
|
|
158
158
|
private aliasSort;
|
|
@@ -551,7 +551,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
551
551
|
return someKey(sort, (key) => Boolean(meta.relations[key]));
|
|
552
552
|
}
|
|
553
553
|
/**
|
|
554
|
-
* Aggregate results are keyed by `$group`/`$
|
|
554
|
+
* Aggregate results are keyed by `$group`/`$select` alias rather than by column, so an aggregate
|
|
555
555
|
* `$sort` addresses those aliases as-is - the same reason the SQL dialects sort by alias there.
|
|
556
556
|
*/
|
|
557
557
|
aliasSort(sort) {
|
|
@@ -958,7 +958,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
958
958
|
}
|
|
959
959
|
}
|
|
960
960
|
// $group stage
|
|
961
|
-
const { groupId, groupAccumulators, distinctReducers } = this.buildGroupSpec(getMeta(entity), parseGroupMap(q.$group, q.$
|
|
961
|
+
const { groupId, groupAccumulators, distinctReducers } = this.buildGroupSpec(getMeta(entity), parseGroupMap(q.$group, q.$select));
|
|
962
962
|
pipeline.push({ $group: { _id: hasKeys(groupId) ? groupId : null, ...groupAccumulators } });
|
|
963
963
|
// Project stage - rename _id fields back to their original names, and reduce collected distinct
|
|
964
964
|
// sets. Needed whenever there are group keys OR any distinct alias.
|
|
@@ -1008,7 +1008,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1008
1008
|
*/
|
|
1009
1009
|
buildGroupSpec(meta, groupEntries) {
|
|
1010
1010
|
if (!groupEntries.length) {
|
|
1011
|
-
throw new TypeError('aggregate requires at least one $group column or $
|
|
1011
|
+
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
1012
1012
|
}
|
|
1013
1013
|
const groupId = {};
|
|
1014
1014
|
const groupAccumulators = {};
|
|
@@ -126,7 +126,6 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
126
126
|
async internalAggregate(entity, q, opts) {
|
|
127
127
|
return this.timed('internalAggregate', undefined, async () => {
|
|
128
128
|
const pipeline = this.dialect.buildAggregateStages(entity, q, opts);
|
|
129
|
-
// oxlint-disable-next-line typescript/no-explicit-any -- aggregate result type matches QueryAggregateResult at runtime but TS can't verify
|
|
130
129
|
return this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
131
130
|
});
|
|
132
131
|
}
|
|
@@ -329,17 +329,17 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
329
329
|
async internalAggregate(entity, q, opts) {
|
|
330
330
|
const ctx = this.dialect.createContext();
|
|
331
331
|
this.dialect.aggregate(ctx, entity, q, opts);
|
|
332
|
-
|
|
333
|
-
const res = await this.all(ctx.sql, ctx.values);
|
|
332
|
+
const rows = await this.all(ctx.sql, ctx.values);
|
|
334
333
|
const hydratable = this.dialect.hydratableAggregates(entity, q);
|
|
335
|
-
for (const row of
|
|
334
|
+
for (const row of rows) {
|
|
335
|
+
const cells = row;
|
|
336
336
|
for (const [alias, kind] of hydratable) {
|
|
337
|
-
if (
|
|
338
|
-
|
|
337
|
+
if (cells[alias] != null) {
|
|
338
|
+
cells[alias] = decodeColumn(cells[alias], kind);
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
|
-
return
|
|
342
|
+
return rows;
|
|
343
343
|
}
|
|
344
344
|
async internalInsertMany(entity, rows) {
|
|
345
345
|
const meta = getMeta(entity);
|
|
@@ -15,9 +15,9 @@ import { type CanonicalType, type ForeignKeyAction } from './types.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export interface BuildSchemaASTOptions {
|
|
17
17
|
/** Custom resolver for a table's own name, unqualified. */
|
|
18
|
-
resolveTableName?: (meta: EntityMeta<
|
|
18
|
+
resolveTableName?: (meta: EntityMeta<object>) => string;
|
|
19
19
|
/** Custom resolver for the schema a table lives in; `undefined` leaves it unqualified. */
|
|
20
|
-
resolveSchema?: (meta: EntityMeta<
|
|
20
|
+
resolveSchema?: (meta: EntityMeta<object>) => string | undefined;
|
|
21
21
|
/** Custom column name resolver */
|
|
22
22
|
resolveColumnName?: (key: string, field: FieldOptions) => string;
|
|
23
23
|
/** Naming strategy to use */
|
|
@@ -31,7 +31,7 @@ export interface BuildSchemaASTOptions {
|
|
|
31
31
|
* Three passes, because each needs the one before it to have finished for *every* entity: a relation
|
|
32
32
|
* resolves against a table another entity declares, and an index against the columns of its own.
|
|
33
33
|
*/
|
|
34
|
-
export declare function buildSchemaAST(entities: readonly Type<
|
|
34
|
+
export declare function buildSchemaAST(entities: readonly Type<object>[], options?: BuildSchemaASTOptions): SchemaAST;
|
|
35
35
|
/**
|
|
36
36
|
* Resolve the canonical type for a field, inheriting from the referenced
|
|
37
37
|
* entity's primary key when the field is a foreign-key reference
|
|
@@ -168,7 +168,7 @@ function addRelationshipsFromEntity(ctx, meta) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
-
* Add indexes from field options (`@Field({ index })`), from `@Index
|
|
171
|
+
* Add indexes from field options (`@Field({ index })`), from `@Index`, and for every foreign
|
|
172
172
|
* key none of those already serves.
|
|
173
173
|
*/
|
|
174
174
|
function addIndexesFromEntity(ctx, meta) {
|
|
@@ -230,7 +230,7 @@ function resolveIncludeColumn(ctx, meta, column) {
|
|
|
230
230
|
return field ? ctx.resolveColumnName(column, field) : column;
|
|
231
231
|
}
|
|
232
232
|
/**
|
|
233
|
-
* One `@Index
|
|
233
|
+
* One `@Index`. Its entries keep the authored form (expression, prefix length, order) with
|
|
234
234
|
* names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
|
|
235
235
|
* subset, which is what diffing and introspection compare.
|
|
236
236
|
*/
|
package/dist/type/config.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface Config {
|
|
|
17
17
|
* List of entity classes to be managed by the ORM.
|
|
18
18
|
* If omitted, classes that completed `defineEntity` (including via `@Entity()`) are discovered via `getEntities()`.
|
|
19
19
|
*/
|
|
20
|
-
entities?: Type<
|
|
20
|
+
entities?: Type<object>[];
|
|
21
21
|
/**
|
|
22
22
|
* The directory where migration files are stored.
|
|
23
23
|
* @default './migrations'
|