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.
- package/dist/api/config.d.ts +1 -4
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/sonamu.d.ts +2 -0
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +19 -47
- package/dist/bin/cli.js +6 -6
- package/dist/database/base-model.d.ts +1 -1
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +15 -4
- package/dist/database/code-generator.d.ts.map +1 -1
- package/dist/database/code-generator.js +3 -3
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +1 -1
- package/dist/database/puri-wrapper.d.ts +11 -11
- package/dist/database/puri-wrapper.d.ts.map +1 -1
- package/dist/database/puri-wrapper.js +7 -11
- package/dist/database/puri.d.ts +36 -17
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +54 -7
- package/dist/database/puri.types.d.ts +54 -17
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +2 -4
- package/dist/database/puri.types.test-d.js +129 -0
- package/dist/database/upsert-builder.d.ts +16 -10
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +10 -19
- package/dist/entity/entity-manager.d.ts +113 -22
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/entity/entity-manager.js +1 -1
- package/dist/entity/entity.d.ts +34 -0
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +110 -37
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +341 -149
- package/dist/migration/migration-set.d.ts.map +1 -1
- package/dist/migration/migration-set.js +21 -5
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +7 -1
- package/dist/migration/postgresql-schema-reader.d.ts +11 -1
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +111 -10
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +4 -3
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.js +12 -2
- package/dist/template/implementations/generated_sso.template.d.ts +3 -3
- package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
- package/dist/template/implementations/generated_sso.template.js +50 -2
- package/dist/template/implementations/model.template.js +6 -6
- package/dist/template/implementations/model_test.template.js +4 -4
- package/dist/template/implementations/view_enums_dropdown.template.js +2 -2
- package/dist/template/implementations/view_enums_select.template.js +2 -2
- package/dist/template/implementations/view_form.template.d.ts.map +1 -1
- package/dist/template/implementations/view_form.template.js +12 -9
- package/dist/template/implementations/view_id_async_select.template.js +4 -4
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.js +12 -9
- package/dist/template/implementations/view_search_input.template.js +2 -2
- package/dist/template/template.js +2 -2
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +17 -2
- package/dist/testing/fixture-manager.d.ts +2 -1
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +29 -29
- package/dist/types/types.d.ts +593 -68
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +113 -9
- package/dist/vector/chunking.d.ts +25 -0
- package/dist/vector/chunking.d.ts.map +1 -0
- package/dist/vector/chunking.js +97 -0
- package/dist/vector/config.d.ts +12 -0
- package/dist/vector/config.d.ts.map +1 -0
- package/dist/vector/config.js +83 -0
- package/dist/vector/embedding.d.ts +42 -0
- package/dist/vector/embedding.d.ts.map +1 -0
- package/dist/vector/embedding.js +147 -0
- package/dist/vector/types.d.ts +105 -0
- package/dist/vector/types.d.ts.map +1 -0
- package/dist/vector/types.js +5 -0
- package/dist/vector/vector-search.d.ts +47 -0
- package/dist/vector/vector-search.d.ts.map +1 -0
- package/dist/vector/vector-search.js +176 -0
- package/package.json +9 -8
- package/src/api/config.ts +0 -4
- package/src/api/sonamu.ts +21 -36
- package/src/bin/cli.ts +5 -5
- package/src/database/base-model.ts +20 -11
- package/src/database/code-generator.ts +6 -2
- package/src/database/db.ts +1 -0
- package/src/database/puri-wrapper.ts +22 -16
- package/src/database/puri.ts +150 -27
- package/src/database/puri.types.test-d.ts +457 -0
- package/src/database/puri.types.ts +231 -33
- package/src/database/upsert-builder.ts +43 -34
- package/src/entity/entity-manager.ts +2 -2
- package/src/entity/entity.ts +134 -44
- package/src/index.ts +6 -0
- package/src/migration/code-generation.ts +377 -174
- package/src/migration/migration-set.ts +22 -3
- package/src/migration/migrator.ts +6 -0
- package/src/migration/postgresql-schema-reader.ts +121 -21
- package/src/syncer/syncer.ts +3 -2
- package/src/template/implementations/generated.template.ts +51 -9
- package/src/template/implementations/generated_sso.template.ts +71 -2
- package/src/template/implementations/model.template.ts +5 -5
- package/src/template/implementations/model_test.template.ts +3 -3
- package/src/template/implementations/view_enums_dropdown.template.ts +1 -1
- package/src/template/implementations/view_enums_select.template.ts +1 -1
- package/src/template/implementations/view_form.template.ts +11 -8
- package/src/template/implementations/view_id_async_select.template.ts +3 -3
- package/src/template/implementations/view_list.template.ts +11 -8
- package/src/template/implementations/view_search_input.template.ts +1 -1
- package/src/template/template.ts +1 -1
- package/src/template/zod-converter.ts +20 -0
- package/src/testing/fixture-manager.ts +31 -30
- package/src/types/types.ts +226 -48
- package/src/vector/chunking.ts +115 -0
- package/src/vector/config.ts +68 -0
- package/src/vector/embedding.ts +193 -0
- package/src/vector/types.ts +122 -0
- package/src/vector/vector-search.ts +261 -0
- package/dist/template/implementations/view_enums_buttonset.template.d.ts +0 -17
- package/dist/template/implementations/view_enums_buttonset.template.d.ts.map +0 -1
- package/dist/template/implementations/view_enums_buttonset.template.js +0 -31
- package/dist/template/implementations/view_list_columns.template.d.ts +0 -17
- package/dist/template/implementations/view_list_columns.template.d.ts.map +0 -1
- package/dist/template/implementations/view_list_columns.template.js +0 -49
- package/src/template/implementations/view_enums_buttonset.template.ts +0 -34
- package/src/template/implementations/view_list_columns.template.ts +0 -53
|
@@ -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.
|
|
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
|
-
|
|
160
|
-
|
|
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(
|
|
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
|
-
|
|
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의 경우
|
|
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 처리:
|
|
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
|
-
(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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) =>
|
|
42
|
-
|
|
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);
|
package/src/database/db.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
151
|
-
|
|
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
|
|
155
|
-
|
|
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:
|
|
164
|
+
ubUpsertOrInsert<TTable extends TableName<TSchema> & keyof DatabaseSchemaExtend>(
|
|
165
|
+
tableName: TTable,
|
|
160
166
|
mode: "upsert" | "insert",
|
|
161
|
-
|
|
167
|
+
options?: UpsertOptions<TTable>,
|
|
162
168
|
): Promise<number[]> {
|
|
163
|
-
return this.upsertBuilder.upsertOrInsert(this.knex, tableName, mode,
|
|
169
|
+
return this.upsertBuilder.upsertOrInsert(this.knex, tableName, mode, options);
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
ubUpdateBatch(
|