sonamu 0.7.21 → 0.7.23

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 (200) hide show
  1. package/dist/ai/agents/agent.d.ts +6 -1
  2. package/dist/ai/agents/agent.d.ts.map +1 -1
  3. package/dist/ai/agents/agent.js +20 -5
  4. package/dist/api/base-frame.d.ts +4 -0
  5. package/dist/api/base-frame.d.ts.map +1 -1
  6. package/dist/api/base-frame.js +9 -1
  7. package/dist/api/caster.d.ts.map +1 -1
  8. package/dist/api/caster.js +2 -2
  9. package/dist/api/config.d.ts +35 -3
  10. package/dist/api/config.d.ts.map +1 -1
  11. package/dist/api/config.js +1 -1
  12. package/dist/api/decorators.d.ts +4 -4
  13. package/dist/api/decorators.d.ts.map +1 -1
  14. package/dist/api/decorators.js +80 -18
  15. package/dist/api/index.d.ts +1 -0
  16. package/dist/api/index.d.ts.map +1 -1
  17. package/dist/api/index.js +2 -1
  18. package/dist/api/secret.d.ts +7 -0
  19. package/dist/api/secret.d.ts.map +1 -0
  20. package/dist/api/secret.js +17 -0
  21. package/dist/api/sonamu.d.ts +17 -8
  22. package/dist/api/sonamu.d.ts.map +1 -1
  23. package/dist/api/sonamu.js +265 -47
  24. package/dist/cache/cache-manager.d.ts +11 -0
  25. package/dist/cache/cache-manager.d.ts.map +1 -0
  26. package/dist/cache/cache-manager.js +22 -0
  27. package/dist/cache/decorator.d.ts +31 -0
  28. package/dist/cache/decorator.d.ts.map +1 -0
  29. package/dist/cache/decorator.js +86 -0
  30. package/dist/cache/drivers.d.ts +33 -0
  31. package/dist/cache/drivers.d.ts.map +1 -0
  32. package/dist/cache/drivers.js +36 -0
  33. package/dist/cache/index.d.ts +4 -0
  34. package/dist/cache/index.d.ts.map +1 -0
  35. package/dist/cache/index.js +8 -0
  36. package/dist/cache/types.d.ts +28 -0
  37. package/dist/cache/types.d.ts.map +1 -0
  38. package/dist/cache/types.js +6 -0
  39. package/dist/database/base-model.d.ts +4 -2
  40. package/dist/database/base-model.d.ts.map +1 -1
  41. package/dist/database/base-model.js +9 -4
  42. package/dist/database/code-generator.d.ts +3 -1
  43. package/dist/database/code-generator.d.ts.map +1 -1
  44. package/dist/database/code-generator.js +3 -2
  45. package/dist/database/db.d.ts +1 -1
  46. package/dist/database/db.d.ts.map +1 -1
  47. package/dist/database/db.js +5 -5
  48. package/dist/database/knex.d.ts +3 -0
  49. package/dist/database/knex.d.ts.map +1 -0
  50. package/dist/database/knex.js +29 -0
  51. package/dist/database/puri.types.d.ts.map +1 -1
  52. package/dist/database/puri.types.js +1 -1
  53. package/dist/database/upsert-builder.d.ts.map +1 -1
  54. package/dist/database/upsert-builder.js +49 -5
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -1
  58. package/dist/logger/category.d.ts +4 -0
  59. package/dist/logger/category.d.ts.map +1 -0
  60. package/dist/logger/category.js +34 -0
  61. package/dist/logger/configure.d.ts +9 -0
  62. package/dist/logger/configure.d.ts.map +1 -0
  63. package/dist/logger/configure.js +115 -0
  64. package/dist/migration/code-generation.d.ts +5 -1
  65. package/dist/migration/code-generation.d.ts.map +1 -1
  66. package/dist/migration/code-generation.js +13 -7
  67. package/dist/migration/migrator.d.ts +1 -1
  68. package/dist/migration/migrator.d.ts.map +1 -1
  69. package/dist/migration/migrator.js +7 -7
  70. package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
  71. package/dist/migration/postgresql-schema-reader.js +5 -3
  72. package/dist/naite/naite.d.ts +0 -4
  73. package/dist/naite/naite.d.ts.map +1 -1
  74. package/dist/naite/naite.js +11 -19
  75. package/dist/ssr/index.d.ts +4 -0
  76. package/dist/ssr/index.d.ts.map +1 -0
  77. package/dist/ssr/index.js +4 -0
  78. package/dist/ssr/registry.d.ts +10 -0
  79. package/dist/ssr/registry.d.ts.map +1 -0
  80. package/dist/ssr/registry.js +43 -0
  81. package/dist/ssr/renderer.d.ts +6 -0
  82. package/dist/ssr/renderer.d.ts.map +1 -0
  83. package/dist/ssr/renderer.js +70 -0
  84. package/dist/ssr/types.d.ts +19 -0
  85. package/dist/ssr/types.d.ts.map +1 -0
  86. package/dist/ssr/types.js +4 -0
  87. package/dist/syncer/syncer.d.ts +1 -0
  88. package/dist/syncer/syncer.d.ts.map +1 -1
  89. package/dist/syncer/syncer.js +58 -1
  90. package/dist/tasks/decorator.d.ts +1 -0
  91. package/dist/tasks/decorator.d.ts.map +1 -1
  92. package/dist/tasks/decorator.js +9 -7
  93. package/dist/tasks/step-wrapper.d.ts +5 -0
  94. package/dist/tasks/step-wrapper.d.ts.map +1 -1
  95. package/dist/tasks/step-wrapper.js +11 -6
  96. package/dist/tasks/workflow-manager.d.ts +2 -0
  97. package/dist/tasks/workflow-manager.d.ts.map +1 -1
  98. package/dist/tasks/workflow-manager.js +5 -2
  99. package/dist/template/implementations/entry-server.template.d.ts +17 -0
  100. package/dist/template/implementations/entry-server.template.d.ts.map +1 -0
  101. package/dist/template/implementations/entry-server.template.js +78 -0
  102. package/dist/template/implementations/model.template.d.ts.map +1 -1
  103. package/dist/template/implementations/model.template.js +5 -3
  104. package/dist/template/implementations/queries.template.d.ts +17 -0
  105. package/dist/template/implementations/queries.template.d.ts.map +1 -0
  106. package/dist/template/implementations/queries.template.js +83 -0
  107. package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -1
  108. package/dist/template/implementations/view_enums_select.template.js +34 -20
  109. package/dist/template/implementations/view_form.template.d.ts +2 -1
  110. package/dist/template/implementations/view_form.template.d.ts.map +1 -1
  111. package/dist/template/implementations/view_form.template.js +301 -129
  112. package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -1
  113. package/dist/template/implementations/view_id_async_select.template.js +136 -57
  114. package/dist/template/implementations/view_list.template.d.ts +2 -0
  115. package/dist/template/implementations/view_list.template.d.ts.map +1 -1
  116. package/dist/template/implementations/view_list.template.js +392 -227
  117. package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
  118. package/dist/template/implementations/view_search_input.template.js +46 -30
  119. package/dist/template/zod-converter.d.ts.map +1 -1
  120. package/dist/template/zod-converter.js +2 -2
  121. package/dist/testing/bootstrap.d.ts +28 -0
  122. package/dist/testing/bootstrap.d.ts.map +1 -0
  123. package/dist/testing/bootstrap.js +120 -0
  124. package/dist/testing/fixture-loader.d.ts +21 -0
  125. package/dist/testing/fixture-loader.d.ts.map +1 -0
  126. package/dist/testing/fixture-loader.js +28 -0
  127. package/dist/testing/fixture-manager.d.ts +1 -1
  128. package/dist/testing/fixture-manager.d.ts.map +1 -1
  129. package/dist/testing/fixture-manager.js +7 -7
  130. package/dist/testing/index.d.ts +4 -0
  131. package/dist/testing/index.d.ts.map +1 -0
  132. package/dist/testing/index.js +5 -0
  133. package/dist/testing/naite-vitest-reporter.d.ts +12 -0
  134. package/dist/testing/naite-vitest-reporter.d.ts.map +1 -0
  135. package/dist/testing/naite-vitest-reporter.js +17 -0
  136. package/dist/types/types.d.ts +5 -6
  137. package/dist/types/types.d.ts.map +1 -1
  138. package/dist/types/types.js +7 -8
  139. package/dist/ui/ai-client.d.ts +3 -1
  140. package/dist/ui/ai-client.d.ts.map +1 -1
  141. package/dist/ui/ai-client.js +27 -8
  142. package/dist/ui-web/assets/index-CTYv3qL6.js +92 -0
  143. package/dist/ui-web/index.html +1 -1
  144. package/package.json +43 -20
  145. package/src/ai/agents/agent.ts +38 -19
  146. package/src/api/base-frame.ts +8 -0
  147. package/src/api/caster.ts +6 -1
  148. package/src/api/config.ts +38 -4
  149. package/src/api/decorators.ts +106 -20
  150. package/src/api/index.ts +1 -0
  151. package/src/api/secret.ts +23 -0
  152. package/src/api/sonamu.ts +334 -61
  153. package/src/cache/cache-manager.ts +23 -0
  154. package/src/cache/decorator.ts +116 -0
  155. package/src/cache/drivers.ts +42 -0
  156. package/src/cache/index.ts +16 -0
  157. package/src/cache/types.ts +32 -0
  158. package/src/database/base-model.ts +7 -3
  159. package/src/database/code-generator.ts +3 -1
  160. package/src/database/db.ts +5 -5
  161. package/src/database/knex.ts +34 -0
  162. package/src/database/puri.types.ts +2 -3
  163. package/src/database/upsert-builder.ts +58 -4
  164. package/src/index.ts +4 -0
  165. package/src/logger/category.ts +42 -0
  166. package/src/logger/configure.ts +132 -0
  167. package/src/migration/code-generation.ts +19 -6
  168. package/src/migration/migrator.ts +7 -6
  169. package/src/migration/postgresql-schema-reader.ts +7 -2
  170. package/src/naite/naite.ts +10 -18
  171. package/src/shared/web.shared.ts.txt +1 -1
  172. package/src/ssr/index.ts +13 -0
  173. package/src/ssr/registry.ts +52 -0
  174. package/src/ssr/renderer.ts +105 -0
  175. package/src/ssr/types.ts +20 -0
  176. package/src/syncer/syncer.ts +59 -0
  177. package/src/tasks/decorator.ts +20 -4
  178. package/src/tasks/step-wrapper.ts +14 -5
  179. package/src/tasks/workflow-manager.ts +9 -1
  180. package/src/template/implementations/entry-server.template.ts +81 -0
  181. package/src/template/implementations/model.template.ts +4 -2
  182. package/src/template/implementations/queries.template.ts +111 -0
  183. package/src/template/implementations/view_enums_select.template.ts +33 -19
  184. package/src/template/implementations/view_form.template.ts +324 -145
  185. package/src/template/implementations/view_id_async_select.template.ts +145 -56
  186. package/src/template/implementations/view_list.template.ts +446 -236
  187. package/src/template/implementations/view_search_input.template.ts +45 -29
  188. package/src/template/zod-converter.ts +4 -1
  189. package/src/testing/bootstrap.ts +176 -0
  190. package/src/testing/fixture-loader.ts +28 -0
  191. package/src/testing/fixture-manager.ts +7 -6
  192. package/src/testing/index.ts +3 -0
  193. package/src/testing/naite-vitest-reporter.ts +18 -0
  194. package/src/types/types.ts +4 -5
  195. package/src/ui/ai-client.ts +82 -50
  196. package/dist/template/implementations/view_enums_dropdown.template.d.ts +0 -17
  197. package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +0 -1
  198. package/dist/template/implementations/view_enums_dropdown.template.js +0 -50
  199. package/dist/ui-web/assets/index-B87IyofX.js +0 -92
  200. package/src/template/implementations/view_enums_dropdown.template.ts +0 -53
