@tulip-systems/drive 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +662 -0
  2. package/package.json +113 -0
  3. package/src/components/content.tsx +13 -0
  4. package/src/components/context.client.tsx +12 -0
  5. package/src/components/dnd.client.tsx +47 -0
  6. package/src/components/grid-card.client.tsx +252 -0
  7. package/src/components/grid.client.tsx +96 -0
  8. package/src/components/navigation/breadcrumbs.client.tsx +125 -0
  9. package/src/components/navigation/header.client.tsx +45 -0
  10. package/src/components/navigation/toolbar.client.tsx +35 -0
  11. package/src/components/navigation/view-switcher.client.tsx +32 -0
  12. package/src/components/selection.client.tsx +48 -0
  13. package/src/components/view.client.tsx +67 -0
  14. package/src/config/filters.ts +14 -0
  15. package/src/config/types.tsx +90 -0
  16. package/src/entry.client.ts +7 -0
  17. package/src/entry.server.ts +4 -0
  18. package/src/entry.ts +10 -0
  19. package/src/lib/constants.ts +19 -0
  20. package/src/lib/contracts.ts +121 -0
  21. package/src/lib/dto.ts +83 -0
  22. package/src/lib/helpers.server.ts +14 -0
  23. package/src/lib/helpers.ts +32 -0
  24. package/src/lib/search-params.ts +5 -0
  25. package/src/lib/validators.ts +89 -0
  26. package/src/providers/google/components/command-file-update.tsx +100 -0
  27. package/src/providers/google/components/command-folder-create.tsx +104 -0
  28. package/src/providers/google/components/command-folder-update.tsx +100 -0
  29. package/src/providers/google/components/content.client.tsx +6 -0
  30. package/src/providers/google/components/navigation.client.tsx +21 -0
  31. package/src/providers/google/components/provider.client.tsx +60 -0
  32. package/src/providers/google/components/view.client.tsx +158 -0
  33. package/src/providers/google/config/columns-data.tsx +81 -0
  34. package/src/providers/google/config/filters.ts +3 -0
  35. package/src/providers/google/entry.client.ts +10 -0
  36. package/src/providers/google/entry.server.ts +5 -0
  37. package/src/providers/google/entry.ts +12 -0
  38. package/src/providers/google/lib/constants.ts +10 -0
  39. package/src/providers/google/lib/dto.ts +104 -0
  40. package/src/providers/google/lib/helpers.ts +37 -0
  41. package/src/providers/google/lib/router.server.ts +62 -0
  42. package/src/providers/google/lib/schema.ts +9 -0
  43. package/src/providers/google/lib/search-params.ts +7 -0
  44. package/src/providers/google/lib/service.server.ts +792 -0
  45. package/src/providers/google/lib/validators.ts +148 -0
  46. package/src/providers/local/components/command-file-update.tsx +93 -0
  47. package/src/providers/local/components/command-file-upload.tsx +29 -0
  48. package/src/providers/local/components/command-folder-create.tsx +100 -0
  49. package/src/providers/local/components/command-folder-update.tsx +93 -0
  50. package/src/providers/local/components/content.client.tsx +3 -0
  51. package/src/providers/local/components/navigation.client.tsx +23 -0
  52. package/src/providers/local/components/provider.client.tsx +90 -0
  53. package/src/providers/local/components/upload-zone-context.client.tsx +43 -0
  54. package/src/providers/local/components/upload-zone.client.tsx +182 -0
  55. package/src/providers/local/components/view.client.tsx +145 -0
  56. package/src/providers/local/config/columns-data.tsx +81 -0
  57. package/src/providers/local/config/filters.ts +14 -0
  58. package/src/providers/local/entry.client.ts +18 -0
  59. package/src/providers/local/entry.server.ts +7 -0
  60. package/src/providers/local/entry.ts +14 -0
  61. package/src/providers/local/lib/constants.ts +23 -0
  62. package/src/providers/local/lib/helpers.ts +105 -0
  63. package/src/providers/local/lib/route-handler.server.ts +153 -0
  64. package/src/providers/local/lib/router.server.ts +137 -0
  65. package/src/providers/local/lib/schema.ts +104 -0
  66. package/src/providers/local/lib/search-params.ts +4 -0
  67. package/src/providers/local/lib/service.server.ts +1116 -0
  68. package/src/providers/local/lib/upload.client.ts +177 -0
  69. package/src/providers/local/lib/validators.ts +154 -0
  70. package/src/styles.css +1 -0
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { findStatus } from "@tulip-systems/core/components";
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@tulip-systems/core/components/client";
5
+ import { type TableColumnDef, TableColumnHeader } from "@tulip-systems/core/data-tables";
6
+ import { createTableSelectCell, TableTextCell } from "@tulip-systems/core/data-tables/client";
7
+ import { FolderIcon } from "lucide-react";
8
+ import Link from "next/link";
9
+ import { createSerializer, useQueryStates } from "nuqs";
10
+ import { nodeSubtypeConfig, nodeSubtypeVariants } from "@/config/types";
11
+ import { driveTreeSearchParams } from "@/lib/search-params";
12
+ import type { GoogleDriveNode } from "../lib/validators";
13
+
14
+ export const googleDriveColumns: TableColumnDef<GoogleDriveNode>[] = [
15
+ createTableSelectCell(),
16
+ {
17
+ id: "icon",
18
+ accessorKey: "icon",
19
+ header: () => null,
20
+ cell: ({ row }) => {
21
+ if (row.original.type === "file") {
22
+ const subtype = findStatus(nodeSubtypeConfig, row.original.subtype);
23
+ if (!subtype) return null;
24
+
25
+ return (
26
+ <TableTextCell className="w-4">
27
+ <Tooltip>
28
+ <TooltipTrigger>
29
+ <subtype.icon
30
+ className={nodeSubtypeVariants({
31
+ status: row.original.subtype,
32
+ className: "size-4",
33
+ })}
34
+ />
35
+ </TooltipTrigger>
36
+
37
+ <TooltipContent>{subtype.label}</TooltipContent>
38
+ </Tooltip>
39
+ </TableTextCell>
40
+ );
41
+ }
42
+
43
+ if (row.original.type === "folder") {
44
+ return (
45
+ <TableTextCell className="w-4">
46
+ <FolderIcon className="w-4 min-w-4" />
47
+ </TableTextCell>
48
+ );
49
+ }
50
+
51
+ return null;
52
+ },
53
+ enableSorting: false,
54
+ },
55
+ {
56
+ id: "name",
57
+ accessorKey: "name",
58
+ header: ({ column }) => <TableColumnHeader column={column} title="Naam" />,
59
+ cell: ({ row }) => {
60
+ const [query] = useQueryStates(driveTreeSearchParams);
61
+ const serialize = createSerializer(driveTreeSearchParams);
62
+
63
+ return (
64
+ <TableTextCell className="w-full min-w-64">
65
+ <Link
66
+ href={
67
+ row.original.type === "file"
68
+ ? ((row.original.links.view as string) ?? "#")
69
+ : serialize({ ...query, parentId: row.original.id })
70
+ }
71
+ className="truncate hover:underline"
72
+ target={row.original.type === "file" ? "_blank" : "_self"}
73
+ >
74
+ {row.getValue("name")}
75
+ </Link>
76
+ </TableTextCell>
77
+ );
78
+ },
79
+ enableSorting: false,
80
+ },
81
+ ];
@@ -0,0 +1,3 @@
1
+ import { driveTreeFilters } from "@/config/filters";
2
+
3
+ export const googleDriveTreeFilters = driveTreeFilters;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Components
3
+ */
4
+ export * from "./components/command-file-update";
5
+ export * from "./components/command-folder-create";
6
+ export * from "./components/command-folder-update";
7
+ export * from "./components/content.client";
8
+ export * from "./components/navigation.client";
9
+ export * from "./components/provider.client";
10
+ export * from "./components/view.client";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Lib
3
+ */
4
+ export * from "./lib/router.server";
5
+ export * from "./lib/service.server";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Config
3
+ */
4
+
5
+ export * from "./config/columns-data";
6
+ export * from "./config/filters";
7
+ /**
8
+ * Lib
9
+ */
10
+ export * from "./lib/dto";
11
+ export * from "./lib/search-params";
12
+ export * from "./lib/validators";
@@ -0,0 +1,10 @@
1
+ export const GOOGLE_DRIVE_FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
2
+
3
+ /**
4
+ * Minimal Google file fields needed to build a complete `GoogleDriveNode`.
5
+ *
6
+ * Keeping this list explicit prevents over-fetching large provider payloads and
7
+ * ensures every service method receives metadata with the same expected shape.
8
+ */
9
+ export const GOOGLE_DRIVE_NODE_FIELDS =
10
+ "id,name,mimeType,size,parents,driveId,createdTime,modifiedTime,trashed,trashedTime,webViewLink,webContentLink,thumbnailLink,contentRestrictions,capabilities";
@@ -0,0 +1,104 @@
1
+ import type { drive_v3 } from "googleapis";
2
+ import { inferLocalDriveNodeSubtype } from "../../local/entry";
3
+ import { GOOGLE_DRIVE_FOLDER_MIME_TYPE } from "./constants";
4
+ import type { GoogleDriveNode } from "./validators";
5
+
6
+ /**
7
+ * Input used by the DTO factory.
8
+ *
9
+ * `namespace` can be supplied by callers when Google metadata does not include
10
+ * enough drive context. Most service calls rely on `driveId` or the first parent
11
+ * from the Google response instead.
12
+ */
13
+ export type GoogleDriveNodeDTOInput = {
14
+ file: drive_v3.Schema$File;
15
+ namespace?: string;
16
+ };
17
+
18
+ /**
19
+ * Factory for mapping Google Drive API file metadata to the provider-neutral node shape.
20
+ * This is the single source of truth for how Google Drive metadata maps to our shared
21
+ * `DriveNode` contract, ensuring consistent mapping across all service methods.
22
+ * The factory also handles missing or partial metadata from Google by applying sensible
23
+ * defaults and fallbacks, so the shared DTO always satisfies the base contract expected
24
+ * by the UI and other consumers.
25
+ */
26
+ export const GoogleDriveNodeDTO = {
27
+ /**
28
+ * Maps raw Google Drive file metadata to the provider-neutral Drive node shape.
29
+ *
30
+ * Google models files and folders with the same `drive_v3.Schema$File` object.
31
+ * Folders are detected by their special MIME type, while every other MIME type
32
+ * is treated as a file and mapped to a provider-neutral subtype.
33
+ */
34
+ create({ file, namespace }: GoogleDriveNodeDTOInput): GoogleDriveNode {
35
+ if (!file.id) {
36
+ throw new Error("Google Drive file has no id");
37
+ }
38
+
39
+ // Google Drive folders are files with a special MIME type.
40
+ const isFolder = file.mimeType === GOOGLE_DRIVE_FOLDER_MIME_TYPE;
41
+
42
+ // Google can omit timestamps in partial responses. Fall back to stable dates
43
+ // so the shared DTO always satisfies the base Drive node contract.
44
+ const createdAt = file.createdTime ? new Date(file.createdTime) : new Date();
45
+ const updatedAt = file.modifiedTime ? new Date(file.modifiedTime) : createdAt;
46
+
47
+ // Google calls soft deletion `trashed`; the shared Drive contract exposes it
48
+ // as `archivedAt` for consistency with local/object-storage providers.
49
+ const trashedAt = file.trashedTime ? new Date(file.trashedTime) : null;
50
+ const archivedAt = file.trashed
51
+ ? (trashedAt ?? new Date(file.modifiedTime ?? Date.now()))
52
+ : null;
53
+ const contentType = isFolder ? null : (file.mimeType ?? null);
54
+ const name = file.name ?? "Untitled";
55
+ const googleParents = file.parents ?? [];
56
+ const [googleParentId] = googleParents;
57
+
58
+ // The shared `namespace` is the drive/root boundary used by the UI. Prefer
59
+ // explicit caller context, then Google's shared drive id, then the first
60
+ // parent as a last-resort root context for My Drive-like responses.
61
+ const nodeNamespace = namespace ?? file.driveId ?? googleParentId;
62
+
63
+ if (!nodeNamespace) {
64
+ throw new Error("Google Drive file has no namespace");
65
+ }
66
+
67
+ const readonly =
68
+ file.contentRestrictions?.some((restriction) => restriction.readOnly) ??
69
+ file.capabilities?.canEdit === false;
70
+
71
+ return {
72
+ id: file.id,
73
+ provider: "google",
74
+ createdAt,
75
+ updatedAt,
76
+ name,
77
+ namespace: nodeNamespace,
78
+ type: isFolder ? "folder" : "file",
79
+ subtype: isFolder ? "other" : inferLocalDriveNodeSubtype({ name, contentType }),
80
+ size: file.size ? Number(file.size) : null,
81
+ contentType,
82
+ readonly,
83
+ hidden: false,
84
+ archivedAt,
85
+ parentId: googleParentId && googleParentId !== nodeNamespace ? googleParentId : null,
86
+ links: {
87
+ view: file.webViewLink ?? file.webContentLink ?? null,
88
+ download: file.webContentLink ?? null,
89
+ preview: file.webViewLink ?? null,
90
+ thumbnail: file.thumbnailLink ?? null,
91
+ },
92
+ availability: "ready",
93
+ googleDriveId: file.driveId ?? null,
94
+ mimeType: file.mimeType ?? null,
95
+ webViewLink: file.webViewLink ?? null,
96
+ webContentLink: file.webContentLink ?? null,
97
+ thumbnailLink: file.thumbnailLink ?? null,
98
+ trashed: file.trashed ?? false,
99
+ trashedAt,
100
+ googleParents,
101
+ canEdit: file.capabilities?.canEdit ?? null,
102
+ } satisfies GoogleDriveNode;
103
+ },
104
+ };
@@ -0,0 +1,37 @@
1
+ import { Readable } from "node:stream";
2
+ import type { ObjectBodyInput } from "@tulip-systems/core/storage";
3
+
4
+ /**
5
+ * Escapes dynamic values before interpolating them into Google Drive `q` strings.
6
+ *
7
+ * Drive query values are wrapped in single quotes. Backslashes and single quotes
8
+ * can otherwise break the query literal or change the meaning of the expression.
9
+ */
10
+ export function escapeGoogleDriveQueryValue(value: string) {
11
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
12
+ }
13
+
14
+ /**
15
+ * Returns true when a Google API error represents a missing file/folder.
16
+ *
17
+ * The Google client throws rich error objects rather than typed exceptions, so
18
+ * the service checks the normalized `code` property and treats only 404 as a
19
+ * nullable not-found result. Permission, quota, and transport failures should
20
+ * keep bubbling up.
21
+ */
22
+ export function isGoogleNotFound(error: unknown) {
23
+ return typeof error === "object" && error != null && "code" in error && error.code === 404;
24
+ }
25
+
26
+ /**
27
+ * Normalizes supported object body inputs into a readable stream for googleapis.
28
+ *
29
+ * The Drive client accepts a stream-like `media.body`. Local upload flows can
30
+ * provide strings, buffers, array-like chunks, or already-created streams, so we
31
+ * pass through streams and wrap everything else with `Readable.from`.
32
+ */
33
+ export function toGoogleDriveReadable(body: ObjectBodyInput) {
34
+ if (body instanceof Readable) return body;
35
+ if (typeof body === "string") return Readable.from([body]);
36
+ return Readable.from([body]);
37
+ }
@@ -0,0 +1,62 @@
1
+ import type { TDatabaseSchema } from "@tulip-systems/core/config";
2
+ import { bulkActionSchema } from "@tulip-systems/core/router";
3
+ import { initRPC } from "@tulip-systems/core/router/server";
4
+ import { z } from "zod";
5
+ import type { GoogleDrive } from "./service.server";
6
+ import {
7
+ createGoogleDriveFolderInputSchema,
8
+ getGoogleDriveNodesByParentIdInputSchema,
9
+ googleDriveNodeSchema,
10
+ googleDriveNodeWithChildrenSchema,
11
+ listGoogleDriveTreeInputSchema,
12
+ updateGoogleDriveNodeInputSchema,
13
+ } from "./validators";
14
+
15
+ export function createGoogleDriveProcedures<TSchema extends TDatabaseSchema>(
16
+ drive: GoogleDrive<TSchema>,
17
+ ) {
18
+ const { protectedProcedure } = initRPC<TSchema>();
19
+
20
+ return {
21
+ getNodeWithChildren: protectedProcedure
22
+ .input(z.object({ id: z.string(), namespace: z.string() }))
23
+ .output(googleDriveNodeWithChildrenSchema.nullable())
24
+ .handler(async ({ input }) => drive.getNodeWithChildren(input)),
25
+ getNodesByParentId: protectedProcedure
26
+ .input(getGoogleDriveNodesByParentIdInputSchema)
27
+ .output(z.array(googleDriveNodeSchema))
28
+ .handler(async ({ input }) => drive.getNodesByParentId(input)),
29
+ listTree: protectedProcedure
30
+ .input(listGoogleDriveTreeInputSchema)
31
+ .handler(async ({ input }) => drive.listTree(input)),
32
+ getFolderParents: protectedProcedure
33
+ .input(z.object({ id: z.string().nullable(), namespace: z.string() }))
34
+ .handler(async ({ input }) => drive.getFolderParents(input)),
35
+ createFolder: protectedProcedure
36
+ .input(createGoogleDriveFolderInputSchema)
37
+ .output(googleDriveNodeSchema)
38
+ .handler(async ({ input }) => drive.createFolder(input)),
39
+ updateNode: protectedProcedure
40
+ .input(z.object({ id: z.string(), data: updateGoogleDriveNodeInputSchema }))
41
+ .output(googleDriveNodeSchema)
42
+ .handler(async ({ input }) => drive.updateNode(input.id, input.data)),
43
+ moveNode: protectedProcedure
44
+ .input(z.object({ id: z.string(), parentId: z.string().nullish() }))
45
+ .output(googleDriveNodeSchema)
46
+ .handler(async ({ input }) =>
47
+ drive.moveNode({ id: input.id, parentId: input.parentId ?? null }),
48
+ ),
49
+ archiveNodes: protectedProcedure
50
+ .input(bulkActionSchema)
51
+ .output(z.array(googleDriveNodeSchema))
52
+ .handler(async ({ input }) => drive.archiveNodes(input.ids)),
53
+ restoreNodes: protectedProcedure
54
+ .input(bulkActionSchema)
55
+ .output(z.array(googleDriveNodeSchema))
56
+ .handler(async ({ input }) => drive.restoreNodes(input.ids)),
57
+ deleteNodes: protectedProcedure
58
+ .input(bulkActionSchema)
59
+ .output(z.void())
60
+ .handler(async ({ input }) => drive.deleteNodes(input.ids)),
61
+ };
62
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Node table
3
+ */
4
+
5
+ /**
6
+ * Drive schema
7
+ **/
8
+ const googleDriveSchema = {};
9
+ export type GoogleDriveSchema = typeof googleDriveSchema;
@@ -0,0 +1,7 @@
1
+ import { resolveFiltersSearchParams } from "@tulip-systems/core/data-tables";
2
+ import { googleDriveTreeFilters } from "../config/filters";
3
+
4
+ /**
5
+ * Filters
6
+ */
7
+ export const googleDriveTreeFilterSearchParams = resolveFiltersSearchParams(googleDriveTreeFilters);