sonamu 0.7.4 → 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 +4 -3
  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 +9 -8
  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 +3 -2
  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
@@ -0,0 +1,176 @@
1
+ import pgvector from "pgvector/knex";
2
+ import { DEFAULT_VECTOR_CONFIG } from "./config.js";
3
+ import { Embedding } from "./embedding.js";
4
+ /**
5
+ * 벡터 검색
6
+ * pgvector를 활용한 벡터 검색 및 하이브리드 검색 지원
7
+ */ export class VectorSearch {
8
+ db;
9
+ config;
10
+ embedding;
11
+ tableName;
12
+ constructor(db, tableName, config = {}){
13
+ this.db = db;
14
+ this.tableName = tableName;
15
+ this.config = {
16
+ voyage: {
17
+ ...DEFAULT_VECTOR_CONFIG.voyage,
18
+ ...config.voyage
19
+ },
20
+ openai: {
21
+ ...DEFAULT_VECTOR_CONFIG.openai,
22
+ ...config.openai
23
+ },
24
+ chunking: {
25
+ ...DEFAULT_VECTOR_CONFIG.chunking,
26
+ ...config.chunking
27
+ },
28
+ search: {
29
+ ...DEFAULT_VECTOR_CONFIG.search,
30
+ ...config.search
31
+ },
32
+ pgvector: {
33
+ ...DEFAULT_VECTOR_CONFIG.pgvector,
34
+ ...config.pgvector
35
+ }
36
+ };
37
+ this.embedding = new Embedding(config);
38
+ }
39
+ /**
40
+ * 단일 항목에 임베딩 저장
41
+ */ async saveEmbedding(id, text, provider, embeddingColumn = "content_embedding") {
42
+ const { embedding } = await this.embedding.embedOne(text, provider, "document");
43
+ await this.db(this.tableName).where("id", id).update({
44
+ [embeddingColumn]: pgvector.toSql(embedding)
45
+ });
46
+ }
47
+ /**
48
+ * 여러 항목에 임베딩 일괄 저장
49
+ */ async saveEmbeddingsBatch(items, provider, embeddingColumn = "content_embedding", onProgress) {
50
+ const texts = items.map((item)=>item.text);
51
+ const embeddings = await this.embedding.embedBatch(texts, provider, "document", onProgress);
52
+ await this.db.transaction(async (trx)=>{
53
+ for(let i = 0; i < items.length; i++){
54
+ await trx(this.tableName).where("id", items[i].id).update({
55
+ [embeddingColumn]: pgvector.toSql(embeddings[i].embedding)
56
+ });
57
+ }
58
+ });
59
+ }
60
+ /**
61
+ * 벡터 검색 (코사인 유사도)
62
+ */ async search(query, provider, options = {}) {
63
+ const { embeddingColumn = "content_embedding", limit = this.config.search.defaultLimit, threshold = this.config.search.similarityThreshold, where } = options;
64
+ // 쿼리 임베딩 (input_type: 'query' 중요!)
65
+ const { embedding } = await this.embedding.embedOne(query, provider, "query");
66
+ // pgvector 세션 설정
67
+ if (this.config.pgvector.iterativeScan) {
68
+ await this.db.raw("SET hnsw.iterative_scan = relaxed_order");
69
+ }
70
+ await this.db.raw(`SET hnsw.ef_search = ${this.config.pgvector.efSearch}`);
71
+ // 코사인 유사도 = 1 - 코사인 거리
72
+ const vectorStr = pgvector.toSql(embedding);
73
+ let queryBuilder = this.db(this.tableName).select("*").select(this.db.raw(`1 - (${embeddingColumn} <=> ?::vector) AS similarity`, [
74
+ vectorStr
75
+ ])).whereNotNull(embeddingColumn).orderByRaw(`${embeddingColumn} <=> ?::vector`, [
76
+ vectorStr
77
+ ]).limit(limit);
78
+ if (where) {
79
+ queryBuilder = queryBuilder.whereRaw(where);
80
+ }
81
+ const rows = await queryBuilder;
82
+ return rows.filter((row)=>row.similarity >= threshold).map((row)=>({
83
+ id: row.id,
84
+ similarity: parseFloat(String(row.similarity)),
85
+ data: row
86
+ }));
87
+ }
88
+ /**
89
+ * 하이브리드 검색 (Vector + FTS)
90
+ */ async hybridSearch(query, provider, options = {}) {
91
+ const { embeddingColumn = "content_embedding", ftsColumn = "content_tsv", limit = this.config.search.defaultLimit, vectorWeight = this.config.search.vectorWeight, ftsWeight = this.config.search.ftsWeight } = options;
92
+ const { embedding } = await this.embedding.embedOne(query, provider, "query");
93
+ const vectorStr = pgvector.toSql(embedding);
94
+ // pgvector 세션 설정
95
+ if (this.config.pgvector.iterativeScan) {
96
+ await this.db.raw("SET hnsw.iterative_scan = relaxed_order");
97
+ }
98
+ await this.db.raw(`SET hnsw.ef_search = ${this.config.pgvector.efSearch}`);
99
+ const sql = `
100
+ WITH vector_search AS (
101
+ SELECT
102
+ id,
103
+ ROW_NUMBER() OVER (ORDER BY ${embeddingColumn} <=> ?::vector) AS rank
104
+ FROM ${this.tableName}
105
+ WHERE ${embeddingColumn} IS NOT NULL
106
+ ORDER BY ${embeddingColumn} <=> ?::vector
107
+ LIMIT 50
108
+ ),
109
+ fts_search AS (
110
+ SELECT
111
+ id,
112
+ ROW_NUMBER() OVER (ORDER BY ts_rank(${ftsColumn}, query) DESC) AS rank
113
+ FROM ${this.tableName}, plainto_tsquery('simple', ?) query
114
+ WHERE ${ftsColumn} @@ query
115
+ LIMIT 50
116
+ ),
117
+ combined AS (
118
+ SELECT
119
+ COALESCE(v.id, f.id) AS id,
120
+ COALESCE(1.0 / (60 + v.rank), 0) AS vector_score,
121
+ COALESCE(1.0 / (60 + f.rank), 0) AS fts_score
122
+ FROM vector_search v
123
+ FULL OUTER JOIN fts_search f ON v.id = f.id
124
+ )
125
+ SELECT
126
+ t.*,
127
+ c.vector_score,
128
+ c.fts_score,
129
+ (c.vector_score * ? + c.fts_score * ?) AS similarity
130
+ FROM combined c
131
+ JOIN ${this.tableName} t ON c.id = t.id
132
+ ORDER BY similarity DESC
133
+ LIMIT ?
134
+ `;
135
+ const { rows } = await this.db.raw(sql, [
136
+ vectorStr,
137
+ vectorStr,
138
+ query,
139
+ vectorWeight,
140
+ ftsWeight,
141
+ limit
142
+ ]);
143
+ return rows.map((row)=>({
144
+ id: row.id,
145
+ similarity: parseFloat(String(row.similarity)),
146
+ vectorScore: parseFloat(String(row.vector_score)),
147
+ ftsScore: parseFloat(String(row.fts_score)),
148
+ data: row
149
+ }));
150
+ }
151
+ /**
152
+ * 임베딩 현황 조회
153
+ */ async getEmbeddingStatus(embeddingColumn = "content_embedding") {
154
+ const result = await this.db(this.tableName).count("* as total").count(`${embeddingColumn} as with_embedding`).first();
155
+ const total = parseInt(String(result?.total ?? 0), 10);
156
+ const withEmbedding = parseInt(String(result?.with_embedding ?? 0), 10);
157
+ return {
158
+ total,
159
+ withEmbedding,
160
+ withoutEmbedding: total - withEmbedding
161
+ };
162
+ }
163
+ /**
164
+ * 임베딩이 없는 항목 ID 조회
165
+ */ async getItemsWithoutEmbedding(embeddingColumn = "content_embedding", limit = 100) {
166
+ const rows = await this.db(this.tableName).select("id").whereNull(embeddingColumn).orderBy("id").limit(limit);
167
+ return rows.map((row)=>row.id);
168
+ }
169
+ /**
170
+ * Embedding 인스턴스 반환 (고급 사용)
171
+ */ getEmbedding() {
172
+ return this.embedding;
173
+ }
174
+ }
175
+
176
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy92ZWN0b3IvdmVjdG9yLXNlYXJjaC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IEtuZXggfSBmcm9tIFwia25leFwiO1xuaW1wb3J0IHBndmVjdG9yIGZyb20gXCJwZ3ZlY3Rvci9rbmV4XCI7XG5pbXBvcnQgeyBERUZBVUxUX1ZFQ1RPUl9DT05GSUcgfSBmcm9tIFwiLi9jb25maWdcIjtcbmltcG9ydCB7IEVtYmVkZGluZyB9IGZyb20gXCIuL2VtYmVkZGluZ1wiO1xuaW1wb3J0IHR5cGUge1xuICBFbWJlZGRpbmdJdGVtLFxuICBFbWJlZGRpbmdQcm92aWRlcixcbiAgSHlicmlkU2VhcmNoT3B0aW9ucyxcbiAgSHlicmlkU2VhcmNoUmVzdWx0LFxuICBQcm9ncmVzc0NhbGxiYWNrLFxuICBWZWN0b3JDb25maWcsXG4gIFZlY3RvclNlYXJjaE9wdGlvbnMsXG4gIFZlY3RvclNlYXJjaFJlc3VsdCxcbn0gZnJvbSBcIi4vdHlwZXNcIjtcblxuLyoqXG4gKiDrsqHthLAg6rKA7IOJXG4gKiBwZ3ZlY3RvcuulvCDtmZzsmqntlZwg67Kh7YSwIOqygOyDiSDrsI8g7ZWY7J2067iM66as65OcIOqygOyDiSDsp4Dsm5BcbiAqL1xuZXhwb3J0IGNsYXNzIFZlY3RvclNlYXJjaDxUID0gUmVjb3JkPHN0cmluZywgdW5rbm93bj4+IHtcbiAgcHJpdmF0ZSBkYjogS25leDtcbiAgcHJpdmF0ZSBjb25maWc6IFZlY3RvckNvbmZpZztcbiAgcHJpdmF0ZSBlbWJlZGRpbmc6IEVtYmVkZGluZztcbiAgcHJpdmF0ZSB0YWJsZU5hbWU6IHN0cmluZztcblxuICBjb25zdHJ1Y3RvcihkYjogS25leCwgdGFibGVOYW1lOiBzdHJpbmcsIGNvbmZpZzogUGFydGlhbDxWZWN0b3JDb25maWc+ID0ge30pIHtcbiAgICB0aGlzLmRiID0gZGI7XG4gICAgdGhpcy50YWJsZU5hbWUgPSB0YWJsZU5hbWU7XG4gICAgdGhpcy5jb25maWcgPSB7XG4gICAgICB2b3lhZ2U6IHsgLi4uREVGQVVMVF9WRUNUT1JfQ09ORklHLnZveWFnZSwgLi4uY29uZmlnLnZveWFnZSB9LFxuICAgICAgb3BlbmFpOiB7IC4uLkRFRkFVTFRfVkVDVE9SX0NPTkZJRy5vcGVuYWksIC4uLmNvbmZpZy5vcGVuYWkgfSxcbiAgICAgIGNodW5raW5nOiB7IC4uLkRFRkFVTFRfVkVDVE9SX0NPTkZJRy5jaHVua2luZywgLi4uY29uZmlnLmNodW5raW5nIH0sXG4gICAgICBzZWFyY2g6IHsgLi4uREVGQVVMVF9WRUNUT1JfQ09ORklHLnNlYXJjaCwgLi4uY29uZmlnLnNlYXJjaCB9LFxuICAgICAgcGd2ZWN0b3I6IHsgLi4uREVGQVVMVF9WRUNUT1JfQ09ORklHLnBndmVjdG9yLCAuLi5jb25maWcucGd2ZWN0b3IgfSxcbiAgICB9O1xuICAgIHRoaXMuZW1iZWRkaW5nID0gbmV3IEVtYmVkZGluZyhjb25maWcpO1xuICB9XG5cbiAgLyoqXG4gICAqIOuLqOydvCDtla3rqqnsl5Ag7J6E67Kg65SpIOyggOyepVxuICAgKi9cbiAgYXN5bmMgc2F2ZUVtYmVkZGluZyhcbiAgICBpZDogbnVtYmVyLFxuICAgIHRleHQ6IHN0cmluZyxcbiAgICBwcm92aWRlcjogRW1iZWRkaW5nUHJvdmlkZXIsXG4gICAgZW1iZWRkaW5nQ29sdW1uOiBzdHJpbmcgPSBcImNvbnRlbnRfZW1iZWRkaW5nXCIsXG4gICk6IFByb21pc2U8dm9pZD4ge1xuICAgIGNvbnN0IHsgZW1iZWRkaW5nIH0gPSBhd2FpdCB0aGlzLmVtYmVkZGluZy5lbWJlZE9uZSh0ZXh0LCBwcm92aWRlciwgXCJkb2N1bWVudFwiKTtcblxuICAgIGF3YWl0IHRoaXMuZGIodGhpcy50YWJsZU5hbWUpXG4gICAgICAud2hlcmUoXCJpZFwiLCBpZClcbiAgICAgIC51cGRhdGUoe1xuICAgICAgICBbZW1iZWRkaW5nQ29sdW1uXTogcGd2ZWN0b3IudG9TcWwoZW1iZWRkaW5nKSxcbiAgICAgIH0pO1xuICB9XG5cbiAgLyoqXG4gICAqIOyXrOufrCDtla3rqqnsl5Ag7J6E67Kg65SpIOydvOq0hCDsoIDsnqVcbiAgICovXG4gIGFzeW5jIHNhdmVFbWJlZGRpbmdzQmF0Y2goXG4gICAgaXRlbXM6IEVtYmVkZGluZ0l0ZW1bXSxcbiAgICBwcm92aWRlcjogRW1iZWRkaW5nUHJvdmlkZXIsXG4gICAgZW1iZWRkaW5nQ29sdW1uOiBzdHJpbmcgPSBcImNvbnRlbnRfZW1iZWRkaW5nXCIsXG4gICAgb25Qcm9ncmVzcz86IFByb2dyZXNzQ2FsbGJhY2ssXG4gICk6IFByb21pc2U8dm9pZD4ge1xuICAgIGNvbnN0IHRleHRzID0gaXRlbXMubWFwKChpdGVtKSA9PiBpdGVtLnRleHQpO1xuICAgIGNvbnN0IGVtYmVkZGluZ3MgPSBhd2FpdCB0aGlzLmVtYmVkZGluZy5lbWJlZEJhdGNoKHRleHRzLCBwcm92aWRlciwgXCJkb2N1bWVudFwiLCBvblByb2dyZXNzKTtcblxuICAgIGF3YWl0IHRoaXMuZGIudHJhbnNhY3Rpb24oYXN5bmMgKHRyeCkgPT4ge1xuICAgICAgZm9yIChsZXQgaSA9IDA7IGkgPCBpdGVtcy5sZW5ndGg7IGkrKykge1xuICAgICAgICBhd2FpdCB0cngodGhpcy50YWJsZU5hbWUpXG4gICAgICAgICAgLndoZXJlKFwiaWRcIiwgaXRlbXNbaV0uaWQpXG4gICAgICAgICAgLnVwZGF0ZSh7XG4gICAgICAgICAgICBbZW1iZWRkaW5nQ29sdW1uXTogcGd2ZWN0b3IudG9TcWwoZW1iZWRkaW5nc1tpXS5lbWJlZGRpbmcpLFxuICAgICAgICAgIH0pO1xuICAgICAgfVxuICAgIH0pO1xuICB9XG5cbiAgLyoqXG4gICAqIOuyoe2EsCDqsoDsg4kgKOy9lOyCrOyduCDsnKDsgqzrj4QpXG4gICAqL1xuICBhc3luYyBzZWFyY2goXG4gICAgcXVlcnk6IHN0cmluZyxcbiAgICBwcm92aWRlcjogRW1iZWRkaW5nUHJvdmlkZXIsXG4gICAgb3B0aW9uczogVmVjdG9yU2VhcmNoT3B0aW9ucyA9IHt9LFxuICApOiBQcm9taXNlPFZlY3RvclNlYXJjaFJlc3VsdDxUPltdPiB7XG4gICAgY29uc3Qge1xuICAgICAgZW1iZWRkaW5nQ29sdW1uID0gXCJjb250ZW50X2VtYmVkZGluZ1wiLFxuICAgICAgbGltaXQgPSB0aGlzLmNvbmZpZy5zZWFyY2guZGVmYXVsdExpbWl0LFxuICAgICAgdGhyZXNob2xkID0gdGhpcy5jb25maWcuc2VhcmNoLnNpbWlsYXJpdHlUaHJlc2hvbGQsXG4gICAgICB3aGVyZSxcbiAgICB9ID0gb3B0aW9ucztcblxuICAgIC8vIOy/vOumrCDsnoTrsqDrlKkgKGlucHV0X3R5cGU6ICdxdWVyeScg7KSR7JqUISlcbiAgICBjb25zdCB7IGVtYmVkZGluZyB9ID0gYXdhaXQgdGhpcy5lbWJlZGRpbmcuZW1iZWRPbmUocXVlcnksIHByb3ZpZGVyLCBcInF1ZXJ5XCIpO1xuXG4gICAgLy8gcGd2ZWN0b3Ig7IS47IWYIOyEpOyglVxuICAgIGlmICh0aGlzLmNvbmZpZy5wZ3ZlY3Rvci5pdGVyYXRpdmVTY2FuKSB7XG4gICAgICBhd2FpdCB0aGlzLmRiLnJhdyhcIlNFVCBobnN3Lml0ZXJhdGl2ZV9zY2FuID0gcmVsYXhlZF9vcmRlclwiKTtcbiAgICB9XG4gICAgYXdhaXQgdGhpcy5kYi5yYXcoYFNFVCBobnN3LmVmX3NlYXJjaCA9ICR7dGhpcy5jb25maWcucGd2ZWN0b3IuZWZTZWFyY2h9YCk7XG5cbiAgICAvLyDsvZTsgqzsnbgg7Jyg7IKs64+EID0gMSAtIOy9lOyCrOyduCDqsbDrpqxcbiAgICBjb25zdCB2ZWN0b3JTdHIgPSBwZ3ZlY3Rvci50b1NxbChlbWJlZGRpbmcpO1xuICAgIGxldCBxdWVyeUJ1aWxkZXIgPSB0aGlzLmRiKHRoaXMudGFibGVOYW1lKVxuICAgICAgLnNlbGVjdChcIipcIilcbiAgICAgIC5zZWxlY3QodGhpcy5kYi5yYXcoYDEgLSAoJHtlbWJlZGRpbmdDb2x1bW59IDw9PiA/Ojp2ZWN0b3IpIEFTIHNpbWlsYXJpdHlgLCBbdmVjdG9yU3RyXSkpXG4gICAgICAud2hlcmVOb3ROdWxsKGVtYmVkZGluZ0NvbHVtbilcbiAgICAgIC5vcmRlckJ5UmF3KGAke2VtYmVkZGluZ0NvbHVtbn0gPD0+ID86OnZlY3RvcmAsIFt2ZWN0b3JTdHJdKVxuICAgICAgLmxpbWl0KGxpbWl0KTtcblxuICAgIGlmICh3aGVyZSkge1xuICAgICAgcXVlcnlCdWlsZGVyID0gcXVlcnlCdWlsZGVyLndoZXJlUmF3KHdoZXJlKTtcbiAgICB9XG5cbiAgICBjb25zdCByb3dzID0gYXdhaXQgcXVlcnlCdWlsZGVyO1xuXG4gICAgcmV0dXJuIHJvd3NcbiAgICAgIC5maWx0ZXIoKHJvdzogeyBzaW1pbGFyaXR5OiBudW1iZXIgfSkgPT4gcm93LnNpbWlsYXJpdHkgPj0gdGhyZXNob2xkKVxuICAgICAgLm1hcCgocm93OiBUICYgeyBzaW1pbGFyaXR5OiBudW1iZXIgfSkgPT4gKHtcbiAgICAgICAgaWQ6IChyb3cgYXMgdW5rbm93biBhcyB7IGlkOiBudW1iZXIgfSkuaWQsXG4gICAgICAgIHNpbWlsYXJpdHk6IHBhcnNlRmxvYXQoU3RyaW5nKHJvdy5zaW1pbGFyaXR5KSksXG4gICAgICAgIGRhdGE6IHJvdyBhcyBULFxuICAgICAgfSkpO1xuICB9XG5cbiAgLyoqXG4gICAqIO2VmOydtOu4jOumrOuTnCDqsoDsg4kgKFZlY3RvciArIEZUUylcbiAgICovXG4gIGFzeW5jIGh5YnJpZFNlYXJjaChcbiAgICBxdWVyeTogc3RyaW5nLFxuICAgIHByb3ZpZGVyOiBFbWJlZGRpbmdQcm92aWRlcixcbiAgICBvcHRpb25zOiBIeWJyaWRTZWFyY2hPcHRpb25zID0ge30sXG4gICk6IFByb21pc2U8SHlicmlkU2VhcmNoUmVzdWx0PFQ+W10+IHtcbiAgICBjb25zdCB7XG4gICAgICBlbWJlZGRpbmdDb2x1bW4gPSBcImNvbnRlbnRfZW1iZWRkaW5nXCIsXG4gICAgICBmdHNDb2x1bW4gPSBcImNvbnRlbnRfdHN2XCIsXG4gICAgICBsaW1pdCA9IHRoaXMuY29uZmlnLnNlYXJjaC5kZWZhdWx0TGltaXQsXG4gICAgICB2ZWN0b3JXZWlnaHQgPSB0aGlzLmNvbmZpZy5zZWFyY2gudmVjdG9yV2VpZ2h0LFxuICAgICAgZnRzV2VpZ2h0ID0gdGhpcy5jb25maWcuc2VhcmNoLmZ0c1dlaWdodCxcbiAgICB9ID0gb3B0aW9ucztcblxuICAgIGNvbnN0IHsgZW1iZWRkaW5nIH0gPSBhd2FpdCB0aGlzLmVtYmVkZGluZy5lbWJlZE9uZShxdWVyeSwgcHJvdmlkZXIsIFwicXVlcnlcIik7XG4gICAgY29uc3QgdmVjdG9yU3RyID0gcGd2ZWN0b3IudG9TcWwoZW1iZWRkaW5nKTtcblxuICAgIC8vIHBndmVjdG9yIOyEuOyFmCDshKTsoJVcbiAgICBpZiAodGhpcy5jb25maWcucGd2ZWN0b3IuaXRlcmF0aXZlU2Nhbikge1xuICAgICAgYXdhaXQgdGhpcy5kYi5yYXcoXCJTRVQgaG5zdy5pdGVyYXRpdmVfc2NhbiA9IHJlbGF4ZWRfb3JkZXJcIik7XG4gICAgfVxuICAgIGF3YWl0IHRoaXMuZGIucmF3KGBTRVQgaG5zdy5lZl9zZWFyY2ggPSAke3RoaXMuY29uZmlnLnBndmVjdG9yLmVmU2VhcmNofWApO1xuXG4gICAgY29uc3Qgc3FsID0gYFxuICAgICAgV0lUSCB2ZWN0b3Jfc2VhcmNoIEFTIChcbiAgICAgICAgU0VMRUNUXG4gICAgICAgICAgaWQsXG4gICAgICAgICAgUk9XX05VTUJFUigpIE9WRVIgKE9SREVSIEJZICR7ZW1iZWRkaW5nQ29sdW1ufSA8PT4gPzo6dmVjdG9yKSBBUyByYW5rXG4gICAgICAgIEZST00gJHt0aGlzLnRhYmxlTmFtZX1cbiAgICAgICAgV0hFUkUgJHtlbWJlZGRpbmdDb2x1bW59IElTIE5PVCBOVUxMXG4gICAgICAgIE9SREVSIEJZICR7ZW1iZWRkaW5nQ29sdW1ufSA8PT4gPzo6dmVjdG9yXG4gICAgICAgIExJTUlUIDUwXG4gICAgICApLFxuICAgICAgZnRzX3NlYXJjaCBBUyAoXG4gICAgICAgIFNFTEVDVFxuICAgICAgICAgIGlkLFxuICAgICAgICAgIFJPV19OVU1CRVIoKSBPVkVSIChPUkRFUiBCWSB0c19yYW5rKCR7ZnRzQ29sdW1ufSwgcXVlcnkpIERFU0MpIEFTIHJhbmtcbiAgICAgICAgRlJPTSAke3RoaXMudGFibGVOYW1lfSwgcGxhaW50b190c3F1ZXJ5KCdzaW1wbGUnLCA/KSBxdWVyeVxuICAgICAgICBXSEVSRSAke2Z0c0NvbHVtbn0gQEAgcXVlcnlcbiAgICAgICAgTElNSVQgNTBcbiAgICAgICksXG4gICAgICBjb21iaW5lZCBBUyAoXG4gICAgICAgIFNFTEVDVFxuICAgICAgICAgIENPQUxFU0NFKHYuaWQsIGYuaWQpIEFTIGlkLFxuICAgICAgICAgIENPQUxFU0NFKDEuMCAvICg2MCArIHYucmFuayksIDApIEFTIHZlY3Rvcl9zY29yZSxcbiAgICAgICAgICBDT0FMRVNDRSgxLjAgLyAoNjAgKyBmLnJhbmspLCAwKSBBUyBmdHNfc2NvcmVcbiAgICAgICAgRlJPTSB2ZWN0b3Jfc2VhcmNoIHZcbiAgICAgICAgRlVMTCBPVVRFUiBKT0lOIGZ0c19zZWFyY2ggZiBPTiB2LmlkID0gZi5pZFxuICAgICAgKVxuICAgICAgU0VMRUNUXG4gICAgICAgIHQuKixcbiAgICAgICAgYy52ZWN0b3Jfc2NvcmUsXG4gICAgICAgIGMuZnRzX3Njb3JlLFxuICAgICAgICAoYy52ZWN0b3Jfc2NvcmUgKiA/ICsgYy5mdHNfc2NvcmUgKiA/KSBBUyBzaW1pbGFyaXR5XG4gICAgICBGUk9NIGNvbWJpbmVkIGNcbiAgICAgIEpPSU4gJHt0aGlzLnRhYmxlTmFtZX0gdCBPTiBjLmlkID0gdC5pZFxuICAgICAgT1JERVIgQlkgc2ltaWxhcml0eSBERVNDXG4gICAgICBMSU1JVCA/XG4gICAgYDtcblxuICAgIGNvbnN0IHsgcm93cyB9ID0gYXdhaXQgdGhpcy5kYi5yYXcoc3FsLCBbXG4gICAgICB2ZWN0b3JTdHIsXG4gICAgICB2ZWN0b3JTdHIsXG4gICAgICBxdWVyeSxcbiAgICAgIHZlY3RvcldlaWdodCxcbiAgICAgIGZ0c1dlaWdodCxcbiAgICAgIGxpbWl0LFxuICAgIF0pO1xuXG4gICAgcmV0dXJuIHJvd3MubWFwKFxuICAgICAgKFxuICAgICAgICByb3c6IFQgJiB7XG4gICAgICAgICAgc2ltaWxhcml0eTogbnVtYmVyO1xuICAgICAgICAgIHZlY3Rvcl9zY29yZTogbnVtYmVyO1xuICAgICAgICAgIGZ0c19zY29yZTogbnVtYmVyO1xuICAgICAgICB9LFxuICAgICAgKSA9PiAoe1xuICAgICAgICBpZDogKHJvdyBhcyB1bmtub3duIGFzIHsgaWQ6IG51bWJlciB9KS5pZCxcbiAgICAgICAgc2ltaWxhcml0eTogcGFyc2VGbG9hdChTdHJpbmcocm93LnNpbWlsYXJpdHkpKSxcbiAgICAgICAgdmVjdG9yU2NvcmU6IHBhcnNlRmxvYXQoU3RyaW5nKHJvdy52ZWN0b3Jfc2NvcmUpKSxcbiAgICAgICAgZnRzU2NvcmU6IHBhcnNlRmxvYXQoU3RyaW5nKHJvdy5mdHNfc2NvcmUpKSxcbiAgICAgICAgZGF0YTogcm93IGFzIFQsXG4gICAgICB9KSxcbiAgICApO1xuICB9XG5cbiAgLyoqXG4gICAqIOyehOuyoOuUqSDtmITtmakg7KGw7ZqMXG4gICAqL1xuICBhc3luYyBnZXRFbWJlZGRpbmdTdGF0dXMoZW1iZWRkaW5nQ29sdW1uOiBzdHJpbmcgPSBcImNvbnRlbnRfZW1iZWRkaW5nXCIpOiBQcm9taXNlPHtcbiAgICB0b3RhbDogbnVtYmVyO1xuICAgIHdpdGhFbWJlZGRpbmc6IG51bWJlcjtcbiAgICB3aXRob3V0RW1iZWRkaW5nOiBudW1iZXI7XG4gIH0+IHtcbiAgICBjb25zdCByZXN1bHQgPSBhd2FpdCB0aGlzLmRiKHRoaXMudGFibGVOYW1lKVxuICAgICAgLmNvdW50KFwiKiBhcyB0b3RhbFwiKVxuICAgICAgLmNvdW50KGAke2VtYmVkZGluZ0NvbHVtbn0gYXMgd2l0aF9lbWJlZGRpbmdgKVxuICAgICAgLmZpcnN0KCk7XG5cbiAgICBjb25zdCB0b3RhbCA9IHBhcnNlSW50KFN0cmluZyhyZXN1bHQ/LnRvdGFsID8/IDApLCAxMCk7XG4gICAgY29uc3Qgd2l0aEVtYmVkZGluZyA9IHBhcnNlSW50KFN0cmluZyhyZXN1bHQ/LndpdGhfZW1iZWRkaW5nID8/IDApLCAxMCk7XG5cbiAgICByZXR1cm4ge1xuICAgICAgdG90YWwsXG4gICAgICB3aXRoRW1iZWRkaW5nLFxuICAgICAgd2l0aG91dEVtYmVkZGluZzogdG90YWwgLSB3aXRoRW1iZWRkaW5nLFxuICAgIH07XG4gIH1cblxuICAvKipcbiAgICog7J6E67Kg65Sp7J20IOyXhuuKlCDtla3rqqkgSUQg7KGw7ZqMXG4gICAqL1xuICBhc3luYyBnZXRJdGVtc1dpdGhvdXRFbWJlZGRpbmcoXG4gICAgZW1iZWRkaW5nQ29sdW1uOiBzdHJpbmcgPSBcImNvbnRlbnRfZW1iZWRkaW5nXCIsXG4gICAgbGltaXQ6IG51bWJlciA9IDEwMCxcbiAgKTogUHJvbWlzZTxudW1iZXJbXT4ge1xuICAgIGNvbnN0IHJvd3MgPSBhd2FpdCB0aGlzLmRiKHRoaXMudGFibGVOYW1lKVxuICAgICAgLnNlbGVjdChcImlkXCIpXG4gICAgICAud2hlcmVOdWxsKGVtYmVkZGluZ0NvbHVtbilcbiAgICAgIC5vcmRlckJ5KFwiaWRcIilcbiAgICAgIC5saW1pdChsaW1pdCk7XG5cbiAgICByZXR1cm4gcm93cy5tYXAoKHJvdzogeyBpZDogbnVtYmVyIH0pID0+IHJvdy5pZCk7XG4gIH1cblxuICAvKipcbiAgICogRW1iZWRkaW5nIOyduOyKpO2EtOyKpCDrsJjtmZggKOqzoOq4iSDsgqzsmqkpXG4gICAqL1xuICBnZXRFbWJlZGRpbmcoKTogRW1iZWRkaW5nIHtcbiAgICByZXR1cm4gdGhpcy5lbWJlZGRpbmc7XG4gIH1cbn1cbiJdLCJuYW1lcyI6WyJwZ3ZlY3RvciIsIkRFRkFVTFRfVkVDVE9SX0NPTkZJRyIsIkVtYmVkZGluZyIsIlZlY3RvclNlYXJjaCIsImRiIiwiY29uZmlnIiwiZW1iZWRkaW5nIiwidGFibGVOYW1lIiwidm95YWdlIiwib3BlbmFpIiwiY2h1bmtpbmciLCJzZWFyY2giLCJzYXZlRW1iZWRkaW5nIiwiaWQiLCJ0ZXh0IiwicHJvdmlkZXIiLCJlbWJlZGRpbmdDb2x1bW4iLCJlbWJlZE9uZSIsIndoZXJlIiwidXBkYXRlIiwidG9TcWwiLCJzYXZlRW1iZWRkaW5nc0JhdGNoIiwiaXRlbXMiLCJvblByb2dyZXNzIiwidGV4dHMiLCJtYXAiLCJpdGVtIiwiZW1iZWRkaW5ncyIsImVtYmVkQmF0Y2giLCJ0cmFuc2FjdGlvbiIsInRyeCIsImkiLCJsZW5ndGgiLCJxdWVyeSIsIm9wdGlvbnMiLCJsaW1pdCIsImRlZmF1bHRMaW1pdCIsInRocmVzaG9sZCIsInNpbWlsYXJpdHlUaHJlc2hvbGQiLCJpdGVyYXRpdmVTY2FuIiwicmF3IiwiZWZTZWFyY2giLCJ2ZWN0b3JTdHIiLCJxdWVyeUJ1aWxkZXIiLCJzZWxlY3QiLCJ3aGVyZU5vdE51bGwiLCJvcmRlckJ5UmF3Iiwid2hlcmVSYXciLCJyb3dzIiwiZmlsdGVyIiwicm93Iiwic2ltaWxhcml0eSIsInBhcnNlRmxvYXQiLCJTdHJpbmciLCJkYXRhIiwiaHlicmlkU2VhcmNoIiwiZnRzQ29sdW1uIiwidmVjdG9yV2VpZ2h0IiwiZnRzV2VpZ2h0Iiwic3FsIiwidmVjdG9yU2NvcmUiLCJ2ZWN0b3Jfc2NvcmUiLCJmdHNTY29yZSIsImZ0c19zY29yZSIsImdldEVtYmVkZGluZ1N0YXR1cyIsInJlc3VsdCIsImNvdW50IiwiZmlyc3QiLCJ0b3RhbCIsInBhcnNlSW50Iiwid2l0aEVtYmVkZGluZyIsIndpdGhfZW1iZWRkaW5nIiwid2l0aG91dEVtYmVkZGluZyIsImdldEl0ZW1zV2l0aG91dEVtYmVkZGluZyIsIndoZXJlTnVsbCIsIm9yZGVyQnkiLCJnZXRFbWJlZGRpbmciXSwibWFwcGluZ3MiOiJBQUNBLE9BQU9BLGNBQWMsZ0JBQWdCO0FBQ3JDLFNBQVNDLHFCQUFxQixRQUFRLGNBQVc7QUFDakQsU0FBU0MsU0FBUyxRQUFRLGlCQUFjO0FBWXhDOzs7Q0FHQyxHQUNELE9BQU8sTUFBTUM7SUFDSEMsR0FBUztJQUNUQyxPQUFxQjtJQUNyQkMsVUFBcUI7SUFDckJDLFVBQWtCO0lBRTFCLFlBQVlILEVBQVEsRUFBRUcsU0FB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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonamu",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "Sonamu — TypeScript Fullstack API Framework",
5
5
  "keywords": [
6
6
  "typescript",
@@ -70,15 +70,16 @@
70
70
  "minimatch": "^10.0.3",
71
71
  "node-sql-parser": "^5.2.0",
72
72
  "pg": "^8.16.3",
73
+ "pgvector": "^0.2.1",
73
74
  "prompts": "^2.4.2",
74
75
  "qs": "^6.11.0",
75
76
  "radashi": "^12.2.0",
76
77
  "tsicli": "^1.0.5",
77
78
  "vitest": "^4.0.10",
78
79
  "zod": "^4.1.12",
80
+ "@sonamu-kit/hmr-runner": "^0.1.1",
79
81
  "@sonamu-kit/ts-loader": "^2.1.3",
80
- "@sonamu-kit/hmr-hook": "^0.4.1",
81
- "@sonamu-kit/hmr-runner": "^0.1.1"
82
+ "@sonamu-kit/hmr-hook": "^0.4.1"
82
83
  },
