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.
- package/dist/ai/agents/agent.d.ts +6 -1
- package/dist/ai/agents/agent.d.ts.map +1 -1
- package/dist/ai/agents/agent.js +20 -5
- package/dist/api/base-frame.d.ts +4 -0
- package/dist/api/base-frame.d.ts.map +1 -1
- package/dist/api/base-frame.js +9 -1
- package/dist/api/caster.d.ts.map +1 -1
- package/dist/api/caster.js +2 -2
- package/dist/api/config.d.ts +35 -3
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/decorators.d.ts +4 -4
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +80 -18
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -1
- package/dist/api/secret.d.ts +7 -0
- package/dist/api/secret.d.ts.map +1 -0
- package/dist/api/secret.js +17 -0
- package/dist/api/sonamu.d.ts +17 -8
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +265 -47
- package/dist/cache/cache-manager.d.ts +11 -0
- package/dist/cache/cache-manager.d.ts.map +1 -0
- package/dist/cache/cache-manager.js +22 -0
- package/dist/cache/decorator.d.ts +31 -0
- package/dist/cache/decorator.d.ts.map +1 -0
- package/dist/cache/decorator.js +86 -0
- package/dist/cache/drivers.d.ts +33 -0
- package/dist/cache/drivers.d.ts.map +1 -0
- package/dist/cache/drivers.js +36 -0
- package/dist/cache/index.d.ts +4 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +8 -0
- package/dist/cache/types.d.ts +28 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +6 -0
- package/dist/database/base-model.d.ts +4 -2
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +9 -4
- package/dist/database/code-generator.d.ts +3 -1
- package/dist/database/code-generator.d.ts.map +1 -1
- package/dist/database/code-generator.js +3 -2
- package/dist/database/db.d.ts +1 -1
- package/dist/database/db.d.ts.map +1 -1
- package/dist/database/db.js +5 -5
- package/dist/database/knex.d.ts +3 -0
- package/dist/database/knex.d.ts.map +1 -0
- package/dist/database/knex.js +29 -0
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +1 -1
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +49 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/logger/category.d.ts +4 -0
- package/dist/logger/category.d.ts.map +1 -0
- package/dist/logger/category.js +34 -0
- package/dist/logger/configure.d.ts +9 -0
- package/dist/logger/configure.d.ts.map +1 -0
- package/dist/logger/configure.js +115 -0
- package/dist/migration/code-generation.d.ts +5 -1
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +13 -7
- package/dist/migration/migrator.d.ts +1 -1
- package/dist/migration/migrator.d.ts.map +1 -1
- package/dist/migration/migrator.js +7 -7
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +5 -3
- package/dist/naite/naite.d.ts +0 -4
- package/dist/naite/naite.d.ts.map +1 -1
- package/dist/naite/naite.js +11 -19
- package/dist/ssr/index.d.ts +4 -0
- package/dist/ssr/index.d.ts.map +1 -0
- package/dist/ssr/index.js +4 -0
- package/dist/ssr/registry.d.ts +10 -0
- package/dist/ssr/registry.d.ts.map +1 -0
- package/dist/ssr/registry.js +43 -0
- package/dist/ssr/renderer.d.ts +6 -0
- package/dist/ssr/renderer.d.ts.map +1 -0
- package/dist/ssr/renderer.js +70 -0
- package/dist/ssr/types.d.ts +19 -0
- package/dist/ssr/types.d.ts.map +1 -0
- package/dist/ssr/types.js +4 -0
- package/dist/syncer/syncer.d.ts +1 -0
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +58 -1
- package/dist/tasks/decorator.d.ts +1 -0
- package/dist/tasks/decorator.d.ts.map +1 -1
- package/dist/tasks/decorator.js +9 -7
- package/dist/tasks/step-wrapper.d.ts +5 -0
- package/dist/tasks/step-wrapper.d.ts.map +1 -1
- package/dist/tasks/step-wrapper.js +11 -6
- package/dist/tasks/workflow-manager.d.ts +2 -0
- package/dist/tasks/workflow-manager.d.ts.map +1 -1
- package/dist/tasks/workflow-manager.js +5 -2
- package/dist/template/implementations/entry-server.template.d.ts +17 -0
- package/dist/template/implementations/entry-server.template.d.ts.map +1 -0
- package/dist/template/implementations/entry-server.template.js +78 -0
- package/dist/template/implementations/model.template.d.ts.map +1 -1
- package/dist/template/implementations/model.template.js +5 -3
- package/dist/template/implementations/queries.template.d.ts +17 -0
- package/dist/template/implementations/queries.template.d.ts.map +1 -0
- package/dist/template/implementations/queries.template.js +83 -0
- package/dist/template/implementations/view_enums_select.template.d.ts.map +1 -1
- package/dist/template/implementations/view_enums_select.template.js +34 -20
- package/dist/template/implementations/view_form.template.d.ts +2 -1
- package/dist/template/implementations/view_form.template.d.ts.map +1 -1
- package/dist/template/implementations/view_form.template.js +301 -129
- package/dist/template/implementations/view_id_async_select.template.d.ts.map +1 -1
- package/dist/template/implementations/view_id_async_select.template.js +136 -57
- package/dist/template/implementations/view_list.template.d.ts +2 -0
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.js +392 -227
- package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
- package/dist/template/implementations/view_search_input.template.js +46 -30
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +2 -2
- package/dist/testing/bootstrap.d.ts +28 -0
- package/dist/testing/bootstrap.d.ts.map +1 -0
- package/dist/testing/bootstrap.js +120 -0
- package/dist/testing/fixture-loader.d.ts +21 -0
- package/dist/testing/fixture-loader.d.ts.map +1 -0
- package/dist/testing/fixture-loader.js +28 -0
- package/dist/testing/fixture-manager.d.ts +1 -1
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +7 -7
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/naite-vitest-reporter.d.ts +12 -0
- package/dist/testing/naite-vitest-reporter.d.ts.map +1 -0
- package/dist/testing/naite-vitest-reporter.js +17 -0
- package/dist/types/types.d.ts +5 -6
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +7 -8
- package/dist/ui/ai-client.d.ts +3 -1
- package/dist/ui/ai-client.d.ts.map +1 -1
- package/dist/ui/ai-client.js +27 -8
- package/dist/ui-web/assets/index-CTYv3qL6.js +92 -0
- package/dist/ui-web/index.html +1 -1
- package/package.json +43 -20
- package/src/ai/agents/agent.ts +38 -19
- package/src/api/base-frame.ts +8 -0
- package/src/api/caster.ts +6 -1
- package/src/api/config.ts +38 -4
- package/src/api/decorators.ts +106 -20
- package/src/api/index.ts +1 -0
- package/src/api/secret.ts +23 -0
- package/src/api/sonamu.ts +334 -61
- package/src/cache/cache-manager.ts +23 -0
- package/src/cache/decorator.ts +116 -0
- package/src/cache/drivers.ts +42 -0
- package/src/cache/index.ts +16 -0
- package/src/cache/types.ts +32 -0
- package/src/database/base-model.ts +7 -3
- package/src/database/code-generator.ts +3 -1
- package/src/database/db.ts +5 -5
- package/src/database/knex.ts +34 -0
- package/src/database/puri.types.ts +2 -3
- package/src/database/upsert-builder.ts +58 -4
- package/src/index.ts +4 -0
- package/src/logger/category.ts +42 -0
- package/src/logger/configure.ts +132 -0
- package/src/migration/code-generation.ts +19 -6
- package/src/migration/migrator.ts +7 -6
- package/src/migration/postgresql-schema-reader.ts +7 -2
- package/src/naite/naite.ts +10 -18
- package/src/shared/web.shared.ts.txt +1 -1
- package/src/ssr/index.ts +13 -0
- package/src/ssr/registry.ts +52 -0
- package/src/ssr/renderer.ts +105 -0
- package/src/ssr/types.ts +20 -0
- package/src/syncer/syncer.ts +59 -0
- package/src/tasks/decorator.ts +20 -4
- package/src/tasks/step-wrapper.ts +14 -5
- package/src/tasks/workflow-manager.ts +9 -1
- package/src/template/implementations/entry-server.template.ts +81 -0
- package/src/template/implementations/model.template.ts +4 -2
- package/src/template/implementations/queries.template.ts +111 -0
- package/src/template/implementations/view_enums_select.template.ts +33 -19
- package/src/template/implementations/view_form.template.ts +324 -145
- package/src/template/implementations/view_id_async_select.template.ts +145 -56
- package/src/template/implementations/view_list.template.ts +446 -236
- package/src/template/implementations/view_search_input.template.ts +45 -29
- package/src/template/zod-converter.ts +4 -1
- package/src/testing/bootstrap.ts +176 -0
- package/src/testing/fixture-loader.ts +28 -0
- package/src/testing/fixture-manager.ts +7 -6
- package/src/testing/index.ts +3 -0
- package/src/testing/naite-vitest-reporter.ts +18 -0
- package/src/types/types.ts +4 -5
- package/src/ui/ai-client.ts +82 -50
- package/dist/template/implementations/view_enums_dropdown.template.d.ts +0 -17
- package/dist/template/implementations/view_enums_dropdown.template.d.ts.map +0 -1
- package/dist/template/implementations/view_enums_dropdown.template.js +0 -50
- package/dist/ui-web/assets/index-B87IyofX.js +0 -92
- 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
|
-
|
|
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
|
-
|
|
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();
|
package/src/database/db.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "async_hooks";
|
|
2
|
-
import
|
|
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 =
|
|
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] =
|
|
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: "
|
|
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>> =
|
|
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 모드 -
|
|
296
|
-
const
|
|
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) =>
|
|
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"]).
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
148
|
-
|
|
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
|
};
|
package/src/naite/naite.ts
CHANGED
|
@@ -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})?$/;
|
package/src/ssr/index.ts
ADDED