metal-orm 1.0.51 → 1.0.53
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 +18 -15
- package/dist/index.cjs +103 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -3
- package/dist/index.d.ts +68 -3
- package/dist/index.js +101 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/aggregate-functions.ts +10 -0
- package/src/core/functions/standard-strategy.ts +1 -1
- package/src/index.ts +4 -2
- package/src/orm/execute.ts +37 -36
- package/src/orm/jsonify.ts +27 -0
- package/src/orm/orm-session.ts +28 -22
- package/src/orm/save-graph-types.ts +55 -0
- package/src/orm/save-graph.ts +141 -105
- package/src/query-builder/delete.ts +5 -4
- package/src/query-builder/select.ts +68 -10
- package/src/query-builder/update.ts +5 -4
package/package.json
CHANGED
|
@@ -45,6 +45,16 @@ export const min = buildAggregate('MIN');
|
|
|
45
45
|
*/
|
|
46
46
|
export const max = buildAggregate('MAX');
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Creates a COUNT(*) function expression.
|
|
50
|
+
* @returns Function node with COUNT(*)
|
|
51
|
+
*/
|
|
52
|
+
export const countAll = (): FunctionNode => ({
|
|
53
|
+
type: 'Function',
|
|
54
|
+
name: 'COUNT',
|
|
55
|
+
args: []
|
|
56
|
+
});
|
|
57
|
+
|
|
48
58
|
type GroupConcatOrderByInput = {
|
|
49
59
|
column: ColumnRef | ColumnNode;
|
|
50
60
|
direction?: OrderDirection;
|
|
@@ -16,7 +16,7 @@ export class StandardFunctionStrategy implements FunctionStrategy {
|
|
|
16
16
|
|
|
17
17
|
protected registerStandard() {
|
|
18
18
|
// Register ANSI standard implementations
|
|
19
|
-
this.add('COUNT', ({ compiledArgs }) => `COUNT(${compiledArgs.join(', ')})`);
|
|
19
|
+
this.add('COUNT', ({ compiledArgs }) => compiledArgs.length ? `COUNT(${compiledArgs.join(', ')})` : 'COUNT(*)');
|
|
20
20
|
this.add('SUM', ({ compiledArgs }) => `SUM(${compiledArgs[0]})`);
|
|
21
21
|
this.add('AVG', ({ compiledArgs }) => `AVG(${compiledArgs[0]})`);
|
|
22
22
|
this.add('MIN', ({ compiledArgs }) => `MIN(${compiledArgs[0]})`);
|
package/src/index.ts
CHANGED
|
@@ -38,8 +38,10 @@ export * from './orm/execution-context.js';
|
|
|
38
38
|
export * from './orm/hydration-context.js';
|
|
39
39
|
export * from './orm/domain-event-bus.js';
|
|
40
40
|
export * from './orm/runtime-types.js';
|
|
41
|
-
export * from './orm/query-logger.js';
|
|
42
|
-
export * from './
|
|
41
|
+
export * from './orm/query-logger.js';
|
|
42
|
+
export * from './orm/jsonify.js';
|
|
43
|
+
export * from './orm/save-graph-types.js';
|
|
44
|
+
export * from './decorators/index.js';
|
|
43
45
|
|
|
44
46
|
// NEW: execution abstraction + helpers
|
|
45
47
|
export * from './core/execution/db-executor.js';
|
package/src/orm/execute.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { EntityInstance } from '../schema/types.js';
|
|
|
3
3
|
import { hydrateRows } from './hydration.js';
|
|
4
4
|
import { OrmSession } from './orm-session.ts';
|
|
5
5
|
import { SelectQueryBuilder } from '../query-builder/select.js';
|
|
6
|
-
import { createEntityProxy, createEntityFromRow } from './entity.js';
|
|
7
|
-
import { EntityContext } from './entity-context.js';
|
|
8
|
-
import { ExecutionContext } from './execution-context.js';
|
|
9
|
-
import { HydrationContext } from './hydration-context.js';
|
|
6
|
+
import { createEntityProxy, createEntityFromRow } from './entity.js';
|
|
7
|
+
import { EntityContext } from './entity-context.js';
|
|
8
|
+
import { ExecutionContext } from './execution-context.js';
|
|
9
|
+
import { HydrationContext } from './hydration-context.js';
|
|
10
10
|
|
|
11
11
|
type Row = Record<string, unknown>;
|
|
12
12
|
|
|
13
|
-
const flattenResults = (results: { columns: string[]; values: unknown[][] }[]): Row[] => {
|
|
13
|
+
const flattenResults = (results: { columns: string[]; values: unknown[][] }[]): Row[] => {
|
|
14
14
|
const rows: Row[] = [];
|
|
15
15
|
for (const result of results) {
|
|
16
16
|
const { columns, values } = result;
|
|
@@ -23,20 +23,21 @@ const flattenResults = (results: { columns: string[]; values: unknown[][] }[]):
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
return rows;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const executeWithContexts = async <TTable extends TableDef>(
|
|
29
|
+
execCtx: ExecutionContext,
|
|
30
|
+
entityCtx: EntityContext,
|
|
31
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
32
|
+
): Promise<EntityInstance<TTable>[]> => {
|
|
33
|
+
const ast = qb.getAST();
|
|
34
|
+
const compiled = execCtx.dialect.compileSelect(ast);
|
|
35
|
+
const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
36
|
+
const rows = flattenResults(executed);
|
|
37
|
+
|
|
38
|
+
if (ast.setOps && ast.setOps.length > 0) {
|
|
39
|
+
return rows.map(row => createEntityProxy(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
|
|
40
|
+
}
|
|
40
41
|
|
|
41
42
|
const hydrated = hydrateRows(rows, qb.getHydrationPlan());
|
|
42
43
|
return hydrated.map(row => createEntityFromRow(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
|
|
@@ -49,12 +50,12 @@ const executeWithEntityContext = async <TTable extends TableDef>(
|
|
|
49
50
|
* @param qb - The select query builder
|
|
50
51
|
* @returns Promise resolving to array of entity instances
|
|
51
52
|
*/
|
|
52
|
-
export async function executeHydrated<TTable extends TableDef>(
|
|
53
|
-
session: OrmSession,
|
|
54
|
-
qb: SelectQueryBuilder<unknown, TTable>
|
|
55
|
-
): Promise<EntityInstance<TTable>[]> {
|
|
56
|
-
return
|
|
57
|
-
}
|
|
53
|
+
export async function executeHydrated<TTable extends TableDef>(
|
|
54
|
+
session: OrmSession,
|
|
55
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
56
|
+
): Promise<EntityInstance<TTable>[]> {
|
|
57
|
+
return executeWithContexts(session.getExecutionContext(), session, qb);
|
|
58
|
+
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* Executes a hydrated query using execution and hydration contexts.
|
|
@@ -64,14 +65,14 @@ export async function executeHydrated<TTable extends TableDef>(
|
|
|
64
65
|
* @param qb - The select query builder
|
|
65
66
|
* @returns Promise resolving to array of entity instances
|
|
66
67
|
*/
|
|
67
|
-
export async function executeHydratedWithContexts<TTable extends TableDef>(
|
|
68
|
-
|
|
69
|
-
hydCtx: HydrationContext,
|
|
70
|
-
qb: SelectQueryBuilder<unknown, TTable>
|
|
71
|
-
): Promise<EntityInstance<TTable>[]> {
|
|
72
|
-
const entityCtx = hydCtx.entityContext;
|
|
73
|
-
if (!entityCtx) {
|
|
74
|
-
throw new Error('Hydration context is missing an EntityContext');
|
|
75
|
-
}
|
|
76
|
-
return
|
|
77
|
-
}
|
|
68
|
+
export async function executeHydratedWithContexts<TTable extends TableDef>(
|
|
69
|
+
execCtx: ExecutionContext,
|
|
70
|
+
hydCtx: HydrationContext,
|
|
71
|
+
qb: SelectQueryBuilder<unknown, TTable>
|
|
72
|
+
): Promise<EntityInstance<TTable>[]> {
|
|
73
|
+
const entityCtx = hydCtx.entityContext;
|
|
74
|
+
if (!entityCtx) {
|
|
75
|
+
throw new Error('Hydration context is missing an EntityContext');
|
|
76
|
+
}
|
|
77
|
+
return executeWithContexts(execCtx, entityCtx, qb);
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type JsonifyScalar<T> = T extends Date ? string : T;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shallow JSON-friendly mapping:
|
|
5
|
+
* - Date -> ISO string
|
|
6
|
+
* - Everything else unchanged
|
|
7
|
+
*/
|
|
8
|
+
export type Jsonify<T> = {
|
|
9
|
+
[K in keyof T]: JsonifyScalar<T[K]>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a shallow, JSON-friendly copy of an object by converting `Date` values to ISO strings.
|
|
14
|
+
* This intentionally does not deep-walk nested objects/relations.
|
|
15
|
+
*/
|
|
16
|
+
export const jsonify = <T extends object>(value: T): Jsonify<T> => {
|
|
17
|
+
const record = value as Record<string, unknown>;
|
|
18
|
+
const result: Record<string, unknown> = {};
|
|
19
|
+
|
|
20
|
+
for (const key of Object.keys(record)) {
|
|
21
|
+
const entry = record[key];
|
|
22
|
+
result[key] = entry instanceof Date ? entry.toISOString() : entry;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result as Jsonify<T>;
|
|
26
|
+
};
|
|
27
|
+
|
package/src/orm/orm-session.ts
CHANGED
|
@@ -27,9 +27,10 @@ import {
|
|
|
27
27
|
RelationKey,
|
|
28
28
|
TrackedEntity
|
|
29
29
|
} from './runtime-types.js';
|
|
30
|
-
import { executeHydrated } from './execute.js';
|
|
31
|
-
import { runInTransaction } from './transaction-runner.js';
|
|
32
|
-
import { saveGraphInternal, SaveGraphOptions } from './save-graph.js';
|
|
30
|
+
import { executeHydrated } from './execute.js';
|
|
31
|
+
import { runInTransaction } from './transaction-runner.js';
|
|
32
|
+
import { saveGraphInternal, SaveGraphOptions } from './save-graph.js';
|
|
33
|
+
import type { SaveGraphInputPayload } from './save-graph-types.js';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Interface for ORM interceptors that allow hooking into the flush lifecycle.
|
|
@@ -297,25 +298,30 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
|
|
|
297
298
|
return executeHydrated(this, qb);
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
/**
|
|
301
|
-
* Saves an entity graph (root + nested relations) based on a DTO-like payload.
|
|
302
|
-
* @param entityClass - Root entity constructor
|
|
303
|
-
* @param payload - DTO payload containing column values and nested relations
|
|
304
|
-
* @param options - Graph save options
|
|
305
|
-
* @returns The root entity instance
|
|
306
|
-
*/
|
|
307
|
-
async saveGraph<TCtor extends EntityConstructor<object>>(
|
|
308
|
-
entityClass: TCtor,
|
|
309
|
-
payload:
|
|
310
|
-
options?: SaveGraphOptions & { transactional?: boolean }
|
|
311
|
-
): Promise<InstanceType<TCtor
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Saves an entity graph (root + nested relations) based on a DTO-like payload.
|
|
303
|
+
* @param entityClass - Root entity constructor
|
|
304
|
+
* @param payload - DTO payload containing column values and nested relations
|
|
305
|
+
* @param options - Graph save options
|
|
306
|
+
* @returns The root entity instance
|
|
307
|
+
*/
|
|
308
|
+
async saveGraph<TCtor extends EntityConstructor<object>>(
|
|
309
|
+
entityClass: TCtor,
|
|
310
|
+
payload: SaveGraphInputPayload<InstanceType<TCtor>>,
|
|
311
|
+
options?: SaveGraphOptions & { transactional?: boolean }
|
|
312
|
+
): Promise<InstanceType<TCtor>>;
|
|
313
|
+
async saveGraph<TCtor extends EntityConstructor<object>>(
|
|
314
|
+
entityClass: TCtor,
|
|
315
|
+
payload: Record<string, unknown>,
|
|
316
|
+
options?: SaveGraphOptions & { transactional?: boolean }
|
|
317
|
+
): Promise<InstanceType<TCtor>> {
|
|
318
|
+
const { transactional = true, ...graphOptions } = options ?? {};
|
|
319
|
+
const execute = () => saveGraphInternal(this, entityClass, payload, graphOptions);
|
|
320
|
+
if (!transactional) {
|
|
321
|
+
return execute();
|
|
322
|
+
}
|
|
323
|
+
return this.transaction(() => execute());
|
|
324
|
+
}
|
|
319
325
|
|
|
320
326
|
/**
|
|
321
327
|
* Persists an entity (either inserts or updates).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BelongsToReference,
|
|
3
|
+
HasManyCollection,
|
|
4
|
+
HasOneReference,
|
|
5
|
+
ManyToManyCollection
|
|
6
|
+
} from '../schema/types.js';
|
|
7
|
+
|
|
8
|
+
type AnyId = number | string;
|
|
9
|
+
type AnyFn = (...args: unknown[]) => unknown;
|
|
10
|
+
|
|
11
|
+
type RelationWrapper =
|
|
12
|
+
| HasManyCollection<unknown>
|
|
13
|
+
| HasOneReference<unknown>
|
|
14
|
+
| BelongsToReference<unknown>
|
|
15
|
+
| ManyToManyCollection<unknown>;
|
|
16
|
+
|
|
17
|
+
type FunctionKeys<T> = {
|
|
18
|
+
[K in keyof T & string]-?: T[K] extends AnyFn ? K : never;
|
|
19
|
+
}[keyof T & string];
|
|
20
|
+
|
|
21
|
+
type RelationKeys<T> = {
|
|
22
|
+
[K in keyof T & string]-?: NonNullable<T[K]> extends RelationWrapper ? K : never;
|
|
23
|
+
}[keyof T & string];
|
|
24
|
+
|
|
25
|
+
type ColumnKeys<T> = Exclude<keyof T & string, FunctionKeys<T> | RelationKeys<T>>;
|
|
26
|
+
|
|
27
|
+
export type SaveGraphJsonScalar<T> = T extends Date ? string : T;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Input scalar type for `OrmSession.saveGraph` payloads.
|
|
31
|
+
*
|
|
32
|
+
* Note: runtime coercion is opt-in via `SaveGraphOptions.coerce`.
|
|
33
|
+
*/
|
|
34
|
+
export type SaveGraphInputScalar<T> = T;
|
|
35
|
+
|
|
36
|
+
type ColumnInput<TEntity> = {
|
|
37
|
+
[K in ColumnKeys<TEntity>]?: SaveGraphInputScalar<TEntity[K]>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type RelationInputValue<T> =
|
|
41
|
+
T extends HasManyCollection<infer C> ? Array<SaveGraphInputPayload<C> | AnyId> :
|
|
42
|
+
T extends HasOneReference<infer C> ? SaveGraphInputPayload<C> | AnyId | null :
|
|
43
|
+
T extends BelongsToReference<infer P> ? SaveGraphInputPayload<P> | AnyId | null :
|
|
44
|
+
T extends ManyToManyCollection<infer Tgt> ? Array<SaveGraphInputPayload<Tgt> | AnyId> :
|
|
45
|
+
never;
|
|
46
|
+
|
|
47
|
+
type RelationInput<TEntity> = {
|
|
48
|
+
[K in RelationKeys<TEntity>]?: RelationInputValue<NonNullable<TEntity[K]>>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Typed payload accepted by `OrmSession.saveGraph`:
|
|
53
|
+
* - Only entity scalar keys + relation keys are accepted.
|
|
54
|
+
*/
|
|
55
|
+
export type SaveGraphInputPayload<TEntity> = ColumnInput<TEntity> & RelationInput<TEntity>;
|
package/src/orm/save-graph.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EntityInstance,
|
|
3
|
-
HasManyCollection,
|
|
4
|
-
HasOneReference,
|
|
5
|
-
BelongsToReference,
|
|
6
|
-
ManyToManyCollection
|
|
7
|
-
} from '../schema/types.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
type
|
|
12
|
-
type
|
|
1
|
+
import type {
|
|
2
|
+
EntityInstance,
|
|
3
|
+
HasManyCollection,
|
|
4
|
+
HasOneReference,
|
|
5
|
+
BelongsToReference,
|
|
6
|
+
ManyToManyCollection
|
|
7
|
+
} from '../schema/types.js';
|
|
8
|
+
import { normalizeColumnType, type ColumnDef } from '../schema/column-types.js';
|
|
9
|
+
import {
|
|
10
|
+
RelationKinds,
|
|
11
|
+
type BelongsToManyRelation,
|
|
12
|
+
type BelongsToRelation,
|
|
13
|
+
type HasManyRelation,
|
|
13
14
|
type HasOneRelation,
|
|
14
15
|
type RelationDef
|
|
15
16
|
} from '../schema/relation.js';
|
|
@@ -23,10 +24,16 @@ import type { OrmSession } from './orm-session.js';
|
|
|
23
24
|
/**
|
|
24
25
|
* Options for controlling the behavior of save graph operations.
|
|
25
26
|
*/
|
|
26
|
-
export interface SaveGraphOptions {
|
|
27
|
-
/** Remove existing collection members that are not present in the payload */
|
|
28
|
-
pruneMissing?: boolean;
|
|
29
|
-
|
|
27
|
+
export interface SaveGraphOptions {
|
|
28
|
+
/** Remove existing collection members that are not present in the payload */
|
|
29
|
+
pruneMissing?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Coerce JSON-friendly input values into DB-friendly primitives.
|
|
32
|
+
* Currently:
|
|
33
|
+
* - Date -> ISO string (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
|
|
34
|
+
*/
|
|
35
|
+
coerce?: 'json';
|
|
36
|
+
}
|
|
30
37
|
|
|
31
38
|
/** Represents an entity object with arbitrary properties. */
|
|
32
39
|
|
|
@@ -44,26 +51,55 @@ type AnyEntity = Record<string, unknown>;
|
|
|
44
51
|
|
|
45
52
|
*/
|
|
46
53
|
|
|
47
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
55
|
+
|
|
56
|
+
const coerceColumnValue = (
|
|
57
|
+
table: TableDef,
|
|
58
|
+
columnName: string,
|
|
59
|
+
value: unknown,
|
|
60
|
+
options: SaveGraphOptions
|
|
61
|
+
): unknown => {
|
|
62
|
+
if (options.coerce !== 'json') return value;
|
|
63
|
+
if (value === null || value === undefined) return value;
|
|
64
|
+
|
|
65
|
+
const column = table.columns[columnName] as unknown as ColumnDef | undefined;
|
|
66
|
+
if (!column) return value;
|
|
67
|
+
|
|
68
|
+
const normalized = normalizeColumnType(column.type);
|
|
69
|
+
|
|
70
|
+
const isDateLikeColumn =
|
|
71
|
+
normalized === 'date' ||
|
|
72
|
+
normalized === 'datetime' ||
|
|
73
|
+
normalized === 'timestamp' ||
|
|
74
|
+
normalized === 'timestamptz';
|
|
75
|
+
|
|
76
|
+
if (isDateLikeColumn && value instanceof Date) {
|
|
77
|
+
return value.toISOString();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Future coercions can be added here based on `normalized`.
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const pickColumns = (table: TableDef, payload: AnyEntity, options: SaveGraphOptions): Record<string, unknown> => {
|
|
85
|
+
const columns: Record<string, unknown> = {};
|
|
86
|
+
for (const key of Object.keys(table.columns)) {
|
|
87
|
+
if (payload[key] !== undefined) {
|
|
88
|
+
columns[key] = coerceColumnValue(table, key, payload[key], options);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return columns;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const ensureEntity = <TTable extends TableDef>(
|
|
95
|
+
session: OrmSession,
|
|
96
|
+
table: TTable,
|
|
97
|
+
payload: AnyEntity,
|
|
98
|
+
options: SaveGraphOptions
|
|
99
|
+
): EntityInstance<TTable> => {
|
|
100
|
+
const pk = findPrimaryKey(table);
|
|
101
|
+
const row = pickColumns(table, payload, options);
|
|
102
|
+
const pkValue = payload[pk];
|
|
67
103
|
|
|
68
104
|
if (pkValue !== undefined && pkValue !== null) {
|
|
69
105
|
const tracked = session.getEntity(table, pkValue);
|
|
@@ -76,16 +112,16 @@ const ensureEntity = <TTable extends TableDef>(
|
|
|
76
112
|
}
|
|
77
113
|
}
|
|
78
114
|
|
|
79
|
-
return createEntityFromRow(session, table, row) as EntityInstance<TTable>;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const assignColumns = (table: TableDef, entity: AnyEntity, payload: AnyEntity): void => {
|
|
83
|
-
for (const key of Object.keys(table.columns)) {
|
|
84
|
-
if (payload[key] !== undefined) {
|
|
85
|
-
entity[key] = payload[key];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
};
|
|
115
|
+
return createEntityFromRow(session, table, row) as EntityInstance<TTable>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const assignColumns = (table: TableDef, entity: AnyEntity, payload: AnyEntity, options: SaveGraphOptions): void => {
|
|
119
|
+
for (const key of Object.keys(table.columns)) {
|
|
120
|
+
if (payload[key] !== undefined) {
|
|
121
|
+
entity[key] = coerceColumnValue(table, key, payload[key], options);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
89
125
|
|
|
90
126
|
const isEntityInCollection = (items: AnyEntity[], pkName: string, entity: AnyEntity): boolean => {
|
|
91
127
|
if (items.includes(entity)) return true;
|
|
@@ -121,13 +157,13 @@ const handleHasMany = async (
|
|
|
121
157
|
const asObj = typeof item === 'object' ? (item as AnyEntity) : { [targetPk]: item };
|
|
122
158
|
const pkValue = asObj[targetPk];
|
|
123
159
|
|
|
124
|
-
const current =
|
|
125
|
-
findInCollectionByPk(existing, targetPk, pkValue) ??
|
|
126
|
-
(pkValue !== undefined && pkValue !== null ? session.getEntity(targetTable, pkValue) : undefined);
|
|
127
|
-
|
|
128
|
-
const entity = current ?? ensureEntity(session, targetTable, asObj);
|
|
129
|
-
assignColumns(targetTable, entity as AnyEntity, asObj);
|
|
130
|
-
await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
|
|
160
|
+
const current =
|
|
161
|
+
findInCollectionByPk(existing, targetPk, pkValue) ??
|
|
162
|
+
(pkValue !== undefined && pkValue !== null ? session.getEntity(targetTable, pkValue) : undefined);
|
|
163
|
+
|
|
164
|
+
const entity = current ?? ensureEntity(session, targetTable, asObj, options);
|
|
165
|
+
assignColumns(targetTable, entity as AnyEntity, asObj, options);
|
|
166
|
+
await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
|
|
131
167
|
|
|
132
168
|
if (!isEntityInCollection(collection.getItems() as unknown as AnyEntity[], targetPk, entity as unknown as AnyEntity)) {
|
|
133
169
|
collection.attach(entity);
|
|
@@ -170,11 +206,11 @@ const handleHasOne = async (
|
|
|
170
206
|
}
|
|
171
207
|
return;
|
|
172
208
|
}
|
|
173
|
-
const attached = ref.set(payload as AnyEntity);
|
|
174
|
-
if (attached) {
|
|
175
|
-
await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
|
|
176
|
-
}
|
|
177
|
-
};
|
|
209
|
+
const attached = ref.set(payload as AnyEntity);
|
|
210
|
+
if (attached) {
|
|
211
|
+
await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
178
214
|
|
|
179
215
|
const handleBelongsTo = async (
|
|
180
216
|
session: OrmSession,
|
|
@@ -198,11 +234,11 @@ const handleBelongsTo = async (
|
|
|
198
234
|
}
|
|
199
235
|
return;
|
|
200
236
|
}
|
|
201
|
-
const attached = ref.set(payload as AnyEntity);
|
|
202
|
-
if (attached) {
|
|
203
|
-
await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
|
|
204
|
-
}
|
|
205
|
-
};
|
|
237
|
+
const attached = ref.set(payload as AnyEntity);
|
|
238
|
+
if (attached) {
|
|
239
|
+
await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
206
242
|
|
|
207
243
|
const handleBelongsToMany = async (
|
|
208
244
|
session: OrmSession,
|
|
@@ -229,14 +265,14 @@ const handleBelongsToMany = async (
|
|
|
229
265
|
continue;
|
|
230
266
|
}
|
|
231
267
|
|
|
232
|
-
const asObj = item as AnyEntity;
|
|
233
|
-
const pkValue = asObj[targetPk];
|
|
234
|
-
const entity = pkValue !== undefined && pkValue !== null
|
|
235
|
-
? session.getEntity(targetTable, pkValue) ?? ensureEntity(session, targetTable, asObj)
|
|
236
|
-
: ensureEntity(session, targetTable, asObj);
|
|
237
|
-
|
|
238
|
-
assignColumns(targetTable, entity as AnyEntity, asObj);
|
|
239
|
-
await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
|
|
268
|
+
const asObj = item as AnyEntity;
|
|
269
|
+
const pkValue = asObj[targetPk];
|
|
270
|
+
const entity = pkValue !== undefined && pkValue !== null
|
|
271
|
+
? session.getEntity(targetTable, pkValue) ?? ensureEntity(session, targetTable, asObj, options)
|
|
272
|
+
: ensureEntity(session, targetTable, asObj, options);
|
|
273
|
+
|
|
274
|
+
assignColumns(targetTable, entity as AnyEntity, asObj, options);
|
|
275
|
+
await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
|
|
240
276
|
|
|
241
277
|
if (!isEntityInCollection(collection.getItems() as unknown as AnyEntity[], targetPk, entity as unknown as AnyEntity)) {
|
|
242
278
|
collection.attach(entity);
|
|
@@ -278,36 +314,36 @@ const applyRelation = async (
|
|
|
278
314
|
}
|
|
279
315
|
};
|
|
280
316
|
|
|
281
|
-
const applyGraphToEntity = async (
|
|
282
|
-
session: OrmSession,
|
|
283
|
-
table: TableDef,
|
|
284
|
-
entity: AnyEntity,
|
|
285
|
-
payload: AnyEntity,
|
|
286
|
-
options: SaveGraphOptions
|
|
287
|
-
): Promise<void> => {
|
|
288
|
-
assignColumns(table, entity, payload);
|
|
289
|
-
|
|
290
|
-
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
291
|
-
if (!(relationName in payload)) continue;
|
|
292
|
-
await applyRelation(session, table, entity, relationName, relation as RelationDef, payload[relationName], options);
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
export const saveGraph = async <TTable extends TableDef>(
|
|
297
|
-
session: OrmSession,
|
|
298
|
-
entityClass: EntityConstructor,
|
|
299
|
-
payload: AnyEntity,
|
|
300
|
-
options: SaveGraphOptions = {}
|
|
301
|
-
): Promise<EntityInstance<TTable>> => {
|
|
317
|
+
const applyGraphToEntity = async (
|
|
318
|
+
session: OrmSession,
|
|
319
|
+
table: TableDef,
|
|
320
|
+
entity: AnyEntity,
|
|
321
|
+
payload: AnyEntity,
|
|
322
|
+
options: SaveGraphOptions
|
|
323
|
+
): Promise<void> => {
|
|
324
|
+
assignColumns(table, entity, payload, options);
|
|
325
|
+
|
|
326
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
327
|
+
if (!(relationName in payload)) continue;
|
|
328
|
+
await applyRelation(session, table, entity, relationName, relation as RelationDef, payload[relationName], options);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
export const saveGraph = async <TTable extends TableDef>(
|
|
333
|
+
session: OrmSession,
|
|
334
|
+
entityClass: EntityConstructor,
|
|
335
|
+
payload: AnyEntity,
|
|
336
|
+
options: SaveGraphOptions = {}
|
|
337
|
+
): Promise<EntityInstance<TTable>> => {
|
|
302
338
|
const table = getTableDefFromEntity(entityClass);
|
|
303
339
|
if (!table) {
|
|
304
340
|
throw new Error('Entity metadata has not been bootstrapped');
|
|
305
341
|
}
|
|
306
342
|
|
|
307
|
-
const root = ensureEntity<TTable>(session, table as TTable, payload);
|
|
308
|
-
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
309
|
-
return root;
|
|
310
|
-
};
|
|
343
|
+
const root = ensureEntity<TTable>(session, table as TTable, payload, options);
|
|
344
|
+
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
345
|
+
return root;
|
|
346
|
+
};
|
|
311
347
|
|
|
312
348
|
/**
|
|
313
349
|
|
|
@@ -325,7 +361,7 @@ export const saveGraph = async <TTable extends TableDef>(
|
|
|
325
361
|
|
|
326
362
|
*/
|
|
327
363
|
|
|
328
|
-
export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
364
|
+
export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
329
365
|
|
|
330
366
|
session: OrmSession,
|
|
331
367
|
|
|
@@ -337,7 +373,7 @@ export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
|
337
373
|
|
|
338
374
|
): Promise<InstanceType<TCtor>> => {
|
|
339
375
|
|
|
340
|
-
const table = getTableDefFromEntity(entityClass);
|
|
376
|
+
const table = getTableDefFromEntity(entityClass);
|
|
341
377
|
|
|
342
378
|
if (!table) {
|
|
343
379
|
|
|
@@ -345,10 +381,10 @@ export const saveGraphInternal = async <TCtor extends EntityConstructor>(
|
|
|
345
381
|
|
|
346
382
|
}
|
|
347
383
|
|
|
348
|
-
const root = ensureEntity(session, table, payload);
|
|
349
|
-
|
|
350
|
-
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
351
|
-
|
|
352
|
-
return root as unknown as InstanceType<TCtor>;
|
|
353
|
-
|
|
354
|
-
};
|
|
384
|
+
const root = ensureEntity(session, table, payload, options);
|
|
385
|
+
|
|
386
|
+
await applyGraphToEntity(session, table, root as AnyEntity, payload, options);
|
|
387
|
+
|
|
388
|
+
return root as unknown as InstanceType<TCtor>;
|
|
389
|
+
|
|
390
|
+
};
|
|
@@ -127,10 +127,11 @@ export class DeleteQueryBuilder<T> {
|
|
|
127
127
|
* @param session - The ORM session to execute the query with
|
|
128
128
|
* @returns A promise that resolves to the query results
|
|
129
129
|
*/
|
|
130
|
-
async execute(session: OrmSession): Promise<QueryResult[]> {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
async execute(session: OrmSession): Promise<QueryResult[]> {
|
|
131
|
+
const execCtx = session.getExecutionContext();
|
|
132
|
+
const compiled = this.compile(execCtx.dialect);
|
|
133
|
+
return execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
134
|
+
}
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
137
|
* Returns the Abstract Syntax Tree (AST) representation of the query
|