@edkstack/files 0.1.12 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { n as createFilesServer } from "../server-Cxx9ToSf.mjs";
1
+ import { n as createFilesServer } from "../server-CeZ5g-TS.mjs";
2
2
  import "../server/index.mjs";
3
3
  import { ReactNode } from "react";
4
4
  import * as react_jsx_runtime0 from "react/jsx-runtime";
@@ -35,6 +35,7 @@ declare function useFilesClient(): {
35
35
  size: number;
36
36
  mimeType: string;
37
37
  createdAt: Date;
38
+ url: string;
38
39
  };
39
40
  400: {
40
41
  message: string;
@@ -65,6 +66,7 @@ declare function useUpload(options: InferMutationOptions<FilesClient["api"]["fil
65
66
  size: number;
66
67
  mimeType: string;
67
68
  createdAt: Date;
69
+ url: string;
68
70
  } | null, {
69
71
  status: 400;
70
72
  value: {
package/dist/index.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { a as schema_d_exports, i as Visibility, n as createFilesServer, r as PurposePolicy, t as FilesServerOptions } from "./server-Cxx9ToSf.mjs";
1
+ import { a as schema_d_exports, i as Visibility, n as createFilesServer, r as PurposePolicy, t as FilesServerOptions } from "./server-CeZ5g-TS.mjs";
2
2
  import "./server/index.mjs";
3
3
  export { FilesServerOptions, PurposePolicy, Visibility, createFilesServer, schema_d_exports as schema };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as schema_exports, t as createFilesServer } from "./server-D1-_uiJo.mjs";
1
+ import { n as schema_exports, t as createFilesServer } from "./server-BKDrcvWV.mjs";
2
2
  import "./server/index.mjs";
3
3
 
4
4
  export { createFilesServer, schema_exports as schema };
@@ -1,2 +1,2 @@
1
- import { a as schema_d_exports, i as Visibility, n as createFilesServer, r as PurposePolicy, t as FilesServerOptions } from "../server-Cxx9ToSf.mjs";
1
+ import { a as schema_d_exports, i as Visibility, n as createFilesServer, r as PurposePolicy, t as FilesServerOptions } from "../server-CeZ5g-TS.mjs";
2
2
  export { FilesServerOptions, type PurposePolicy, type Visibility, createFilesServer, schema_d_exports as schema };
@@ -1,3 +1,3 @@
1
- import { n as schema_exports, t as createFilesServer } from "../server-D1-_uiJo.mjs";
1
+ import { n as schema_exports, t as createFilesServer } from "../server-BKDrcvWV.mjs";
2
2
 
3
3
  export { createFilesServer, schema_exports as schema };
@@ -4,7 +4,7 @@ import { index, integer, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg
4
4
  import { Elysia, t } from "elysia";
5
5
  import { S3Client } from "bun";
6
6
  import { extname } from "path";
7
- import { and, eq, sql } from "drizzle-orm";
7
+ import { and, eq, inArray, sql } from "drizzle-orm";
8
8
 
9
9
  //#region src/server/schema.ts
10
10
  var schema_exports = /* @__PURE__ */ __exportAll({ files: () => files });
@@ -37,6 +37,7 @@ function createRouter(options) {
37
37
  visibility: policy.visibility ?? "private"
38
38
  });
39
39
  return status(200, {
40
+ url: await service.getUrl({ id: uploaded.id }) ?? "",
40
41
  id: uploaded.id,
41
42
  name: uploaded.name,
42
43
  key: uploaded.key,
@@ -61,6 +62,7 @@ function UploadRequest(purposes) {
61
62
  }
62
63
  const ErrorResponse = t.Object({ message: t.String() });
63
64
  const FileResponse = t.Object({
65
+ url: t.String(),
64
66
  id: t.String(),
65
67
  name: t.Nullable(t.String()),
66
68
  key: t.String(),
@@ -72,37 +74,60 @@ const FileResponse = t.Object({
72
74
  //#endregion
73
75
  //#region src/server/service.ts
74
76
  function createService(options) {
75
- const { db, s3: s3Options, keyPrefix = "files", presignExpiresIn = 3600 } = options;
76
- const { endpoint } = s3Options;
77
- const s3Client = new S3Client(s3Options);
77
+ const { db, s3, keyPrefix = "files", presignExpiresIn = 3600 } = options;
78
+ const { endpoint } = s3;
79
+ const s3Client = new S3Client(s3);
78
80
  return {
81
+ async listFiles(params) {
82
+ return await db.select().from(files).where(inArray(files.id, params.ids)).limit(params.ids.length);
83
+ },
79
84
  async getFile(params) {
80
85
  const [found] = await db.select().from(files).where(eq(files.id, params.id)).limit(1);
81
- if (!found) throw new Error("File not found");
82
- return found;
86
+ return found ?? null;
87
+ },
88
+ buildUrl(file) {
89
+ if (file.visibility === "public") return `${endpoint}/${file.key}`;
90
+ return s3Client.presign(file.key, { expiresIn: presignExpiresIn });
83
91
  },
84
92
  async getUrl(params) {
85
93
  const [found] = await db.select({
86
94
  key: files.key,
87
95
  visibility: files.visibility
88
96
  }).from(files).where(eq(files.id, params.id)).limit(1);
89
- if (!found) throw new Error("File not found");
90
- if (found.visibility === "public") return `${endpoint}/${found.key}`;
91
- return s3Client.presign(found.key, { expiresIn: presignExpiresIn });
97
+ if (!found) return null;
98
+ return this.buildUrl(found);
99
+ },
100
+ async getUrls(params) {
101
+ const rows = await db.select({
102
+ id: files.id,
103
+ key: files.key,
104
+ visibility: files.visibility
105
+ }).from(files).where(inArray(files.id, params.ids)).limit(params.ids.length);
106
+ return params.ids.map((id) => {
107
+ const row = rows.find((row) => row.id === id);
108
+ if (!row) return {
109
+ id,
110
+ url: null
111
+ };
112
+ return {
113
+ id,
114
+ url: this.buildUrl(row)
115
+ };
116
+ });
92
117
  },
93
118
  async uploadFile(params) {
94
119
  const id = nanoid();
95
120
  const ext = extname(params.file.name);
96
121
  const key = [
97
122
  keyPrefix,
98
- params.visibility,
99
123
  params.purpose,
100
124
  `${id}${ext}`
101
125
  ].join("/");
102
126
  const s3file = s3Client.file(key);
103
127
  await s3file.write(params.file, {
104
128
  type: params.file.type,
105
- acl: params.visibility === "public" ? "public-read" : "private"
129
+ acl: params.visibility === "public" ? "public-read" : "private",
130
+ contentDisposition: `attachment; filename="${params.file.name}"`
106
131
  });
107
132
  try {
108
133
  const [created] = await db.insert(files).values({
@@ -116,7 +141,7 @@ function createService(options) {
116
141
  if (!created) throw new Error("Failed to create file record");
117
142
  return created;
118
143
  } catch (error) {
119
- await s3file.delete().catch();
144
+ await s3file.delete().catch(() => {});
120
145
  throw error;
121
146
  }
122
147
  },
@@ -125,15 +150,33 @@ function createService(options) {
125
150
  if (!deleted) return;
126
151
  await s3Client.delete(deleted.key);
127
152
  },
153
+ async deleteFiles(params) {
154
+ const deleted = await db.delete(files).where(inArray(files.id, params.ids)).returning({ key: files.key });
155
+ if (deleted.length === 0) return;
156
+ await Promise.all(deleted.map((file) => s3Client.delete(file.key)));
157
+ },
128
158
  async acquireFile(params) {
129
159
  const [updated] = await db.update(files).set({ refCount: sql`${files.refCount} + 1` }).where(and(eq(files.id, params.id), params.purpose ? eq(files.purpose, params.purpose) : void 0)).returning();
130
160
  if (!updated) throw new Error("File not found");
131
161
  return updated;
132
162
  },
163
+ async acquireFiles(params) {
164
+ return await db.update(files).set({ refCount: sql`${files.refCount} + 1` }).where(and(inArray(files.id, params.ids), params.purpose ? eq(files.purpose, params.purpose) : void 0)).returning();
165
+ },
133
166
  async releaseFile(params) {
134
167
  const [updated] = await db.update(files).set({ refCount: sql`${files.refCount} - 1` }).where(eq(files.id, params.id)).returning();
135
168
  if (!updated) throw new Error("File not found");
136
169
  if (updated.refCount < 1) await this.deleteFile({ id: params.id });
170
+ },
171
+ async releaseFiles(params) {
172
+ const updated = await db.update(files).set({ refCount: sql`${files.refCount} - 1` }).where(inArray(files.id, params.ids)).returning({
173
+ id: files.id,
174
+ refCount: files.refCount
175
+ });
176
+ if (updated.length === 0) return;
177
+ const toDelete = updated.filter((file) => file.refCount < 1);
178
+ if (toDelete.length === 0) return;
179
+ await Promise.all(toDelete.map((file) => this.deleteFile({ id: file.id })));
137
180
  }
138
181
  };
139
182
  }
@@ -157,4 +200,4 @@ function createFilesServer(options) {
157
200
 
158
201
  //#endregion
159
202
  export { schema_exports as n, createFilesServer as t };
160
- //# sourceMappingURL=server-D1-_uiJo.mjs.map
203
+ //# sourceMappingURL=server-BKDrcvWV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-BKDrcvWV.mjs","names":[],"sources":["../src/server/schema.ts","../src/server/router.ts","../src/server/service.ts","../src/server/server.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\r\nimport { index, integer, pgTable, text, timestamp, pgEnum } from \"drizzle-orm/pg-core\";\r\n\r\nexport const files = pgTable(\"files\", {\r\n id: text(\"id\").primaryKey().$defaultFn(() => `file_${nanoid()}`),\r\n purpose: text(\"purpose\").notNull(),\r\n name: text(\"name\"),\r\n key: text(\"key\").notNull().unique(),\r\n size: integer(\"size\").notNull(),\r\n mimeType: text(\"mime_type\").notNull().default(\"application/octet-stream\"),\r\n refCount: integer(\"ref_count\").notNull().default(0),\r\n visibility: pgEnum(\"visibility\", [\"private\", \"public\"])().notNull().default(\"private\"),\r\n createdAt: timestamp(\"created_at\").notNull().defaultNow(),\r\n updatedAt: timestamp(\"updated_at\").notNull().defaultNow(),\r\n}, (table) => [\r\n index(\"files_key_idx\").on(table.key),\r\n index(\"files_created_at_idx\").on(table.createdAt),\r\n]);","import { Elysia, t } from \"elysia\";\r\nimport type { Service } from \"./service\";\r\n\r\nexport type Visibility = \"private\" | \"public\";\r\n\r\nexport interface PurposePolicy {\r\n maxSize?: number;\r\n allowedMimeTypes?: string[];\r\n visibility?: Visibility;\r\n}\r\n\r\nexport function createRouter<const TPurpose extends string>(\r\n options: {\r\n service: Service;\r\n policies: Record<TPurpose, PurposePolicy>\r\n }\r\n) {\r\n \r\n const { \r\n service, \r\n policies,\r\n } = options;\r\n\r\n return new Elysia({\r\n prefix: \"/api\",\r\n }).post(\"/files/upload\", async ({ status, body }) => {\r\n const { file, purpose } = body;\r\n const policy = policies[purpose as TPurpose];\r\n if (!policy) {\r\n return status(400, {\r\n message: \"Purpose not supported\",\r\n });\r\n }\r\n if (policy.maxSize !== undefined && file.size > policy.maxSize) {\r\n return status(400, {\r\n message: \"File size exceeds the maximum allowed size\",\r\n });\r\n }\r\n if (policy.allowedMimeTypes !== undefined && !policy.allowedMimeTypes.includes(file.type)) {\r\n return status(400, {\r\n message: \"File type not allowed\",\r\n });\r\n }\r\n const uploaded = await service.uploadFile({ \r\n file, \r\n purpose, \r\n visibility: policy.visibility ?? \"private\" \r\n });\r\n const url = await service.getUrl({ id: uploaded.id });\r\n return status(200, {\r\n url: url ?? \"\",\r\n id: uploaded.id,\r\n name: uploaded.name,\r\n key: uploaded.key,\r\n size: uploaded.size,\r\n mimeType: uploaded.mimeType,\r\n createdAt: uploaded.createdAt,\r\n });\r\n }, {\r\n body: UploadRequest(Object.keys(policies) as TPurpose[]),\r\n response: {\r\n 200: FileResponse,\r\n 400: ErrorResponse,\r\n 500: ErrorResponse,\r\n }\r\n });\r\n}\r\n\r\nfunction UploadRequest<const TPurpose extends string>(\r\n purposes: TPurpose[]\r\n) {\r\n return t.Object({\r\n file: t.File(),\r\n purpose: t.Union(\r\n purposes.map((p) => t.Literal(p)) as [\r\n ReturnType<typeof t.Literal<TPurpose>>,\r\n ...ReturnType<typeof t.Literal<TPurpose>>[],\r\n ]\r\n )\r\n });\r\n}\r\n\r\nconst ErrorResponse = t.Object({\r\n message: t.String(),\r\n});\r\n\r\nconst FileResponse = t.Object({\r\n url: t.String(),\r\n id: t.String(),\r\n name: t.Nullable(t.String()),\r\n key: t.String(),\r\n size: t.Number(),\r\n mimeType: t.String(),\r\n createdAt: t.Date(),\r\n});","import { S3Client, type S3Options } from \"bun\";\r\nimport { nanoid } from \"nanoid\";\r\nimport { extname } from \"path\";\r\nimport { eq, sql, and, inArray } from \"drizzle-orm\";\r\nimport type { PgDatabase } from \"drizzle-orm/pg-core\";\r\nimport { files } from \"./schema\";\r\n\r\nexport type Service = ReturnType<typeof createService>;\r\n\r\ntype ById = { id: string };\r\ntype ByIds = { ids: string[] };\r\n\r\nexport function createService(options: {\r\n db: PgDatabase<any, any, any>;\r\n s3: S3Options;\r\n keyPrefix?: string;\r\n presignExpiresIn?: number;\r\n}) {\r\n const { \r\n db, \r\n s3,\r\n keyPrefix = \"files\",\r\n presignExpiresIn = 3600\r\n } = options;\r\n \r\n const { endpoint } = s3;\r\n const s3Client = new S3Client(s3);\r\n\r\n return {\r\n\r\n async listFiles(params: ByIds): Promise<typeof files.$inferSelect[]> {\r\n return await db\r\n .select()\r\n .from(files)\r\n .where(inArray(files.id, params.ids))\r\n .limit(params.ids.length);\r\n },\r\n\r\n async getFile(params: ById): Promise<typeof files.$inferSelect | null> {\r\n const [found] = await db\r\n .select()\r\n .from(files)\r\n .where(eq(files.id, params.id))\r\n .limit(1);\r\n return found ?? null;\r\n },\r\n\r\n buildUrl(file: Pick<typeof files.$inferSelect, \"key\" | \"visibility\">): string {\r\n if (file.visibility === \"public\") {\r\n return `${endpoint}/${file.key}`;\r\n }\r\n return s3Client.presign(file.key, { \r\n expiresIn: presignExpiresIn,\r\n });\r\n },\r\n\r\n async getUrl(params: ById): Promise<string | null> {\r\n const [found] = await db\r\n .select({ \r\n key: files.key,\r\n visibility: files.visibility,\r\n })\r\n .from(files)\r\n .where(eq(files.id, params.id))\r\n .limit(1);\r\n if (!found) return null;\r\n return this.buildUrl(found);\r\n },\r\n\r\n async getUrls(params: ByIds): Promise<{ id: string; url: string | null }[]> {\r\n const rows = await db\r\n .select({\r\n id: files.id,\r\n key: files.key,\r\n visibility: files.visibility,\r\n })\r\n .from(files)\r\n .where(inArray(files.id, params.ids))\r\n .limit(params.ids.length);\r\n return params.ids.map(id => {\r\n const row = rows.find(row => row.id === id);\r\n if (!row) return { id, url: null };\r\n return { id, url: this.buildUrl(row) };\r\n });\r\n },\r\n\r\n async uploadFile(params: {\r\n file: File;\r\n purpose: string;\r\n visibility: \"private\" | \"public\";\r\n }): Promise<typeof files.$inferSelect> {\r\n const id = nanoid();\r\n const ext = extname(params.file.name);\r\n const key = [keyPrefix, params.purpose, `${id}${ext}`].join(\"/\");\r\n const s3file = s3Client.file(key);\r\n await s3file.write(params.file, {\r\n type: params.file.type,\r\n acl: params.visibility === \"public\" ? \"public-read\" : \"private\",\r\n contentDisposition: `attachment; filename=\"${params.file.name}\"`,\r\n });\r\n try {\r\n const [created] = await db.insert(files)\r\n .values({\r\n purpose: params.purpose,\r\n key,\r\n size: s3file.size,\r\n name: s3file.name ?? null,\r\n mimeType: s3file.type,\r\n visibility: params.visibility,\r\n })\r\n .returning();\r\n if (!created) {\r\n throw new Error(\"Failed to create file record\");\r\n }\r\n return created;\r\n } catch (error) {\r\n await s3file.delete().catch(() => {});\r\n throw error;\r\n }\r\n },\r\n\r\n async deleteFile(params: { id: string }): Promise<void> {\r\n const [deleted] = await db\r\n .delete(files)\r\n .where(eq(files.id, params.id))\r\n .returning();\r\n if (!deleted) return;\r\n await s3Client.delete(deleted.key);\r\n },\r\n\r\n async deleteFiles(params: ByIds): Promise<void> {\r\n const deleted = await db\r\n .delete(files)\r\n .where(inArray(files.id, params.ids))\r\n .returning({\r\n key: files.key,\r\n });\r\n\r\n if (deleted.length === 0) return;\r\n\r\n await Promise.all(\r\n deleted.map((file) => s3Client.delete(file.key)),\r\n );\r\n },\r\n\r\n async acquireFile(params: ById & { \r\n purpose?: string \r\n }): Promise<typeof files.$inferSelect> {\r\n const [updated] = await db\r\n .update(files)\r\n .set({ refCount: sql`${files.refCount} + 1` })\r\n .where(\r\n and(\r\n eq(files.id, params.id),\r\n params.purpose ? eq(files.purpose, params.purpose) : undefined,\r\n )\r\n )\r\n .returning();\r\n if (!updated) {\r\n throw new Error(\"File not found\");\r\n }\r\n return updated;\r\n },\r\n\r\n async acquireFiles(\r\n params: ByIds & { purpose?: string },\r\n ): Promise<typeof files.$inferSelect[]> {\r\n const updated = await db\r\n .update(files)\r\n .set({ refCount: sql`${files.refCount} + 1` })\r\n .where(\r\n and(\r\n inArray(files.id, params.ids),\r\n params.purpose ? eq(files.purpose, params.purpose) : undefined,\r\n ),\r\n )\r\n .returning();\r\n return updated;\r\n },\r\n\r\n async releaseFile(params: ById): Promise<void> {\r\n const [updated] = await db\r\n .update(files)\r\n .set({ refCount: sql`${files.refCount} - 1` })\r\n .where(eq(files.id, params.id))\r\n .returning();\r\n if (!updated) {\r\n throw new Error(\"File not found\");\r\n }\r\n if (updated.refCount < 1) {\r\n await this.deleteFile({ id: params.id });\r\n }\r\n },\r\n\r\n async releaseFiles(params: ByIds): Promise<void> {\r\n const updated = await db\r\n .update(files)\r\n .set({ refCount: sql`${files.refCount} - 1` })\r\n .where(inArray(files.id, params.ids))\r\n .returning({\r\n id: files.id,\r\n refCount: files.refCount,\r\n });\r\n if (updated.length === 0) return;\r\n const toDelete = updated.filter((file) => file.refCount < 1);\r\n if (toDelete.length === 0) return;\r\n await Promise.all(\r\n toDelete.map((file) => this.deleteFile({ id: file.id })),\r\n );\r\n },\r\n };\r\n}","import type { PgDatabase } from \"drizzle-orm/pg-core\";\r\nimport { createRouter, type PurposePolicy } from \"./router\";\r\nimport { createService, type Service } from \"./service\";\r\nimport type { S3Options } from \"bun\";\r\n\r\nexport interface FilesServerOptions<TPurposes extends string> {\r\n db: PgDatabase<any, any, any>;\r\n s3: S3Options;\r\n policies: Record<TPurposes, PurposePolicy>;\r\n presignExpiresIn?: number;\r\n}\r\n\r\nexport function createFilesServer<const TPurpose extends string>(\r\n options: FilesServerOptions<TPurpose>\r\n): {\r\n readonly router: ReturnType<typeof createRouter<TPurpose>>;\r\n readonly service: Service;\r\n} {\r\n const service = createService({\r\n db: options.db,\r\n s3: options.s3,\r\n presignExpiresIn: options.presignExpiresIn,\r\n });\r\n const router = createRouter({\r\n service,\r\n policies: options.policies,\r\n });\r\n return {\r\n router,\r\n service,\r\n };\r\n}"],"mappings":"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"}
@@ -190,14 +190,23 @@ type Service = ReturnType<typeof createService>;
190
190
  type ById = {
191
191
  id: string;
192
192
  };
193
+ type ByIds = {
194
+ ids: string[];
195
+ };
193
196
  declare function createService(options: {
194
197
  db: PgDatabase<any, any, any>;
195
198
  s3: S3Options;
196
199
  keyPrefix?: string;
197
200
  presignExpiresIn?: number;
198
201
  }): {
199
- getFile(params: ById): Promise<typeof files.$inferSelect>;
200
- getUrl(params: ById): Promise<string>;
202
+ listFiles(params: ByIds): Promise<(typeof files.$inferSelect)[]>;
203
+ getFile(params: ById): Promise<typeof files.$inferSelect | null>;
204
+ buildUrl(file: Pick<typeof files.$inferSelect, "key" | "visibility">): string;
205
+ getUrl(params: ById): Promise<string | null>;
206
+ getUrls(params: ByIds): Promise<{
207
+ id: string;
208
+ url: string | null;
209
+ }[]>;
201
210
  uploadFile(params: {
202
211
  file: File;
203
212
  purpose: string;
@@ -206,10 +215,15 @@ declare function createService(options: {
206
215
  deleteFile(params: {
207
216
  id: string;
208
217
  }): Promise<void>;
218
+ deleteFiles(params: ByIds): Promise<void>;
209
219
  acquireFile(params: ById & {
210
220
  purpose?: string;
211
221
  }): Promise<typeof files.$inferSelect>;
222
+ acquireFiles(params: ByIds & {
223
+ purpose?: string;
224
+ }): Promise<(typeof files.$inferSelect)[]>;
212
225
  releaseFile(params: ById): Promise<void>;
226
+ releaseFiles(params: ByIds): Promise<void>;
213
227
  };
214
228
  //#endregion
215
229
  //#region src/server/router.d.ts
@@ -257,6 +271,7 @@ declare function createRouter<const TPurpose extends string>(options: {
257
271
  size: number;
258
272
  mimeType: string;
259
273
  createdAt: Date;
274
+ url: string;
260
275
  };
261
276
  400: {
262
277
  message: string;
@@ -305,4 +320,4 @@ declare function createFilesServer<const TPurpose extends string>(options: Files
305
320
  };
306
321
  //#endregion
307
322
  export { schema_d_exports as a, Visibility as i, createFilesServer as n, PurposePolicy as r, FilesServerOptions as t };
308
- //# sourceMappingURL=server-Cxx9ToSf.d.mts.map
323
+ //# sourceMappingURL=server-CeZ5g-TS.d.mts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@edkstack/files",
3
3
  "description": "File management utilities for EDK Stack",
4
- "version": "0.1.12",
4
+ "version": "0.2.2",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-D1-_uiJo.mjs","names":[],"sources":["../src/server/schema.ts","../src/server/router.ts","../src/server/service.ts","../src/server/server.ts"],"sourcesContent":["import { nanoid } from \"nanoid\";\r\nimport { index, integer, pgTable, text, timestamp, pgEnum } from \"drizzle-orm/pg-core\";\r\n\r\nexport const files = pgTable(\"files\", {\r\n id: text(\"id\").primaryKey().$defaultFn(() => `file_${nanoid()}`),\r\n purpose: text(\"purpose\").notNull(),\r\n name: text(\"name\"),\r\n key: text(\"key\").notNull().unique(),\r\n size: integer(\"size\").notNull(),\r\n mimeType: text(\"mime_type\").notNull().default(\"application/octet-stream\"),\r\n refCount: integer(\"ref_count\").notNull().default(0),\r\n visibility: pgEnum(\"visibility\", [\"private\", \"public\"])().notNull().default(\"private\"),\r\n createdAt: timestamp(\"created_at\").notNull().defaultNow(),\r\n updatedAt: timestamp(\"updated_at\").notNull().defaultNow(),\r\n}, (table) => [\r\n index(\"files_key_idx\").on(table.key),\r\n index(\"files_created_at_idx\").on(table.createdAt),\r\n]);","import { Elysia, t } from \"elysia\";\r\nimport type { Service } from \"./service\";\r\n\r\nexport type Visibility = \"private\" | \"public\";\r\n\r\nexport interface PurposePolicy {\r\n maxSize?: number;\r\n allowedMimeTypes?: string[];\r\n visibility?: Visibility;\r\n}\r\n\r\nexport function createRouter<const TPurpose extends string>(\r\n options: {\r\n service: Service;\r\n policies: Record<TPurpose, PurposePolicy>\r\n }\r\n) {\r\n \r\n const { \r\n service, \r\n policies,\r\n } = options;\r\n\r\n return new Elysia({\r\n prefix: \"/api\",\r\n }).post(\"/files/upload\", async ({ status, body }) => {\r\n const { file, purpose } = body;\r\n const policy = policies[purpose as TPurpose];\r\n if (!policy) {\r\n return status(400, {\r\n message: \"Purpose not supported\",\r\n });\r\n }\r\n if (policy.maxSize !== undefined && file.size > policy.maxSize) {\r\n return status(400, {\r\n message: \"File size exceeds the maximum allowed size\",\r\n });\r\n }\r\n if (policy.allowedMimeTypes !== undefined && !policy.allowedMimeTypes.includes(file.type)) {\r\n return status(400, {\r\n message: \"File type not allowed\",\r\n });\r\n }\r\n const uploaded = await service.uploadFile({ \r\n file, \r\n purpose, \r\n visibility: policy.visibility ?? \"private\" \r\n });\r\n return status(200, {\r\n id: uploaded.id,\r\n name: uploaded.name,\r\n key: uploaded.key,\r\n size: uploaded.size,\r\n mimeType: uploaded.mimeType,\r\n createdAt: uploaded.createdAt,\r\n });\r\n }, {\r\n body: UploadRequest(Object.keys(policies) as TPurpose[]),\r\n response: {\r\n 200: FileResponse,\r\n 400: ErrorResponse,\r\n 500: ErrorResponse,\r\n }\r\n });\r\n}\r\n\r\nfunction UploadRequest<const TPurpose extends string>(\r\n purposes: TPurpose[]\r\n) {\r\n return t.Object({\r\n file: t.File(),\r\n purpose: t.Union(\r\n purposes.map((p) => t.Literal(p)) as [\r\n ReturnType<typeof t.Literal<TPurpose>>,\r\n ...ReturnType<typeof t.Literal<TPurpose>>[],\r\n ]\r\n )\r\n });\r\n}\r\n\r\nconst ErrorResponse = t.Object({\r\n message: t.String(),\r\n});\r\n\r\nconst FileResponse = t.Object({\r\n id: t.String(),\r\n name: t.Nullable(t.String()),\r\n key: t.String(),\r\n size: t.Number(),\r\n mimeType: t.String(),\r\n createdAt: t.Date(),\r\n});","import { S3Client, type S3Options } from \"bun\";\r\nimport { nanoid } from \"nanoid\";\r\nimport { extname } from \"path\";\r\nimport { eq, sql, and } from \"drizzle-orm\";\r\nimport type { PgDatabase } from \"drizzle-orm/pg-core\";\r\nimport { files } from \"./schema\";\r\n\r\nexport type Service = ReturnType<typeof createService>;\r\n\r\ntype ById = { id: string };\r\n\r\nexport function createService(options: {\r\n db: PgDatabase<any, any, any>;\r\n s3: S3Options;\r\n keyPrefix?: string;\r\n presignExpiresIn?: number;\r\n}) {\r\n const { \r\n db, \r\n s3: s3Options,\r\n keyPrefix = \"files\",\r\n presignExpiresIn = 3600\r\n } = options;\r\n \r\n const { endpoint } = s3Options;\r\n const s3Client = new S3Client(s3Options);\r\n\r\n return {\r\n\r\n async getFile(params: ById): Promise<typeof files.$inferSelect> {\r\n const [found] = await db\r\n .select()\r\n .from(files)\r\n .where(eq(files.id, params.id))\r\n .limit(1);\r\n if (!found) {\r\n throw new Error(\"File not found\");\r\n }\r\n return found;\r\n },\r\n\r\n async getUrl(params: ById): Promise<string> {\r\n const [found] = await db\r\n .select({ \r\n key: files.key,\r\n visibility: files.visibility,\r\n })\r\n .from(files)\r\n .where(eq(files.id, params.id))\r\n .limit(1);\r\n if (!found) {\r\n throw new Error(\"File not found\");\r\n }\r\n if (found.visibility === \"public\") {\r\n return `${endpoint}/${found.key}`;\r\n }\r\n return s3Client.presign(found.key, { expiresIn: presignExpiresIn });\r\n },\r\n\r\n async uploadFile(params: {\r\n file: File;\r\n purpose: string;\r\n visibility: \"private\" | \"public\";\r\n }): Promise<typeof files.$inferSelect> {\r\n const id = nanoid();\r\n const ext = extname(params.file.name);\r\n const key = [keyPrefix, params.visibility, params.purpose, `${id}${ext}`].join(\"/\");\r\n const s3file = s3Client.file(key);\r\n await s3file.write(params.file, {\r\n type: params.file.type,\r\n acl: params.visibility === \"public\" ? \"public-read\" : \"private\",\r\n });\r\n try {\r\n const [created] = await db.insert(files)\r\n .values({\r\n purpose: params.purpose,\r\n key,\r\n size: s3file.size,\r\n name: s3file.name ?? null,\r\n mimeType: s3file.type,\r\n visibility: params.visibility,\r\n })\r\n .returning();\r\n if (!created) {\r\n throw new Error(\"Failed to create file record\");\r\n }\r\n return created;\r\n } catch (error) {\r\n await s3file.delete().catch();\r\n throw error;\r\n }\r\n },\r\n\r\n async deleteFile(params: { id: string }): Promise<void> {\r\n const [deleted] = await db\r\n .delete(files)\r\n .where(eq(files.id, params.id))\r\n .returning();\r\n if (!deleted) return;\r\n await s3Client.delete(deleted.key);\r\n },\r\n\r\n async acquireFile(params: ById & { \r\n purpose?: string \r\n }): Promise<typeof files.$inferSelect> {\r\n const [updated] = await db\r\n .update(files)\r\n .set({ refCount: sql`${files.refCount} + 1` })\r\n .where(\r\n and(\r\n eq(files.id, params.id),\r\n params.purpose ? eq(files.purpose, params.purpose) : undefined,\r\n )\r\n )\r\n .returning();\r\n if (!updated) {\r\n throw new Error(\"File not found\");\r\n }\r\n return updated;\r\n },\r\n\r\n async releaseFile(params: ById): Promise<void> {\r\n const [updated] = await db\r\n .update(files)\r\n .set({ refCount: sql`${files.refCount} - 1` })\r\n .where(eq(files.id, params.id))\r\n .returning();\r\n if (!updated) {\r\n throw new Error(\"File not found\");\r\n }\r\n if (updated.refCount < 1) {\r\n await this.deleteFile({ id: params.id });\r\n }\r\n },\r\n };\r\n}","import type { PgDatabase } from \"drizzle-orm/pg-core\";\r\nimport { createRouter, type PurposePolicy } from \"./router\";\r\nimport { createService, type Service } from \"./service\";\r\nimport type { S3Options } from \"bun\";\r\n\r\nexport interface FilesServerOptions<TPurposes extends string> {\r\n db: PgDatabase<any, any, any>;\r\n s3: S3Options;\r\n policies: Record<TPurposes, PurposePolicy>;\r\n presignExpiresIn?: number;\r\n}\r\n\r\nexport function createFilesServer<const TPurpose extends string>(\r\n options: FilesServerOptions<TPurpose>\r\n): {\r\n readonly router: ReturnType<typeof createRouter<TPurpose>>;\r\n readonly service: Service;\r\n} {\r\n const service = createService({\r\n db: options.db,\r\n s3: options.s3,\r\n presignExpiresIn: options.presignExpiresIn,\r\n });\r\n const router = createRouter({\r\n service,\r\n policies: options.policies,\r\n });\r\n return {\r\n router,\r\n service,\r\n };\r\n}"],"mappings":"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"}