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.
Files changed (46) hide show
  1. package/dist/api/decorators.d.ts +2 -3
  2. package/dist/api/decorators.d.ts.map +1 -1
  3. package/dist/api/decorators.js +1 -1
  4. package/dist/api/sonamu.d.ts.map +1 -1
  5. package/dist/api/sonamu.js +5 -5
  6. package/dist/database/base-model.d.ts.map +1 -1
  7. package/dist/database/base-model.js +8 -3
  8. package/dist/database/puri.d.ts +8 -3
  9. package/dist/database/puri.d.ts.map +1 -1
  10. package/dist/database/puri.js +120 -15
  11. package/dist/database/puri.types.d.ts +5 -2
  12. package/dist/database/puri.types.d.ts.map +1 -1
  13. package/dist/database/puri.types.js +1 -1
  14. package/dist/dict/sonamu-dictionary.d.ts.map +1 -1
  15. package/dist/dict/sonamu-dictionary.js +24 -9
  16. package/dist/storage/buffered-file.d.ts +3 -3
  17. package/dist/storage/buffered-file.d.ts.map +1 -1
  18. package/dist/storage/buffered-file.js +2 -2
  19. package/dist/storage/index.d.ts +1 -1
  20. package/dist/storage/index.d.ts.map +1 -1
  21. package/dist/storage/index.js +1 -1
  22. package/dist/storage/storage-manager.d.ts +3 -4
  23. package/dist/storage/storage-manager.d.ts.map +1 -1
  24. package/dist/storage/storage-manager.js +2 -2
  25. package/dist/storage/types.d.ts +3 -2
  26. package/dist/storage/types.d.ts.map +1 -1
  27. package/dist/storage/uploaded-file.d.ts +3 -3
  28. package/dist/storage/uploaded-file.d.ts.map +1 -1
  29. package/dist/storage/uploaded-file.js +1 -1
  30. package/package.json +5 -2
  31. package/src/api/__tests__/sonamu.storage.test.ts +145 -0
  32. package/src/api/decorators.ts +2 -3
  33. package/src/api/sonamu.ts +6 -6
  34. package/src/database/__tests__/puri-hydration.test.ts +226 -0
  35. package/src/database/base-model.ts +7 -3
  36. package/src/database/puri.ts +204 -15
  37. package/src/database/puri.types.test-d.ts +29 -0
  38. package/src/database/puri.types.ts +10 -2
  39. package/src/dict/sonamu-dictionary.ts +28 -6
  40. package/src/storage/__tests__/storage-manager.test.ts +64 -0
  41. package/src/storage/buffered-file.ts +3 -3
  42. package/src/storage/index.ts +1 -1
  43. package/src/storage/storage-manager.ts +4 -5
  44. package/src/storage/storage.test-d.ts +47 -0
  45. package/src/storage/types.ts +3 -2
  46. 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 DriverKey } from "./drivers";
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 디스크 이름 ("fs" 또는 "s3")
18
+ * @param diskName 설정에 등록한 디스크 이름
19
19
  * @param key 저장 경로 (예: 'uploads/avatar.png')
20
20
  * @returns 저장된 파일의 URL
21
21
  */
