@tulip-systems/drive-local 0.11.1

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 (61) hide show
  1. package/LICENSE +662 -0
  2. package/dist/client.d.mts +12 -0
  3. package/dist/client.mjs +12 -0
  4. package/dist/database/drizzle/lib/adapter.d.mts +310 -0
  5. package/dist/database/drizzle/lib/adapter.mjs +520 -0
  6. package/dist/database/drizzle/lib/compilers.d.mts +395 -0
  7. package/dist/database/drizzle/lib/compilers.mjs +21 -0
  8. package/dist/database/drizzle/lib/schema.d.mts +61 -0
  9. package/dist/drizzle.d.mts +4 -0
  10. package/dist/drizzle.mjs +3 -0
  11. package/dist/index.d.mts +8 -0
  12. package/dist/index.mjs +8 -0
  13. package/dist/root/components/command-file-update.d.mts +11 -0
  14. package/dist/root/components/command-file-update.mjs +44 -0
  15. package/dist/root/components/command-file-upload.d.mts +5 -0
  16. package/dist/root/components/command-file-upload.mjs +31 -0
  17. package/dist/root/components/command-folder-create.d.mts +11 -0
  18. package/dist/root/components/command-folder-create.mjs +50 -0
  19. package/dist/root/components/command-folder-update.d.mts +11 -0
  20. package/dist/root/components/command-folder-update.mjs +44 -0
  21. package/dist/root/components/content.client.d.mts +5 -0
  22. package/dist/root/components/content.client.mjs +5 -0
  23. package/dist/root/components/navigation.client.d.mts +11 -0
  24. package/dist/root/components/navigation.client.mjs +12 -0
  25. package/dist/root/components/provider.client.d.mts +16 -0
  26. package/dist/root/components/provider.client.mjs +65 -0
  27. package/dist/root/components/upload-zone-context.client.d.mts +36 -0
  28. package/dist/root/components/upload-zone-context.client.mjs +19 -0
  29. package/dist/root/components/upload-zone.client.d.mts +16 -0
  30. package/dist/root/components/upload-zone.client.mjs +147 -0
  31. package/dist/root/components/view.client.d.mts +20 -0
  32. package/dist/root/components/view.client.mjs +86 -0
  33. package/dist/root/config/columns-data.d.mts +6 -0
  34. package/dist/root/config/columns-data.mjs +62 -0
  35. package/dist/root/config/filters.d.mts +404 -0
  36. package/dist/root/config/filters.mjs +122 -0
  37. package/dist/root/config/sorting.d.mts +38 -0
  38. package/dist/root/config/sorting.mjs +26 -0
  39. package/dist/root/lib/constants.d.mts +12 -0
  40. package/dist/root/lib/constants.mjs +27 -0
  41. package/dist/root/lib/database/contracts.d.mts +112 -0
  42. package/dist/root/lib/database/filters.d.mts +28 -0
  43. package/dist/root/lib/database/filters.mjs +73 -0
  44. package/dist/root/lib/helpers.d.mts +33 -0
  45. package/dist/root/lib/helpers.mjs +78 -0
  46. package/dist/root/lib/route-handler.server.d.mts +36 -0
  47. package/dist/root/lib/route-handler.server.mjs +111 -0
  48. package/dist/root/lib/router.server.d.mts +819 -0
  49. package/dist/root/lib/router.server.mjs +87 -0
  50. package/dist/root/lib/search-params.d.mts +15 -0
  51. package/dist/root/lib/search-params.mjs +6 -0
  52. package/dist/root/lib/service.server.d.mts +522 -0
  53. package/dist/root/lib/service.server.mjs +613 -0
  54. package/dist/root/lib/upload.client.d.mts +60 -0
  55. package/dist/root/lib/upload.client.mjs +97 -0
  56. package/dist/root/lib/validators.d.mts +710 -0
  57. package/dist/root/lib/validators.mjs +116 -0
  58. package/dist/server.d.mts +5 -0
  59. package/dist/server.mjs +4 -0
  60. package/package.json +83 -0
  61. package/styles.css +1 -0
