@tulip-systems/drive 0.8.0 → 0.8.2

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 (243) hide show
  1. package/dist/client.d.mts +5 -0
  2. package/dist/client.mjs +6 -0
  3. package/dist/components/content.d.mts +14 -0
  4. package/dist/components/content.d.mts.map +1 -0
  5. package/dist/components/content.mjs +18 -0
  6. package/dist/components/content.mjs.map +1 -0
  7. package/dist/components/context.client.d.mts +14 -0
  8. package/dist/components/context.client.d.mts.map +1 -0
  9. package/dist/components/dnd.client.d.mts +17 -0
  10. package/dist/components/dnd.client.d.mts.map +1 -0
  11. package/dist/components/dnd.client.mjs +30 -0
  12. package/dist/components/dnd.client.mjs.map +1 -0
  13. package/dist/components/grid-card.client.d.mts +43 -0
  14. package/dist/components/grid-card.client.d.mts.map +1 -0
  15. package/dist/components/grid-card.client.mjs +173 -0
  16. package/dist/components/grid-card.client.mjs.map +1 -0
  17. package/dist/components/grid.client.d.mts +54 -0
  18. package/dist/components/grid.client.d.mts.map +1 -0
  19. package/dist/components/grid.client.mjs +54 -0
  20. package/dist/components/grid.client.mjs.map +1 -0
  21. package/dist/components/navigation/breadcrumbs.client.d.mts +26 -0
  22. package/dist/components/navigation/breadcrumbs.client.d.mts.map +1 -0
  23. package/dist/components/navigation/breadcrumbs.client.mjs +52 -0
  24. package/dist/components/navigation/breadcrumbs.client.mjs.map +1 -0
  25. package/dist/components/navigation/header.client.d.mts +28 -0
  26. package/dist/components/navigation/header.client.d.mts.map +1 -0
  27. package/dist/components/navigation/header.client.mjs +41 -0
  28. package/dist/components/navigation/header.client.mjs.map +1 -0
  29. package/dist/components/navigation/toolbar.client.d.mts +24 -0
  30. package/dist/components/navigation/toolbar.client.d.mts.map +1 -0
  31. package/dist/components/navigation/toolbar.client.mjs +35 -0
  32. package/dist/components/navigation/toolbar.client.mjs.map +1 -0
  33. package/dist/components/navigation/view-switcher.client.d.mts +10 -0
  34. package/dist/components/navigation/view-switcher.client.d.mts.map +1 -0
  35. package/dist/components/navigation/view-switcher.client.mjs +36 -0
  36. package/dist/components/navigation/view-switcher.client.mjs.map +1 -0
  37. package/dist/components/selection.client.d.mts +28 -0
  38. package/dist/components/selection.client.d.mts.map +1 -0
  39. package/dist/components/selection.client.mjs +39 -0
  40. package/dist/components/selection.client.mjs.map +1 -0
  41. package/dist/components/view.client.d.mts +26 -0
  42. package/dist/components/view.client.d.mts.map +1 -0
  43. package/dist/components/view.client.mjs +44 -0
  44. package/dist/components/view.client.mjs.map +1 -0
  45. package/dist/config/filters.mjs +15 -0
  46. package/dist/config/filters.mjs.map +1 -0
  47. package/dist/config/types.mjs +61 -0
  48. package/dist/config/types.mjs.map +1 -0
  49. package/dist/google/client.d.mts +8 -0
  50. package/dist/google/client.mjs +9 -0
  51. package/dist/google/server.d.mts +3 -0
  52. package/dist/google/server.mjs +4 -0
  53. package/dist/google.d.mts +6 -0
  54. package/dist/google.mjs +7 -0
  55. package/dist/index.d.mts +7 -0
  56. package/dist/index.mjs +7 -0
  57. package/dist/lib/constants.d.mts +12 -0
  58. package/dist/lib/constants.d.mts.map +1 -0
  59. package/dist/lib/constants.mjs +21 -0
  60. package/dist/lib/constants.mjs.map +1 -0
  61. package/dist/lib/contracts.d.mts +101 -0
  62. package/dist/lib/contracts.d.mts.map +1 -0
  63. package/dist/lib/dto.d.mts +118 -0
  64. package/dist/lib/dto.d.mts.map +1 -0
  65. package/dist/lib/dto.mjs +58 -0
  66. package/dist/lib/dto.mjs.map +1 -0
  67. package/dist/lib/helpers.d.mts +18 -0
  68. package/dist/lib/helpers.d.mts.map +1 -0
  69. package/dist/lib/helpers.mjs +37 -0
  70. package/dist/lib/helpers.mjs.map +1 -0
  71. package/dist/lib/helpers.server.d.mts +14 -0
  72. package/dist/lib/helpers.server.d.mts.map +1 -0
  73. package/dist/lib/helpers.server.mjs +16 -0
  74. package/dist/lib/helpers.server.mjs.map +1 -0
  75. package/dist/lib/search-params.d.mts +9 -0
  76. package/dist/lib/search-params.d.mts.map +1 -0
  77. package/dist/lib/search-params.mjs +8 -0
  78. package/dist/lib/search-params.mjs.map +1 -0
  79. package/dist/lib/validators.d.mts +158 -0
  80. package/dist/lib/validators.d.mts.map +1 -0
  81. package/dist/lib/validators.mjs +66 -0
  82. package/dist/lib/validators.mjs.map +1 -0
  83. package/dist/local/client.d.mts +13 -0
  84. package/dist/local/client.mjs +13 -0
  85. package/dist/local/server.d.mts +4 -0
  86. package/dist/local/server.mjs +5 -0
  87. package/dist/local.d.mts +8 -0
  88. package/dist/local.mjs +9 -0
  89. package/dist/providers/google/components/command-file-update.d.mts +22 -0
  90. package/dist/providers/google/components/command-file-update.d.mts.map +1 -0
  91. package/dist/providers/google/components/command-file-update.mjs +52 -0
  92. package/dist/providers/google/components/command-file-update.mjs.map +1 -0
  93. package/dist/providers/google/components/command-folder-create.d.mts +22 -0
  94. package/dist/providers/google/components/command-folder-create.d.mts.map +1 -0
  95. package/dist/providers/google/components/command-folder-create.mjs +59 -0
  96. package/dist/providers/google/components/command-folder-create.mjs.map +1 -0
  97. package/dist/providers/google/components/command-folder-update.d.mts +22 -0
  98. package/dist/providers/google/components/command-folder-update.d.mts.map +1 -0
  99. package/dist/providers/google/components/command-folder-update.mjs +52 -0
  100. package/dist/providers/google/components/command-folder-update.mjs.map +1 -0
  101. package/dist/providers/google/components/content.client.d.mts +10 -0
  102. package/dist/providers/google/components/content.client.d.mts.map +1 -0
  103. package/dist/providers/google/components/content.client.mjs +11 -0
  104. package/dist/providers/google/components/content.client.mjs.map +1 -0
  105. package/dist/providers/google/components/navigation.client.d.mts +16 -0
  106. package/dist/providers/google/components/navigation.client.d.mts.map +1 -0
  107. package/dist/providers/google/components/navigation.client.mjs +18 -0
  108. package/dist/providers/google/components/navigation.client.mjs.map +1 -0
  109. package/dist/providers/google/components/provider.client.d.mts +33 -0
  110. package/dist/providers/google/components/provider.client.d.mts.map +1 -0
  111. package/dist/providers/google/components/provider.client.mjs +43 -0
  112. package/dist/providers/google/components/provider.client.mjs.map +1 -0
  113. package/dist/providers/google/components/view.client.d.mts +41 -0
  114. package/dist/providers/google/components/view.client.d.mts.map +1 -0
  115. package/dist/providers/google/components/view.client.mjs +98 -0
  116. package/dist/providers/google/components/view.client.mjs.map +1 -0
  117. package/dist/providers/google/config/columns-data.d.mts +8 -0
  118. package/dist/providers/google/config/columns-data.d.mts.map +1 -0
  119. package/dist/providers/google/config/columns-data.mjs +70 -0
  120. package/dist/providers/google/config/columns-data.mjs.map +1 -0
  121. package/dist/providers/google/config/filters.d.mts +16 -0
  122. package/dist/providers/google/config/filters.d.mts.map +1 -0
  123. package/dist/providers/google/config/filters.mjs +8 -0
  124. package/dist/providers/google/config/filters.mjs.map +1 -0
  125. package/dist/providers/google/lib/constants.mjs +13 -0
  126. package/dist/providers/google/lib/constants.mjs.map +1 -0
  127. package/dist/providers/google/lib/dto.d.mts +39 -0
  128. package/dist/providers/google/lib/dto.d.mts.map +1 -0
  129. package/dist/providers/google/lib/dto.mjs +66 -0
  130. package/dist/providers/google/lib/dto.mjs.map +1 -0
  131. package/dist/providers/google/lib/helpers.mjs +46 -0
  132. package/dist/providers/google/lib/helpers.mjs.map +1 -0
  133. package/dist/providers/google/lib/router.server.d.mts +612 -0
  134. package/dist/providers/google/lib/router.server.d.mts.map +1 -0
  135. package/dist/providers/google/lib/router.server.mjs +40 -0
  136. package/dist/providers/google/lib/router.server.mjs.map +1 -0
  137. package/dist/providers/google/lib/search-params.d.mts +15 -0
  138. package/dist/providers/google/lib/search-params.d.mts.map +1 -0
  139. package/dist/providers/google/lib/search-params.mjs +12 -0
  140. package/dist/providers/google/lib/search-params.mjs.map +1 -0
  141. package/dist/providers/google/lib/service.server.d.mts +186 -0
  142. package/dist/providers/google/lib/service.server.d.mts.map +1 -0
  143. package/dist/providers/google/lib/service.server.mjs +613 -0
  144. package/dist/providers/google/lib/service.server.mjs.map +1 -0
  145. package/dist/providers/google/lib/validators.d.mts +303 -0
  146. package/dist/providers/google/lib/validators.d.mts.map +1 -0
  147. package/dist/providers/google/lib/validators.mjs +59 -0
  148. package/dist/providers/google/lib/validators.mjs.map +1 -0
  149. package/dist/providers/local/components/command-file-update.d.mts +18 -0
  150. package/dist/providers/local/components/command-file-update.d.mts.map +1 -0
  151. package/dist/providers/local/components/command-file-update.mjs +48 -0
  152. package/dist/providers/local/components/command-file-update.mjs.map +1 -0
  153. package/dist/providers/local/components/command-file-upload.d.mts +11 -0
  154. package/dist/providers/local/components/command-file-upload.d.mts.map +1 -0
  155. package/dist/providers/local/components/command-file-upload.mjs +35 -0
  156. package/dist/providers/local/components/command-file-upload.mjs.map +1 -0
  157. package/dist/providers/local/components/command-folder-create.d.mts +18 -0
  158. package/dist/providers/local/components/command-folder-create.d.mts.map +1 -0
  159. package/dist/providers/local/components/command-folder-create.mjs +55 -0
  160. package/dist/providers/local/components/command-folder-create.mjs.map +1 -0
  161. package/dist/providers/local/components/command-folder-update.d.mts +18 -0
  162. package/dist/providers/local/components/command-folder-update.d.mts.map +1 -0
  163. package/dist/providers/local/components/command-folder-update.mjs +48 -0
  164. package/dist/providers/local/components/command-folder-update.mjs.map +1 -0
  165. package/dist/providers/local/components/content.client.d.mts +7 -0
  166. package/dist/providers/local/components/content.client.d.mts.map +1 -0
  167. package/dist/providers/local/components/content.client.mjs +8 -0
  168. package/dist/providers/local/components/content.client.mjs.map +1 -0
  169. package/dist/providers/local/components/navigation.client.d.mts +16 -0
  170. package/dist/providers/local/components/navigation.client.d.mts.map +1 -0
  171. package/dist/providers/local/components/navigation.client.mjs +18 -0
  172. package/dist/providers/local/components/navigation.client.mjs.map +1 -0
  173. package/dist/providers/local/components/provider.client.d.mts +40 -0
  174. package/dist/providers/local/components/provider.client.d.mts.map +1 -0
  175. package/dist/providers/local/components/provider.client.mjs +61 -0
  176. package/dist/providers/local/components/provider.client.mjs.map +1 -0
  177. package/dist/providers/local/components/upload-zone-context.client.d.mts +38 -0
  178. package/dist/providers/local/components/upload-zone-context.client.d.mts.map +1 -0
  179. package/dist/providers/local/components/upload-zone-context.client.mjs +23 -0
  180. package/dist/providers/local/components/upload-zone-context.client.mjs.map +1 -0
  181. package/dist/providers/local/components/upload-zone.client.d.mts +30 -0
  182. package/dist/providers/local/components/upload-zone.client.d.mts.map +1 -0
  183. package/dist/providers/local/components/upload-zone.client.mjs +147 -0
  184. package/dist/providers/local/components/upload-zone.client.mjs.map +1 -0
  185. package/dist/providers/local/components/view.client.d.mts +32 -0
  186. package/dist/providers/local/components/view.client.d.mts.map +1 -0
  187. package/dist/providers/local/components/view.client.mjs +91 -0
  188. package/dist/providers/local/components/view.client.mjs.map +1 -0
  189. package/dist/providers/local/config/columns-data.d.mts +8 -0
  190. package/dist/providers/local/config/columns-data.d.mts.map +1 -0
  191. package/dist/providers/local/config/columns-data.mjs +70 -0
  192. package/dist/providers/local/config/columns-data.mjs.map +1 -0
  193. package/dist/providers/local/config/filters.d.mts +26 -0
  194. package/dist/providers/local/config/filters.d.mts.map +1 -0
  195. package/dist/providers/local/config/filters.mjs +15 -0
  196. package/dist/providers/local/config/filters.mjs.map +1 -0
  197. package/dist/providers/local/lib/constants.d.mts +12 -0
  198. package/dist/providers/local/lib/constants.d.mts.map +1 -0
  199. package/dist/providers/local/lib/constants.mjs +29 -0
  200. package/dist/providers/local/lib/constants.mjs.map +1 -0
  201. package/dist/providers/local/lib/helpers.d.mts +45 -0
  202. package/dist/providers/local/lib/helpers.d.mts.map +1 -0
  203. package/dist/providers/local/lib/helpers.mjs +110 -0
  204. package/dist/providers/local/lib/helpers.mjs.map +1 -0
  205. package/dist/providers/local/lib/route-handler.server.d.mts +34 -0
  206. package/dist/providers/local/lib/route-handler.server.d.mts.map +1 -0
  207. package/dist/providers/local/lib/route-handler.server.mjs +114 -0
  208. package/dist/providers/local/lib/route-handler.server.mjs.map +1 -0
  209. package/dist/providers/local/lib/router.server.d.mts +41648 -0
  210. package/dist/providers/local/lib/router.server.d.mts.map +1 -0
  211. package/dist/providers/local/lib/router.server.mjs +52 -0
  212. package/dist/providers/local/lib/router.server.mjs.map +1 -0
  213. package/dist/providers/local/lib/schema.d.mts +1113 -0
  214. package/dist/providers/local/lib/schema.d.mts.map +1 -0
  215. package/dist/providers/local/lib/schema.mjs +71 -0
  216. package/dist/providers/local/lib/schema.mjs.map +1 -0
  217. package/dist/providers/local/lib/search-params.d.mts +14 -0
  218. package/dist/providers/local/lib/search-params.d.mts.map +1 -0
  219. package/dist/providers/local/lib/search-params.mjs +9 -0
  220. package/dist/providers/local/lib/search-params.mjs.map +1 -0
  221. package/dist/providers/local/lib/service.server.d.mts +489 -0
  222. package/dist/providers/local/lib/service.server.d.mts.map +1 -0
  223. package/dist/providers/local/lib/service.server.mjs +668 -0
  224. package/dist/providers/local/lib/service.server.mjs.map +1 -0
  225. package/dist/providers/local/lib/upload.client.d.mts +62 -0
  226. package/dist/providers/local/lib/upload.client.d.mts.map +1 -0
  227. package/dist/providers/local/lib/upload.client.mjs +100 -0
  228. package/dist/providers/local/lib/upload.client.mjs.map +1 -0
  229. package/dist/providers/local/lib/validators.d.mts +454 -0
  230. package/dist/providers/local/lib/validators.d.mts.map +1 -0
  231. package/dist/providers/local/lib/validators.mjs +96 -0
  232. package/dist/providers/local/lib/validators.mjs.map +1 -0
  233. package/dist/server.d.mts +2 -0
  234. package/dist/server.mjs +3 -0
  235. package/package.json +3 -2
  236. package/src/components/grid-card.client.tsx +4 -1
  237. package/src/providers/google/components/view.client.tsx +9 -1
  238. package/src/providers/google/lib/helpers.ts +9 -0
  239. package/src/providers/local/components/provider.client.tsx +2 -1
  240. package/src/providers/local/components/upload-zone-context.client.tsx +1 -0
  241. package/src/providers/local/components/upload-zone.client.tsx +14 -1
  242. package/src/providers/local/components/view.client.tsx +10 -2
  243. package/src/providers/local/lib/helpers.ts +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.server.mjs","names":["#db","#getWritableParent","#getSubtreeAssetIds"],"sources":["../../../../src/providers/local/lib/service.server.ts"],"sourcesContent":["import stream from \"node:stream/consumers\";\nimport type { TDatabaseSchema } from \"@tulip-systems/core/config\";\nimport type { TableQueryResponse } from \"@tulip-systems/core/data-tables/server\";\nimport {\n convertOrderByToQueryParams,\n convertSearchToQueryParams,\n createTableQueryResponse,\n} from \"@tulip-systems/core/data-tables/server\";\nimport type { Database } from \"@tulip-systems/core/database/server\";\nimport { ServerError } from \"@tulip-systems/core/router/server\";\nimport { type ObjectBodyInput, storageAssets } from \"@tulip-systems/core/storage\";\nimport type { Storage } from \"@tulip-systems/core/storage/server\";\nimport { addSeconds } from \"date-fns\";\nimport {\n and,\n asc,\n desc,\n eq,\n getTableColumns,\n inArray,\n isNotNull,\n isNull,\n type SQL,\n sql,\n} from \"drizzle-orm\";\nimport { after } from \"next/server\";\nimport type { QueryResult } from \"pg\";\nimport type {\n DriveArchive,\n DriveDirectUpload,\n DriveNodeMutations,\n DrivePresignedUpload,\n DrivePreviewGenerator,\n DriveReader,\n DriveReadonly,\n} from \"../../../lib/contracts\";\nimport { deviceSizes } from \"./constants\";\nimport {\n inferLocalDriveNodeSubtype,\n isLocalDriveFile,\n isLocalDriveFolder,\n toLocalDriveNode,\n} from \"./helpers\";\nimport { nodePresignedUrls, nodes, nodeVariants } from \"./schema\";\nimport {\n type CreateLocalDriveFolderSchema,\n type GetLocalDriveNodesByParentIdInput,\n type GetLocalFileURLSchema,\n getLocalFileURLSchemaDefaults,\n type ListLocalDriveFlatSchema,\n type ListLocalDriveTreeSchema,\n type LocalDriveFileNode,\n type LocalDriveNode,\n type LocalDriveNodeChild,\n type LocalDriveNodeWithAsset,\n type LocalDriveNodeWithChildren,\n type LocalNode,\n type PresignLocalDriveFileInput,\n type UpdateLocalDriveNodeInput,\n type UploadLocalDriveFileSchema,\n} from \"./validators\";\n\n/**\n * Drive Service Config\n */\nexport type LocalDriveConfig<TSchema extends TDatabaseSchema> = {\n db: Database<TSchema>;\n storage: Storage<TSchema>;\n};\n\n/**\n * Drive Service\n */\nexport class LocalDrive<TSchema extends TDatabaseSchema>\n implements\n DriveReader<LocalDriveNode>,\n DriveNodeMutations<LocalDriveNode>,\n DriveDirectUpload<LocalDriveNode, ObjectBodyInput>,\n DrivePresignedUpload<LocalDriveFileNode>,\n DrivePreviewGenerator,\n DriveReadonly<LocalDriveNode>,\n DriveArchive<LocalDriveNode>\n{\n #db: Database<TSchema>;\n storage: Storage<TSchema>;\n\n constructor({ db, storage }: LocalDriveConfig<TSchema>) {\n this.#db = db;\n this.storage = storage;\n }\n\n /**\n * Loads and validates the parent folder for write operations.\n *\n * The returned parent is guaranteed to exist, be a folder, belong to the\n * same namespace, and not be archived. Root-level operations return `null`.\n *\n * @param input - Parent identifier and target namespace.\n * @returns The validated parent node or `null` for root operations.\n */\n async #getWritableParent(input: { parentId?: string | null; namespace: string }) {\n if (!input.parentId) return null;\n\n const [parent] = await this.#db\n .select()\n .from(nodes)\n .where(eq(nodes.id, input.parentId))\n .limit(1);\n\n if (!parent) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"Parent not found\" });\n }\n\n if (!isLocalDriveFolder(parent)) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"Parent is not a folder\" });\n }\n\n if (parent.namespace !== input.namespace) {\n throw new ServerError(\"BAD_REQUEST\", {\n message: \"Parent is not in the same namespace\",\n });\n }\n\n if (parent.archivedAt) {\n throw new ServerError(\"BAD_REQUEST\", {\n message: \"Parent is archived\",\n });\n }\n\n return parent;\n }\n\n /**\n * Collects the storage asset ids associated with a node subtree.\n *\n * This includes both the main file assets attached to file nodes and any\n * generated variant assets stored in `node_variants`.\n *\n * @param nodeIds - Node ids that belong to the subtree.\n * @param subtree - Lightweight subtree rows returned by `getNodeChildren`.\n * @returns A de-duplicated list of storage asset ids.\n */\n async #getSubtreeAssetIds(\n nodeIds: string[],\n subtree: Array<Pick<LocalNode, \"id\" | \"assetId\" | \"type\" | \"parentId\"> & { depth: number }>,\n ) {\n const fileAssetIds = [\n ...new Set(\n subtree\n .filter((node) => node.type === \"file\")\n .map((node) => node.assetId)\n .filter((assetId): assetId is string => assetId != null),\n ),\n ];\n\n const variantAssets = await this.#db\n .select({ assetId: nodeVariants.assetId })\n .from(nodeVariants)\n .where(inArray(nodeVariants.nodeId, nodeIds));\n\n return [...new Set([...fileAssetIds, ...variantAssets.map((variant) => variant.assetId)])];\n }\n\n /**\n * Retrieves the node row for a given id.\n *\n * This is a thin lookup helper that returns the raw database result for the\n * matching node. It does not load children, variants, or perform namespace\n * validation.\n *\n * @param id - The node identifier.\n * @returns A promise resolving to the matching node row as a single-item array,\n * or an empty array when no node exists.\n */\n async getNodeById(id: string): Promise<LocalDriveNode | null> {\n const [node] = await this.#db.select().from(nodes).where(eq(nodes.id, id));\n return node ? toLocalDriveNode(node) : null;\n }\n\n /**\n * Retrieves a node together with its direct children for a namespace.\n *\n * This is used by the drive UI when opening a node detail view that expects\n * the immediate child nodes in the same payload.\n *\n * @param input - The target node id and namespace.\n * @returns The node with its children when found, otherwise `null`.\n */\n async getNodeWithChildren(input: {\n id: string;\n namespace: string;\n }): Promise<LocalDriveNodeWithChildren | null> {\n const node = await this.getNodeById(input.id);\n if (!node || node.namespace !== input.namespace) return null;\n\n const children = await this.getNodesByParentId({\n filters: {\n namespace: input.namespace,\n parentId: node.id,\n hidden: false,\n isArchived: false,\n },\n });\n\n return { ...node, children };\n }\n\n /**\n * Lists nodes within a folder scope using the current table-query filters.\n *\n * Behavior:\n * - Scopes results to a namespace\n * - Resolves either root nodes (`parentId = null`) or a specific parent folder\n * - Applies optional filters such as ids, types, hidden state, and archive state\n * - Applies search and sorting through the shared table-query utilities\n *\n * @param input - Table query input containing drive filters, search, and sorting.\n * @returns A promise resolving to the matching child nodes.\n */\n async getNodesByParentId({\n filters,\n ...query\n }: GetLocalDriveNodesByParentIdInput): Promise<LocalDriveNode[]> {\n const orderBy = convertOrderByToQueryParams(query, nodes, asc(nodes.createdAt));\n const search = convertSearchToQueryParams(query, [nodes.name]);\n const archivedFilter =\n filters.isArchived === true\n ? isNotNull(nodes.archivedAt)\n : filters.isArchived === false\n ? isNull(nodes.archivedAt)\n : undefined;\n const parentFilter = filters.parentId\n ? eq(nodes.parentId, filters.parentId)\n : isNull(nodes.parentId);\n\n const items = await this.#db\n .select()\n .from(nodes)\n .where(\n and(\n filters.nodeIds != null ? inArray(nodes.id, filters.nodeIds) : undefined,\n filters.types != null ? inArray(nodes.type, filters.types) : undefined,\n archivedFilter,\n filters.hidden != null ? eq(nodes.hidden, filters.hidden) : undefined,\n parentFilter,\n eq(nodes.namespace, filters.namespace),\n search,\n ),\n )\n .orderBy(orderBy as SQL);\n\n return items.map(toLocalDriveNode);\n }\n\n /**\n * Lists tree-scoped nodes with pagination, search, and richer local filters.\n *\n * This query is intended for admin table views where additional local fields\n * such as subtype and contentType are part of the filtering contract.\n *\n * @param input - Table query input with tree filters.\n * @returns A paginated table-query response.\n */\n async listTree({\n filters,\n ...query\n }: ListLocalDriveTreeSchema): Promise<TableQueryResponse<LocalDriveNodeWithAsset>> {\n const orderBy = convertOrderByToQueryParams(query, nodes, desc(nodes.createdAt));\n const search = convertSearchToQueryParams(query, [nodes.name]);\n\n const archivedFilter =\n filters.isArchived === true\n ? isNotNull(nodes.archivedAt)\n : filters.isArchived === false\n ? isNull(nodes.archivedAt)\n : undefined;\n const parentFilter = filters.parentId\n ? eq(nodes.parentId, filters.parentId)\n : isNull(nodes.parentId);\n\n const limit = query.limit;\n const cursor = query.cursor;\n const offset = cursor * limit;\n\n const where = and(\n filters.nodeIds != null ? inArray(nodes.id, filters.nodeIds) : undefined,\n filters.types != null ? inArray(nodes.type, filters.types) : undefined,\n filters.subtypes != null ? inArray(nodes.subtype, filters.subtypes) : undefined,\n filters.contentTypes != null ? inArray(nodes.contentType, filters.contentTypes) : undefined,\n archivedFilter,\n filters.hidden != null ? eq(nodes.hidden, filters.hidden) : undefined,\n parentFilter,\n eq(nodes.namespace, filters.namespace),\n search,\n );\n\n const [data, total] = await this.#db.transaction(async (tx) => {\n const data = await tx\n .select({\n ...getTableColumns(nodes),\n asset: storageAssets,\n })\n .from(nodes)\n .where(where)\n .leftJoin(storageAssets, eq(nodes.assetId, storageAssets.id))\n .orderBy(orderBy as SQL)\n .limit(limit)\n .offset(offset);\n\n const total = await tx.$count(nodes, where);\n return [data, total] as const;\n });\n\n return createTableQueryResponse({\n data: data.map(({ asset, ...node }) => ({ ...toLocalDriveNode(node), asset })),\n input: query,\n total,\n });\n }\n\n /**\n * Lists flat nodes with pagination, search, and richer local filters.\n *\n * Unlike `listTree`, this query ignores parent scoping and is intended for\n * flat file-manager style views.\n *\n * @param input - Table query input with flat filters.\n * @returns A paginated table-query response.\n */\n async listFlat({\n filters,\n ...query\n }: ListLocalDriveFlatSchema): Promise<TableQueryResponse<LocalDriveNodeWithAsset>> {\n const orderBy = convertOrderByToQueryParams(query, nodes, desc(nodes.createdAt));\n const search = convertSearchToQueryParams(query, [nodes.name]);\n const archivedFilter =\n filters.isArchived === true\n ? isNotNull(nodes.archivedAt)\n : filters.isArchived === false\n ? isNull(nodes.archivedAt)\n : undefined;\n const limit = query.limit;\n const cursor = query.cursor;\n const offset = cursor * limit;\n\n const where = and(\n filters.nodeIds != null ? inArray(nodes.id, filters.nodeIds) : undefined,\n filters.types != null ? inArray(nodes.type, filters.types) : undefined,\n filters.subtypes != null ? inArray(nodes.subtype, filters.subtypes) : undefined,\n filters.contentTypes != null ? inArray(nodes.contentType, filters.contentTypes) : undefined,\n archivedFilter,\n filters.hidden != null ? eq(nodes.hidden, filters.hidden) : undefined,\n filters.namespace ? eq(nodes.namespace, filters.namespace) : undefined,\n search,\n );\n\n const [data, total] = await this.#db.transaction(async (tx) => {\n const data = await tx\n .select({\n ...getTableColumns(nodes),\n asset: storageAssets,\n })\n .from(nodes)\n .where(where)\n .leftJoin(storageAssets, eq(nodes.assetId, storageAssets.id))\n .orderBy(orderBy as SQL)\n .limit(limit)\n .offset(offset);\n\n const total = await tx.$count(nodes, where);\n return [data, total] as const;\n });\n\n return createTableQueryResponse({\n data: data.map(({ asset, ...node }) => ({ ...toLocalDriveNode(node), asset })),\n input: query,\n total,\n });\n }\n\n /**\n * Resolves the ancestor chain for a folder or file node.\n *\n * The returned list starts with the root-most ancestor and ends with the\n * requested node. This is primarily used for breadcrumb navigation.\n *\n * @param input - The node id and namespace to resolve.\n * @returns A promise resolving to the ordered ancestor chain.\n */\n async getFolderParents(input: { id: string | null; namespace: string }) {\n if (!input.id) return [];\n\n const startNodeQuery = this.#db\n .select({\n id: nodes.id,\n name: nodes.name,\n parentId: nodes.parentId,\n depth: sql<number>`0`.as(\"depth\"),\n })\n .from(nodes)\n .where(and(eq(nodes.id, input.id), eq(nodes.namespace, input.namespace)));\n\n const alias = \"parent_nodes\";\n const parentNodesQueryAlias = startNodeQuery.as(alias);\n const recursiveQueryName = sql.raw(`\"${alias}\"`);\n\n const recursiveQuery = startNodeQuery.unionAll(\n this.#db\n .select({\n id: nodes.id,\n name: nodes.name,\n parentId: nodes.parentId,\n depth: sql<number>`${parentNodesQueryAlias.depth} + 1`,\n })\n .from(nodes)\n .innerJoin(recursiveQueryName, eq(nodes.id, parentNodesQueryAlias.parentId)),\n );\n\n const result = (await this.#db.execute(\n sql`WITH RECURSIVE ${recursiveQueryName} AS ${recursiveQuery} SELECT * FROM ${recursiveQueryName}`,\n )) as QueryResult<Pick<LocalNode, \"id\" | \"name\" | \"parentId\"> & { depth: number }>;\n\n return result.rows?.toSorted((a, b) => Number(b.depth) - Number(a.depth)) ?? [];\n }\n\n /**\n * Retrieves the structural subtree for one or more root node ids.\n *\n * The result includes the provided start nodes and all descendant nodes,\n * returned as a lightweight shape containing only the fields needed for\n * recursive drive operations such as delete, archive, and validation.\n *\n * Depth starts at `0` for the provided root nodes and increases for each\n * descendant level. Duplicate paths are collapsed by keeping the minimum depth.\n *\n * @param ids - Root node ids to traverse from.\n * @returns A promise resolving to the subtree rows for the provided nodes.\n */\n async getNodeChildren(ids: string[]) {\n if (ids.length === 0) return [];\n\n const startNodeQuery = this.#db\n .select({\n id: nodes.id,\n assetId: nodes.assetId,\n type: nodes.type,\n parentId: nodes.parentId,\n depth: sql<number>`0`.as(\"depth\"),\n })\n .from(nodes)\n .where(inArray(nodes.id, ids));\n\n const alias = \"child_nodes\";\n const childNodesQueryAlias = startNodeQuery.as(alias);\n const recursiveQueryName = sql.raw(`\"${alias}\"`);\n\n const recursiveQuery = startNodeQuery.unionAll(\n this.#db\n .select({\n id: nodes.id,\n assetId: nodes.assetId,\n type: nodes.type,\n parentId: nodes.parentId,\n depth: sql<number>`${childNodesQueryAlias.depth} + 1`,\n })\n .from(nodes)\n .innerJoin(recursiveQueryName, eq(nodes.parentId, childNodesQueryAlias.id)),\n );\n\n const result = (await this.#db.execute(\n sql`WITH RECURSIVE ${recursiveQueryName} AS ${recursiveQuery}\n SELECT\n ${childNodesQueryAlias.id} as id,\n ${childNodesQueryAlias.assetId} as \"assetId\",\n ${childNodesQueryAlias.type} as type,\n ${childNodesQueryAlias.parentId} as \"parentId\",\n MIN(${childNodesQueryAlias.depth})::int as depth\n FROM ${recursiveQueryName}\n GROUP BY\n ${childNodesQueryAlias.id},\n ${childNodesQueryAlias.assetId},\n ${childNodesQueryAlias.type},\n ${childNodesQueryAlias.parentId}\n `,\n )) as QueryResult<Pick<LocalNode, \"id\" | \"assetId\" | \"type\" | \"parentId\"> & { depth: number }>;\n\n return (result.rows ?? []) as LocalDriveNodeChild[];\n }\n\n /**\n * Resolves an access URL for a file node and the requested variant.\n *\n * Behavior:\n * - Rejects folder nodes because only file nodes can resolve a file URL\n * - Reuses a cached URL when an unexpired entry exists for the requested\n * variant and disposition\n * - Resolves the requested preview variant asset when available\n * - Falls back to the main file asset when the requested variant does not exist\n * - Delegates final URL generation to the storage service\n * - Stores the generated URL in the local cache table for future reuse\n *\n * @param node - The file node to resolve.\n * @param options - URL options such as variant and content disposition.\n * @returns A promise resolving to a temporary file URL.\n */\n async getURL(node: LocalNode, options: GetLocalFileURLSchema = getLocalFileURLSchemaDefaults) {\n if (!isLocalDriveFile(node)) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"Node is not a file\" });\n }\n\n const [presignedUrl] = await this.#db\n .select({ url: nodePresignedUrls.url, expiresAt: nodePresignedUrls.expiresAt })\n .from(nodePresignedUrls)\n .where(\n and(\n eq(nodePresignedUrls.nodeId, node.id),\n eq(nodePresignedUrls.variant, options.variant),\n eq(nodePresignedUrls.disposition, options.disposition),\n ),\n );\n\n if (presignedUrl && presignedUrl.expiresAt > new Date()) return presignedUrl.url;\n\n const expiresIn = 3600 * 24; // 24 hours\n\n const [variantRecord] =\n options.variant === \"main\"\n ? []\n : await this.#db\n .select({\n id: nodeVariants.id,\n assetId: nodeVariants.assetId,\n variant: nodeVariants.variant,\n })\n .from(nodeVariants)\n .where(\n and(eq(nodeVariants.nodeId, node.id), eq(nodeVariants.variant, options.variant)),\n );\n\n // If the requested variant does not exist, fallback to main\n const variant = variantRecord ? options.variant : \"main\";\n const assetId = variantRecord?.assetId ?? node.assetId;\n\n if (!assetId) {\n throw new ServerError(\"NOT_FOUND\", { message: \"Storage asset not found\" });\n }\n\n console.info(\n `Generating new signed url for file: ${node.id} with variant: ${variant} and disposition: ${options.disposition}`,\n );\n\n // Generate the presigned url that expires in 24 hours\n const url = await this.storage.getObjectURL(assetId, {\n disposition: `${options.disposition}; filename=\"${node.name}\"`,\n expiresIn,\n });\n\n // Add the presigned url to the database\n after(async () => {\n await this.#db\n .insert(nodePresignedUrls)\n .values({\n nodeId: node.id,\n url,\n variant,\n variantId: variantRecord?.id,\n disposition: options.disposition,\n expiresAt: addSeconds(new Date(), expiresIn),\n })\n .onConflictDoUpdate({\n target: [\n nodePresignedUrls.nodeId,\n nodePresignedUrls.variant,\n nodePresignedUrls.disposition,\n ],\n set: { url, expiresAt: addSeconds(new Date(), expiresIn) },\n });\n });\n\n return url;\n }\n\n /**\n * Moves a node to a different parent folder.\n *\n * The target parent is validated before the move is persisted. Moving a node\n * into itself or into one of its descendants is rejected to keep the tree\n * structure valid.\n *\n * @param input - The node id and the target parent id.\n * @returns A promise resolving to the updated node.\n * @throws {ServerError} If the move is invalid.\n */\n async moveNode(input: { id: string; parentId: string | null }) {\n const [node] = await this.#db.select().from(nodes).where(eq(nodes.id, input.id)).limit(1);\n\n if (!node) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"Node not found\" });\n }\n\n if (node.readonly) {\n throw new ServerError(\"BAD_REQUEST\", {\n message: \"Deze node is alleen leesbaar en kan niet worden gewijzigd\",\n });\n }\n\n if (input.parentId === node.id) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"Node cannot be moved into itself\" });\n }\n\n if (input.parentId !== undefined) {\n await this.#getWritableParent({ parentId: input.parentId, namespace: node.namespace });\n }\n\n if (input.parentId) {\n const subtree = await this.getNodeChildren([input.id]);\n\n if (subtree.some((child) => child.id === input.parentId)) {\n throw new ServerError(\"BAD_REQUEST\", {\n message: \"Node cannot be moved into one of its descendants\",\n });\n }\n }\n\n if (node.parentId === input.parentId) {\n return toLocalDriveNode(node);\n }\n\n const [result] = await this.#db\n .update(nodes)\n .set({ parentId: input.parentId })\n .where(eq(nodes.id, input.id))\n .returning();\n\n if (!result) {\n throw new ServerError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Node kon niet worden gewijzigd\",\n });\n }\n\n return toLocalDriveNode(result);\n }\n\n /**\n * Uploads a file through the storage service and creates the matching node.\n *\n * Behavior:\n * - Validates the parent folder when `parentId` is provided\n * - Uploads the file bytes through the storage service\n * - Persists the created storage asset id on the node\n * - Cleans up the uploaded asset when node creation fails\n *\n * @param input - File metadata and body.\n * @returns A promise resolving to the created file node.\n * @throws {ServerError} If validation or persistence fails.\n */\n async uploadFile(input: UploadLocalDriveFileSchema & { body: ObjectBodyInput }) {\n await this.#getWritableParent({ parentId: input.parentId, namespace: input.namespace });\n\n const asset = await this.storage.upload({\n name: input.name,\n body: input.body,\n contentType: input.contentType,\n size: input.size,\n visibility: \"private\",\n });\n\n try {\n const [result] = await this.#db\n .insert(nodes)\n .values({\n type: \"file\",\n name: input.name,\n namespace: input.namespace,\n parentId: input.parentId,\n size: asset.size ?? input.size,\n contentType: asset.contentType ?? input.contentType,\n hidden: input.hidden ?? false,\n readonly: input.readonly ?? false,\n subtype: inferLocalDriveNodeSubtype(input),\n assetId: asset.id,\n })\n .returning();\n\n if (!result) {\n throw new ServerError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Oep! Er is iets fout gegaan\",\n });\n }\n\n return toLocalDriveNode(result);\n } catch (error) {\n await this.storage.purgeAsset(asset.id).catch(() => undefined);\n throw error;\n }\n }\n\n /**\n * Creates a direct upload intent and the matching file node.\n *\n * Behavior:\n * - Validates the parent folder when `parentId` is provided\n * - Requests a presigned upload from the storage service\n * - Creates the file node linked to the pending storage asset\n * - Purges the pending asset when node creation fails\n *\n * @param input - File metadata used to create the upload intent.\n * @returns The created asset intent, presigned URL, and file node.\n * @throws {ServerError} If validation or node creation fails.\n */\n async presignUpload(input: PresignLocalDriveFileInput) {\n await this.#getWritableParent(input);\n\n // Generate the presigned url\n const { presignedUrl, ...asset } = await this.storage.presignUpload({\n uploadId: input.uploadId,\n name: input.name,\n contentType: input.contentType,\n size: input.size,\n visibility: input.visibility,\n });\n\n try {\n const [node] = await this.#db\n .insert(nodes)\n .values({\n type: \"file\",\n name: input.name,\n namespace: input.namespace,\n parentId: input.parentId,\n size: asset.size ?? input.size,\n contentType: asset.contentType ?? input.contentType,\n hidden: input.hidden ?? false,\n readonly: input.readonly ?? false,\n assetId: asset.id,\n subtype: inferLocalDriveNodeSubtype(input),\n })\n .returning();\n\n if (!node) {\n throw new ServerError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Oep! Er is iets fout gegaan\",\n });\n }\n\n return {\n uploadId: asset.uploadId,\n presignedUrl,\n node: toLocalDriveNode(node) as LocalDriveFileNode,\n asset,\n };\n } catch (error) {\n await this.storage.purgeAsset(asset.id).catch(() => undefined);\n throw error;\n }\n }\n\n /**\n * Finalizes a presigned upload and synchronizes the node metadata.\n *\n * Behavior:\n * - Confirms the pending asset through the storage service\n * - Finds the matching node by `assetId`\n * - Synchronizes final asset metadata onto the node\n * - Schedules preview generation for image files\n *\n * @param uploadId - The storage upload id returned by the presign step.\n * @returns A promise resolving to the finalized file node.\n * @throws {ServerError} If the asset or node cannot be resolved.\n */\n async confirmUpload(uploadId: string) {\n const asset = await this.storage.confirmUpload(uploadId);\n\n const [node] = await this.#db.select().from(nodes).where(eq(nodes.assetId, asset.id)).limit(1);\n\n if (!node) {\n throw new ServerError(\"NOT_FOUND\", { message: \"File not found\" });\n }\n\n const [result] = await this.#db\n .update(nodes)\n .set({\n size: asset.size ?? node.size,\n contentType: asset.contentType ?? node.contentType,\n })\n .where(eq(nodes.assetId, asset.id))\n .returning();\n\n if (!result) {\n throw new ServerError(\"NOT_FOUND\", { message: \"File not found\" });\n }\n\n /**\n * Generate the preview version of the file\n */\n after(async () => {\n await this.generatePreviews({ id: result.id });\n });\n\n return toLocalDriveNode(result) as LocalDriveFileNode;\n }\n\n /**\n * Generates preview assets for an image node.\n *\n * The original file is loaded from storage, resized to the configured device\n * widths, uploaded as separate storage assets, and linked through\n * `node_variants`. Existing preview variants are skipped.\n *\n * @param input - The target file node identifier.\n * @returns A promise that resolves once preview generation completes.\n * @throws {ServerError} If the source node or asset cannot be resolved.\n */\n async generatePreviews(input: { id: string }) {\n const [node] = await this.#db.select().from(nodes).where(eq(nodes.id, input.id)).limit(1);\n\n if (!node) {\n throw new ServerError(\"NOT_FOUND\", { message: \"Node not found\" });\n }\n\n if (!node.assetId) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"Node has no storage asset\" });\n }\n\n /**\n * Get the main version of the file\n */\n const response = await this.storage.getObject(node.assetId);\n\n const contentType = response.contentType;\n if (!response.body) {\n throw new ServerError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Oeps! Er is iets fout gegaan\",\n });\n }\n\n /**\n * Transform the main version of the file to a buffer\n */\n const buffer = await stream.buffer(response.body);\n\n /**\n * Generate the preview versions for images\n */\n if (contentType?.startsWith(\"image/\")) {\n const sharp = await import(\"sharp\");\n const sourceAsset = await this.storage.getAssetById(node.assetId);\n\n if (!sourceAsset) {\n throw new ServerError(\"NOT_FOUND\", { message: \"Storage asset not found\" });\n }\n\n const existingVariants = await this.#db\n .select({ variant: nodeVariants.variant })\n .from(nodeVariants)\n .where(eq(nodeVariants.nodeId, input.id));\n\n const existingVariantNames = new Set(existingVariants.map((variant) => variant.variant));\n\n // Generate the preview versions\n await Promise.allSettled(\n deviceSizes.flatMap(async (width) => {\n const variant = `preview-${width}` as const;\n if (existingVariantNames.has(variant)) return;\n\n // Generate the preview\n const preview = await sharp.default(buffer).resize({ width }).webp().toBuffer();\n\n // Upload the preview and add the variant to the database\n return this.#db.transaction(async (tx) => {\n const asset = await this.storage.upload({\n body: preview,\n name: `${input.id}-preview-${width}.webp`,\n contentType: \"image/webp\",\n size: preview.byteLength,\n visibility: sourceAsset.visibility,\n });\n\n try {\n await tx.insert(nodeVariants).values({\n nodeId: input.id,\n assetId: asset.id,\n variant,\n width,\n });\n } catch (error) {\n await this.storage.purgeAsset(asset.id).catch(() => undefined);\n throw error;\n }\n });\n }),\n );\n }\n }\n\n /**\n * Creates a folder node in the drive tree.\n *\n * The parent folder is validated before the folder is inserted. Folders do\n * not own a storage asset and are stored purely as drive metadata.\n *\n * @param input - Folder metadata.\n * @returns A promise resolving to the created folder node.\n * @throws {ServerError} If validation or insertion fails.\n */\n async createFolder(input: CreateLocalDriveFolderSchema) {\n await this.#getWritableParent({ parentId: input.parentId, namespace: input.namespace });\n\n /**\n * Create the folder\n */\n const [result] = await this.#db\n .insert(nodes)\n .values({\n type: \"folder\",\n name: input.name,\n namespace: input.namespace,\n parentId: input.parentId,\n hidden: input.hidden ?? false,\n readonly: input.readonly ?? false,\n })\n .returning();\n\n if (!result) {\n throw new ServerError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Folder kon niet worden aangemaakt\",\n });\n }\n\n return toLocalDriveNode(result);\n }\n\n /**\n * Updates mutable node metadata.\n *\n * Supported updates currently include renaming, reparenting, visibility flags,\n * and archive state. Structural updates are validated before being persisted.\n *\n * @param id - The target node id.\n * @param data - Partial node fields to update.\n * @returns A promise resolving to the updated node.\n * @throws {ServerError} If the node does not exist or the update is invalid.\n */\n async updateNode(id: string, data: UpdateLocalDriveNodeInput) {\n const [node] = await this.#db.select().from(nodes).where(eq(nodes.id, id)).limit(1);\n\n if (!node) {\n throw new ServerError(\"NOT_FOUND\", { message: \"Node not found\" });\n }\n\n if (data.parentId !== undefined) {\n await this.#getWritableParent({ parentId: data.parentId, namespace: node.namespace });\n }\n\n const [result] = await this.#db\n .update(nodes)\n .set({\n name: data.name,\n parentId: data.parentId,\n hidden: data.hidden,\n readonly: data.readonly,\n archivedAt: data.archivedAt,\n })\n .where(eq(nodes.id, id))\n .returning();\n\n if (!result) {\n throw new ServerError(\"INTERNAL_SERVER_ERROR\", {\n message: \"Node kon niet worden gewijzigd\",\n });\n }\n\n return toLocalDriveNode(result);\n }\n\n /**\n * Sets the readonly state for multiple nodes.\n *\n * This method is intended for bulk lock and unlock operations in the drive UI.\n *\n * @param ids - The node ids to update.\n * @param readonly - The readonly state to apply.\n * @returns A promise resolving to the updated nodes.\n * @throws {ServerError} If no ids are provided or no matching nodes are found.\n */\n async setReadonly(ids: string[], readonly: boolean) {\n if (ids.length === 0) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"No node ids provided\" });\n }\n\n const result = await this.#db\n .update(nodes)\n .set({ readonly })\n .where(inArray(nodes.id, [...new Set(ids)]))\n .returning();\n\n if (result.length === 0) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"No matching nodes found\" });\n }\n\n return result.map(toLocalDriveNode);\n }\n\n /**\n * Archives nodes by setting their archive timestamp.\n *\n * This is a drive-level archive state and does not delete the underlying\n * storage assets.\n *\n * @param ids - The node ids to archive.\n * @returns A promise resolving to the archived nodes.\n * @throws {ServerError} If no ids are provided or no matching nodes are found.\n */\n async archiveNodes(ids: string[]) {\n if (ids.length === 0) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"No node ids provided\" });\n }\n\n const result = await this.#db\n .update(nodes)\n .set({ archivedAt: new Date() })\n .where(and(inArray(nodes.id, [...new Set(ids)]), isNull(nodes.archivedAt)))\n .returning();\n\n if (result.length === 0) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"No matching nodes found\" });\n }\n\n return result.map(toLocalDriveNode);\n }\n\n /**\n * Restores archived nodes by clearing their archive timestamp.\n *\n * @param ids - The node ids to restore.\n * @returns A promise resolving to the restored nodes.\n * @throws {ServerError} If no ids are provided or no matching nodes are found.\n */\n async restoreNodes(ids: string[]) {\n if (ids.length === 0) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"No node ids provided\" });\n }\n\n const result = await this.#db\n .update(nodes)\n .set({ archivedAt: null })\n .where(and(inArray(nodes.id, [...new Set(ids)]), isNotNull(nodes.archivedAt)))\n .returning();\n\n if (result.length === 0) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"No matching nodes found\" });\n }\n\n return result.map(toLocalDriveNode);\n }\n\n /**\n * Permanently deletes a node subtree and all linked assets.\n *\n * The subtree includes the provided node and all descendants. Associated file\n * assets and generated preview assets are purged from storage after the nodes\n * are removed from the drive catalog.\n *\n * @param id - The root node id to delete.\n * @throws {ServerError} If the node does not exist.\n */\n async deleteNode(id: string) {\n const subtree = await this.getNodeChildren([id]); // includes root + descendants\n if (subtree.length === 0) {\n throw new ServerError(\"NOT_FOUND\", { message: \"Node not found\" });\n }\n\n const nodeIds = [...new Set(subtree.map((node) => node.id))];\n const assetIds = await this.#getSubtreeAssetIds(nodeIds, subtree);\n\n // then DB cleanup\n await this.#db.delete(nodes).where(inArray(nodes.id, nodeIds));\n\n if (assetIds.length > 0) {\n await this.storage.purgeAssets(assetIds);\n }\n }\n\n /**\n * Permanently deletes multiple node subtrees and their linked assets.\n *\n * Each provided id is treated as a subtree root. All descendant nodes are\n * removed together with their main file assets and generated preview assets.\n *\n * @param ids - Root node ids to delete.\n * @returns A promise that resolves when deletion completes.\n * @throws {ServerError} If no ids are provided or no matching nodes are found.\n */\n async deleteNodes(ids: string[]) {\n if (ids.length === 0) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"No node ids provided\" });\n }\n\n const subtree = await this.getNodeChildren(ids);\n if (subtree.length === 0) {\n throw new ServerError(\"BAD_REQUEST\", { message: \"No matching nodes found\" });\n }\n\n const nodeIds = [...new Set(subtree.map((node) => node.id))];\n const assetIds = await this.#getSubtreeAssetIds(nodeIds, subtree);\n\n /**\n * Delete files and folders in a database\n */\n await this.#db.delete(nodes).where(inArray(nodes.id, nodeIds));\n\n if (assetIds.length > 0) {\n await this.storage.purgeAssets(assetIds);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAyEA,IAAa,aAAb,MASA;CACE;CACA;CAEA,YAAY,EAAE,IAAI,WAAsC;AACtD,QAAKA,KAAM;AACX,OAAK,UAAU;;;;;;;;;;;CAYjB,OAAMC,kBAAmB,OAAwD;AAC/E,MAAI,CAAC,MAAM,SAAU,QAAO;EAE5B,MAAM,CAAC,UAAU,MAAM,MAAKD,GACzB,QAAQ,CACR,KAAK,MAAM,CACX,MAAM,GAAG,MAAM,IAAI,MAAM,SAAS,CAAC,CACnC,MAAM,EAAE;AAEX,MAAI,CAAC,OACH,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,oBAAoB,CAAC;AAGvE,MAAI,CAAC,mBAAmB,OAAO,CAC7B,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,0BAA0B,CAAC;AAG7E,MAAI,OAAO,cAAc,MAAM,UAC7B,OAAM,IAAI,YAAY,eAAe,EACnC,SAAS,uCACV,CAAC;AAGJ,MAAI,OAAO,WACT,OAAM,IAAI,YAAY,eAAe,EACnC,SAAS,sBACV,CAAC;AAGJ,SAAO;;;;;;;;;;;;CAaT,OAAME,mBACJ,SACA,SACA;EACA,MAAM,eAAe,CACnB,GAAG,IAAI,IACL,QACG,QAAQ,SAAS,KAAK,SAAS,OAAO,CACtC,KAAK,SAAS,KAAK,QAAQ,CAC3B,QAAQ,YAA+B,WAAW,KAAK,CAC3D,CACF;EAED,MAAM,gBAAgB,MAAM,MAAKF,GAC9B,OAAO,EAAE,SAAS,aAAa,SAAS,CAAC,CACzC,KAAK,aAAa,CAClB,MAAM,QAAQ,aAAa,QAAQ,QAAQ,CAAC;AAE/C,SAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,cAAc,GAAG,cAAc,KAAK,YAAY,QAAQ,QAAQ,CAAC,CAAC,CAAC;;;;;;;;;;;;;CAc5F,MAAM,YAAY,IAA4C;EAC5D,MAAM,CAAC,QAAQ,MAAM,MAAKA,GAAI,QAAQ,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,MAAM,IAAI,GAAG,CAAC;AAC1E,SAAO,OAAO,iBAAiB,KAAK,GAAG;;;;;;;;;;;CAYzC,MAAM,oBAAoB,OAGqB;EAC7C,MAAM,OAAO,MAAM,KAAK,YAAY,MAAM,GAAG;AAC7C,MAAI,CAAC,QAAQ,KAAK,cAAc,MAAM,UAAW,QAAO;EAExD,MAAM,WAAW,MAAM,KAAK,mBAAmB,EAC7C,SAAS;GACP,WAAW,MAAM;GACjB,UAAU,KAAK;GACf,QAAQ;GACR,YAAY;GACb,EACF,CAAC;AAEF,SAAO;GAAE,GAAG;GAAM;GAAU;;;;;;;;;;;;;;CAe9B,MAAM,mBAAmB,EACvB,SACA,GAAG,SAC4D;EAC/D,MAAM,UAAU,4BAA4B,OAAO,OAAO,IAAI,MAAM,UAAU,CAAC;EAC/E,MAAM,SAAS,2BAA2B,OAAO,CAAC,MAAM,KAAK,CAAC;EAC9D,MAAM,iBACJ,QAAQ,eAAe,OACnB,UAAU,MAAM,WAAW,GAC3B,QAAQ,eAAe,QACrB,OAAO,MAAM,WAAW,GACxB;EACR,MAAM,eAAe,QAAQ,WACzB,GAAG,MAAM,UAAU,QAAQ,SAAS,GACpC,OAAO,MAAM,SAAS;AAkB1B,UAhBc,MAAM,MAAKA,GACtB,QAAQ,CACR,KAAK,MAAM,CACX,MACC,IACE,QAAQ,WAAW,OAAO,QAAQ,MAAM,IAAI,QAAQ,QAAQ,GAAG,QAC/D,QAAQ,SAAS,OAAO,QAAQ,MAAM,MAAM,QAAQ,MAAM,GAAG,QAC7D,gBACA,QAAQ,UAAU,OAAO,GAAG,MAAM,QAAQ,QAAQ,OAAO,GAAG,QAC5D,cACA,GAAG,MAAM,WAAW,QAAQ,UAAU,EACtC,OACD,CACF,CACA,QAAQ,QAAe,EAEb,IAAI,iBAAiB;;;;;;;;;;;CAYpC,MAAM,SAAS,EACb,SACA,GAAG,SAC8E;EACjF,MAAM,UAAU,4BAA4B,OAAO,OAAO,KAAK,MAAM,UAAU,CAAC;EAChF,MAAM,SAAS,2BAA2B,OAAO,CAAC,MAAM,KAAK,CAAC;EAE9D,MAAM,iBACJ,QAAQ,eAAe,OACnB,UAAU,MAAM,WAAW,GAC3B,QAAQ,eAAe,QACrB,OAAO,MAAM,WAAW,GACxB;EACR,MAAM,eAAe,QAAQ,WACzB,GAAG,MAAM,UAAU,QAAQ,SAAS,GACpC,OAAO,MAAM,SAAS;EAE1B,MAAM,QAAQ,MAAM;EAEpB,MAAM,SADS,MAAM,SACG;EAExB,MAAM,QAAQ,IACZ,QAAQ,WAAW,OAAO,QAAQ,MAAM,IAAI,QAAQ,QAAQ,GAAG,QAC/D,QAAQ,SAAS,OAAO,QAAQ,MAAM,MAAM,QAAQ,MAAM,GAAG,QAC7D,QAAQ,YAAY,OAAO,QAAQ,MAAM,SAAS,QAAQ,SAAS,GAAG,QACtE,QAAQ,gBAAgB,OAAO,QAAQ,MAAM,aAAa,QAAQ,aAAa,GAAG,QAClF,gBACA,QAAQ,UAAU,OAAO,GAAG,MAAM,QAAQ,QAAQ,OAAO,GAAG,QAC5D,cACA,GAAG,MAAM,WAAW,QAAQ,UAAU,EACtC,OACD;EAED,MAAM,CAAC,MAAM,SAAS,MAAM,MAAKA,GAAI,YAAY,OAAO,OAAO;AAc7D,UAAO,CAbM,MAAM,GAChB,OAAO;IACN,GAAG,gBAAgB,MAAM;IACzB,OAAO;IACR,CAAC,CACD,KAAK,MAAM,CACX,MAAM,MAAM,CACZ,SAAS,eAAe,GAAG,MAAM,SAAS,cAAc,GAAG,CAAC,CAC5D,QAAQ,QAAe,CACvB,MAAM,MAAM,CACZ,OAAO,OAAO,EAEH,MAAM,GAAG,OAAO,OAAO,MAAM,CACvB;IACpB;AAEF,SAAO,yBAAyB;GAC9B,MAAM,KAAK,KAAK,EAAE,OAAO,GAAG,YAAY;IAAE,GAAG,iBAAiB,KAAK;IAAE;IAAO,EAAE;GAC9E,OAAO;GACP;GACD,CAAC;;;;;;;;;;;CAYJ,MAAM,SAAS,EACb,SACA,GAAG,SAC8E;EACjF,MAAM,UAAU,4BAA4B,OAAO,OAAO,KAAK,MAAM,UAAU,CAAC;EAChF,MAAM,SAAS,2BAA2B,OAAO,CAAC,MAAM,KAAK,CAAC;EAC9D,MAAM,iBACJ,QAAQ,eAAe,OACnB,UAAU,MAAM,WAAW,GAC3B,QAAQ,eAAe,QACrB,OAAO,MAAM,WAAW,GACxB;EACR,MAAM,QAAQ,MAAM;EAEpB,MAAM,SADS,MAAM,SACG;EAExB,MAAM,QAAQ,IACZ,QAAQ,WAAW,OAAO,QAAQ,MAAM,IAAI,QAAQ,QAAQ,GAAG,QAC/D,QAAQ,SAAS,OAAO,QAAQ,MAAM,MAAM,QAAQ,MAAM,GAAG,QAC7D,QAAQ,YAAY,OAAO,QAAQ,MAAM,SAAS,QAAQ,SAAS,GAAG,QACtE,QAAQ,gBAAgB,OAAO,QAAQ,MAAM,aAAa,QAAQ,aAAa,GAAG,QAClF,gBACA,QAAQ,UAAU,OAAO,GAAG,MAAM,QAAQ,QAAQ,OAAO,GAAG,QAC5D,QAAQ,YAAY,GAAG,MAAM,WAAW,QAAQ,UAAU,GAAG,QAC7D,OACD;EAED,MAAM,CAAC,MAAM,SAAS,MAAM,MAAKA,GAAI,YAAY,OAAO,OAAO;AAc7D,UAAO,CAbM,MAAM,GAChB,OAAO;IACN,GAAG,gBAAgB,MAAM;IACzB,OAAO;IACR,CAAC,CACD,KAAK,MAAM,CACX,MAAM,MAAM,CACZ,SAAS,eAAe,GAAG,MAAM,SAAS,cAAc,GAAG,CAAC,CAC5D,QAAQ,QAAe,CACvB,MAAM,MAAM,CACZ,OAAO,OAAO,EAEH,MAAM,GAAG,OAAO,OAAO,MAAM,CACvB;IACpB;AAEF,SAAO,yBAAyB;GAC9B,MAAM,KAAK,KAAK,EAAE,OAAO,GAAG,YAAY;IAAE,GAAG,iBAAiB,KAAK;IAAE;IAAO,EAAE;GAC9E,OAAO;GACP;GACD,CAAC;;;;;;;;;;;CAYJ,MAAM,iBAAiB,OAAiD;AACtE,MAAI,CAAC,MAAM,GAAI,QAAO,EAAE;EAExB,MAAM,iBAAiB,MAAKA,GACzB,OAAO;GACN,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,OAAO,GAAW,IAAI,GAAG,QAAQ;GAClC,CAAC,CACD,KAAK,MAAM,CACX,MAAM,IAAI,GAAG,MAAM,IAAI,MAAM,GAAG,EAAE,GAAG,MAAM,WAAW,MAAM,UAAU,CAAC,CAAC;EAE3E,MAAM,QAAQ;EACd,MAAM,wBAAwB,eAAe,GAAG,MAAM;EACtD,MAAM,qBAAqB,IAAI,IAAI,IAAI,MAAM,GAAG;EAEhD,MAAM,iBAAiB,eAAe,SACpC,MAAKA,GACF,OAAO;GACN,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,OAAO,GAAW,GAAG,sBAAsB,MAAM;GAClD,CAAC,CACD,KAAK,MAAM,CACX,UAAU,oBAAoB,GAAG,MAAM,IAAI,sBAAsB,SAAS,CAAC,CAC/E;AAMD,UAJgB,MAAM,MAAKA,GAAI,QAC7B,GAAG,kBAAkB,mBAAmB,MAAM,eAAe,iBAAiB,qBAC/E,EAEa,MAAM,UAAU,GAAG,MAAM,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE;;;;;;;;;;;;;;;CAgBjF,MAAM,gBAAgB,KAAe;AACnC,MAAI,IAAI,WAAW,EAAG,QAAO,EAAE;EAE/B,MAAM,iBAAiB,MAAKA,GACzB,OAAO;GACN,IAAI,MAAM;GACV,SAAS,MAAM;GACf,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,OAAO,GAAW,IAAI,GAAG,QAAQ;GAClC,CAAC,CACD,KAAK,MAAM,CACX,MAAM,QAAQ,MAAM,IAAI,IAAI,CAAC;EAEhC,MAAM,QAAQ;EACd,MAAM,uBAAuB,eAAe,GAAG,MAAM;EACrD,MAAM,qBAAqB,IAAI,IAAI,IAAI,MAAM,GAAG;EAEhD,MAAM,iBAAiB,eAAe,SACpC,MAAKA,GACF,OAAO;GACN,IAAI,MAAM;GACV,SAAS,MAAM;GACf,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,OAAO,GAAW,GAAG,qBAAqB,MAAM;GACjD,CAAC,CACD,KAAK,MAAM,CACX,UAAU,oBAAoB,GAAG,MAAM,UAAU,qBAAqB,GAAG,CAAC,CAC9E;AAmBD,UAjBgB,MAAM,MAAKA,GAAI,QAC7B,GAAG,kBAAkB,mBAAmB,MAAM,eAAe;;YAEvD,qBAAqB,GAAG;YACxB,qBAAqB,QAAQ;YAC7B,qBAAqB,KAAK;YAC1B,qBAAqB,SAAS;gBAC1B,qBAAqB,MAAM;eAC5B,mBAAmB;;YAEtB,qBAAqB,GAAG;YACxB,qBAAqB,QAAQ;YAC7B,qBAAqB,KAAK;YAC1B,qBAAqB,SAAS;QAErC,EAEc,QAAQ,EAAE;;;;;;;;;;;;;;;;;;CAmB3B,MAAM,OAAO,MAAiB,UAAiC,+BAA+B;AAC5F,MAAI,CAAC,iBAAiB,KAAK,CACzB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,sBAAsB,CAAC;EAGzE,MAAM,CAAC,gBAAgB,MAAM,MAAKA,GAC/B,OAAO;GAAE,KAAK,kBAAkB;GAAK,WAAW,kBAAkB;GAAW,CAAC,CAC9E,KAAK,kBAAkB,CACvB,MACC,IACE,GAAG,kBAAkB,QAAQ,KAAK,GAAG,EACrC,GAAG,kBAAkB,SAAS,QAAQ,QAAQ,EAC9C,GAAG,kBAAkB,aAAa,QAAQ,YAAY,CACvD,CACF;AAEH,MAAI,gBAAgB,aAAa,4BAAY,IAAI,MAAM,CAAE,QAAO,aAAa;EAE7E,MAAM,YAAY,OAAO;EAEzB,MAAM,CAAC,iBACL,QAAQ,YAAY,SAChB,EAAE,GACF,MAAM,MAAKA,GACR,OAAO;GACN,IAAI,aAAa;GACjB,SAAS,aAAa;GACtB,SAAS,aAAa;GACvB,CAAC,CACD,KAAK,aAAa,CAClB,MACC,IAAI,GAAG,aAAa,QAAQ,KAAK,GAAG,EAAE,GAAG,aAAa,SAAS,QAAQ,QAAQ,CAAC,CACjF;EAGT,MAAM,UAAU,gBAAgB,QAAQ,UAAU;EAClD,MAAM,UAAU,eAAe,WAAW,KAAK;AAE/C,MAAI,CAAC,QACH,OAAM,IAAI,YAAY,aAAa,EAAE,SAAS,2BAA2B,CAAC;AAG5E,UAAQ,KACN,uCAAuC,KAAK,GAAG,iBAAiB,QAAQ,oBAAoB,QAAQ,cACrG;EAGD,MAAM,MAAM,MAAM,KAAK,QAAQ,aAAa,SAAS;GACnD,aAAa,GAAG,QAAQ,YAAY,cAAc,KAAK,KAAK;GAC5D;GACD,CAAC;AAGF,QAAM,YAAY;AAChB,SAAM,MAAKA,GACR,OAAO,kBAAkB,CACzB,OAAO;IACN,QAAQ,KAAK;IACb;IACA;IACA,WAAW,eAAe;IAC1B,aAAa,QAAQ;IACrB,WAAW,2BAAW,IAAI,MAAM,EAAE,UAAU;IAC7C,CAAC,CACD,mBAAmB;IAClB,QAAQ;KACN,kBAAkB;KAClB,kBAAkB;KAClB,kBAAkB;KACnB;IACD,KAAK;KAAE;KAAK,WAAW,2BAAW,IAAI,MAAM,EAAE,UAAU;KAAE;IAC3D,CAAC;IACJ;AAEF,SAAO;;;;;;;;;;;;;CAcT,MAAM,SAAS,OAAgD;EAC7D,MAAM,CAAC,QAAQ,MAAM,MAAKA,GAAI,QAAQ,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE;AAEzF,MAAI,CAAC,KACH,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,kBAAkB,CAAC;AAGrE,MAAI,KAAK,SACP,OAAM,IAAI,YAAY,eAAe,EACnC,SAAS,6DACV,CAAC;AAGJ,MAAI,MAAM,aAAa,KAAK,GAC1B,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,oCAAoC,CAAC;AAGvF,MAAI,MAAM,aAAa,OACrB,OAAM,MAAKC,kBAAmB;GAAE,UAAU,MAAM;GAAU,WAAW,KAAK;GAAW,CAAC;AAGxF,MAAI,MAAM,UAGR;QAFgB,MAAM,KAAK,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAE1C,MAAM,UAAU,MAAM,OAAO,MAAM,SAAS,CACtD,OAAM,IAAI,YAAY,eAAe,EACnC,SAAS,oDACV,CAAC;;AAIN,MAAI,KAAK,aAAa,MAAM,SAC1B,QAAO,iBAAiB,KAAK;EAG/B,MAAM,CAAC,UAAU,MAAM,MAAKD,GACzB,OAAO,MAAM,CACb,IAAI,EAAE,UAAU,MAAM,UAAU,CAAC,CACjC,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG,CAAC,CAC7B,WAAW;AAEd,MAAI,CAAC,OACH,OAAM,IAAI,YAAY,yBAAyB,EAC7C,SAAS,kCACV,CAAC;AAGJ,SAAO,iBAAiB,OAAO;;;;;;;;;;;;;;;CAgBjC,MAAM,WAAW,OAA+D;AAC9E,QAAM,MAAKC,kBAAmB;GAAE,UAAU,MAAM;GAAU,WAAW,MAAM;GAAW,CAAC;EAEvF,MAAM,QAAQ,MAAM,KAAK,QAAQ,OAAO;GACtC,MAAM,MAAM;GACZ,MAAM,MAAM;GACZ,aAAa,MAAM;GACnB,MAAM,MAAM;GACZ,YAAY;GACb,CAAC;AAEF,MAAI;GACF,MAAM,CAAC,UAAU,MAAM,MAAKD,GACzB,OAAO,MAAM,CACb,OAAO;IACN,MAAM;IACN,MAAM,MAAM;IACZ,WAAW,MAAM;IACjB,UAAU,MAAM;IAChB,MAAM,MAAM,QAAQ,MAAM;IAC1B,aAAa,MAAM,eAAe,MAAM;IACxC,QAAQ,MAAM,UAAU;IACxB,UAAU,MAAM,YAAY;IAC5B,SAAS,2BAA2B,MAAM;IAC1C,SAAS,MAAM;IAChB,CAAC,CACD,WAAW;AAEd,OAAI,CAAC,OACH,OAAM,IAAI,YAAY,yBAAyB,EAC7C,SAAS,+BACV,CAAC;AAGJ,UAAO,iBAAiB,OAAO;WACxB,OAAO;AACd,SAAM,KAAK,QAAQ,WAAW,MAAM,GAAG,CAAC,YAAY,OAAU;AAC9D,SAAM;;;;;;;;;;;;;;;;CAiBV,MAAM,cAAc,OAAmC;AACrD,QAAM,MAAKC,kBAAmB,MAAM;EAGpC,MAAM,EAAE,cAAc,GAAG,UAAU,MAAM,KAAK,QAAQ,cAAc;GAClE,UAAU,MAAM;GAChB,MAAM,MAAM;GACZ,aAAa,MAAM;GACnB,MAAM,MAAM;GACZ,YAAY,MAAM;GACnB,CAAC;AAEF,MAAI;GACF,MAAM,CAAC,QAAQ,MAAM,MAAKD,GACvB,OAAO,MAAM,CACb,OAAO;IACN,MAAM;IACN,MAAM,MAAM;IACZ,WAAW,MAAM;IACjB,UAAU,MAAM;IAChB,MAAM,MAAM,QAAQ,MAAM;IAC1B,aAAa,MAAM,eAAe,MAAM;IACxC,QAAQ,MAAM,UAAU;IACxB,UAAU,MAAM,YAAY;IAC5B,SAAS,MAAM;IACf,SAAS,2BAA2B,MAAM;IAC3C,CAAC,CACD,WAAW;AAEd,OAAI,CAAC,KACH,OAAM,IAAI,YAAY,yBAAyB,EAC7C,SAAS,+BACV,CAAC;AAGJ,UAAO;IACL,UAAU,MAAM;IAChB;IACA,MAAM,iBAAiB,KAAK;IAC5B;IACD;WACM,OAAO;AACd,SAAM,KAAK,QAAQ,WAAW,MAAM,GAAG,CAAC,YAAY,OAAU;AAC9D,SAAM;;;;;;;;;;;;;;;;CAiBV,MAAM,cAAc,UAAkB;EACpC,MAAM,QAAQ,MAAM,KAAK,QAAQ,cAAc,SAAS;EAExD,MAAM,CAAC,QAAQ,MAAM,MAAKA,GAAI,QAAQ,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,MAAM,SAAS,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE;AAE9F,MAAI,CAAC,KACH,OAAM,IAAI,YAAY,aAAa,EAAE,SAAS,kBAAkB,CAAC;EAGnE,MAAM,CAAC,UAAU,MAAM,MAAKA,GACzB,OAAO,MAAM,CACb,IAAI;GACH,MAAM,MAAM,QAAQ,KAAK;GACzB,aAAa,MAAM,eAAe,KAAK;GACxC,CAAC,CACD,MAAM,GAAG,MAAM,SAAS,MAAM,GAAG,CAAC,CAClC,WAAW;AAEd,MAAI,CAAC,OACH,OAAM,IAAI,YAAY,aAAa,EAAE,SAAS,kBAAkB,CAAC;;;;AAMnE,QAAM,YAAY;AAChB,SAAM,KAAK,iBAAiB,EAAE,IAAI,OAAO,IAAI,CAAC;IAC9C;AAEF,SAAO,iBAAiB,OAAO;;;;;;;;;;;;;CAcjC,MAAM,iBAAiB,OAAuB;EAC5C,MAAM,CAAC,QAAQ,MAAM,MAAKA,GAAI,QAAQ,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE;AAEzF,MAAI,CAAC,KACH,OAAM,IAAI,YAAY,aAAa,EAAE,SAAS,kBAAkB,CAAC;AAGnE,MAAI,CAAC,KAAK,QACR,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,6BAA6B,CAAC;;;;EAMhF,MAAM,WAAW,MAAM,KAAK,QAAQ,UAAU,KAAK,QAAQ;EAE3D,MAAM,cAAc,SAAS;AAC7B,MAAI,CAAC,SAAS,KACZ,OAAM,IAAI,YAAY,yBAAyB,EAC7C,SAAS,gCACV,CAAC;;;;EAMJ,MAAM,SAAS,MAAM,OAAO,OAAO,SAAS,KAAK;;;;AAKjD,MAAI,aAAa,WAAW,SAAS,EAAE;GACrC,MAAM,QAAQ,MAAM,OAAO;GAC3B,MAAM,cAAc,MAAM,KAAK,QAAQ,aAAa,KAAK,QAAQ;AAEjE,OAAI,CAAC,YACH,OAAM,IAAI,YAAY,aAAa,EAAE,SAAS,2BAA2B,CAAC;GAG5E,MAAM,mBAAmB,MAAM,MAAKA,GACjC,OAAO,EAAE,SAAS,aAAa,SAAS,CAAC,CACzC,KAAK,aAAa,CAClB,MAAM,GAAG,aAAa,QAAQ,MAAM,GAAG,CAAC;GAE3C,MAAM,uBAAuB,IAAI,IAAI,iBAAiB,KAAK,YAAY,QAAQ,QAAQ,CAAC;AAGxF,SAAM,QAAQ,WACZ,YAAY,QAAQ,OAAO,UAAU;IACnC,MAAM,UAAU,WAAW;AAC3B,QAAI,qBAAqB,IAAI,QAAQ,CAAE;IAGvC,MAAM,UAAU,MAAM,MAAM,QAAQ,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,UAAU;AAG/E,WAAO,MAAKA,GAAI,YAAY,OAAO,OAAO;KACxC,MAAM,QAAQ,MAAM,KAAK,QAAQ,OAAO;MACtC,MAAM;MACN,MAAM,GAAG,MAAM,GAAG,WAAW,MAAM;MACnC,aAAa;MACb,MAAM,QAAQ;MACd,YAAY,YAAY;MACzB,CAAC;AAEF,SAAI;AACF,YAAM,GAAG,OAAO,aAAa,CAAC,OAAO;OACnC,QAAQ,MAAM;OACd,SAAS,MAAM;OACf;OACA;OACD,CAAC;cACK,OAAO;AACd,YAAM,KAAK,QAAQ,WAAW,MAAM,GAAG,CAAC,YAAY,OAAU;AAC9D,YAAM;;MAER;KACF,CACH;;;;;;;;;;;;;CAcL,MAAM,aAAa,OAAqC;AACtD,QAAM,MAAKC,kBAAmB;GAAE,UAAU,MAAM;GAAU,WAAW,MAAM;GAAW,CAAC;;;;EAKvF,MAAM,CAAC,UAAU,MAAM,MAAKD,GACzB,OAAO,MAAM,CACb,OAAO;GACN,MAAM;GACN,MAAM,MAAM;GACZ,WAAW,MAAM;GACjB,UAAU,MAAM;GAChB,QAAQ,MAAM,UAAU;GACxB,UAAU,MAAM,YAAY;GAC7B,CAAC,CACD,WAAW;AAEd,MAAI,CAAC,OACH,OAAM,IAAI,YAAY,yBAAyB,EAC7C,SAAS,qCACV,CAAC;AAGJ,SAAO,iBAAiB,OAAO;;;;;;;;;;;;;CAcjC,MAAM,WAAW,IAAY,MAAiC;EAC5D,MAAM,CAAC,QAAQ,MAAM,MAAKA,GAAI,QAAQ,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE;AAEnF,MAAI,CAAC,KACH,OAAM,IAAI,YAAY,aAAa,EAAE,SAAS,kBAAkB,CAAC;AAGnE,MAAI,KAAK,aAAa,OACpB,OAAM,MAAKC,kBAAmB;GAAE,UAAU,KAAK;GAAU,WAAW,KAAK;GAAW,CAAC;EAGvF,MAAM,CAAC,UAAU,MAAM,MAAKD,GACzB,OAAO,MAAM,CACb,IAAI;GACH,MAAM,KAAK;GACX,UAAU,KAAK;GACf,QAAQ,KAAK;GACb,UAAU,KAAK;GACf,YAAY,KAAK;GAClB,CAAC,CACD,MAAM,GAAG,MAAM,IAAI,GAAG,CAAC,CACvB,WAAW;AAEd,MAAI,CAAC,OACH,OAAM,IAAI,YAAY,yBAAyB,EAC7C,SAAS,kCACV,CAAC;AAGJ,SAAO,iBAAiB,OAAO;;;;;;;;;;;;CAajC,MAAM,YAAY,KAAe,UAAmB;AAClD,MAAI,IAAI,WAAW,EACjB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,wBAAwB,CAAC;EAG3E,MAAM,SAAS,MAAM,MAAKA,GACvB,OAAO,MAAM,CACb,IAAI,EAAE,UAAU,CAAC,CACjB,MAAM,QAAQ,MAAM,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,CAC3C,WAAW;AAEd,MAAI,OAAO,WAAW,EACpB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,2BAA2B,CAAC;AAG9E,SAAO,OAAO,IAAI,iBAAiB;;;;;;;;;;;;CAarC,MAAM,aAAa,KAAe;AAChC,MAAI,IAAI,WAAW,EACjB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,wBAAwB,CAAC;EAG3E,MAAM,SAAS,MAAM,MAAKA,GACvB,OAAO,MAAM,CACb,IAAI,EAAE,4BAAY,IAAI,MAAM,EAAE,CAAC,CAC/B,MAAM,IAAI,QAAQ,MAAM,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,EAAE,OAAO,MAAM,WAAW,CAAC,CAAC,CAC1E,WAAW;AAEd,MAAI,OAAO,WAAW,EACpB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,2BAA2B,CAAC;AAG9E,SAAO,OAAO,IAAI,iBAAiB;;;;;;;;;CAUrC,MAAM,aAAa,KAAe;AAChC,MAAI,IAAI,WAAW,EACjB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,wBAAwB,CAAC;EAG3E,MAAM,SAAS,MAAM,MAAKA,GACvB,OAAO,MAAM,CACb,IAAI,EAAE,YAAY,MAAM,CAAC,CACzB,MAAM,IAAI,QAAQ,MAAM,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,CAAC,EAAE,UAAU,MAAM,WAAW,CAAC,CAAC,CAC7E,WAAW;AAEd,MAAI,OAAO,WAAW,EACpB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,2BAA2B,CAAC;AAG9E,SAAO,OAAO,IAAI,iBAAiB;;;;;;;;;;;;CAarC,MAAM,WAAW,IAAY;EAC3B,MAAM,UAAU,MAAM,KAAK,gBAAgB,CAAC,GAAG,CAAC;AAChD,MAAI,QAAQ,WAAW,EACrB,OAAM,IAAI,YAAY,aAAa,EAAE,SAAS,kBAAkB,CAAC;EAGnE,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,SAAS,KAAK,GAAG,CAAC,CAAC;EAC5D,MAAM,WAAW,MAAM,MAAKE,mBAAoB,SAAS,QAAQ;AAGjE,QAAM,MAAKF,GAAI,OAAO,MAAM,CAAC,MAAM,QAAQ,MAAM,IAAI,QAAQ,CAAC;AAE9D,MAAI,SAAS,SAAS,EACpB,OAAM,KAAK,QAAQ,YAAY,SAAS;;;;;;;;;;;;CAc5C,MAAM,YAAY,KAAe;AAC/B,MAAI,IAAI,WAAW,EACjB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,wBAAwB,CAAC;EAG3E,MAAM,UAAU,MAAM,KAAK,gBAAgB,IAAI;AAC/C,MAAI,QAAQ,WAAW,EACrB,OAAM,IAAI,YAAY,eAAe,EAAE,SAAS,2BAA2B,CAAC;EAG9E,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,SAAS,KAAK,GAAG,CAAC,CAAC;EAC5D,MAAM,WAAW,MAAM,MAAKE,mBAAoB,SAAS,QAAQ;;;;AAKjE,QAAM,MAAKF,GAAI,OAAO,MAAM,CAAC,MAAM,QAAQ,MAAM,IAAI,QAAQ,CAAC;AAE9D,MAAI,SAAS,SAAS,EACpB,OAAM,KAAK,QAAQ,YAAY,SAAS"}
