sonamu 0.7.3 → 0.7.5

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.
Files changed (133) hide show
  1. package/dist/api/config.d.ts +1 -4
  2. package/dist/api/config.d.ts.map +1 -1
  3. package/dist/api/config.js +1 -1
  4. package/dist/api/sonamu.d.ts +2 -0
  5. package/dist/api/sonamu.d.ts.map +1 -1
  6. package/dist/api/sonamu.js +19 -47
  7. package/dist/bin/cli.js +6 -6
  8. package/dist/database/base-model.d.ts +1 -1
  9. package/dist/database/base-model.d.ts.map +1 -1
  10. package/dist/database/base-model.js +15 -4
  11. package/dist/database/code-generator.d.ts.map +1 -1
  12. package/dist/database/code-generator.js +3 -3
  13. package/dist/database/db.d.ts.map +1 -1
  14. package/dist/database/db.js +1 -1
  15. package/dist/database/puri-wrapper.d.ts +11 -11
  16. package/dist/database/puri-wrapper.d.ts.map +1 -1
  17. package/dist/database/puri-wrapper.js +7 -11
  18. package/dist/database/puri.d.ts +36 -17
  19. package/dist/database/puri.d.ts.map +1 -1
  20. package/dist/database/puri.js +54 -7
  21. package/dist/database/puri.types.d.ts +54 -17
  22. package/dist/database/puri.types.d.ts.map +1 -1
  23. package/dist/database/puri.types.js +2 -4
  24. package/dist/database/puri.types.test-d.js +129 -0
  25. package/dist/database/upsert-builder.d.ts +16 -10
  26. package/dist/database/upsert-builder.d.ts.map +1 -1
  27. package/dist/database/upsert-builder.js +10 -19
  28. package/dist/entity/entity-manager.d.ts +113 -22
  29. package/dist/entity/entity-manager.d.ts.map +1 -1
  30. package/dist/entity/entity-manager.js +1 -1
  31. package/dist/entity/entity.d.ts +34 -0
  32. package/dist/entity/entity.d.ts.map +1 -1
  33. package/dist/entity/entity.js +110 -37
  34. package/dist/index.d.ts +5 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +8 -2
  37. package/dist/migration/code-generation.d.ts.map +1 -1
  38. package/dist/migration/code-generation.js +341 -149
  39. package/dist/migration/migration-set.d.ts.map +1 -1
  40. package/dist/migration/migration-set.js +21 -5
  41. package/dist/migration/migrator.d.ts.map +1 -1
  42. package/dist/migration/migrator.js +7 -1
  43. package/dist/migration/postgresql-schema-reader.d.ts +11 -1
  44. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  45. package/dist/migration/postgresql-schema-reader.js +111 -10
  46. package/dist/syncer/syncer.d.ts.map +1 -1
  47. package/dist/syncer/syncer.js +5 -4
  48. package/dist/template/implementations/generated.template.d.ts.map +1 -1
  49. package/dist/template/implementations/generated.template.js +12 -2
  50. package/dist/template/implementations/generated_sso.template.d.ts +3 -3
  51. package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
  52. package/dist/template/implementations/generated_sso.template.js +50 -2
  53. package/dist/template/implementations/model.template.js +6 -6
  54. package/dist/template/implementations/model_test.template.js +4 -4
  55. package/dist/template/implementations/view_enums_dropdown.template.js +2 -2
  56. package/dist/template/implementations/view_enums_select.template.js +2 -2
  57. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  58. package/dist/template/implementations/view_form.template.js +12 -9
  59. package/dist/template/implementations/view_id_async_select.template.js +4 -4
  60. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  61. package/dist/template/implementations/view_list.template.js +12 -9
  62. package/dist/template/implementations/view_search_input.template.js +2 -2
  63. package/dist/template/template.js +2 -2
  64. package/dist/template/zod-converter.d.ts.map +1 -1
  65. package/dist/template/zod-converter.js +17 -2
  66. package/dist/testing/fixture-manager.d.ts +2 -1
  67. package/dist/testing/fixture-manager.d.ts.map +1 -1
  68. package/dist/testing/fixture-manager.js +29 -29
  69. package/dist/types/types.d.ts +593 -68
  70. package/dist/types/types.d.ts.map +1 -1
  71. package/dist/types/types.js +113 -9
  72. package/dist/vector/chunking.d.ts +25 -0
  73. package/dist/vector/chunking.d.ts.map +1 -0
  74. package/dist/vector/chunking.js +97 -0
  75. package/dist/vector/config.d.ts +12 -0
  76. package/dist/vector/config.d.ts.map +1 -0
  77. package/dist/vector/config.js +83 -0
  78. package/dist/vector/embedding.d.ts +42 -0
  79. package/dist/vector/embedding.d.ts.map +1 -0
  80. package/dist/vector/embedding.js +147 -0
  81. package/dist/vector/types.d.ts +105 -0
  82. package/dist/vector/types.d.ts.map +1 -0
  83. package/dist/vector/types.js +5 -0
  84. package/dist/vector/vector-search.d.ts +47 -0
  85. package/dist/vector/vector-search.d.ts.map +1 -0
  86. package/dist/vector/vector-search.js +176 -0
  87. package/package.json +11 -11
  88. package/src/api/config.ts +0 -4
  89. package/src/api/sonamu.ts +21 -36
  90. package/src/bin/cli.ts +5 -5
  91. package/src/database/base-model.ts +20 -11
  92. package/src/database/code-generator.ts +6 -2
  93. package/src/database/db.ts +1 -0
  94. package/src/database/puri-wrapper.ts +22 -16
  95. package/src/database/puri.ts +150 -27
  96. package/src/database/puri.types.test-d.ts +457 -0
  97. package/src/database/puri.types.ts +231 -33
  98. package/src/database/upsert-builder.ts +43 -34
  99. package/src/entity/entity-manager.ts +2 -2
  100. package/src/entity/entity.ts +134 -44
  101. package/src/index.ts +6 -0
  102. package/src/migration/code-generation.ts +377 -174
  103. package/src/migration/migration-set.ts +22 -3
  104. package/src/migration/migrator.ts +6 -0
  105. package/src/migration/postgresql-schema-reader.ts +121 -21
  106. package/src/syncer/syncer.ts +4 -3
  107. package/src/template/implementations/generated.template.ts +51 -9
  108. package/src/template/implementations/generated_sso.template.ts +71 -2
  109. package/src/template/implementations/model.template.ts +5 -5
  110. package/src/template/implementations/model_test.template.ts +3 -3
  111. package/src/template/implementations/view_enums_dropdown.template.ts +1 -1
  112. package/src/template/implementations/view_enums_select.template.ts +1 -1
  113. package/src/template/implementations/view_form.template.ts +11 -8
  114. package/src/template/implementations/view_id_async_select.template.ts +3 -3
  115. package/src/template/implementations/view_list.template.ts +11 -8
  116. package/src/template/implementations/view_search_input.template.ts +1 -1
  117. package/src/template/template.ts +1 -1
  118. package/src/template/zod-converter.ts +20 -0
  119. package/src/testing/fixture-manager.ts +31 -30
  120. package/src/types/types.ts +226 -48
  121. package/src/vector/chunking.ts +115 -0
  122. package/src/vector/config.ts +68 -0
  123. package/src/vector/embedding.ts +193 -0
  124. package/src/vector/types.ts +122 -0
  125. package/src/vector/vector-search.ts +261 -0
  126. package/dist/template/implementations/view_enums_buttonset.template.d.ts +0 -17
  127. package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +0 -1
  128. package/dist/template/implementations/view_enums_buttonset.template.js +0 -31
  129. package/dist/template/implementations/view_list_columns.template.d.ts +0 -17
  130. package/dist/template/implementations/view_list_columns.template.d.ts.map +0 -1
  131. package/dist/template/implementations/view_list_columns.template.js +0 -49
  132. package/src/template/implementations/view_enums_buttonset.template.ts +0 -34
  133. 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) => index.columns.find((col) => col.includes(`${prop.joinTable}.`)))
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) => col.replace(`${prop.joinTable}.`, "")),
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.slice(1, -1);
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) => idx.column_name),
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 columns = await compareDB
158
- .select(
159
- "column_name",
160
- "data_type",
161
- "udt_name",
162
- "character_maximum_length",
163
- "numeric_precision",
164
- "numeric_scale",
165
- "is_nullable",
166
- "column_default",
167
- )
168
- .from("information_schema.columns")
169
- .where({ table_name: tableName })
170
- .orderBy("ordinal_position");
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<MigrationColumn, "type" | "length" | "precision" | "scale" | "numberType"> {
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
  }
