sonamu 0.13.2 → 0.13.4
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/decorators.d.ts +2 -3
- package/dist/api/decorators.d.ts.map +1 -1
- package/dist/api/decorators.js +1 -1
- package/dist/api/sonamu.d.ts.map +1 -1
- package/dist/api/sonamu.js +5 -5
- package/dist/database/base-model.d.ts.map +1 -1
- package/dist/database/base-model.js +8 -3
- package/dist/database/puri.d.ts +8 -3
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +120 -15
- package/dist/database/puri.types.d.ts +5 -2
- package/dist/database/puri.types.d.ts.map +1 -1
- package/dist/database/puri.types.js +1 -1
- package/dist/dict/sonamu-dictionary.d.ts.map +1 -1
- package/dist/dict/sonamu-dictionary.js +24 -9
- package/dist/storage/buffered-file.d.ts +3 -3
- package/dist/storage/buffered-file.d.ts.map +1 -1
- package/dist/storage/buffered-file.js +2 -2
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/storage-manager.d.ts +3 -4
- package/dist/storage/storage-manager.d.ts.map +1 -1
- package/dist/storage/storage-manager.js +2 -2
- package/dist/storage/types.d.ts +3 -2
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/uploaded-file.d.ts +3 -3
- package/dist/storage/uploaded-file.d.ts.map +1 -1
- package/dist/storage/uploaded-file.js +1 -1
- package/package.json +5 -2
- package/src/api/__tests__/sonamu.storage.test.ts +145 -0
- package/src/api/decorators.ts +2 -3
- package/src/api/sonamu.ts +6 -6
- package/src/database/__tests__/puri-hydration.test.ts +226 -0
- package/src/database/base-model.ts +7 -3
- package/src/database/puri.ts +204 -15
- package/src/database/puri.types.test-d.ts +29 -0
- package/src/database/puri.types.ts +10 -2
- package/src/dict/sonamu-dictionary.ts +28 -6
- package/src/storage/__tests__/storage-manager.test.ts +64 -0
- package/src/storage/buffered-file.ts +3 -3
- package/src/storage/index.ts +1 -1
- package/src/storage/storage-manager.ts +4 -5
- package/src/storage/storage.test-d.ts +47 -0
- package/src/storage/types.ts +3 -2
- package/src/storage/uploaded-file.ts +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type MultipartFile } from "@fastify/multipart";
|
|
2
2
|
import { BaseFile } from "./base-file";
|
|
3
|
-
import { type
|
|
3
|
+
import { type DiskName } from "./types";
|
|
4
4
|
/**
|
|
5
5
|
* Buffer 모드로 업로드된 파일
|
|
6
6
|
* 메모리에 로드된 상태로, md5 계산이나 이미지 처리 등 유연한 작업이 가능합니다.
|
|
@@ -15,11 +15,11 @@ export declare class BufferedFile extends BaseFile {
|
|
|
15
15
|
md5(): Promise<string>;
|
|
16
16
|
/**
|
|
17
17
|
* 파일을 디스크에 저장
|
|
18
|
-
* @param diskName 디스크 이름
|
|
18
|
+
* @param diskName 설정에 등록한 디스크 이름
|
|
19
19
|
* @param key 저장 경로 (예: 'uploads/avatar.png')
|
|
20
20
|
* @returns 저장된 파일의 URL
|
|
21
21
|
*/
|
|
22
|
-
saveToDisk(diskName:
|
|
22
|
+
saveToDisk(diskName: DiskName, key: string): Promise<string>;
|
|
23
23
|
/** 원본 MultipartFile 접근 */
|
|
24
24
|
get raw(): MultipartFile;
|
|
25
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buffered-file.d.ts","sourceRoot":"","sources":["../../src/storage/buffered-file.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"buffered-file.d.ts","sourceRoot":"","sources":["../../src/storage/buffered-file.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC;;;GAGG;AACH,qBAAa,YAAa,SAAQ,QAAQ;IACxC,OAAO,CAAC,SAAS,CAAgB;IACjC,OAAO,CAAC,WAAW,CAAS;IAE5B,YAAY,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAQ9C;IAED,gBAAgB;IAChB,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,gBAAgB;IACV,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAE3B;IAED;;;;;OAKG;IACG,UAAU,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAcjE;IAED,0BAA0B;IAC1B,IAAI,GAAG,IAAI,aAAa,CAEvB;CACF"}
|
|
@@ -27,7 +27,7 @@ var init_buffered_file = __esmMin((() => {
|
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* 파일을 디스크에 저장
|
|
30
|
-
* @param diskName 디스크 이름
|
|
30
|
+
* @param diskName 설정에 등록한 디스크 이름
|
|
31
31
|
* @param key 저장 경로 (예: 'uploads/avatar.png')
|
|
32
32
|
* @returns 저장된 파일의 URL
|
|
33
33
|
*/
|
|
@@ -48,4 +48,4 @@ var init_buffered_file = __esmMin((() => {
|
|
|
48
48
|
init_buffered_file();
|
|
49
49
|
export { BufferedFile, init_buffered_file };
|
|
50
50
|
|
|
51
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
51
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVmZmVyZWQtZmlsZS5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi9zcmMvc3RvcmFnZS9idWZmZXJlZC1maWxlLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNyZWF0ZUhhc2ggfSBmcm9tIFwiY3J5cHRvXCI7XG5cbmltcG9ydCB7IHR5cGUgTXVsdGlwYXJ0RmlsZSB9IGZyb20gXCJAZmFzdGlmeS9tdWx0aXBhcnRcIjtcblxuaW1wb3J0IHsgQmFzZUZpbGUgfSBmcm9tIFwiLi9iYXNlLWZpbGVcIjtcbmltcG9ydCB7IHR5cGUgRGlza05hbWUgfSBmcm9tIFwiLi90eXBlc1wiO1xuXG4vKipcbiAqIEJ1ZmZlciDrqqjrk5zroZwg7JeF66Gc65Oc65CcIO2MjOydvFxuICog66mU66qo66as7JeQIOuhnOuTnOuQnCDsg4Htg5zroZwsIG1kNSDqs4TsgrDsnbTrgpgg7J2066+47KeAIOyymOumrCDrk7Eg7Jyg7Jew7ZWcIOyekeyXheydtCDqsIDriqXtlanri4jri6QuXG4gKi9cbmV4cG9ydCBjbGFzcyBCdWZmZXJlZEZpbGUgZXh0ZW5kcyBCYXNlRmlsZSB7XG4gIHByaXZhdGUgZmlsZVZhbHVlOiBNdWx0aXBhcnRGaWxlO1xuICBwcml2YXRlIGJ1ZmZlclZhbHVlOiBCdWZmZXI7XG5cbiAgY29uc3RydWN0b3IoZmlsZTogTXVsdGlwYXJ0RmlsZSwgYnVmZmVyOiBCdWZmZXIpIHtcbiAgICBzdXBlcih7XG4gICAgICBmaWxlbmFtZTogZmlsZS5maWxlbmFtZSxcbiAgICAgIG1pbWV0eXBlOiBmaWxlLm1pbWV0eXBlLFxuICAgICAgc2l6ZTogYnVmZmVyLmxlbmd0aCxcbiAgICB9KTtcbiAgICB0aGlzLmZpbGVWYWx1ZSA9IGZpbGU7XG4gICAgdGhpcy5idWZmZXJWYWx1ZSA9IGJ1ZmZlcjtcbiAgfVxuXG4gIC8qKiDtjIzsnbwgQnVmZmVyICovXG4gIGdldCBidWZmZXIoKTogQnVmZmVyIHtcbiAgICByZXR1cm4gdGhpcy5idWZmZXJWYWx1ZTtcbiAgfVxuXG4gIC8qKiBNRDUg7ZW07IucIOqzhOyCsCAqL1xuICBhc3luYyBtZDUoKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgICByZXR1cm4gY3JlYXRlSGFzaChcIm1kNVwiKS51cGRhdGUodGhpcy5idWZmZXJWYWx1ZSkuZGlnZXN0KFwiaGV4XCIpO1xuICB9XG5cbiAgLyoqXG4gICAqIO2MjOydvOydhCDrlJTsiqTtgazsl5Ag7KCA7J6lXG4gICAqIEBwYXJhbSBkaXNrTmFtZSDshKTsoJXsl5Ag65Ox66Gd7ZWcIOuUlOyKpO2BrCDsnbTrpoRcbiAgICogQHBhcmFtIGtleSDsoIDsnqUg6rK966GcICjsmIg6ICd1cGxvYWRzL2F2YXRhci5wbmcnKVxuICAgKiBAcmV0dXJucyDsoIDsnqXrkJwg7YyM7J287J2YIFVSTFxuICAgKi9cbiAgYXN5bmMgc2F2ZVRvRGlzayhkaXNrTmFtZTogRGlza05hbWUsIGtleTogc3RyaW5nKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgICAvLyDsiJztmZgg7J2Y7KG07ISxIOuwqeyngOulvCDsnITtlbQg64+Z7KCBIGltcG9ydFxuICAgIGNvbnN0IHsgU29uYW11IH0gPSBhd2FpdCBpbXBvcnQoXCIuLi9hcGkvc29uYW11XCIpO1xuICAgIGNvbnN0IGRpc2sgPSBTb25hbXUuc3RvcmFnZS51c2UoZGlza05hbWUpO1xuXG4gICAgYXdhaXQgZGlzay5wdXQoa2V5LCBuZXcgVWludDhBcnJheSh0aGlzLmJ1ZmZlclZhbHVlKSwge1xuICAgICAgY29udGVudFR5cGU6IHRoaXMubWltZXR5cGUsXG4gICAgfSk7XG5cbiAgICB0aGlzW1wiX3VybFwiXSA9IGF3YWl0IGRpc2suZ2V0VXJsKGtleSk7XG4gICAgdGhpc1tcIl9zaWduZWRVcmxcIl0gPSBhd2FpdCBkaXNrLmdldFNpZ25lZFVybChrZXkpO1xuXG4gICAgLy8gc2lnbmVkIHVybOydgCDrp4zro4wg7Iuc6rCE7J20IOyeiOq4sCDrlYzrrLjsl5AsIHVuc2lnbmVkIHVybOydhCDrsJjtmZjtlanri4jri6QuXG4gICAgcmV0dXJuIHRoaXNbXCJfdXJsXCJdO1xuICB9XG5cbiAgLyoqIOybkOuzuCBNdWx0aXBhcnRGaWxlIOygkeq3vCAqL1xuICBnZXQgcmF3KCk6IE11bHRpcGFydEZpbGUge1xuICAgIHJldHVybiB0aGlzLmZpbGVWYWx1ZTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Q0FJc0MsZUFBQTtDQU96QixlQUFiLGNBQWtDLFNBQVM7RUFDekM7RUFDQTtFQUVBLFlBQVksTUFBcUIsUUFBZ0I7R0FDL0MsTUFBTTtJQUNKLFVBQVUsS0FBSztJQUNmLFVBQVUsS0FBSztJQUNmLE1BQU0sT0FBTztHQUNmLENBQUM7R0FDRCxLQUFLLFlBQVk7R0FDakIsS0FBSyxjQUFjO0VBQ3JCOztFQUdBLElBQUksU0FBaUI7R0FDbkIsT0FBTyxLQUFLO0VBQ2Q7O0VBR0EsTUFBTSxNQUF1QjtHQUMzQixPQUFPLFdBQVcsS0FBSyxDQUFDLENBQUMsT0FBTyxLQUFLLFdBQVcsQ0FBQyxDQUFDLE9BQU8sS0FBSztFQUNoRTs7Ozs7OztFQVFBLE1BQU0sV0FBVyxVQUFvQixLQUE4QjtHQUVqRSxNQUFNLEVBQUUsV0FBVyxNQUFNLE9BQU87R0FDaEMsTUFBTSxPQUFPLE9BQU8sUUFBUSxJQUFJLFFBQVE7R0FFeEMsTUFBTSxLQUFLLElBQUksS0FBSyxJQUFJLFdBQVcsS0FBSyxXQUFXLEdBQUcsRUFDcEQsYUFBYSxLQUFLLFNBQ3BCLENBQUM7R0FFRCxLQUFLLFVBQVUsTUFBTSxLQUFLLE9BQU8sR0FBRztHQUNwQyxLQUFLLGdCQUFnQixNQUFNLEtBQUssYUFBYSxHQUFHO0dBR2hELE9BQU8sS0FBSztFQUNkOztFQUdBLElBQUksTUFBcUI7R0FDdkIsT0FBTyxLQUFLO0VBQ2Q7Q0FDRiJ9
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { BaseFile } from "./base-file";
|
|
|
2
2
|
export { BufferedFile } from "./buffered-file";
|
|
3
3
|
export { type DriverKey, drivers } from "./drivers";
|
|
4
4
|
export { StorageManager } from "./storage-manager";
|
|
5
|
-
export type { KeyGenerator, StorageConfig } from "./types";
|
|
5
|
+
export type { DiskName, KeyGenerator, StorageConfig } from "./types";
|
|
6
6
|
export { UploadedFile as StreamedFile } from "./uploaded-file";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,KAAK,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,KAAK,SAAS,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,EAAE,YAAY,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/storage/index.js
CHANGED
|
@@ -10,4 +10,4 @@ init_storage_manager();
|
|
|
10
10
|
init_uploaded_file();
|
|
11
11
|
export { BaseFile, BufferedFile, StorageManager, UploadedFile as StreamedFile, drivers };
|
|
12
12
|
|
|
13
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
13
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3N0b3JhZ2UvaW5kZXgudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gU3RvcmFnZSDshJzruIzrqqjrk4ggZXhwb3J0c1xuZXhwb3J0IHsgQmFzZUZpbGUgfSBmcm9tIFwiLi9iYXNlLWZpbGVcIjtcbmV4cG9ydCB7IEJ1ZmZlcmVkRmlsZSB9IGZyb20gXCIuL2J1ZmZlcmVkLWZpbGVcIjtcbmV4cG9ydCB7IHR5cGUgRHJpdmVyS2V5LCBkcml2ZXJzIH0gZnJvbSBcIi4vZHJpdmVyc1wiO1xuZXhwb3J0IHsgU3RvcmFnZU1hbmFnZXIgfSBmcm9tIFwiLi9zdG9yYWdlLW1hbmFnZXJcIjtcbmV4cG9ydCB0eXBlIHsgRGlza05hbWUsIEtleUdlbmVyYXRvciwgU3RvcmFnZUNvbmZpZyB9IGZyb20gXCIuL3R5cGVzXCI7XG5leHBvcnQgeyBVcGxvYWRlZEZpbGUgYXMgU3RyZWFtZWRGaWxlIH0gZnJvbSBcIi4vdXBsb2FkZWQtZmlsZVwiO1xuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7QUFDc0MsZUFBQTtBQUNRLG1CQUFBO0FBRUkscUJBQUE7QUFFWSxtQkFBQSJ9
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Disk } from "flydrive";
|
|
2
|
-
import { type
|
|
3
|
-
import { type StorageConfig } from "./types";
|
|
2
|
+
import { type DiskName, type StorageConfig } from "./types";
|
|
4
3
|
/**
|
|
5
4
|
* 여러 디스크를 관리하는 매니저
|
|
6
5
|
*/
|
|
@@ -10,8 +9,8 @@ export declare class StorageManager {
|
|
|
10
9
|
constructor(config: StorageConfig);
|
|
11
10
|
/**
|
|
12
11
|
* 디스크 인스턴스 반환 (lazy initialization)
|
|
13
|
-
* @param diskName 디스크 이름
|
|
12
|
+
* @param diskName 설정에 등록한 디스크 이름
|
|
14
13
|
*/
|
|
15
|
-
use(diskName:
|
|
14
|
+
use(diskName: DiskName): Disk;
|
|
16
15
|
}
|
|
17
16
|
//# sourceMappingURL=storage-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-manager.d.ts","sourceRoot":"","sources":["../../src/storage/storage-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAGhC,OAAO,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"storage-manager.d.ts","sourceRoot":"","sources":["../../src/storage/storage-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAGhC,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAE5D;;GAEG;AACH,qBAAa,cAAc;IAGb,OAAO,CAAC,MAAM;IAF1B,OAAO,CAAC,KAAK,CAAkC;IAE/C,YAAoB,MAAM,EAAE,aAAa,EAAI;IAE7C;;;OAGG;IACH,GAAG,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAW5B;CACF"}
|
|
@@ -13,7 +13,7 @@ var init_storage_manager = __esmMin((() => {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* 디스크 인스턴스 반환 (lazy initialization)
|
|
16
|
-
* @param diskName 디스크 이름
|
|
16
|
+
* @param diskName 설정에 등록한 디스크 이름
|
|
17
17
|
*/
|
|
18
18
|
use(diskName) {
|
|
19
19
|
if (!this.disks.has(diskName)) {
|
|
@@ -31,4 +31,4 @@ var init_storage_manager = __esmMin((() => {
|
|
|
31
31
|
init_storage_manager();
|
|
32
32
|
export { StorageManager, init_storage_manager };
|
|
33
33
|
|
|
34
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
34
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS1tYW5hZ2VyLmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zdG9yYWdlL3N0b3JhZ2UtbWFuYWdlci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBEaXNrIH0gZnJvbSBcImZseWRyaXZlXCI7XG5cbmltcG9ydCB7IGFzc2VydERlZmluZWQgfSBmcm9tIFwiLi4vdXRpbHMvdXRpbHNcIjtcbmltcG9ydCB7IHR5cGUgRGlza05hbWUsIHR5cGUgU3RvcmFnZUNvbmZpZyB9IGZyb20gXCIuL3R5cGVzXCI7XG5cbi8qKlxuICog7Jes65+sIOuUlOyKpO2BrOulvCDqtIDrpqztlZjripQg66ek64uI7KCAXG4gKi9cbmV4cG9ydCBjbGFzcyBTdG9yYWdlTWFuYWdlciB7XG4gIHByaXZhdGUgZGlza3M6IE1hcDxEaXNrTmFtZSwgRGlzaz4gPSBuZXcgTWFwKCk7XG5cbiAgY29uc3RydWN0b3IocHJpdmF0ZSBjb25maWc6IFN0b3JhZ2VDb25maWcpIHt9XG5cbiAgLyoqXG4gICAqIOuUlOyKpO2BrCDsnbjsiqTthLTsiqQg67CY7ZmYIChsYXp5IGluaXRpYWxpemF0aW9uKVxuICAgKiBAcGFyYW0gZGlza05hbWUg7ISk7KCV7JeQIOuTseuhne2VnCDrlJTsiqTtgawg7J2066aEXG4gICAqL1xuICB1c2UoZGlza05hbWU6IERpc2tOYW1lKTogRGlzayB7XG4gICAgaWYgKCF0aGlzLmRpc2tzLmhhcyhkaXNrTmFtZSkpIHtcbiAgICAgIGNvbnN0IGZhY3RvcnkgPSB0aGlzLmNvbmZpZy5kcml2ZXJzW2Rpc2tOYW1lXTtcbiAgICAgIGlmICghZmFjdG9yeSkge1xuICAgICAgICBjb25zdCBhdmFpbGFibGUgPSBPYmplY3Qua2V5cyh0aGlzLmNvbmZpZy5kcml2ZXJzKS5qb2luKFwiLCBcIik7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgVW5rbm93biBkaXNrOiBcIiR7ZGlza05hbWV9XCIuIEF2YWlsYWJsZTogJHthdmFpbGFibGV9YCk7XG4gICAgICB9XG4gICAgICB0aGlzLmRpc2tzLnNldChkaXNrTmFtZSwgbmV3IERpc2soZmFjdG9yeSgpKSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIGFzc2VydERlZmluZWQodGhpcy5kaXNrcy5nZXQoZGlza05hbWUpLCBgRGlzayAke2Rpc2tOYW1lfSBub3QgZm91bmRgKTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Q0FFOEMsV0FBQTtDQU1qQyxpQkFBYixNQUE0QjtFQUdOO0VBRnBCLHdCQUFxQyxJQUFJLElBQUk7RUFFN0MsWUFBWSxRQUErQjtHQUF2QixLQUFBLFNBQUE7RUFBd0I7Ozs7O0VBTTVDLElBQUksVUFBMEI7R0FDNUIsSUFBSSxDQUFDLEtBQUssTUFBTSxJQUFJLFFBQVEsR0FBRztJQUM3QixNQUFNLFVBQVUsS0FBSyxPQUFPLFFBQVE7SUFDcEMsSUFBSSxDQUFDLFNBQVM7S0FDWixNQUFNLFlBQVksT0FBTyxLQUFLLEtBQUssT0FBTyxPQUFPLENBQUMsQ0FBQyxLQUFLLElBQUk7S0FDNUQsTUFBTSxJQUFJLE1BQU0sa0JBQWtCLFNBQVMsZ0JBQWdCLFdBQVc7SUFDeEU7SUFDQSxLQUFLLE1BQU0sSUFBSSxVQUFVLElBQUksS0FBSyxRQUFRLENBQUMsQ0FBQztHQUM5QztHQUVBLE9BQU8sY0FBYyxLQUFLLE1BQU0sSUFBSSxRQUFRLEdBQUcsUUFBUSxTQUFTLFdBQVc7RUFDN0U7Q0FDRiJ9
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DriverContract } from "flydrive/types";
|
|
2
|
-
|
|
2
|
+
/** storage.drivers에 등록한 디스크 이름. 드라이버 종류(fs·s3)와는 독립적이다. */
|
|
3
|
+
export type DiskName = string;
|
|
3
4
|
/**
|
|
4
5
|
* Stream 모드에서 파일 키 생성 함수 타입
|
|
5
6
|
*/
|
|
@@ -12,7 +13,7 @@ export type KeyGenerator = (file: {
|
|
|
12
13
|
*/
|
|
13
14
|
export type StorageConfig = {
|
|
14
15
|
/** 디스크별 드라이버 팩토리 */
|
|
15
|
-
drivers: Record<
|
|
16
|
+
drivers: Record<DiskName, () => DriverContract>;
|
|
16
17
|
/** stream 모드의 전역 키 생성 함수 (생략 시 timestamp-random 기본값) */
|
|
17
18
|
keyGenerator?: KeyGenerator;
|
|
18
19
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,2DAA2D;AAC3D,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAE9B;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAE/B;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,cAAc,CAAC,CAAC;IAChD,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseFile } from "./base-file";
|
|
2
|
-
import { type
|
|
2
|
+
import { type DiskName } from "./types";
|
|
3
3
|
/**
|
|
4
4
|
* Stream 모드로 업로드된 파일
|
|
5
5
|
* 이미 저장소에 스트리밍 완료된 상태로, url/key 등 메타데이터만 접근 가능합니다.
|
|
@@ -14,12 +14,12 @@ export declare class UploadedFile extends BaseFile {
|
|
|
14
14
|
url: string;
|
|
15
15
|
signedUrl: string;
|
|
16
16
|
key: string;
|
|
17
|
-
diskName:
|
|
17
|
+
diskName: DiskName;
|
|
18
18
|
});
|
|
19
19
|
/** 저장소 내 키 */
|
|
20
20
|
get key(): string;
|
|
21
21
|
/** 저장된 디스크 이름 */
|
|
22
|
-
get diskName():
|
|
22
|
+
get diskName(): DiskName;
|
|
23
23
|
/**
|
|
24
24
|
* 저장소에서 파일을 다운로드합니다.
|
|
25
25
|
* 스트림 모드로 업로드된 파일을 나중에 처리해야 할 때 사용합니다.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uploaded-file.d.ts","sourceRoot":"","sources":["../../src/storage/uploaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"uploaded-file.d.ts","sourceRoot":"","sources":["../../src/storage/uploaded-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC;;;GAGG;AACH,qBAAa,YAAa,SAAQ,QAAQ;IACxC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAW;IAEhC,YAAY,MAAM,EAAE;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,SAAS,EAAE,MAAM,CAAC;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,EAAE,QAAQ,CAAC;KACpB,EAUA;IAED,cAAc;IACd,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,iBAAiB;IACjB,IAAI,QAAQ,IAAI,QAAQ,CAEvB;IAED;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAMhC;CACF"}
|
|
@@ -40,4 +40,4 @@ var init_uploaded_file = __esmMin((() => {
|
|
|
40
40
|
init_uploaded_file();
|
|
41
41
|
export { UploadedFile, init_uploaded_file };
|
|
42
42
|
|
|
43
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,
|
|
43
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBsb2FkZWQtZmlsZS5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi9zcmMvc3RvcmFnZS91cGxvYWRlZC1maWxlLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IEJhc2VGaWxlIH0gZnJvbSBcIi4vYmFzZS1maWxlXCI7XG5pbXBvcnQgeyB0eXBlIERpc2tOYW1lIH0gZnJvbSBcIi4vdHlwZXNcIjtcblxuLyoqXG4gKiBTdHJlYW0g66qo65Oc66GcIOyXheuhnOuTnOuQnCDtjIzsnbxcbiAqIOydtOuvuCDsoIDsnqXshozsl5Ag7Iqk7Yq466as67CNIOyZhOujjOuQnCDsg4Htg5zroZwsIHVybC9rZXkg65OxIOuplO2DgOuNsOydtO2EsOunjCDsoJHqt7wg6rCA64ql7ZWp64uI64ukLlxuICovXG5leHBvcnQgY2xhc3MgVXBsb2FkZWRGaWxlIGV4dGVuZHMgQmFzZUZpbGUge1xuICBwcml2YXRlIGtleVZhbHVlOiBzdHJpbmc7XG4gIHByaXZhdGUgZGlza05hbWVWYWx1ZTogRGlza05hbWU7XG5cbiAgY29uc3RydWN0b3IocGFyYW1zOiB7XG4gICAgZmlsZW5hbWU6IHN0cmluZztcbiAgICBtaW1ldHlwZTogc3RyaW5nO1xuICAgIHNpemU6IG51bWJlcjtcbiAgICB1cmw6IHN0cmluZztcbiAgICBzaWduZWRVcmw6IHN0cmluZztcbiAgICBrZXk6IHN0cmluZztcbiAgICBkaXNrTmFtZTogRGlza05hbWU7XG4gIH0pIHtcbiAgICBzdXBlcih7XG4gICAgICBmaWxlbmFtZTogcGFyYW1zLmZpbGVuYW1lLFxuICAgICAgbWltZXR5cGU6IHBhcmFtcy5taW1ldHlwZSxcbiAgICAgIHNpemU6IHBhcmFtcy5zaXplLFxuICAgICAgdXJsOiBwYXJhbXMudXJsLFxuICAgICAgc2lnbmVkVXJsOiBwYXJhbXMuc2lnbmVkVXJsLFxuICAgIH0pO1xuICAgIHRoaXMua2V5VmFsdWUgPSBwYXJhbXMua2V5O1xuICAgIHRoaXMuZGlza05hbWVWYWx1ZSA9IHBhcmFtcy5kaXNrTmFtZTtcbiAgfVxuXG4gIC8qKiDsoIDsnqXshowg64K0IO2CpCAqL1xuICBnZXQga2V5KCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIHRoaXMua2V5VmFsdWU7XG4gIH1cblxuICAvKiog7KCA7J6l65CcIOuUlOyKpO2BrCDsnbTrpoQgKi9cbiAgZ2V0IGRpc2tOYW1lKCk6IERpc2tOYW1lIHtcbiAgICByZXR1cm4gdGhpcy5kaXNrTmFtZVZhbHVlO1xuICB9XG5cbiAgLyoqXG4gICAqIOyggOyepeyGjOyXkOyEnCDtjIzsnbzsnYQg64uk7Jq066Gc65Oc7ZWp64uI64ukLlxuICAgKiDsiqTtirjrprwg66qo65Oc66GcIOyXheuhnOuTnOuQnCDtjIzsnbzsnYQg64KY7KSR7JeQIOyymOumrO2VtOyVvCDtlaAg65WMIOyCrOyaqe2VqeuLiOuLpC5cbiAgICovXG4gIGFzeW5jIGRvd25sb2FkKCk6IFByb21pc2U8QnVmZmVyPiB7XG4gICAgY29uc3QgeyBTb25hbXUgfSA9IGF3YWl0IGltcG9ydChcIi4uL2FwaS9zb25hbXVcIik7XG4gICAgY29uc3QgZGlzayA9IFNvbmFtdS5zdG9yYWdlLnVzZSh0aGlzLmRpc2tOYW1lVmFsdWUpO1xuXG4gICAgY29uc3QgdWludDhBcnJheSA9IGF3YWl0IGRpc2suZ2V0KHRoaXMua2V5VmFsdWUpO1xuICAgIHJldHVybiBCdWZmZXIuZnJvbSh1aW50OEFycmF5KTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7OztDQUFzQyxlQUFBO0NBT3pCLGVBQWIsY0FBa0MsU0FBUztFQUN6QztFQUNBO0VBRUEsWUFBWSxRQVFUO0dBQ0QsTUFBTTtJQUNKLFVBQVUsT0FBTztJQUNqQixVQUFVLE9BQU87SUFDakIsTUFBTSxPQUFPO0lBQ2IsS0FBSyxPQUFPO0lBQ1osV0FBVyxPQUFPO0dBQ3BCLENBQUM7R0FDRCxLQUFLLFdBQVcsT0FBTztHQUN2QixLQUFLLGdCQUFnQixPQUFPO0VBQzlCOztFQUdBLElBQUksTUFBYztHQUNoQixPQUFPLEtBQUs7RUFDZDs7RUFHQSxJQUFJLFdBQXFCO0dBQ3ZCLE9BQU8sS0FBSztFQUNkOzs7OztFQU1BLE1BQU0sV0FBNEI7R0FDaEMsTUFBTSxFQUFFLFdBQVcsTUFBTSxPQUFPO0dBR2hDLE1BQU0sYUFBYSxNQUZOLE9BQU8sUUFBUSxJQUFJLEtBQUssYUFFWixDQUFBLENBQUssSUFBSSxLQUFLLFFBQVE7R0FDL0MsT0FBTyxPQUFPLEtBQUssVUFBVTtFQUMvQjtDQUNGIn0=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonamu",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.4",
|
|
4
4
|
"description": "Sonamu — TypeScript Fullstack API Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"@logtape/logtape": "2.4.2",
|
|
104
104
|
"@logtape/pretty": "2.4.2",
|
|
105
105
|
"@logtape/redaction": "2.4.2",
|
|
106
|
-
"@sheetkit/node": "^0.
|
|
106
|
+
"@sheetkit/node": "^1.0.1",
|
|
107
107
|
"@sonamu-kit/hmr-hook": "^0.7.0",
|
|
108
108
|
"@sonamu-kit/hmr-runner": "^0.3.0",
|
|
109
109
|
"@sonamu-kit/tasks": "^0.4.0",
|
|
@@ -194,6 +194,9 @@
|
|
|
194
194
|
"optional": true
|
|
195
195
|
}
|
|
196
196
|
},
|
|
197
|
+
"engines": {
|
|
198
|
+
"node": ">=24"
|
|
199
|
+
},
|
|
197
200
|
"scripts": {
|
|
198
201
|
"dev": "nodemon exec",
|
|
199
202
|
"prebuild": "tsx ../../scripts/hard-link.ts",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { DB } from "../../database/db";
|
|
6
|
+
import { EntityManager } from "../../entity/entity-manager";
|
|
7
|
+
import * as envModule from "../../env";
|
|
8
|
+
import { drivers } from "../../storage/drivers";
|
|
9
|
+
import { Syncer } from "../../syncer/syncer";
|
|
10
|
+
import { TemplateManager } from "../../template";
|
|
11
|
+
import { type AbsolutePath } from "../../utils/path-utils";
|
|
12
|
+
import * as utils from "../../utils/utils";
|
|
13
|
+
import * as configModule from "../config";
|
|
14
|
+
import { type SonamuConfig } from "../config";
|
|
15
|
+
import { Sonamu } from "../sonamu";
|
|
16
|
+
|
|
17
|
+
// 스토리지 자체는 실제 FS 드라이버로 검증하고 DB·모델 자동 로딩만 격리한다.
|
|
18
|
+
describe("서버 없이 스토리지 초기화", () => {
|
|
19
|
+
const snapshot = Sonamu.captureTestingSnapshot();
|
|
20
|
+
const originalInitialized = Sonamu.isInitialized;
|
|
21
|
+
const originalForTesting = Sonamu.forTesting;
|
|
22
|
+
const originalServer = Sonamu.server;
|
|
23
|
+
const originalStorage = Sonamu["storageValue"];
|
|
24
|
+
const originalCache = Sonamu["cacheValue"];
|
|
25
|
+
const originalWorkflows = Sonamu["workflowsValue"];
|
|
26
|
+
const originalDbConfig = Sonamu["dbConfigValue"];
|
|
27
|
+
const originalBoot = Sonamu["boot"];
|
|
28
|
+
const originalInitializeWorkflows = Sonamu["initializeWorkflows"];
|
|
29
|
+
|
|
30
|
+
let config: SonamuConfig;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.stubEnv("NODE_ENV", "test");
|
|
34
|
+
Sonamu.isInitialized = false;
|
|
35
|
+
Reflect.set(Sonamu, "storageValue", null);
|
|
36
|
+
config = {
|
|
37
|
+
projectName: "storage-test",
|
|
38
|
+
api: { dir: "api", route: { prefix: "/api" } },
|
|
39
|
+
database: {},
|
|
40
|
+
i18n: { defaultLocale: "ko", supportedLocales: ["ko"] },
|
|
41
|
+
sync: { targets: [] },
|
|
42
|
+
logging: false,
|
|
43
|
+
server: {
|
|
44
|
+
apiConfig: {
|
|
45
|
+
contextProvider: (context) => context,
|
|
46
|
+
guardHandler: () => {},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
vi.spyOn(configModule, "loadConfig").mockImplementation(async () => config);
|
|
51
|
+
vi.spyOn(envModule, "readAllEnvironmentSnapshots").mockReturnValue({
|
|
52
|
+
development: {},
|
|
53
|
+
test: {},
|
|
54
|
+
staging: {},
|
|
55
|
+
production: {},
|
|
56
|
+
});
|
|
57
|
+
vi.spyOn(DB, "setConfig").mockImplementation(() => {});
|
|
58
|
+
vi.spyOn(EntityManager, "autoload").mockResolvedValue(undefined);
|
|
59
|
+
vi.spyOn(Syncer.prototype, "autoloadTypes").mockResolvedValue(undefined);
|
|
60
|
+
vi.spyOn(Syncer.prototype, "autoloadModels").mockResolvedValue(undefined);
|
|
61
|
+
vi.spyOn(Syncer.prototype, "autoloadApis").mockResolvedValue(undefined);
|
|
62
|
+
vi.spyOn(Syncer.prototype, "autoloadWorkflows").mockResolvedValue(undefined);
|
|
63
|
+
vi.spyOn(Syncer.prototype, "autoloadSsrRoutes").mockResolvedValue(undefined);
|
|
64
|
+
vi.spyOn(TemplateManager, "autoload").mockResolvedValue(undefined);
|
|
65
|
+
// 워크플로 초기화는 별도 DB 연결을 만들므로 스토리지 수명주기 검사에서 제외한다.
|
|
66
|
+
Sonamu["initializeWorkflows"] = async () => {};
|
|
67
|
+
// SAFETY: 테스트에서만 쓰는 절대 경로이며 파일 접근은 위의 설정·자동 로딩 경계에서 격리한다.
|
|
68
|
+
vi.spyOn(utils, "findApiRootPath").mockReturnValue(
|
|
69
|
+
"/tmp/sonamu-storage-test/api" as AbsolutePath,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
if (Sonamu.server && Sonamu.server !== originalServer) await Sonamu.server.close();
|
|
75
|
+
Sonamu.restoreTestingSnapshot(snapshot);
|
|
76
|
+
Sonamu.isInitialized = originalInitialized;
|
|
77
|
+
Sonamu.forTesting = originalForTesting;
|
|
78
|
+
Sonamu.server = originalServer;
|
|
79
|
+
Reflect.set(Sonamu, "storageValue", originalStorage);
|
|
80
|
+
Reflect.set(Sonamu, "cacheValue", originalCache);
|
|
81
|
+
Reflect.set(Sonamu, "workflowsValue", originalWorkflows);
|
|
82
|
+
Reflect.set(Sonamu, "dbConfigValue", originalDbConfig);
|
|
83
|
+
Sonamu["boot"] = originalBoot;
|
|
84
|
+
Sonamu["initializeWorkflows"] = originalInitializeWorkflows;
|
|
85
|
+
vi.restoreAllMocks();
|
|
86
|
+
vi.unstubAllEnvs();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
function configureStorage() {
|
|
90
|
+
const factory = vi.fn(
|
|
91
|
+
drivers.fs({
|
|
92
|
+
location: tmpdir(),
|
|
93
|
+
visibility: "public",
|
|
94
|
+
urlBuilder: {
|
|
95
|
+
async generateURL(key) {
|
|
96
|
+
return `https://cdn.example.com/${key}`;
|
|
97
|
+
},
|
|
98
|
+
async generateSignedURL(key) {
|
|
99
|
+
return `https://cdn.example.com/${key}?signed`;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
config.server.storage = { drivers: { images: factory } };
|
|
105
|
+
return factory;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
it.each(["init", "initForTesting"] as const)(
|
|
109
|
+
"%s에서 디스크 생성 없이 설정을 준비하고 URL을 만든다",
|
|
110
|
+
async (method) => {
|
|
111
|
+
const factory = configureStorage();
|
|
112
|
+
if (method === "init") await Sonamu.init(true, false);
|
|
113
|
+
else await Sonamu.initForTesting();
|
|
114
|
+
|
|
115
|
+
const storage = Sonamu.storage;
|
|
116
|
+
expect(factory).not.toHaveBeenCalled();
|
|
117
|
+
const disk = storage.use("images");
|
|
118
|
+
expect(await disk.getUrl("photo.jpg")).toBe("https://cdn.example.com/photo.jpg");
|
|
119
|
+
expect(await disk.getSignedUrl("photo.jpg")).toBe("https://cdn.example.com/photo.jpg?signed");
|
|
120
|
+
expect(storage.use("images")).toBe(disk);
|
|
121
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
122
|
+
await Sonamu.init(true, false);
|
|
123
|
+
expect(Sonamu.storage).toBe(storage);
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
it("설정이 없으면 기존 초기화 오류를 유지한다", async () => {
|
|
128
|
+
await Sonamu.initForTesting();
|
|
129
|
+
expect(() => Sonamu.storage).toThrow("Storage has not been initialized");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("init 다음 서버를 생성해도 기존 디스크 인스턴스를 유지한다", async () => {
|
|
133
|
+
const factory = configureStorage();
|
|
134
|
+
await Sonamu.init(true, false);
|
|
135
|
+
const storage = Sonamu.storage;
|
|
136
|
+
const disk = storage.use("images");
|
|
137
|
+
vi.spyOn(Sonamu, "withFastify").mockResolvedValue(undefined);
|
|
138
|
+
// 실제 listen과 프로세스 종료 훅만 격리한다.
|
|
139
|
+
Sonamu["boot"] = async () => {};
|
|
140
|
+
await Sonamu.createServer({ doSilent: true });
|
|
141
|
+
expect(Sonamu.storage).toBe(storage);
|
|
142
|
+
expect(Sonamu.storage.use("images")).toBe(disk);
|
|
143
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
|
144
|
+
});
|
|
145
|
+
});
|
package/src/api/decorators.ts
CHANGED
|
@@ -15,8 +15,7 @@ import { PuriTransactionWrapper } from "../database/puri-wrapper";
|
|
|
15
15
|
import { type TransactionalOptions } from "../database/puri-wrapper";
|
|
16
16
|
import { UpsertBuilder } from "../database/upsert-builder";
|
|
17
17
|
import { convertDomainToCategory } from "../logger/category";
|
|
18
|
-
import { type
|
|
19
|
-
import { type KeyGenerator } from "../storage/types";
|
|
18
|
+
import { type DiskName, type KeyGenerator } from "../storage/types";
|
|
20
19
|
import { type ApiParam, type ApiParamType } from "../types/types";
|
|
21
20
|
import { getModelNameFromClassName } from "../utils/class-name";
|
|
22
21
|
import { BaseFrameClass } from "./base-frame";
|
|
@@ -85,7 +84,7 @@ type BufferUploadOptions = {
|
|
|
85
84
|
};
|
|
86
85
|
type StreamUploadOptions = {
|
|
87
86
|
consume: "stream";
|
|
88
|
-
destination:
|
|
87
|
+
destination: DiskName;
|
|
89
88
|
keyGenerator?: KeyGenerator;
|
|
90
89
|
};
|
|
91
90
|
export type UploadDecoratorOptions = {
|
package/src/api/sonamu.ts
CHANGED
|
@@ -411,6 +411,12 @@ class SonamuClass {
|
|
|
411
411
|
const { EntityManager } = await import("../entity/entity-manager");
|
|
412
412
|
await EntityManager.autoload(doSilent);
|
|
413
413
|
|
|
414
|
+
// 테스트와 CLI에서도 URL 생성에 쓰며, 드라이버는 use() 시점까지 생성하지 않는다.
|
|
415
|
+
if (this.config.server.storage) {
|
|
416
|
+
const { StorageManager } = await import("../storage/storage-manager");
|
|
417
|
+
this.storageValue = new StorageManager(this.config.server.storage);
|
|
418
|
+
}
|
|
419
|
+
|
|
414
420
|
// Cache 초기화
|
|
415
421
|
await this.initializeCache(this.config.server.cache, forTesting);
|
|
416
422
|
|
|
@@ -490,12 +496,6 @@ class SonamuClass {
|
|
|
490
496
|
this.server = server;
|
|
491
497
|
this.websocketRuntime = new WebSocketRuntime(options.websocket);
|
|
492
498
|
|
|
493
|
-
// Storage 설정 → StorageManager 생성
|
|
494
|
-
if (options.storage) {
|
|
495
|
-
const { StorageManager } = await import("../storage/storage-manager");
|
|
496
|
-
this.storageValue = new StorageManager(options.storage);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
499
|
// 플러그인 등록
|
|
500
500
|
if (options.plugins) {
|
|
501
501
|
await this.registerPlugins(server, options.plugins);
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { knex, type Knex } from "knex";
|
|
2
|
+
import { afterAll, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { Puri } from "../puri";
|
|
5
|
+
|
|
6
|
+
type Schema = { users: { id: number; name: string; nickname: string | null; payload: object } };
|
|
7
|
+
const db = knex({ client: "pg" });
|
|
8
|
+
const query = () => new Puri<Schema, Schema, Schema["users"]>(db, "users");
|
|
9
|
+
|
|
10
|
+
type RawRow = { [column: string]: string | number | boolean | null | RawRow[] };
|
|
11
|
+
|
|
12
|
+
function result(
|
|
13
|
+
builder: Knex.QueryBuilder,
|
|
14
|
+
rows: RawRow | RawRow[] | undefined,
|
|
15
|
+
presence: Record<string, readonly boolean[]> = {},
|
|
16
|
+
) {
|
|
17
|
+
if (vi.isMockFunction(builder.clone)) builder.clone.mockRestore();
|
|
18
|
+
const clone = builder.clone.bind(builder);
|
|
19
|
+
vi.spyOn(builder, "clone").mockImplementation(() => {
|
|
20
|
+
const cloned = clone();
|
|
21
|
+
result(cloned, rows, presence);
|
|
22
|
+
return cloned;
|
|
23
|
+
});
|
|
24
|
+
vi.spyOn(builder, "then").mockImplementation((fulfilled, rejected) => {
|
|
25
|
+
const markers = [
|
|
26
|
+
...builder.toQuery().matchAll(/"([^"]+)"\.\* IS DISTINCT FROM NULL AS "([^"]+)"/g),
|
|
27
|
+
];
|
|
28
|
+
const withPresence = (row: RawRow, index: number) => ({
|
|
29
|
+
...row,
|
|
30
|
+
...Object.fromEntries(
|
|
31
|
+
markers.map(([, alias, column]) => [column, presence[alias]?.[index] ?? true]),
|
|
32
|
+
),
|
|
33
|
+
});
|
|
34
|
+
const output = Array.isArray(rows)
|
|
35
|
+
? rows.map(withPresence)
|
|
36
|
+
: rows
|
|
37
|
+
? withPresence(rows, 0)
|
|
38
|
+
: rows;
|
|
39
|
+
return Promise.resolve(output).then(fulfilled, rejected);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
afterAll(() => db.destroy());
|
|
44
|
+
|
|
45
|
+
describe("Puri 중첩 select 결과", () => {
|
|
46
|
+
it("중첩 select 별칭만 복원하고 평평한 별칭과 JSON 값은 보존한다", async () => {
|
|
47
|
+
const selected = query().select({
|
|
48
|
+
user: { id: "users.id", data: "users.payload" },
|
|
49
|
+
literal__name: "users.name",
|
|
50
|
+
payload: "users.payload",
|
|
51
|
+
});
|
|
52
|
+
const payload = [{ arbitrary__key: "원본" }];
|
|
53
|
+
result(selected.rawQuery(), [
|
|
54
|
+
{ user__id: 1, user__data: payload, literal__name: "이름", payload },
|
|
55
|
+
]);
|
|
56
|
+
const rows = await selected;
|
|
57
|
+
expect(rows).toEqual([{ user: { id: 1, data: payload }, literal__name: "이름", payload }]);
|
|
58
|
+
expect(rows[0]?.user.data).toBe(payload);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("중첩 select의 빈 JSON 배열을 관계 없음으로 바꾸지 않는다", async () => {
|
|
62
|
+
const selected = query().select({ user: { data: "users.payload" } });
|
|
63
|
+
result(selected.rawQuery(), [{ user__data: [] }]);
|
|
64
|
+
expect(await selected).toEqual([{ user: { data: [] } }]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("중첩 단계마다 null 관계를 복원하고 id가 없는 그룹도 처리한다", async () => {
|
|
68
|
+
const selected = query()
|
|
69
|
+
.leftJoin({ user: "users" }, (join) => join.on("users.id", "user.id"))
|
|
70
|
+
.leftJoin({ user__detail: "users" }, (join) => join.on("users.id", "user__detail.id"))
|
|
71
|
+
.select({ user: { id: "user.id", detail: { name: "user__detail.name" } } });
|
|
72
|
+
result(
|
|
73
|
+
selected.rawQuery(),
|
|
74
|
+
[
|
|
75
|
+
{ user__id: 1, user__detail__name: null },
|
|
76
|
+
{ user__id: null, user__detail__name: "무시할 값" },
|
|
77
|
+
],
|
|
78
|
+
{ user: [true, false], user__detail: [false, true] },
|
|
79
|
+
);
|
|
80
|
+
expect(await selected).toEqual([{ user: { id: 1, detail: null } }, { user: null }]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("nullable 필드만 선택한 일반 그룹은 null로 축약하지 않는다", async () => {
|
|
84
|
+
const selected = query().select({ profile: { nickname: "users.nickname" } });
|
|
85
|
+
result(selected.rawQuery(), [{ profile__nickname: null }]);
|
|
86
|
+
expect(await selected).toEqual([{ profile: { nickname: null } }]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("LEFT JOIN 별칭과 다른 이름으로 묶은 그룹은 객체로 유지한다", async () => {
|
|
90
|
+
const selected = query()
|
|
91
|
+
.leftJoin({ colleague: "users" }, (join) => join.on("users.id", "colleague.id"))
|
|
92
|
+
.select({
|
|
93
|
+
profile: { nickname: "colleague.nickname" },
|
|
94
|
+
colleague: { nickname: "colleague.nickname" },
|
|
95
|
+
});
|
|
96
|
+
result(selected.rawQuery(), [{ profile__nickname: null, colleague__nickname: null }], {
|
|
97
|
+
colleague: [false],
|
|
98
|
+
});
|
|
99
|
+
expect(await selected).toEqual([{ profile: { nickname: null }, colleague: null }]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("INNER JOIN 별칭과 같은 그룹도 nullable 필드를 보존한다", async () => {
|
|
103
|
+
const selected = query()
|
|
104
|
+
.join({ colleague: "users" }, "users.id", "colleague.id")
|
|
105
|
+
.select({ colleague: { nickname: "colleague.nickname" } });
|
|
106
|
+
result(selected.rawQuery(), [{ colleague__nickname: null }]);
|
|
107
|
+
expect(await selected).toEqual([{ colleague: { nickname: null } }]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("선택한 값이 모두 null이어도 실제 LEFT JOIN 행이 있으면 객체를 유지한다", async () => {
|
|
111
|
+
const selected = query()
|
|
112
|
+
.leftJoin({ colleague: "users" }, (join) => join.on("users.id", "colleague.id"))
|
|
113
|
+
.select({ colleague: { nickname: "colleague.nickname" } });
|
|
114
|
+
result(selected.rawQuery(), [{ colleague__nickname: null }, { colleague__nickname: null }], {
|
|
115
|
+
colleague: [true, false],
|
|
116
|
+
});
|
|
117
|
+
expect(await selected).toEqual([{ colleague: { nickname: null } }, { colleague: null }]);
|
|
118
|
+
expect(selected.toQuery()).not.toContain("__sonamu_presence_");
|
|
119
|
+
expect(await selected.rawQuery()).toEqual([
|
|
120
|
+
{ colleague__nickname: null },
|
|
121
|
+
{ colleague__nickname: null },
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("그룹화한 no-id projection은 SQL 열을 추가하지 않고 객체를 유지한다", async () => {
|
|
126
|
+
const selected = query()
|
|
127
|
+
.leftJoin({ colleague: "users" }, (join) => join.on("users.id", "colleague.id"))
|
|
128
|
+
.select({ colleague: { nickname: "colleague.nickname" } })
|
|
129
|
+
.groupBy("colleague.nickname");
|
|
130
|
+
result(selected.rawQuery(), [{ colleague__nickname: null }], { colleague: [false] });
|
|
131
|
+
expect(await selected).toEqual([{ colleague: { nickname: null } }]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it.each([false, true])(
|
|
135
|
+
"다른 이름의 관계 그룹도 id 원본 LEFT JOIN을 따른다 (원시 표현식: %s)",
|
|
136
|
+
async (raw) => {
|
|
137
|
+
const selected = query()
|
|
138
|
+
.leftJoin({ staffs: "users" }, (join) => join.on("users.id", "staffs.id"))
|
|
139
|
+
.select({ manager: { id: "staffs.id", name: "staffs.name" } });
|
|
140
|
+
if (raw) selected.appendSelect({ total: Puri.rawNumber("(SELECT COUNT(*) FROM users)") });
|
|
141
|
+
result(
|
|
142
|
+
selected.rawQuery(),
|
|
143
|
+
[
|
|
144
|
+
{ manager__id: 1, manager__name: "담당자" },
|
|
145
|
+
{ manager__id: null, manager__name: null },
|
|
146
|
+
],
|
|
147
|
+
{ staffs: [true, false] },
|
|
148
|
+
);
|
|
149
|
+
expect(await selected.clone()).toEqual([
|
|
150
|
+
{ manager: { id: 1, name: "담당자" } },
|
|
151
|
+
{ manager: null },
|
|
152
|
+
]);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
it("관계 id 선택을 clear한 뒤 일반 nullable id 그룹을 선택하면 관계 의미도 지운다", async () => {
|
|
157
|
+
const selected = query()
|
|
158
|
+
.leftJoin({ staffs: "users" }, (join) => join.on("users.id", "staffs.id"))
|
|
159
|
+
.select({ manager: { id: "staffs.id" } })
|
|
160
|
+
.clear("select")
|
|
161
|
+
.select({ manager: { id: "users.nickname" } });
|
|
162
|
+
result(selected.rawQuery(), [{ manager__id: null }]);
|
|
163
|
+
expect(await selected).toEqual([{ manager: { id: null } }]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("객체 키 안의 구분자는 중첩 경로로 다시 해석하지 않는다", async () => {
|
|
167
|
+
const selected = query().select({ user__profile: { display__name: "users.name" } });
|
|
168
|
+
result(selected.rawQuery(), [{ user__profile__display__name: "이름" }]);
|
|
169
|
+
expect(await selected).toEqual([{ user__profile: { display__name: "이름" } }]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it.each(["then", "catch", "finally"] as const)(
|
|
173
|
+
"%s 실행에서도 중첩 결과를 반환한다",
|
|
174
|
+
async (method) => {
|
|
175
|
+
const selected = query().select({ user: { id: "users.id" } });
|
|
176
|
+
result(selected.rawQuery(), [{ user__id: 1 }]);
|
|
177
|
+
const rows =
|
|
178
|
+
method === "then"
|
|
179
|
+
? await selected.then()
|
|
180
|
+
: method === "catch"
|
|
181
|
+
? await selected.catch()
|
|
182
|
+
: await selected.finally();
|
|
183
|
+
expect(rows).toEqual([{ user: { id: 1 } }]);
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
it.each(["then", "catch", "finally"] as const)(
|
|
188
|
+
"first의 %s 실행과 조회 결과 없음도 처리한다",
|
|
189
|
+
async (method) => {
|
|
190
|
+
const selected = query()
|
|
191
|
+
.select({ user: { id: "users.id" } })
|
|
192
|
+
.first();
|
|
193
|
+
result(selected.knexQuery, { user__id: 1 });
|
|
194
|
+
const row =
|
|
195
|
+
method === "then"
|
|
196
|
+
? await selected.then()
|
|
197
|
+
: method === "catch"
|
|
198
|
+
? await selected.catch()
|
|
199
|
+
: await selected.finally();
|
|
200
|
+
expect(row).toEqual({ user: { id: 1 } });
|
|
201
|
+
result(selected.knexQuery, undefined);
|
|
202
|
+
expect(await selected).toBeUndefined();
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
it.each(["select", "columns"] as const)(
|
|
207
|
+
"clone과 appendSelect는 중첩 경로를 보존하고 clear(%s)는 복제본에만 적용된다",
|
|
208
|
+
async (statement) => {
|
|
209
|
+
const original = query().select({ user: { id: "users.id" } });
|
|
210
|
+
const cloned = original.clone().appendSelect({ detail: { name: "users.name" } });
|
|
211
|
+
result(cloned.rawQuery(), [{ user__id: 1, detail__name: "이름" }]);
|
|
212
|
+
expect(await cloned).toEqual([{ user: { id: 1 }, detail: { name: "이름" } }]);
|
|
213
|
+
cloned.clear(statement).select({ user__id: "users.id" });
|
|
214
|
+
result(cloned.rawQuery(), [{ user__id: 2 }]);
|
|
215
|
+
expect(await cloned).toEqual([{ user__id: 2 }]);
|
|
216
|
+
result(original.rawQuery(), [{ user__id: 1 }]);
|
|
217
|
+
expect(await original).toEqual([{ user: { id: 1 } }]);
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
it("rawQuery는 기존의 평평한 SQL 결과를 그대로 반환한다", async () => {
|
|
222
|
+
const selected = query().select({ user: { id: "users.id" } });
|
|
223
|
+
result(selected.rawQuery(), [{ user__id: 1 }]);
|
|
224
|
+
expect(await selected.rawQuery()).toEqual([{ user__id: 1 }]);
|
|
225
|
+
});
|
|
226
|
+
});
|