appos 0.3.5-0 → 0.3.7-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.
Files changed (242) hide show
  1. package/dist/bin/main.mjs +3 -3
  2. package/dist/bin/youch-handler-D7REmh8Z.mjs +2 -0
  3. package/dist/exports/api/adapter-C2mMJKvG.mjs +1504 -0
  4. package/dist/exports/api/{auth-schema.mjs → auth-schema-CON4yFFY.mjs} +2 -3
  5. package/dist/exports/api/bun-sqlite-dialect-CPpPZa23.mjs +155 -0
  6. package/dist/exports/{cli/_virtual/rolldown_runtime.mjs → api/chunk-CyXqrcp_.mjs} +14 -1
  7. package/dist/exports/api/conditions-DjBAWfiK.mjs +116 -0
  8. package/dist/exports/api/dialect-CUUD24Ge.mjs +72 -0
  9. package/dist/exports/api/env-CwT3zhez.mjs +1 -0
  10. package/dist/exports/api/esm-_hkVMulx.mjs +15816 -0
  11. package/dist/exports/api/event-BHFSE6hY.mjs +20983 -0
  12. package/dist/exports/api/extract-blob-metadata-BMgUEPcW.mjs +4 -0
  13. package/dist/exports/api/extract-blob-metadata-_9RUEwoU.mjs +9818 -0
  14. package/dist/exports/api/generate-image-variant-DOIzj1wJ.mjs +4 -0
  15. package/dist/exports/api/generate-image-variant-DxOPoGAk.mjs +118 -0
  16. package/dist/exports/api/generate-preview-CvTFuq58.mjs +160 -0
  17. package/dist/exports/api/generate-preview-Dk3coswZ.mjs +4 -0
  18. package/dist/exports/api/index.d.mts +7347 -20
  19. package/dist/exports/api/index.mjs +146856 -18
  20. package/dist/exports/api/kysely-adapter-CmP2TbkS.mjs +296 -0
  21. package/dist/exports/api/memory-adapter-BzhRhLFK.mjs +212 -0
  22. package/dist/exports/api/node-sqlite-dialect-Bl5suBxl.mjs +155 -0
  23. package/dist/exports/api/orm-CMqufD21.mjs +153690 -0
  24. package/dist/exports/api/orm-DXqYuKvB.d.mts +11 -0
  25. package/dist/exports/api/orm.d.mts +2 -11
  26. package/dist/exports/api/orm.mjs +4 -42
  27. package/dist/exports/api/pdf-YxB2Hm1p.mjs +25822 -0
  28. package/dist/exports/api/purge-attachment-Cf6KH2Jv.mjs +34 -0
  29. package/dist/exports/api/purge-attachment-DIuil7ps.mjs +4 -0
  30. package/dist/exports/{cli/api/workflows/purge-audit-logs.mjs → api/purge-audit-logs-z6j_Pu47.mjs} +3 -3
  31. package/dist/exports/{cli/api/workflows/purge-unattached-blobs.mjs → api/purge-unattached-blobs-B0TfY5Hk.mjs} +1 -1
  32. package/dist/exports/api/react-BYhV5bYt.mjs +1131 -0
  33. package/dist/exports/api/server.node-DyVrQ6cz.mjs +19298 -0
  34. package/dist/exports/api/table-6bTIEqON.mjs +2636 -0
  35. package/dist/exports/api/{workflows/track-db-changes.mjs → track-db-changes-BysaV6nN.mjs} +14 -14
  36. package/dist/exports/api/{workflow.mjs → workflow-BuSWHcX-.mjs} +121 -10
  37. package/dist/exports/api/workflows/auth-schema-BFve3hgZ.mjs +2813 -0
  38. package/dist/exports/api/workflows/{_virtual/rolldown_runtime.mjs → chunk-B36mNPO4.mjs} +5 -1
  39. package/dist/exports/api/workflows/extract-blob-metadata-BcWKvY2K.mjs +9818 -0
  40. package/dist/exports/{cli/api/workflows/generate-image-variant.mjs → api/workflows/generate-image-variant-BAFvL_zV.mjs} +20 -20
  41. package/dist/exports/{cli/api/workflows/generate-preview.mjs → api/workflows/generate-preview-BjlIVYYp.mjs} +6 -6
  42. package/dist/exports/api/workflows/index.d.mts +2011 -1
  43. package/dist/exports/api/workflows/index.mjs +2 -1
  44. package/dist/exports/api/workflows/pdf-DUB8zyIc.mjs +25822 -0
  45. package/dist/exports/{cli/api/workflows/purge-attachment.mjs → api/workflows/purge-attachment-k_sdxTPY.mjs} +3 -3
  46. package/dist/exports/api/workflows/{purge-audit-logs.mjs → purge-audit-logs-zCyWj4Mw.mjs} +12 -3
  47. package/dist/exports/api/workflows/{purge-unattached-blobs.mjs → purge-unattached-blobs-CdsuFAeW.mjs} +1 -1
  48. package/dist/exports/api/workflows/track-db-changes-CiKaI9gT.mjs +20943 -0
  49. package/dist/exports/api/workflows/track-db-changes-DEWQYryO.mjs +4 -0
  50. package/dist/exports/{cli/api/workflow.mjs → api/workflows/workflow-BjM2xCa6.mjs} +23 -3
  51. package/dist/exports/api/workflows/zod-Br0enFWK.mjs +12333 -0
  52. package/dist/exports/api/zod-CdrJdBtV.mjs +12571 -0
  53. package/dist/exports/cli/auth-schema-CNuOqPi-.mjs +2813 -0
  54. package/dist/exports/{api/_virtual/rolldown_runtime.mjs → cli/chunk-B36mNPO4.mjs} +5 -1
  55. package/dist/exports/cli/extract-blob-metadata-CV1Ke90d.mjs +9818 -0
  56. package/dist/exports/{api/workflows/generate-image-variant.mjs → cli/generate-image-variant-CM1BrVvZ.mjs} +20 -20
  57. package/dist/exports/{api/workflows/generate-preview.mjs → cli/generate-preview-DdHQ3ukz.mjs} +6 -6
  58. package/dist/exports/cli/index.d.mts +2142 -2
  59. package/dist/exports/cli/index.mjs +41 -1
  60. package/dist/exports/cli/pdf-CLUPEIdT.mjs +25822 -0
  61. package/dist/exports/{api/workflows/purge-attachment.mjs → cli/purge-attachment-Dc_J74dG.mjs} +3 -3
  62. package/dist/exports/cli/purge-audit-logs-DFhWh-Bx.mjs +56 -0
  63. package/dist/exports/cli/purge-unattached-blobs-C1MRlM_2.mjs +46 -0
  64. package/dist/exports/cli/track-db-changes-CfneOF2_.mjs +20943 -0
  65. package/dist/exports/{api/workflows/api/workflow.mjs → cli/workflow-DSbbXBMt.mjs} +23 -3
  66. package/dist/exports/cli/zod-7q0_Mtnn.mjs +12333 -0
  67. package/dist/exports/tests/api.d.mts +7354 -5
  68. package/dist/exports/tests/api.mjs +79 -1
  69. package/dist/exports/tests/auth-schema-B_qHPoa4.mjs +1 -0
  70. package/dist/exports/tests/chunk-Cii4FAhs.mjs +1 -0
  71. package/dist/exports/tests/conditions-BeWxRN_Z.mjs +1 -0
  72. package/dist/exports/tests/constants-D_m07gCJ.mjs +1 -0
  73. package/dist/exports/tests/dist-Cx2HEBOB.mjs +296 -0
  74. package/dist/exports/tests/dist-DnACOvNW.mjs +7 -0
  75. package/dist/exports/tests/extract-blob-metadata-6xQrarjB.mjs +169 -0
  76. package/dist/exports/tests/generate-image-variant-KQITLmCx.mjs +1 -0
  77. package/dist/exports/tests/generate-preview-YnujQo5I.mjs +1 -0
  78. package/dist/exports/tests/mock-BvoS23fi.mjs +1 -0
  79. package/dist/exports/tests/mock.mjs +1 -1
  80. package/dist/exports/tests/pdf-lZ-as2o7.mjs +13 -0
  81. package/dist/exports/tests/purge-attachment-DTSmCl-z.mjs +1 -0
  82. package/dist/exports/tests/purge-audit-logs-D0_UUhN2.mjs +1 -0
  83. package/dist/exports/tests/purge-unattached-blobs-IENmSB8K.mjs +1 -0
  84. package/dist/exports/tests/react.mjs +6 -1
  85. package/dist/exports/tests/setup.d.mts +1 -1
  86. package/dist/exports/tests/setup.mjs +34 -1
  87. package/dist/exports/tests/table-CmPSf_pE.mjs +1 -0
  88. package/dist/exports/tests/track-db-changes-Td9Q3DLd.mjs +1 -0
  89. package/dist/exports/tests/{api/workflow.mjs → workflow-BWRWyC83.mjs} +1 -1
  90. package/dist/exports/tests/youch-handler-efUxKCCs.mjs +1 -0
  91. package/dist/exports/tests/zod-CkJO9qpV.mjs +23 -0
  92. package/dist/exports/vendors/date.js +13236 -1
  93. package/dist/exports/vendors/toolkit.js +2206 -1
  94. package/dist/exports/vendors/zod.js +12344 -1
  95. package/dist/exports/vite/index.d.mts +1 -1
  96. package/dist/exports/vite/index.mjs +26 -1
  97. package/dist/exports/vite/lib-B0s0q6aX.mjs +382 -0
  98. package/dist/exports/vite/lib-BkrbWMaV.mjs +1 -0
  99. package/dist/exports/web/browser-ponyfill-DxTJMXNq.js +443 -0
  100. package/dist/exports/web/chunk-DksrlJLg.js +49 -0
  101. package/dist/exports/web/index.d.ts +2546 -2
  102. package/dist/exports/web/index.js +9295 -6
  103. package/dist/exports/web/plugin-DqTFWG7p.js +250 -0
  104. package/package.json +1 -7
  105. package/dist/bin/youch-handler-DrYdbUhe.mjs +0 -2
  106. package/dist/exports/api/app-context.d.mts +0 -115
  107. package/dist/exports/api/app-context.mjs +0 -24
  108. package/dist/exports/api/auth-schema.d.mts +0 -4248
  109. package/dist/exports/api/auth.d.mts +0 -402
  110. package/dist/exports/api/auth.mjs +0 -188
  111. package/dist/exports/api/cache.d.mts +0 -44
  112. package/dist/exports/api/cache.mjs +0 -28
  113. package/dist/exports/api/config.d.mts +0 -28
  114. package/dist/exports/api/config.mjs +0 -72
  115. package/dist/exports/api/constants.mjs +0 -92
  116. package/dist/exports/api/container.d.mts +0 -210
  117. package/dist/exports/api/container.mjs +0 -49
  118. package/dist/exports/api/database.d.mts +0 -101
  119. package/dist/exports/api/database.mjs +0 -219
  120. package/dist/exports/api/event.d.mts +0 -235
  121. package/dist/exports/api/event.mjs +0 -236
  122. package/dist/exports/api/i18n.d.mts +0 -34
  123. package/dist/exports/api/i18n.mjs +0 -45
  124. package/dist/exports/api/instrumentation.d.mts +0 -7
  125. package/dist/exports/api/instrumentation.mjs +0 -40
  126. package/dist/exports/api/logger.d.mts +0 -21
  127. package/dist/exports/api/logger.mjs +0 -26
  128. package/dist/exports/api/mailer.d.mts +0 -70
  129. package/dist/exports/api/mailer.mjs +0 -37
  130. package/dist/exports/api/middleware/request-logger.d.mts +0 -24
  131. package/dist/exports/api/middleware.d.mts +0 -39
  132. package/dist/exports/api/middleware.mjs +0 -73
  133. package/dist/exports/api/openapi.d.mts +0 -271
  134. package/dist/exports/api/openapi.mjs +0 -507
  135. package/dist/exports/api/otel.d.mts +0 -40
  136. package/dist/exports/api/otel.mjs +0 -56
  137. package/dist/exports/api/redis.d.mts +0 -34
  138. package/dist/exports/api/redis.mjs +0 -41
  139. package/dist/exports/api/storage-schema.d.mts +0 -707
  140. package/dist/exports/api/storage-schema.mjs +0 -72
  141. package/dist/exports/api/storage.d.mts +0 -506
  142. package/dist/exports/api/storage.mjs +0 -833
  143. package/dist/exports/api/web/auth.mjs +0 -17
  144. package/dist/exports/api/workflow.d.mts +0 -250
  145. package/dist/exports/api/workflows/api/auth-schema.mjs +0 -373
  146. package/dist/exports/api/workflows/api/auth.d.mts +0 -379
  147. package/dist/exports/api/workflows/api/cache.d.mts +0 -44
  148. package/dist/exports/api/workflows/api/config.d.mts +0 -18
  149. package/dist/exports/api/workflows/api/container.d.mts +0 -167
  150. package/dist/exports/api/workflows/api/database.d.mts +0 -47
  151. package/dist/exports/api/workflows/api/event.d.mts +0 -68
  152. package/dist/exports/api/workflows/api/event.mjs +0 -126
  153. package/dist/exports/api/workflows/api/logger.d.mts +0 -21
  154. package/dist/exports/api/workflows/api/mailer.d.mts +0 -70
  155. package/dist/exports/api/workflows/api/orm.d.mts +0 -13
  156. package/dist/exports/api/workflows/api/redis.mjs +0 -3
  157. package/dist/exports/api/workflows/api/storage-schema.d.mts +0 -699
  158. package/dist/exports/api/workflows/api/storage.d.mts +0 -396
  159. package/dist/exports/api/workflows/api/workflow.d.mts +0 -24
  160. package/dist/exports/api/workflows/constants.mjs +0 -23
  161. package/dist/exports/api/workflows/extract-blob-metadata.mjs +0 -132
  162. package/dist/exports/api/workflows/generate-image-variant.d.mts +0 -63
  163. package/dist/exports/api/workflows/track-db-changes.d.mts +0 -72
  164. package/dist/exports/cli/api/auth-schema.mjs +0 -373
  165. package/dist/exports/cli/api/auth.d.mts +0 -379
  166. package/dist/exports/cli/api/cache.d.mts +0 -44
  167. package/dist/exports/cli/api/config.d.mts +0 -18
  168. package/dist/exports/cli/api/container.d.mts +0 -167
  169. package/dist/exports/cli/api/database.d.mts +0 -47
  170. package/dist/exports/cli/api/event.d.mts +0 -68
  171. package/dist/exports/cli/api/event.mjs +0 -126
  172. package/dist/exports/cli/api/logger.d.mts +0 -21
  173. package/dist/exports/cli/api/mailer.d.mts +0 -70
  174. package/dist/exports/cli/api/orm.d.mts +0 -13
  175. package/dist/exports/cli/api/redis.mjs +0 -3
  176. package/dist/exports/cli/api/storage-schema.d.mts +0 -699
  177. package/dist/exports/cli/api/storage.d.mts +0 -396
  178. package/dist/exports/cli/api/workflow.d.mts +0 -2
  179. package/dist/exports/cli/api/workflows/extract-blob-metadata.mjs +0 -132
  180. package/dist/exports/cli/api/workflows/generate-image-variant.d.mts +0 -63
  181. package/dist/exports/cli/api/workflows/track-db-changes.mjs +0 -110
  182. package/dist/exports/cli/command.d.mts +0 -56
  183. package/dist/exports/cli/command.mjs +0 -43
  184. package/dist/exports/cli/constants.mjs +0 -23
  185. package/dist/exports/cli/context.d.mts +0 -170
  186. package/dist/exports/devtools/index.d.ts +0 -3
  187. package/dist/exports/devtools/index.js +0 -4
  188. package/dist/exports/tests/_virtual/rolldown_runtime.mjs +0 -1
  189. package/dist/exports/tests/api/app-context.d.mts +0 -115
  190. package/dist/exports/tests/api/app-context.mjs +0 -1
  191. package/dist/exports/tests/api/auth-schema.d.mts +0 -4248
  192. package/dist/exports/tests/api/auth-schema.mjs +0 -1
  193. package/dist/exports/tests/api/auth.d.mts +0 -402
  194. package/dist/exports/tests/api/cache.d.mts +0 -44
  195. package/dist/exports/tests/api/config.d.mts +0 -28
  196. package/dist/exports/tests/api/container.d.mts +0 -210
  197. package/dist/exports/tests/api/database.d.mts +0 -101
  198. package/dist/exports/tests/api/database.mjs +0 -1
  199. package/dist/exports/tests/api/event.d.mts +0 -235
  200. package/dist/exports/tests/api/event.mjs +0 -1
  201. package/dist/exports/tests/api/i18n.d.mts +0 -34
  202. package/dist/exports/tests/api/index.d.mts +0 -26
  203. package/dist/exports/tests/api/logger.d.mts +0 -21
  204. package/dist/exports/tests/api/mailer.d.mts +0 -70
  205. package/dist/exports/tests/api/middleware/error-handler.mjs +0 -1
  206. package/dist/exports/tests/api/middleware/health.mjs +0 -1
  207. package/dist/exports/tests/api/middleware/i18n.mjs +0 -1
  208. package/dist/exports/tests/api/middleware/request-logger.d.mts +0 -24
  209. package/dist/exports/tests/api/middleware/request-logger.mjs +0 -1
  210. package/dist/exports/tests/api/middleware/shutdown.mjs +0 -1
  211. package/dist/exports/tests/api/middleware/timeout.mjs +0 -1
  212. package/dist/exports/tests/api/middleware/youch-handler.mjs +0 -1
  213. package/dist/exports/tests/api/middleware.d.mts +0 -39
  214. package/dist/exports/tests/api/middleware.mjs +0 -1
  215. package/dist/exports/tests/api/openapi.d.mts +0 -271
  216. package/dist/exports/tests/api/openapi.mjs +0 -1
  217. package/dist/exports/tests/api/orm.d.mts +0 -13
  218. package/dist/exports/tests/api/otel.d.mts +0 -40
  219. package/dist/exports/tests/api/redis.d.mts +0 -34
  220. package/dist/exports/tests/api/redis.mjs +0 -1
  221. package/dist/exports/tests/api/server.mjs +0 -1
  222. package/dist/exports/tests/api/storage-schema.d.mts +0 -707
  223. package/dist/exports/tests/api/storage.d.mts +0 -506
  224. package/dist/exports/tests/api/workflow.d.mts +0 -250
  225. package/dist/exports/tests/api/workflows/extract-blob-metadata.mjs +0 -1
  226. package/dist/exports/tests/api/workflows/generate-image-variant.d.mts +0 -63
  227. package/dist/exports/tests/api/workflows/generate-image-variant.mjs +0 -1
  228. package/dist/exports/tests/api/workflows/generate-preview.mjs +0 -1
  229. package/dist/exports/tests/api/workflows/purge-attachment.mjs +0 -1
  230. package/dist/exports/tests/api/workflows/purge-audit-logs.mjs +0 -1
  231. package/dist/exports/tests/api/workflows/purge-unattached-blobs.mjs +0 -1
  232. package/dist/exports/tests/api/workflows/track-db-changes.mjs +0 -1
  233. package/dist/exports/tests/constants.mjs +0 -1
  234. package/dist/exports/tests/instrumentation.d.mts +0 -7
  235. package/dist/exports/tests/instrumentation.mjs +0 -1
  236. package/dist/exports/web/api/auth.d.ts +0 -125
  237. package/dist/exports/web/api/database.d.ts +0 -4
  238. package/dist/exports/web/api/logger.d.ts +0 -1
  239. package/dist/exports/web/auth.d.ts +0 -2388
  240. package/dist/exports/web/auth.js +0 -75
  241. package/dist/exports/web/i18n.d.ts +0 -42
  242. package/dist/exports/web/i18n.js +0 -45