@@ -0,0 +1,520 @@
1
+ import { localDriveFiltersParser, localDrivePresignedUrlFiltersParser, localDriveVariantFiltersParser } from "../../../root/config/filters.mjs";
2
+ import { localDriveTableSortsParser } from "../../../root/config/sorting.mjs";
3
+ import { localDriveDatabaseNodeFiltersParser } from "../../../root/lib/database/filters.mjs";
4
+ import { compileLocalDriveDatabaseNodeFiltersToDrizzle, compileLocalDriveFiltersToDrizzle, compileLocalDrivePresignedUrlFiltersToDrizzle, compileLocalDriveSortsToDrizzle, compileLocalDriveVariantFiltersToDrizzle } from "./compilers.mjs";
5
+ import { and, eq, inArray, sql } from "drizzle-orm";
6
+ //#region src/database/drizzle/lib/adapter.ts
7
+ /**
8
+ * Creates the Drizzle database adapter for the local drive package.
9
+ *
10
+ * The local drive package owns the drive behavior, but the consumer app owns the
11
+ * Drizzle schema and migrations. Pass the app's `db` instance and a schema map
12
+ * with the local drive tables under the expected contract keys.
13
+ *
14
+ * @example
15
+ * import { localDriveDrizzleAdapter } from "@tulip-systems/drive-local/drizzle";
16
+ * import { LocalDrive } from "@tulip-systems/drive-local/server";
17
+ * import * as driveSchema from "@/server/drive-local/schema";
18
+ * import * as storageSchema from "@/server/storage/schema";
19
+ *
20
+ * export const drive = new LocalDrive({
21
+ * database: localDriveDrizzleAdapter({
22
+ * db,
23
+ * schema: { ...driveSchema, storageAssets: storageSchema.storageAssets },
24
+ * }),
25
+ * storage,
26
+ * });
27
+ *
28
+ * @example
29
+ * // You can also remap app-owned table names to the local drive contract keys.
30
+ * localDriveDrizzleAdapter({
31
+ * db,
32
+ * schema: {
33
+ * nodes: driveNodes,
34
+ * nodeVariants: driveNodeVariants,
35
+ * nodePresignedUrls: driveNodePresignedUrls,
36
+ * storageAssets: assets,
37
+ * },
38
+ * });
39
+ */
40
+ function localDriveDrizzleAdapter({ db, schema }) {
41
+ const { nodePresignedUrls, nodeVariants, nodes, storageAssets } = schema;
42
+ function parseNodeSorts(sorts) {
43
+ const parsedSorts = localDriveTableSortsParser.parse(sorts);
44
+ return compileLocalDriveSortsToDrizzle(nodes).parse(parsedSorts);
45
+ }
46
+ function parseNodeFilters(filters) {
47
+ const parsedFilters = localDriveFiltersParser.parse(filters);
48
+ return compileLocalDriveFiltersToDrizzle(nodes).parse(parsedFilters);
49
+ }
50
+ function parseDatabaseNodeFilters(filters) {
51
+ const parsedFilters = localDriveDatabaseNodeFiltersParser.parse(filters);
52
+ return compileLocalDriveDatabaseNodeFiltersToDrizzle(nodes).parse(parsedFilters);
53
+ }
54
+ function parseVariantFilters(filters) {
55
+ const parsedFilters = localDriveVariantFiltersParser.parse(filters);
56
+ return compileLocalDriveVariantFiltersToDrizzle(nodeVariants).parse(parsedFilters);
57
+ }
58
+ function parsePresignedUrlFilters(filters) {
59
+ const parsedFilters = localDrivePresignedUrlFiltersParser.parse(filters);
60
+ return compileLocalDrivePresignedUrlFiltersToDrizzle(nodePresignedUrls).parse(parsedFilters);
61
+ }
62
+ return {
63
+ db,
64
+ schema,
65
+ /**
66
+ * Finds a local drive node by its primary id.
67
+ *
68
+ * @example
69
+ * const node = await database.findNodeById("019d...");
70
+ */
71
+ findNodeById: async (id) => {
72
+ const [node] = await db.select({
73
+ id: nodes.id,
74
+ createdAt: nodes.createdAt,
75
+ updatedAt: nodes.updatedAt,
76
+ name: nodes.name,
77
+ namespace: nodes.namespace,
78
+ type: nodes.type,
79
+ subtype: nodes.subtype,
80
+ size: nodes.size,
81
+ contentType: nodes.contentType,
82
+ readonly: nodes.readonly,
83
+ hidden: nodes.hidden,
84
+ archivedAt: nodes.archivedAt,
85
+ parentId: nodes.parentId,
86
+ assetId: nodes.assetId
87
+ }).from(nodes).where(eq(nodes.id, id)).limit(1);
88
+ return node ?? null;
89
+ },
90
+ /**
91
+ * Finds the first local drive node matching the provided database filters.
92
+ *
93
+ * @example
94
+ * const node = await database.findFirstNode({ filters: { assetIds: [asset.id] } });
95
+ */
96
+ findFirstNode: async (input) => {
97
+ let query = db.select({
98
+ id: nodes.id,
99
+ createdAt: nodes.createdAt,
100
+ updatedAt: nodes.updatedAt,
101
+ name: nodes.name,
102
+ namespace: nodes.namespace,
103
+ type: nodes.type,
104
+ subtype: nodes.subtype,
105
+ size: nodes.size,
106
+ contentType: nodes.contentType,
107
+ readonly: nodes.readonly,
108
+ hidden: nodes.hidden,
109
+ archivedAt: nodes.archivedAt,
110
+ parentId: nodes.parentId,
111
+ assetId: nodes.assetId
112
+ }).from(nodes).$dynamic();
113
+ if (input?.filters) {
114
+ const where = parseDatabaseNodeFilters(input.filters);
115
+ query = query.where(where);
116
+ }
117
+ const [node] = await query.limit(1);
118
+ return node ?? null;
119
+ },
120
+ /**
121
+ * Finds local drive nodes matching the provided filters, sorts, and pagination limits.
122
+ *
123
+ * @example
124
+ * const nodes = await database.findNodes({
125
+ * filters: { namespace: "products", parentId: null },
126
+ * sorts: [{ key: "name", direction: "asc" }],
127
+ * });
128
+ */
129
+ findNodes: async (input = {}) => {
130
+ let query = db.select({
131
+ id: nodes.id,
132
+ createdAt: nodes.createdAt,
133
+ updatedAt: nodes.updatedAt,
134
+ name: nodes.name,
135
+ namespace: nodes.namespace,
136
+ type: nodes.type,
137
+ subtype: nodes.subtype,
138
+ size: nodes.size,
139
+ contentType: nodes.contentType,
140
+ readonly: nodes.readonly,
141
+ hidden: nodes.hidden,
142
+ archivedAt: nodes.archivedAt,
143
+ parentId: nodes.parentId,
144
+ assetId: nodes.assetId
145
+ }).from(nodes).$dynamic();
146
+ if (input.filters) {
147
+ const where = parseDatabaseNodeFilters(input.filters);
148
+ query = query.where(where);
149
+ }
150
+ if (input.sorts) {
151
+ const orderBy = parseNodeSorts(input.sorts);
152
+ query = query.orderBy(...orderBy);
153
+ }
154
+ if (input.limit !== void 0) query = query.limit(input.limit);
155
+ if (input.offset !== void 0) query = query.offset(input.offset);
156
+ return await query;
157
+ },
158
+ /**
159
+ * Lists paginated nodes with their linked storage asset.
160
+ *
161
+ * @example
162
+ * const result = await database.listNodesWithAsset({
163
+ * limit: 20,
164
+ * offset: 0,
165
+ * filters: { namespace: "products", parentId: null },
166
+ * sorts: [],
167
+ * });
168
+ */
169
+ listNodesWithAsset: async ({ filters, limit, offset, sorts }) => {
170
+ const [data, total] = await db.transaction(async (tx) => {
171
+ const where = parseNodeFilters(filters);
172
+ let query = tx.select({
173
+ id: nodes.id,
174
+ createdAt: nodes.createdAt,
175
+ updatedAt: nodes.updatedAt,
176
+ name: nodes.name,
177
+ namespace: nodes.namespace,
178
+ type: nodes.type,
179
+ subtype: nodes.subtype,
180
+ size: nodes.size,
181
+ contentType: nodes.contentType,
182
+ readonly: nodes.readonly,
183
+ hidden: nodes.hidden,
184
+ archivedAt: nodes.archivedAt,
185
+ parentId: nodes.parentId,
186
+ assetId: nodes.assetId,
187
+ asset: {
188
+ id: storageAssets.id,
189
+ createdAt: storageAssets.createdAt,
190
+ updatedAt: storageAssets.updatedAt,
191
+ provider: storageAssets.provider,
192
+ bucket: storageAssets.bucket,
193
+ key: storageAssets.key,
194
+ status: storageAssets.status,
195
+ visibility: storageAssets.visibility,
196
+ size: storageAssets.size,
197
+ contentType: storageAssets.contentType,
198
+ uploadId: storageAssets.uploadId,
199
+ name: storageAssets.name,
200
+ metadata: storageAssets.metadata,
201
+ etag: storageAssets.etag,
202
+ uploadedAt: storageAssets.uploadedAt,
203
+ deletedAt: storageAssets.deletedAt
204
+ }
205
+ }).from(nodes).leftJoin(storageAssets, eq(nodes.assetId, storageAssets.id)).$dynamic();
206
+ if (where) query = query.where(where);
207
+ if (sorts && sorts.length > 0) {
208
+ const orderBy = parseNodeSorts(sorts);
209
+ query = query.orderBy(...orderBy);
210
+ }
211
+ return [await query.limit(limit).offset(offset), await tx.$count(nodes, where)];
212
+ });
213
+ return {
214
+ data,
215
+ total
216
+ };
217
+ },
218
+ /**
219
+ * Resolves the ancestor chain for a folder or file within a namespace.
220
+ *
221
+ * @example
222
+ * const parents = await database.getNodeParents({ id: node.id, namespace: "products" });
223
+ */
224
+ getNodeParents: async (input) => {
225
+ if (!input.id) return [];
226
+ const startNodeQuery = db.select({
227
+ id: nodes.id,
228
+ name: nodes.name,
229
+ parentId: nodes.parentId,
230
+ depth: sql`0`.as("depth")
231
+ }).from(nodes).where(and(eq(nodes.id, input.id), eq(nodes.namespace, input.namespace)));
232
+ const alias = "parent_nodes";
233
+ const parentNodesQueryAlias = startNodeQuery.as(alias);
234
+ const recursiveQueryName = sql.raw(`"${alias}"`);
235
+ const recursiveQuery = startNodeQuery.unionAll(db.select({
236
+ id: nodes.id,
237
+ name: nodes.name,
238
+ parentId: nodes.parentId,
239
+ depth: sql`${parentNodesQueryAlias.depth} + 1`
240
+ }).from(nodes).innerJoin(recursiveQueryName, eq(nodes.id, parentNodesQueryAlias.parentId)));
241
+ return (await db.execute(sql`WITH RECURSIVE ${recursiveQueryName} AS ${recursiveQuery}
242
+ SELECT
243
+ ${parentNodesQueryAlias.id} as id,
244
+ ${parentNodesQueryAlias.name} as name,
245
+ ${parentNodesQueryAlias.parentId} as "parentId",
246
+ ${parentNodesQueryAlias.depth}::int as depth
247
+ FROM ${recursiveQueryName}`)).rows ?? [];
248
+ },
249
+ /**
250
+ * Resolves all recursive descendants for one or more nodes.
251
+ *
252
+ * @example
253
+ * const descendants = await database.getNodeChildren([folder.id]);
254
+ */
255
+ getNodeChildren: async (ids) => {
256
+ if (ids.length === 0) return [];
257
+ const startNodeQuery = db.select({
258
+ id: nodes.id,
259
+ createdAt: nodes.createdAt,
260
+ updatedAt: nodes.updatedAt,
261
+ name: nodes.name,
262
+ namespace: nodes.namespace,
263
+ type: nodes.type,
264
+ subtype: nodes.subtype,
265
+ size: nodes.size,
266
+ contentType: nodes.contentType,
267
+ readonly: nodes.readonly,
268
+ hidden: nodes.hidden,
269
+ archivedAt: nodes.archivedAt,
270
+ parentId: nodes.parentId,
271
+ assetId: nodes.assetId,
272
+ depth: sql`0`.as("depth")
273
+ }).from(nodes).where(inArray(nodes.id, ids));
274
+ const alias = "child_nodes";
275
+ const childNodesQueryAlias = startNodeQuery.as(alias);
276
+ const recursiveQueryName = sql.raw(`"${alias}"`);
277
+ const recursiveQuery = startNodeQuery.unionAll(db.select({
278
+ id: nodes.id,
279
+ createdAt: nodes.createdAt,
280
+ updatedAt: nodes.updatedAt,
281
+ name: nodes.name,
282
+ namespace: nodes.namespace,
283
+ type: nodes.type,
284
+ subtype: nodes.subtype,
285
+ size: nodes.size,
286
+ contentType: nodes.contentType,
287
+ readonly: nodes.readonly,
288
+ hidden: nodes.hidden,
289
+ archivedAt: nodes.archivedAt,
290
+ parentId: nodes.parentId,
291
+ assetId: nodes.assetId,
292
+ depth: sql`${childNodesQueryAlias.depth} + 1`
293
+ }).from(nodes).innerJoin(recursiveQueryName, eq(nodes.parentId, childNodesQueryAlias.id)));
294
+ return (await db.execute(sql`WITH RECURSIVE ${recursiveQueryName} AS ${recursiveQuery}
295
+ SELECT
296
+ ${childNodesQueryAlias.id} as id,
297
+ ${childNodesQueryAlias.createdAt} as "createdAt",
298
+ ${childNodesQueryAlias.updatedAt} as "updatedAt",
299
+ ${childNodesQueryAlias.name} as name,
300
+ ${childNodesQueryAlias.namespace} as namespace,
301
+ ${childNodesQueryAlias.type} as type,
302
+ ${childNodesQueryAlias.subtype} as subtype,
303
+ ${childNodesQueryAlias.size} as size,
304
+ ${childNodesQueryAlias.contentType} as "contentType",
305
+ ${childNodesQueryAlias.readonly} as readonly,
306
+ ${childNodesQueryAlias.hidden} as hidden,
307
+ ${childNodesQueryAlias.archivedAt} as "archivedAt",
308
+ ${childNodesQueryAlias.parentId} as "parentId",
309
+ ${childNodesQueryAlias.assetId} as "assetId",
310
+ MIN(${childNodesQueryAlias.depth})::int as depth
311
+ FROM ${recursiveQueryName}
312
+ GROUP BY
313
+ ${childNodesQueryAlias.id},
314
+ ${childNodesQueryAlias.createdAt},
315
+ ${childNodesQueryAlias.updatedAt},
316
+ ${childNodesQueryAlias.name},
317
+ ${childNodesQueryAlias.namespace},
318
+ ${childNodesQueryAlias.type},
319
+ ${childNodesQueryAlias.subtype},
320
+ ${childNodesQueryAlias.size},
321
+ ${childNodesQueryAlias.contentType},
322
+ ${childNodesQueryAlias.readonly},
323
+ ${childNodesQueryAlias.hidden},
324
+ ${childNodesQueryAlias.archivedAt},
325
+ ${childNodesQueryAlias.parentId},
326
+ ${childNodesQueryAlias.assetId}
327
+ `)).rows ?? [];
328
+ },
329
+ /**
330
+ * Creates a node row.
331
+ *
332
+ * @example
333
+ * const node = await database.createNode({
334
+ * type: "file",
335
+ * name: "manual.pdf",
336
+ * namespace: "products",
337
+ * subtype: "document",
338
+ * assetId: asset.id,
339
+ * });
340
+ */
341
+ createNode: async (input) => {
342
+ const [node] = await db.insert(nodes).values(input).returning({
343
+ id: nodes.id,
344
+ createdAt: nodes.createdAt,
345
+ updatedAt: nodes.updatedAt,
346
+ name: nodes.name,
347
+ namespace: nodes.namespace,
348
+ type: nodes.type,
349
+ subtype: nodes.subtype,
350
+ size: nodes.size,
351
+ contentType: nodes.contentType,
352
+ readonly: nodes.readonly,
353
+ hidden: nodes.hidden,
354
+ archivedAt: nodes.archivedAt,
355
+ parentId: nodes.parentId,
356
+ assetId: nodes.assetId
357
+ });
358
+ return node ?? null;
359
+ },
360
+ /**
361
+ * Updates a node row by primary id.
362
+ *
363
+ * @example
364
+ * const updated = await database.updateNodeById(node.id, { name: "Updated name" });
365
+ */
366
+ updateNodeById: async (id, data) => {
367
+ const [node] = await db.update(nodes).set(data).where(eq(nodes.id, id)).returning({
368
+ id: nodes.id,
369
+ createdAt: nodes.createdAt,
370
+ updatedAt: nodes.updatedAt,
371
+ name: nodes.name,
372
+ namespace: nodes.namespace,
373
+ type: nodes.type,
374
+ subtype: nodes.subtype,
375
+ size: nodes.size,
376
+ contentType: nodes.contentType,
377
+ readonly: nodes.readonly,
378
+ hidden: nodes.hidden,
379
+ archivedAt: nodes.archivedAt,
380
+ parentId: nodes.parentId,
381
+ assetId: nodes.assetId
382
+ });
383
+ return node ?? null;
384
+ },
385
+ /**
386
+ * Updates nodes matching the provided filters.
387
+ *
388
+ * @example
389
+ * const [updated] = await database.updateNodes({
390
+ * filters: { nodeIds: [node.id] },
391
+ * data: { name: "Updated name" },
392
+ * });
393
+ */
394
+ updateNodes: async ({ data, filters }) => {
395
+ const where = parseDatabaseNodeFilters(filters);
396
+ if (!where) return [];
397
+ return await db.update(nodes).set(data).where(where).returning({
398
+ id: nodes.id,
399
+ createdAt: nodes.createdAt,
400
+ updatedAt: nodes.updatedAt,
401
+ name: nodes.name,
402
+ namespace: nodes.namespace,
403
+ type: nodes.type,
404
+ subtype: nodes.subtype,
405
+ size: nodes.size,
406
+ contentType: nodes.contentType,
407
+ readonly: nodes.readonly,
408
+ hidden: nodes.hidden,
409
+ archivedAt: nodes.archivedAt,
410
+ parentId: nodes.parentId,
411
+ assetId: nodes.assetId
412
+ });
413
+ },
414
+ /**
415
+ * Deletes a node by primary id and relies on database cascades for dependent rows.
416
+ *
417
+ * @example
418
+ * await database.deleteNodeById(node.id);
419
+ */
420
+ deleteNodeById: async (id) => {
421
+ await db.delete(nodes).where(eq(nodes.id, id));
422
+ },
423
+ /**
424
+ * Deletes nodes matching the provided filters.
425
+ *
426
+ * @example
427
+ * await database.deleteNodes({ filters: { nodeIds: [node.id] } });
428
+ */
429
+ deleteNodes: async ({ filters }) => {
430
+ const where = parseDatabaseNodeFilters(filters);
431
+ if (!where) return;
432
+ await db.delete(nodes).where(where);
433
+ },
434
+ /**
435
+ * Finds generated variants matching the provided filters.
436
+ *
437
+ * @example
438
+ * const variants = await database.findVariants({ filters: { nodeIds: [node.id] } });
439
+ */
440
+ findVariants: async (input = {}) => {
441
+ let query = db.select({
442
+ id: nodeVariants.id,
443
+ createdAt: nodeVariants.createdAt,
444
+ updatedAt: nodeVariants.updatedAt,
445
+ nodeId: nodeVariants.nodeId,
446
+ assetId: nodeVariants.assetId,
447
+ variant: nodeVariants.variant,
448
+ width: nodeVariants.width
449
+ }).from(nodeVariants).$dynamic();
450
+ if (input.filters) {
451
+ const where = parseVariantFilters(input.filters);
452
+ if (where) query = query.where(where);
453
+ }
454
+ if (input.limit !== void 0) query = query.limit(input.limit);
455
+ if (input.offset !== void 0) query = query.offset(input.offset);
456
+ return await query;
457
+ },
458
+ /**
459
+ * Records a generated image variant and its storage asset.
460
+ *
461
+ * @example
462
+ * await database.createVariant({
463
+ * nodeId: node.id,
464
+ * assetId: previewAsset.id,
465
+ * variant: "preview-640",
466
+ * width: 640,
467
+ * });
468
+ */
469
+ createVariant: async (input) => {
470
+ await db.insert(nodeVariants).values(input);
471
+ },
472
+ /**
473
+ * Finds the first cached presigned URL matching the provided filters.
474
+ *
475
+ * @example
476
+ * const cached = await database.findFirstPresignedUrl({
477
+ * filters: { nodeId: node.id, variant: "main", disposition: "inline" },
478
+ * });
479
+ */
480
+ findFirstPresignedUrl: async (input) => {
481
+ let query = db.select({
482
+ url: nodePresignedUrls.url,
483
+ expiresAt: nodePresignedUrls.expiresAt
484
+ }).from(nodePresignedUrls).$dynamic();
485
+ if (input?.filters) {
486
+ const where = parsePresignedUrlFilters(input.filters);
487
+ if (where) query = query.where(where);
488
+ }
489
+ const [presignedUrl] = await query.limit(1);
490
+ return presignedUrl ?? null;
491
+ },
492
+ /**
493
+ * Creates or refreshes the cached presigned URL for a node variant.
494
+ *
495
+ * @example
496
+ * await database.upsertPresignedUrl({
497
+ * nodeId: node.id,
498
+ * variant: "main",
499
+ * disposition: "attachment",
500
+ * url,
501
+ * expiresAt,
502
+ * });
503
+ */
504
+ upsertPresignedUrl: async (input) => {
505
+ await db.insert(nodePresignedUrls).values(input).onConflictDoUpdate({
506
+ target: [
507
+ nodePresignedUrls.nodeId,
508
+ nodePresignedUrls.variant,
509
+ nodePresignedUrls.disposition
510
+ ],
511
+ set: {
512
+ url: input.url,
513
+ expiresAt: input.expiresAt
514
+ }
515
+ });
516
+ }
517
+ };
518
+ }
519
+ //#endregion
520
+ export { localDriveDrizzleAdapter };