22
- saveToDisk(diskName: DriverKey, key: string): Promise<string>;
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,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;;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,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclE;IAED,0BAA0B;IAC1B,IAAI,GAAG,IAAI,aAAa,CAEvB;CACF"}
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 디스크 이름 ("fs" 또는 "s3")
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVmZmVyZWQtZmlsZS5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi9zcmMvc3RvcmFnZS9idWZmZXJlZC1maWxlLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNyZWF0ZUhhc2ggfSBmcm9tIFwiY3J5cHRvXCI7XG5cbmltcG9ydCB7IHR5cGUgTXVsdGlwYXJ0RmlsZSB9IGZyb20gXCJAZmFzdGlmeS9tdWx0aXBhcnRcIjtcblxuaW1wb3J0IHsgQmFzZUZpbGUgfSBmcm9tIFwiLi9iYXNlLWZpbGVcIjtcbmltcG9ydCB7IHR5cGUgRHJpdmVyS2V5IH0gZnJvbSBcIi4vZHJpdmVyc1wiO1xuXG4vKipcbiAqIEJ1ZmZlciDrqqjrk5zroZwg7JeF66Gc65Oc65CcIO2MjOydvFxuICog66mU66qo66as7JeQIOuhnOuTnOuQnCDsg4Htg5zroZwsIG1kNSDqs4TsgrDsnbTrgpgg7J2066+47KeAIOyymOumrCDrk7Eg7Jyg7Jew7ZWcIOyekeyXheydtCDqsIDriqXtlanri4jri6QuXG4gKi9cbmV4cG9ydCBjbGFzcyBCdWZmZXJlZEZpbGUgZXh0ZW5kcyBCYXNlRmlsZSB7XG4gIHByaXZhdGUgZmlsZVZhbHVlOiBNdWx0aXBhcnRGaWxlO1xuICBwcml2YXRlIGJ1ZmZlclZhbHVlOiBCdWZmZXI7XG5cbiAgY29uc3RydWN0b3IoZmlsZTogTXVsdGlwYXJ0RmlsZSwgYnVmZmVyOiBCdWZmZXIpIHtcbiAgICBzdXBlcih7XG4gICAgICBmaWxlbmFtZTogZmlsZS5maWxlbmFtZSxcbiAgICAgIG1pbWV0eXBlOiBmaWxlLm1pbWV0eXBlLFxuICAgICAgc2l6ZTogYnVmZmVyLmxlbmd0aCxcbiAgICB9KTtcbiAgICB0aGlzLmZpbGVWYWx1ZSA9IGZpbGU7XG4gICAgdGhpcy5idWZmZXJWYWx1ZSA9IGJ1ZmZlcjtcbiAgfVxuXG4gIC8qKiDtjIzsnbwgQnVmZmVyICovXG4gIGdldCBidWZmZXIoKTogQnVmZmVyIHtcbiAgICByZXR1cm4gdGhpcy5idWZmZXJWYWx1ZTtcbiAgfVxuXG4gIC8qKiBNRDUg7ZW07IucIOqzhOyCsCAqL1xuICBhc3luYyBtZDUoKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgICByZXR1cm4gY3JlYXRlSGFzaChcIm1kNVwiKS51cGRhdGUodGhpcy5idWZmZXJWYWx1ZSkuZGlnZXN0KFwiaGV4XCIpO1xuICB9XG5cbiAgLyoqXG4gICAqIO2MjOydvOydhCDrlJTsiqTtgazsl5Ag7KCA7J6lXG4gICAqIEBwYXJhbSBkaXNrTmFtZSDrlJTsiqTtgawg7J2066aEIChcImZzXCIg65iQ64qUIFwiczNcIilcbiAgICogQHBhcmFtIGtleSDsoIDsnqUg6rK966GcICjsmIg6ICd1cGxvYWRzL2F2YXRhci5wbmcnKVxuICAgKiBAcmV0dXJucyDsoIDsnqXrkJwg7YyM7J287J2YIFVSTFxuICAgKi9cbiAgYXN5bmMgc2F2ZVRvRGlzayhkaXNrTmFtZTogRHJpdmVyS2V5LCBrZXk6IHN0cmluZyk6IFByb21pc2U8c3RyaW5nPiB7XG4gICAgLy8g7Iic7ZmYIOydmOyhtOyEsSDrsKnsp4Drpbwg7JyE7ZW0IOuPmeyggSBpbXBvcnRcbiAgICBjb25zdCB7IFNvbmFtdSB9ID0gYXdhaXQgaW1wb3J0KFwiLi4vYXBpL3NvbmFtdVwiKTtcbiAgICBjb25zdCBkaXNrID0gU29uYW11LnN0b3JhZ2UudXNlKGRpc2tOYW1lKTtcblxuICAgIGF3YWl0IGRpc2sucHV0KGtleSwgbmV3IFVpbnQ4QXJyYXkodGhpcy5idWZmZXJWYWx1ZSksIHtcbiAgICAgIGNvbnRlbnRUeXBlOiB0aGlzLm1pbWV0eXBlLFxuICAgIH0pO1xuXG4gICAgdGhpc1tcIl91cmxcIl0gPSBhd2FpdCBkaXNrLmdldFVybChrZXkpO1xuICAgIHRoaXNbXCJfc2lnbmVkVXJsXCJdID0gYXdhaXQgZGlzay5nZXRTaWduZWRVcmwoa2V5KTtcblxuICAgIC8vIHNpZ25lZCB1cmzsnYAg66eM66OMIOyLnOqwhOydtCDsnojquLAg65WM66y47JeQLCB1bnNpZ25lZCB1cmzsnYQg67CY7ZmY7ZWp64uI64ukLlxuICAgIHJldHVybiB0aGlzW1wiX3VybFwiXTtcbiAgfVxuXG4gIC8qKiDsm5Drs7ggTXVsdGlwYXJ0RmlsZSDsoJHqt7wgKi9cbiAgZ2V0IHJhdygpOiBNdWx0aXBhcnRGaWxlIHtcbiAgICByZXR1cm4gdGhpcy5maWxlVmFsdWU7XG4gIH1cbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7O0NBSXNDLGVBQUE7Q0FPekIsZUFBYixjQUFrQyxTQUFTO0VBQ3pDO0VBQ0E7RUFFQSxZQUFZLE1BQXFCLFFBQWdCO0dBQy9DLE1BQU07SUFDSixVQUFVLEtBQUs7SUFDZixVQUFVLEtBQUs7SUFDZixNQUFNLE9BQU87R0FDZixDQUFDO0dBQ0QsS0FBSyxZQUFZO0dBQ2pCLEtBQUssY0FBYztFQUNyQjs7RUFHQSxJQUFJLFNBQWlCO0dBQ25CLE9BQU8sS0FBSztFQUNkOztFQUdBLE1BQU0sTUFBdUI7R0FDM0IsT0FBTyxXQUFXLEtBQUssQ0FBQyxDQUFDLE9BQU8sS0FBSyxXQUFXLENBQUMsQ0FBQyxPQUFPLEtBQUs7RUFDaEU7Ozs7Ozs7RUFRQSxNQUFNLFdBQVcsVUFBcUIsS0FBOEI7R0FFbEUsTUFBTSxFQUFFLFdBQVcsTUFBTSxPQUFPO0dBQ2hDLE1BQU0sT0FBTyxPQUFPLFFBQVEsSUFBSSxRQUFRO0dBRXhDLE1BQU0sS0FBSyxJQUFJLEtBQUssSUFBSSxXQUFXLEtBQUssV0FBVyxHQUFHLEVBQ3BELGFBQWEsS0FBSyxTQUNwQixDQUFDO0dBRUQsS0FBSyxVQUFVLE1BQU0sS0FBSyxPQUFPLEdBQUc7R0FDcEMsS0FBSyxnQkFBZ0IsTUFBTSxLQUFLLGFBQWEsR0FBRztHQUdoRCxPQUFPLEtBQUs7RUFDZDs7RUFHQSxJQUFJLE1BQXFCO0dBQ3ZCLE9BQU8sS0FBSztFQUNkO0NBQ0YifQ==
51
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVmZmVyZWQtZmlsZS5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi9zcmMvc3RvcmFnZS9idWZmZXJlZC1maWxlLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNyZWF0ZUhhc2ggfSBmcm9tIFwiY3J5cHRvXCI7XG5cbmltcG9ydCB7IHR5cGUgTXVsdGlwYXJ0RmlsZSB9IGZyb20gXCJAZmFzdGlmeS9tdWx0aXBhcnRcIjtcblxuaW1wb3J0IHsgQmFzZUZpbGUgfSBmcm9tIFwiLi9iYXNlLWZpbGVcIjtcbmltcG9ydCB7IHR5cGUgRGlza05hbWUgfSBmcm9tIFwiLi90eXBlc1wiO1xuXG4vKipcbiAqIEJ1ZmZlciDrqqjrk5zroZwg7JeF66Gc65Oc65CcIO2MjOydvFxuICog66mU66qo66as7JeQIOuhnOuTnOuQnCDsg4Htg5zroZwsIG1kNSDqs4TsgrDsnbTrgpgg7J2066+47KeAIOyymOumrCDrk7Eg7Jyg7Jew7ZWcIOyekeyXheydtCDqsIDriqXtlanri4jri6QuXG4gKi9cbmV4cG9ydCBjbGFzcyBCdWZmZXJlZEZpbGUgZXh0ZW5kcyBCYXNlRmlsZSB7XG4gIHByaXZhdGUgZmlsZVZhbHVlOiBNdWx0aXBhcnRGaWxlO1xuICBwcml2YXRlIGJ1ZmZlclZhbHVlOiBCdWZmZXI7XG5cbiAgY29uc3RydWN0b3IoZmlsZTogTXVsdGlwYXJ0RmlsZSwgYnVmZmVyOiBCdWZmZXIpIHtcbiAgICBzdXBlcih7XG4gICAgICBmaWxlbmFtZTogZmlsZS5maWxlbmFtZSxcbiAgICAgIG1pbWV0eXBlOiBmaWxlLm1pbWV0eXBlLFxuICAgICAgc2l6ZTogYnVmZmVyLmxlbmd0aCxcbiAgICB9KTtcbiAgICB0aGlzLmZpbGVWYWx1ZSA9IGZpbGU7XG4gICAgdGhpcy5idWZmZXJWYWx1ZSA9IGJ1ZmZlcjtcbiAgfVxuXG4gIC8qKiDtjIzsnbwgQnVmZmVyICovXG4gIGdldCBidWZmZXIoKTogQnVmZmVyIHtcbiAgICByZXR1cm4gdGhpcy5idWZmZXJWYWx1ZTtcbiAgfVxuXG4gIC8qKiBNRDUg7ZW07IucIOqzhOyCsCAqL1xuICBhc3luYyBtZDUoKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgICByZXR1cm4gY3JlYXRlSGFzaChcIm1kNVwiKS51cGRhdGUodGhpcy5idWZmZXJWYWx1ZSkuZGlnZXN0KFwiaGV4XCIpO1xuICB9XG5cbiAgLyoqXG4gICAqIO2MjOydvOydhCDrlJTsiqTtgazsl5Ag7KCA7J6lXG4gICAqIEBwYXJhbSBkaXNrTmFtZSDshKTsoJXsl5Ag65Ox66Gd7ZWcIOuUlOyKpO2BrCDsnbTrpoRcbiAgICogQHBhcmFtIGtleSDsoIDsnqUg6rK966GcICjsmIg6ICd1cGxvYWRzL2F2YXRhci5wbmcnKVxuICAgKiBAcmV0dXJucyDsoIDsnqXrkJwg7YyM7J287J2YIFVSTFxuICAgKi9cbiAgYXN5bmMgc2F2ZVRvRGlzayhkaXNrTmFtZTogRGlza05hbWUsIGtleTogc3RyaW5nKTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgICAvLyDsiJztmZgg7J2Y7KG07ISxIOuwqeyngOulvCDsnITtlbQg64+Z7KCBIGltcG9ydFxuICAgIGNvbnN0IHsgU29uYW11IH0gPSBhd2FpdCBpbXBvcnQoXCIuLi9hcGkvc29uYW11XCIpO1xuICAgIGNvbnN0IGRpc2sgPSBTb25hbXUuc3RvcmFnZS51c2UoZGlza05hbWUpO1xuXG4gICAgYXdhaXQgZGlzay5wdXQoa2V5LCBuZXcgVWludDhBcnJheSh0aGlzLmJ1ZmZlclZhbHVlKSwge1xuICAgICAgY29udGVudFR5cGU6IHRoaXMubWltZXR5cGUsXG4gICAgfSk7XG5cbiAgICB0aGlzW1wiX3VybFwiXSA9IGF3YWl0IGRpc2suZ2V0VXJsKGtleSk7XG4gICAgdGhpc1tcIl9zaWduZWRVcmxcIl0gPSBhd2FpdCBkaXNrLmdldFNpZ25lZFVybChrZXkpO1xuXG4gICAgLy8gc2lnbmVkIHVybOydgCDrp4zro4wg7Iuc6rCE7J20IOyeiOq4sCDrlYzrrLjsl5AsIHVuc2lnbmVkIHVybOydhCDrsJjtmZjtlanri4jri6QuXG4gICAgcmV0dXJuIHRoaXNbXCJfdXJsXCJdO1xuICB9XG5cbiAgLyoqIOybkOuzuCBNdWx0aXBhcnRGaWxlIOygkeq3vCAqL1xuICBnZXQgcmF3KCk6IE11bHRpcGFydEZpbGUge1xuICAgIHJldHVybiB0aGlzLmZpbGVWYWx1ZTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Q0FJc0MsZUFBQTtDQU96QixlQUFiLGNBQWtDLFNBQVM7RUFDekM7RUFDQTtFQUVBLFlBQVksTUFBcUIsUUFBZ0I7R0FDL0MsTUFBTTtJQUNKLFVBQVUsS0FBSztJQUNmLFVBQVUsS0FBSztJQUNmLE1BQU0sT0FBTztHQUNmLENBQUM7R0FDRCxLQUFLLFlBQVk7R0FDakIsS0FBSyxjQUFjO0VBQ3JCOztFQUdBLElBQUksU0FBaUI7R0FDbkIsT0FBTyxLQUFLO0VBQ2Q7O0VBR0EsTUFBTSxNQUF1QjtHQUMzQixPQUFPLFdBQVcsS0FBSyxDQUFDLENBQUMsT0FBTyxLQUFLLFdBQVcsQ0FBQyxDQUFDLE9BQU8sS0FBSztFQUNoRTs7Ozs7OztFQVFBLE1BQU0sV0FBVyxVQUFvQixLQUE4QjtHQUVqRSxNQUFNLEVBQUUsV0FBVyxNQUFNLE9BQU87R0FDaEMsTUFBTSxPQUFPLE9BQU8sUUFBUSxJQUFJLFFBQVE7R0FFeEMsTUFBTSxLQUFLLElBQUksS0FBSyxJQUFJLFdBQVcsS0FBSyxXQUFXLEdBQUcsRUFDcEQsYUFBYSxLQUFLLFNBQ3BCLENBQUM7R0FFRCxLQUFLLFVBQVUsTUFBTSxLQUFLLE9BQU8sR0FBRztHQUNwQyxLQUFLLGdCQUFnQixNQUFNLEtBQUssYUFBYSxHQUFHO0dBR2hELE9BQU8sS0FBSztFQUNkOztFQUdBLElBQUksTUFBcUI7R0FDdkIsT0FBTyxLQUFLO0VBQ2Q7Q0FDRiJ9
@@ -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;AAC3D,OAAO,EAAE,YAAY,IAAI,YAAY,EAAE,MAAM,iBAAiB,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"}
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3N0b3JhZ2UvaW5kZXgudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gU3RvcmFnZSDshJzruIzrqqjrk4ggZXhwb3J0c1xuZXhwb3J0IHsgQmFzZUZpbGUgfSBmcm9tIFwiLi9iYXNlLWZpbGVcIjtcbmV4cG9ydCB7IEJ1ZmZlcmVkRmlsZSB9IGZyb20gXCIuL2J1ZmZlcmVkLWZpbGVcIjtcbmV4cG9ydCB7IHR5cGUgRHJpdmVyS2V5LCBkcml2ZXJzIH0gZnJvbSBcIi4vZHJpdmVyc1wiO1xuZXhwb3J0IHsgU3RvcmFnZU1hbmFnZXIgfSBmcm9tIFwiLi9zdG9yYWdlLW1hbmFnZXJcIjtcbmV4cG9ydCB0eXBlIHsgS2V5R2VuZXJhdG9yLCBTdG9yYWdlQ29uZmlnIH0gZnJvbSBcIi4vdHlwZXNcIjtcbmV4cG9ydCB7IFVwbG9hZGVkRmlsZSBhcyBTdHJlYW1lZEZpbGUgfSBmcm9tIFwiLi91cGxvYWRlZC1maWxlXCI7XG4iXSwibWFwcGluZ3MiOiI7Ozs7OztBQUNzQyxlQUFBO0FBQ1EsbUJBQUE7QUFFSSxxQkFBQTtBQUVZLG1CQUFBIn0=
13
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3N0b3JhZ2UvaW5kZXgudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gU3RvcmFnZSDshJzruIzrqqjrk4ggZXhwb3J0c1xuZXhwb3J0IHsgQmFzZUZpbGUgfSBmcm9tIFwiLi9iYXNlLWZpbGVcIjtcbmV4cG9ydCB7IEJ1ZmZlcmVkRmlsZSB9IGZyb20gXCIuL2J1ZmZlcmVkLWZpbGVcIjtcbmV4cG9ydCB7IHR5cGUgRHJpdmVyS2V5LCBkcml2ZXJzIH0gZnJvbSBcIi4vZHJpdmVyc1wiO1xuZXhwb3J0IHsgU3RvcmFnZU1hbmFnZXIgfSBmcm9tIFwiLi9zdG9yYWdlLW1hbmFnZXJcIjtcbmV4cG9ydCB0eXBlIHsgRGlza05hbWUsIEtleUdlbmVyYXRvciwgU3RvcmFnZUNvbmZpZyB9IGZyb20gXCIuL3R5cGVzXCI7XG5leHBvcnQgeyBVcGxvYWRlZEZpbGUgYXMgU3RyZWFtZWRGaWxlIH0gZnJvbSBcIi4vdXBsb2FkZWQtZmlsZVwiO1xuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7QUFDc0MsZUFBQTtBQUNRLG1CQUFBO0FBRUkscUJBQUE7QUFFWSxtQkFBQSJ9
@@ -1,6 +1,5 @@
1
1
  import { Disk } from "flydrive";
