@goscribe/server 1.0.1 → 1.0.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.
- package/package.json +1 -1
- package/prisma/schema.prisma +2 -2
- package/src/lib/file.ts +0 -0
- package/src/lib/storage.ts +13 -0
- package/src/routers/workspace.ts +121 -16
- package/src/server.ts +5 -1
package/package.json
CHANGED
package/prisma/schema.prisma
CHANGED
|
@@ -140,8 +140,8 @@ model FileAsset {
|
|
|
140
140
|
name String
|
|
141
141
|
mimeType String
|
|
142
142
|
size Int
|
|
143
|
-
bucket String
|
|
144
|
-
objectKey String
|
|
143
|
+
bucket String?
|
|
144
|
+
objectKey String?
|
|
145
145
|
url String? // optional if serving via signed GET per-view
|
|
146
146
|
checksum String? // optional server-side integrity
|
|
147
147
|
|
package/src/lib/file.ts
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/server/lib/gcs.ts
|
|
2
|
+
import { Storage } from "@google-cloud/storage";
|
|
3
|
+
|
|
4
|
+
export const storage = new Storage({
|
|
5
|
+
projectId: process.env.GCP_PROJECT_ID,
|
|
6
|
+
credentials: {
|
|
7
|
+
client_email: process.env.GCP_CLIENT_EMAIL,
|
|
8
|
+
private_key: process.env.GCP_PRIVATE_KEY?.replace(/\\n/g, "\n"),
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const bucket = storage.bucket(process.env.GCP_BUCKET!);
|
|
13
|
+
|
package/src/routers/workspace.ts
CHANGED
|
@@ -1,51 +1,156 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { router, publicProcedure, authedProcedure } from '../trpc';
|
|
3
|
+
import { bucket } from 'src/lib/storage';
|
|
3
4
|
|
|
4
5
|
export const workspace = router({
|
|
5
6
|
// Mutation with Zod input
|
|
6
7
|
list: publicProcedure
|
|
7
8
|
.query(async ({ ctx, input }) => {
|
|
9
|
+
const workspaces = await ctx.db.workspace.findMany({
|
|
10
|
+
where: {
|
|
11
|
+
ownerId: ctx.session?.user.id,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
return workspaces;
|
|
8
15
|
}),
|
|
9
16
|
|
|
10
17
|
create: publicProcedure
|
|
11
18
|
.input(z.object({
|
|
12
|
-
|
|
19
|
+
name: z.string().min(1).max(100),
|
|
20
|
+
description: z.string().max(500).optional(),
|
|
13
21
|
}))
|
|
14
|
-
.mutation(({ input
|
|
15
|
-
|
|
22
|
+
.mutation(({ ctx, input}) => {
|
|
23
|
+
return ctx.db.workspace.create({
|
|
24
|
+
data: {
|
|
25
|
+
title: input.name,
|
|
26
|
+
description: input.description,
|
|
27
|
+
ownerId: ctx.session?.user.id,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
16
30
|
}),
|
|
17
31
|
get: publicProcedure
|
|
18
32
|
.input(z.object({
|
|
19
33
|
id: z.string().uuid(),
|
|
20
34
|
}))
|
|
21
|
-
.query(({ input }) => {
|
|
35
|
+
.query(({ ctx, input }) => {
|
|
36
|
+
return ctx.db.workspace.findUnique({
|
|
37
|
+
where: {
|
|
38
|
+
id: input.id,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
22
41
|
}),
|
|
23
42
|
update: publicProcedure
|
|
24
43
|
.input(z.object({
|
|
25
44
|
id: z.string().uuid(),
|
|
45
|
+
name: z.string().min(1).max(100).optional(),
|
|
46
|
+
description: z.string().max(500).optional(),
|
|
26
47
|
}))
|
|
27
|
-
.mutation(({ input }) => {
|
|
28
|
-
|
|
48
|
+
.mutation(({ ctx, input }) => {
|
|
49
|
+
return ctx.db.workspace.update({
|
|
50
|
+
where: {
|
|
51
|
+
id: input.id,
|
|
52
|
+
},
|
|
53
|
+
data: {
|
|
54
|
+
title: input.name,
|
|
55
|
+
description: input.description,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
29
58
|
}),
|
|
30
59
|
delete: publicProcedure
|
|
31
60
|
.input(z.object({
|
|
32
61
|
id: z.string().uuid(),
|
|
33
62
|
}))
|
|
34
|
-
.mutation(({ input }) => {
|
|
35
|
-
|
|
63
|
+
.mutation(({ ctx, input }) => {
|
|
64
|
+
ctx.db.workspace.delete({
|
|
65
|
+
where: {
|
|
66
|
+
id: input.id,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
return true;
|
|
36
70
|
}),
|
|
37
|
-
|
|
71
|
+
uploadFiles: publicProcedure
|
|
38
72
|
.input(z.object({
|
|
39
|
-
|
|
73
|
+
id: z.string().uuid(),
|
|
74
|
+
files: z.array(
|
|
75
|
+
z.object({
|
|
76
|
+
filename: z.string().min(1).max(255),
|
|
77
|
+
contentType: z.string().min(1).max(100),
|
|
78
|
+
size: z.number().min(1), // size in bytes
|
|
79
|
+
})
|
|
80
|
+
),
|
|
40
81
|
}))
|
|
41
|
-
.mutation(({ input }) => {
|
|
42
|
-
|
|
82
|
+
.mutation(async ({ ctx, input }) => {
|
|
83
|
+
const results = [];
|
|
84
|
+
|
|
85
|
+
for (const file of input.files) {
|
|
86
|
+
// 1. Insert into DB
|
|
87
|
+
const record = await ctx.db.fileAsset.create({
|
|
88
|
+
data: {
|
|
89
|
+
userId: ctx.session.user.id,
|
|
90
|
+
name: file.filename,
|
|
91
|
+
mimeType: file.contentType,
|
|
92
|
+
size: file.size,
|
|
93
|
+
workspaceId: input.id,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 2. Generate signed URL for direct upload
|
|
98
|
+
const [url] = await bucket
|
|
99
|
+
.file(`${ctx.session.user.id}/${record.id}-${file.filename}`)
|
|
100
|
+
.getSignedUrl({
|
|
101
|
+
action: "write",
|
|
102
|
+
expires: Date.now() + 5 * 60 * 1000, // 5 min
|
|
103
|
+
contentType: file.contentType,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 3. Update record with bucket info
|
|
107
|
+
await ctx.db.fileAsset.update({
|
|
108
|
+
where: { id: record.id },
|
|
109
|
+
data: {
|
|
110
|
+
bucket: bucket.name,
|
|
111
|
+
objectKey: `${ctx.session.user.id}/${record.id}-${file.filename}`,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
results.push({
|
|
116
|
+
fileId: record.id,
|
|
117
|
+
uploadUrl: url,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return results;
|
|
122
|
+
|
|
43
123
|
}),
|
|
44
|
-
|
|
124
|
+
deleteFiles: publicProcedure
|
|
45
125
|
.input(z.object({
|
|
46
|
-
fileId: z.string().uuid(),
|
|
126
|
+
fileId: z.array(z.string().uuid()),
|
|
127
|
+
id: z.string().uuid(),
|
|
47
128
|
}))
|
|
48
|
-
.mutation(({ input }) => {
|
|
49
|
-
|
|
129
|
+
.mutation(({ ctx, input }) => {
|
|
130
|
+
const files = ctx.db.fileAsset.findMany({
|
|
131
|
+
where: {
|
|
132
|
+
id: { in: input.fileId },
|
|
133
|
+
workspaceId: input.id,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Delete from GCS
|
|
138
|
+
files.then((fileRecords) => {
|
|
139
|
+
fileRecords.forEach((file) => {
|
|
140
|
+
if (file.bucket && file.objectKey) {
|
|
141
|
+
const gcsFile = bucket.file(file.objectKey);
|
|
142
|
+
gcsFile.delete({ ignoreNotFound: true }).catch((err: unknown) => {
|
|
143
|
+
console.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return ctx.db.fileAsset.deleteMany({
|
|
150
|
+
where: {
|
|
151
|
+
id: { in: input.fileId },
|
|
152
|
+
workspaceId: input.id,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
50
155
|
}),
|
|
51
156
|
});
|
package/src/server.ts
CHANGED
|
@@ -16,7 +16,11 @@ async function main() {
|
|
|
16
16
|
|
|
17
17
|
// Middlewares
|
|
18
18
|
app.use(helmet());
|
|
19
|
-
app.use(cors({
|
|
19
|
+
app.use(cors({
|
|
20
|
+
origin: "http://localhost:3000", // your Next.js dev URL
|
|
21
|
+
credentials: true, // allow cookies
|
|
22
|
+
}));
|
|
23
|
+
|
|
20
24
|
app.use(morgan('dev'));
|
|
21
25
|
app.use(compression());
|
|
22
26
|
app.use(express.json());
|