@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,153 @@
|
|
|
1
|
+
import { getSession } from "@tulip-systems/core/auth/server";
|
|
2
|
+
import type { TDatabaseSchema, TulipContext } from "@tulip-systems/core/config";
|
|
3
|
+
import { ServerError } from "@tulip-systems/core/router/server";
|
|
4
|
+
import { connection, type NextRequest } from "next/server";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
import type { LocalDrive } from "./service.server";
|
|
7
|
+
import { getLocalFileURLSchema } from "./validators";
|
|
8
|
+
|
|
9
|
+
type RouteCtx = {
|
|
10
|
+
params: Promise<{ rest?: string[] }>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type CreateLocalDriveRouteHandlerProps<TSchema extends TDatabaseSchema> = Pick<
|
|
14
|
+
TulipContext<TSchema>,
|
|
15
|
+
"auth"
|
|
16
|
+
> & {
|
|
17
|
+
drive: LocalDrive<TSchema>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a catch-all drive route handler for `/api/drive/[[...rest]]`.
|
|
22
|
+
*
|
|
23
|
+
* Current support:
|
|
24
|
+
* - `GET /api/drive/files/:id?variant=:variant&disposition=:disposition`
|
|
25
|
+
* resolves a drive node, checks access for private local assets, and redirects
|
|
26
|
+
* to a short-lived file URL for the requested node variant.
|
|
27
|
+
*
|
|
28
|
+
* @param props - Route handler dependencies.
|
|
29
|
+
* @returns Next.js route handlers for GET, POST, PUT, PATCH, and DELETE.
|
|
30
|
+
*/
|
|
31
|
+
export function createLocalDriveRouteHandler<TSchema extends TDatabaseSchema>(
|
|
32
|
+
context: CreateLocalDriveRouteHandlerProps<TSchema>,
|
|
33
|
+
) {
|
|
34
|
+
/**
|
|
35
|
+
* GET /api/drive/[[...rest]]
|
|
36
|
+
*
|
|
37
|
+
* Current route support:
|
|
38
|
+
* - /api/drive/files/:id?variant=:variant&disposition=:disposition
|
|
39
|
+
*/
|
|
40
|
+
async function GET(request: NextRequest, ctx: RouteCtx) {
|
|
41
|
+
const { rest = [] } = await ctx.params;
|
|
42
|
+
const resource = rest[0];
|
|
43
|
+
|
|
44
|
+
if (resource === "files") {
|
|
45
|
+
const id = rest[1];
|
|
46
|
+
if (!id) {
|
|
47
|
+
return Response.json(
|
|
48
|
+
{ error: { code: "BAD_REQUEST", message: "Missing file id" } },
|
|
49
|
+
{ status: 400 },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const searchParams = request.nextUrl.searchParams;
|
|
54
|
+
|
|
55
|
+
const variant = searchParams.get("variant") ?? undefined;
|
|
56
|
+
const disposition = searchParams.get("disposition") ?? undefined;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const node = await context.drive.getNodeById(id);
|
|
60
|
+
|
|
61
|
+
if (!node) {
|
|
62
|
+
return Response.json(
|
|
63
|
+
{ error: { code: "NOT_FOUND", message: "Node not found" } },
|
|
64
|
+
{ status: 404 },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (node.assetId) {
|
|
69
|
+
const asset = await context.drive.storage.getAssetById(node.assetId);
|
|
70
|
+
|
|
71
|
+
if (!asset) {
|
|
72
|
+
return Response.json(
|
|
73
|
+
{ error: { code: "NOT_FOUND", message: "Asset not found" } },
|
|
74
|
+
{ status: 404 },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (asset.visibility === "private") {
|
|
79
|
+
const session = await getSession(context);
|
|
80
|
+
|
|
81
|
+
if (!session) {
|
|
82
|
+
return Response.json(
|
|
83
|
+
{ error: { code: "UNAUTHORIZED", message: "Unauthorized" } },
|
|
84
|
+
{ status: 401 },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const options = getLocalFileURLSchema.parse({ variant, disposition });
|
|
91
|
+
const url = await context.drive.getURL(node, options);
|
|
92
|
+
|
|
93
|
+
return Response.redirect(url, 307);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof z.ZodError) {
|
|
96
|
+
return Response.json(
|
|
97
|
+
{ error: { code: "BAD_REQUEST", message: "Invalid request input" } },
|
|
98
|
+
{ status: 400 },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (error instanceof ServerError) {
|
|
103
|
+
if (error.code === "BAD_REQUEST") {
|
|
104
|
+
return Response.json(
|
|
105
|
+
{ error: { code: "BAD_REQUEST", message: error.message } },
|
|
106
|
+
{ status: 400 },
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (error.code === "NOT_FOUND") {
|
|
111
|
+
return Response.json(
|
|
112
|
+
{ error: { code: "NOT_FOUND", message: error.message } },
|
|
113
|
+
{ status: 404 },
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (error.code === "UNAUTHORIZED") {
|
|
118
|
+
return Response.json(
|
|
119
|
+
{ error: { code: "UNAUTHORIZED", message: error.message } },
|
|
120
|
+
{ status: 401 },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.error("Drive route error", error);
|
|
126
|
+
|
|
127
|
+
return Response.json(
|
|
128
|
+
{ error: { code: "INTERNAL_SERVER_ERROR", message: "Something went wrong" } },
|
|
129
|
+
{ status: 500 },
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return Response.json(
|
|
135
|
+
{ error: { code: "NOT_FOUND", message: "Route not found" } },
|
|
136
|
+
{ status: 404 },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* POST, PUT, PATCH, DELETE /api/drive/[[...rest]]
|
|
142
|
+
*/
|
|
143
|
+
async function POST() {
|
|
144
|
+
await connection();
|
|
145
|
+
|
|
146
|
+
return Response.json(
|
|
147
|
+
{ error: { code: "NOT_FOUND", message: "Route not found" } },
|
|
148
|
+
{ status: 404 },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { GET, POST, PUT: POST, PATCH: POST, DELETE: POST };
|
|
153
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { bulkActionSchema } from "@tulip-systems/core/router";
|
|
2
|
+
import { initRPC } from "@tulip-systems/core/router/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { DriveSchema } from "./schema";
|
|
5
|
+
import type { LocalDrive } from "./service.server";
|
|
6
|
+
import {
|
|
7
|
+
createLocalDriveFolderInputSchema,
|
|
8
|
+
getLocalDriveNodesByParentIdInputSchema,
|
|
9
|
+
getLocalFileURLSchema,
|
|
10
|
+
listLocalDriveFlatInputSchema,
|
|
11
|
+
listLocalDriveTreeInputSchema,
|
|
12
|
+
localDriveNodeSchema,
|
|
13
|
+
localDriveNodeWithChildrenSchema,
|
|
14
|
+
presignLocalDriveFileInputSchema,
|
|
15
|
+
updateLocalDriveNodeInputSchema,
|
|
16
|
+
} from "./validators";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create Drive base procedures
|
|
20
|
+
*/
|
|
21
|
+
export function createLocalDriveProcedures(drive: LocalDrive<DriveSchema>) {
|
|
22
|
+
const { protectedProcedure } = initRPC<DriveSchema>();
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
/**
|
|
26
|
+
* Get node with children
|
|
27
|
+
*/
|
|
28
|
+
getNodeWithChildren: protectedProcedure
|
|
29
|
+
.input(z.object({ id: z.string(), namespace: z.string() }))
|
|
30
|
+
.output(localDriveNodeWithChildrenSchema.nullable())
|
|
31
|
+
.handler(async ({ input }) => drive.getNodeWithChildren(input)),
|
|
32
|
+
/**
|
|
33
|
+
* Get folders by parent id
|
|
34
|
+
*/
|
|
35
|
+
getNodesByParentId: protectedProcedure
|
|
36
|
+
.input(getLocalDriveNodesByParentIdInputSchema)
|
|
37
|
+
.output(z.array(localDriveNodeSchema))
|
|
38
|
+
.handler(async ({ input }) => drive.getNodesByParentId(input)),
|
|
39
|
+
/**
|
|
40
|
+
* List tree nodes with pagination and search.
|
|
41
|
+
*/
|
|
42
|
+
listTree: protectedProcedure
|
|
43
|
+
.input(listLocalDriveTreeInputSchema)
|
|
44
|
+
.handler(async ({ input }) => drive.listTree(input)),
|
|
45
|
+
/**
|
|
46
|
+
* List flat nodes with pagination and search.
|
|
47
|
+
*/
|
|
48
|
+
listFlat: protectedProcedure
|
|
49
|
+
.input(listLocalDriveFlatInputSchema)
|
|
50
|
+
.handler(async ({ input }) => drive.listFlat(input)),
|
|
51
|
+
/**
|
|
52
|
+
* Get parents of a folder
|
|
53
|
+
*/
|
|
54
|
+
getFolderParents: protectedProcedure
|
|
55
|
+
.input(z.object({ id: z.string().nullable(), namespace: z.string() }))
|
|
56
|
+
.handler(async ({ input }) => drive.getFolderParents(input)),
|
|
57
|
+
/**
|
|
58
|
+
* Get url for opening a file
|
|
59
|
+
*/
|
|
60
|
+
getURL: protectedProcedure
|
|
61
|
+
.input(getLocalFileURLSchema.extend({ id: z.string() }))
|
|
62
|
+
.output(z.string().nullable())
|
|
63
|
+
.handler(async ({ input: { id, ...options } }) => {
|
|
64
|
+
const node = await drive.getNodeById(id);
|
|
65
|
+
if (!node) return null;
|
|
66
|
+
|
|
67
|
+
return await drive.getURL(node, options);
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a direct upload intent for a file
|
|
72
|
+
*/
|
|
73
|
+
presignUpload: protectedProcedure
|
|
74
|
+
.input(presignLocalDriveFileInputSchema)
|
|
75
|
+
.handler(async ({ input }) => drive.presignUpload(input)),
|
|
76
|
+
/**
|
|
77
|
+
* Confirm a direct upload intent
|
|
78
|
+
*/
|
|
79
|
+
confirmUpload: protectedProcedure
|
|
80
|
+
.input(z.object({ uploadId: z.string() }))
|
|
81
|
+
.handler(async ({ input }) => drive.confirmUpload(input.uploadId)),
|
|
82
|
+
/**
|
|
83
|
+
* Create a folder
|
|
84
|
+
*/
|
|
85
|
+
createFolder: protectedProcedure
|
|
86
|
+
.input(createLocalDriveFolderInputSchema)
|
|
87
|
+
.output(localDriveNodeSchema)
|
|
88
|
+
.handler(async ({ input }) => drive.createFolder(input)),
|
|
89
|
+
/**
|
|
90
|
+
* Update node
|
|
91
|
+
*/
|
|
92
|
+
updateNode: protectedProcedure
|
|
93
|
+
.input(z.object({ id: z.string(), data: updateLocalDriveNodeInputSchema }))
|
|
94
|
+
.output(localDriveNodeSchema)
|
|
95
|
+
.handler(async ({ input }) => drive.updateNode(input.id, input.data)),
|
|
96
|
+
/**
|
|
97
|
+
* Set readonly
|
|
98
|
+
*/
|
|
99
|
+
setReadonly: protectedProcedure
|
|
100
|
+
.input(bulkActionSchema.extend({ readonly: z.boolean() }))
|
|
101
|
+
.output(z.array(localDriveNodeSchema))
|
|
102
|
+
.handler(async ({ input }) => drive.setReadonly(input.ids, input.readonly)),
|
|
103
|
+
/**
|
|
104
|
+
* Move node
|
|
105
|
+
*/
|
|
106
|
+
moveNode: protectedProcedure
|
|
107
|
+
.input(z.object({ id: z.string(), parentId: z.string().nullish() }))
|
|
108
|
+
.output(localDriveNodeSchema)
|
|
109
|
+
.handler(async ({ input }) =>
|
|
110
|
+
drive.moveNode({
|
|
111
|
+
id: input.id,
|
|
112
|
+
parentId: input.parentId ?? null,
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
/**
|
|
116
|
+
* Archive nodes
|
|
117
|
+
*/
|
|
118
|
+
archiveNodes: protectedProcedure
|
|
119
|
+
.input(bulkActionSchema)
|
|
120
|
+
.output(z.array(localDriveNodeSchema))
|
|
121
|
+
.handler(async ({ input }) => drive.archiveNodes(input.ids)),
|
|
122
|
+
/**
|
|
123
|
+
* Restore nodes
|
|
124
|
+
*/
|
|
125
|
+
restoreNodes: protectedProcedure
|
|
126
|
+
.input(bulkActionSchema)
|
|
127
|
+
.output(z.array(localDriveNodeSchema))
|
|
128
|
+
.handler(async ({ input }) => drive.restoreNodes(input.ids)),
|
|
129
|
+
/**
|
|
130
|
+
* Delete nodes
|
|
131
|
+
*/
|
|
132
|
+
deleteNodes: protectedProcedure
|
|
133
|
+
.input(bulkActionSchema)
|
|
134
|
+
.output(z.void())
|
|
135
|
+
.handler(async ({ input }) => drive.deleteNodes(input.ids)),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { baseColumns } from "@tulip-systems/core/config";
|
|
2
|
+
import { imageDispositions, storageAssets } from "@tulip-systems/core/storage";
|
|
3
|
+
import { relations } from "drizzle-orm";
|
|
4
|
+
import { type AnyPgColumn, boolean, pgEnum, pgTable, unique } from "drizzle-orm/pg-core";
|
|
5
|
+
import { imageVariants, nodeSubtypes } from "./constants";
|
|
6
|
+
|
|
7
|
+
export const nodeTypeEnum = pgEnum("node_types", ["file", "folder"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Node table
|
|
11
|
+
*/
|
|
12
|
+
export const nodes = pgTable("nodes", (t) => ({
|
|
13
|
+
...baseColumns,
|
|
14
|
+
name: t.text().notNull(),
|
|
15
|
+
namespace: t.text().notNull().default("global"),
|
|
16
|
+
type: nodeTypeEnum(),
|
|
17
|
+
subtype: t.text({ enum: nodeSubtypes }).notNull().default("other"),
|
|
18
|
+
size: t.integer(),
|
|
19
|
+
contentType: t.varchar({ length: 255 }),
|
|
20
|
+
readonly: boolean().default(false),
|
|
21
|
+
hidden: boolean().default(false),
|
|
22
|
+
archivedAt: t.timestamp(),
|
|
23
|
+
parentId: t.uuid().references((): AnyPgColumn => nodes.id, { onDelete: "cascade" }),
|
|
24
|
+
assetId: t.uuid().references(() => storageAssets.id, { onDelete: "cascade" }),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
export const nodesRelations = relations(nodes, ({ one, many }) => ({
|
|
28
|
+
parent: one(nodes, {
|
|
29
|
+
fields: [nodes.parentId],
|
|
30
|
+
references: [nodes.id],
|
|
31
|
+
relationName: "parent",
|
|
32
|
+
}),
|
|
33
|
+
children: many(nodes, {
|
|
34
|
+
relationName: "parent",
|
|
35
|
+
}),
|
|
36
|
+
urls: many(nodePresignedUrls),
|
|
37
|
+
variants: many(nodeVariants),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Node variants table
|
|
42
|
+
*/
|
|
43
|
+
export const nodeVariants = pgTable("node_variants", (t) => ({
|
|
44
|
+
...baseColumns,
|
|
45
|
+
nodeId: t
|
|
46
|
+
.uuid()
|
|
47
|
+
.notNull()
|
|
48
|
+
.references(() => nodes.id, { onDelete: "cascade" }),
|
|
49
|
+
assetId: t
|
|
50
|
+
.uuid()
|
|
51
|
+
.notNull()
|
|
52
|
+
.references(() => storageAssets.id, { onDelete: "cascade" }),
|
|
53
|
+
variant: t.text({ enum: imageVariants }).notNull(),
|
|
54
|
+
width: t.integer().notNull(),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
export const nodeVariantsRelations = relations(nodeVariants, ({ one }) => ({
|
|
58
|
+
node: one(nodes, {
|
|
59
|
+
fields: [nodeVariants.nodeId],
|
|
60
|
+
references: [nodes.id],
|
|
61
|
+
relationName: "node",
|
|
62
|
+
}),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Node presigned urls table
|
|
67
|
+
*/
|
|
68
|
+
export const nodePresignedUrls = pgTable(
|
|
69
|
+
"node_presigned_urls",
|
|
70
|
+
(t) => ({
|
|
71
|
+
...baseColumns,
|
|
72
|
+
url: t.text().notNull().unique(),
|
|
73
|
+
variant: t.text({ enum: imageVariants }).notNull(),
|
|
74
|
+
disposition: t.text({ enum: imageDispositions }).notNull(),
|
|
75
|
+
expiresAt: t.timestamp().notNull(),
|
|
76
|
+
nodeId: t
|
|
77
|
+
.uuid()
|
|
78
|
+
.notNull()
|
|
79
|
+
.references(() => nodes.id, { onDelete: "cascade" }),
|
|
80
|
+
variantId: t.uuid().references(() => nodeVariants.id, { onDelete: "set null" }),
|
|
81
|
+
}),
|
|
82
|
+
(t) => [unique("node_presigned_url_unique").on(t.nodeId, t.variant, t.disposition)],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export const nodePresignedUrlsRelations = relations(nodePresignedUrls, ({ one }) => ({
|
|
86
|
+
node: one(nodes, {
|
|
87
|
+
fields: [nodePresignedUrls.nodeId],
|
|
88
|
+
references: [nodes.id],
|
|
89
|
+
relationName: "node",
|
|
90
|
+
}),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Drive schema
|
|
95
|
+
**/
|
|
96
|
+
const driveSchema = {
|
|
97
|
+
nodes,
|
|
98
|
+
nodesRelations,
|
|
99
|
+
nodeVariants,
|
|
100
|
+
nodeVariantsRelations,
|
|
101
|
+
nodePresignedUrls,
|
|
102
|
+
nodePresignedUrlsRelations,
|
|
103
|
+
};
|
|
104
|
+
export type DriveSchema = typeof driveSchema;
|