@@ -0,0 +1,16 @@
1
+ // Cache manager factory
2
+ export { createCacheManager, createTestCacheManager } from "./cache-manager";
3
+
4
+ // Decorator
5
+ export { cache, getCacheManagerRef, setCacheManagerRef } from "./decorator";
6
+
7
+ // Drivers & Store builder
8
+ export {
9
+ drivers,
10
+ fileDriver,
11
+ knexDriver,
12
+ memoryDriver,
13
+ redisBusDriver,
14
+ redisDriver,
15
+ store,
16
+ } from "./drivers";
@@ -0,0 +1,32 @@
1
+ import type { BentoCache } from "bentocache";
2
+ import type { RawCommonOptions } from "bentocache/types";
3
+
4
+ /**
5
+ * 캐시 설정 (sonamu.config.ts에서 사용)
6
+ */
7
+ export type CacheConfig = ConstructorParameters<typeof BentoCache>[0];
8
+
9
+ /**
10
+ * @cache 데코레이터 옵션
11
+ */
12
+ export type CacheDecoratorOptions = {
13
+ /**
14
+ * 캐시 키
15
+ * - 문자열: 고정 키 (args가 자동으로 suffix로 추가됨)
16
+ * - 함수: 인자를 받아 키 생성
17
+ * - 미지정: ModelName.methodName:serializedArgs 형태로 자동 생성
18
+ */
19
+ key?: string | ((...args: unknown[]) => string);
20
+
21
+ /**
22
+ * 사용할 스토어 이름
23
+ * 미지정 시 default 스토어 사용
24
+ */
25
+ store?: string;
26
+ } & RawCommonOptions;
27
+
28
+ /**
29
+ * CacheManager 타입 (BentoCache 확장)
30
+ */
31
+ // biome-ignore lint/suspicious/noExplicitAny: BentoCache의 제네릭 타입을 그대로 사용
32
+ export type CacheManager = BentoCache<any>;
@@ -1,10 +1,11 @@
1
1
  /** biome-ignore-all lint/suspicious/noExplicitAny: Puri의 타입은 개별 모델에서 확정되므로 BaseModel에서는 any를 허용함 */
