sonamu 0.7.4 → 0.7.6
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/api/config.d.ts +1 -4
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/sonamu.d.ts +2 -0
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +19 -47
- package/dist/bin/cli.js +6 -6
- package/dist/database/base-model.d.ts +1 -1
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +15 -4
- package/dist/database/code-generator.d.ts.map +1 -1
- package/dist/database/code-generator.js +3 -3
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +1 -1
- package/dist/database/puri-wrapper.d.ts +11 -11
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri-wrapper.js +7 -11
- package/dist/database/puri.d.ts +36 -17
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +54 -7
- package/dist/database/puri.types.d.ts +54 -17
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +2 -4
- package/dist/database/puri.types.test-d.js +129 -0
- package/dist/database/upsert-builder.d.ts +16 -10
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +10 -19
- package/dist/entity/entity-manager.d.ts +113 -22
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/entity/entity-manager.js +1 -1
- package/dist/entity/entity.d.ts +34 -0
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +110 -37
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +341 -149
- package/dist/migration/migration-set.d.ts.map +1 -1
- package/dist/migration/migration-set.js +21 -5
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +7 -1
- package/dist/migration/postgresql-schema-reader.d.ts +11 -1
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +111 -10
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +7 -4
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.js +12 -2
- package/dist/template/implementations/generated_sso.template.d.ts +3 -3
- package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
- package/dist/template/implementations/generated_sso.template.js +50 -2
- package/dist/template/implementations/model.template.d.ts.map +1 -1
- package/dist/template/implementations/model.template.js +20 -15
- package/dist/template/implementations/model_test.template.js +4 -4
- package/dist/template/implementations/service.template.d.ts.map +1 -1
- package/dist/template/implementations/service.template.js +2 -2
- package/dist/template/implementations/view_enums_dropdown.template.js +2 -2
- package/dist/template/implementations/view_enums_select.template.js +2 -2
- package/dist/template/implementations/view_form.template.d.ts.map +1 -1
- package/dist/template/implementations/view_form.template.js +12 -9
- package/dist/template/implementations/view_id_async_select.template.js +4 -4
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.js +12 -9
- package/dist/template/implementations/view_search_input.template.js +2 -2
- package/dist/template/template.js +2 -2
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +17 -2
- package/dist/testing/fixture-manager.d.ts +2 -1
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +29 -29
- package/dist/types/types.d.ts +593 -68
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +113 -9
- package/dist/vector/chunking.d.ts +25 -0
- package/dist/vector/chunking.d.ts.map +1 -0
- package/dist/vector/chunking.js +97 -0
- package/dist/vector/config.d.ts +12 -0
- package/dist/vector/config.d.ts.map +1 -0
- package/dist/vector/config.js +83 -0
- package/dist/vector/embedding.d.ts +42 -0
- package/dist/vector/embedding.d.ts.map +1 -0
- package/dist/vector/embedding.js +147 -0
- package/dist/vector/types.d.ts +105 -0
- package/dist/vector/types.d.ts.map +1 -0
- package/dist/vector/types.js +5 -0
- package/dist/vector/vector-search.d.ts +47 -0
- package/dist/vector/vector-search.d.ts.map +1 -0
- package/dist/vector/vector-search.js +176 -0
- package/package.json +9 -8
- package/src/api/config.ts +0 -4
- package/src/api/sonamu.ts +21 -36
- package/src/bin/cli.ts +5 -5
- package/src/database/base-model.ts +20 -11
- package/src/database/code-generator.ts +6 -2
- package/src/database/db.ts +1 -0
- package/src/database/puri-wrapper.ts +22 -16
- package/src/database/puri.ts +150 -27
- package/src/database/puri.types.test-d.ts +457 -0
- package/src/database/puri.types.ts +231 -33
- package/src/database/upsert-builder.ts +43 -34
- package/src/entity/entity-manager.ts +2 -2
- package/src/entity/entity.ts +134 -44
- package/src/index.ts +6 -0
- package/src/migration/code-generation.ts +377 -174
- package/src/migration/migration-set.ts +22 -3
- package/src/migration/migrator.ts +6 -0
- package/src/migration/postgresql-schema-reader.ts +121 -21
- package/src/syncer/syncer.ts +6 -3
- package/src/template/implementations/generated.template.ts +51 -9
- package/src/template/implementations/generated_sso.template.ts +71 -2
- package/src/template/implementations/model.template.ts +25 -15
- package/src/template/implementations/model_test.template.ts +3 -3
- package/src/template/implementations/service.template.ts +5 -1
- package/src/template/implementations/view_enums_dropdown.template.ts +1 -1
- package/src/template/implementations/view_enums_select.template.ts +1 -1
- package/src/template/implementations/view_form.template.ts +11 -8
- package/src/template/implementations/view_id_async_select.template.ts +3 -3
- package/src/template/implementations/view_list.template.ts +11 -8
- package/src/template/implementations/view_search_input.template.ts +1 -1
- package/src/template/template.ts +1 -1
- package/src/template/zod-converter.ts +20 -0
- package/src/testing/fixture-manager.ts +31 -30
- package/src/types/types.ts +226 -48
- package/src/vector/chunking.ts +115 -0
- package/src/vector/config.ts +68 -0
- package/src/vector/embedding.ts +193 -0
- package/src/vector/types.ts +122 -0
- package/src/vector/vector-search.ts +261 -0
- package/dist/template/implementations/view_enums_buttonset.template.d.ts +0 -17
- package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +0 -1
- package/dist/template/implementations/view_enums_buttonset.template.js +0 -31
- package/dist/template/implementations/view_list_columns.template.d.ts +0 -17
- package/dist/template/implementations/view_list_columns.template.d.ts.map +0 -1
- package/dist/template/implementations/view_list_columns.template.js +0 -49
- package/src/template/implementations/view_enums_buttonset.template.ts +0 -34
- package/src/template/implementations/view_list_columns.template.ts +0 -53
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isOneToOneRelationProp,
|
|
12
12
|
isRelationProp,
|
|
13
13
|
isStringProp,
|
|
14
|
+
isVectorProp,
|
|
14
15
|
isVirtualProp,
|
|
15
16
|
type MigrationColumn,
|
|
16
17
|
type MigrationColumnType,
|
|
@@ -63,6 +64,14 @@ export function getMigrationSetFromEntity(entity: Entity): MigrationSetAndJoinTa
|
|
|
63
64
|
scale: prop.scale,
|
|
64
65
|
numberType: isNumberProp(prop) ? (prop.numberType ?? "numeric") : "numeric",
|
|
65
66
|
}),
|
|
67
|
+
// Vector 타입의 경우 dimensions 추가
|
|
68
|
+
...(isVectorProp(prop) && {
|
|
69
|
+
dimensions: prop.dimensions,
|
|
70
|
+
}),
|
|
71
|
+
// Generated Column 정보 추가
|
|
72
|
+
...(prop.generated && {
|
|
73
|
+
generated: prop.generated,
|
|
74
|
+
}),
|
|
66
75
|
};
|
|
67
76
|
|
|
68
77
|
r.columns.push(column);
|
|
@@ -90,10 +99,16 @@ export function getMigrationSetFromEntity(entity: Entity): MigrationSetAndJoinTa
|
|
|
90
99
|
indexes: [
|
|
91
100
|
// 조인 테이블에 걸린 인덱스 찾아와서 연결
|
|
92
101
|
...entity.indexes
|
|
93
|
-
.filter((index) =>
|
|
102
|
+
.filter((index) =>
|
|
103
|
+
index.columns.find((col) => col.name.includes(`${prop.joinTable}.`)),
|
|
104
|
+
)
|
|
94
105
|
.map((index) => ({
|
|
95
106
|
...index,
|
|
96
|
-
columns: index.columns.map((col) =>
|
|
107
|
+
columns: index.columns.map((col) => ({
|
|
108
|
+
name: col.name.replace(`${prop.joinTable}.`, ""),
|
|
109
|
+
nullsFirst: col.nullsFirst,
|
|
110
|
+
sortOrder: col.sortOrder,
|
|
111
|
+
})),
|
|
97
112
|
})),
|
|
98
113
|
],
|
|
99
114
|
columns: [
|
|
@@ -163,7 +178,7 @@ export function getMigrationSetFromEntity(entity: Entity): MigrationSetAndJoinTa
|
|
|
163
178
|
|
|
164
179
|
// indexes
|
|
165
180
|
migrationSet.indexes = entity.indexes.filter((index) =>
|
|
166
|
-
index.columns.find((col) => col.includes(".") === false),
|
|
181
|
+
index.columns.find((col) => col.name.includes(".") === false),
|
|
167
182
|
);
|
|
168
183
|
|
|
169
184
|
return migrationSet;
|
|
@@ -213,6 +228,10 @@ function resolveEntityPropTypeToMigrationColumnType(prop: EntityProp): Migration
|
|
|
213
228
|
return "uuid[]";
|
|
214
229
|
case "json":
|
|
215
230
|
return "json";
|
|
231
|
+
case "vector":
|
|
232
|
+
return "vector";
|
|
233
|
+
case "vector[]":
|
|
234
|
+
return "vector[]";
|
|
216
235
|
default:
|
|
217
236
|
exhaustive(prop);
|
|
218
237
|
throw new Error(`Unknown entity prop type: ${(prop as { type: string }).type}`);
|
|
@@ -405,6 +405,12 @@ export class Migrator {
|
|
|
405
405
|
const tdb = knex(Sonamu.dbConfig.test);
|
|
406
406
|
!isTest() && console.log(chalk.magenta(`${shadowDatabase} 삭제`));
|
|
407
407
|
await tdb.raw(`DROP DATABASE IF EXISTS ${shadowDatabase}`);
|
|
408
|
+
await tdb.raw(`
|
|
409
|
+
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
|
410
|
+
FROM pg_stat_activity
|
|
411
|
+
WHERE datname = '${tdbConn.database}'
|
|
412
|
+
AND pid <> pg_backend_pid();
|
|
413
|
+
`);
|
|
408
414
|
await tdb.raw(`CREATE DATABASE ${shadowDatabase} TEMPLATE ${tdbConn.database}`);
|
|
409
415
|
|
|
410
416
|
// Shadow DB에 연결
|
|
@@ -18,6 +18,8 @@ export type PgColumn = {
|
|
|
18
18
|
numeric_scale: number | null;
|
|
19
19
|
is_nullable: string;
|
|
20
20
|
column_default: string | null;
|
|
21
|
+
is_generated: string; // 's' = STORED, 'v' = VIRTUAL, '' = none
|
|
22
|
+
generation_expression: string | null;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
type PgIndex = {
|
|
@@ -26,6 +28,9 @@ type PgIndex = {
|
|
|
26
28
|
is_unique: boolean;
|
|
27
29
|
is_primary: boolean;
|
|
28
30
|
index_type: string;
|
|
31
|
+
nulls_first: boolean;
|
|
32
|
+
sort_order: "ASC" | "DESC";
|
|
33
|
+
nulls_not_distinct: boolean;
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
type PgForeign = {
|
|
@@ -56,13 +61,40 @@ class PostgreSQLSchemaReaderClass {
|
|
|
56
61
|
return null;
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
// vector 컬럼의 dimensions 조회
|
|
65
|
+
const vectorDimensions = await this.getVectorDimensions(compareDB, table);
|
|
66
|
+
|
|
59
67
|
const columns: MigrationColumn[] = dbColumns.map((dbColumn) => {
|
|
60
68
|
const dbColType = this.resolveDBColType(dbColumn);
|
|
69
|
+
|
|
70
|
+
// vector 타입인 경우 dimensions 설정
|
|
71
|
+
if (dbColType.type === "vector") {
|
|
72
|
+
dbColType.dimensions = vectorDimensions[dbColumn.column_name] ?? 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
return {
|
|
62
76
|
name: dbColumn.column_name,
|
|
63
77
|
nullable: dbColumn.is_nullable === "YES",
|
|
64
78
|
...dbColType,
|
|
79
|
+
// Generated Column 처리
|
|
80
|
+
...(() => {
|
|
81
|
+
if (dbColumn.is_generated === "s" || dbColumn.is_generated === "v") {
|
|
82
|
+
return {
|
|
83
|
+
generated: {
|
|
84
|
+
type: dbColumn.is_generated === "s" ? "STORED" : "VIRTUAL",
|
|
85
|
+
expression: dbColumn.generation_expression ?? "",
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {};
|
|
90
|
+
})(),
|
|
91
|
+
// Default 값 처리 (Generated Column이 아닌 경우만)
|
|
65
92
|
...(() => {
|
|
93
|
+
// Generated Column은 default 값이 없음
|
|
94
|
+
if (dbColumn.is_generated === "s" || dbColumn.is_generated === "v") {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
if (dbColumn.column_default !== null) {
|
|
67
99
|
// PostgreSQL default 값 정리 (nextval, CURRENT_TIMESTAMP 등)
|
|
68
100
|
let defaultValue = dbColumn.column_default;
|
|
@@ -75,9 +107,9 @@ class PostgreSQLSchemaReaderClass {
|
|
|
75
107
|
// 타입 캐스팅 제거 (예: '1'::integer → 1)
|
|
76
108
|
defaultValue = defaultValue.replace(/::[\w\s]+$/g, "");
|
|
77
109
|
|
|
78
|
-
//
|
|
110
|
+
// 따옴표가 single quote인 경우 double quote로 변환
|
|
79
111
|
if (defaultValue.startsWith("'") && defaultValue.endsWith("'")) {
|
|
80
|
-
defaultValue = defaultValue.
|
|
112
|
+
defaultValue = defaultValue.replaceAll("'", '"');
|
|
81
113
|
}
|
|
82
114
|
|
|
83
115
|
return {
|
|
@@ -110,7 +142,12 @@ class PostgreSQLSchemaReaderClass {
|
|
|
110
142
|
return {
|
|
111
143
|
type,
|
|
112
144
|
name: indexName,
|
|
113
|
-
columns: currentIndexes.map((idx) =>
|
|
145
|
+
columns: currentIndexes.map((idx) => ({
|
|
146
|
+
name: idx.column_name,
|
|
147
|
+
nullsFirst: idx.nulls_first,
|
|
148
|
+
sortOrder: idx.sort_order,
|
|
149
|
+
})),
|
|
150
|
+
nullsNotDistinct: firstIndex.nulls_not_distinct,
|
|
114
151
|
};
|
|
115
152
|
});
|
|
116
153
|
|
|
@@ -153,21 +190,30 @@ class PostgreSQLSchemaReaderClass {
|
|
|
153
190
|
compareDB: Knex,
|
|
154
191
|
tableName: string,
|
|
155
192
|
): Promise<[PgColumn[], PgIndex[], PgForeign[]]> {
|
|
156
|
-
// Columns 조회
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
.
|
|
170
|
-
.
|
|
193
|
+
// Columns 조회 (Generated Column 정보 포함)
|
|
194
|
+
const columnsQuery = `
|
|
195
|
+
SELECT
|
|
196
|
+
c.column_name,
|
|
197
|
+
c.data_type,
|
|
198
|
+
c.udt_name,
|
|
199
|
+
c.character_maximum_length,
|
|
200
|
+
c.numeric_precision,
|
|
201
|
+
c.numeric_scale,
|
|
202
|
+
c.is_nullable,
|
|
203
|
+
c.column_default,
|
|
204
|
+
COALESCE(a.attgenerated, '') as is_generated,
|
|
205
|
+
c.generation_expression
|
|
206
|
+
FROM information_schema.columns c
|
|
207
|
+
LEFT JOIN pg_attribute a ON a.attname = c.column_name
|
|
208
|
+
AND a.attrelid = (
|
|
209
|
+
SELECT oid FROM pg_class WHERE relname = c.table_name
|
|
210
|
+
AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = c.table_schema)
|
|
211
|
+
)
|
|
212
|
+
WHERE c.table_name = ?
|
|
213
|
+
AND c.table_schema = 'public'
|
|
214
|
+
ORDER BY c.ordinal_position
|
|
215
|
+
`;
|
|
216
|
+
const columns = (await compareDB.raw(columnsQuery, [tableName])).rows as PgColumn[];
|
|
171
217
|
if (columns.length === 0) {
|
|
172
218
|
throw new Error(`Table not found: ${tableName}`);
|
|
173
219
|
}
|
|
@@ -179,12 +225,23 @@ class PostgreSQLSchemaReaderClass {
|
|
|
179
225
|
a.attname as column_name,
|
|
180
226
|
ix.indisunique as is_unique,
|
|
181
227
|
ix.indisprimary as is_primary,
|
|
182
|
-
am.amname as index_type
|
|
228
|
+
am.amname as index_type,
|
|
229
|
+
-- NULLS FIRST/LAST 확인 (비트 연산)
|
|
230
|
+
(opt & 2) = 2 AS nulls_first,
|
|
231
|
+
-- ASC/DESC 확인
|
|
232
|
+
CASE
|
|
233
|
+
WHEN (opt & 1) = 1 THEN 'DESC'
|
|
234
|
+
ELSE 'ASC'
|
|
235
|
+
END AS sort_order,
|
|
236
|
+
ix.indnullsnotdistinct AS nulls_not_distinct
|
|
183
237
|
FROM pg_class t
|
|
184
238
|
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
185
239
|
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
186
|
-
JOIN pg_attribute a ON a.attrelid = t.oid
|
|
187
240
|
JOIN pg_am am ON i.relam = am.oid
|
|
241
|
+
JOIN LATERAL unnest(ix.indkey, ix.indoption)
|
|
242
|
+
WITH ORDINALITY AS u(attnum, opt, ord) ON true
|
|
243
|
+
-- unnest에서 나온 attnum으로 직접 조인
|
|
244
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = u.attnum
|
|
188
245
|
WHERE t.relname = ?
|
|
189
246
|
AND a.attnum = ANY(ix.indkey)
|
|
190
247
|
ORDER BY i.relname, array_position(ix.indkey, a.attnum)
|
|
@@ -218,12 +275,43 @@ class PostgreSQLSchemaReaderClass {
|
|
|
218
275
|
return [columns, indexes, foreigns];
|
|
219
276
|
}
|
|
220
277
|
|
|
278
|
+
/**
|
|
279
|
+
* vector 컬럼의 dimensions를 조회합니다.
|
|
280
|
+
* pg_attribute의 atttypmod에서 차원 수를 추출합니다.
|
|
281
|
+
*/
|
|
282
|
+
private async getVectorDimensions(
|
|
283
|
+
compareDB: Knex,
|
|
284
|
+
tableName: string,
|
|
285
|
+
): Promise<Record<string, number>> {
|
|
286
|
+
const query = `
|
|
287
|
+
SELECT
|
|
288
|
+
a.attname as column_name,
|
|
289
|
+
a.atttypmod as dimensions
|
|
290
|
+
FROM pg_attribute a
|
|
291
|
+
JOIN pg_class c ON a.attrelid = c.oid
|
|
292
|
+
JOIN pg_type t ON a.atttypid = t.oid
|
|
293
|
+
WHERE c.relname = ?
|
|
294
|
+
AND t.typname = 'vector'
|
|
295
|
+
AND a.attnum > 0
|
|
296
|
+
`;
|
|
297
|
+
const result = await compareDB.raw(query, [tableName]);
|
|
298
|
+
const dimensions: Record<string, number> = {};
|
|
299
|
+
for (const row of result.rows) {
|
|
300
|
+
// atttypmod에서 실제 dimensions 값 추출
|
|
301
|
+
dimensions[row.column_name] = row.dimensions > 0 ? row.dimensions : 0;
|
|
302
|
+
}
|
|
303
|
+
return dimensions;
|
|
304
|
+
}
|
|
305
|
+
|
|
221
306
|
/**
|
|
222
307
|
* PostgreSQL 컬럼 타입을 분석하여 MigrationColumn 객체로 변환합니다.
|
|
223
308
|
*/
|
|
224
309
|
resolveDBColType(
|
|
225
310
|
dbColumn: PgColumn,
|
|
226
|
-
): Pick<
|
|
311
|
+
): Pick<
|
|
312
|
+
MigrationColumn,
|
|
313
|
+
"type" | "length" | "precision" | "scale" | "numberType" | "dimensions"
|
|
314
|
+
> {
|
|
227
315
|
const {
|
|
228
316
|
udt_name: _udt_name,
|
|
229
317
|
character_maximum_length,
|
|
@@ -304,6 +392,18 @@ class PostgreSQLSchemaReaderClass {
|
|
|
304
392
|
return { type: "json" };
|
|
305
393
|
}
|
|
306
394
|
|
|
395
|
+
// Vector (pgvector)
|
|
396
|
+
if (udt_name === "vector") {
|
|
397
|
+
// vector 타입의 차원 수는 column_default나 별도 쿼리로 확인해야 함
|
|
398
|
+
// 현재는 기본값 0으로 설정 (실제 dimensions는 getMigrationSetFromDB에서 별도 쿼리로 확인)
|
|
399
|
+
return { type: `vector${singleOrArray}`, dimensions: 0 };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// tsvector (PostgreSQL 전문 검색용 타입)
|
|
403
|
+
if (udt_name === "tsvector") {
|
|
404
|
+
return { type: "tsvector" };
|
|
405
|
+
}
|
|
406
|
+
|
|
307
407
|
throw new Error(`resolve 불가능한 PostgreSQL 컬럼 타입: ${udt_name}`);
|
|
308
408
|
}
|
|
309
409
|
}
|
package/src/syncer/syncer.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { hot } from "@sonamu-kit/hmr-hook";
|
|
|
2
2
|
import assert from "assert";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
5
|
+
import inflection from "inflection";
|
|
5
6
|
import { minimatch } from "minimatch";
|
|
6
7
|
import path, { dirname } from "path";
|
|
7
8
|
import { group, unique } from "radashi";
|
|
@@ -321,11 +322,13 @@ export class Syncer {
|
|
|
321
322
|
namesRecord: EntityManager.getNamesFromId(entityId),
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
|
-
if (modelPath.endsWith("frame.ts")) {
|
|
325
|
-
const [, frameName] = modelPath.match(/.+\/(.+)\.frame
|
|
325
|
+
if (modelPath.endsWith(".frame.ts")) {
|
|
326
|
+
const [, frameName] = modelPath.match(/.+\/(.+)\.frame\.ts$/) ?? [];
|
|
326
327
|
assert(frameName);
|
|
328
|
+
// frameName을 PascalCase로 변환 (dashboard -> Dashboard)
|
|
329
|
+
const frameId = inflection.camelize(frameName);
|
|
327
330
|
return {
|
|
328
|
-
namesRecord: EntityManager.getNamesFromId(
|
|
331
|
+
namesRecord: EntityManager.getNamesFromId(frameId),
|
|
329
332
|
};
|
|
330
333
|
}
|
|
331
334
|
throw new Error("not reachable");
|
|
@@ -177,18 +177,60 @@ export class Template__generated extends Template {
|
|
|
177
177
|
.filter((prop) => isVirtualProp(prop))
|
|
178
178
|
.map((prop) => prop.name);
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* hasDefault props
|
|
182
|
+
* - nullable 또는 dbDefault가 있는 컬럼 (id 포함)
|
|
183
|
+
* - relation이 아니거나, relation이어도 nullable이면 포함
|
|
184
|
+
*/
|
|
185
|
+
const hasDefaultColumns = entity.props
|
|
186
|
+
.filter(
|
|
187
|
+
(prop) =>
|
|
188
|
+
(prop.type !== "relation" || prop.nullable === true) &&
|
|
189
|
+
(prop.nullable === true || (prop.type !== "relation" && prop.dbDefault !== undefined)),
|
|
190
|
+
)
|
|
191
|
+
.map((prop) => (prop.type === "relation" ? `${prop.name}_id` : prop.name))
|
|
192
|
+
.concat("id");
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* generated props
|
|
196
|
+
* - generated 속성이 있는 컬럼 (INSERT/UPDATE 시 값 제공 불가)
|
|
197
|
+
*/
|
|
198
|
+
const generatedColumns = entity.props
|
|
199
|
+
.filter((prop) => prop.type !== "relation" && prop.generated !== undefined)
|
|
200
|
+
.map((prop) => prop.name);
|
|
201
|
+
|
|
202
|
+
const hasMetadata =
|
|
203
|
+
fulltextColumns.length > 0 ||
|
|
204
|
+
virtualProps.length > 0 ||
|
|
205
|
+
hasDefaultColumns.length > 0 ||
|
|
206
|
+
generatedColumns.length > 0;
|
|
207
|
+
|
|
180
208
|
const lines = [
|
|
181
209
|
`export const ${schemaName} = ${schemaBody};`,
|
|
182
210
|
`export type ${schemaName} = z.infer<typeof ${schemaName}>` +
|
|
183
|
-
(
|
|
184
|
-
? ` & {
|
|
185
|
-
.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.
|
|
191
|
-
|
|
211
|
+
(hasMetadata
|
|
212
|
+
? ` & {${
|
|
213
|
+
(fulltextColumns.length > 0
|
|
214
|
+
? `readonly __fulltext__: readonly [${fulltextColumns
|
|
215
|
+
.map((col) => `"${col}"`)
|
|
216
|
+
.join(", ")}],`
|
|
217
|
+
: "") +
|
|
218
|
+
(virtualProps.length > 0
|
|
219
|
+
? `readonly __virtual__: readonly [${virtualProps.map((prop) => `"${prop}"`).join(", ")}],`
|
|
220
|
+
: "") +
|
|
221
|
+
(
|
|
222
|
+
hasDefaultColumns.length > 0
|
|
223
|
+
? `readonly __hasDefault__: readonly [${hasDefaultColumns
|
|
224
|
+
.map((col) => `"${col}"`)
|
|
225
|
+
.join(", ")}],`
|
|
226
|
+
: ""
|
|
227
|
+
) +
|
|
228
|
+
(generatedColumns.length > 0
|
|
229
|
+
? `readonly __generated__: readonly [${generatedColumns
|
|
230
|
+
.map((col) => `"${col}"`)
|
|
231
|
+
.join(", ")}],`
|
|
232
|
+
: "")
|
|
233
|
+
}}`
|
|
192
234
|
: "") +
|
|
193
235
|
";",
|
|
194
236
|
];
|
|
@@ -4,7 +4,11 @@ import { unique } from "radashi";
|
|
|
4
4
|
import { Sonamu } from "../../api";
|
|
5
5
|
import type { Entity } from "../../entity/entity";
|
|
6
6
|
import { EntityManager } from "../../entity/entity-manager";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isBelongsToOneRelationProp,
|
|
9
|
+
isManyToManyRelationProp,
|
|
10
|
+
isOneToOneRelationProp,
|
|
11
|
+
} from "../../types/types";
|
|
8
12
|
import { Template } from "../template";
|
|
9
13
|
import type { SourceCode } from "./generated.template";
|
|
10
14
|
|
|
@@ -71,6 +75,12 @@ export class Template__generated_sso extends Template {
|
|
|
71
75
|
return [puriSubsetQuery, puriLoaderQuery];
|
|
72
76
|
});
|
|
73
77
|
|
|
78
|
+
// ForeignKey 타입 생성
|
|
79
|
+
const fkTypeSourceCode = this.getForeignKeyTypeSourceCode(entities);
|
|
80
|
+
if (fkTypeSourceCode) {
|
|
81
|
+
sourceCodes.push(fkTypeSourceCode);
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
// DatabaseSchema 생성
|
|
75
85
|
const dbSchemaSourceCode = this.getDatabaseSchemaSourceCode(entities);
|
|
76
86
|
if (dbSchemaSourceCode) {
|
|
@@ -114,11 +124,15 @@ export class Template__generated_sso extends Template {
|
|
|
114
124
|
};
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
|
|
127
|
+
//===============================================
|
|
128
|
+
// private Helper Methods
|
|
129
|
+
//===============================================
|
|
130
|
+
private getDatabaseSchemaSourceCode(entities: Entity[]): SourceCode | null {
|
|
118
131
|
if (entities.length === 0) {
|
|
119
132
|
return null;
|
|
120
133
|
}
|
|
121
134
|
|
|
135
|
+
// DatabaseSchemaExtend - 테이블 스키마 타입 정의
|
|
122
136
|
const entitySchemaLines = entities.map((entity) => `${entity.table}: ${entity.id}BaseSchema;`);
|
|
123
137
|
|
|
124
138
|
const joinTables = unique(
|
|
@@ -132,6 +146,14 @@ export class Template__generated_sso extends Template {
|
|
|
132
146
|
(joinTable) => joinTable.table,
|
|
133
147
|
);
|
|
134
148
|
|
|
149
|
+
// DatabaseForeignKeys - FK 컬럼을 가진 테이블만 정의
|
|
150
|
+
const entitiesWithFk = entities.filter(
|
|
151
|
+
(entity) => this.getForeignKeyColumns(entity).length > 0,
|
|
152
|
+
);
|
|
153
|
+
const fkMetadataLines = entitiesWithFk.map(
|
|
154
|
+
(entity) => `${entity.table}: ${entity.id}ForeignKeys;`,
|
|
155
|
+
);
|
|
156
|
+
|
|
135
157
|
return {
|
|
136
158
|
label: `DatabaseSchema`,
|
|
137
159
|
lines: [
|
|
@@ -143,9 +165,56 @@ export class Template__generated_sso extends Template {
|
|
|
143
165
|
`${joinTable.table}: ManyToManyBaseSchema<"${joinTable.fromTableKey}", "${joinTable.toTableKey}">;`,
|
|
144
166
|
),
|
|
145
167
|
` }`,
|
|
168
|
+
``,
|
|
169
|
+
` export interface DatabaseForeignKeys {`,
|
|
170
|
+
...fkMetadataLines,
|
|
171
|
+
` }`,
|
|
146
172
|
`}`,
|
|
147
173
|
],
|
|
148
174
|
importKeys: entities.map((entity) => `${entity.id}BaseSchema`),
|
|
149
175
|
};
|
|
150
176
|
}
|
|
177
|
+
|
|
178
|
+
// FK 관계를 컬럼명으로 변환 (예: company → company_id)
|
|
179
|
+
private getForeignKeyColumns(entity: Entity): string[] {
|
|
180
|
+
return entity.props
|
|
181
|
+
.filter((prop) => {
|
|
182
|
+
if (isBelongsToOneRelationProp(prop)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
if (isOneToOneRelationProp(prop) && prop.hasJoinColumn) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
})
|
|
190
|
+
.map((prop) => `${prop.name}_id`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private getForeignKeyTypeSourceCode(entities: Entity[]): SourceCode | null {
|
|
194
|
+
if (entities.length === 0) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// FK가 있는 엔티티만 타입 생성
|
|
199
|
+
const entitiesWithFk = entities.filter(
|
|
200
|
+
(entity) => this.getForeignKeyColumns(entity).length > 0,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (entitiesWithFk.length === 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const fkTypeLines = entitiesWithFk.map((entity) => {
|
|
208
|
+
const fkColumns = this.getForeignKeyColumns(entity);
|
|
209
|
+
const fkTypeValue = fkColumns.map((col) => `"${col}"`).join(" | ");
|
|
210
|
+
|
|
211
|
+
return `export type ${entity.id}ForeignKeys = ${fkTypeValue};`;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
label: `ForeignKey Types`,
|
|
216
|
+
lines: fkTypeLines,
|
|
217
|
+
importKeys: [],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
151
220
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type z from "zod";
|
|
1
2
|
import { Sonamu } from "../../api";
|
|
2
3
|
import { EntityManager, type EntityNamesRecord } from "../../entity/entity-manager";
|
|
3
4
|
import { Naite } from "../../naite/naite";
|
|
@@ -5,7 +6,6 @@ import type { TemplateOptions } from "../../types/types";
|
|
|
5
6
|
import { Template } from "../template";
|
|
6
7
|
import { getZodTypeById, zodTypeToRenderingNode } from "../zod-converter";
|
|
7
8
|
import { Template__view_list } from "./view_list.template";
|
|
8
|
-
|
|
9
9
|
export class Template__model extends Template {
|
|
10
10
|
constructor() {
|
|
11
11
|
super("model");
|
|
@@ -26,6 +26,9 @@ export class Template__model extends Template {
|
|
|
26
26
|
const listParamsZodType = await getZodTypeById(`${entityId}ListParams`);
|
|
27
27
|
const listParamsNode = zodTypeToRenderingNode(listParamsZodType);
|
|
28
28
|
|
|
29
|
+
const subsetKeyZodType = await getZodTypeById(`${entityId}SubsetKey`);
|
|
30
|
+
const subsetKeys = (subsetKeyZodType as z.ZodEnum).enum;
|
|
31
|
+
|
|
29
32
|
const names = EntityManager.getNamesFromId(entityId);
|
|
30
33
|
const entity = EntityManager.get(entityId);
|
|
31
34
|
|
|
@@ -91,17 +94,17 @@ class ${entityId}ModelClass extends BaseModelClass<
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
@api({ httpMethod: "GET", clients: ["axios", "swr"], resourceName: "${names.capitalPlural}" })
|
|
94
|
-
async findMany<T extends ${entityId}SubsetKey>(
|
|
97
|
+
async findMany<T extends ${entityId}SubsetKey, LP extends ${entityId}ListParams>(
|
|
95
98
|
subset: T,
|
|
96
|
-
|
|
97
|
-
): Promise<ListResult
|
|
99
|
+
rawParams?: LP,
|
|
100
|
+
): Promise<ListResult<LP, ${entityId}SubsetMapping[T]>> {
|
|
98
101
|
// params with defaults
|
|
99
|
-
params = {
|
|
102
|
+
const params = {
|
|
100
103
|
num: 24,
|
|
101
104
|
page: 1,
|
|
102
|
-
search: "${def.search}",
|
|
103
|
-
orderBy: "${def.orderBy}",
|
|
104
|
-
...
|
|
105
|
+
search: "${def.search}" as const,
|
|
106
|
+
orderBy: "${def.orderBy}" as const,
|
|
107
|
+
...rawParams,
|
|
105
108
|
};
|
|
106
109
|
|
|
107
110
|
// build queries
|
|
@@ -132,18 +135,25 @@ class ${entityId}ModelClass extends BaseModelClass<
|
|
|
132
135
|
exhaustive(params.orderBy);
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
|
-
|
|
136
|
-
const
|
|
138
|
+
|
|
139
|
+
const enhancers = this.createEnhancers({
|
|
140
|
+
${Object.keys(subsetKeys)
|
|
141
|
+
.map(
|
|
142
|
+
(key) => `${key}: (row) => ({
|
|
143
|
+
...row,
|
|
144
|
+
// 서브셋별로 virtual 필드 계산로직 추가
|
|
145
|
+
}),`,
|
|
146
|
+
)
|
|
147
|
+
.join("\n")}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return this.executeSubsetQuery({
|
|
137
151
|
subset,
|
|
138
152
|
qb,
|
|
139
153
|
params,
|
|
154
|
+
enhancers,
|
|
140
155
|
debug: false,
|
|
141
156
|
});
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
rows,
|
|
145
|
-
total,
|
|
146
|
-
};
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
@api({ httpMethod: "POST" })
|
|
@@ -23,10 +23,10 @@ export class Template__model_test extends Template {
|
|
|
23
23
|
return {
|
|
24
24
|
...this.getTargetAndPath(names),
|
|
25
25
|
body: `
|
|
26
|
-
import { describe,
|
|
27
|
-
import { bootstrap } from '../../testing/bootstrap';
|
|
26
|
+
import { describe, expect, vi } from "vitest";
|
|
27
|
+
import { bootstrap, test } from '../../testing/bootstrap';
|
|
28
28
|
|
|
29
|
-
bootstrap();
|
|
29
|
+
bootstrap(vi);
|
|
30
30
|
describe.skip("${entityId}ModelTest", () => {
|
|
31
31
|
test("Query", async () => {
|
|
32
32
|
expect(true).toBe(true);
|
|
@@ -36,7 +36,11 @@ export class Template__service extends Template {
|
|
|
36
36
|
syncer: { apis },
|
|
37
37
|
} = Sonamu;
|
|
38
38
|
|
|
39
|
-
const apisForThisModel = apis.filter(
|
|
39
|
+
const apisForThisModel = apis.filter(
|
|
40
|
+
(api) =>
|
|
41
|
+
api.modelName === `${namesRecord.capital}Model` ||
|
|
42
|
+
api.modelName === `${namesRecord.capital}Frame`,
|
|
43
|
+
);
|
|
40
44
|
|
|
41
45
|
// 서비스 TypeSource
|
|
42
46
|
const { lines, importKeys } = this.getTypeSource(apisForThisModel);
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
DropdownProps,
|
|
29
29
|
} from 'semantic-ui-react';
|
|
30
30
|
|
|
31
|
-
import { ${enumId}Label } from '
|
|
31
|
+
import { ${enumId}Label } from '@/services/sonamu.generated';
|
|
32
32
|
|
|
33
33
|
export function ${enumId}Dropdown(props: DropdownProps) {
|
|
34
34
|
const options = Object.entries(${enumId}Label).map(([key, label]) => {
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
DropdownProps,
|
|
29
29
|
} from 'semantic-ui-react';
|
|
30
30
|
|
|
31
|
-
import { ${enumId}, ${enumId}Label } from '
|
|
31
|
+
import { ${enumId}, ${enumId}Label } from '@/services/sonamu.generated';
|
|
32
32
|
|
|
33
33
|
export type ${enumId}SelectProps = {
|
|
34
34
|
placeholder?: string;
|