@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,613 @@
1
+ import { GOOGLE_DRIVE_FOLDER_MIME_TYPE, GOOGLE_DRIVE_NODE_FIELDS } from "./constants.mjs";
2
+ import { GoogleDriveNodeDTO } from "./dto.mjs";
3
+ import { escapeGoogleDriveQueryValue, isGoogleNotFound, toGoogleDriveReadable } from "./helpers.mjs";
4
+ import { ServerError } from "@tulip-systems/core/router/server";
5
+ import { google } from "googleapis";
6
+
7
+ //#region src/providers/google/lib/service.server.ts
8
+ const DEFAULT_PAGE_SIZE = 10;
9
+ const CHILDREN_PAGE_SIZE = 1e3;
10
+ /**
11
+ * Google Drive provider.
12
+ *
13
+ * A single service instance can operate on multiple Google shared drives. The
14
+ * shared Drive `namespace` is the Google `driveId`, so namespace is supplied by
15
+ * method inputs rather than by the constructor.
16
+ */
17
+ var GoogleDrive = class {
18
+ client;
19
+ constructor(config) {
20
+ this.client = google.drive({
21
+ auth: config.auth,
22
+ version: "v3"
23
+ });
24
+ }
25
+ /**
26
+ * Fetches raw Google Drive metadata for a file or folder.
27
+ *
28
+ * Google Drive uses the same `files.get` endpoint for files and folders. We
29
+ * request only `GOOGLE_DRIVE_NODE_FIELDS` so DTO mapping receives a predictable
30
+ * shape and we avoid over-fetching provider metadata.
31
+ *
32
+ * A Google 404 is translated to `null` to match the public `getNodeById`
33
+ * contract. All other errors are rethrown because they represent real failures
34
+ * such as insufficient permissions, invalid credentials, quota limits, or API
35
+ * outages.
36
+ */
37
+ async #getFile(id) {
38
+ try {
39
+ return (await this.client.files.get({
40
+ fileId: id,
41
+ supportsAllDrives: true,
42
+ fields: GOOGLE_DRIVE_NODE_FIELDS
43
+ })).data;
44
+ } catch (error) {
45
+ if (isGoogleNotFound(error)) return null;
46
+ throw error;
47
+ }
48
+ }
49
+ /**
50
+ * Loads a single Google Drive file/folder by its Google file id.
51
+ *
52
+ * Returns `null` when Google reports the file does not exist or is not visible to
53
+ * the authenticated account. Other Google API errors are allowed to bubble up so
54
+ * callers do not accidentally treat permission, quota, or transport failures as
55
+ * missing data.
56
+ *
57
+ * The returned Google metadata is normalized into the shared Drive node shape by
58
+ * `#toNode`, which also validates that Google returned the minimum metadata we
59
+ * need to identify the node and its namespace.
60
+ */
61
+ async getNodeById(id) {
62
+ const file = await this.#getFile(id);
63
+ return file ? this.#toNode(file) : null;
64
+ }
65
+ /**
66
+ * Converts a table sort field into a Google Drive `orderBy` value.
67
+ *
68
+ * Google Drive only accepts a fixed set of field names in `files.list.orderBy`.
69
+ * The table layer uses provider-neutral names such as `createdAt` and
70
+ * `updatedAt`, so this method maps those aliases to Google's native field names.
71
+ *
72
+ * Unknown sort fields intentionally fall back to `modifiedTime desc` instead of
73
+ * being passed through, because Google returns a 400 for unsupported order fields.
74
+ */
75
+ #orderBy(input) {
76
+ const direction = input.order === "asc" ? "" : " desc";
77
+ switch (input.sort) {
78
+ case "name": return `name${direction}`;
79
+ case "createdAt":
80
+ case "createdTime": return `createdTime${direction}`;
81
+ case "updatedAt":
82
+ case "modifiedTime": return `modifiedTime${direction}`;
83
+ default: return "modifiedTime desc";
84
+ }
85
+ }
86
+ /**
87
+ * Scopes results to a parent folder.
88
+ *
89
+ * Google Drive represents folder membership as `'<folderId>' in parents`.
90
+ * A `null` parent maps to the provider namespace/root drive id.
91
+ * An `undefined` parent means no parent filter should be applied.
92
+ */
93
+ #parentQuery(input) {
94
+ if (input.parentId === void 0) return null;
95
+ return `'${escapeGoogleDriveQueryValue(input.parentId ?? input.namespace)}' in parents`;
96
+ }
97
+ /**
98
+ * Filters results to a specific set of Google file ids.
99
+ */
100
+ #idQuery(nodeIds) {
101
+ if (!nodeIds?.length) return null;
102
+ return `(${nodeIds.map((id) => `id = '${escapeGoogleDriveQueryValue(id)}'`).join(" or ")})`;
103
+ }
104
+ /**
105
+ * Converts provider-neutral node types to Google Drive MIME-type filters.
106
+ *
107
+ * Google Drive folders are identified by their folder MIME type. Every other
108
+ * MIME type is treated as a file.
109
+ */
110
+ #typeQuery(types) {
111
+ if (types?.length !== 1) return null;
112
+ return types[0] === "folder" ? `mimeType = '${GOOGLE_DRIVE_FOLDER_MIME_TYPE}'` : `mimeType != '${GOOGLE_DRIVE_FOLDER_MIME_TYPE}'`;
113
+ }
114
+ /**
115
+ * Filters files by MIME type.
116
+ *
117
+ * This only applies to files. Folder filtering should use `#typeQuery`.
118
+ */
119
+ #contentTypeQuery(contentTypes) {
120
+ if (!contentTypes?.length) return null;
121
+ return `(${contentTypes.map((contentType) => `mimeType = '${escapeGoogleDriveQueryValue(contentType)}'`).join(" or ")})`;
122
+ }
123
+ /**
124
+ * Filters archived/active nodes.
125
+ *
126
+ * Google Drive calls this state `trashed`; the shared Drive contract calls it
127
+ * archived/soft-deleted. By default we return active nodes only.
128
+ */
129
+ #archiveQuery(isArchived) {
130
+ return `trashed = ${isArchived === true ? "true" : "false"}`;
131
+ }
132
+ /**
133
+ * Applies a name search.
134
+ *
135
+ * Google Drive supports `name contains '<value>'`, which is a substring match.
136
+ */
137
+ #searchQuery(search) {
138
+ if (!search) return null;
139
+ return `name contains '${escapeGoogleDriveQueryValue(search)}'`;
140
+ }
141
+ #buildQuery(input) {
142
+ return [
143
+ this.#parentQuery(input),
144
+ this.#idQuery(input.nodeIds),
145
+ this.#typeQuery(input.types),
146
+ this.#contentTypeQuery(input.contentTypes),
147
+ this.#archiveQuery(input.isArchived),
148
+ this.#searchQuery(input.search)
149
+ ].filter((clause) => typeof clause === "string" && clause.length > 0).join(" and ");
150
+ }
151
+ /**
152
+ * Lists the direct children of a Google Drive folder.
153
+ *
154
+ * Google Drive stores both files and folders behind the `files.list` endpoint.
155
+ * This helper translates our drive query input into a Google Drive search query
156
+ * and returns only the immediate children for the requested parent.
157
+ *
158
+ * The result is paginated by Google, so we keep requesting pages until all
159
+ * matching children are loaded. Recursive traversal is intentionally not handled
160
+ * here; callers that need full subtrees should use `getNodeChildren`.
161
+ */
162
+ async #listChildren(input) {
163
+ const filters = input?.filters;
164
+ const files = [];
165
+ let pageToken;
166
+ do {
167
+ const result = await this.client.files.list({
168
+ pageToken,
169
+ pageSize: CHILDREN_PAGE_SIZE,
170
+ corpora: "drive",
171
+ driveId: input.filters.namespace,
172
+ includeItemsFromAllDrives: true,
173
+ supportsAllDrives: true,
174
+ q: this.#buildQuery({
175
+ namespace: input.filters.namespace,
176
+ parentId: input.filters.parentId,
177
+ nodeIds: filters?.nodeIds,
178
+ types: filters?.types,
179
+ contentTypes: filters?.contentTypes,
180
+ isArchived: filters?.isArchived,
181
+ search: input?.search
182
+ }),
183
+ orderBy: this.#orderBy(input),
184
+ fields: `nextPageToken, files(${GOOGLE_DRIVE_NODE_FIELDS})`
185
+ });
186
+ files.push(...result.data.files ?? []);
187
+ pageToken = result.data.nextPageToken ?? void 0;
188
+ } while (pageToken);
189
+ return files.map((file) => this.#toNode(file));
190
+ }
191
+ /**
192
+ * Loads a node and its direct children within a namespace.
193
+ *
194
+ * Google Drive does not enforce our provider-neutral `namespace` at `files.get`
195
+ * time, so the node is fetched by id first and then checked against the requested
196
+ * namespace/shared drive. A missing node or namespace mismatch returns
197
+ * `null`, matching the shared Drive reader contract.
198
+ *
199
+ * Only direct children are loaded. Recursive traversal is handled separately by
200
+ * `getNodeChildren`.
201
+ */
202
+ async getNodeWithChildren(input) {
203
+ const node = await this.getNodeById(input.id);
204
+ if (!node || node.namespace !== input.namespace) return null;
205
+ const children = await this.#listChildren({ filters: {
206
+ parentId: node.id,
207
+ namespace: node.namespace,
208
+ isArchived: false
209
+ } });
210
+ return {
211
+ ...node,
212
+ children
213
+ };
214
+ }
215
+ /**
216
+ * Lists the direct children of a parent folder.
217
+ *
218
+ * This is the Google Drive implementation of the shared Drive reader method. It
219
+ * delegates to `#listChildren`, which translates the provider-neutral filters,
220
+ * search, and sorting options into a Google Drive `files.list` query.
221
+ *
222
+ * Only immediate children are returned. Recursive traversal is handled by
223
+ * `getNodeChildren`.
224
+ */
225
+ async getNodesByParentId(input) {
226
+ return this.#listChildren(input);
227
+ }
228
+ /**
229
+ * Resolves the Google Drive page token for a numeric table cursor.
230
+ *
231
+ * The shared table API uses numeric cursors (`0`, `1`, `2`, ...), while Google
232
+ * Drive pagination uses opaque `nextPageToken` values. To bridge that mismatch,
233
+ * we walk Google pages until we reach the requested table page.
234
+ *
235
+ * The intermediate requests only fetch file ids because their content is thrown
236
+ * away; we only need each response's `nextPageToken`.
237
+ *
238
+ * Returns `exhausted: true` when Google runs out of pages before the requested
239
+ * cursor is reached.
240
+ */
241
+ async #pageCursorAt(input, remainingPages, pageToken) {
242
+ if (remainingPages === 0) return {
243
+ pageToken,
244
+ exhausted: false
245
+ };
246
+ const nextPageToken = (await this.client.files.list({
247
+ pageSize: input.limit,
248
+ pageToken,
249
+ corpora: "drive",
250
+ driveId: input.namespace,
251
+ includeItemsFromAllDrives: true,
252
+ supportsAllDrives: true,
253
+ q: input.q,
254
+ orderBy: input.orderBy,
255
+ fields: "nextPageToken, files(id)"
256
+ })).data.nextPageToken ?? void 0;
257
+ if (!nextPageToken) return { exhausted: true };
258
+ return this.#pageCursorAt(input, remainingPages - 1, nextPageToken);
259
+ }
260
+ /**
261
+ * Lists Google Drive nodes using the shared table pagination shape.
262
+ *
263
+ * The shared table API uses numeric cursors, but Google Drive uses opaque page
264
+ * tokens. Before fetching the requested page, `#pageCursorAt` advances through
265
+ * Google pages until it resolves the page token for the requested numeric cursor.
266
+ *
267
+ * Google Drive does not return a total row count for `files.list`, so pagination
268
+ * totals are estimated from the current cursor, returned page size, and whether
269
+ * Google reports another page. The estimate is only meant to support table/infinite
270
+ * pagination controls; it should not be treated as an exact item count.
271
+ */
272
+ async #listNodes(input) {
273
+ const limit = input.limit ?? DEFAULT_PAGE_SIZE;
274
+ const cursor = input.cursor ?? 0;
275
+ const pageCursor = await this.#pageCursorAt({
276
+ ...input,
277
+ limit
278
+ }, cursor);
279
+ if (pageCursor.exhausted) return {
280
+ data: [],
281
+ pagination: {
282
+ total: cursor * limit,
283
+ totalPages: Math.max(1, cursor),
284
+ limit,
285
+ hasNextPage: false,
286
+ hasPreviousPage: cursor > 0,
287
+ cursor,
288
+ nextCursor: null,
289
+ previousCursor: cursor > 0 ? cursor - 1 : null
290
+ }
291
+ };
292
+ const result = await this.client.files.list({
293
+ pageSize: limit,
294
+ pageToken: pageCursor.pageToken,
295
+ corpora: "drive",
296
+ driveId: input.namespace,
297
+ includeItemsFromAllDrives: true,
298
+ supportsAllDrives: true,
299
+ q: input.q,
300
+ orderBy: input.orderBy,
301
+ fields: `nextPageToken, files(${GOOGLE_DRIVE_NODE_FIELDS})`
302
+ });
303
+ const data = result.data.files?.map((file) => this.#toNode(file)) ?? [];
304
+ const hasNextPage = result.data.nextPageToken != null;
305
+ const total = cursor * limit + data.length + (hasNextPage ? limit : 0);
306
+ return {
307
+ data,
308
+ pagination: {
309
+ total,
310
+ totalPages: Math.max(1, Math.ceil(total / limit)),
311
+ limit,
312
+ hasNextPage,
313
+ hasPreviousPage: cursor > 0,
314
+ cursor,
315
+ nextCursor: hasNextPage ? cursor + 1 : null,
316
+ previousCursor: cursor > 0 ? cursor - 1 : null
317
+ }
318
+ };
319
+ }
320
+ /**
321
+ * Lists one page of Google Drive nodes for a tree/folder view.
322
+ *
323
+ * The public input uses the shared table-query shape: pagination, sorting,
324
+ * search, and drive filters. This method translates that input into the internal
325
+ * Google Drive list request shape used by `#listNodes`.
326
+ *
327
+ * Google Drive does not expose a native tree endpoint. This returns only the
328
+ * direct children for the requested `parentId`; recursive traversal is handled
329
+ * by `getNodeChildren`.
330
+ */
331
+ async listTree(input) {
332
+ return this.#listNodes({
333
+ namespace: input.filters.namespace,
334
+ limit: input.limit,
335
+ cursor: input.cursor,
336
+ orderBy: this.#orderBy(input),
337
+ q: this.#buildQuery({
338
+ namespace: input.filters.namespace,
339
+ parentId: input.filters.parentId,
340
+ nodeIds: input.filters.nodeIds,
341
+ types: input.filters.types,
342
+ contentTypes: input.filters.contentTypes,
343
+ isArchived: input.filters.isArchived,
344
+ search: input.search
345
+ })
346
+ });
347
+ }
348
+ /**
349
+ * Walks from a Google Drive node up through its parent chain.
350
+ *
351
+ * Google Drive only exposes direct parents, so breadcrumbs must be resolved one
352
+ * node at a time. The `seen` set prevents accidental infinite loops if Google
353
+ * ever returns cyclic or inconsistent parent metadata.
354
+ *
355
+ * `seen` is treated immutably so each recursive call receives its own traversal
356
+ * state instead of mutating caller-owned state.
357
+ */
358
+ async #walkParents(currentId, namespace, depth = 0, seen = /* @__PURE__ */ new Set()) {
359
+ if (!currentId || seen.has(currentId)) return [];
360
+ const node = await this.getNodeById(currentId);
361
+ if (!node || node.namespace !== namespace) return [];
362
+ const nextSeen = new Set([...seen, node.id]);
363
+ return [{
364
+ id: node.id,
365
+ name: node.name,
366
+ parentId: node.parentId,
367
+ depth
368
+ }, ...await this.#walkParents(node.parentId, namespace, depth + 1, nextSeen)];
369
+ }
370
+ /**
371
+ * Resolves the breadcrumb parent chain for a Google Drive node.
372
+ *
373
+ * A `null` id represents the namespace root and therefore has no parent chain.
374
+ * The returned list is ordered from the root-most ancestor to the requested
375
+ * node, matching breadcrumb display order.
376
+ */
377
+ async getFolderParents(input) {
378
+ if (!input.id) return [];
379
+ return (await this.#walkParents(input.id, input.namespace)).toSorted((a, b) => Number(b.depth) - Number(a.depth));
380
+ }
381
+ /**
382
+ * Walks a Google Drive subtree breadth-first.
383
+ *
384
+ * Google Drive has no recursive children endpoint, so folder descendants must be
385
+ * loaded one folder at a time. The returned rows include the starting nodes and
386
+ * all descendants with their relative depth.
387
+ */
388
+ async #walkChildren(queue, seen = /* @__PURE__ */ new Set()) {
389
+ const [current, ...rest] = queue;
390
+ if (!current) return [];
391
+ if (seen.has(current.id)) return this.#walkChildren(rest, seen);
392
+ const node = await this.getNodeById(current.id);
393
+ if (!node) return this.#walkChildren(rest, seen);
394
+ const nextSeen = new Set([...seen, node.id]);
395
+ const currentChild = {
396
+ id: node.id,
397
+ type: node.type,
398
+ parentId: node.parentId,
399
+ depth: current.depth
400
+ };
401
+ if (node.type !== "folder") return [currentChild, ...await this.#walkChildren(rest, nextSeen)];
402
+ const children = await this.#listChildren({ filters: {
403
+ namespace: node.namespace,
404
+ parentId: node.id,
405
+ isArchived: false
406
+ } });
407
+ const nextQueue = [...rest, ...children.map((child) => ({
408
+ id: child.id,
409
+ depth: current.depth + 1
410
+ }))];
411
+ return [currentChild, ...await this.#walkChildren(nextQueue, nextSeen)];
412
+ }
413
+ /**
414
+ * Resolves the subtree for one or more Google Drive nodes.
415
+ */
416
+ async getNodeChildren(ids) {
417
+ const uniqueIds = Array.from(new Set(ids));
418
+ return this.#walkChildren(uniqueIds.map((id) => ({
419
+ id,
420
+ depth: 0
421
+ })));
422
+ }
423
+ /**
424
+ * Creates a Google Drive folder in the requested parent.
425
+ *
426
+ * Google Drive represents folders as files with the special folder MIME type.
427
+ * Creating a folder therefore uses the same `files.create` endpoint as file
428
+ * uploads, but without a media body.
429
+ *
430
+ * A `null` or missing `parentId` means "create in the namespace root", where the
431
+ * namespace is the shared drive id/root folder used by this provider.
432
+ */
433
+ async createFolder(input) {
434
+ const result = await this.client.files.create({
435
+ requestBody: {
436
+ name: input.name,
437
+ parents: [input.parentId ?? input.namespace],
438
+ mimeType: GOOGLE_DRIVE_FOLDER_MIME_TYPE
439
+ },
440
+ supportsAllDrives: true,
441
+ fields: GOOGLE_DRIVE_NODE_FIELDS
442
+ });
443
+ return this.#toNode(result.data);
444
+ }
445
+ /**
446
+ * Uploads a file body to Google Drive and returns the created node.
447
+ *
448
+ * Google Drive creates files through `files.create` with two parts:
449
+ * metadata in `requestBody` and the actual file content in `media.body`.
450
+ *
451
+ * A `null` or missing `parentId` means "upload to the namespace root", where the
452
+ * namespace is the Google shared drive/root id used by this provider.
453
+ *
454
+ * The input body can be a provider-neutral object body type. `toReadable`
455
+ * normalizes it into a Node.js readable stream because the Google API client
456
+ * expects stream-compatible media bodies.
457
+ */
458
+ async uploadFile(input) {
459
+ const result = await this.client.files.create({
460
+ requestBody: {
461
+ name: input.name,
462
+ parents: [input.parentId ?? input.namespace],
463
+ mimeType: input.contentType ?? void 0
464
+ },
465
+ media: {
466
+ mimeType: input.contentType ?? void 0,
467
+ body: toGoogleDriveReadable(input.body)
468
+ },
469
+ supportsAllDrives: true,
470
+ fields: GOOGLE_DRIVE_NODE_FIELDS
471
+ });
472
+ return this.#toNode(result.data);
473
+ }
474
+ /**
475
+ * Updates mutable Google Drive node metadata.
476
+ *
477
+ * Google Drive uses `files.update` for metadata changes such as renaming and
478
+ * moving a file/folder to or from the trash. The shared Drive API calls this
479
+ * soft-delete state "archived"; Google Drive exposes it as `trashed`, so
480
+ * `data.isArchived` is translated directly to Google's `trashed` field.
481
+ *
482
+ * Parent changes are intentionally not handled here. Moving a node requires
483
+ * Google Drive's `addParents`/`removeParents` API and should go through
484
+ * `moveNode` instead.
485
+ */
486
+ async updateNode(id, data) {
487
+ const result = await this.client.files.update({
488
+ fileId: id,
489
+ requestBody: {
490
+ name: data.name,
491
+ trashed: data.isArchived
492
+ },
493
+ supportsAllDrives: true,
494
+ fields: GOOGLE_DRIVE_NODE_FIELDS
495
+ });
496
+ return this.#toNode(result.data);
497
+ }
498
+ /**
499
+ * Moves a Google Drive node to another parent folder.
500
+ *
501
+ * Google Drive does not allow parent changes through the normal metadata
502
+ * `requestBody`. Moving requires `addParents` and `removeParents` on
503
+ * `files.update`, so this operation is intentionally separate from `updateNode`.
504
+ *
505
+ * A `null` parent means "move to the namespace root". For this provider, the
506
+ * namespace is the Google shared drive/root id.
507
+ */
508
+ async moveNode(input) {
509
+ const node = await this.getNodeById(input.id);
510
+ if (!node) throw new ServerError("NOT_FOUND", { message: "Node not found" });
511
+ if (node.readonly) throw new ServerError("BAD_REQUEST", { message: "Node is readonly and cannot be changed" });
512
+ if (input.parentId === input.id) throw new ServerError("BAD_REQUEST", { message: "Node cannot be moved into itself" });
513
+ if (input.parentId) {
514
+ if ((await this.getNodeChildren([input.id])).some((child) => child.id === input.parentId)) throw new ServerError("BAD_REQUEST", { message: "Node cannot be moved into one of its descendants" });
515
+ }
516
+ const result = await this.client.files.update({
517
+ fileId: input.id,
518
+ addParents: input.parentId ?? node.namespace,
519
+ removeParents: node.googleParents.join(",") || void 0,
520
+ supportsAllDrives: true,
521
+ fields: GOOGLE_DRIVE_NODE_FIELDS
522
+ });
523
+ return this.#toNode(result.data);
524
+ }
525
+ /**
526
+ * Updates the Google Drive trash state for multiple nodes.
527
+ *
528
+ * Google Drive exposes archive/restore behavior through the `trashed` metadata
529
+ * field on `files.update`. Each node requires a separate API request, so updates
530
+ * are executed in parallel after duplicate ids are removed.
531
+ */
532
+ async #setTrashed(ids, trashed) {
533
+ const uniqueIds = [...new Set(ids)];
534
+ return (await Promise.all(uniqueIds.map((id) => this.client.files.update({
535
+ fileId: id,
536
+ requestBody: { trashed },
537
+ supportsAllDrives: true,
538
+ fields: GOOGLE_DRIVE_NODE_FIELDS
539
+ })))).map((result) => this.#toNode(result.data));
540
+ }
541
+ /**
542
+ * Moves Google Drive nodes to trash.
543
+ *
544
+ * The shared Drive API calls this "archive", while Google Drive calls the same
545
+ * state `trashed`. This is a soft-delete operation; nodes can be restored with
546
+ * `restoreNodes`.
547
+ */
548
+ async archiveNodes(ids) {
549
+ return this.#setTrashed(ids, true);
550
+ }
551
+ /**
552
+ * Restores Google Drive nodes from trash.
553
+ *
554
+ * This reverses `archiveNodes` by setting Google Drive's `trashed` flag back to
555
+ * `false`.
556
+ */
557
+ async restoreNodes(ids) {
558
+ return this.#setTrashed(ids, false);
559
+ }
560
+ /**
561
+ * Permanently deletes a Google Drive node.
562
+ *
563
+ * This uses Google Drive `files.delete`, which removes the file/folder rather
564
+ * than moving it to trash. Soft deletion/trashing should go through the archive
565
+ * method instead.
566
+ */
567
+ async deleteNode(id) {
568
+ await this.client.files.delete({
569
+ fileId: id,
570
+ supportsAllDrives: true
571
+ });
572
+ }
573
+ /**
574
+ * Permanently deletes multiple Google Drive nodes.
575
+ *
576
+ * Duplicate ids are removed before issuing delete requests so the same Google
577
+ * file is not deleted more than once. Deletions are executed in parallel because
578
+ * Google Drive exposes deletion as one request per file id.
579
+ *
580
+ * This is a hard delete. Soft deletion/trashing should go through the archive
581
+ * method instead.
582
+ */
583
+ async deleteNodes(ids) {
584
+ const uniqueIds = [...new Set(ids)];
585
+ await Promise.all(uniqueIds.map((id) => this.client.files.delete({
586
+ fileId: id,
587
+ supportsAllDrives: true
588
+ })));
589
+ }
590
+ /**
591
+ * Converts raw Google Drive file metadata into the provider's normalized node DTO.
592
+ *
593
+ * Google Drive API responses are loosely typed and may omit fields that our
594
+ * drive abstraction requires, such as `id` or a namespace/root identifier. Those
595
+ * cases indicate an invalid provider response rather than a caller error, so
596
+ * they are normalized to an internal server error.
597
+ *
598
+ * Any unexpected mapping error is rethrown so it can be handled by the normal
599
+ * server error boundary/logging path.
600
+ */
601
+ #toNode(file) {
602
+ try {
603
+ return GoogleDriveNodeDTO.create({ file });
604
+ } catch (error) {
605
+ if (error instanceof Error && ["Google Drive file has no id", "Google Drive file has no namespace"].includes(error.message)) throw new ServerError("INTERNAL_SERVER_ERROR", { message: error.message });
606
+ throw error;
607
+ }
608
+ }
609
+ };
610
+
611
+ //#endregion
612
+ export { GoogleDrive };
613
+ //# sourceMappingURL=service.server.mjs.map