2
-
2
+ import { getLogger, type Logger } from "@logtape/logtape";
3
3
  import type { Knex } from "knex";
4
4
  import { cloneDeep, group, isObject, omit, set } from "radashi";
5
5
  import type { ListResult } from "..";
6
6
  import { Sonamu } from "../api";
7
7
  import { EntityManager } from "../entity/entity-manager";
8
+ import { convertDomainToCategory } from "../logger/category";
8
9
  import type { DatabaseSchemaExtend, SonamuQueryMode } from "../types/types";
9
10
  import { getJoinTables, getTableNamesFromWhere } from "../utils/sql-parser";
10
11
  import { chunk } from "../utils/utils";
@@ -33,12 +34,15 @@ export class BaseModelClass<
33
34
  TSubsetQueries extends Record<TSubsetKey, PuriSubsetFn> = never,
34
35
  TLoaderQueries extends PuriLoaderQueries<TSubsetKey> = never,
35
36
  > {
36
- public modelName: string = "Unknown";
37
+ protected readonly logger: Logger;
37
38
 
38
39
  constructor(
40
+ public readonly modelName: string = this.constructor.name,
39
41
  protected subsetQueries?: TSubsetQueries,
40
42
  protected loaderQueries?: TLoaderQueries,
41
- ) {}
43
+ ) {
44
+ this.logger = getLogger(convertDomainToCategory(this.modelName, "model"));
45
+ }
42
46
 