83
84
  "devDependencies": {
84
85
  "@biomejs/biome": "^2.3.7",
@@ -94,15 +95,15 @@
94
95
  "typescript": "^5.9.3"
95
96
  },
96
97
  "peerDependencies": {
98
+ "@ai-sdk/openai": "^3.0.0-beta.75",
99
+ "@ai-sdk/provider": "^3.0.0-beta.22",
100
+ "@ai-sdk/provider-utils": "^4.0.0-beta.40",
97
101
  "@swc/cli": "^0.7.8",
98
102
  "@swc/core": "^1.13.5",
103
+ "ai": "^6.0.0-beta.138",
99
104
  "fastify": "^4.23.2",
100
105
  "knex": "^3.1.0",
101
- "typescript": "^5.9.3",
102
- "@ai-sdk/openai": "^3.0.0-beta.75",
103
- "@ai-sdk/provider": "^3.0.0-beta.22",
104
- "@ai-sdk/provider-utils": "^4.0.0-beta.40",
105
- "ai": "^6.0.0-beta.138"
106
+ "typescript": "^5.9.3"
106
107
  },
107
108
  "peerDependenciesMeta": {
108
109
  "@ai-sdk/openai": {
package/src/api/config.ts CHANGED
@@ -97,10 +97,6 @@ export type SonamuConfigExport =
97
97
  | (() => SonamuConfig)
98
98
  | (() => Promise<SonamuConfig>);
99
99
 
100
- export function defineConfig(config: SonamuConfig): Promise<SonamuConfig>;
101
- export function defineConfig(config: Promise<SonamuConfig>): Promise<SonamuConfig>;
102
- export function defineConfig(config: () => SonamuConfig): Promise<SonamuConfig>;
103
- export function defineConfig(config: () => Promise<SonamuConfig>): Promise<SonamuConfig>;
104
100
  export function defineConfig(config: SonamuConfigExport): Promise<SonamuConfig> {
105
101
  if (typeof config === "function") {
106
102
  return Promise.resolve(config());
package/src/api/sonamu.ts CHANGED
@@ -17,6 +17,8 @@ import type { ExtendedApi } from "./decorators";
17
17
 
18
18
  export type SonamuSecrets = {
19
19
  anthropic_api_key?: string;
20
+ voyage_api_key?: string;
21
+ openai_api_key?: string;
20
22
  };
21
23
  class SonamuClass {
22
24
  public isInitialized: boolean = false;
@@ -155,10 +157,19 @@ class SonamuClass {
155
157
  // sonamu.config.ts 기본값 설정
156
158
  this.config.database.database = this.config.database.database ?? "postgresql";
157
159
 
160
+ // API 키 환경변수 로드
161
+ const secrets: SonamuSecrets = {};
158
162
  if (process.env.ANTHROPIC_API_KEY) {
159
- this.secrets = {
160
- anthropic_api_key: process.env.ANTHROPIC_API_KEY,
161
- };
163
+ secrets.anthropic_api_key = process.env.ANTHROPIC_API_KEY;
164
+ }
165
+ if (process.env.VOYAGE_API_KEY) {
166
+ secrets.voyage_api_key = process.env.VOYAGE_API_KEY;
167
+ }
168
+ if (process.env.OPENAI_API_KEY) {
169
+ secrets.openai_api_key = process.env.OPENAI_API_KEY;
170
+ }
171
+ if (Object.keys(secrets).length > 0) {
172
+ this.secrets = secrets;
162
173
  }
163
174
 
164
175
  // DB 로드
@@ -169,16 +180,18 @@ class SonamuClass {
169
180
  console.log(chalk.green("DB Config Loaded!"));
170
181
  }
171
182
 
172
- // 테스팅인 경우 엔티티 로드 & 싱크 없이 중단
183
+ // Entity 로드
184
+ // 테스트에서도 Entity 정보는 필요합니다.
185
+ // upsert가 제대로 작동하려면 entity의 unique index 정보가 필요하기 때문입니다.
186
+ const { EntityManager } = await import("../entity/entity-manager");
187
+ await EntityManager.autoload(doSilent);
188
+
189
+ // 테스팅인 경우 싱크 없이 중단
173
190
  if (forTesting) {
174
191
  this.isInitialized = true;
175
192
  return;
176
193
  }
177
194
 
178
- // Entity 로드
179
- const { EntityManager } = await import("../entity/entity-manager");
180
- await EntityManager.autoload(doSilent);
181
-
182
195
  // Syncer
183
196
  const { Syncer } = await import("../syncer/syncer");
184
197
  this.syncer = new Syncer();
@@ -387,30 +400,6 @@ class SonamuClass {
387
400
  // Content-Type
388
401
  reply.type(api.options.contentType ?? "application/json");
389
402
 
390
- // 캐시
391
- const { cacheKey, cacheTtl, cachedData } = await (async () => {
392
- if (config.cache) {
393
- try {
394
- const cacheKeyRes = config.cache.resolveKey(api.path, reqBody);
395
- if (cacheKeyRes.cache === false) {
396
- return { cacheKey: null, cachedData: null };
397
- }
398
-
399
- const cacheKey = cacheKeyRes.key;
400
- const cacheTtl = cacheKeyRes.ttl;
401
- const cachedData = await config.cache.get(cacheKey);
402
- return { cacheKey, cacheTtl, cachedData };
403
- } catch (e) {
404
- console.error(e);
405
- }
406
- return { cacheKey: null, cachedData: null };
407
- }
408
- return { cacheKey: null, cachedData: null };
409
- })();
410
- if (cachedData !== null) {
411
- return cachedData;
412
- }
413
-
414
403
  // createSSEFactory 함수에 미리 request의 socket과 reply를 바인딩.
415
404
  const { createSSEFactory } = await import("../stream/sse");
416
405
  const createSSE = (<T extends ZodObject>(
@@ -458,10 +447,6 @@ class SonamuClass {
458
447
  );
459
448
  reply.type(api.options.contentType ?? "application/json");
460
449
 
461
- // 캐시 키 있는 경우 갱신 후 저장
462
- if (config.cache && cacheKey) {
463
- await config.cache.put(cacheKey, result, cacheTtl);
464
- }
465
450
  return result;
466
451
  });
467
452
  };
package/src/bin/cli.ts CHANGED
@@ -22,7 +22,7 @@ import { BUILD_DIR, SWC_BUILD_COMMAND, TSC_TYPE_CHECK_COMMAND } from "./build-co
22
22
  let migrator: Migrator;
23
23
 
24
24
  async function bootstrap() {
25
- const notToInit = ["dev", "build", "start"].includes(process.argv[2] ?? "");
25
+ const notToInit = ["dev", "build", "start", "ui"].includes(process.argv[2] ?? "");
26
26
  if (!notToInit) {
27
27
  await Sonamu.init(false, false);
28
28
  }
@@ -451,9 +451,11 @@ async function scaffold_model_test(entityId: string) {
451
451
 
452
452
  async function ui() {
453
453
  try {
454
+ const apiRootPath = findApiRootPath();
455
+
454
456
  // 사용자 프로젝트의 패키지들 중에서 @sonamu-kit/ui를 찾습니다.
455
457
  // 이를 위해서 createRequire를 사용하여 프로젝트 경로 기준으로 resolve합니다.
456
- const projectRequire = createRequire(path.join(Sonamu.apiRootPath, "package.json"));
458
+ const projectRequire = createRequire(path.join(apiRootPath, "package.json"));
457
459
  const uiPackagePath = projectRequire.resolve("@sonamu-kit/ui"); // 없으면 여기서 터져요(MODULE_NOT_FOUND)
458
460
  const uiNodePath = path.join(path.dirname(uiPackagePath), "run-ui.js");
459
461
 
@@ -481,9 +483,7 @@ async function ui() {
481
483
  env: {
482
484
  ...process.env,
483
485
  HOT: "yes",
484
- PROJECT_NAME: Sonamu.config.projectName ?? path.basename(Sonamu.apiRootPath),
485
- API_ROOT_PATH: Sonamu.apiRootPath,
486
- UI_PORT: (Sonamu.config.ui?.port ?? 57000).toString(),
486
+ API_ROOT_PATH: apiRootPath, // UI는 얘만 알면 돼요! 나머지는 얘가 떠서 알아서 할 것임 ㅎ
487
487
  },
488
488
  },
489
489
  );
@@ -306,23 +306,32 @@ export class BaseModelClass<
306
306
  * Flat 레코드를 중첩 객체로 변환
307
307
  *
308
308
  * - `user__name` → `{ user: { name } }`
309
- * - nullable relation의 경우 모든 필드가 null이면 객체 자체를 null로
309
+ * - nullable relation의 경우 id 필드가 null이면 객체 자체를 null로
310
310
  */
311
311
  hydrate<T extends UnknownDBRecord>(rows: T[]): T[] {
312
312
  return rows.map((row: T) => {
313
- // nullable relation 처리: 관련 필드가 전부 null 경우 방지
313
+ // nullable relation 처리: 그룹의 id 필드가 null이면 객체 전체를 null로
314
314
  const nestedKeys = Object.keys(row).filter((key) => key.includes("__"));
315
315
  const groups = Object.groupBy(nestedKeys, (key) => key.split("__")[0]);
316
+
317
+ // id 필드가 null인 그룹 찾기 (예: parent__id가 null이면 parent 그룹 전체가 null)
316
318
  const nullKeys = Object.entries(groups)
317
- .filter(
318
- ([_, data]) =>
319
- data &&
320
- data.length > 1 &&
321
- data.every(
322
- (field) =>
323
- row[field] === null || (Array.isArray(row[field]) && row[field].length === 0),
324
- ),
325
- )
319
+ .filter(([groupKey, fields]) => {
320
+ if (!fields || fields.length === 0) return false;
321
+
322
+ // 그룹의 id 필드 찾기 (예: "parent__id")
323
+ const idField = `${groupKey}__id`;
324
+ if (idField in row) {
325
+ // id 필드가 null이면 객체 전체가 null
326
+ return row[idField] === null;
327
+ }
328
+
329
+ // id 필드가 없으면 기존 로직: 모든 필드가 null인지 확인
330
+ return fields.every(
331
+ (field) =>
332
+ row[field] === null || (Array.isArray(row[field]) && row[field].length === 0),
333
+ );
334
+ })
326
335
  .map(([key]) => key);
327
336
 
328
337
  const hydrated = Object.keys(row).reduce((r, field) => {
@@ -38,8 +38,12 @@ export class CodeGenerator {
38
38
  drop: [] as MigrationIndex[],
39
39
  };
40
40
  const extraIndexes = {
41
- db: diff(dbIndexes, entityIndexes, (col) => [col.type, col.columns.join("-")].join("//")),
42
- entity: diff(entityIndexes, dbIndexes, (col) => [col.type, col.columns.join("-")].join("//")),
41
+ db: diff(dbIndexes, entityIndexes, (col) =>
42
+ [col.type, col.columns.map((c) => c.name).join("-")].join("//"),
43
+ ),
44
+ entity: diff(entityIndexes, dbIndexes, (col) =>
45
+ [col.type, col.columns.map((c) => c.name).join("-")].join("//"),
46
+ ),
43
47
  };
44
48
  if (extraIndexes.entity.length > 0) {
45
49
  indexesTo.add = indexesTo.add.concat(extraIndexes.entity);
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "async_hooks";
2
2
  import knex, { type Knex } from "knex";
3
3
  import { assign } from "radashi";
4
+
4
5
  import { Sonamu } from "../api";
5
6
  import type { DatabaseConfig, SonamuConfig } from "../api/config";
6
7
  import { TransactionContext } from "./transaction-context";
@@ -5,8 +5,8 @@ import type { Knex } from "knex";
5
5
  import type { DatabaseSchemaExtend } from "../types/types";
6
6
  import type { DBPreset } from "./db";
7
7
  import { Puri } from "./puri";
8
- import type { ColumnKeys, OmitMetadataColumns, PuriTable } from "./puri.types";
9
- import type { UBRef, UpsertBuilder } from "./upsert-builder";
8
+ import type { ColumnKeys, OmitInternalTypeKeys, PuriTable } from "./puri.types";
9
+ import type { InsertOnlyOptions, UBRef, UpsertBuilder, UpsertOptions } from "./upsert-builder";
10
10
 
11
11
  type TableName<TSchema extends DatabaseSchemaExtend> = Extract<keyof TSchema, string>;
12
12
 
@@ -32,7 +32,7 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
32
32
  ): Puri<
33
33
  TSchema,
34
34
  Record<TTable, PuriTable<TSchema[TTable]>>,
35
- OmitMetadataColumns<PuriTable<TSchema[TTable]>>
35
+ OmitInternalTypeKeys<PuriTable<TSchema[TTable]>>
36
36
  >;
37
37
  // 테이블명 + Alias로 시작
38
38
  from<TTable extends keyof TSchema, TAlias extends string>(
@@ -42,7 +42,7 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
42
42
  ): Puri<
43
43
  TSchema,
44
44
  Record<TAlias, PuriTable<TSchema[TTable]>>,
45
- OmitMetadataColumns<PuriTable<TSchema[TTable]>>
45
+ OmitInternalTypeKeys<PuriTable<TSchema[TTable]>>
46
46
  >;
47
47
  // 서브쿼리로 시작
48
48
  from<TAlias extends string, TSubResult>(
@@ -52,7 +52,7 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
52
52
  ): Puri<
53
53
  TSchema,
54
54
  Record<TAlias, PuriTable<TSubResult>>,
55
- OmitMetadataColumns<PuriTable<TSubResult>>
55
+ OmitInternalTypeKeys<PuriTable<TSubResult>>
56
56
  >;
57
57
  from(spec: any): any {
58
58
  return new Puri(this.knex, spec);
@@ -64,7 +64,7 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
64
64
  ): Puri<
65
65
  TSchema,
66
66
  Record<TTable, PuriTable<TSchema[TTable]>>,
67
- OmitMetadataColumns<PuriTable<TSchema[TTable]>>
67
+ OmitInternalTypeKeys<PuriTable<TSchema[TTable]>>
68
68
  >;
69
69
  // 테이블명 + Alias로 시작
70
70
  table<TTable extends keyof TSchema, TAlias extends string>(
@@ -74,7 +74,7 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
74
74
  ): Puri<
75
75
  TSchema,
76
76
  Record<TAlias, PuriTable<TSchema[TTable]>>,
77
- OmitMetadataColumns<PuriTable<TSchema[TTable]>>
77
+ OmitInternalTypeKeys<PuriTable<TSchema[TTable]>>
78
78
  >;
79
79
  // 서브쿼리로 시작
80
80
  table<TAlias extends string, TSubResult>(
@@ -84,7 +84,7 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
84
84
  ): Puri<
85
85
  TSchema,
86
86
  Record<TAlias, PuriTable<TSubResult>>,
87
- OmitMetadataColumns<PuriTable<TSubResult>>
87
+ OmitInternalTypeKeys<PuriTable<TSubResult>>
88
88
  >;
89
89
  table(spec: any): any {
90
90
  return new Puri(this.knex, spec);
@@ -147,20 +147,26 @@ export class PuriWrapper<TSchema extends DatabaseSchemaExtend = DatabaseSchemaEx
147
147
  return this.upsertBuilder.register(tableName, row);
148
148
  }
149
149
 
150
- ubUpsert(tableName: TableName<TSchema>, chunkSize?: number): Promise<number[]> {
151
- return this.upsertBuilder.upsert(this.knex, tableName, { chunkSize });
150
+ ubUpsert<TTable extends TableName<TSchema> & keyof DatabaseSchemaExtend>(
151
+ tableName: TTable,
152
+ options?: UpsertOptions<TTable>,
153
+ ): Promise<number[]> {
154
+ return this.upsertBuilder.upsert(this.knex, tableName, options);
152
155
  }
153
156
 
154
- ubInsertOnly(tableName: TableName<TSchema>, chunkSize?: number): Promise<number[]> {
155
- return this.upsertBuilder.insertOnly(this.knex, tableName, chunkSize);
157
+ ubInsertOnly<TTable extends TableName<TSchema> & keyof DatabaseSchemaExtend>(
158
+ tableName: TTable,
159
+ options?: InsertOnlyOptions,
160
+ ): Promise<number[]> {
161
+ return this.upsertBuilder.insertOnly(this.knex, tableName, options);
156
162
  }
157
163
 
158
- ubUpsertOrInsert(
159
- tableName: TableName<TSchema>,
164
+ ubUpsertOrInsert<TTable extends TableName<TSchema> & keyof DatabaseSchemaExtend>(
165
+ tableName: TTable,
160
166
  mode: "upsert" | "insert",
161
- chunkSize?: number,
167
+ options?: UpsertOptions<TTable>,
162
168
  ): Promise<number[]> {
163
- return this.upsertBuilder.upsertOrInsert(this.knex, tableName, mode, { chunkSize });
169
+ return this.upsertBuilder.upsertOrInsert(this.knex, tableName, mode, options);
164
170
  }
165
171
 
166
172
  ubUpdateBatch(