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.
- package/README.md +15 -14
- package/dist/index.cjs +188 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -2
- package/dist/index.d.ts +58 -2
- package/dist/index.js +187 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-entities/render.mjs +115 -12
- package/scripts/inflection/compound.mjs +72 -0
- package/scripts/inflection/en.mjs +26 -0
- package/scripts/inflection/index.mjs +29 -0
- package/scripts/inflection/pt-br.mjs +391 -0
- package/scripts/naming-strategy.mjs +27 -63
- package/scripts/pt-pluralizer.mjs +19 -0
- package/src/core/ddl/introspect/mssql.ts +74 -2
- package/src/core/ddl/introspect/postgres.ts +69 -39
- package/src/core/ddl/introspect/sqlite.ts +69 -5
- package/src/index.ts +4 -2
- package/src/orm/jsonify.ts +27 -0
- package/src/orm/orm-session.ts +28 -22
- package/src/orm/save-graph-types.ts +57 -0
- package/src/orm/save-graph.ts +141 -105
- package/src/schema/column-types.ts +14 -9
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
|
+
};
|
|
@@ -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
|
|
144
|
-
*/
|
|
145
|
-
|
|
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]
|