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
package/src/api/sonamu.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { dispose as logtapeDispose } from "@logtape/logtape";
|
|
1
2
|
import assert from "assert";
|
|
2
3
|
import { AsyncLocalStorage } from "async_hooks";
|
|
3
4
|
import type { FSWatcher } from "chokidar";
|
|
4
5
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
6
|
+
import fs from "fs";
|
|
5
7
|
import type { IncomingMessage, Server, ServerResponse } from "http";
|
|
6
8
|
import os from "os";
|
|
7
9
|
import path from "path";
|
|
8
10
|
import type { ZodObject } from "zod";
|
|
9
11
|
import { createMockSSEFactory, DB, isDaemonServer } from "..";
|
|
12
|
+
import type { CacheConfig, CacheManager } from "../cache/types";
|
|
10
13
|
import type { SonamuDBConfig } from "../database/db";
|
|
11
14
|
import { Naite } from "../naite/naite";
|
|
12
15
|
import type { StorageManager } from "../storage/storage-manager";
|
|
@@ -17,12 +20,8 @@ import type { AbsolutePath } from "../utils/path-utils";
|
|
|
17
20
|
import type { SonamuConfig, SonamuServerOptions, SonamuTaskOptions } from "./config";
|
|
18
21
|
import type { AuthContext, Context, UploadContext } from "./context";
|
|
19
22
|
import type { ExtendedApi } from "./decorators";
|
|
23
|
+
import { getSecrets, type SonamuSecrets } from "./secret";
|
|
20
24
|
|
|
21
|
-
export type SonamuSecrets = {
|
|
22
|
-
anthropic_api_key?: string;
|
|
23
|
-
voyage_api_key?: string;
|
|
24
|
-
openai_api_key?: string;
|
|
25
|
-
};
|
|
26
25
|
class SonamuClass {
|
|
27
26
|
public isInitialized: boolean = false;
|
|
28
27
|
public asyncLocalStorage: AsyncLocalStorage<{
|
|
@@ -109,13 +108,7 @@ class SonamuClass {
|
|
|
109
108
|
return this._config;
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
set secrets(secrets: SonamuSecrets) {
|
|
114
|
-
this._secrets = secrets;
|
|
115
|
-
}
|
|
116
|
-
get secrets(): SonamuSecrets | null {
|
|
117
|
-
return this._secrets;
|
|
118
|
-
}
|
|
111
|
+
public readonly secrets: SonamuSecrets = getSecrets();
|
|
119
112
|
|
|
120
113
|
private _storage: StorageManager | null = null;
|
|
121
114
|
/**
|
|
@@ -128,6 +121,17 @@ class SonamuClass {
|
|
|
128
121
|
return this._storage;
|
|
129
122
|
}
|
|
130
123
|
|
|
124
|
+
private _cache: CacheManager | null = null;
|
|
125
|
+
/**
|
|
126
|
+
* CacheManager 인스턴스 (BentoCache)
|
|
127
|
+
*/
|
|
128
|
+
get cache(): CacheManager {
|
|
129
|
+
if (!this._cache) {
|
|
130
|
+
throw new Error("Cache has not been initialized. Check cache config in sonamu.config.ts.");
|
|
131
|
+
}
|
|
132
|
+
return this._cache;
|
|
133
|
+
}
|
|
134
|
+
|
|
131
135
|
private _workflows: WorkflowManager | null = null;
|
|
132
136
|
get workflows(): WorkflowManager {
|
|
133
137
|
if (this._workflows === null) {
|
|
@@ -167,24 +171,19 @@ class SonamuClass {
|
|
|
167
171
|
const { findApiRootPath } = await import("../utils/utils");
|
|
168
172
|
this.apiRootPath = apiRootPath ?? findApiRootPath();
|
|
169
173
|
|
|
174
|
+
// 설정을 로딩하는 것부터 시작
|
|
170
175
|
const { loadConfig } = await import("./config");
|
|
171
176
|
this.config = await loadConfig(this.apiRootPath);
|
|
172
177
|
// sonamu.config.ts 기본값 설정
|
|
173
|
-
this.config.database.database = this.config.database.database ?? "
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
if (process.env.OPENAI_API_KEY) {
|
|
184
|
-
secrets.openai_api_key = process.env.OPENAI_API_KEY;
|
|
185
|
-
}
|
|
186
|
-
if (Object.keys(secrets).length > 0) {
|
|
187
|
-
this.secrets = secrets;
|
|
178
|
+
this.config.database.database = this.config.database.database ?? "pg";
|
|
179
|
+
this.config.database.defaultOptions.client = this.config.database.database ?? "pg";
|
|
180
|
+
|
|
181
|
+
// 로깅 설정
|
|
182
|
+
const { configureLogTape } = await import("../logger/configure");
|
|
183
|
+
if (this.config.logging !== false) {
|
|
184
|
+
await configureLogTape({
|
|
185
|
+
...this.config.logging,
|
|
186
|
+
});
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
// DB 로드
|
|
@@ -201,6 +200,9 @@ class SonamuClass {
|
|
|
201
200
|
const { EntityManager } = await import("../entity/entity-manager");
|
|
202
201
|
await EntityManager.autoload(doSilent);
|
|
203
202
|
|
|
203
|
+
// Cache 초기화
|
|
204
|
+
await this.initializeCache(this.config.server.cache, forTesting);
|
|
205
|
+
|
|
204
206
|
// 테스팅인 경우 싱크 없이 중단
|
|
205
207
|
if (forTesting) {
|
|
206
208
|
this.isInitialized = true;
|
|
@@ -214,14 +216,14 @@ class SonamuClass {
|
|
|
214
216
|
const { Syncer } = await import("../syncer/syncer");
|
|
215
217
|
this.syncer = new Syncer();
|
|
216
218
|
|
|
217
|
-
// Autoload: Models / Types / APIs
|
|
219
|
+
// Autoload: Models / Types / APIs / Workflows / Templates / SSR Routes
|
|
218
220
|
await this.syncer.autoloadTypes();
|
|
219
221
|
await this.syncer.autoloadModels();
|
|
220
222
|
await this.syncer.autoloadApis();
|
|
221
223
|
await this.syncer.autoloadWorkflows();
|
|
222
|
-
|
|
223
224
|
const { TemplateManager } = await import("../template");
|
|
224
225
|
await TemplateManager.autoload();
|
|
226
|
+
await this.syncer.autoloadSSRRoutes();
|
|
225
227
|
|
|
226
228
|
const { isLocal, isTest } = await import("../utils/controller");
|
|
227
229
|
if (isLocal()) {
|
|
@@ -232,7 +234,6 @@ class SonamuClass {
|
|
|
232
234
|
const { isHotReloadServer } = await import("../utils/controller");
|
|
233
235
|
if (isLocal() && !isTest() && isHotReloadServer() && enableSync) {
|
|
234
236
|
await this.syncer.sync();
|
|
235
|
-
|
|
236
237
|
await this.startWatcher();
|
|
237
238
|
}
|
|
238
239
|
|
|
@@ -249,8 +250,17 @@ class SonamuClass {
|
|
|
249
250
|
}
|
|
250
251
|
|
|
251
252
|
const options = this.config.server;
|
|
252
|
-
const fastify =
|
|
253
|
-
const
|
|
253
|
+
const { default: fastify } = await import("fastify");
|
|
254
|
+
const { getLogTapeFastifyLogger } = await import("@logtape/fastify");
|
|
255
|
+
const server = fastify({
|
|
256
|
+
...options.fastify,
|
|
257
|
+
logger:
|
|
258
|
+
this.config.logging !== false
|
|
259
|
+
? getLogTapeFastifyLogger({
|
|
260
|
+
category: this.config.logging?.fastifyCategory ?? ["fastify"],
|
|
261
|
+
})
|
|
262
|
+
: undefined,
|
|
263
|
+
});
|
|
254
264
|
this.server = server;
|
|
255
265
|
|
|
256
266
|
// Storage 설정 → StorageManager 생성
|
|
@@ -352,49 +362,254 @@ class SonamuClass {
|
|
|
352
362
|
const { sonamuUIApiPlugin } = await import("../ui/api");
|
|
353
363
|
server.register(sonamuUIApiPlugin);
|
|
354
364
|
|
|
355
|
-
//
|
|
365
|
+
// 로컬/프로덕션 환경 분기
|
|
356
366
|
const { isLocal } = await import("../utils/controller");
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
// Sonamu UI
|
|
360
|
-
if (request.url.startsWith("/sonamu-ui")) {
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const found = this.syncer.apis.find(
|
|
365
|
-
(api) =>
|
|
366
|
-
this.config.api.route.prefix + api.path === request.url.split("?")[0] &&
|
|
367
|
-
(api.options.httpMethod ?? "GET") === request.method.toUpperCase(),
|
|
368
|
-
);
|
|
369
|
-
if (found) {
|
|
370
|
-
return this.createApiHandler(found, config)(request, reply);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (request.url.startsWith("/api/")) {
|
|
374
|
-
const { NotFoundException } = await import("../exceptions/so-exceptions");
|
|
375
|
-
throw new NotFoundException(`존재하지 않는 API 접근입니다. ${request.url}`);
|
|
376
|
-
}
|
|
367
|
+
const webPath = path.join(this.appRootPath, "web");
|
|
368
|
+
const hasWeb = fs.existsSync(webPath);
|
|
377
369
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
370
|
+
if (isLocal()) {
|
|
371
|
+
// 로컬 개발 환경: Vite Dev Server + 통합 핸들러
|
|
372
|
+
if (hasWeb) {
|
|
373
|
+
await this.setupViteDevServer(server, webPath, config);
|
|
374
|
+
}
|
|
381
375
|
} else {
|
|
376
|
+
// 프로덕션 환경: 개별 API 라우트 + 정적 파일 서빙
|
|
382
377
|
for (const api of this.syncer.apis) {
|
|
383
|
-
// model
|
|
384
378
|
if (this.syncer.models[api.modelName] === undefined) {
|
|
385
379
|
throw new Error(`정의되지 않은 모델에 접근 ${api.modelName}`);
|
|
386
380
|
}
|
|
387
381
|
|
|
388
|
-
// route
|
|
389
382
|
server.route({
|
|
390
383
|
method: api.options.httpMethod ?? "GET",
|
|
391
384
|
url: this.config.api.route.prefix + api.path,
|
|
392
385
|
handler: this.createApiHandler(api, config),
|
|
393
|
-
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (hasWeb) {
|
|
390
|
+
await this.setupStaticWebServer(server, webPath, config);
|
|
394
391
|
}
|
|
395
392
|
}
|
|
396
393
|
}
|
|
397
394
|
|
|
395
|
+
// biome-ignore lint/suspicious/noExplicitAny: ViteDevServer 타입을 동적으로 로드해야 함
|
|
396
|
+
private viteServer: any = null;
|
|
397
|
+
|
|
398
|
+
private async setupViteDevServer(
|
|
399
|
+
server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
|
|
400
|
+
webPath: string,
|
|
401
|
+
config: SonamuFastifyConfig,
|
|
402
|
+
): Promise<void> {
|
|
403
|
+
// @fastify/middie 등록 (Connect-style middleware 지원)
|
|
404
|
+
await server.register((await import("@fastify/middie")).default);
|
|
405
|
+
|
|
406
|
+
const vite = await import("vite");
|
|
407
|
+
|
|
408
|
+
this.viteServer = await vite.createServer({
|
|
409
|
+
root: webPath,
|
|
410
|
+
server: {
|
|
411
|
+
middlewareMode: true,
|
|
412
|
+
hmr: {
|
|
413
|
+
server: server.server,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
appType: "custom",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Vite middleware 등록 (Vite 에셋 처리)
|
|
420
|
+
server.use((req, res, next) => {
|
|
421
|
+
// API와 Sonamu UI는 Fastify 라우트가 처리하도록 skip
|
|
422
|
+
if (req.url?.startsWith(this.config.api.route.prefix) || req.url?.startsWith("/sonamu-ui")) {
|
|
423
|
+
return next();
|
|
424
|
+
}
|
|
425
|
+
// 나머지는 Vite middleware로 전달
|
|
426
|
+
return this.viteServer.middlewares(req, res, next);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// API 동적 라우팅 (catch-all 전에 등록)
|
|
430
|
+
for (const api of this.syncer.apis) {
|
|
431
|
+
if (this.syncer.models[api.modelName] === undefined) {
|
|
432
|
+
throw new Error(`정의되지 않은 모델에 접근 ${api.modelName}`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
server.route({
|
|
436
|
+
method: api.options.httpMethod ?? "GET",
|
|
437
|
+
url: this.config.api.route.prefix + api.path,
|
|
438
|
+
handler: this.createApiHandler(api, config),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Catch-all 핸들러: SSR + CSR fallback
|
|
443
|
+
server.setNotFoundHandler(async (request, reply) => {
|
|
444
|
+
const url = request.url;
|
|
445
|
+
|
|
446
|
+
// SSR 라우트 체크
|
|
447
|
+
const { matchSSRRoute } = await import("../ssr");
|
|
448
|
+
const match = matchSSRRoute(url);
|
|
449
|
+
|
|
450
|
+
if (match) {
|
|
451
|
+
console.log(`[SSR] Matched route: ${match.route.path}`);
|
|
452
|
+
// SSR 렌더링
|
|
453
|
+
try {
|
|
454
|
+
const { renderSSR } = await import("../ssr");
|
|
455
|
+
const html = await renderSSR(
|
|
456
|
+
url,
|
|
457
|
+
match.route,
|
|
458
|
+
match.params,
|
|
459
|
+
request,
|
|
460
|
+
reply,
|
|
461
|
+
config,
|
|
462
|
+
this.viteServer,
|
|
463
|
+
);
|
|
464
|
+
reply.type("text/html").send(html);
|
|
465
|
+
return;
|
|
466
|
+
} catch (e) {
|
|
467
|
+
console.error("SSR Error:", e);
|
|
468
|
+
console.log("Falling back to CSR...");
|
|
469
|
+
// fallback to CSR (아래 로직 실행)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// CSR fallback
|
|
474
|
+
try {
|
|
475
|
+
const fs = await import("node:fs/promises");
|
|
476
|
+
let template = await fs.readFile(
|
|
477
|
+
path.join(this.viteServer.config.root, "index.html"),
|
|
478
|
+
"utf-8",
|
|
479
|
+
);
|
|
480
|
+
template = await this.viteServer.transformIndexHtml(url, template);
|
|
481
|
+
|
|
482
|
+
reply.type("text/html").send(template);
|
|
483
|
+
return;
|
|
484
|
+
} catch (e) {
|
|
485
|
+
this.viteServer.ssrFixStacktrace(e as Error);
|
|
486
|
+
console.error(e);
|
|
487
|
+
reply.status(500).send((e as Error).message);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// 서버 종료 시 Vite도 종료
|
|
493
|
+
server.addHook("onClose", async () => {
|
|
494
|
+
await this.viteServer.close();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
console.log("✓ Vite dev server integrated");
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private async setupStaticWebServer(
|
|
501
|
+
server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
|
|
502
|
+
_webPath: string,
|
|
503
|
+
config: SonamuFastifyConfig,
|
|
504
|
+
): Promise<void> {
|
|
505
|
+
// 경로 명확화: api/public/web, api/dist/ssr
|
|
506
|
+
const webDistPath = path.join(this.apiRootPath, "public", "web");
|
|
507
|
+
const ssrPath = path.join(this.apiRootPath, "dist", "ssr");
|
|
508
|
+
|
|
509
|
+
if (!fs.existsSync(webDistPath)) {
|
|
510
|
+
console.warn(`⚠ Web dist not found: ${webDistPath}`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// SSR entry 존재 여부 확인
|
|
515
|
+
const ssrEntryPath = path.join(ssrPath, "entry-server.generated.js");
|
|
516
|
+
const ssrAvailable = fs.existsSync(ssrEntryPath);
|
|
517
|
+
|
|
518
|
+
if (!ssrAvailable) {
|
|
519
|
+
console.warn(`⚠ SSR entry not found: ${ssrEntryPath}`);
|
|
520
|
+
console.warn(" SSR will be disabled. Only CSR will work.");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// SSR 라우트 로드 (production에서만, 사용자 프로젝트의 ssr/routes.ts)
|
|
524
|
+
if (ssrAvailable) {
|
|
525
|
+
const ssrRoutesPath = path.join(this.apiRootPath, "dist", "ssr", "routes.js");
|
|
526
|
+
if (fs.existsSync(ssrRoutesPath)) {
|
|
527
|
+
await import(ssrRoutesPath);
|
|
528
|
+
console.log("✓ SSR routes loaded");
|
|
529
|
+
} else {
|
|
530
|
+
console.warn(`⚠ SSR routes not found: ${ssrRoutesPath}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 롤링 업데이트 대응: asset hash 불일치 시 현재 버전 직접 서빙
|
|
535
|
+
server.get("/assets/:filename", async (request, reply) => {
|
|
536
|
+
const requestedFile = (request.params as { filename: string }).filename;
|
|
537
|
+
const assetsDir = path.join(webDistPath, "assets");
|
|
538
|
+
|
|
539
|
+
// index-*.js 또는 index-*.css 요청인 경우
|
|
540
|
+
if (/^index-[a-f0-9]+\.(js|css)$/.test(requestedFile)) {
|
|
541
|
+
const ext = requestedFile.split(".").pop();
|
|
542
|
+
const files = fs.readdirSync(assetsDir);
|
|
543
|
+
const currentFile = files.find((f) => f.startsWith("index-") && f.endsWith(`.${ext}`));
|
|
544
|
+
|
|
545
|
+
if (currentFile) {
|
|
546
|
+
const filePath = path.join(assetsDir, currentFile);
|
|
547
|
+
const content = fs.readFileSync(filePath);
|
|
548
|
+
reply.type(ext === "js" ? "application/javascript" : "text/css");
|
|
549
|
+
reply.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
550
|
+
return reply.send(content);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 일반 파일 서빙
|
|
555
|
+
const filePath = path.join(assetsDir, requestedFile);
|
|
556
|
+
if (fs.existsSync(filePath)) {
|
|
557
|
+
const content = fs.readFileSync(filePath);
|
|
558
|
+
const ext = requestedFile.split(".").pop();
|
|
559
|
+
reply.type(ext === "js" ? "application/javascript" : ext === "css" ? "text/css" : "");
|
|
560
|
+
if (requestedFile.includes("-")) {
|
|
561
|
+
reply.header("Cache-Control", "public, max-age=31536000, immutable");
|
|
562
|
+
}
|
|
563
|
+
return reply.send(content);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
reply.code(404).send("Not found");
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// SPA/SSR 라우팅
|
|
570
|
+
server.setNotFoundHandler(async (request, reply) => {
|
|
571
|
+
// /api, /sonamu-ui는 404 그대로
|
|
572
|
+
if (request.url.startsWith("/api") || request.url.startsWith("/sonamu-ui")) {
|
|
573
|
+
reply.code(404).send({ error: "Not Found" });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const url = request.url;
|
|
578
|
+
|
|
579
|
+
// SSR 라우트 체크
|
|
580
|
+
if (ssrAvailable) {
|
|
581
|
+
const { matchSSRRoute } = await import("../ssr");
|
|
582
|
+
const match = matchSSRRoute(url);
|
|
583
|
+
|
|
584
|
+
if (match) {
|
|
585
|
+
try {
|
|
586
|
+
// renderSSR 재사용 (vite 없이 호출 = production 모드)
|
|
587
|
+
const { renderSSR } = await import("../ssr/renderer");
|
|
588
|
+
const html = await renderSSR(url, match.route, match.params, request, reply, config);
|
|
589
|
+
reply.type("text/html").send(html);
|
|
590
|
+
console.log(`[SSR] Matched route: ${match.route.path}`);
|
|
591
|
+
return;
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.error("[SSR Error]", {
|
|
594
|
+
url: request.url,
|
|
595
|
+
route: match.route.path,
|
|
596
|
+
error: e instanceof Error ? e.message : String(e),
|
|
597
|
+
timestamp: new Date().toISOString(),
|
|
598
|
+
});
|
|
599
|
+
// CSR로 fallback
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// CSR fallback (SSR 실패 시 또는 SSR 라우트가 아닌 경우)
|
|
605
|
+
const indexPath = path.join(webDistPath, "index.html");
|
|
606
|
+
const html = fs.readFileSync(indexPath, "utf-8");
|
|
607
|
+
reply.type("text/html").send(html);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
console.log(`✓ Static web server configured with ${ssrAvailable ? "SSR" : "CSR only"} support`);
|
|
611
|
+
}
|
|
612
|
+
|
|
398
613
|
createApiHandler(
|
|
399
614
|
api: ExtendedApi,
|
|
400
615
|
config: SonamuFastifyConfig,
|
|
@@ -450,6 +665,35 @@ class SonamuClass {
|
|
|
450
665
|
};
|
|
451
666
|
}
|
|
452
667
|
|
|
668
|
+
/**
|
|
669
|
+
* SSR용 API 호출 (HTTP 오버헤드 없이 직접 호출)
|
|
670
|
+
* createApiHandler의 로직을 재사용하되, request 파싱 대신 params 직접 사용
|
|
671
|
+
*/
|
|
672
|
+
async invokeApiForSSR(
|
|
673
|
+
api: ExtendedApi,
|
|
674
|
+
// biome-ignore lint/suspicious/noExplicitAny: SSR에서 다양한 타입의 params를 받아야 함
|
|
675
|
+
params: any[],
|
|
676
|
+
config: SonamuFastifyConfig,
|
|
677
|
+
request: FastifyRequest,
|
|
678
|
+
reply: FastifyReply,
|
|
679
|
+
): Promise<unknown> {
|
|
680
|
+
// Context 생성 (기존 메소드 재사용)
|
|
681
|
+
const context = await this.createContext(config, request, reply);
|
|
682
|
+
|
|
683
|
+
// args 생성: Context 파라미터는 주입, 나머지는 params에서 가져오기
|
|
684
|
+
const { ApiParamType } = await import("../types/types");
|
|
685
|
+
let paramsIndex = 0;
|
|
686
|
+
const args = api.parameters.map((param) => {
|
|
687
|
+
if (ApiParamType.isContext(param.type)) {
|
|
688
|
+
return context;
|
|
689
|
+
}
|
|
690
|
+
return params[paramsIndex++];
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// 모델 메서드 호출 (기존 메서드 재사용)
|
|
694
|
+
return this.invokeModelMethod(api, args, context, reply);
|
|
695
|
+
}
|
|
696
|
+
|
|
453
697
|
async invokeModelMethod(
|
|
454
698
|
api: ExtendedApi,
|
|
455
699
|
args: unknown[],
|
|
@@ -614,6 +858,29 @@ class SonamuClass {
|
|
|
614
858
|
}
|
|
615
859
|
}
|
|
616
860
|
|
|
861
|
+
private async initializeCache(config: CacheConfig | undefined, forTesting: boolean) {
|
|
862
|
+
const { setCacheManagerRef } = await import("../cache/decorator");
|
|
863
|
+
|
|
864
|
+
// 테스트 환경에서 메모리 드라이버 자동 사용
|
|
865
|
+
if (forTesting) {
|
|
866
|
+
const { createTestCacheManager } = await import("../cache/cache-manager");
|
|
867
|
+
this._cache = createTestCacheManager();
|
|
868
|
+
setCacheManagerRef(this._cache);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// 설정이 없으면 캐시 비활성화
|
|
873
|
+
if (!config) {
|
|
874
|
+
setCacheManagerRef(null);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// 설정에 따라 CacheManager 생성
|
|
879
|
+
const { createCacheManager } = await import("../cache/cache-manager");
|
|
880
|
+
this._cache = createCacheManager(config);
|
|
881
|
+
setCacheManagerRef(this._cache);
|
|
882
|
+
}
|
|
883
|
+
|
|
617
884
|
private async initializeWorkflows(options: SonamuTaskOptions | undefined) {
|
|
618
885
|
const { WorkflowManager } = await import("../tasks/workflow-manager");
|
|
619
886
|
// NOTE: @sonamu-kit/tasks 안에선 knex config를 수정하기 때문에 connection이 아닌 config 째로 보냅니다.
|
|
@@ -715,9 +982,15 @@ class SonamuClass {
|
|
|
715
982
|
|
|
716
983
|
async destroy(): Promise<void> {
|
|
717
984
|
const { BaseModel } = await import("../database/base-model");
|
|
985
|
+
// 먼저 처리해야함.
|
|
718
986
|
await BaseModel.destroy();
|
|
719
|
-
await
|
|
720
|
-
|
|
987
|
+
await Promise.allSettled([
|
|
988
|
+
this._workflows?.destroy() ?? Promise.resolve(),
|
|
989
|
+
this._cache?.disconnect() ?? Promise.resolve(),
|
|
990
|
+
this.watcher?.close() ?? Promise.resolve(),
|
|
991
|
+
logtapeDispose(),
|
|
992
|
+
]);
|
|
721
993
|
}
|
|
722
994
|
}
|
|
995
|
+
|
|
723
996
|
export const Sonamu = new SonamuClass();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BentoCache, bentostore } from "bentocache";
|
|
2
|
+
import { memoryDriver } from "bentocache/drivers/memory";
|
|
3
|
+
import type { CacheConfig, CacheManager } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BentoCache 인스턴스를 생성합니다.
|
|
7
|
+
*/
|
|
8
|
+
export function createCacheManager(config: CacheConfig): CacheManager {
|
|
9
|
+
return new BentoCache(config);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 테스트 환경용 기본 CacheManager를 생성합니다.
|
|
14
|
+
* 메모리 드라이버만 사용하는 간단한 설정
|
|
15
|
+
*/
|
|
16
|
+
export function createTestCacheManager(): CacheManager {
|
|
17
|
+
return new BentoCache({
|
|
18
|
+
default: "memory",
|
|
19
|
+
stores: {
|
|
20
|
+
memory: bentostore().useL1Layer(memoryDriver({ maxItems: 1000 })),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { BaseFrameClass } from "../api/base-frame";
|
|
2
|
+
import { BaseModelClass } from "../database/base-model";
|
|
3
|
+
import type { CacheDecoratorOptions, CacheManager } from "./types";
|
|
4
|
+
|
|
5
|
+
type DecoratorTarget = { constructor: { name: string } };
|
|
6
|
+
|
|
7
|
+
// 캐시 매니저 참조 (Sonamu.init에서 설정됨)
|
|
8
|
+
let cacheManagerRef: CacheManager | null = null;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 캐시 매니저 참조 설정 (내부 사용)
|
|
12
|
+
*/
|
|
13
|
+
export function setCacheManagerRef(manager: CacheManager | null): void {
|
|
14
|
+
cacheManagerRef = manager;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 캐시 매니저 참조 가져오기 (내부 사용)
|
|
19
|
+
*/
|
|
20
|
+
export function getCacheManagerRef(): CacheManager | null {
|
|
21
|
+
return cacheManagerRef;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 캐시 키 생성
|
|
26
|
+
*/
|
|
27
|
+
function generateCacheKey(
|
|
28
|
+
modelName: string,
|
|
29
|
+
methodName: string,
|
|
30
|
+
args: unknown[],
|
|
31
|
+
keyOption?: CacheDecoratorOptions["key"],
|
|
32
|
+
): string {
|
|
33
|
+
// 커스텀 키 함수 사용
|
|
34
|
+
if (typeof keyOption === "function") {
|
|
35
|
+
return keyOption(...args);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 문자열 키 + args suffix
|
|
39
|
+
if (typeof keyOption === "string") {
|
|
40
|
+
const argsSuffix = serializeArgs(args);
|
|
41
|
+
return argsSuffix ? `${keyOption}:${argsSuffix}` : keyOption;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 자동 생성: ModelName.methodName:serializedArgs
|
|
45
|
+
const baseKey = `${modelName}.${methodName}`;
|
|
46
|
+
const argsSuffix = serializeArgs(args);
|
|
47
|
+
return argsSuffix ? `${baseKey}:${argsSuffix}` : baseKey;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 인자 직렬화
|
|
52
|
+
*/
|
|
53
|
+
function serializeArgs(args: unknown[]): string {
|
|
54
|
+
if (args.length === 0) return "";
|
|
55
|
+
|
|
56
|
+
// 단일 primitive 값
|
|
57
|
+
if (args.length === 1) {
|
|
58
|
+
const arg = args[0];
|
|
59
|
+
if (arg === null || arg === undefined) return "";
|
|
60
|
+
if (typeof arg === "string" || typeof arg === "number" || typeof arg === "boolean") {
|
|
61
|
+
return String(arg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 복잡한 값은 JSON 직렬화
|
|
66
|
+
try {
|
|
67
|
+
return JSON.stringify(args);
|
|
68
|
+
} catch {
|
|
69
|
+
// 직렬화 실패 시 toString 사용
|
|
70
|
+
return args.map((arg) => String(arg)).join(":");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @cache 데코레이터
|
|
76
|
+
*
|
|
77
|
+
* 메서드의 결과를 캐싱합니다.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* class UserModelClass extends BaseModel {
|
|
81
|
+
* @cache({ ttl: '10m', tags: ['user'] })
|
|
82
|
+
* @api()
|
|
83
|
+
* async findById(id: number) {
|
|
84
|
+
* return this.findOne(['id', id]);
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
*/
|
|
88
|
+
export function cache(options: CacheDecoratorOptions = {}) {
|
|
89
|
+
return (_target: DecoratorTarget, propertyKey: string, descriptor: PropertyDescriptor) => {
|
|
90
|
+
const originalMethod = descriptor.value;
|
|
91
|
+
|
|
92
|
+
descriptor.value = async function (this: BaseModelClass | BaseFrameClass, ...args: unknown[]) {
|
|
93
|
+
const manager = cacheManagerRef;
|
|
94
|
+
|
|
95
|
+
if (!manager) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"CacheManager is not initialized. Please configure 'cache' in sonamu.config.ts.",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const modelName = this instanceof BaseModelClass ? this.modelName : this.frameName;
|
|
102
|
+
const methodName = propertyKey;
|
|
103
|
+
|
|
104
|
+
const cacheKey = generateCacheKey(modelName, methodName, args, options.key);
|
|
105
|
+
const store = options.store ? manager.use(options.store) : manager;
|
|
106
|
+
|
|
107
|
+
return store.getOrSet({
|
|
108
|
+
...options,
|
|
109
|
+
key: cacheKey,
|
|
110
|
+
factory: () => originalMethod.apply(this, args),
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return descriptor;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BentoCache 드라이버 re-export
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* import { drivers, store } from 'sonamu/cache';
|
|
6
|
+
*
|
|
7
|
+
* cache: {
|
|
8
|
+
* stores: {
|
|
9
|
+
* main: store()
|
|
10
|
+
* .useL1Layer(drivers.memory({ maxSize: '100mb' }))
|
|
11
|
+
* .useL2Layer(drivers.redis({ connection }))
|
|
12
|
+
* .useBus(drivers.redisBus({ connection }))
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Store builder
|
|
18
|
+
export { bentostore as store } from "bentocache";
|
|
19
|
+
|
|
20
|
+
import { fileDriver as _fileDriver } from "bentocache/drivers/file";
|
|
21
|
+
import { knexDriver as _knexDriver } from "bentocache/drivers/knex";
|
|
22
|
+
import { memoryDriver as _memoryDriver } from "bentocache/drivers/memory";
|
|
23
|
+
import {
|
|
24
|
+
redisBusDriver as _redisBusDriver,
|
|
25
|
+
redisDriver as _redisDriver,
|
|
26
|
+
} from "bentocache/drivers/redis";
|
|
27
|
+
|
|
28
|
+
// 개별 드라이버 export
|
|
29
|
+
export const memoryDriver = _memoryDriver;
|
|
30
|
+
export const fileDriver = _fileDriver;
|
|
31
|
+
export const redisDriver = _redisDriver;
|
|
32
|
+
export const redisBusDriver = _redisBusDriver;
|
|
33
|
+
export const knexDriver = _knexDriver;
|
|
34
|
+
|
|
35
|
+
// 편의를 위한 drivers 객체
|
|
36
|
+
export const drivers = {
|
|
37
|
+
memory: _memoryDriver,
|
|
38
|
+
file: _fileDriver,
|
|
39
|
+
redis: _redisDriver,
|
|
40
|
+
redisBus: _redisBusDriver,
|
|
41
|
+
knex: _knexDriver,
|
|
42
|
+
};
|