2
- import { type DriverKey } from "./drivers";
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 디스크 이름 (없으면 default)
12
+ * @param diskName 설정에 등록한 디스크 이름
14
13
  */
15
- use(diskName: DriverKey): Disk;
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,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;GAEG;AACH,qBAAa,cAAc;IAGb,OAAO,CAAC,MAAM;IAF1B,OAAO,CAAC,KAAK,CAAmC;IAEhD,YAAoB,MAAM,EAAE,aAAa,EAAI;IAE7C;;;OAGG;IACH,GAAG,CAAC,QAAQ,EAAE,SAAS,GAAG,IAAI,CAW7B;CACF"}
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 디스크 이름 (없으면 default)
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS1tYW5hZ2VyLmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zdG9yYWdlL3N0b3JhZ2UtbWFuYWdlci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBEaXNrIH0gZnJvbSBcImZseWRyaXZlXCI7XG5cbmltcG9ydCB7IGFzc2VydERlZmluZWQgfSBmcm9tIFwiLi4vdXRpbHMvdXRpbHNcIjtcbmltcG9ydCB7IHR5cGUgRHJpdmVyS2V5IH0gZnJvbSBcIi4vZHJpdmVyc1wiO1xuaW1wb3J0IHsgdHlwZSBTdG9yYWdlQ29uZmlnIH0gZnJvbSBcIi4vdHlwZXNcIjtcblxuLyoqXG4gKiDsl6zrn6wg65SU7Iqk7YGs66W8IOq0gOumrO2VmOuKlCDrp6Tri4jsoIBcbiAqL1xuZXhwb3J0IGNsYXNzIFN0b3JhZ2VNYW5hZ2VyIHtcbiAgcHJpdmF0ZSBkaXNrczogTWFwPERyaXZlcktleSwgRGlzaz4gPSBuZXcgTWFwKCk7XG5cbiAgY29uc3RydWN0b3IocHJpdmF0ZSBjb25maWc6IFN0b3JhZ2VDb25maWcpIHt9XG5cbiAgLyoqXG4gICAqIOuUlOyKpO2BrCDsnbjsiqTthLTsiqQg67CY7ZmYIChsYXp5IGluaXRpYWxpemF0aW9uKVxuICAgKiBAcGFyYW0gZGlza05hbWUg65SU7Iqk7YGsIOydtOumhCAo7JeG7Jy866m0IGRlZmF1bHQpXG4gICAqL1xuICB1c2UoZGlza05hbWU6IERyaXZlcktleSk6IERpc2sge1xuICAgIGlmICghdGhpcy5kaXNrcy5oYXMoZGlza05hbWUpKSB7XG4gICAgICBjb25zdCBmYWN0b3J5ID0gdGhpcy5jb25maWcuZHJpdmVyc1tkaXNrTmFtZV07XG4gICAgICBpZiAoIWZhY3RvcnkpIHtcbiAgICAgICAgY29uc3QgYXZhaWxhYmxlID0gT2JqZWN0LmtleXModGhpcy5jb25maWcuZHJpdmVycykuam9pbihcIiwgXCIpO1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFVua25vd24gZGlzazogXCIke2Rpc2tOYW1lfVwiLiBBdmFpbGFibGU6ICR7YXZhaWxhYmxlfWApO1xuICAgICAgfVxuICAgICAgdGhpcy5kaXNrcy5zZXQoZGlza05hbWUsIG5ldyBEaXNrKGZhY3RvcnkoKSkpO1xuICAgIH1cblxuICAgIHJldHVybiBhc3NlcnREZWZpbmVkKHRoaXMuZGlza3MuZ2V0KGRpc2tOYW1lKSwgYERpc2sgJHtkaXNrTmFtZX0gbm90IGZvdW5kYCk7XG4gIH1cbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7O0NBRThDLFdBQUE7Q0FPakMsaUJBQWIsTUFBNEI7RUFHTjtFQUZwQix3QkFBc0MsSUFBSSxJQUFJO0VBRTlDLFlBQVksUUFBK0I7R0FBdkIsS0FBQSxTQUFBO0VBQXdCOzs7OztFQU01QyxJQUFJLFVBQTJCO0dBQzdCLElBQUksQ0FBQyxLQUFLLE1BQU0sSUFBSSxRQUFRLEdBQUc7SUFDN0IsTUFBTSxVQUFVLEtBQUssT0FBTyxRQUFRO0lBQ3BDLElBQUksQ0FBQyxTQUFTO0tBQ1osTUFBTSxZQUFZLE9BQU8sS0FBSyxLQUFLLE9BQU8sT0FBTyxDQUFDLENBQUMsS0FBSyxJQUFJO0tBQzVELE1BQU0sSUFBSSxNQUFNLGtCQUFrQixTQUFTLGdCQUFnQixXQUFXO0lBQ3hFO0lBQ0EsS0FBSyxNQUFNLElBQUksVUFBVSxJQUFJLEtBQUssUUFBUSxDQUFDLENBQUM7R0FDOUM7R0FFQSxPQUFPLGNBQWMsS0FBSyxNQUFNLElBQUksUUFBUSxHQUFHLFFBQVEsU0FBUyxXQUFXO0VBQzdFO0NBQ0YifQ==
34
+ //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS1tYW5hZ2VyLmpzIiwibmFtZXMiOltdLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zdG9yYWdlL3N0b3JhZ2UtbWFuYWdlci50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBEaXNrIH0gZnJvbSBcImZseWRyaXZlXCI7XG5cbmltcG9ydCB7IGFzc2VydERlZmluZWQgfSBmcm9tIFwiLi4vdXRpbHMvdXRpbHNcIjtcbmltcG9ydCB7IHR5cGUgRGlza05hbWUsIHR5cGUgU3RvcmFnZUNvbmZpZyB9IGZyb20gXCIuL3R5cGVzXCI7XG5cbi8qKlxuICog7Jes65+sIOuUlOyKpO2BrOulvCDqtIDrpqztlZjripQg66ek64uI7KCAXG4gKi9cbmV4cG9ydCBjbGFzcyBTdG9yYWdlTWFuYWdlciB7XG4gIHByaXZhdGUgZGlza3M6IE1hcDxEaXNrTmFtZSwgRGlzaz4gPSBuZXcgTWFwKCk7XG5cbiAgY29uc3RydWN0b3IocHJpdmF0ZSBjb25maWc6IFN0b3JhZ2VDb25maWcpIHt9XG5cbiAgLyoqXG4gICAqIOuUlOyKpO2BrCDsnbjsiqTthLTsiqQg67CY7ZmYIChsYXp5IGluaXRpYWxpemF0aW9uKVxuICAgKiBAcGFyYW0gZGlza05hbWUg7ISk7KCV7JeQIOuTseuhne2VnCDrlJTsiqTtgawg7J2066aEXG4gICAqL1xuICB1c2UoZGlza05hbWU6IERpc2tOYW1lKTogRGlzayB7XG4gICAgaWYgKCF0aGlzLmRpc2tzLmhhcyhkaXNrTmFtZSkpIHtcbiAgICAgIGNvbnN0IGZhY3RvcnkgPSB0aGlzLmNvbmZpZy5kcml2ZXJzW2Rpc2tOYW1lXTtcbiAgICAgIGlmICghZmFjdG9yeSkge1xuICAgICAgICBjb25zdCBhdmFpbGFibGUgPSBPYmplY3Qua2V5cyh0aGlzLmNvbmZpZy5kcml2ZXJzKS5qb2luKFwiLCBcIik7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgVW5rbm93biBkaXNrOiBcIiR7ZGlza05hbWV9XCIuIEF2YWlsYWJsZTogJHthdmFpbGFibGV9YCk7XG4gICAgICB9XG4gICAgICB0aGlzLmRpc2tzLnNldChkaXNrTmFtZSwgbmV3IERpc2soZmFjdG9yeSgpKSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIGFzc2VydERlZmluZWQodGhpcy5kaXNrcy5nZXQoZGlza05hbWUpLCBgRGlzayAke2Rpc2tOYW1lfSBub3QgZm91bmRgKTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Q0FFOEMsV0FBQTtDQU1qQyxpQkFBYixNQUE0QjtFQUdOO0VBRnBCLHdCQUFxQyxJQUFJLElBQUk7RUFFN0MsWUFBWSxRQUErQjtHQUF2QixLQUFBLFNBQUE7RUFBd0I7Ozs7O0VBTTVDLElBQUksVUFBMEI7R0FDNUIsSUFBSSxDQUFDLEtBQUssTUFBTSxJQUFJLFFBQVEsR0FBRztJQUM3QixNQUFNLFVBQVUsS0FBSyxPQUFPLFFBQVE7SUFDcEMsSUFBSSxDQUFDLFNBQVM7S0FDWixNQUFNLFlBQVksT0FBTyxLQUFLLEtBQUssT0FBTyxPQUFPLENBQUMsQ0FBQyxLQUFLLElBQUk7S0FDNUQsTUFBTSxJQUFJLE1BQU0sa0JBQWtCLFNBQVMsZ0JBQWdCLFdBQVc7SUFDeEU7SUFDQSxLQUFLLE1BQU0sSUFBSSxVQUFVLElBQUksS0FBSyxRQUFRLENBQUMsQ0FBQztHQUM5QztHQUVBLE9BQU8sY0FBYyxLQUFLLE1BQU0sSUFBSSxRQUFRLEdBQUcsUUFBUSxTQUFTLFdBQVc7RUFDN0U7Q0FDRiJ9
@@ -1,5 +1,6 @@
1
1
  import { type DriverContract } from "flydrive/types";
