@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.
- package/LICENSE +662 -0
- package/package.json +113 -0
- package/src/components/content.tsx +13 -0
- package/src/components/context.client.tsx +12 -0
- package/src/components/dnd.client.tsx +47 -0
- package/src/components/grid-card.client.tsx +252 -0
- package/src/components/grid.client.tsx +96 -0
- package/src/components/navigation/breadcrumbs.client.tsx +125 -0
- package/src/components/navigation/header.client.tsx +45 -0
- package/src/components/navigation/toolbar.client.tsx +35 -0
- package/src/components/navigation/view-switcher.client.tsx +32 -0
- package/src/components/selection.client.tsx +48 -0
- package/src/components/view.client.tsx +67 -0
- package/src/config/filters.ts +14 -0
- package/src/config/types.tsx +90 -0
- package/src/entry.client.ts +7 -0
- package/src/entry.server.ts +4 -0
- package/src/entry.ts +10 -0
- package/src/lib/constants.ts +19 -0
- package/src/lib/contracts.ts +121 -0
- package/src/lib/dto.ts +83 -0
- package/src/lib/helpers.server.ts +14 -0
- package/src/lib/helpers.ts +32 -0
- package/src/lib/search-params.ts +5 -0
- package/src/lib/validators.ts +89 -0
- package/src/providers/google/components/command-file-update.tsx +100 -0
- package/src/providers/google/components/command-folder-create.tsx +104 -0
- package/src/providers/google/components/command-folder-update.tsx +100 -0
- package/src/providers/google/components/content.client.tsx +6 -0
- package/src/providers/google/components/navigation.client.tsx +21 -0
- package/src/providers/google/components/provider.client.tsx +60 -0
- package/src/providers/google/components/view.client.tsx +158 -0
- package/src/providers/google/config/columns-data.tsx +81 -0
- package/src/providers/google/config/filters.ts +3 -0
- package/src/providers/google/entry.client.ts +10 -0
- package/src/providers/google/entry.server.ts +5 -0
- package/src/providers/google/entry.ts +12 -0
- package/src/providers/google/lib/constants.ts +10 -0
- package/src/providers/google/lib/dto.ts +104 -0
- package/src/providers/google/lib/helpers.ts +37 -0
- package/src/providers/google/lib/router.server.ts +62 -0
- package/src/providers/google/lib/schema.ts +9 -0
- package/src/providers/google/lib/search-params.ts +7 -0
- package/src/providers/google/lib/service.server.ts +792 -0
- package/src/providers/google/lib/validators.ts +148 -0
- package/src/providers/local/components/command-file-update.tsx +93 -0
- package/src/providers/local/components/command-file-upload.tsx +29 -0
- package/src/providers/local/components/command-folder-create.tsx +100 -0
- package/src/providers/local/components/command-folder-update.tsx +93 -0
- package/src/providers/local/components/content.client.tsx +3 -0
- package/src/providers/local/components/navigation.client.tsx +23 -0
- package/src/providers/local/components/provider.client.tsx +90 -0
- package/src/providers/local/components/upload-zone-context.client.tsx +43 -0
- package/src/providers/local/components/upload-zone.client.tsx +182 -0
- package/src/providers/local/components/view.client.tsx +145 -0
- package/src/providers/local/config/columns-data.tsx +81 -0
- package/src/providers/local/config/filters.ts +14 -0
- package/src/providers/local/entry.client.ts +18 -0
- package/src/providers/local/entry.server.ts +7 -0
- package/src/providers/local/entry.ts +14 -0
- package/src/providers/local/lib/constants.ts +23 -0
- package/src/providers/local/lib/helpers.ts +105 -0
- package/src/providers/local/lib/route-handler.server.ts +153 -0
- package/src/providers/local/lib/router.server.ts +137 -0
- package/src/providers/local/lib/schema.ts +104 -0
- package/src/providers/local/lib/search-params.ts +4 -0
- package/src/providers/local/lib/service.server.ts +1116 -0
- package/src/providers/local/lib/upload.client.ts +177 -0
- package/src/providers/local/lib/validators.ts +154 -0
- 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,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,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
|
+
}
|