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
|
@@ -54,12 +54,18 @@ type ForeignKeyEntry = {
|
|
|
54
54
|
onUpdate?: ReferentialAction;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
type ColumnCommentRow = {
|
|
58
|
-
table_schema: string;
|
|
59
|
-
table_name: string;
|
|
60
|
-
column_name: string;
|
|
61
|
-
description: string | null;
|
|
62
|
-
};
|
|
57
|
+
type ColumnCommentRow = {
|
|
58
|
+
table_schema: string;
|
|
59
|
+
table_name: string;
|
|
60
|
+
column_name: string;
|
|
61
|
+
description: string | null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type TableCommentRow = {
|
|
65
|
+
table_schema: string;
|
|
66
|
+
table_name: string;
|
|
67
|
+
description: string | null;
|
|
68
|
+
};
|
|
63
69
|
|
|
64
70
|
/** Row type for PostgreSQL index query results from pg_catalog tables. */
|
|
65
71
|
type IndexQueryRow = {
|
|
@@ -111,32 +117,55 @@ export const postgresIntrospector: SchemaIntrospector = {
|
|
|
111
117
|
.orderBy(PgInformationSchemaColumns.columns.ordinal_position);
|
|
112
118
|
|
|
113
119
|
const columnRows = await runSelect<ColumnIntrospectRow>(qbColumns, ctx);
|
|
114
|
-
const columnCommentRows = (await queryRows(
|
|
115
|
-
ctx.executor,
|
|
116
|
-
`
|
|
117
|
-
SELECT
|
|
118
|
-
ns.nspname AS table_schema,
|
|
119
|
-
cls.relname AS table_name,
|
|
120
|
-
att.attname AS column_name,
|
|
121
|
-
pg_catalog.col_description(cls.oid, att.attnum) AS description
|
|
122
|
-
FROM pg_catalog.pg_attribute att
|
|
123
|
-
JOIN pg_catalog.pg_class cls ON cls.oid = att.attrelid
|
|
124
|
-
JOIN pg_catalog.pg_namespace ns ON ns.oid = cls.relnamespace
|
|
125
|
-
WHERE ns.nspname = $1
|
|
126
|
-
AND att.attnum > 0
|
|
127
|
-
AND NOT att.attisdropped
|
|
128
|
-
`,
|
|
129
|
-
[schema]
|
|
130
|
-
)) as ColumnCommentRow[];
|
|
131
|
-
const columnComments = new Map<string, string>();
|
|
132
|
-
columnCommentRows.forEach(r => {
|
|
133
|
-
if (!shouldIncludeTable(r.table_name, options)) return;
|
|
134
|
-
if (!r.description) return;
|
|
135
|
-
const key = `${r.table_schema}.${r.table_name}.${r.column_name}`;
|
|
136
|
-
const trimmed = r.description.trim();
|
|
137
|
-
if (!trimmed) return;
|
|
138
|
-
columnComments.set(key, trimmed);
|
|
139
|
-
});
|
|
120
|
+
const columnCommentRows = (await queryRows(
|
|
121
|
+
ctx.executor,
|
|
122
|
+
`
|
|
123
|
+
SELECT
|
|
124
|
+
ns.nspname AS table_schema,
|
|
125
|
+
cls.relname AS table_name,
|
|
126
|
+
att.attname AS column_name,
|
|
127
|
+
pg_catalog.col_description(cls.oid, att.attnum) AS description
|
|
128
|
+
FROM pg_catalog.pg_attribute att
|
|
129
|
+
JOIN pg_catalog.pg_class cls ON cls.oid = att.attrelid
|
|
130
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = cls.relnamespace
|
|
131
|
+
WHERE ns.nspname = $1
|
|
132
|
+
AND att.attnum > 0
|
|
133
|
+
AND NOT att.attisdropped
|
|
134
|
+
`,
|
|
135
|
+
[schema]
|
|
136
|
+
)) as ColumnCommentRow[];
|
|
137
|
+
const columnComments = new Map<string, string>();
|
|
138
|
+
columnCommentRows.forEach(r => {
|
|
139
|
+
if (!shouldIncludeTable(r.table_name, options)) return;
|
|
140
|
+
if (!r.description) return;
|
|
141
|
+
const key = `${r.table_schema}.${r.table_name}.${r.column_name}`;
|
|
142
|
+
const trimmed = r.description.trim();
|
|
143
|
+
if (!trimmed) return;
|
|
144
|
+
columnComments.set(key, trimmed);
|
|
145
|
+
});
|
|
146
|
+
const tableCommentRows = (await queryRows(
|
|
147
|
+
ctx.executor,
|
|
148
|
+
`
|
|
149
|
+
SELECT
|
|
150
|
+
ns.nspname AS table_schema,
|
|
151
|
+
cls.relname AS table_name,
|
|
152
|
+
pg_catalog.obj_description(cls.oid) AS description
|
|
153
|
+
FROM pg_catalog.pg_class cls
|
|
154
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = cls.relnamespace
|
|
155
|
+
WHERE ns.nspname = $1
|
|
156
|
+
AND cls.relkind IN ('r', 'p')
|
|
157
|
+
`,
|
|
158
|
+
[schema]
|
|
159
|
+
)) as TableCommentRow[];
|
|
160
|
+
const tableComments = new Map<string, string>();
|
|
161
|
+
tableCommentRows.forEach(r => {
|
|
162
|
+
if (!shouldIncludeTable(r.table_name, options)) return;
|
|
163
|
+
if (!r.description) return;
|
|
164
|
+
const key = `${r.table_schema}.${r.table_name}`;
|
|
165
|
+
const trimmed = r.description.trim();
|
|
166
|
+
if (!trimmed) return;
|
|
167
|
+
tableComments.set(key, trimmed);
|
|
168
|
+
});
|
|
140
169
|
|
|
141
170
|
// Primary key columns query
|
|
142
171
|
const qbPk = new SelectQueryBuilder(PgKeyColumnUsage)
|
|
@@ -303,13 +332,14 @@ export const postgresIntrospector: SchemaIntrospector = {
|
|
|
303
332
|
return;
|
|
304
333
|
}
|
|
305
334
|
if (!tablesByKey.has(key)) {
|
|
306
|
-
tablesByKey.set(key, {
|
|
307
|
-
name: r.table_name,
|
|
308
|
-
schema: r.table_schema,
|
|
309
|
-
columns: [],
|
|
310
|
-
primaryKey: pkMap.get(key) || [],
|
|
311
|
-
indexes: []
|
|
312
|
-
|
|
335
|
+
tablesByKey.set(key, {
|
|
336
|
+
name: r.table_name,
|
|
337
|
+
schema: r.table_schema,
|
|
338
|
+
columns: [],
|
|
339
|
+
primaryKey: pkMap.get(key) || [],
|
|
340
|
+
indexes: [],
|
|
341
|
+
comment: tableComments.get(key)
|
|
342
|
+
});
|
|
313
343
|
}
|
|
314
344
|
const cols = tablesByKey.get(key)!;
|
|
315
345
|
const commentKey = `${r.table_schema}.${r.table_name}.${r.column_name}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SchemaIntrospector, IntrospectOptions } from './types.js';
|
|
2
|
-
import { shouldIncludeTable } from './utils.js';
|
|
3
|
-
import { DatabaseSchema, DatabaseTable, DatabaseIndex } from '../schema-types.js';
|
|
2
|
+
import { shouldIncludeTable, queryRows } from './utils.js';
|
|
3
|
+
import { DatabaseSchema, DatabaseTable, DatabaseIndex, DatabaseColumn } from '../schema-types.js';
|
|
4
4
|
import type { IntrospectContext } from './context.js';
|
|
5
5
|
import { runSelectNode } from './run-select.js';
|
|
6
6
|
import type { SelectQueryNode, TableNode } from '../../ast/query.js';
|
|
@@ -89,6 +89,58 @@ const runPragma = async <T>(
|
|
|
89
89
|
return (await runSelectNode<T>(query, ctx)) as T[];
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
const loadSqliteSchemaComments = async (ctx: IntrospectContext) => {
|
|
93
|
+
const tableComments = new Map<string, string>();
|
|
94
|
+
const columnComments = new Map<string, string>();
|
|
95
|
+
const tableExists = await queryRows(
|
|
96
|
+
ctx.executor,
|
|
97
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name='schema_comments' LIMIT 1`
|
|
98
|
+
);
|
|
99
|
+
if (!tableExists.length) {
|
|
100
|
+
return { tableComments, columnComments };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const commentRows = await queryRows(
|
|
104
|
+
ctx.executor,
|
|
105
|
+
`SELECT object_type, schema_name, table_name, column_name, comment FROM schema_comments`
|
|
106
|
+
);
|
|
107
|
+
for (const row of commentRows) {
|
|
108
|
+
const objectType = typeof row.object_type === 'string' ? row.object_type.toLowerCase() : '';
|
|
109
|
+
const tableName = typeof row.table_name === 'string' ? row.table_name : '';
|
|
110
|
+
if (!tableName) continue;
|
|
111
|
+
const columnName = typeof row.column_name === 'string' ? row.column_name : '';
|
|
112
|
+
const schemaName = typeof row.schema_name === 'string' ? row.schema_name : '';
|
|
113
|
+
const rawComment = row.comment;
|
|
114
|
+
if (rawComment == null) continue;
|
|
115
|
+
const commentText = String(rawComment).trim();
|
|
116
|
+
if (!commentText) continue;
|
|
117
|
+
|
|
118
|
+
const addTableComment = () => {
|
|
119
|
+
tableComments.set(tableName, commentText);
|
|
120
|
+
if (schemaName) {
|
|
121
|
+
tableComments.set(`${schemaName}.${tableName}`, commentText);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const addColumnComment = () => {
|
|
125
|
+
columnComments.set(`${tableName}.${columnName}`, commentText);
|
|
126
|
+
if (schemaName) {
|
|
127
|
+
columnComments.set(`${schemaName}.${tableName}.${columnName}`, commentText);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (objectType === 'table') {
|
|
132
|
+
addTableComment();
|
|
133
|
+
} else if (objectType === 'column' && columnName) {
|
|
134
|
+
addColumnComment();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
tableComments,
|
|
140
|
+
columnComments
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
92
144
|
export const sqliteIntrospector: SchemaIntrospector = {
|
|
93
145
|
async introspect(ctx: IntrospectContext, options: IntrospectOptions): Promise<DatabaseSchema> {
|
|
94
146
|
const alias = 'sqlite_master';
|
|
@@ -103,6 +155,7 @@ export const sqliteIntrospector: SchemaIntrospector = {
|
|
|
103
155
|
)
|
|
104
156
|
};
|
|
105
157
|
|
|
158
|
+
const { tableComments, columnComments } = await loadSqliteSchemaComments(ctx);
|
|
106
159
|
const tableRows = (await runSelectNode<SqliteTableRow>(tablesQuery, ctx)) as SqliteTableRow[];
|
|
107
160
|
const tables: DatabaseTable[] = [];
|
|
108
161
|
|
|
@@ -134,16 +187,27 @@ export const sqliteIntrospector: SchemaIntrospector = {
|
|
|
134
187
|
ctx
|
|
135
188
|
);
|
|
136
189
|
|
|
137
|
-
const tableEntry: DatabaseTable = {
|
|
190
|
+
const tableEntry: DatabaseTable = {
|
|
191
|
+
name: tableName,
|
|
192
|
+
columns: [],
|
|
193
|
+
primaryKey: [],
|
|
194
|
+
indexes: [],
|
|
195
|
+
comment: tableComments.get(tableName)
|
|
196
|
+
};
|
|
138
197
|
|
|
139
198
|
tableInfo.forEach(info => {
|
|
140
|
-
|
|
199
|
+
const column: DatabaseColumn = {
|
|
141
200
|
name: info.name,
|
|
142
201
|
type: info.type,
|
|
143
202
|
notNull: info.notnull === 1,
|
|
144
203
|
default: info.dflt_value ?? undefined,
|
|
145
204
|
autoIncrement: false
|
|
146
|
-
}
|
|
205
|
+
};
|
|
206
|
+
const columnComment = columnComments.get(`${tableName}.${info.name}`);
|
|
207
|
+
if (columnComment) {
|
|
208
|
+
column.comment = columnComment;
|
|
209
|
+
}
|
|
210
|
+
tableEntry.columns.push(column);
|
|
147
211
|
if (info.pk && info.pk > 0) {
|
|
148
212
|
tableEntry.primaryKey = tableEntry.primaryKey || [];
|
|
149
213
|
tableEntry.primaryKey.push(info.name);
|
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';
|
|
@@ -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,57 @@
|
|
|
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 that accepts JSON-friendly values for common runtime types.
|
|
31
|
+
* Currently:
|
|
32
|
+
* - Date fields accept `Date | string` (ISO string recommended)
|
|
33
|
+
*/
|
|
34
|
+
export type SaveGraphInputScalar<T> =
|
|
35
|
+
T extends Date ? Date | string : 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
|
+
* - Scalars can use JSON-friendly values (e.g., Date fields accept ISO strings).
|
|
56
|
+
*/
|
|
57
|
+
export type SaveGraphInputPayload<TEntity> = ColumnInput<TEntity> & RelationInput<TEntity>;
|