@@ -1,833 +0,0 @@
1
- import { orm_exports } from "./orm.mjs";
2
- import { extractBlobMetadata } from "./workflows/extract-blob-metadata.mjs";
3
- import { generateImageVariant } from "./workflows/generate-image-variant.mjs";
4
- import { generatePreview } from "./workflows/generate-preview.mjs";
5
- import { purgeAttachment } from "./workflows/purge-attachment.mjs";
6
- import { join } from "node:path";
7
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
8
- import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
9
- import { tmpdir } from "node:os";
10
- import { DriveManager } from "flydrive";
11
- import { FSDriver } from "flydrive/drivers/fs";
12
- import { S3Driver } from "flydrive/drivers/s3";
13
-
14
- //#region src/api/storage.ts
15
- /**
16
- * Creates an S3 disk configuration with common boilerplate handled.
17
- *
18
- * Algorithm:
19
- * 1. Build base config with bucket, region, visibility
20
- * 2. If endpoint provided, add endpoint + forcePathStyle
21
- * 3. If credentials provided, add credentials object
22
- *
23
- * @example
24
- * ```typescript
25
- * defineS3Disk({
26
- * bucket: "my-bucket",
27
- * region: "us-east-1",
28
- * endpoint: "http://localhost:4566",
29
- * credentials: {
30
- * accessKeyId: "test",
31
- * secretAccessKey: "test",
32
- * },
33
- * })
34
- * ```
35
- */
36
- function defineS3Disk(opts) {
37
- return {
38
- bucket: opts.bucket,
39
- region: opts.region,
40
- visibility: opts.visibility ?? "private",
41
- ...opts.endpoint && {
42
- endpoint: opts.endpoint,
43
- forcePathStyle: true
44
- },
45
- ...opts.credentials && { credentials: opts.credentials }
46
- };
47
- }
48
- /**
49
- * Calculate checksum for a buffer.
50
- */
51
- function calculateChecksum(buffer) {
52
- return createHash("md5").update(buffer).digest("hex");
53
- }
54
- /**
55
- * Generate deterministic variant key.
56
- */
57
- function generateVariantKey(blobId, transformations) {
58
- return `variants/${blobId}/${createHash("sha256").update(JSON.stringify({
59
- blobId,
60
- transformations
61
- })).digest("hex")}`;
62
- }
63
- /**
64
- * Generate a secure key for blob storage.
65
- */
66
- function generateKey(prefix) {
67
- const key = `${Date.now()}-${randomBytes(16).toString("hex")}`;
68
- return prefix ? `${prefix}/${key}` : key;
69
- }
70
- /**
71
- * Define storage service with FlyDrive.
72
- * @template TDisks - Record of disk configurations keyed by disk name
73
- * @template TDb - Database type for table name extraction
74
- * @template TAttachments - Mapping of table names to valid attachment names
75
- */
76
- function defineStorage(opts) {
77
- if (!opts.default) throw new Error("Storage: 'default' disk must be specified");
78
- if (!opts.disks || Object.keys(opts.disks).length === 0) throw new Error("Storage: At least one disk must be configured");
79
- const defaultDisk = opts.default;
80
- const operationalServices = {};
81
- const signedUrlServices = {};
82
- for (const [name, diskConfig] of Object.entries(opts.disks)) {
83
- const config = diskConfig;
84
- if ("driver" in config) {
85
- if (config.driver === "fs") {
86
- const fsDriver = () => new FSDriver(config);
87
- operationalServices[name] = fsDriver;
88
- signedUrlServices[name] = fsDriver;
89
- } else if (config.driver === "s3") {
90
- operationalServices[name] = () => new S3Driver(config);
91
- if (opts.publicEndpoint) signedUrlServices[name] = () => new S3Driver({
92
- ...config,
93
- endpoint: opts.publicEndpoint
94
- });
95
- else signedUrlServices[name] = () => new S3Driver(config);
96
- }
97
- } else if ("location" in config) {
98
- const fsDriver = () => new FSDriver(config);
99
- operationalServices[name] = fsDriver;
100
- signedUrlServices[name] = fsDriver;
101
- } else if ("bucket" in config) {
102
- operationalServices[name] = () => new S3Driver(config);
103
- if (opts.publicEndpoint) {
104
- const signedUrlConfig = {
105
- ...config,
106
- endpoint: opts.publicEndpoint
107
- };
108
- signedUrlServices[name] = () => new S3Driver(signedUrlConfig);
109
- } else signedUrlServices[name] = () => new S3Driver(config);
110
- }
111
- }
112
- const operationalDrive = new DriveManager({
113
- default: defaultDisk,
114
- services: operationalServices
115
- });
116
- const signedUrlDrive = new DriveManager({
117
- default: defaultDisk,
118
- services: signedUrlServices
119
- });
120
- return new StorageService(operationalDrive, opts.database, defaultDisk, signedUrlDrive, opts.purgeCron, opts.secret);
121
- }
122
- /**
123
- * Storage service for blob operations.
124
- *
125
- * This service uses a dual-drive architecture when configured:
126
- * - `drive`: For actual file operations (upload, delete, read) using internal endpoints
127
- * - `signedUrlDrive`: For generating browser-accessible signed URLs with public endpoints
128
- *
129
- * This allows containers to use fast internal network for operations while
130
- * generating URLs that work in end-user browsers.
131
- *
132
- * @template TDiskNames - Union of disk names (e.g., "private" | "public")
133
- * @template TTableNames - Union of table names for type-safe attachment methods
134
- * @template TAttachments - Mapping of table names to valid attachment names
135
- */
136
- var StorageService = class {
137
- /**
138
- * The drive manager for file operations.
139
- */
140
- drive;
141
- /**
142
- * The database instance with storage schema and relations.
143
- */
144
- db;
145
- /**
146
- * The default disk name.
147
- */
148
- defaultDisk;
149
- /**
150
- * Cron expression for unattached blob purge schedule.
151
- *
152
- * @default "0 0 * * *"
153
- */
154
- purgeCron;
155
- /**
156
- * Secret key for signing blob IDs.
157
- */
158
- secret;
159
- /**
160
- * Drive manager for generating signed URLs (public endpoint).
161
- */
162
- signedUrlDrive;
163
- constructor(drive, db, defaultDisk, signedUrlDrive, purgeCron, secret) {
164
- this.drive = drive;
165
- this.db = db;
166
- this.defaultDisk = defaultDisk;
167
- this.signedUrlDrive = signedUrlDrive;
168
- this.purgeCron = purgeCron;
169
- this.secret = secret;
170
- }
171
- /**
172
- * Create a blob record and upload file.
173
- */
174
- async createBlob(file, options) {
175
- const key = generateKey(options.prefix);
176
- const buffer = Buffer.from(file);
177
- const checksum = calculateChecksum(buffer);
178
- const serviceName = options.serviceName || this.defaultDisk;
179
- await (options.serviceName ? this.drive.use(options.serviceName) : this.drive.use()).put(key, buffer, { contentType: options.contentType });
180
- const [blob] = await this.db.insert(this.db._.fullSchema.storageBlobs).values({
181
- key,
182
- filename: options.filename,
183
- contentType: options.contentType,
184
- metadata: options.metadata,
185
- serviceName,
186
- byteSize: buffer.byteLength,
187
- checksum
188
- }).returning();
189
- return blob;
190
- }
191
- /**
192
- * Get blob by ID.
193
- */
194
- async getBlob(id) {
195
- const [blob] = await this.db.select().from(this.db._.fullSchema.storageBlobs).where((0, orm_exports.eq)(this.db._.fullSchema.storageBlobs.id, id));
196
- return blob || null;
197
- }
198
- /**
199
- * Download blob content.
200
- */
201
- async downloadBlob(id) {
202
- const blob = await this.getBlob(id);
203
- if (!blob) return null;
204
- const content = await this.drive.use(blob.serviceName).getBytes(blob.key);
205
- return Buffer.from(content);
206
- }
207
- /**
208
- * Delete blob and its content.
209
- */
210
- async deleteBlob(id) {
211
- const blob = await this.getBlob(id);
212
- if (!blob) return false;
213
- await this.drive.use(blob.serviceName).delete(blob.key);
214
- await this.db.delete(this.db._.fullSchema.storageBlobs).where((0, orm_exports.eq)(this.db._.fullSchema.storageBlobs.id, id));
215
- return true;
216
- }
217
- /**
218
- * Get signed URL for blob. If blob is on public service, returns permanent
219
- * URL instead.
220
- */
221
- async getSignedUrl(id, options = {}) {
222
- const blob = await this.getBlob(id);
223
- if (!blob) return null;
224
- if (blob.serviceName === "public") return this.getPublicUrl(id);
225
- const disk = this.signedUrlDrive.use(blob.serviceName);
226
- try {
227
- const signedUrlOptions = { expiresIn: options.expiresIn || 3600 };
228
- if (options.disposition) {
229
- const filename = options.filename || blob.filename;
230
- signedUrlOptions.contentDisposition = options.disposition === "attachment" ? `attachment; filename="${filename}"` : `inline; filename="${filename}"`;
231
- }
232
- return await disk.getSignedUrl(blob.key, signedUrlOptions);
233
- } catch (error) {
234
- const baseUrl = `/storage/${blob.id}`;
235
- if (options.disposition) return `${baseUrl}?${new URLSearchParams({
236
- disposition: options.disposition,
237
- ...options.filename && { filename: options.filename }
238
- }).toString()}`;
239
- return baseUrl;
240
- }
241
- }
242
- /**
243
- * Get permanent public URL (no expiration).
244
- * Works for blobs on public storage service.
245
- */
246
- async getPublicUrl(id) {
247
- const blob = await this.getBlob(id);
248
- if (!blob) return null;
249
- const disk = this.drive.use(blob.serviceName);
250
- if (blob.serviceName === "public") try {
251
- if ("getUrl" in disk && typeof disk.getUrl === "function") return await disk.getUrl(blob.key);
252
- } catch {}
253
- return `/storage/${blob.id}`;
254
- }
255
- /**
256
- * Create attachment between a record and blob.
257
- *
258
- * @param recordType - The table name (e.g., 'users')
259
- * @param recordId - The record ID
260
- * @param blobId - The blob ID to attach
261
- * @param name - The attachment name (e.g., 'avatar')
262
- * @param replaceExisting - If true, replaces existing attachment with same name (for one-to-one)
263
- */
264
- async createAttachment(recordType, recordId, blobId, name, replaceExisting = false) {
265
- if (replaceExisting) {
266
- const lockKey = `${recordType}:${recordId}:${name}`;
267
- const lockId = Array.from(lockKey).reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0) | 0, 0);
268
- return await this.db.transaction(async (tx) => {
269
- await tx.execute(orm_exports.sql`SELECT pg_advisory_xact_lock(${lockId})`);
270
- await tx.delete(this.db._.fullSchema.storageAttachments).where((0, orm_exports.and)((0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.recordType, recordType), (0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.recordId, recordId), (0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.name, name)));
271
- const [attachment$1] = await tx.insert(this.db._.fullSchema.storageAttachments).values({
272
- recordType,
273
- recordId,
274
- blobId,
275
- name
276
- }).returning();
277
- return attachment$1;
278
- });
279
- }
280
- const [attachment] = await this.db.insert(this.db._.fullSchema.storageAttachments).values({
281
- recordType,
282
- recordId,
283
- blobId,
284
- name
285
- }).returning();
286
- return attachment;
287
- }
288
- /**
289
- * Get attachments for a record with their associated blobs.
290
- */
291
- async getAttachments(recordType, recordId, name) {
292
- return this.db.query.storageAttachments.findMany({
293
- where: {
294
- recordType,
295
- recordId,
296
- ...name && { name }
297
- },
298
- with: { blob: true }
299
- });
300
- }
301
- /**
302
- * Get attachments by their IDs with associated blobs.
303
- *
304
- * @param attachmentIds - Array of attachment IDs to fetch.
305
- */
306
- async getAttachmentsByIds(attachmentIds) {
307
- if (attachmentIds.length === 0) return [];
308
- return this.db.query.storageAttachments.findMany({
309
- where: { id: { in: attachmentIds } },
310
- with: { blob: true }
311
- });
312
- }
313
- /**
314
- * Delete a single attachment by ID.
315
- */
316
- async deleteAttachment(attachmentId) {
317
- return ((await this.db.delete(this.db._.fullSchema.storageAttachments).where((0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.id, attachmentId))).rowCount ?? 0) > 0;
318
- }
319
- /**
320
- * Delete attachments for a record (without deleting the blobs).
321
- */
322
- async deleteAttachments(recordType, recordId, name) {
323
- const whereClause = name ? (0, orm_exports.and)((0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.recordType, recordType), (0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.recordId, recordId), (0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.name, name)) : (0, orm_exports.and)((0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.recordType, recordType), (0, orm_exports.eq)(this.db._.fullSchema.storageAttachments.recordId, recordId));
324
- return (await this.db.delete(this.db._.fullSchema.storageAttachments).where(whereClause)).rowCount ?? 0;
325
- }
326
- /**
327
- * Get direct upload credentials (for S3). Also creates a pending blob record
328
- * that will be finalized after upload.
329
- */
330
- async getDirectUploadUrl(options) {
331
- const serviceName = options.serviceName || this.defaultDisk;
332
- const disk = this.drive.use(serviceName);
333
- try {
334
- const key = generateKey();
335
- if ("putSignedUrl" in disk) {
336
- const uploadUrl = await disk.putSignedUrl(key, {
337
- expiresIn: options.expiresIn || 3600,
338
- contentType: options.contentType
339
- });
340
- const [blob] = await this.db.insert(this.db._.fullSchema.storageBlobs).values({
341
- key,
342
- filename: options.filename,
343
- contentType: options.contentType,
344
- metadata: {
345
- ...options.metadata,
346
- pending: true
347
- },
348
- serviceName,
349
- byteSize: 0,
350
- checksum: ""
351
- }).returning();
352
- return {
353
- url: uploadUrl,
354
- key,
355
- blobId: blob.id,
356
- headers: { "Content-Type": options.contentType || "application/octet-stream" }
357
- };
358
- }
359
- } catch {}
360
- return null;
361
- }
362
- /**
363
- * Finalize a direct upload by updating blob metadata. Call this after the
364
- * client has uploaded to S3.
365
- */
366
- async finalizeDirectUpload(blobId, actualSize) {
367
- const blob = await this.getBlob(blobId);
368
- if (!blob) throw new Error(`Blob ${blobId} not found`);
369
- const newMetadata = blob.metadata && typeof blob.metadata === "object" ? { ...blob.metadata } : {};
370
- delete newMetadata.pending;
371
- await this.db.update(this.db._.fullSchema.storageBlobs).set({
372
- byteSize: actualSize,
373
- metadata: newMetadata
374
- }).where((0, orm_exports.eq)(this.db._.fullSchema.storageBlobs.id, blobId));
375
- }
376
- /**
377
- * Update blob metadata (for automatic extraction).
378
- */
379
- async updateBlobMetadata(blobId, metadata) {
380
- const blob = await this.getBlob(blobId);
381
- if (!blob) throw new Error(`Blob ${blobId} not found`);
382
- const newMetadata = {
383
- ...blob.metadata && typeof blob.metadata === "object" ? blob.metadata : {},
384
- ...metadata
385
- };
386
- await this.db.update(this.db._.fullSchema.storageBlobs).set({ metadata: newMetadata }).where((0, orm_exports.eq)(this.db._.fullSchema.storageBlobs.id, blobId));
387
- }
388
- /**
389
- * Get existing variant or return null.
390
- */
391
- async getVariant(blobId, transformations) {
392
- const variationDigest = createHash("sha256").update(JSON.stringify(transformations)).digest("hex");
393
- const [variantRecord] = await this.db.query.storageVariantRecords.findMany({
394
- where: {
395
- blobId,
396
- variationDigest
397
- },
398
- with: { blob: true },
399
- limit: 1
400
- });
401
- return variantRecord?.blob || null;
402
- }
403
- /**
404
- * Create variant blob and record.
405
- */
406
- async createVariant(blobId, transformations, variantBuffer) {
407
- const blob = await this.getBlob(blobId);
408
- if (!blob) throw new Error(`Blob ${blobId} not found`);
409
- const key = generateVariantKey(blobId, transformations);
410
- const checksum = calculateChecksum(variantBuffer);
411
- await this.drive.use(blob.serviceName).put(key, variantBuffer, { contentType: blob.contentType || "application/octet-stream" });
412
- const [variantBlob] = await this.db.insert(this.db._.fullSchema.storageBlobs).values({
413
- key,
414
- filename: blob.filename,
415
- contentType: blob.contentType,
416
- metadata: {
417
- sourceBlob: blobId,
418
- transformations
419
- },
420
- serviceName: blob.serviceName,
421
- byteSize: variantBuffer.byteLength,
422
- checksum
423
- }).returning();
424
- const variationDigest = createHash("sha256").update(JSON.stringify(transformations)).digest("hex");
425
- await this.db.insert(this.db._.fullSchema.storageVariantRecords).values({
426
- blobId,
427
- variationDigest,
428
- id: variantBlob.id
429
- });
430
- return variantBlob;
431
- }
432
- /**
433
- * Get blobs that have no attachments (orphaned). Useful for cleanup jobs.
434
- */
435
- async getUnattachedBlobs(options = {}) {
436
- const olderThan = options.olderThan || (/* @__PURE__ */ new Date(Date.now() - 2880 * 60 * 1e3)).toISOString();
437
- const limit = options.limit || 1e3;
438
- return (await this.db.select({ blob: this.db._.fullSchema.storageBlobs }).from(this.db._.fullSchema.storageBlobs).leftJoin(this.db._.fullSchema.storageAttachments, (0, orm_exports.eq)(this.db._.fullSchema.storageBlobs.id, this.db._.fullSchema.storageAttachments.blobId)).where((0, orm_exports.and)((0, orm_exports.isNull)(this.db._.fullSchema.storageAttachments.id), (0, orm_exports.lt)(this.db._.fullSchema.storageBlobs.createdAt, olderThan))).limit(limit)).map((row) => row.blob);
439
- }
440
- /**
441
- * Get pending blobs that were never finalized (stuck direct uploads).
442
- */
443
- async getPendingBlobs(olderThan = (/* @__PURE__ */ new Date(Date.now() - 1440 * 60 * 1e3)).toISOString()) {
444
- return await this.db.select().from(this.db._.fullSchema.storageBlobs).where((0, orm_exports.and)((0, orm_exports.lt)(this.db._.fullSchema.storageBlobs.createdAt, olderThan), orm_exports.sql`${this.db._.fullSchema.storageBlobs.metadata}->>'pending' = 'true'`));
445
- }
446
- /**
447
- * Purge unattached blobs.
448
- */
449
- async purgeUnattached(olderThan) {
450
- const blobs = await this.getUnattachedBlobs({ olderThan });
451
- let purgedCount = 0;
452
- for (const blob of blobs) try {
453
- await this.deleteBlob(blob.id);
454
- purgedCount++;
455
- } catch (error) {
456
- console.error(`Failed to purge blob ${blob.id}:`, error);
457
- }
458
- return purgedCount;
459
- }
460
- /**
461
- * Create a signed, tamper-proof reference to a blob.
462
- * Use findSigned() to resolve back to blob.
463
- *
464
- * Algorithm:
465
- * 1. Create payload with blobId and expiration timestamp
466
- * 2. JSON stringify and base64url encode the payload
467
- * 3. Create HMAC-SHA256 signature of the encoded payload
468
- * 4. Return payload.signature format
469
- */
470
- signedId(blobId, expiresIn = 3600) {
471
- if (!this.secret) throw new Error("Storage secret not configured");
472
- const payload = {
473
- blobId,
474
- exp: Math.floor(Date.now() / 1e3) + expiresIn
475
- };
476
- const data = JSON.stringify(payload);
477
- const signature = createHmac("sha256", this.secret).update(data).digest("base64url");
478
- return `${Buffer.from(data).toString("base64url")}.${signature}`;
479
- }
480
- /**
481
- * Find blob by signed ID. Returns null if invalid or expired.
482
- * Uses constant-time comparison to prevent timing attacks.
483
- *
484
- * Algorithm:
485
- * 1. Split signed ID into payload and signature
486
- * 2. Decode and recompute expected signature
487
- * 3. Use timingSafeEqual for constant-time comparison
488
- * 4. Check expiration timestamp
489
- * 5. Return blob if valid, null otherwise
490
- */
491
- async findSigned(signedId) {
492
- if (!this.secret) throw new Error("Storage secret not configured");
493
- try {
494
- const [encodedPayload, signature] = signedId.split(".");
495
- if (!encodedPayload || !signature) return null;
496
- const data = Buffer.from(encodedPayload, "base64url").toString();
497
- const expectedSignature = createHmac("sha256", this.secret).update(data).digest("base64url");
498
- const sigBuffer = Buffer.from(signature, "base64url");
499
- const expectedBuffer = Buffer.from(expectedSignature, "base64url");
500
- if (sigBuffer.length !== expectedBuffer.length || !timingSafeEqual(sigBuffer, expectedBuffer)) return null;
501
- const payload = JSON.parse(data);
502
- if (payload.exp && payload.exp < Math.floor(Date.now() / 1e3)) return null;
503
- return this.getBlob(payload.blobId);
504
- } catch {
505
- return null;
506
- }
507
- }
508
- /**
509
- * Get a single attachment handle (one-to-one relationship).
510
- * Similar to Rails' `has_one_attached :avatar`.
511
- *
512
- * Algorithm:
513
- * Returns an object with methods to manage a single attachment:
514
- * - attach(): Replace existing attachment with new blob
515
- * - get(): Get attached blob or null
516
- * - url(): Get signed URL
517
- * - variant(): Get/generate image variant
518
- * - purge(): Delete attachment and blob
519
- *
520
- * @template TTable - The table name (must be in TTableNames)
521
- */
522
- one(recordType, recordId, name) {
523
- const storage = this;
524
- const attachmentName = name;
525
- return {
526
- async attach(blobId) {
527
- if (!await storage.getBlob(blobId)) throw new Error(`Blob ${blobId} not found`);
528
- const attachment = await storage.createAttachment(recordType, recordId, blobId, attachmentName, true);
529
- await extractBlobMetadata.start({ blobId });
530
- return attachment;
531
- },
532
- async get() {
533
- return (await storage.getAttachments(recordType, recordId, attachmentName))[0]?.blob ?? null;
534
- },
535
- async attached() {
536
- return await this.get() !== null;
537
- },
538
- async url(options) {
539
- const blob = await this.get();
540
- if (!blob) return null;
541
- return storage.getSignedUrl(blob.id, options);
542
- },
543
- async publicUrl() {
544
- const blob = await this.get();
545
- if (!blob) return null;
546
- return storage.getPublicUrl(blob.id);
547
- },
548
- async metadata() {
549
- return (await this.get())?.metadata ?? null;
550
- },
551
- async analyzed() {
552
- return (await this.metadata())?.analyzed === true;
553
- },
554
- async representable() {
555
- const blob = await this.get();
556
- if (!blob?.contentType) return false;
557
- return [
558
- "image/",
559
- "video/",
560
- "application/pdf"
561
- ].some((t) => blob.contentType?.startsWith(t));
562
- },
563
- async variant(transformations, expiresIn = 3600) {
564
- const blob = await this.get();
565
- if (!blob) return null;
566
- const existing = await storage.getVariant(blob.id, transformations);
567
- if (existing) return storage.getSignedUrl(existing.id, { expiresIn });
568
- await generateImageVariant.start({
569
- blobId: blob.id,
570
- transformations
571
- });
572
- return null;
573
- },
574
- async preview(expiresIn = 3600, timeInSeconds = 1) {
575
- const blob = await this.get();
576
- if (!blob) return null;
577
- const existing = await storage.getVariant(blob.id, { preview: true });
578
- if (existing) return storage.getSignedUrl(existing.id, { expiresIn });
579
- await generatePreview.start({
580
- blobId: blob.id,
581
- timeInSeconds
582
- });
583
- return null;
584
- },
585
- async detach() {
586
- return await storage.deleteAttachments(recordType, recordId, attachmentName) > 0;
587
- },
588
- async purge() {
589
- const results = await storage.getAttachments(recordType, recordId, attachmentName);
590
- if (results.length === 0) return false;
591
- const blob = results[0].blob;
592
- if (!blob) return false;
593
- await storage.deleteAttachments(recordType, recordId, attachmentName);
594
- await storage.deleteBlob(blob.id);
595
- return true;
596
- },
597
- async purgeLater() {
598
- const results = await storage.getAttachments(recordType, recordId, attachmentName);
599
- if (results.length === 0) return false;
600
- await purgeAttachment.start({ attachmentIds: [results[0].id] });
601
- return true;
602
- },
603
- async download() {
604
- const blob = await this.get();
605
- if (!blob) return null;
606
- return storage.downloadBlob(blob.id);
607
- },
608
- async open(callback) {
609
- const blob = await this.get();
610
- if (!blob) return null;
611
- const buffer = await storage.downloadBlob(blob.id);
612
- if (!buffer) return null;
613
- const tempDir = await mkdtemp(join(tmpdir(), "attachment-"));
614
- const tempPath = join(tempDir, blob.filename);
615
- try {
616
- await writeFile(tempPath, buffer);
617
- return await callback(tempPath);
618
- } finally {
619
- await rm(tempDir, {
620
- recursive: true,
621
- force: true
622
- });
623
- }
624
- },
625
- async representation(transformations, expiresIn = 3600) {
626
- const blob = await this.get();
627
- if (!blob) return null;
628
- if (blob.contentType?.startsWith("image/")) return this.variant(transformations, expiresIn);
629
- return this.preview(expiresIn);
630
- },
631
- async byteSize() {
632
- return (await this.get())?.byteSize ?? null;
633
- },
634
- async contentType() {
635
- return (await this.get())?.contentType ?? null;
636
- },
637
- async filename() {
638
- return (await this.get())?.filename ?? null;
639
- },
640
- async signedId(expiresIn = 3600) {
641
- const blob = await this.get();
642
- if (!blob) return null;
643
- return storage.signedId(blob.id, expiresIn);
644
- }
645
- };
646
- }
647
- /**
648
- * Get a multiple attachment handle (one-to-many relationship).
649
- * Similar to Rails' `has_many_attached :images`.
650
- *
651
- * Algorithm:
652
- * Returns an object with methods to manage multiple attachments:
653
- * - attach(): Add blobs (doesn't replace existing)
654
- * - list(): Get all attached blobs
655
- * - urls(): Get signed URLs for all
656
- * - variants(): Get/generate variants for all
657
- * - purge(): Delete all or specific attachment
658
- *
659
- * @template TTable - The table name (must be in TTableNames)
660
- */
661
- many(recordType, recordId, name) {
662
- const storage = this;
663
- const attachmentName = name;
664
- return {
665
- async attach(blobIds) {
666
- const ids = Array.isArray(blobIds) ? blobIds : [blobIds];
667
- const blobs = await Promise.all(ids.map((id) => storage.getBlob(id)));
668
- const missing = ids.filter((_, i) => !blobs[i]);
669
- if (missing.length > 0) throw new Error(`Blobs not found: ${missing.join(", ")}`);
670
- const attachments = await Promise.all(ids.map((id) => storage.createAttachment(recordType, recordId, id, attachmentName)));
671
- await Promise.all(ids.map((id) => extractBlobMetadata.start({ blobId: id })));
672
- return attachments;
673
- },
674
- async list() {
675
- return (await storage.getAttachments(recordType, recordId, attachmentName)).map((r) => r.blob).filter((b) => b !== null);
676
- },
677
- async count() {
678
- return (await this.list()).length;
679
- },
680
- async urls(options) {
681
- const blobs = await this.list();
682
- return (await Promise.all(blobs.map((b) => storage.getSignedUrl(b.id, options)))).filter((u) => u !== null);
683
- },
684
- async publicUrls() {
685
- const blobs = await this.list();
686
- return Promise.all(blobs.map((b) => storage.getPublicUrl(b.id)));
687
- },
688
- async metadata() {
689
- return (await this.list()).map((b) => b.metadata ?? null);
690
- },
691
- async analyzed() {
692
- return (await this.metadata()).map((m) => m?.analyzed === true);
693
- },
694
- async representable() {
695
- return (await this.list()).map((b) => {
696
- if (!b.contentType) return false;
697
- return [
698
- "image/",
699
- "video/",
700
- "application/pdf"
701
- ].some((t) => b.contentType?.startsWith(t));
702
- });
703
- },
704
- async variants(transformations, expiresIn = 3600) {
705
- const blobs = await this.list();
706
- return Promise.all(blobs.map(async (blob) => {
707
- const existing = await storage.getVariant(blob.id, transformations);
708
- if (existing) return storage.getSignedUrl(existing.id, { expiresIn });
709
- await generateImageVariant.start({
710
- blobId: blob.id,
711
- transformations
712
- });
713
- return null;
714
- }));
715
- },
716
- async previews(expiresIn = 3600, timeInSeconds = 1) {
717
- const blobs = await this.list();
718
- return Promise.all(blobs.map(async (blob) => {
719
- const existing = await storage.getVariant(blob.id, { preview: true });
720
- if (existing) return storage.getSignedUrl(existing.id, { expiresIn });
721
- await generatePreview.start({
722
- blobId: blob.id,
723
- timeInSeconds
724
- });
725
- return null;
726
- }));
727
- },
728
- async detach(blobId) {
729
- if (blobId) {
730
- const attachment = (await storage.getAttachments(recordType, recordId, attachmentName)).find((r) => r.blob?.id === blobId);
731
- if (attachment) {
732
- await storage.deleteAttachment(attachment.id);
733
- return 1;
734
- }
735
- return 0;
736
- }
737
- return storage.deleteAttachments(recordType, recordId, attachmentName);
738
- },
739
- async purge(blobId) {
740
- const results = await storage.getAttachments(recordType, recordId, attachmentName);
741
- if (blobId) {
742
- const attachment = results.find((r) => r.blob?.id === blobId);
743
- if (!attachment) return 0;
744
- await storage.deleteAttachment(attachment.id);
745
- await storage.deleteBlob(blobId);
746
- return 1;
747
- }
748
- let count = 0;
749
- for (const r of results) {
750
- await storage.deleteAttachment(r.id);
751
- if (r.blob) {
752
- await storage.deleteBlob(r.blob.id);
753
- count++;
754
- }
755
- }
756
- return count;
757
- },
758
- async purgeLater(blobId) {
759
- const results = await storage.getAttachments(recordType, recordId, attachmentName);
760
- if (blobId) {
761
- const attachment = results.find((r) => r.blob?.id === blobId);
762
- if (!attachment) return 0;
763
- await purgeAttachment.start({ attachmentIds: [attachment.id] });
764
- return 1;
765
- }
766
- if (results.length > 0) await purgeAttachment.start({ attachmentIds: results.map((r) => r.id) });
767
- return results.length;
768
- },
769
- async download() {
770
- const blobs = await this.list();
771
- const buffers = [];
772
- for (const blob of blobs) {
773
- const buffer = await storage.downloadBlob(blob.id);
774
- if (buffer) buffers.push(buffer);
775
- }
776
- return buffers;
777
- },
778
- async open(callback) {
779
- const blobs = await this.list();
780
- const results = [];
781
- for (const blob of blobs) {
782
- const buffer = await storage.downloadBlob(blob.id);
783
- if (!buffer) continue;
784
- const tempDir = await mkdtemp(join(tmpdir(), "attachment-"));
785
- const tempPath = join(tempDir, blob.filename);
786
- try {
787
- await writeFile(tempPath, buffer);
788
- results.push(await callback(tempPath, blob));
789
- } finally {
790
- await rm(tempDir, {
791
- recursive: true,
792
- force: true
793
- });
794
- }
795
- }
796
- return results;
797
- },
798
- async representations(transformations, expiresIn = 3600) {
799
- const blobs = await this.list();
800
- return Promise.all(blobs.map(async (blob) => {
801
- if (blob.contentType?.startsWith("image/")) {
802
- const existing = await storage.getVariant(blob.id, transformations);
803
- if (existing) return storage.getSignedUrl(existing.id, { expiresIn });
804
- await generateImageVariant.start({
805
- blobId: blob.id,
806
- transformations
807
- });
808
- return null;
809
- }
810
- const preview = await storage.getVariant(blob.id, { preview: true });
811
- if (preview) return storage.getSignedUrl(preview.id, { expiresIn });
812
- await generatePreview.start({ blobId: blob.id });
813
- return null;
814
- }));
815
- },
816
- async byteSizes() {
817
- return (await this.list()).map((b) => b.byteSize);
818
- },
819
- async contentTypes() {
820
- return (await this.list()).map((b) => b.contentType);
821
- },
822
- async filenames() {
823
- return (await this.list()).map((b) => b.filename);
824
- },
825
- async signedIds(expiresIn = 3600) {
826
- return (await this.list()).map((b) => storage.signedId(b.id, expiresIn));
827
- }
828
- };
829
- }
830
- };
831
-
832
- //#endregion
833
- export { StorageService, defineS3Disk, defineStorage };