@edkstack/files 0.2.2 → 0.2.3

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-CeZ5g-TS.mjs";
1
+ import { n as createFilesServer } from "../server-DT8zI0-g.mjs";
2
2
  import "../server/index.mjs";
3
3
  import { ReactNode } from "react";
4
4
  import * as react_jsx_runtime0 from "react/jsx-runtime";
@@ -31,7 +31,7 @@ declare function useFilesClient(): {
31
31
  200: {
32
32
  id: string;
33
33
  name: string | null;
34
- key: string;
34
+ purpose: string;
35
35
  size: number;
36
36
  mimeType: string;
37
37
  createdAt: Date;
@@ -62,7 +62,7 @@ declare function useFilesClient(): {
62
62
  declare function useUpload(options: InferMutationOptions<FilesClient["api"]["files"]["upload"]["post"]>): _tanstack_react_query0.UseMutationResult<{
63
63
  id: string;
64
64
  name: string | null;
65
- key: string;
65
+ purpose: string;
66
66
  size: number;
67
67
  mimeType: string;
68
68
  createdAt: Date;
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-CeZ5g-TS.mjs";
1
+ import { a as schema_d_exports, i as Visibility, n as createFilesServer, r as PurposePolicy, t as FilesServerOptions } from "./server-DT8zI0-g.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-BKDrcvWV.mjs";
1
+ import { n as schema_exports, t as createFilesServer } from "./server-CVIdVIq6.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-CeZ5g-TS.mjs";
1
+ import { a as schema_d_exports, i as Visibility, n as createFilesServer, r as PurposePolicy, t as FilesServerOptions } from "../server-DT8zI0-g.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-BKDrcvWV.mjs";
1
+ import { n as schema_exports, t as createFilesServer } from "../server-CVIdVIq6.mjs";
2
2
 
3
3
  export { createFilesServer, schema_exports as schema };
@@ -37,10 +37,10 @@ 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
+ url: uploaded.url,
41
41
  id: uploaded.id,
42
42
  name: uploaded.name,
43
- key: uploaded.key,
43
+ purpose: uploaded.purpose,
44
44
  size: uploaded.size,
45
45
  mimeType: uploaded.mimeType,
46
46
  createdAt: uploaded.createdAt
@@ -65,7 +65,7 @@ const FileResponse = t.Object({
65
65
  url: t.String(),
66
66
  id: t.String(),
67
67
  name: t.Nullable(t.String()),
68
- key: t.String(),
68
+ purpose: t.String(),
69
69
  size: t.Number(),
70
70
  mimeType: t.String(),
71
71
  createdAt: t.Date()
@@ -79,11 +79,18 @@ function createService(options) {
79
79
  const s3Client = new S3Client(s3);
80
80
  return {
81
81
  async listFiles(params) {
82
- return await db.select().from(files).where(inArray(files.id, params.ids)).limit(params.ids.length);
82
+ return (await db.select().from(files).where(inArray(files.id, params.ids)).limit(params.ids.length)).map((item) => ({
83
+ ...item,
84
+ url: this.buildUrl(item)
85
+ }));
83
86
  },
84
87
  async getFile(params) {
85
88
  const [found] = await db.select().from(files).where(eq(files.id, params.id)).limit(1);
86
- return found ?? null;
89
+ if (!found) return null;
90
+ return {
91
+ ...found,
92
+ url: this.buildUrl(found)
93
+ };
87
94
  },
88
95
  buildUrl(file) {
89
96
  if (file.visibility === "public") return `${endpoint}/${file.key}`;
@@ -124,10 +131,11 @@ function createService(options) {
124
131
  `${id}${ext}`
125
132
  ].join("/");
126
133
  const s3file = s3Client.file(key);
134
+ const filename = params.keepFileName === true ? params.file.name : params.keepFileName;
127
135
  await s3file.write(params.file, {
128
136
  type: params.file.type,
129
137
  acl: params.visibility === "public" ? "public-read" : "private",
130
- contentDisposition: `attachment; filename="${params.file.name}"`
138
+ contentDisposition: filename ? `attachment; filename="${filename}"` : void 0
131
139
  });
132
140
  try {
133
141
  const [created] = await db.insert(files).values({
@@ -139,7 +147,10 @@ function createService(options) {
139
147
  visibility: params.visibility
140
148
  }).returning();
141
149
  if (!created) throw new Error("Failed to create file record");
142
- return created;
150
+ return {
151
+ ...created,
152
+ url: this.buildUrl(created)
153
+ };
143
154
  } catch (error) {
144
155
  await s3file.delete().catch(() => {});
145
156
  throw error;
@@ -158,10 +169,16 @@ function createService(options) {
158
169
  async acquireFile(params) {
159
170
  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();
160
171
  if (!updated) throw new Error("File not found");
161
- return updated;
172
+ return {
173
+ ...updated,
174
+ url: this.buildUrl(updated)
175
+ };
162
176
  },
163
177
  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();
178
+ 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()).map((item) => ({
179
+ ...item,
180
+ url: this.buildUrl(item)
181
+ }));
165
182
  },
166
183
  async releaseFile(params) {
167
184
  const [updated] = await db.update(files).set({ refCount: sql`${files.refCount} - 1` }).where(eq(files.id, params.id)).returning();
@@ -200,4 +217,4 @@ function createFilesServer(options) {
200
217
 
201
218
  //#endregion
202
219
  export { schema_exports as n, createFilesServer as t };
203
- //# sourceMappingURL=server-BKDrcvWV.mjs.map
220
+ //# sourceMappingURL=server-CVIdVIq6.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-CVIdVIq6.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 url: uploaded.url,\r\n id: uploaded.id,\r\n name: uploaded.name,\r\n purpose: uploaded.purpose,\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 purpose: 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 type FileWithUrl = typeof files.$inferSelect & { url: 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<FileWithUrl[]> {\r\n const items = 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 return items.map(item => ({\r\n ...item,\r\n url: this.buildUrl(item),\r\n }));\r\n },\r\n\r\n async getFile(params: ById): Promise<FileWithUrl | 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 if (!found) return null;\r\n return {\r\n ...found,\r\n url: this.buildUrl(found),\r\n };\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 keepFileName?: boolean | string;\r\n }): Promise<FileWithUrl> {\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 const filename = params.keepFileName === true ? params.file.name : params.keepFileName;\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: filename ? `attachment; filename=\"${filename}\"` : undefined,\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 {\r\n ...created,\r\n url: this.buildUrl(created),\r\n };\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 if (deleted.length === 0) return;\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<FileWithUrl> {\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 {\r\n ...updated,\r\n url: this.buildUrl(updated),\r\n };\r\n },\r\n\r\n async acquireFiles(\r\n params: ByIds & { purpose?: string },\r\n ): Promise<FileWithUrl[]> {\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.map(item => ({\r\n ...item,\r\n url: this.buildUrl(item),\r\n }));\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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"}
@@ -193,14 +193,17 @@ type ById = {
193
193
  type ByIds = {
194
194
  ids: string[];
195
195
  };
196
+ type FileWithUrl = typeof files.$inferSelect & {
197
+ url: string;
198
+ };
196
199
  declare function createService(options: {
197
200
  db: PgDatabase<any, any, any>;
198
201
  s3: S3Options;
199
202
  keyPrefix?: string;
200
203
  presignExpiresIn?: number;
201
204
  }): {
202
- listFiles(params: ByIds): Promise<(typeof files.$inferSelect)[]>;
203
- getFile(params: ById): Promise<typeof files.$inferSelect | null>;
205
+ listFiles(params: ByIds): Promise<FileWithUrl[]>;
206
+ getFile(params: ById): Promise<FileWithUrl | null>;
204
207
  buildUrl(file: Pick<typeof files.$inferSelect, "key" | "visibility">): string;
205
208
  getUrl(params: ById): Promise<string | null>;
206
209
  getUrls(params: ByIds): Promise<{
@@ -211,17 +214,18 @@ declare function createService(options: {
211
214
  file: File;
212
215
  purpose: string;
213
216
  visibility: "private" | "public";
214
- }): Promise<typeof files.$inferSelect>;
217
+ keepFileName?: boolean | string;
218
+ }): Promise<FileWithUrl>;
215
219
  deleteFile(params: {
216
220
  id: string;
217
221
  }): Promise<void>;
218
222
  deleteFiles(params: ByIds): Promise<void>;
219
223
  acquireFile(params: ById & {
220
224
  purpose?: string;
221
- }): Promise<typeof files.$inferSelect>;
225
+ }): Promise<FileWithUrl>;
222
226
  acquireFiles(params: ByIds & {
223
227
  purpose?: string;
224
- }): Promise<(typeof files.$inferSelect)[]>;
228
+ }): Promise<FileWithUrl[]>;
225
229
  releaseFile(params: ById): Promise<void>;
226
230
  releaseFiles(params: ByIds): Promise<void>;
227
231
  };
@@ -267,7 +271,7 @@ declare function createRouter<const TPurpose extends string>(options: {
267
271
  200: {
268
272
  id: string;
269
273
  name: string | null;
270
- key: string;
274
+ purpose: string;
271
275
  size: number;
272
276
  mimeType: string;
273
277
  createdAt: Date;
@@ -320,4 +324,4 @@ declare function createFilesServer<const TPurpose extends string>(options: Files
320
324
  };
321
325
  //#endregion
322
326
  export { schema_d_exports as a, Visibility as i, createFilesServer as n, PurposePolicy as r, FilesServerOptions as t };
323
- //# sourceMappingURL=server-CeZ5g-TS.d.mts.map
327
+ //# sourceMappingURL=server-DT8zI0-g.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.2.2",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -1 +0,0 @@
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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"}