metal-orm 1.0.50 → 1.0.52

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.
@@ -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
- RelationKinds,
10
- type BelongsToManyRelation,
11
- type BelongsToRelation,
12
- type HasManyRelation,
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 pickColumns = (table: TableDef, payload: AnyEntity): Record<string, unknown> => {
50
- const columns: Record<string, unknown> = {};
51
- for (const key of Object.keys(table.columns)) {
52
- if (payload[key] !== undefined) {
53
- columns[key] = payload[key];
54
- }
55
- }
56
- return columns;
57
- };
58
-
59
- const ensureEntity = <TTable extends TableDef>(
60
- session: OrmSession,
61
- table: TTable,
62
- payload: AnyEntity
63
- ): EntityInstance<TTable> => {
64
- const pk = findPrimaryKey(table);
65
- const row = pickColumns(table, payload);
66
- const pkValue = payload[pk];
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
+ };
@@ -134,15 +134,20 @@ export const col = {
134
134
 
135
135
  /**
136
136
  * Creates a variable character column definition
137
- * @param length - Maximum length of the string
138
- * @returns ColumnDef with VARCHAR type
139
- */
140
- varchar: (length: number): ColumnDef<'VARCHAR'> => ({ name: '', type: 'VARCHAR', args: [length] }),
141
-
142
- /**
143
- * Creates a fixed precision decimal column definition
144
- */
145
- decimal: (precision: number, scale = 0): ColumnDef<'DECIMAL'> => ({
137
+ * @param length - Maximum length of the string
138
+ * @returns ColumnDef with VARCHAR type
139
+ */
140
+ varchar: (length: number): ColumnDef<'VARCHAR'> => ({ name: '', type: 'VARCHAR', args: [length] }),
141
+
142
+ /**
143
+ * Creates a text column definition
144
+ */
145
+ text: (): ColumnDef<'TEXT'> => ({ name: '', type: 'TEXT' }),
146
+
147
+ /**
148
+ * Creates a fixed precision decimal column definition
149
+ */
150
+ decimal: (precision: number, scale = 0): ColumnDef<'DECIMAL'> => ({
146
151
  name: '',
147
152
  type: 'DECIMAL',
148
153
  args: [precision, scale]