sonamu 0.7.53 → 0.8.0
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 +9 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +1 -1
- package/dist/api/sonamu.d.ts +21 -1
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +159 -65
- package/dist/auth/plugins/entity-definitions/anonymous.d.ts +10 -0
- package/dist/auth/plugins/entity-definitions/anonymous.d.ts.map +1 -0
- package/dist/auth/plugins/entity-definitions/anonymous.js +23 -0
- package/dist/auth/plugins/entity-definitions/api-key.d.ts +9 -0
- package/dist/auth/plugins/entity-definitions/api-key.d.ts.map +1 -0
- package/dist/auth/plugins/entity-definitions/api-key.js +199 -0
- package/dist/auth/plugins/entity-definitions/index.d.ts +6 -0
- package/dist/auth/plugins/entity-definitions/index.d.ts.map +1 -1
- package/dist/auth/plugins/entity-definitions/index.js +20 -2
- package/dist/auth/plugins/entity-definitions/jwt.d.ts +9 -0
- package/dist/auth/plugins/entity-definitions/jwt.d.ts.map +1 -0
- package/dist/auth/plugins/entity-definitions/jwt.js +67 -0
- package/dist/auth/plugins/entity-definitions/organization.d.ts +9 -0
- package/dist/auth/plugins/entity-definitions/organization.d.ts.map +1 -0
- package/dist/auth/plugins/entity-definitions/organization.js +424 -0
- package/dist/auth/plugins/entity-definitions/passkey.d.ts +10 -0
- package/dist/auth/plugins/entity-definitions/passkey.d.ts.map +1 -0
- package/dist/auth/plugins/entity-definitions/passkey.js +129 -0
- package/dist/auth/plugins/entity-definitions/sso.d.ts +10 -0
- package/dist/auth/plugins/entity-definitions/sso.d.ts.map +1 -0
- package/dist/auth/plugins/entity-definitions/sso.js +110 -0
- package/dist/auth/plugins/entity-definitions/types.d.ts +1 -1
- package/dist/auth/plugins/entity-definitions/types.d.ts.map +1 -1
- package/dist/auth/plugins/entity-definitions/types.js +1 -1
- package/dist/auth/plugins/wrappers/admin.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/admin.js +2 -4
- package/dist/auth/plugins/wrappers/anonymous.d.ts +18 -0
- package/dist/auth/plugins/wrappers/anonymous.d.ts.map +1 -0
- package/dist/auth/plugins/wrappers/anonymous.js +26 -0
- package/dist/auth/plugins/wrappers/api-key.d.ts +18 -0
- package/dist/auth/plugins/wrappers/api-key.d.ts.map +1 -0
- package/dist/auth/plugins/wrappers/api-key.js +38 -0
- package/dist/auth/plugins/wrappers/index.d.ts +6 -0
- package/dist/auth/plugins/wrappers/index.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/index.js +7 -1
- package/dist/auth/plugins/wrappers/jwt.d.ts +18 -0
- package/dist/auth/plugins/wrappers/jwt.d.ts.map +1 -0
- package/dist/auth/plugins/wrappers/jwt.js +30 -0
- package/dist/auth/plugins/wrappers/organization.d.ts +18 -0
- package/dist/auth/plugins/wrappers/organization.d.ts.map +1 -0
- package/dist/auth/plugins/wrappers/organization.js +67 -0
- package/dist/auth/plugins/wrappers/passkey.d.ts +18 -0
- package/dist/auth/plugins/wrappers/passkey.d.ts.map +1 -0
- package/dist/auth/plugins/wrappers/passkey.js +32 -0
- package/dist/auth/plugins/wrappers/phone-number.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/phone-number.js +2 -4
- package/dist/auth/plugins/wrappers/sso.d.ts +853 -0
- package/dist/auth/plugins/wrappers/sso.d.ts.map +1 -0
- package/dist/auth/plugins/wrappers/sso.js +36 -0
- package/dist/auth/plugins/wrappers/two-factor.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/two-factor.js +2 -4
- package/dist/auth/plugins/wrappers/username.d.ts.map +1 -1
- package/dist/auth/plugins/wrappers/username.js +2 -4
- package/dist/bin/build-config.d.ts +2 -2
- package/dist/bin/build-config.js +6 -7
- package/dist/bin/cli.js +417 -32
- package/dist/bin/fixture.d.ts +27 -0
- package/dist/bin/fixture.d.ts.map +1 -0
- package/dist/bin/fixture.js +245 -0
- package/dist/cache/decorator.d.ts +4 -3
- package/dist/cache/decorator.d.ts.map +1 -1
- package/dist/cache/decorator.js +5 -4
- package/dist/cone/cone-generator.d.ts +33 -0
- package/dist/cone/cone-generator.d.ts.map +1 -0
- package/dist/cone/cone-generator.js +286 -0
- package/dist/database/_batch_update.d.ts.map +1 -1
- package/dist/database/_batch_update.js +16 -2
- package/dist/database/puri-subset.test-d.js +1 -1
- package/dist/database/puri-subset.types.d.ts +1 -1
- package/dist/database/puri-subset.types.d.ts.map +1 -1
- package/dist/database/puri-subset.types.js +1 -1
- package/dist/database/puri.d.ts +4 -0
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +20 -2
- package/dist/database/upsert-builder.d.ts.map +1 -1
- package/dist/database/upsert-builder.js +19 -3
- package/dist/dict/en.d.ts +15 -0
- package/dist/dict/en.d.ts.map +1 -1
- package/dist/dict/en.js +2 -1
- package/dist/dict/ko.d.ts +15 -0
- package/dist/dict/ko.d.ts.map +1 -1
- package/dist/dict/ko.js +2 -1
- package/dist/dict/rc-keys.d.ts +28 -0
- package/dist/dict/rc-keys.d.ts.map +1 -1
- package/dist/dict/rc-keys.js +31 -1
- package/dist/dict/sd.d.ts.map +1 -1
- package/dist/dict/sd.js +20 -4
- package/dist/entity/entity-manager.d.ts +298 -2
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/entity/entity-manager.js +4 -1
- package/dist/entity/entity-template-cone.d.ts +14 -0
- package/dist/entity/entity-template-cone.d.ts.map +1 -0
- package/dist/entity/entity-template-cone.js +222 -0
- package/dist/entity/entity.d.ts +47 -2
- package/dist/entity/entity.d.ts.map +1 -1
- package/dist/entity/entity.js +161 -14
- package/dist/ssr/renderer.js +3 -3
- package/dist/syncer/api-parser.js +12 -1
- package/dist/syncer/checksum.d.ts +0 -14
- package/dist/syncer/checksum.d.ts.map +1 -1
- package/dist/syncer/checksum.js +1 -23
- package/dist/syncer/syncer-actions.d.ts.map +1 -1
- package/dist/syncer/syncer-actions.js +8 -2
- package/dist/syncer/syncer.d.ts +1 -1
- package/dist/syncer/syncer.d.ts.map +1 -1
- package/dist/syncer/syncer.js +17 -10
- package/dist/tasks/workflow-manager.d.ts +13 -1
- package/dist/tasks/workflow-manager.d.ts.map +1 -1
- package/dist/tasks/workflow-manager.js +18 -1
- package/dist/template/entity-converter.js +4 -4
- package/dist/template/helpers.d.ts +10 -0
- package/dist/template/helpers.d.ts.map +1 -1
- package/dist/template/helpers.js +48 -1
- package/dist/template/implementations/entry-server.template.d.ts +1 -1
- package/dist/template/implementations/entry-server.template.js +7 -2
- package/dist/template/implementations/generated.template.d.ts.map +1 -1
- package/dist/template/implementations/generated.template.js +5 -1
- package/dist/template/implementations/generated_http.template.d.ts +1 -0
- package/dist/template/implementations/generated_http.template.d.ts.map +1 -1
- package/dist/template/implementations/generated_http.template.js +6 -2
- package/dist/template/implementations/generated_sso.template.d.ts.map +1 -1
- package/dist/template/implementations/generated_sso.template.js +29 -8
- package/dist/template/implementations/queries.template.d.ts.map +1 -1
- package/dist/template/implementations/queries.template.js +9 -1
- package/dist/template/implementations/sd.template.d.ts +1 -1
- package/dist/template/implementations/sd.template.d.ts.map +1 -1
- package/dist/template/implementations/sd.template.js +28 -4
- package/dist/template/implementations/services.template.d.ts.map +1 -1
- package/dist/template/implementations/services.template.js +12 -12
- package/dist/template/implementations/view_form.template.d.ts +11 -7
- package/dist/template/implementations/view_form.template.d.ts.map +1 -1
- package/dist/template/implementations/view_form.template.js +97 -87
- package/dist/template/implementations/view_list.template.d.ts +3 -3
- package/dist/template/implementations/view_list.template.d.ts.map +1 -1
- package/dist/template/implementations/view_list.template.js +115 -109
- package/dist/template/implementations/view_search_input.template.d.ts.map +1 -1
- package/dist/template/implementations/view_search_input.template.js +18 -14
- package/dist/template/zod-converter.d.ts.map +1 -1
- package/dist/template/zod-converter.js +95 -7
- package/dist/testing/_relation-graph.js +1 -1
- package/dist/testing/data-explorer.d.ts +61 -0
- package/dist/testing/data-explorer.d.ts.map +1 -0
- package/dist/testing/data-explorer.js +274 -0
- package/dist/testing/faker-mappings.d.ts +20 -0
- package/dist/testing/faker-mappings.d.ts.map +1 -0
- package/dist/testing/faker-mappings.js +421 -0
- package/dist/testing/fixture-generator.d.ts +161 -0
- package/dist/testing/fixture-generator.d.ts.map +1 -0
- package/dist/testing/fixture-generator.js +954 -0
- package/dist/testing/fixture-manager.d.ts +6 -1
- package/dist/testing/fixture-manager.d.ts.map +1 -1
- package/dist/testing/fixture-manager.js +72 -4
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +4 -1
- package/dist/types/types.d.ts +1520 -26
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +136 -22
- package/dist/ui/ai-client.d.ts.map +1 -1
- package/dist/ui/ai-client.js +9 -4
- package/dist/ui/api.d.ts.map +1 -1
- package/dist/ui/api.js +303 -24
- package/dist/ui-web/assets/index-CsUr-_pV.js +254 -0
- package/dist/ui-web/assets/index-T42zzs1K.css +1 -0
- package/dist/ui-web/index.html +2 -2
- package/dist/utils/fs-utils.d.ts +2 -1
- package/dist/utils/fs-utils.d.ts.map +1 -1
- package/dist/utils/fs-utils.js +14 -3
- package/package.json +19 -11
- package/src/api/config.ts +12 -1
- package/src/api/sonamu.ts +179 -65
- package/src/auth/plugins/entity-definitions/anonymous.ts +17 -0
- package/src/auth/plugins/entity-definitions/api-key.ts +93 -0
- package/src/auth/plugins/entity-definitions/index.ts +18 -0
- package/src/auth/plugins/entity-definitions/jwt.ts +35 -0
- package/src/auth/plugins/entity-definitions/organization.ts +215 -0
- package/src/auth/plugins/entity-definitions/passkey.ts +64 -0
- package/src/auth/plugins/entity-definitions/sso.ts +62 -0
- package/src/auth/plugins/entity-definitions/types.ts +11 -1
- package/src/auth/plugins/wrappers/admin.ts +1 -3
- package/src/auth/plugins/wrappers/anonymous.ts +30 -0
- package/src/auth/plugins/wrappers/api-key.ts +42 -0
- package/src/auth/plugins/wrappers/index.ts +6 -0
- package/src/auth/plugins/wrappers/jwt.ts +34 -0
- package/src/auth/plugins/wrappers/organization.ts +73 -0
- package/src/auth/plugins/wrappers/passkey.ts +36 -0
- package/src/auth/plugins/wrappers/phone-number.ts +1 -3
- package/src/auth/plugins/wrappers/sso.ts +40 -0
- package/src/auth/plugins/wrappers/two-factor.ts +1 -3
- package/src/auth/plugins/wrappers/username.ts +1 -3
- package/src/bin/build-config.ts +6 -6
- package/src/bin/cli.ts +452 -31
- package/src/bin/fixture.ts +302 -0
- package/src/cache/decorator.ts +4 -3
- package/src/cone/cone-generator.ts +363 -0
- package/src/database/_batch_update.ts +11 -0
- package/src/database/puri-subset.test-d.ts +13 -13
- package/src/database/puri-subset.types.ts +1 -1
- package/src/database/puri.ts +43 -1
- package/src/database/upsert-builder.ts +16 -2
- package/src/dict/en.ts +1 -0
- package/src/dict/ko.ts +1 -0
- package/src/dict/rc-keys.ts +32 -0
- package/src/dict/sd.ts +23 -3
- package/src/entity/entity-manager.ts +4 -0
- package/src/entity/entity-template-cone.ts +298 -0
- package/src/entity/entity.ts +189 -13
- package/src/shared/app.shared.ts.txt +5 -0
- package/src/shared/web.shared.ts.txt +9 -5
- package/src/skills/project/README.md +21 -0
- package/src/skills/project/architecture.md +373 -0
- package/src/skills/project/business-logic.md +270 -0
- package/src/skills/project/requirements.md +160 -0
- package/src/skills/sonamu/SKILL.md +168 -3
- package/src/skills/sonamu/api.md +102 -0
- package/src/skills/sonamu/database.md +220 -1
- package/src/skills/sonamu/entity-relations.md +89 -1
- package/src/skills/sonamu/fixture-cli.md +501 -0
- package/src/skills/sonamu/frontend.md +214 -0
- package/src/skills/sonamu/i18n.md +95 -0
- package/src/skills/sonamu/model.md +153 -0
- package/src/skills/sonamu/project-init.md +178 -8
- package/src/skills/sonamu/scaffolding.md +112 -0
- package/src/skills/sonamu/subset.md +9 -3
- package/src/skills/sonamu/testing.md +287 -2
- package/src/skills/sonamu/workflow.md +70 -5
- package/src/ssr/renderer.ts +2 -2
- package/src/syncer/api-parser.ts +12 -0
- package/src/syncer/checksum.ts +0 -38
- package/src/syncer/syncer-actions.ts +7 -1
- package/src/syncer/syncer.ts +16 -5
- package/src/tasks/workflow-manager.ts +29 -8
- package/src/template/entity-converter.ts +3 -3
- package/src/template/helpers.ts +49 -0
- package/src/template/implementations/entry-server.template.ts +1 -1
- package/src/template/implementations/generated.template.ts +4 -0
- package/src/template/implementations/generated_http.template.ts +1 -0
- package/src/template/implementations/generated_sso.template.ts +40 -11
- package/src/template/implementations/queries.template.ts +8 -0
- package/src/template/implementations/sd.template.ts +22 -3
- package/src/template/implementations/services.template.ts +11 -10
- package/src/template/implementations/view_form.template.ts +111 -101
- package/src/template/implementations/view_list.template.ts +120 -119
- package/src/template/implementations/view_search_input.template.ts +17 -13
- package/src/template/zod-converter.ts +103 -6
- package/src/testing/_relation-graph.ts +1 -1
- package/src/testing/data-explorer.ts +427 -0
- package/src/testing/faker-mappings.ts +434 -0
- package/src/testing/fixture-generator.ts +1166 -0
- package/src/testing/fixture-manager.ts +91 -6
- package/src/testing/index.ts +3 -0
- package/src/types/types.ts +222 -26
- package/src/ui/ai-client.ts +9 -1
- package/src/ui/api.ts +429 -23
- package/src/utils/fs-utils.ts +14 -1
- package/dist/template/implementations/view_enums_select.template.d.ts +0 -17
- package/dist/template/implementations/view_enums_select.template.d.ts.map +0 -1
- package/dist/template/implementations/view_enums_select.template.js +0 -62
- package/dist/template/implementations/view_id_async_select.template.d.ts +0 -17
- package/dist/template/implementations/view_id_async_select.template.d.ts.map +0 -1
- package/dist/template/implementations/view_id_async_select.template.js +0 -125
- package/dist/ui-web/assets/index-Bd_2AkLb.css +0 -1
- package/dist/ui-web/assets/index-BpSbhQWo.js +0 -225
- package/src/template/implementations/view_enums_select.template.ts +0 -65
- package/src/template/implementations/view_id_async_select.template.ts +0 -139
package/src/api/sonamu.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
DB,
|
|
19
19
|
isDaemonServer,
|
|
20
20
|
merge,
|
|
21
|
+
NotFoundException,
|
|
21
22
|
} from "..";
|
|
22
23
|
import type { CacheConfig, CacheManager } from "../cache/types";
|
|
23
24
|
import { applyCacheHeaders, CachePresets } from "../cache-control/cache-control";
|
|
@@ -25,6 +26,7 @@ import type { CacheControlConfig, CacheControlRequest } from "../cache-control/t
|
|
|
25
26
|
import { toFastifyCompressOption } from "../compress/compress";
|
|
26
27
|
import type { CompressOptions } from "../compress/types";
|
|
27
28
|
import type { SonamuDBConfig } from "../database/db";
|
|
29
|
+
import { SD } from "../dict/sd";
|
|
28
30
|
import type { LocalizedString } from "../dict/types";
|
|
29
31
|
import { Naite } from "../naite/naite";
|
|
30
32
|
import { BufferedFile } from "../storage/buffered-file";
|
|
@@ -379,7 +381,6 @@ class SonamuClass {
|
|
|
379
381
|
server.register(sonamuUIApiPlugin);
|
|
380
382
|
}
|
|
381
383
|
|
|
382
|
-
// 로컬/프로덕션 환경 분기
|
|
383
384
|
const webPath = path.join(this.appRootPath, "web");
|
|
384
385
|
const hasWeb = await exists(webPath);
|
|
385
386
|
|
|
@@ -396,9 +397,13 @@ class SonamuClass {
|
|
|
396
397
|
: undefined;
|
|
397
398
|
|
|
398
399
|
if (isLocal()) {
|
|
399
|
-
// 로컬 개발 환경:
|
|
400
|
-
|
|
401
|
-
|
|
400
|
+
// 로컬 개발 환경: catch-all로 API를 동적 매칭하여 HMR을 지원합니다.
|
|
401
|
+
// SONAMU_DISABLE_INTEGRATED_WEB=yes로 설정하면 dev_api 모드에서 Vite 통합을 비활성화할 수 있습니다.
|
|
402
|
+
const disableIntegratedWeb = process.env.SONAMU_DISABLE_INTEGRATED_WEB === "yes";
|
|
403
|
+
if (hasWeb && !disableIntegratedWeb) {
|
|
404
|
+
await this.setupDevServerWithVite(server, webPath, config);
|
|
405
|
+
} else {
|
|
406
|
+
this.setupDevServer(server, config);
|
|
402
407
|
}
|
|
403
408
|
} else {
|
|
404
409
|
// 프로덕션 환경: 개별 API 라우트 + 정적 파일 서빙
|
|
@@ -415,20 +420,84 @@ class SonamuClass {
|
|
|
415
420
|
});
|
|
416
421
|
}
|
|
417
422
|
|
|
418
|
-
|
|
419
|
-
|
|
423
|
+
// 프로덕션에서는 web 소스(appRoot/web) 유무와 무관하게,
|
|
424
|
+
// api/web-dist 존재 여부를 setupStaticWebServer 내부에서 판단합니다.
|
|
425
|
+
await this.setupStaticWebServer(server, config, globalCompressOptions);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* dev 모드 공통: catch-all에서 syncer.apis를 동적으로 탐색하여 API 요청을 처리합니다.
|
|
431
|
+
* server.route()로 개별 등록하면 handler가 고정되어 HMR이 동작하지 않으므로,
|
|
432
|
+
* 매 요청마다 syncer.apis를 조회하는 이 방식을 사용합니다.
|
|
433
|
+
*
|
|
434
|
+
* 요청이 /api(정확히는 this.config.api.route.prefix)로 시작하지 않는 경우라면 null을 반환하며 끝냅니다.
|
|
435
|
+
*/
|
|
436
|
+
private handleDevApiRequest(
|
|
437
|
+
request: FastifyRequest,
|
|
438
|
+
config: SonamuFastifyConfig,
|
|
439
|
+
): ((request: FastifyRequest, reply: FastifyReply) => Promise<unknown>) | null {
|
|
440
|
+
const url = this.getPathnameFromUrl(request.url);
|
|
441
|
+
const method = request.method;
|
|
442
|
+
|
|
443
|
+
if (!url.startsWith(this.config.api.route.prefix)) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// syncer.apis의 path는 :param 형태를 포함할 수 있으므로 세그먼트 단위로 매칭합니다.
|
|
448
|
+
// 정규식 생성 방식은 path 문자열 내 특수문자(., +, (, [ 등)로 오작동할 수 있어 사용하지 않습니다.
|
|
449
|
+
const matchedApi = this.syncer.apis.find((api) => {
|
|
450
|
+
if (this.syncer.models[api.modelName] === undefined) {
|
|
451
|
+
return false;
|
|
420
452
|
}
|
|
453
|
+
const apiMethod = api.options.httpMethod ?? "GET";
|
|
454
|
+
if (apiMethod !== method) return false;
|
|
455
|
+
|
|
456
|
+
const fullPath = this.config.api.route.prefix + api.path;
|
|
457
|
+
return this.isPathPatternMatch(fullPath, url);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (!matchedApi) {
|
|
461
|
+
throw new NotFoundException(SD("error.api.notFound"));
|
|
421
462
|
}
|
|
463
|
+
|
|
464
|
+
return this.createApiHandler(matchedApi, config);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* dev api 모드: Vite 없이 API 동적 라우팅만 제공합니다.
|
|
469
|
+
* HMR을 위해 catch-all에서 매 요청마다 syncer.apis를 조회합니다.
|
|
470
|
+
*/
|
|
471
|
+
private setupDevServer(
|
|
472
|
+
server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
|
|
473
|
+
config: SonamuFastifyConfig,
|
|
474
|
+
): void {
|
|
475
|
+
server.route({
|
|
476
|
+
method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"],
|
|
477
|
+
url: `${this.config.api.route.prefix}/*`,
|
|
478
|
+
handler: async (request, reply) => {
|
|
479
|
+
const handler = this.handleDevApiRequest(request, config);
|
|
480
|
+
if (handler) {
|
|
481
|
+
return handler(request, reply);
|
|
482
|
+
}
|
|
483
|
+
// 사실 /api로 시작하지 않는 요청은 여기에 들어오지도 않을 거라 이 라인은 도달 불가능입니다만,
|
|
484
|
+
// 안전빵으로 남겨놓습니다.
|
|
485
|
+
throw new NotFoundException(SD("error.api.notFound"));
|
|
486
|
+
},
|
|
487
|
+
});
|
|
422
488
|
}
|
|
423
489
|
|
|
424
490
|
// biome-ignore lint/suspicious/noExplicitAny: ViteDevServer 타입을 동적으로 로드해야 함
|
|
425
491
|
private viteServer: any = null;
|
|
426
492
|
|
|
427
|
-
|
|
493
|
+
/**
|
|
494
|
+
* dev all 모드: Vite Dev Server를 통합하여 API + SSR + CSR을 모두 제공합니다.
|
|
495
|
+
* API 동적 매칭은 handleDevApiRequest를 공유합니다.
|
|
496
|
+
*/
|
|
497
|
+
private async setupDevServerWithVite(
|
|
428
498
|
server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
|
|
429
499
|
webPath: string,
|
|
430
500
|
config: SonamuFastifyConfig,
|
|
431
|
-
globalCompressOptions?: CompressOptions,
|
|
432
501
|
): Promise<void> {
|
|
433
502
|
// @fastify/middie 등록 (Connect-style middleware 지원)
|
|
434
503
|
await server.register((await import("@fastify/middie")).default);
|
|
@@ -456,49 +525,39 @@ class SonamuClass {
|
|
|
456
525
|
return this.viteServer.middlewares(req, res, next);
|
|
457
526
|
});
|
|
458
527
|
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
if (this.syncer.models[api.modelName] === undefined) {
|
|
462
|
-
throw new Error(`정의되지 않은 모델에 접근 ${api.modelName}`);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
server.route({
|
|
466
|
-
method: api.options.httpMethod ?? "GET",
|
|
467
|
-
url: this.config.api.route.prefix + api.path,
|
|
468
|
-
handler: this.createApiHandler(api, config),
|
|
469
|
-
compress: toFastifyCompressOption(api.options.compress, globalCompressOptions),
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// SSR 라우트 개별 등록 (compress 옵션이 라우트별로 적용되도록)
|
|
474
|
-
const { getSSRRoutes, renderSSR } = await import("../ssr");
|
|
475
|
-
const ssrRoutes = getSSRRoutes();
|
|
476
|
-
|
|
477
|
-
for (const route of ssrRoutes) {
|
|
478
|
-
server.route({
|
|
479
|
-
method: ["GET", "HEAD"],
|
|
480
|
-
url: route.path,
|
|
481
|
-
compress: toFastifyCompressOption(route.compress ?? true, globalCompressOptions),
|
|
482
|
-
handler: async (request, reply) => {
|
|
483
|
-
const url = request.url;
|
|
484
|
-
console.log(`[SSR] Matched route: ${route.path}`);
|
|
485
|
-
|
|
486
|
-
const params = this.extractPathParams(route.path, url);
|
|
487
|
-
const html = await renderSSR(url, route, params, request, reply, config, this.viteServer);
|
|
488
|
-
|
|
489
|
-
reply.type("text/html");
|
|
490
|
-
return html;
|
|
491
|
-
},
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// CSR fallback (SSR 라우트에 매칭되지 않는 모든 요청)
|
|
528
|
+
// catch-all 라우트에서 동적으로 API/SSR 처리
|
|
529
|
+
// 개발 환경에서는 라우트별 compress 옵션을 포기하고 HMR 이점을 취합니다.
|
|
496
530
|
server.route({
|
|
497
|
-
method: ["GET", "HEAD"],
|
|
531
|
+
method: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"],
|
|
498
532
|
url: "/*",
|
|
499
533
|
handler: async (request, reply) => {
|
|
534
|
+
// 1. API 요청 처리
|
|
535
|
+
const result = this.handleDevApiRequest(request, config);
|
|
536
|
+
if (result) {
|
|
537
|
+
return result(request, reply);
|
|
538
|
+
}
|
|
539
|
+
|
|
500
540
|
const url = request.url;
|
|
501
541
|
|
|
542
|
+
// 2. SSR 라우트 처리
|
|
543
|
+
const { matchSSRRoute, renderSSR } = await import("../ssr");
|
|
544
|
+
const ssrMatch = matchSSRRoute(url);
|
|
545
|
+
if (ssrMatch) {
|
|
546
|
+
console.log(`[SSR] Matched route: ${ssrMatch.route.path}`);
|
|
547
|
+
const html = await renderSSR(
|
|
548
|
+
url,
|
|
549
|
+
ssrMatch.route,
|
|
550
|
+
ssrMatch.params,
|
|
551
|
+
request,
|
|
552
|
+
reply,
|
|
553
|
+
config,
|
|
554
|
+
this.viteServer,
|
|
555
|
+
);
|
|
556
|
+
reply.type("text/html");
|
|
557
|
+
return html;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 3. CSR fallback
|
|
502
561
|
try {
|
|
503
562
|
const fs = await import("node:fs/promises");
|
|
504
563
|
let template = await fs.readFile(
|
|
@@ -528,15 +587,14 @@ class SonamuClass {
|
|
|
528
587
|
|
|
529
588
|
private async setupStaticWebServer(
|
|
530
589
|
server: FastifyInstance<Server, IncomingMessage, ServerResponse>,
|
|
531
|
-
_webPath: string,
|
|
532
590
|
config: SonamuFastifyConfig,
|
|
533
591
|
globalCompressOptions: CompressOptions | undefined,
|
|
534
592
|
): Promise<void> {
|
|
535
|
-
// 경로 명확화: api/
|
|
536
|
-
const webDistPath = path.join(this.apiRootPath, "
|
|
537
|
-
const ssrPath = path.join(this.apiRootPath, "dist", "
|
|
593
|
+
// 경로 명확화: api/web-dist/client (정적 파일), api/web-dist/server (SSR entry), api/dist/ssr (SSR routes - API 소유)
|
|
594
|
+
const webDistPath = path.join(this.apiRootPath, "web-dist", "client");
|
|
595
|
+
const ssrPath = path.join(this.apiRootPath, "web-dist", "server");
|
|
538
596
|
const ssrEntryPath = path.join(ssrPath, "entry-server.generated.js");
|
|
539
|
-
const ssrRoutesPath = path.join(
|
|
597
|
+
const ssrRoutesPath = path.join(this.apiRootPath, "dist", "ssr", "routes.js");
|
|
540
598
|
|
|
541
599
|
if (!(await exists(webDistPath))) {
|
|
542
600
|
console.warn(`⚠ Web dist not found: ${webDistPath}`);
|
|
@@ -568,7 +626,14 @@ class SonamuClass {
|
|
|
568
626
|
server.get("/assets/:filename", async (request, reply) => {
|
|
569
627
|
const requestedFile = (request.params as { filename: string }).filename;
|
|
570
628
|
const assetsDir = path.join(webDistPath, "assets");
|
|
571
|
-
const
|
|
629
|
+
const safeFilePath = this.resolvePathWithinBaseDir(assetsDir, requestedFile);
|
|
630
|
+
if (safeFilePath === null) {
|
|
631
|
+
reply.status(403).send();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const normalizedRequestedFile = path.relative(assetsDir, safeFilePath).replace(/\\/g, "/");
|
|
635
|
+
|
|
636
|
+
const assetPath = `/assets/${normalizedRequestedFile}`;
|
|
572
637
|
|
|
573
638
|
// Cache-Control 헤더 결정
|
|
574
639
|
const getCacheControlForAsset = (): CacheControlConfig => {
|
|
@@ -590,8 +655,8 @@ class SonamuClass {
|
|
|
590
655
|
};
|
|
591
656
|
|
|
592
657
|
// index-*.js 또는 index-*.css 요청인 경우
|
|
593
|
-
if (/^index-[a-f0-9]+\.(js|css)$/.test(
|
|
594
|
-
const ext =
|
|
658
|
+
if (/^index-[a-f0-9]+\.(js|css)$/.test(normalizedRequestedFile)) {
|
|
659
|
+
const ext = normalizedRequestedFile.split(".").pop();
|
|
595
660
|
const files = await fs.readdir(assetsDir);
|
|
596
661
|
const currentFile = files.find((f) => f.startsWith("index-") && f.endsWith(`.${ext}`));
|
|
597
662
|
|
|
@@ -605,18 +670,18 @@ class SonamuClass {
|
|
|
605
670
|
}
|
|
606
671
|
|
|
607
672
|
// 일반 파일 서빙
|
|
608
|
-
const filePath =
|
|
673
|
+
const filePath = safeFilePath;
|
|
609
674
|
if (await exists(filePath)) {
|
|
610
675
|
const content = await fs.readFile(filePath);
|
|
611
|
-
const ext =
|
|
676
|
+
const ext = normalizedRequestedFile.split(".").pop();
|
|
612
677
|
reply.type(ext === "js" ? "application/javascript" : ext === "css" ? "text/css" : "");
|
|
613
|
-
if (
|
|
678
|
+
if (normalizedRequestedFile.includes("-")) {
|
|
614
679
|
applyCacheHeaders(reply, getCacheControlForAsset());
|
|
615
680
|
}
|
|
616
681
|
return reply.send(content);
|
|
617
682
|
}
|
|
618
683
|
|
|
619
|
-
reply.
|
|
684
|
+
reply.status(404).send();
|
|
620
685
|
});
|
|
621
686
|
|
|
622
687
|
// SSR 라우트 개별 등록 (compress 옵션이 라우트별로 적용되도록)
|
|
@@ -651,7 +716,7 @@ class SonamuClass {
|
|
|
651
716
|
handler: async (request, reply) => {
|
|
652
717
|
// /api, /sonamu-ui는 404 그대로
|
|
653
718
|
if (request.url.startsWith("/api") || request.url.startsWith("/sonamu-ui")) {
|
|
654
|
-
reply.
|
|
719
|
+
reply.status(404).send();
|
|
655
720
|
return;
|
|
656
721
|
}
|
|
657
722
|
|
|
@@ -671,10 +736,15 @@ class SonamuClass {
|
|
|
671
736
|
}
|
|
672
737
|
|
|
673
738
|
// 정적 파일이 존재할 경우, 정적 파일을 먼저 서빙해야함
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
739
|
+
const requestPath = this.getPathnameFromUrl(request.url);
|
|
740
|
+
const safeFilePath = this.resolvePathWithinBaseDir(webDistPath, requestPath);
|
|
741
|
+
if (safeFilePath === null) {
|
|
742
|
+
reply.status(403).send();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (await fileExists(safeFilePath)) {
|
|
746
|
+
const content = await fs.readFile(safeFilePath);
|
|
747
|
+
return reply.type(mimeLookup(safeFilePath) || "application/octet-stream").send(content);
|
|
678
748
|
}
|
|
679
749
|
|
|
680
750
|
// CSR fallback: index.html 서빙
|
|
@@ -845,7 +915,7 @@ class SonamuClass {
|
|
|
845
915
|
*/
|
|
846
916
|
private extractPathParams(pattern: string, url: string): Record<string, string> {
|
|
847
917
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
848
|
-
const urlParts =
|
|
918
|
+
const urlParts = this.getPathnameFromUrl(url).split("/").filter(Boolean);
|
|
849
919
|
const params: Record<string, string> = {};
|
|
850
920
|
|
|
851
921
|
for (let i = 0; i < patternParts.length; i++) {
|
|
@@ -856,6 +926,50 @@ class SonamuClass {
|
|
|
856
926
|
return params;
|
|
857
927
|
}
|
|
858
928
|
|
|
929
|
+
private isPathPatternMatch(pattern: string, url: string): boolean {
|
|
930
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
931
|
+
const urlParts = this.getPathnameFromUrl(url).split("/").filter(Boolean);
|
|
932
|
+
|
|
933
|
+
if (patternParts.length !== urlParts.length) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
938
|
+
const patternPart = patternParts[i];
|
|
939
|
+
const urlPart = urlParts[i];
|
|
940
|
+
if (patternPart.startsWith(":")) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (patternPart !== urlPart) {
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private getPathnameFromUrl(url: string): string {
|
|
952
|
+
return url.split("?")[0];
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private resolvePathWithinBaseDir(baseDir: string, inputPath: string): string | null {
|
|
956
|
+
try {
|
|
957
|
+
const decoded = decodeURIComponent(inputPath).replace(/\\/g, "/");
|
|
958
|
+
if (decoded.includes("\0")) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
const relativePath = decoded.replace(/^\/+/, "");
|
|
962
|
+
const resolvedPath = path.resolve(baseDir, relativePath);
|
|
963
|
+
const relativeFromBase = path.relative(baseDir, resolvedPath);
|
|
964
|
+
if (relativeFromBase.startsWith("..") || path.isAbsolute(relativeFromBase)) {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
return resolvedPath;
|
|
968
|
+
} catch {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
859
973
|
/**
|
|
860
974
|
* API 응답에 적용할 Cache-Control 설정을 결정합니다.
|
|
861
975
|
* 우선순위: 개별 지정 > cacheControlHandler
|
|
@@ -1037,7 +1151,7 @@ class SonamuClass {
|
|
|
1037
1151
|
}
|
|
1038
1152
|
|
|
1039
1153
|
/*
|
|
1040
|
-
A function that automatically handles init and destroy when using Sonamu via scripts.
|
|
1154
|
+
A function that automatically handles init and destroy when using Sonamu via scripts.
|
|
1041
1155
|
*/
|
|
1042
1156
|
async runScript(fn: () => Promise<void>) {
|
|
1043
1157
|
await this.init(true, false, undefined, false);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BetterAuthEntityDef } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* better-auth Anonymous 플러그인 엔티티 정의
|
|
5
|
+
* https://www.better-auth.com/docs/plugins/anonymous
|
|
6
|
+
*
|
|
7
|
+
* 익명 사용자 인증을 지원합니다.
|
|
8
|
+
* 새로운 테이블을 생성하지 않고 User 테이블에 is_anonymous 필드만 추가합니다.
|
|
9
|
+
*/
|
|
10
|
+
export const anonymousEntityDef: BetterAuthEntityDef = {
|
|
11
|
+
id: "anonymous",
|
|
12
|
+
name: "Anonymous",
|
|
13
|
+
entities: [],
|
|
14
|
+
additionalProps: {
|
|
15
|
+
User: [{ name: "is_anonymous", type: "boolean", nullable: true, desc: "익명 사용자 여부" }],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { BetterAuthEntityDef } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* better-auth API Key 플러그인 엔티티 정의
|
|
5
|
+
* https://www.better-auth.com/docs/plugins/api-key
|
|
6
|
+
*
|
|
7
|
+
* API 키 인증을 지원합니다.
|
|
8
|
+
*/
|
|
9
|
+
export const apiKeyEntityDef: BetterAuthEntityDef = {
|
|
10
|
+
id: "api-key",
|
|
11
|
+
name: "API Key",
|
|
12
|
+
entities: [
|
|
13
|
+
{
|
|
14
|
+
id: "ApiKey",
|
|
15
|
+
table: "api_keys",
|
|
16
|
+
title: "API 키",
|
|
17
|
+
props: [
|
|
18
|
+
{ name: "id", type: "string", desc: "ID" },
|
|
19
|
+
{ name: "key", type: "string", desc: "해시된 API 키" },
|
|
20
|
+
{ name: "start", type: "string", nullable: true, desc: "키 시작 문자열" },
|
|
21
|
+
{ name: "prefix", type: "string", nullable: true, desc: "키 접두사" },
|
|
22
|
+
{ name: "name", type: "string", nullable: true, desc: "키 이름" },
|
|
23
|
+
{ name: "remaining", type: "integer", nullable: true, desc: "남은 요청 수" },
|
|
24
|
+
{ name: "last_request", type: "date", nullable: true, desc: "마지막 요청 시간" },
|
|
25
|
+
{ name: "request_count", type: "integer", desc: "요청 횟수" },
|
|
26
|
+
{ name: "rate_limit_enabled", type: "boolean", desc: "Rate Limit 활성화 여부" },
|
|
27
|
+
{
|
|
28
|
+
name: "rate_limit_time_window",
|
|
29
|
+
type: "integer",
|
|
30
|
+
nullable: true,
|
|
31
|
+
desc: "Rate Limit 시간 창 (ms)",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "rate_limit_max",
|
|
35
|
+
type: "integer",
|
|
36
|
+
nullable: true,
|
|
37
|
+
desc: "Rate Limit 최대 요청 수",
|
|
38
|
+
},
|
|
39
|
+
{ name: "refill_interval", type: "integer", nullable: true, desc: "리필 간격 (ms)" },
|
|
40
|
+
{ name: "refill_amount", type: "integer", nullable: true, desc: "리필 양" },
|
|
41
|
+
{ name: "last_refill_at", type: "date", nullable: true, desc: "마지막 리필 시간" },
|
|
42
|
+
{ name: "expires_at", type: "date", nullable: true, desc: "만료일시" },
|
|
43
|
+
{ name: "enabled", type: "boolean", desc: "활성화 여부" },
|
|
44
|
+
{ name: "permissions", type: "string", nullable: true, desc: "권한" },
|
|
45
|
+
{ name: "metadata", type: "string", nullable: true, desc: "메타데이터 (JSON)" },
|
|
46
|
+
{ name: "created_at", type: "date", dbDefault: "CURRENT_TIMESTAMP", desc: "생성일시" },
|
|
47
|
+
{ name: "updated_at", type: "date", nullable: true, desc: "수정일시" },
|
|
48
|
+
{
|
|
49
|
+
type: "relation",
|
|
50
|
+
name: "user",
|
|
51
|
+
with: "User",
|
|
52
|
+
relationType: "BelongsToOne",
|
|
53
|
+
onDelete: "CASCADE",
|
|
54
|
+
desc: "사용자",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
indexes: [
|
|
58
|
+
{ type: "index", name: "api_keys_user_id_idx", columns: [{ name: "user_id" }] },
|
|
59
|
+
{ type: "unique", name: "api_keys_key_unique", columns: [{ name: "key" }] },
|
|
60
|
+
],
|
|
61
|
+
subsets: {
|
|
62
|
+
A: [
|
|
63
|
+
"id",
|
|
64
|
+
"key",
|
|
65
|
+
"start",
|
|
66
|
+
"prefix",
|
|
67
|
+
"name",
|
|
68
|
+
"remaining",
|
|
69
|
+
"last_request",
|
|
70
|
+
"request_count",
|
|
71
|
+
"rate_limit_enabled",
|
|
72
|
+
"rate_limit_time_window",
|
|
73
|
+
"rate_limit_max",
|
|
74
|
+
"refill_interval",
|
|
75
|
+
"refill_amount",
|
|
76
|
+
"last_refill_at",
|
|
77
|
+
"expires_at",
|
|
78
|
+
"enabled",
|
|
79
|
+
"permissions",
|
|
80
|
+
"metadata",
|
|
81
|
+
"created_at",
|
|
82
|
+
"updated_at",
|
|
83
|
+
"user.id",
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
enums: {
|
|
87
|
+
ApiKeyOrderBy: { "id-desc": "ID최신순", "created_at-desc": "생성일최신순" },
|
|
88
|
+
ApiKeySearchField: { id: "ID", name: "이름" },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
additionalProps: {},
|
|
93
|
+
};
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
export { adminEntityDef } from "./admin";
|
|
2
|
+
export { anonymousEntityDef } from "./anonymous";
|
|
3
|
+
export { apiKeyEntityDef } from "./api-key";
|
|
4
|
+
export { jwtEntityDef } from "./jwt";
|
|
5
|
+
export { organizationEntityDef } from "./organization";
|
|
6
|
+
export { passkeyEntityDef } from "./passkey";
|
|
2
7
|
export { phoneNumberEntityDef } from "./phone-number";
|
|
8
|
+
export { ssoEntityDef } from "./sso";
|
|
3
9
|
export { twoFactorEntityDef } from "./two-factor";
|
|
4
10
|
export type { BetterAuthEntityDef, BetterAuthPluginId } from "./types";
|
|
5
11
|
export { usernameEntityDef } from "./username";
|
|
6
12
|
|
|
7
13
|
import { adminEntityDef } from "./admin";
|
|
14
|
+
import { anonymousEntityDef } from "./anonymous";
|
|
15
|
+
import { apiKeyEntityDef } from "./api-key";
|
|
16
|
+
import { jwtEntityDef } from "./jwt";
|
|
17
|
+
import { organizationEntityDef } from "./organization";
|
|
18
|
+
import { passkeyEntityDef } from "./passkey";
|
|
8
19
|
import { phoneNumberEntityDef } from "./phone-number";
|
|
20
|
+
import { ssoEntityDef } from "./sso";
|
|
9
21
|
import { twoFactorEntityDef } from "./two-factor";
|
|
10
22
|
import type { BetterAuthEntityDef, BetterAuthPluginId } from "./types";
|
|
11
23
|
import { usernameEntityDef } from "./username";
|
|
@@ -19,6 +31,12 @@ export const ENTITY_DEFINITIONS: Record<BetterAuthPluginId, BetterAuthEntityDef>
|
|
|
19
31
|
username: usernameEntityDef,
|
|
20
32
|
"phone-number": phoneNumberEntityDef,
|
|
21
33
|
"2fa": twoFactorEntityDef,
|
|
34
|
+
sso: ssoEntityDef,
|
|
35
|
+
passkey: passkeyEntityDef,
|
|
36
|
+
organization: organizationEntityDef,
|
|
37
|
+
"api-key": apiKeyEntityDef,
|
|
38
|
+
jwt: jwtEntityDef,
|
|
39
|
+
anonymous: anonymousEntityDef,
|
|
22
40
|
};
|
|
23
41
|
|
|
24
42
|
/**
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BetterAuthEntityDef } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* better-auth JWT 플러그인 엔티티 정의
|
|
5
|
+
* https://www.better-auth.com/docs/plugins/jwt
|
|
6
|
+
*
|
|
7
|
+
* JWT 토큰 발급 및 JWKS 키 관리를 지원합니다.
|
|
8
|
+
*/
|
|
9
|
+
export const jwtEntityDef: BetterAuthEntityDef = {
|
|
10
|
+
id: "jwt",
|
|
11
|
+
name: "JWT",
|
|
12
|
+
entities: [
|
|
13
|
+
{
|
|
14
|
+
id: "Jwks",
|
|
15
|
+
table: "jwks",
|
|
16
|
+
title: "JWKS",
|
|
17
|
+
props: [
|
|
18
|
+
{ name: "id", type: "string", desc: "ID" },
|
|
19
|
+
{ name: "public_key", type: "string", desc: "공개키" },
|
|
20
|
+
{ name: "private_key", type: "string", desc: "비밀키" },
|
|
21
|
+
{ name: "created_at", type: "date", dbDefault: "CURRENT_TIMESTAMP", desc: "생성일시" },
|
|
22
|
+
{ name: "expires_at", type: "date", nullable: true, desc: "만료일시" },
|
|
23
|
+
],
|
|
24
|
+
indexes: [],
|
|
25
|
+
subsets: {
|
|
26
|
+
A: ["id", "public_key", "private_key", "created_at", "expires_at"],
|
|
27
|
+
},
|
|
28
|
+
enums: {
|
|
29
|
+
JwksOrderBy: { "id-desc": "ID최신순", "created_at-desc": "생성일최신순" },
|
|
30
|
+
JwksSearchField: { id: "ID" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
additionalProps: {},
|
|
35
|
+
};
|