43
47
  getDB(which: DBPreset): Knex {
44
48
  return DB.getDB(which);
@@ -3,7 +3,7 @@ import { diff } from "radashi";
3
3
  import type { MigrationColumn, MigrationIndex } from "../types/types";
4
4
  import { differenceWith, intersectionBy } from "../utils/utils";
5
5
 
6
- export class CodeGenerator {
6
+ class CodeGeneratorClass {
7
7
  getAlterColumnsTo(entityColumns: MigrationColumn[], dbColumns: MigrationColumn[]) {
8
8
  const columnsTo = {
9
9
  add: [] as MigrationColumn[],
@@ -55,3 +55,5 @@ export class CodeGenerator {
55
55
  return indexesTo;
56
56
  }
57
57
  }
58
+
59
+ export const CodeGenerator = new CodeGeneratorClass();
@@ -1,9 +1,9 @@
1
1
  import { AsyncLocalStorage } from "async_hooks";
2
- import knex, { type Knex } from "knex";
2
+ import type { Knex } from "knex";
3
3
  import { assign } from "radashi";
4
-
5
4
  import { Sonamu } from "../api";
6
5
  import type { DatabaseConfig, SonamuConfig } from "../api/config";
6
+ import { createKnexInstance } from "./knex";
7
7
  import { TransactionContext } from "./transaction-context";
8
8
 
9
9
  /**
@@ -49,7 +49,7 @@ export class DBClass {
49
49
  } else if (this.wdb) {
50
50
  return this.wdb;
51
51
  } else {
52
- this.wdb = knex({
52
+ this.wdb = createKnexInstance({
53
53
  ...dbConfig.test,
54
54
  // 단일 풀
55
55
  pool: {
@@ -65,7 +65,7 @@ export class DBClass {
65
65
 
66
66
  if (!this[instanceName]) {
67
67
  const config = this.getDBConfig(which);
68
- this[instanceName] = knex(config);
68
+ this[instanceName] = createKnexInstance(config);
69
69
  }
70
70
 
71
71
  return this[instanceName];
@@ -112,7 +112,7 @@ export class DBClass {
112
112
  public generateDBConfig(config: SonamuConfig["database"]): SonamuDBConfig {
113
113
  const defaultKnexConfig: Partial<DatabaseConfig> = assign(
114
114
  {
115
- client: "pg",
115
+ client: "postgresql",
116
116
  pool: {
117
117
  min: 1,
118
118
  max: 5,
@@ -0,0 +1,34 @@
1
+ import type { Knex } from "knex";
2
+ import knex from "knex";
3
+
4
+ export function createKnexInstance(config: Knex.Config): Knex {
5
+ config.pool = {
6
+ ...(config.pool ?? {}),
7
+ propagateCreateError: false,
8
+ idleTimeoutMillis: 10000,
9
+ reapIntervalMillis: 1000,
10
+ acquireTimeoutMillis: 30000,
11
+ createTimeoutMillis: 30000,
12
+ afterCreate: ((conn: Knex.Client, done: (err: Error | null, conn: Knex.Client) => void) => {
13
+ conn.on("error", (err: Error) => {
14
+ Object.defineProperty(conn, "__knex__disposed", {
15
+ value: err,
16
+ writable: true,
17
+ configurable: true,
18
+ enumerable: true,
19
+ });
20
+ });
21
+
22
+ done(null, conn);
23
+ }) satisfies Knex.PoolConfig["afterCreate"],
24
+ };
25
+
26
+ const knexInstance = knex(config);
27
+ knexInstance.client.validateConnection = (connection: unknown) => {
28
+ return (
29
+ typeof connection === "object" && connection !== null && !("__knex__disposed" in connection)
30
+ );
31
+ };
32
+
33
+ return knexInstance;
34
+ }
@@ -342,9 +342,8 @@ export type InsertData<T> = Omit<
342
342
  export type InsertResult = Pick<QueryResult<any>, "command" | "rowCount" | "rows" | "oid">;
343
343
 
344
344
  // SubsetQuery를 위한 타입 유틸리티
345
- export type ExtractTTables<T extends Puri<any, any, any>> = T extends Puri<any, infer TTables, any>
346
- ? TTables
347
- : never;
345
+ export type ExtractTTables<T extends Puri<any, any, any>> =
346
+ T extends Puri<any, infer TTables, any> ? TTables : never;
348
347
  export type UnionExtractedTTables<
349
348
  SubsetKey extends string,
350
349
  SubsetQueries extends Record<SubsetKey, PuriSubsetFn>,
@@ -148,8 +148,11 @@ export class UpsertBuilder {
148
148
  rowValue.use ??= "id";
149
149
  table.references.add(`${rowValue.of}.${rowValue.use}`);
150
150
  return [rowKey, rowValue];
151
+ } else if (isArray(rowValue)) {
152
+ // 배열은 그대로 저장
153
+ return [rowKey, rowValue];
151
154
  } else if (typeof rowValue === "object" && !(rowValue instanceof Date)) {
152
- // object인 경우 JSON으로 변환
155
+ // 일반 object인 경우 JSON으로 변환
153
156
  return [rowKey, rowValue === null ? null : JSON.stringify(rowValue)];
154
157
  } else {
155
158
  return [rowKey, rowValue];
@@ -292,11 +295,62 @@ export class UpsertBuilder {
292
295
  // INSERT 모드 - RETURNING 사용
293
296
  resultRows = await wdb.insert(dataForDb).into(tableName).returning(selectFields);
294
297
  } else {
295
- // UPSERT 모드 - onConflict 사용 (unique index 없으면 PK fallback)
296
- const conflictColumns = table.uniqueIndexes[0]?.columns.map((c) => c.name) ?? ["id"];
298
+ // UPSERT 모드 - id 없는 row들의 id를 사전 조회로 채우기
299
+ const rowsWithoutId = dataForDb.filter((row) => !row.id);
300
+
301
+ if (rowsWithoutId.length > 0 && table.uniqueIndexes.length > 0) {
302
+ // 모든 uniqueIndexes로 기존 레코드 조회
303
+ for (const uniqueIndex of table.uniqueIndexes) {
304
+ const columns = uniqueIndex.columns.map((c) => c.name);
305
+
306
+ // 조회할 조건들 추출 (각 row의 unique 컬럼 값들)
307
+ const conditions: unknown[][] = [];
308
+ for (const row of rowsWithoutId) {
309
+ const values = columns.map((col) => row[col]);
310
+ // null이 포함된 조건은 제외 (PostgreSQL UNIQUE는 NULL 무시)
311
+ if (!values.some((v) => v == null)) {
312
+ conditions.push(values);
313
+ }
314
+ }
315
+
316
+ if (conditions.length === 0) continue;
317
+
318
+ // 배치 SELECT
319
+ const existingRows = (await wdb(tableName)
320
+ .whereIn(columns, conditions as Record<string, unknown>[][])
321
+ .select("id", ...columns)) as Record<string, unknown>[];
322
+
323
+ // Map 생성: unique 컬럼 조합 → id
324
+ const existingMap = new Map<string, number>();
325
+ for (const existing of existingRows) {
326
+ const key = columns
327
+ .map((col) => String(existing[col] ?? ""))
328
+ .join("---delimiter---");
329
+ const id = existing.id;
330
+ if (typeof id === "number") {
331
+ existingMap.set(key, id);
332
+ }
333
+ }
334
+
335
+ // id 없는 row들에 매칭되는 id 채우기
336
+ for (const row of rowsWithoutId) {
337
+ if (row.id) continue; // 이미 다른 uniqueIndex에서 채워진 경우 스킵
338
+
339
+ const key = columns.map((col) => String(row[col] ?? "")).join("---delimiter---");
340
+ const existingId = existingMap.get(key);
341
+
342
+ if (existingId) {
343
+ row.id = existingId;
344
+ }
345
+ }
346
+ }
347
+ }
348
+
349
+ // onConflict는 id만 사용 (모든 uniqueIndexes는 이미 사전 조회로 처리됨)
350
+ const conflictColumns = ["id"];
297
351
 
298
352
  const allColumns = Object.keys(dataForDb[0]);
299
- let updateColumns = allColumns.filter((c) => !conflictColumns.includes(c));
353
+ let updateColumns = allColumns.filter((c) => c !== "id");
300
354
 
301
355
  // inherit 옵션 처리 - inherit 컬럼은 update 대상에서 제외
302
356
  if (options?.inherit?.length) {
package/src/index.ts CHANGED
@@ -3,8 +3,11 @@ export * from "./api/config";
3
3
  export type * from "./api/context";
4
4
  export * from "./api/decorators";
5
5
  export * from "./api/sonamu";
6
+ export { cache } from "./cache/decorator";
7
+ export type { CacheDecoratorOptions } from "./cache/types";
6
8
  export * from "./database/base-model";
7
9
  export * from "./database/base-model.types";
10
+ export * from "./database/code-generator";
8
11
  export * from "./database/db";
9
12
  export * from "./database/puri";
10
13
  export * from "./database/puri.types";
@@ -15,6 +18,7 @@ export * from "./entity/entity";
15
18
  export * from "./entity/entity-manager";
16
19
  export * from "./exceptions/error-handler";
17
20
  export * from "./exceptions/so-exceptions";
21
+ export * from "./migration/code-generation";
18
22
  export * from "./migration/migration-set";
19
23
  export * from "./migration/migrator";
20
24
  export * from "./migration/postgresql-schema-reader";
@@ -0,0 +1,42 @@
1
+ // NOTE: import * as inflection 혹은 import { underscore } 사용 시 오류 발생.
2
+ import inflection from "inflection";
3
+ import { asArray } from "../utils/model";
4
+
5
+ // 두 카테고리가 동일한지 확인
6
+ export function isSameCategory(
7
+ categoryA: readonly string[],
8
+ categoryB: string | string[],
9
+ ): boolean {
10
+ const categoryBArr = asArray(categoryB);
11
+ if (categoryA.length !== categoryBArr.length) {
12
+ return false;
13
+ }
14
+
15
+ return categoryA.every((category, index) => categoryBArr[index] === category);
16
+ }
17
+
18
+ // 로깅 카테고리를 지정합니다.
19
+ // 예: SomeAgentClass -> ["sonamu", "agent", "some-agent"]
20
+ // SomeLongWorkflow -> ["sonamu", "workflow", "some-long-workflow"]
21
+ export function convertDomainToCategory(
22
+ name: string,
23
+ type: "model" | "frame" | "agent" | "workflow",
24
+ ): readonly string[] {
25
+ const compareItems = ["class"];
26
+ if (type !== "workflow") {
27
+ compareItems.push(type);
28
+ }
29
+
30
+ // SomeAgentClass -> some_agent, SomeModelClass -> some_model, SomeFrameClass -> some_frame
31
+ const convertedName = inflection
32
+ .underscore(name)
33
+ .split("_")
34
+ .filter((item) => !compareItems.includes(item))
35
+ .join("-");
36
+
37
+ return ["sonamu", type, convertedName];
38
+ }
39
+
40
+ export function convertNaiteKeyToCategory(key: string): readonly string[] {
41
+ return key.split(".").flatMap((stem) => stem.split(":"));
42
+ }
@@ -0,0 +1,132 @@
1
+ import {
2
+ configure,
3
+ type Filter,
4
+ type FilterLike,
5
+ getConsoleSink,
6
+ type LoggerConfig,
7
+ type LogRecord,
8
+ type Sink,
9
+ type TextFormatter,
10
+ } from "@logtape/logtape";
11
+ import { getPrettyFormatter } from "@logtape/pretty";
12
+ import type { FastifyReply, FastifyRequest } from "fastify";
13
+ import { isSameCategory } from "./category";
14
+
15
+ export type SonamuLoggingOptions<TSinkId extends string, TFilterId extends string> = {
16
+ // fastify 로깅 카테고리 (a.b.c의 형태로 넣으면 [a, b, c]로 들어갑니다.)
17
+ // 기본값은 ["fastify"] 입니다.
18
+ fastifyCategory?: readonly string[];
19
+
20
+ // 각 항목들을 설정할 때 "fastify-console"이 들어갈 경우, 덮어씌워집니다.
21
+ sinks?: Record<TSinkId, Sink>;
22
+ filters?: Record<TFilterId, FilterLike>;
23
+
24
+ // 각 항목을 설정할 때 fastifyCategory에 설정된 카테고리가 있을 경우, 기본 logger 설정은 추가되지 않습니다.
25
+ loggers?: LoggerConfig<TSinkId, TFilterId>[];
26
+ };
27
+
28
+ // fastify에 대한 기본 sink 설정
29
+ function defaultFastifySink(fastifyCategory: readonly string[]): Sink {
30
+ const formatter = ((formatter: TextFormatter, record: LogRecord) => {
31
+ // Fastify API Logger의 경우, 응답 코드와 요청 URL을 추가
32
+ const filterFastify = (request: FastifyRequest, record: LogRecord, responseCode?: number) => {
33
+ if (!request.url.startsWith("/api")) {
34
+ return formatter(record);
35
+ }
36
+
37
+ const lastItem = record.message[record.message.length - 1] as string;
38
+ return formatter({
39
+ ...record,
40
+ message: [
41
+ ...record.message.slice(0, -1),
42
+ `[${request.method}${responseCode ? `:${responseCode}` : ""}] ${request.originalUrl} - ${lastItem}`,
43
+ ],
44
+ });
45
+ };
46
+
47
+ if (!isSameCategory(fastifyCategory, [...record.category])) {
48
+ return formatter(record);
49
+ }
50
+
51
+ if ("req" in record.properties && record.properties.req !== null) {
52
+ const request = record.properties.req as FastifyRequest;
53
+ return filterFastify(request, record);
54
+ }
55
+
56
+ if ("res" in record.properties && record.properties.res !== null) {
57
+ const reply = record.properties.res as FastifyReply;
58
+ return filterFastify(reply.request, record, reply.statusCode);
59
+ }
60
+
61
+ return formatter(record);
62
+ }).bind(
63
+ null,
64
+ getPrettyFormatter({
65
+ timestamp: "time",
66
+ categoryWidth: 20,
67
+ categoryTruncate: "middle",
68
+ }),
69
+ );
70
+
71
+ return getConsoleSink({
72
+ formatter,
73
+ });
74
+ }
75
+
76
+ // fastify에 대한 기본 filter 설정 (/api 경로의 요청만 로깅)
77
+ function defaultFastifyFilter(fastifyCategory: readonly string[]): Filter {
78
+ return (record: LogRecord) => {
79
+ if (!isSameCategory([...fastifyCategory], [...record.category])) {
80
+ return false;
81
+ }
82
+
83
+ if ("req" in record.properties && record.properties.req !== null) {
84
+ const request = record.properties.req as FastifyRequest;
85
+ return request.url.startsWith("/api");
86
+ }
87
+
88
+ if ("res" in record.properties && record.properties.res !== null) {
89
+ const reply = record.properties.res as FastifyReply;
90
+ return reply.request.url.startsWith("/api");
91
+ }
92
+
93
+ return true;
94
+ };
95
+ }
96
+
97
+ // 전체 logtape 설정
98
+ export async function configureLogTape<TSinkId extends string, TFilterId extends string>(
99
+ options: SonamuLoggingOptions<TSinkId, TFilterId>,
100
+ ) {
101
+ const fastifyCategory = options.fastifyCategory ?? ["fastify"];
102
+
103
+ const sinks = {
104
+ "fastify-console": defaultFastifySink(fastifyCategory),
105
+ ...(options.sinks ?? {}),
106
+ } as Record<TSinkId | "fastify-console", Sink>;
107
+
108
+ const filters = {
109
+ "fastify-console": defaultFastifyFilter(fastifyCategory),
110
+ ...(options.filters ?? {}),
111
+ } as Record<TFilterId | "fastify-console", FilterLike>;
112
+
113
+ const loggers: Set<LoggerConfig<TSinkId | "fastify-console", TFilterId | "fastify-console">> =
114
+ new Set(options.loggers ?? []);
115
+
116
+ // logtape의 meta logger 표시를 비활성화
117
+ loggers.add({
118
+ category: ["logtape", "meta"],
119
+ lowestLevel: "fatal",
120
+ });
121
+
122
+ if ([...loggers].every((logger) => !isSameCategory([...fastifyCategory], logger.category))) {
123
+ loggers.add({
124
+ category: [...fastifyCategory],
125
+ sinks: ["fastify-console"],
126
+ lowestLevel: "info",
127
+ filters: ["fastify-console"],
128
+ });
129
+ }
130
+
131
+ return configure({ sinks, filters, loggers: [...loggers], reset: true });
132
+ }
@@ -743,7 +743,12 @@ function getAlterIndexesTo(entityIndexes: MigrationIndex[], dbIndexes: Migration
743
743
  return undefined;
744
744
  }
745
745
  if (key === "columns") {
746
- return (index[key] as MigrationIndex["columns"]).flatMap(identity);
746
+ return (index[key] as MigrationIndex["columns"]).map((col) => {
747
+ return Object.keys(col)
748
+ .sort()
749
+ .map((k) => `${k}=${col[k as keyof typeof col]}`)
750
+ .join("//");
751
+ });
747
752
  }
748
753
  return `${key}=${index[key as keyof MigrationIndex]}`;
749
754
  })
@@ -776,14 +781,22 @@ function genIndexDropDefinition(index: MigrationIndex) {
776
781
  /**
777
782
  * DB 조회 결과와 비교하기 위한 인덱스 기본값 설정
778
783
  */
779
- function setMigrationIndexDefaults(index: MigrationIndex): MigrationIndex {
784
+ export function setMigrationIndexDefaults(index: MigrationIndex): MigrationIndex {
785
+ const supportsOrdering =
786
+ index.type !== "hnsw" && // type이 hnsw면 벡터 인덱스
787
+ index.type !== "ivfflat" && // type이 ivfflat면 벡터 인덱스
788
+ (!index.using || index.using === "btree"); // using 체크
789
+
780
790
  return {
781
791
  ...index,
782
792
  columns: index.columns.map((col) => ({
783
- ...col,
784
- sortOrder: col.sortOrder ?? "ASC",
785
- // sortOrder에 따라 nullsFirst의 default 값 설정
786
- nullsFirst: col.nullsFirst ?? col.sortOrder === "DESC",
793
+ name: col.name,
794
+ ...(supportsOrdering
795
+ ? {
796
+ sortOrder: col.sortOrder ?? "ASC",
797
+ nullsFirst: col.nullsFirst ?? col.sortOrder === "DESC",
798
+ }
799
+ : {}),
787
800
  })),
788
801
  nullsNotDistinct: index.nullsNotDistinct ?? false,
789
802
  using: index.using ?? "btree",
@@ -1,11 +1,12 @@
1
1
  import assert from "assert";
2
2
  import chalk from "chalk";
3
3
  import { mkdir, readdir, unlink, writeFile } from "fs/promises";
4
- import knex, { type Knex } from "knex";
4
+ import type { Knex } from "knex";
5
5
  import path from "path";
6
6
  import { group, sum, unique } from "radashi";
7
7
  import { Sonamu } from "../api";
8
8
  import { DB, type SonamuDBConfig } from "../database/db";
9
+ import { createKnexInstance } from "../database/knex";
9
10
  import { EntityManager } from "../entity/entity-manager";
10
11
  import { ServiceUnavailableException } from "../exceptions/so-exceptions";
11
12
  import { Naite } from "../naite/naite";
@@ -67,7 +68,7 @@ export class Migrator {
67
68
  const statuses = await Promise.all(
68
69
  connKeys.map(async (connKey) => {
69
70
  const knexOptions = Sonamu.dbConfig[connKey];
70
- const tConn = knex(knexOptions);
71
+ const tConn = createKnexInstance(knexOptions);
71
72
 
72
73
  const status = await (async () => {
73
74
  try {
@@ -131,7 +132,7 @@ export class Migrator {
131
132
  return [];
132
133
  }
133
134
 
134
- const compareDBconn = knex(Sonamu.dbConfig[status0conn.connKey]);
135
+ const compareDBconn = createKnexInstance(Sonamu.dbConfig[status0conn.connKey]);
135
136
  const genCodes = await this.compareMigrations(compareDBconn);
136
137
 
137
138
  await compareDBconn.destroy();
@@ -184,7 +185,7 @@ export class Migrator {
184
185
  const conns = await Promise.all(
185
186
  configs.map(async (config) => ({
186
187
  connKey: config.connKey,
187
- knex: knex(config.options),
188
+ knex: createKnexInstance(config.options),
188
189
  })),
189
190
  );
190
191
 
@@ -402,7 +403,7 @@ export class Migrator {
402
403
  }
403
404
 
404
405
  // 기존 Shadow DB 삭제 후 Shadow DB 생성
405
- const tdb = knex(Sonamu.dbConfig.test);
406
+ const tdb = createKnexInstance(Sonamu.dbConfig.test);
406
407
  !isTest() && console.log(chalk.magenta(`${shadowDatabase} 삭제`));
407
408
  await tdb.raw(`DROP DATABASE IF EXISTS ${shadowDatabase}`);
408
409
  await tdb.raw(`
@@ -414,7 +415,7 @@ export class Migrator {
414
415
  await tdb.raw(`CREATE DATABASE ${shadowDatabase} TEMPLATE ${tdbConn.database}`);
415
416
 
416
417
  // Shadow DB에 연결
417
- const sdb = knex({
418
+ const sdb = createKnexInstance({
418
419
  ...Sonamu.dbConfig.test,
419
420
  connection: {
420
421
  ...tdbConn,
@@ -144,9 +144,14 @@ class PostgreSQLSchemaReaderClass {
144
144
  name: indexName,
145
145
  columns: currentIndexes.map((idx) => ({
146
146
  name: idx.column_name,
147
- nullsFirst: idx.nulls_first,
148
- sortOrder: idx.sort_order,
147
+ ...(firstIndex.index_type === "btree"
148
+ ? {
149
+ sortOrder: idx.sort_order,
150
+ nullsFirst: idx.nulls_first,
151
+ }
152
+ : {}),
149
153
  })),
154
+
150
155
  nullsNotDistinct: firstIndex.nulls_not_distinct,
151
156
  using: firstIndex.index_type as "btree" | "hash" | "gin" | "gist" | "pgroonga" | undefined,
152
157
  };
@@ -1,8 +1,10 @@
1
1
  /** biome-ignore-all lint/suspicious/noExplicitAny: Naite는 expect와 호응하도록 any를 허용함 */
2
2
 
3
+ import { getLogger } from "@logtape/logtape";
3
4
  import { get } from "radashi";
4
5
  import { Sonamu } from "../api/sonamu";
5
6
  import type { ComparisonOperator } from "../database/puri.types";
7
+ import { convertNaiteKeyToCategory } from "../logger/category";
6
8
  import { isSerializable } from "../utils/object-utils";
7
9
 
8
10
  // StackFrame 타입
@@ -267,6 +269,14 @@ export class NaiteClass {
267
269
 
268
270
  // 항상 배열로 관리
269
271
  const existing = store.get(name) ?? [];
272
+ getLogger(["naite", ...convertNaiteKeyToCategory(name)]).debug(
273
+ `naite: {name} ${existing.length === 0 ? "is empty state" : `already existing with ${existing.length === 0} entries`}, appending new entry`,
274
+ {
275
+ name,
276
+ value,
277
+ },
278
+ );
279
+
270
280
  store.set(name, [...existing, trace]);
271
281
  } catch {
272
282
  // Context 없는 상황에서 Naite.t 호출
@@ -392,24 +402,6 @@ export class NaiteClass {
392
402
  createStore(): NaiteStore {
393
403
  return new Map<string, NaiteTrace[]>();
394
404
  }
395
-
396
- // 일반 로그 레벨
397
- d(_message: string) {
398
- // TODO: Logger 연결
399
- console.log(`[DEBUG] ${_message}`);
400
- }
401
- i(_message: string) {
402
- // TODO: Logger 연결
403
- console.log(`[INFO] ${_message}`);
404
- }
405
- w(_message: string) {
406
- // TODO: Logger 연결
407
- console.log(`[WARN] ${_message}`);
408
- }
409
- e(_message: string) {
410
- // TODO: Logger 연결
411
- console.log(`[ERROR] ${_message}`);
412
- }
413
405
  }
414
406
 
415
407
  export const Naite = new NaiteClass();
@@ -10,7 +10,7 @@ import qs from "qs";
10
10
  import { type core, z } from "zod";
11
11
 
12
12
  // ISO 8601 및 타임존 포맷의 날짜 문자열을 Date 객체로 변환하는 reviver
13
- function dateReviver(_key: string, value: any): any {
13
+ export function dateReviver(_key: string, value: any): any {
14
14
  if (typeof value === "string") {
15
15
  // ISO 8601 형식: 2024-01-15T09:30:00.000Z 또는 2024-01-15T09:30:00+09:00
16
16
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})?$/;
@@ -0,0 +1,13 @@
1
+ export {
2
+ clearSSRRoutes,
3
+ getSSRRoutes,
4
+ matchSSRRoute,
5
+ registerSSR,
6
+ } from "./registry";
7
+ export { renderSSR } from "./renderer";
8
+ export type {
9
+ PreloadConfig,
10
+ PreloadedData,
11
+ SSRQuery,
12
+ SSRRoute,
13
+ } from "./types";