metal-orm 1.0.65 → 1.0.67
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/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -70
- package/dist/index.d.ts +70 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/ast/builders.ts +38 -38
- package/src/core/ast/expression-builders.ts +10 -6
- package/src/core/ast/expression-nodes.ts +5 -5
- package/src/core/ast/join.ts +16 -16
- package/src/core/ast/query.ts +219 -219
- package/src/core/dialect/abstract.ts +20 -20
- package/src/core/functions/array.ts +35 -35
- package/src/core/functions/json.ts +70 -70
- package/src/orm/entity-hydration.ts +72 -72
- package/src/orm/entity-materializer.ts +41 -41
- package/src/orm/entity-meta.ts +18 -18
- package/src/orm/entity-relation-cache.ts +39 -39
- package/src/orm/entity.ts +3 -3
- package/src/orm/identity-map.ts +3 -2
- package/src/orm/relations/belongs-to.ts +2 -2
- package/src/orm/relations/has-many.ts +24 -24
- package/src/orm/relations/has-one.ts +2 -2
- package/src/orm/relations/many-to-many.ts +29 -29
- package/src/orm/save-graph-types.ts +51 -50
- package/src/orm/save-graph.ts +54 -39
- package/src/orm/unit-of-work.ts +59 -58
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import { TableDef } from '../schema/table.js';
|
|
2
|
-
import { EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Caches relation loader results across entities of the same type.
|
|
6
|
-
* @template T - The cache type
|
|
7
|
-
* @param meta - The entity metadata
|
|
8
|
-
* @param relationName - The relation name
|
|
9
|
-
* @param factory - The factory function to create the cache
|
|
10
|
-
* @returns Promise with the cached relation data
|
|
11
|
-
*/
|
|
12
|
-
export const relationLoaderCache = <TTable extends TableDef, T extends Map<string, unknown>>(
|
|
13
|
-
meta: EntityMeta<TTable>,
|
|
14
|
-
relationName: string,
|
|
15
|
-
factory: () => Promise<T>
|
|
16
|
-
): Promise<T> => {
|
|
17
|
-
if (meta.relationCache.has(relationName)) {
|
|
18
|
-
return meta.relationCache.get(relationName)! as Promise<T>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const promise = factory().then(value => {
|
|
22
|
-
for (const tracked of meta.ctx.getEntitiesForTable(meta.table)) {
|
|
23
|
-
const otherMeta = getEntityMeta(tracked.entity);
|
|
24
|
-
if (!otherMeta) continue;
|
|
25
|
-
otherMeta.relationHydration.set(relationName, value);
|
|
26
|
-
}
|
|
27
|
-
return value;
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
meta.relationCache.set(relationName, promise);
|
|
31
|
-
|
|
32
|
-
for (const tracked of meta.ctx.getEntitiesForTable(meta.table)) {
|
|
33
|
-
const otherMeta = getEntityMeta(tracked.entity);
|
|
34
|
-
if (!otherMeta) continue;
|
|
35
|
-
otherMeta.relationCache.set(relationName, promise);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return promise;
|
|
39
|
-
};
|
|
1
|
+
import { TableDef } from '../schema/table.js';
|
|
2
|
+
import { EntityMeta, getEntityMeta } from './entity-meta.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Caches relation loader results across entities of the same type.
|
|
6
|
+
* @template T - The cache type
|
|
7
|
+
* @param meta - The entity metadata
|
|
8
|
+
* @param relationName - The relation name
|
|
9
|
+
* @param factory - The factory function to create the cache
|
|
10
|
+
* @returns Promise with the cached relation data
|
|
11
|
+
*/
|
|
12
|
+
export const relationLoaderCache = <TTable extends TableDef, T extends Map<string, unknown>>(
|
|
13
|
+
meta: EntityMeta<TTable>,
|
|
14
|
+
relationName: string,
|
|
15
|
+
factory: () => Promise<T>
|
|
16
|
+
): Promise<T> => {
|
|
17
|
+
if (meta.relationCache.has(relationName)) {
|
|
18
|
+
return meta.relationCache.get(relationName)! as Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const promise = factory().then(value => {
|
|
22
|
+
for (const tracked of meta.ctx.getEntitiesForTable(meta.table)) {
|
|
23
|
+
const otherMeta = getEntityMeta(tracked.entity);
|
|
24
|
+
if (!otherMeta) continue;
|
|
25
|
+
otherMeta.relationHydration.set(relationName, value);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
meta.relationCache.set(relationName, promise);
|
|
31
|
+
|
|
32
|
+
for (const tracked of meta.ctx.getEntitiesForTable(meta.table)) {
|
|
33
|
+
const otherMeta = getEntityMeta(tracked.entity);
|
|
34
|
+
if (!otherMeta) continue;
|
|
35
|
+
otherMeta.relationCache.set(relationName, promise);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return promise;
|
|
39
|
+
};
|
package/src/orm/entity.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
2
|
import { EntityInstance } from '../schema/types.js';
|
|
3
|
-
import type { EntityContext, PrimaryKey } from './entity-context.js';
|
|
3
|
+
import type { EntityContext, PrimaryKey } from './entity-context.js';
|
|
4
4
|
import { ENTITY_META, EntityMeta, RelationKey } from './entity-meta.js';
|
|
5
5
|
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
6
6
|
import { RelationIncludeOptions } from '../query-builder/relation-types.js';
|
|
@@ -109,13 +109,13 @@ export const createEntityFromRow = <
|
|
|
109
109
|
const pkName = findPrimaryKey(table);
|
|
110
110
|
const pkValue = row[pkName];
|
|
111
111
|
if (pkValue !== undefined && pkValue !== null) {
|
|
112
|
-
const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
|
|
112
|
+
const tracked = ctx.getEntity(table, pkValue as PrimaryKey);
|
|
113
113
|
if (tracked) return tracked as TResult;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
const entity = createEntityProxy(ctx, table, row, lazyRelations, lazyRelationOptions);
|
|
117
117
|
if (pkValue !== undefined && pkValue !== null) {
|
|
118
|
-
ctx.trackManaged(table, pkValue as PrimaryKey, entity);
|
|
118
|
+
ctx.trackManaged(table, pkValue as PrimaryKey, entity);
|
|
119
119
|
} else {
|
|
120
120
|
ctx.trackNew(table, entity);
|
|
121
121
|
}
|
package/src/orm/identity-map.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TableDef } from '../schema/table.js';
|
|
2
2
|
import type { TrackedEntity } from './runtime-types.js';
|
|
3
|
+
import type { PrimaryKey } from './entity-context.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Simple identity map for tracking entities within a session.
|
|
@@ -18,7 +19,7 @@ export class IdentityMap {
|
|
|
18
19
|
* @param pk The primary key value.
|
|
19
20
|
* @returns The entity instance if found, undefined otherwise.
|
|
20
21
|
*/
|
|
21
|
-
getEntity(table: TableDef, pk:
|
|
22
|
+
getEntity(table: TableDef, pk: PrimaryKey): object | undefined {
|
|
22
23
|
const bucket = this.buckets.get(table.name);
|
|
23
24
|
return bucket?.get(this.toIdentityKey(pk))?.entity;
|
|
24
25
|
}
|
|
@@ -54,7 +55,7 @@ export class IdentityMap {
|
|
|
54
55
|
this.buckets.clear();
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
private toIdentityKey(pk:
|
|
58
|
+
private toIdentityKey(pk: PrimaryKey): string {
|
|
58
59
|
return String(pk);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BelongsToReferenceApi } from '../../schema/types.js';
|
|
1
|
+
import { BelongsToReferenceApi } from '../../schema/types.js';
|
|
2
2
|
import { EntityContext } from '../entity-context.js';
|
|
3
3
|
import { RelationKey } from '../runtime-types.js';
|
|
4
4
|
import { BelongsToRelation } from '../../schema/relation.js';
|
|
@@ -26,7 +26,7 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
26
26
|
*
|
|
27
27
|
* @template TParent The type of the parent entity.
|
|
28
28
|
*/
|
|
29
|
-
export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
|
|
29
|
+
export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
|
|
30
30
|
private loaded = false;
|
|
31
31
|
private current: TParent | null = null;
|
|
32
32
|
|
|
@@ -69,29 +69,29 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
69
69
|
this.items = rows.map(row => this.createEntity(row));
|
|
70
70
|
this.loaded = true;
|
|
71
71
|
return this.items;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Gets the current items in the collection.
|
|
76
|
-
* @returns Array of child entities
|
|
77
|
-
*/
|
|
78
|
-
getItems(): TChild[] {
|
|
79
|
-
return this.items;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Array-compatible length for testing frameworks.
|
|
84
|
-
*/
|
|
85
|
-
get length(): number {
|
|
86
|
-
return this.items.length;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Enables iteration over the collection like an array.
|
|
91
|
-
*/
|
|
92
|
-
[Symbol.iterator](): Iterator<TChild> {
|
|
93
|
-
return this.items[Symbol.iterator]();
|
|
94
|
-
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the current items in the collection.
|
|
76
|
+
* @returns Array of child entities
|
|
77
|
+
*/
|
|
78
|
+
getItems(): TChild[] {
|
|
79
|
+
return this.items;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Array-compatible length for testing frameworks.
|
|
84
|
+
*/
|
|
85
|
+
get length(): number {
|
|
86
|
+
return this.items.length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Enables iteration over the collection like an array.
|
|
91
|
+
*/
|
|
92
|
+
[Symbol.iterator](): Iterator<TChild> {
|
|
93
|
+
return this.items[Symbol.iterator]();
|
|
94
|
+
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
97
|
* Adds a new child entity to the collection.
|
|
@@ -125,7 +125,7 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
125
125
|
attach(entity: TChild): void {
|
|
126
126
|
const keyValue = this.root[this.localKey];
|
|
127
127
|
(entity as Record<string, unknown>)[this.relation.foreignKey] = keyValue;
|
|
128
|
-
this.ctx.markDirty(entity as object);
|
|
128
|
+
this.ctx.markDirty(entity as object);
|
|
129
129
|
this.items.push(entity);
|
|
130
130
|
this.ctx.registerRelationChange(
|
|
131
131
|
this.root,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HasOneReferenceApi } from '../../schema/types.js';
|
|
1
|
+
import { HasOneReferenceApi } from '../../schema/types.js';
|
|
2
2
|
import { EntityContext } from '../entity-context.js';
|
|
3
3
|
import { RelationKey } from '../runtime-types.js';
|
|
4
4
|
import { HasOneRelation } from '../../schema/relation.js';
|
|
@@ -26,7 +26,7 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
26
26
|
*
|
|
27
27
|
* @template TChild The type of the child entity.
|
|
28
28
|
*/
|
|
29
|
-
export class DefaultHasOneReference<TChild extends object> implements HasOneReferenceApi<TChild> {
|
|
29
|
+
export class DefaultHasOneReference<TChild extends object> implements HasOneReferenceApi<TChild> {
|
|
30
30
|
private loaded = false;
|
|
31
31
|
private current: TChild | null = null;
|
|
32
32
|
|
|
@@ -28,8 +28,8 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
28
28
|
*
|
|
29
29
|
* @template TTarget The type of the target entities in the collection.
|
|
30
30
|
*/
|
|
31
|
-
export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefined = undefined>
|
|
32
|
-
implements ManyToManyCollection<TTarget, TPivot> {
|
|
31
|
+
export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefined = undefined>
|
|
32
|
+
implements ManyToManyCollection<TTarget, TPivot> {
|
|
33
33
|
private loaded = false;
|
|
34
34
|
private items: TTarget[] = [];
|
|
35
35
|
|
|
@@ -79,33 +79,33 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
79
79
|
return this.items;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
* Returns the currently loaded items.
|
|
84
|
-
* @returns Array of target entities.
|
|
85
|
-
*/
|
|
86
|
-
getItems(): TTarget[] {
|
|
87
|
-
return this.items;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Array-compatible length for testing frameworks.
|
|
92
|
-
*/
|
|
93
|
-
get length(): number {
|
|
94
|
-
return this.items.length;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Enables iteration over the collection like an array.
|
|
99
|
-
*/
|
|
100
|
-
[Symbol.iterator](): Iterator<TTarget> {
|
|
101
|
-
return this.items[Symbol.iterator]();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Attaches an entity to the collection.
|
|
106
|
-
* Registers an 'attach' change in the entity context.
|
|
107
|
-
* @param target Entity instance or its primary key value.
|
|
108
|
-
*/
|
|
82
|
+
/**
|
|
83
|
+
* Returns the currently loaded items.
|
|
84
|
+
* @returns Array of target entities.
|
|
85
|
+
*/
|
|
86
|
+
getItems(): TTarget[] {
|
|
87
|
+
return this.items;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Array-compatible length for testing frameworks.
|
|
92
|
+
*/
|
|
93
|
+
get length(): number {
|
|
94
|
+
return this.items.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Enables iteration over the collection like an array.
|
|
99
|
+
*/
|
|
100
|
+
[Symbol.iterator](): Iterator<TTarget> {
|
|
101
|
+
return this.items[Symbol.iterator]();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Attaches an entity to the collection.
|
|
106
|
+
* Registers an 'attach' change in the entity context.
|
|
107
|
+
* @param target Entity instance or its primary key value.
|
|
108
|
+
*/
|
|
109
109
|
attach(target: TTarget | number | string): void {
|
|
110
110
|
const entity = this.ensureEntity(target);
|
|
111
111
|
const id = this.extractId(entity);
|
|
@@ -1,55 +1,56 @@
|
|
|
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
|
|
14
|
-
| BelongsToReference
|
|
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
|
-
|
|
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
|
|
14
|
+
| BelongsToReference
|
|
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
29
|
/**
|
|
30
30
|
* Input scalar type for `OrmSession.saveGraph` payloads.
|
|
31
31
|
*
|
|
32
32
|
* Note: runtime coercion is opt-in via `SaveGraphOptions.coerce`.
|
|
33
|
+
* For Date columns, string input is only safe when using `coerce: 'json-in'`.
|
|
33
34
|
*/
|
|
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>;
|
|
35
|
+
export type SaveGraphInputScalar<T> = T extends Date ? T | string | number : T;
|
|
36
|
+
|
|
37
|
+
type ColumnInput<TEntity> = {
|
|
38
|
+
[K in ColumnKeys<TEntity>]?: SaveGraphInputScalar<TEntity[K]>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type RelationInputValue<T> =
|
|
42
|
+
T extends HasManyCollection<infer C> ? Array<SaveGraphInputPayload<C> | AnyId> :
|
|
43
|
+
T extends HasOneReference<infer C> ? SaveGraphInputPayload<C> | AnyId | null :
|
|
44
|
+
T extends BelongsToReference<infer P> ? SaveGraphInputPayload<P> | AnyId | null :
|
|
45
|
+
T extends ManyToManyCollection<infer Tgt> ? Array<SaveGraphInputPayload<Tgt> | AnyId> :
|
|
46
|
+
never;
|
|
47
|
+
|
|
48
|
+
type RelationInput<TEntity> = {
|
|
49
|
+
[K in RelationKeys<TEntity>]?: RelationInputValue<NonNullable<TEntity[K]>>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Typed payload accepted by `OrmSession.saveGraph`:
|
|
54
|
+
* - Only entity scalar keys + relation keys are accepted.
|
|
55
|
+
*/
|
|
56
|
+
export type SaveGraphInputPayload<TEntity> = ColumnInput<TEntity> & RelationInput<TEntity>;
|
package/src/orm/save-graph.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { createEntityFromRow } from './entity.js';
|
|
|
20
20
|
import type { EntityConstructor } from './entity-metadata.js';
|
|
21
21
|
import { getTableDefFromEntity } from '../decorators/bootstrap.js';
|
|
22
22
|
import type { OrmSession } from './orm-session.js';
|
|
23
|
+
import type { PrimaryKey } from './entity-context.js';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Options for controlling the behavior of save graph operations.
|
|
@@ -27,18 +28,16 @@ import type { OrmSession } from './orm-session.js';
|
|
|
27
28
|
export interface SaveGraphOptions {
|
|
28
29
|
/** Remove existing collection members that are not present in the payload */
|
|
29
30
|
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
|
-
|
|
36
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Coerce JSON-friendly input values into DB-friendly primitives.
|
|
33
|
+
* Currently:
|
|
34
|
+
* - `json`: Date -> ISO string (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
|
|
35
|
+
* - `json-in`: string/number -> Date (for DATE/DATETIME/TIMESTAMP/TIMESTAMPTZ columns)
|
|
36
|
+
*/
|
|
37
|
+
coerce?: 'json' | 'json-in';
|
|
38
|
+
}
|
|
37
39
|
|
|
38
40
|
/** Represents an entity object with arbitrary properties. */
|
|
39
|
-
|
|
40
|
-
/** Represents an entity object with arbitrary properties. */
|
|
41
|
-
|
|
42
41
|
type AnyEntity = Record<string, unknown>;
|
|
43
42
|
|
|
44
43
|
/**
|
|
@@ -53,33 +52,49 @@ type AnyEntity = Record<string, unknown>;
|
|
|
53
52
|
|
|
54
53
|
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
55
54
|
|
|
56
|
-
const coerceColumnValue = (
|
|
57
|
-
table: TableDef,
|
|
58
|
-
columnName: string,
|
|
59
|
-
value: unknown,
|
|
60
|
-
options: SaveGraphOptions
|
|
61
|
-
): unknown => {
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (!column) return value;
|
|
55
|
+
const coerceColumnValue = (
|
|
56
|
+
table: TableDef,
|
|
57
|
+
columnName: string,
|
|
58
|
+
value: unknown,
|
|
59
|
+
options: SaveGraphOptions
|
|
60
|
+
): unknown => {
|
|
61
|
+
if (value === null || value === undefined) return value;
|
|
62
|
+
|
|
63
|
+
const column = table.columns[columnName] as unknown as ColumnDef | undefined;
|
|
64
|
+
if (!column) return value;
|
|
67
65
|
|
|
68
66
|
const normalized = normalizeColumnType(column.type);
|
|
69
67
|
|
|
70
|
-
const isDateLikeColumn =
|
|
71
|
-
normalized === 'date' ||
|
|
72
|
-
normalized === 'datetime' ||
|
|
73
|
-
normalized === 'timestamp' ||
|
|
74
|
-
normalized === 'timestamptz';
|
|
75
|
-
|
|
76
|
-
if (isDateLikeColumn
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
const isDateLikeColumn =
|
|
69
|
+
normalized === 'date' ||
|
|
70
|
+
normalized === 'datetime' ||
|
|
71
|
+
normalized === 'timestamp' ||
|
|
72
|
+
normalized === 'timestamptz';
|
|
73
|
+
|
|
74
|
+
if (!isDateLikeColumn) return value;
|
|
75
|
+
|
|
76
|
+
if (options.coerce === 'json') {
|
|
77
|
+
if (value instanceof Date) {
|
|
78
|
+
return value.toISOString();
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (options.coerce === 'json-in') {
|
|
84
|
+
if (value instanceof Date) return value;
|
|
85
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
86
|
+
const date = new Date(value);
|
|
87
|
+
if (Number.isNaN(date.getTime())) {
|
|
88
|
+
throw new Error(`Invalid date value for column "${columnName}"`);
|
|
89
|
+
}
|
|
90
|
+
return date;
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Future coercions can be added here based on `normalized`.
|
|
96
|
+
return value;
|
|
97
|
+
};
|
|
83
98
|
|
|
84
99
|
const pickColumns = (table: TableDef, payload: AnyEntity, options: SaveGraphOptions): Record<string, unknown> => {
|
|
85
100
|
const columns: Record<string, unknown> = {};
|
|
@@ -102,7 +117,7 @@ const ensureEntity = <TTable extends TableDef>(
|
|
|
102
117
|
const pkValue = payload[pk];
|
|
103
118
|
|
|
104
119
|
if (pkValue !== undefined && pkValue !== null) {
|
|
105
|
-
const tracked = session.getEntity(table, pkValue as
|
|
120
|
+
const tracked = session.getEntity(table, pkValue as PrimaryKey);
|
|
106
121
|
if (tracked) {
|
|
107
122
|
return tracked as EntityInstance<TTable>;
|
|
108
123
|
}
|
|
@@ -159,7 +174,7 @@ const handleHasMany = async (
|
|
|
159
174
|
|
|
160
175
|
const current =
|
|
161
176
|
findInCollectionByPk(existing, targetPk, pkValue) ??
|
|
162
|
-
(pkValue !== undefined && pkValue !== null ? session.getEntity(targetTable, pkValue as
|
|
177
|
+
(pkValue !== undefined && pkValue !== null ? session.getEntity(targetTable, pkValue as PrimaryKey) : undefined);
|
|
163
178
|
|
|
164
179
|
const entity = current ?? ensureEntity(session, targetTable, asObj, options);
|
|
165
180
|
assignColumns(targetTable, entity as AnyEntity, asObj, options);
|
|
@@ -202,7 +217,7 @@ const handleHasOne = async (
|
|
|
202
217
|
if (typeof payload === 'number' || typeof payload === 'string') {
|
|
203
218
|
const entity = ref.set({ [pk]: payload });
|
|
204
219
|
if (entity) {
|
|
205
|
-
await applyGraphToEntity(session, relation.target, entity as AnyEntity, { [pk]: payload }, options);
|
|
220
|
+
await applyGraphToEntity(session, relation.target, entity as AnyEntity, { [pk]: payload as PrimaryKey }, options);
|
|
206
221
|
}
|
|
207
222
|
return;
|
|
208
223
|
}
|
|
@@ -230,7 +245,7 @@ const handleBelongsTo = async (
|
|
|
230
245
|
if (typeof payload === 'number' || typeof payload === 'string') {
|
|
231
246
|
const entity = ref.set({ [pk]: payload });
|
|
232
247
|
if (entity) {
|
|
233
|
-
await applyGraphToEntity(session, relation.target, entity as AnyEntity, { [pk]: payload }, options);
|
|
248
|
+
await applyGraphToEntity(session, relation.target, entity as AnyEntity, { [pk]: payload as PrimaryKey }, options);
|
|
234
249
|
}
|
|
235
250
|
return;
|
|
236
251
|
}
|
|
@@ -268,7 +283,7 @@ const handleBelongsToMany = async (
|
|
|
268
283
|
const asObj = item as AnyEntity;
|
|
269
284
|
const pkValue = asObj[targetPk];
|
|
270
285
|
const entity = pkValue !== undefined && pkValue !== null
|
|
271
|
-
? session.getEntity(targetTable, pkValue as
|
|
286
|
+
? session.getEntity(targetTable, pkValue as PrimaryKey) ?? ensureEntity(session, targetTable, asObj, options)
|
|
272
287
|
: ensureEntity(session, targetTable, asObj, options);
|
|
273
288
|
|
|
274
289
|
assignColumns(targetTable, entity as AnyEntity, asObj, options);
|