@@ -165,7 +165,7 @@ export class Syncer {
165
165
  }
166
166
 
167
167
  // 이건 프로젝트에 .ts 소스 코드 파일을 생성하는 것이므로 src의 .ts 경로로 갑니다.
168
- const destPath = path.join(Sonamu.appRootPath, target, "./sonamu.shared.ts");
168
+ const destPath = path.join(Sonamu.appRootPath, target, "./src/services/sonamu.shared.ts");
169
169
 
170
170
  // 정말 혹시나지만 target 디렉토리는 있어도 src/services 디렉토리는 없을 수 있으므로 미리 생성해줍니다.
171
171
  if (!(await exists(path.dirname(destPath)))) {
@@ -321,8 +321,9 @@ export class Syncer {
321
321
  namesRecord: EntityManager.getNamesFromId(entityId),
322
322
  };
323
323
  }
324
- if (modelPath.endsWith("frame.ts")) {
325
- const [, frameName] = modelPath.match(/.+\/(.+)\.frame.js$/) ?? [];
324
+ if (modelPath.endsWith(".frame.js") || modelPath.endsWith(".frame.ts")) {
325
+ const [, frameName] = modelPath.match(/.+\/(.+)\.frame\.(js|ts)$/) ?? [];
326
+ console.log(modelPath, "->", frameName);
326
327
  assert(frameName);
327
328
  return {
328
329
  namesRecord: EntityManager.getNamesFromId(frameName),
@@ -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
- (fulltextColumns.length > 0
184
- ? ` & { readonly __fulltext__: readonly [${fulltextColumns
185
- .map((col) => `"${col}"`)
186
- .join(", ")}] }`
187
- : "") +
188
- (virtualProps.length > 0
189
- ? ` & { readonly __virtual__: readonly [${virtualProps
190
- .map((prop) => `"${prop}"`)
191
- .join(", ")}] }`
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 { isManyToManyRelationProp } from "../../types/types";
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
- getDatabaseSchemaSourceCode(entities: Entity[]): SourceCode | null {
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
  }
@@ -91,17 +91,17 @@ class ${entityId}ModelClass extends BaseModelClass<
91
91
  }
92
92
 
93
93
  @api({ httpMethod: "GET", clients: ["axios", "swr"], resourceName: "${names.capitalPlural}" })
94
- async findMany<T extends ${entityId}SubsetKey>(
94
+ async findMany<T extends ${entityId}SubsetKey, LP extends ${entityId}ListParams>(
95
95
  subset: T,
96
- params: ${entityId}ListParams = {}
97
- ): Promise<ListResult<${entityId}SubsetMapping[T]>> {
96
+ rawParams?: LP,
97
+ ): Promise<ListResult<LP, ${entityId}SubsetMapping[T]>> {
98
98
  // params with defaults
99
- params = {
99
+ const params = {
100
100
  num: 24,
101
101
  page: 1,
102
102
  search: "${def.search}",
103
103
  orderBy: "${def.orderBy}",
104
- ...params,
104
+ ...rawParams,
105
105
  };
106
106
 
107
107
  // build queries
@@ -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, test, expect } from "vitest";
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);
@@ -28,7 +28,7 @@ import {
28
28
  DropdownProps,
29
29
  } from 'semantic-ui-react';
30
30
 
31
- import { ${enumId}Label } from 'src/services/sonamu.generated';
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 'src/services/sonamu.generated';
31
+ import { ${enumId}, ${enumId}Label } from '@/services/sonamu.generated';
32
32
 
33
33
  export type ${enumId}SelectProps = {
34
34
  placeholder?: string;
@@ -35,13 +35,13 @@ export class Template__view_form extends Template {
35
35
  if (col.renderType === "enums") {
36
36
  const { id, targetEntityNames } = getEnumInfoFromColName(entityId, col.name);
37
37
  const componentId = `${id}Select`;
38
- return `import { ${componentId} } from "src/components/${targetEntityNames.fs}/${componentId}";`;
38
+ return `import { ${componentId} } from "@/components/${targetEntityNames.fs}/${componentId}";`;
39
39
  } else if (col.renderType === "number-fk_id") {
40
40
  try {
41
41
  const relProp = getRelationPropFromColName(entityId, col.name.replace("_id", ""));
42
42
  const targetNames = EntityManager.getNamesFromId(relProp.with);
43
43
  const componentId = `${relProp.with}IdAsyncSelect`;
44
- return `import { ${componentId} } from "src/components/${targetNames.fs}/${componentId}";`;
44
+ return `import { ${componentId} } from "@/components/${targetNames.fs}/${componentId}";`;
45
45
  } catch {
46
46
  return "";
47
47
  }
@@ -111,6 +111,9 @@ export class Template__view_form extends Template {
111
111
  return `<>${col.name} array</>`;
112
112
  case "object":
113
113
  return `<>${col.name} object</>`;
114
+ case "vector":
115
+ // vector 타입은 일반적으로 API를 통해 생성되므로 읽기 전용으로 표시
116
+ return `<div className="p-8px text-gray-500">[Vector: ${col.name}] - 임베딩 데이터는 API를 통해 자동 생성됩니다.</div>`;
114
117
  default:
115
118
  throw new Error(`대응 불가능한 렌더 타입 ${col.renderType} on ${col.name}`);
116
119
  }
@@ -248,13 +251,13 @@ import {
248
251
  import { DateTime } from "luxon";
249
252
 
250
253
  import { BackLink, LinkInput, NumberInput, BooleanToggle, SQLDateTimeInput, SQLDateInput, useTypeForm, useGoBack, formatDateTime } from "@sonamu-kit/react-sui";
251
- import { defaultCatch } from 'src/services/sonamu.shared';
252
- // import { ImageUploader } from 'src/admin-common/ImageUploader';
253
- // import { useCommonModal } from "src/admin-common/CommonModal";
254
+ import { defaultCatch } from '@/services/sonamu.shared';
255
+ // import { ImageUploader } from '@/admin-common/ImageUploader';
256
+ // import { useCommonModal } from "@/admin-common/CommonModal";
254
257
 
255
- import { ${names.capital}SaveParams } from 'src/services/${names.fs}/${names.fs}.types';
256
- import { ${names.capital}Service } from 'src/services/${names.fs}/${names.fs}.service';
257
- import { ${names.capital}SubsetA } from 'src/services/sonamu.generated';
258
+ import { ${names.capital}SaveParams } from '@/services/${names.fs}/${names.fs}.types';
259
+ import { ${names.capital}Service } from '@/services/${names.fs}/${names.fs}.service';
260
+ import { ${names.capital}SubsetA } from '@/services/sonamu.generated';
258
261
  ${unique(
259
262
  columns
260
263
  .filter((col) => ["number-fk_id", "enums"].includes(col.renderType))
@@ -39,9 +39,9 @@ import React, { useState, useEffect, SyntheticEvent } from "react";
39
39
  import { DropdownProps, DropdownItemProps, DropdownOnSearchChangeData, Dropdown } from "semantic-ui-react";
40
40
  import { ${names.capital}SubsetKey, ${
41
41
  names.capital
42
- }SubsetMapping } from "src/services/sonamu.generated";
43
- import { ${names.capital}Service } from "src/services/${names.fs}/${names.fs}.service";
44
- import { ${names.capital}ListParams } from "src/services/${names.fs}/${names.fs}.types";
42
+ }SubsetMapping } from "@/services/sonamu.generated";
43
+ import { ${names.capital}Service } from "@/services/${names.fs}/${names.fs}.service";
44
+ import { ${names.capital}ListParams } from "@/services/${names.fs}/${names.fs}.types";
45
45
 
46
46
  export function ${names.capital}IdAsyncSelect<T extends ${names.capital}SubsetKey>(
47
47
  { subset, baseListParams, textField, valueField, ...props }: DropdownProps & {