2
- import { type DriverKey } from "./drivers";
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<DriverKey, () => DriverContract>;
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,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;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,SAAS,EAAE,MAAM,cAAc,CAAC,CAAC;IACjD,wDAAwD;IACxD,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC"}
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 DriverKey } from "./drivers";
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: DriverKey;
17
+ diskName: DiskName;
18
18
  });
19
19
  /** 저장소 내 키 */
20
20
  get key(): string;
21
21
  /** 저장된 디스크 이름 */
22
- get diskName(): DriverKey;
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,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C;;;GAGG;AACH,qBAAa,YAAa,SAAQ,QAAQ;IACxC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAY;IAEjC,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,SAAS,CAAC;KACrB,EAUA;IAED,cAAc;IACd,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,iBAAiB;IACjB,IAAI,QAAQ,IAAI,SAAS,CAExB;IAED;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAMhC;CACF"}
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBsb2FkZWQtZmlsZS5qcyIsIm5hbWVzIjpbXSwic291cmNlcyI6WyIuLi8uLi9zcmMvc3RvcmFnZS91cGxvYWRlZC1maWxlLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IEJhc2VGaWxlIH0gZnJvbSBcIi4vYmFzZS1maWxlXCI7XG5pbXBvcnQgeyB0eXBlIERyaXZlcktleSB9IGZyb20gXCIuL2RyaXZlcnNcIjtcblxuLyoqXG4gKiBTdHJlYW0g66qo65Oc66GcIOyXheuhnOuTnOuQnCDtjIzsnbxcbiAqIOydtOuvuCDsoIDsnqXshozsl5Ag7Iqk7Yq466as67CNIOyZhOujjOuQnCDsg4Htg5zroZwsIHVybC9rZXkg65OxIOuplO2DgOuNsOydtO2EsOunjCDsoJHqt7wg6rCA64ql7ZWp64uI64ukLlxuICovXG5leHBvcnQgY2xhc3MgVXBsb2FkZWRGaWxlIGV4dGVuZHMgQmFzZUZpbGUge1xuICBwcml2YXRlIGtleVZhbHVlOiBzdHJpbmc7XG4gIHByaXZhdGUgZGlza05hbWVWYWx1ZTogRHJpdmVyS2V5O1xuXG4gIGNvbnN0cnVjdG9yKHBhcmFtczoge1xuICAgIGZpbGVuYW1lOiBzdHJpbmc7XG4gICAgbWltZXR5cGU6IHN0cmluZztcbiAgICBzaXplOiBudW1iZXI7XG4gICAgdXJsOiBzdHJpbmc7XG4gICAgc2lnbmVkVXJsOiBzdHJpbmc7XG4gICAga2V5OiBzdHJpbmc7XG4gICAgZGlza05hbWU6IERyaXZlcktleTtcbiAgfSkge1xuICAgIHN1cGVyKHtcbiAgICAgIGZpbGVuYW1lOiBwYXJhbXMuZmlsZW5hbWUsXG4gICAgICBtaW1ldHlwZTogcGFyYW1zLm1pbWV0eXBlLFxuICAgICAgc2l6ZTogcGFyYW1zLnNpemUsXG4gICAgICB1cmw6IHBhcmFtcy51cmwsXG4gICAgICBzaWduZWRVcmw6IHBhcmFtcy5zaWduZWRVcmwsXG4gICAgfSk7XG4gICAgdGhpcy5rZXlWYWx1ZSA9IHBhcmFtcy5rZXk7XG4gICAgdGhpcy5kaXNrTmFtZVZhbHVlID0gcGFyYW1zLmRpc2tOYW1lO1xuICB9XG5cbiAgLyoqIOyggOyepeyGjCDrgrQg7YKkICovXG4gIGdldCBrZXkoKTogc3RyaW5nIHtcbiAgICByZXR1cm4gdGhpcy5rZXlWYWx1ZTtcbiAgfVxuXG4gIC8qKiDsoIDsnqXrkJwg65SU7Iqk7YGsIOydtOumhCAqL1xuICBnZXQgZGlza05hbWUoKTogRHJpdmVyS2V5IHtcbiAgICByZXR1cm4gdGhpcy5kaXNrTmFtZVZhbHVlO1xuICB9XG5cbiAgLyoqXG4gICAqIOyggOyepeyGjOyXkOyEnCDtjIzsnbzsnYQg64uk7Jq066Gc65Oc7ZWp64uI64ukLlxuICAgKiDsiqTtirjrprwg66qo65Oc66GcIOyXheuhnOuTnOuQnCDtjIzsnbzsnYQg64KY7KSR7JeQIOyymOumrO2VtOyVvCDtlaAg65WMIOyCrOyaqe2VqeuLiOuLpC5cbiAgICovXG4gIGFzeW5jIGRvd25sb2FkKCk6IFByb21pc2U8QnVmZmVyPiB7XG4gICAgY29uc3QgeyBTb25hbXUgfSA9IGF3YWl0IGltcG9ydChcIi4uL2FwaS9zb25hbXVcIik7XG4gICAgY29uc3QgZGlzayA9IFNvbmFtdS5zdG9yYWdlLnVzZSh0aGlzLmRpc2tOYW1lVmFsdWUpO1xuXG4gICAgY29uc3QgdWludDhBcnJheSA9IGF3YWl0IGRpc2suZ2V0KHRoaXMua2V5VmFsdWUpO1xuICAgIHJldHVybiBCdWZmZXIuZnJvbSh1aW50OEFycmF5KTtcbiAgfVxufVxuIl0sIm1hcHBpbmdzIjoiOzs7OztDQUFzQyxlQUFBO0NBT3pCLGVBQWIsY0FBa0MsU0FBUztFQUN6QztFQUNBO0VBRUEsWUFBWSxRQVFUO0dBQ0QsTUFBTTtJQUNKLFVBQVUsT0FBTztJQUNqQixVQUFVLE9BQU87SUFDakIsTUFBTSxPQUFPO0lBQ2IsS0FBSyxPQUFPO0lBQ1osV0FBVyxPQUFPO0dBQ3BCLENBQUM7R0FDRCxLQUFLLFdBQVcsT0FBTztHQUN2QixLQUFLLGdCQUFnQixPQUFPO0VBQzlCOztFQUdBLElBQUksTUFBYztHQUNoQixPQUFPLEtBQUs7RUFDZDs7RUFHQSxJQUFJLFdBQXNCO0dBQ3hCLE9BQU8sS0FBSztFQUNkOzs7OztFQU1BLE1BQU0sV0FBNEI7R0FDaEMsTUFBTSxFQUFFLFdBQVcsTUFBTSxPQUFPO0dBR2hDLE1BQU0sYUFBYSxNQUZOLE9BQU8sUUFBUSxJQUFJLEtBQUssYUFFWixDQUFBLENBQUssSUFBSSxLQUFLLFFBQVE7R0FDL0MsT0FBTyxPQUFPLEtBQUssVUFBVTtFQUMvQjtDQUNGIn0=
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.2",
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.5.1",
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
+ });
@@ -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 DriverKey } from "../storage/drivers";
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: DriverKey;
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
+ });