@@ -0,0 +1,62 @@
1
+ import { LocalDriveFileNode, LocalDriveNode, PresignLocalDriveFileInput, UpdateLocalDriveNodeInput } from "./validators.mjs";
2
+ import { StorageAsset } from "@tulip-systems/core/storage";
3
+ import { BulkActionSchema } from "@tulip-systems/core/router";
4
+
5
+ //#region src/providers/local/lib/upload.client.d.ts
6
+ /**
7
+ * Upload request
8
+ */
9
+ type PrepareLocalDriveUploadInput = Omit<LocalDriveUploadFileRequest, "name" | "size" | "contentType">;
10
+ type LocalDriveUploadFileRequest = PresignLocalDriveFileInput & {
11
+ file: File;
12
+ };
13
+ type LocalDriveUploadHooks = {
14
+ beforePresign?: (input: LocalDriveUploadFileRequest) => Promise<void> | void;
15
+ afterPresign?: (presignResult: {
16
+ asset: Omit<StorageAsset, "presignedUrl">;
17
+ node: LocalDriveFileNode;
18
+ uploadId: string;
19
+ presignedUrl: string;
20
+ }) => Promise<void> | void;
21
+ beforeConfirm?: (presignResult: {
22
+ asset: Omit<StorageAsset, "presignedUrl">;
23
+ node: LocalDriveFileNode;
24
+ uploadId: string;
25
+ presignedUrl: string;
26
+ }) => Promise<void> | void;
27
+ afterConfirm?: (node: LocalDriveFileNode) => Promise<void> | void;
28
+ };
29
+ /**
30
+ * Upload client
31
+ */
32
+ type LocalDriveUploadClient = {
33
+ prepareUpload: (input: PrepareLocalDriveUploadInput) => LocalDriveUploadFileRequest;
34
+ upload: (input: LocalDriveUploadFileRequest, hooks?: LocalDriveUploadHooks) => Promise<LocalDriveFileNode>;
35
+ deleteFiles: (ids: string[]) => Promise<void>;
36
+ updateNode: (id: string, data: UpdateLocalDriveNodeInput) => Promise<LocalDriveNode>;
37
+ };
38
+ /**
39
+ * Create upload client
40
+ */
41
+ type CreateLocalDriveUploadClientProps = {
42
+ endpoints: {
43
+ presignUpload: (input: PresignLocalDriveFileInput) => Promise<{
44
+ asset: Omit<StorageAsset, "presignedUrl">;
45
+ node: LocalDriveFileNode;
46
+ uploadId: string;
47
+ presignedUrl: string;
48
+ }>;
49
+ confirmUpload: (input: {
50
+ uploadId: string;
51
+ }) => Promise<LocalDriveFileNode>;
52
+ deleteNodes: (input: BulkActionSchema) => Promise<void>;
53
+ updateNode: (input: {
54
+ id: string;
55
+ data: UpdateLocalDriveNodeInput;
56
+ }) => Promise<LocalDriveNode>;
57
+ };
58
+ };
59
+ declare function createLocalDriveUploadClient(props: CreateLocalDriveUploadClientProps): LocalDriveUploadClient;
60
+ //#endregion
61
+ export { LocalDriveUploadClient, LocalDriveUploadFileRequest, LocalDriveUploadHooks, PrepareLocalDriveUploadInput, createLocalDriveUploadClient };
62
+ //# sourceMappingURL=upload.client.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.client.d.mts","names":[],"sources":["../../../../src/providers/local/lib/upload.client.ts"],"mappings":";;;;;;;AAaA;KAAY,4BAAA,GAA+B,IAAA,CACzC,2BAAA;AAAA,KAIU,2BAAA,GAA8B,0BAAA;EAA+B,IAAA,EAAM,IAAA;AAAA;AAAA,KACnE,qBAAA;EACV,aAAA,IAAiB,KAAA,EAAO,2BAAA,KAAgC,OAAA;EACxD,YAAA,IAAgB,aAAA;IACd,KAAA,EAAO,IAAA,CAAK,YAAA;IACZ,IAAA,EAAM,kBAAA;IACN,QAAA;IACA,YAAA;EAAA,MACI,OAAA;EACN,aAAA,IAAiB,aAAA;IACf,KAAA,EAAO,IAAA,CAAK,YAAA;IACZ,IAAA,EAAM,kBAAA;IACN,QAAA;IACA,YAAA;EAAA,MACI,OAAA;EACN,YAAA,IAAgB,IAAA,EAAM,kBAAA,KAAuB,OAAA;AAAA;;;;KAMnC,sBAAA;EACV,aAAA,GAAgB,KAAA,EAAO,4BAAA,KAAiC,2BAAA;EACxD,MAAA,GACE,KAAA,EAAO,2BAAA,EACP,KAAA,GAAQ,qBAAA,KACL,OAAA,CAAQ,kBAAA;EACb,WAAA,GAAc,GAAA,eAAkB,OAAA;EAChC,UAAA,GAAa,EAAA,UAAY,IAAA,EAAM,yBAAA,KAA8B,OAAA,CAAQ,cAAA;AAAA;;;;KAMlE,iCAAA;EACH,SAAA;IACE,aAAA,GAAgB,KAAA,EAAO,0BAAA,KAA+B,OAAA;MACpD,KAAA,EAAO,IAAA,CAAK,YAAA;MACZ,IAAA,EAAM,kBAAA;MACN,QAAA;MACA,YAAA;IAAA;IAEF,aAAA,GAAgB,KAAA;MAAS,QAAA;IAAA,MAAuB,OAAA,CAAQ,kBAAA;IACxD,WAAA,GAAc,KAAA,EAAO,gBAAA,KAAqB,OAAA;IAC1C,UAAA,GAAa,KAAA;MAAS,EAAA;MAAY,IAAA,EAAM,yBAAA;IAAA,MAAgC,OAAA,CAAQ,cAAA;EAAA;AAAA;AAAA,iBAIpE,4BAAA,CACd,KAAA,EAAO,iCAAA,GACN,sBAAA"}
@@ -0,0 +1,100 @@
1
+ import { generateDefaultUUID } from "@tulip-systems/core/config";
2
+
3
+ //#region src/providers/local/lib/upload.client.ts
4
+ function createLocalDriveUploadClient(props) {
5
+ /**
6
+ * Create input schema for the upload method
7
+ * @param {PrepareLocalDriveUploadInput} input
8
+ * @returns {LocalDriveUploadFileRequest}
9
+ */
10
+ function prepareUpload(input) {
11
+ return {
12
+ ...input,
13
+ uploadId: input.uploadId ?? generateDefaultUUID(),
14
+ name: input.file.name,
15
+ size: input.file.size,
16
+ contentType: input.file.type
17
+ };
18
+ }
19
+ /**
20
+ * Upload file to the server
21
+ * @param {LocalDriveUploadFileRequest} input
22
+ * @returns {Promise<LocalDriveFileNode>}
23
+ */
24
+ async function upload(input, hooks) {
25
+ let presignResult = null;
26
+ try {
27
+ /**
28
+ * Presign
29
+ */
30
+ await hooks?.beforePresign?.(input);
31
+ const { file, ...presignInput } = input;
32
+ presignResult = await props.endpoints.presignUpload(presignInput);
33
+ await hooks?.afterPresign?.(presignResult);
34
+ /**
35
+ * Upload the file
36
+ */
37
+ const uploadResponse = await fetch(presignResult.presignedUrl, {
38
+ method: "PUT",
39
+ headers: { "Content-Type": input.file.type },
40
+ body: input.file
41
+ });
42
+ if (!uploadResponse.ok) {
43
+ const message = await uploadResponse.text();
44
+ throw new Error(`Upload failed: ${message}`);
45
+ }
46
+ /**
47
+ * Confirm
48
+ */
49
+ await hooks?.beforeConfirm?.(presignResult);
50
+ const node = await props.endpoints.confirmUpload({ uploadId: presignResult.uploadId });
51
+ await hooks?.afterConfirm?.(node);
52
+ /**
53
+ * Return
54
+ */
55
+ return node;
56
+ } catch (err) {
57
+ console.error("Upload error: ", err);
58
+ /**
59
+ * Delete if upload failed
60
+ * */
61
+ try {
62
+ if (presignResult?.node.id) await deleteFiles([presignResult.node.id]);
63
+ } catch (cleanupErr) {
64
+ console.error("Cleanup delete failed:", cleanupErr);
65
+ }
66
+ throw err;
67
+ }
68
+ }
69
+ /**
70
+ * Delete files
71
+ * @param {string[]} ids
72
+ */
73
+ async function deleteFiles(ids) {
74
+ await props.endpoints.deleteNodes({ ids });
75
+ }
76
+ /**
77
+ * Update node
78
+ * @param {string} id
79
+ * @param {UpdateLocalDriveNodeInput} data
80
+ */
81
+ async function updateNode(id, data) {
82
+ return props.endpoints.updateNode({
83
+ id,
84
+ data
85
+ });
86
+ }
87
+ /**
88
+ * Upload client
89
+ */
90
+ return {
91
+ prepareUpload,
92
+ upload,
93
+ deleteFiles,
94
+ updateNode
95
+ };
96
+ }
97
+
98
+ //#endregion
99
+ export { createLocalDriveUploadClient };
100
+ //# sourceMappingURL=upload.client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.client.mjs","names":[],"sources":["../../../../src/providers/local/lib/upload.client.ts"],"sourcesContent":["import { generateDefaultUUID } from \"@tulip-systems/core/config\";\nimport type { BulkActionSchema } from \"@tulip-systems/core/router\";\nimport type { StorageAsset } from \"@tulip-systems/core/storage\";\nimport type {\n LocalDriveFileNode,\n LocalDriveNode,\n PresignLocalDriveFileInput,\n UpdateLocalDriveNodeInput,\n} from \"./validators\";\n\n/**\n * Upload request\n */\nexport type PrepareLocalDriveUploadInput = Omit<\n LocalDriveUploadFileRequest,\n \"name\" | \"size\" | \"contentType\"\n>;\n\nexport type LocalDriveUploadFileRequest = PresignLocalDriveFileInput & { file: File };\nexport type LocalDriveUploadHooks = {\n beforePresign?: (input: LocalDriveUploadFileRequest) => Promise<void> | void;\n afterPresign?: (presignResult: {\n asset: Omit<StorageAsset, \"presignedUrl\">;\n node: LocalDriveFileNode;\n uploadId: string;\n presignedUrl: string;\n }) => Promise<void> | void;\n beforeConfirm?: (presignResult: {\n asset: Omit<StorageAsset, \"presignedUrl\">;\n node: LocalDriveFileNode;\n uploadId: string;\n presignedUrl: string;\n }) => Promise<void> | void;\n afterConfirm?: (node: LocalDriveFileNode) => Promise<void> | void;\n};\n\n/**\n * Upload client\n */\nexport type LocalDriveUploadClient = {\n prepareUpload: (input: PrepareLocalDriveUploadInput) => LocalDriveUploadFileRequest;\n upload: (\n input: LocalDriveUploadFileRequest,\n hooks?: LocalDriveUploadHooks,\n ) => Promise<LocalDriveFileNode>;\n deleteFiles: (ids: string[]) => Promise<void>;\n updateNode: (id: string, data: UpdateLocalDriveNodeInput) => Promise<LocalDriveNode>;\n};\n\n/**\n * Create upload client\n */\ntype CreateLocalDriveUploadClientProps = {\n endpoints: {\n presignUpload: (input: PresignLocalDriveFileInput) => Promise<{\n asset: Omit<StorageAsset, \"presignedUrl\">;\n node: LocalDriveFileNode;\n uploadId: string;\n presignedUrl: string;\n }>;\n confirmUpload: (input: { uploadId: string }) => Promise<LocalDriveFileNode>;\n deleteNodes: (input: BulkActionSchema) => Promise<void>;\n updateNode: (input: { id: string; data: UpdateLocalDriveNodeInput }) => Promise<LocalDriveNode>;\n };\n};\n\nexport function createLocalDriveUploadClient(\n props: CreateLocalDriveUploadClientProps,\n): LocalDriveUploadClient {\n /**\n * Create input schema for the upload method\n * @param {PrepareLocalDriveUploadInput} input\n * @returns {LocalDriveUploadFileRequest}\n */\n function prepareUpload(input: PrepareLocalDriveUploadInput): LocalDriveUploadFileRequest {\n return {\n ...input,\n uploadId: input.uploadId ?? generateDefaultUUID(),\n name: input.file.name,\n size: input.file.size,\n contentType: input.file.type,\n };\n }\n\n /**\n * Upload file to the server\n * @param {LocalDriveUploadFileRequest} input\n * @returns {Promise<LocalDriveFileNode>}\n */\n async function upload(\n input: LocalDriveUploadFileRequest,\n hooks?: LocalDriveUploadHooks,\n ): Promise<LocalDriveFileNode> {\n let presignResult: {\n asset: Omit<StorageAsset, \"presignedUrl\">;\n node: LocalDriveFileNode;\n uploadId: string;\n presignedUrl: string;\n } | null = null;\n\n try {\n /**\n * Presign\n */\n await hooks?.beforePresign?.(input);\n const { file, ...presignInput } = input;\n presignResult = await props.endpoints.presignUpload(presignInput);\n await hooks?.afterPresign?.(presignResult);\n /**\n * Upload the file\n */\n const uploadResponse = await fetch(presignResult.presignedUrl, {\n method: \"PUT\",\n headers: { \"Content-Type\": input.file.type },\n body: input.file,\n });\n\n if (!uploadResponse.ok) {\n const message = await uploadResponse.text();\n throw new Error(`Upload failed: ${message}`);\n }\n\n /**\n * Confirm\n */\n await hooks?.beforeConfirm?.(presignResult);\n const node = await props.endpoints.confirmUpload({ uploadId: presignResult.uploadId });\n await hooks?.afterConfirm?.(node);\n /**\n * Return\n */\n return node;\n } catch (err) {\n console.error(\"Upload error: \", err);\n\n /**\n * Delete if upload failed\n * */\n try {\n if (presignResult?.node.id) {\n await deleteFiles([presignResult.node.id]);\n }\n } catch (cleanupErr) {\n console.error(\"Cleanup delete failed:\", cleanupErr);\n }\n\n throw err;\n }\n }\n\n /**\n * Delete files\n * @param {string[]} ids\n */\n async function deleteFiles(ids: string[]) {\n await props.endpoints.deleteNodes({ ids });\n }\n\n /**\n * Update node\n * @param {string} id\n * @param {UpdateLocalDriveNodeInput} data\n */\n async function updateNode(id: string, data: UpdateLocalDriveNodeInput) {\n return props.endpoints.updateNode({ id, data });\n }\n\n /**\n * Upload client\n */\n return {\n prepareUpload,\n upload,\n deleteFiles,\n updateNode,\n };\n}\n"],"mappings":";;;AAkEA,SAAgB,6BACd,OACwB;;;;;;CAMxB,SAAS,cAAc,OAAkE;AACvF,SAAO;GACL,GAAG;GACH,UAAU,MAAM,YAAY,qBAAqB;GACjD,MAAM,MAAM,KAAK;GACjB,MAAM,MAAM,KAAK;GACjB,aAAa,MAAM,KAAK;GACzB;;;;;;;CAQH,eAAe,OACb,OACA,OAC6B;EAC7B,IAAI,gBAKO;AAEX,MAAI;;;;AAIF,SAAM,OAAO,gBAAgB,MAAM;GACnC,MAAM,EAAE,MAAM,GAAG,iBAAiB;AAClC,mBAAgB,MAAM,MAAM,UAAU,cAAc,aAAa;AACjE,SAAM,OAAO,eAAe,cAAc;;;;GAI1C,MAAM,iBAAiB,MAAM,MAAM,cAAc,cAAc;IAC7D,QAAQ;IACR,SAAS,EAAE,gBAAgB,MAAM,KAAK,MAAM;IAC5C,MAAM,MAAM;IACb,CAAC;AAEF,OAAI,CAAC,eAAe,IAAI;IACtB,MAAM,UAAU,MAAM,eAAe,MAAM;AAC3C,UAAM,IAAI,MAAM,kBAAkB,UAAU;;;;;AAM9C,SAAM,OAAO,gBAAgB,cAAc;GAC3C,MAAM,OAAO,MAAM,MAAM,UAAU,cAAc,EAAE,UAAU,cAAc,UAAU,CAAC;AACtF,SAAM,OAAO,eAAe,KAAK;;;;AAIjC,UAAO;WACA,KAAK;AACZ,WAAQ,MAAM,kBAAkB,IAAI;;;;AAKpC,OAAI;AACF,QAAI,eAAe,KAAK,GACtB,OAAM,YAAY,CAAC,cAAc,KAAK,GAAG,CAAC;YAErC,YAAY;AACnB,YAAQ,MAAM,0BAA0B,WAAW;;AAGrD,SAAM;;;;;;;CAQV,eAAe,YAAY,KAAe;AACxC,QAAM,MAAM,UAAU,YAAY,EAAE,KAAK,CAAC;;;;;;;CAQ5C,eAAe,WAAW,IAAY,MAAiC;AACrE,SAAO,MAAM,UAAU,WAAW;GAAE;GAAI;GAAM,CAAC;;;;;AAMjD,QAAO;EACL;EACA;EACA;EACA;EACD"}