@edkstack/files 0.2.1 → 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-C_G1kBYN.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-C_G1kBYN.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-B1B4vJ67.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-C_G1kBYN.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-B1B4vJ67.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}`;
@@ -103,9 +110,17 @@ function createService(options) {
103
110
  key: files.key,
104
111
  visibility: files.visibility
105
112
  }).from(files).where(inArray(files.id, params.ids)).limit(params.ids.length);
106
- const urls = {};
107
- for (const row of rows) urls[row.id] = this.buildUrl(row);
108
- return urls;
113
+ return params.ids.map((id) => {
114
+ const row = rows.find((row) => row.id === id);
115
+ if (!row) return {
116
+ id,
117
+ url: null
118
+ };
119
+ return {
120
+ id,
121
+ url: this.buildUrl(row)
122
+ };
123
+ });
109
124
  },
110
125
  async uploadFile(params) {
111
126
  const id = nanoid();
@@ -116,10 +131,11 @@ function createService(options) {
116
131
  `${id}${ext}`
117
132
  ].join("/");
118
133
  const s3file = s3Client.file(key);
134
+ const filename = params.keepFileName === true ? params.file.name : params.keepFileName;
119
135
  await s3file.write(params.file, {
120
136
  type: params.file.type,
121
137
  acl: params.visibility === "public" ? "public-read" : "private",
122
- contentDisposition: `attachment; filename="${params.file.name}"`
138
+ contentDisposition: filename ? `attachment; filename="${filename}"` : void 0
123
139
  });
124
140
  try {
125
141
  const [created] = await db.insert(files).values({
@@ -131,7 +147,10 @@ function createService(options) {
131
147
  visibility: params.visibility
132
148
  }).returning();
133
149
  if (!created) throw new Error("Failed to create file record");
134
- return created;
150
+ return {
151
+ ...created,
152
+ url: this.buildUrl(created)
153
+ };
135
154
  } catch (error) {
136
155
  await s3file.delete().catch(() => {});
137
156
  throw error;
@@ -150,10 +169,16 @@ function createService(options) {
150
169
  async acquireFile(params) {
151
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();
152
171
  if (!updated) throw new Error("File not found");
153
- return updated;
172
+ return {
173
+ ...updated,
174
+ url: this.buildUrl(updated)
175
+ };
154
176
  },
155
177
  async acquireFiles(params) {
156
- 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
+ }));
157
182
  },
158
183
  async releaseFile(params) {
159
184
  const [updated] = await db.update(files).set({ refCount: sql`${files.refCount} - 1` }).where(eq(files.id, params.id)).returning();
@@ -192,4 +217,4 @@ function createFilesServer(options) {
192
217
 
193
218
  //#endregion
194
219
  export { schema_exports as n, createFilesServer as t };
195
- //# sourceMappingURL=server-B1B4vJ67.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":";;;;;;;;;;AAGA,MAAa,QAAQ,QAAQ,SAAS;CACpC,IAAI,KAAK,KAAK,CAAC,YAAY,CAAC,iBAAiB,QAAQ,QAAQ,GAAG;CAChE,SAAS,KAAK,UAAU,CAAC,SAAS;CAClC,MAAM,KAAK,OAAO;CAClB,KAAK,KAAK,MAAM,CAAC,SAAS,CAAC,QAAQ;CACnC,MAAM,QAAQ,OAAO,CAAC,SAAS;CAC/B,UAAU,KAAK,YAAY,CAAC,SAAS,CAAC,QAAQ,2BAA2B;CACzE,UAAU,QAAQ,YAAY,CAAC,SAAS,CAAC,QAAQ,EAAE;CACnD,YAAY,OAAO,cAAc,CAAC,WAAW,SAAS,CAAC,EAAE,CAAC,SAAS,CAAC,QAAQ,UAAU;CACtF,WAAW,UAAU,aAAa,CAAC,SAAS,CAAC,YAAY;CACzD,WAAW,UAAU,aAAa,CAAC,SAAS,CAAC,YAAY;CAC1D,GAAG,UAAU,CACZ,MAAM,gBAAgB,CAAC,GAAG,MAAM,IAAI,EACpC,MAAM,uBAAuB,CAAC,GAAG,MAAM,UAAU,CAClD,CAAC;;;;ACNF,SAAgB,aACd,SAIA;CAEA,MAAM,EACJ,SACA,aACE;AAEJ,QAAO,IAAI,OAAO,EAChB,QAAQ,QACT,CAAC,CAAC,KAAK,iBAAiB,OAAO,EAAE,QAAQ,WAAW;EACnD,MAAM,EAAE,MAAM,YAAY;EAC1B,MAAM,SAAS,SAAS;AACxB,MAAI,CAAC,OACH,QAAO,OAAO,KAAK,EACjB,SAAS,yBACV,CAAC;AAEJ,MAAI,OAAO,YAAY,UAAa,KAAK,OAAO,OAAO,QACrD,QAAO,OAAO,KAAK,EACjB,SAAS,8CACV,CAAC;AAEJ,MAAI,OAAO,qBAAqB,UAAa,CAAC,OAAO,iBAAiB,SAAS,KAAK,KAAK,CACvF,QAAO,OAAO,KAAK,EACjB,SAAS,yBACV,CAAC;EAEJ,MAAM,WAAW,MAAM,QAAQ,WAAW;GACxC;GACA;GACA,YAAY,OAAO,cAAc;GAClC,CAAC;AACF,SAAO,OAAO,KAAK;GACjB,KAAK,SAAS;GACd,IAAI,SAAS;GACb,MAAM,SAAS;GACf,SAAS,SAAS;GAClB,MAAM,SAAS;GACf,UAAU,SAAS;GACnB,WAAW,SAAS;GACrB,CAAC;IACD;EACD,MAAM,cAAc,OAAO,KAAK,SAAS,CAAe;EACxD,UAAU;GACR,KAAK;GACL,KAAK;GACL,KAAK;GACN;EACF,CAAC;;AAGJ,SAAS,cACP,UACA;AACA,QAAO,EAAE,OAAO;EACd,MAAM,EAAE,MAAM;EACd,SAAS,EAAE,MACT,SAAS,KAAK,MAAM,EAAE,QAAQ,EAAE,CAAC,CAIlC;EACF,CAAC;;AAGJ,MAAM,gBAAgB,EAAE,OAAO,EAC7B,SAAS,EAAE,QAAQ,EACpB,CAAC;AAEF,MAAM,eAAe,EAAE,OAAO;CAC5B,KAAK,EAAE,QAAQ;CACf,IAAI,EAAE,QAAQ;CACd,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;CAC5B,SAAS,EAAE,QAAQ;CACnB,MAAM,EAAE,QAAQ;CAChB,UAAU,EAAE,QAAQ;CACpB,WAAW,EAAE,MAAM;CACpB,CAAC;;;;AC/EF,SAAgB,cAAc,SAK3B;CACD,MAAM,EACJ,IACA,IACA,YAAY,SACZ,mBAAmB,SACjB;CAEJ,MAAM,EAAE,aAAa;CACrB,MAAM,WAAW,IAAI,SAAS,GAAG;AAEjC,QAAO;EAEL,MAAM,UAAU,QAAuC;AAMrD,WALc,MAAM,GACjB,QAAQ,CACR,KAAK,MAAM,CACX,MAAM,QAAQ,MAAM,IAAI,OAAO,IAAI,CAAC,CACpC,MAAM,OAAO,IAAI,OAAO,EACd,KAAI,UAAS;IACxB,GAAG;IACH,KAAK,KAAK,SAAS,KAAK;IACzB,EAAE;;EAGL,MAAM,QAAQ,QAA2C;GACvD,MAAM,CAAC,SAAS,MAAM,GACnB,QAAQ,CACR,KAAK,MAAM,CACX,MAAM,GAAG,MAAM,IAAI,OAAO,GAAG,CAAC,CAC9B,MAAM,EAAE;AACX,OAAI,CAAC,MAAO,QAAO;AACnB,UAAO;IACL,GAAG;IACH,KAAK,KAAK,SAAS,MAAM;IAC1B;;EAGH,SAAS,MAAqE;AAC5E,OAAI,KAAK,eAAe,SACtB,QAAO,GAAG,SAAS,GAAG,KAAK;AAE7B,UAAO,SAAS,QAAQ,KAAK,KAAK,EAChC,WAAW,kBACZ,CAAC;;EAGJ,MAAM,OAAO,QAAsC;GACjD,MAAM,CAAC,SAAS,MAAM,GACnB,OAAO;IACN,KAAK,MAAM;IACX,YAAY,MAAM;IACnB,CAAC,CACD,KAAK,MAAM,CACX,MAAM,GAAG,MAAM,IAAI,OAAO,GAAG,CAAC,CAC9B,MAAM,EAAE;AACX,OAAI,CAAC,MAAO,QAAO;AACnB,UAAO,KAAK,SAAS,MAAM;;EAG7B,MAAM,QAAQ,QAA8D;GAC1E,MAAM,OAAO,MAAM,GAChB,OAAO;IACN,IAAI,MAAM;IACV,KAAK,MAAM;IACX,YAAY,MAAM;IACnB,CAAC,CACD,KAAK,MAAM,CACX,MAAM,QAAQ,MAAM,IAAI,OAAO,IAAI,CAAC,CACpC,MAAM,OAAO,IAAI,OAAO;AAC3B,UAAO,OAAO,IAAI,KAAI,OAAM;IAC1B,MAAM,MAAM,KAAK,MAAK,QAAO,IAAI,OAAO,GAAG;AAC3C,QAAI,CAAC,IAAK,QAAO;KAAE;KAAI,KAAK;KAAM;AAClC,WAAO;KAAE;KAAI,KAAK,KAAK,SAAS,IAAI;KAAE;KACtC;;EAGJ,MAAM,WAAW,QAKQ;GACvB,MAAM,KAAK,QAAQ;GACnB,MAAM,MAAM,QAAQ,OAAO,KAAK,KAAK;GACrC,MAAM,MAAM;IAAC;IAAW,OAAO;IAAS,GAAG,KAAK;IAAM,CAAC,KAAK,IAAI;GAChE,MAAM,SAAS,SAAS,KAAK,IAAI;GACjC,MAAM,WAAW,OAAO,iBAAiB,OAAO,OAAO,KAAK,OAAO,OAAO;AAC1E,SAAM,OAAO,MAAM,OAAO,MAAM;IAC9B,MAAM,OAAO,KAAK;IAClB,KAAK,OAAO,eAAe,WAAW,gBAAgB;IACtD,oBAAoB,WAAW,yBAAyB,SAAS,KAAK;IACvE,CAAC;AACF,OAAI;IACF,MAAM,CAAC,WAAW,MAAM,GAAG,OAAO,MAAM,CACrC,OAAO;KACN,SAAS,OAAO;KAChB;KACA,MAAM,OAAO;KACb,MAAM,OAAO,QAAQ;KACrB,UAAU,OAAO;KACjB,YAAY,OAAO;KACpB,CAAC,CACD,WAAW;AACd,QAAI,CAAC,QACH,OAAM,IAAI,MAAM,+BAA+B;AAEjD,WAAO;KACL,GAAG;KACH,KAAK,KAAK,SAAS,QAAQ;KAC5B;YACM,OAAO;AACd,UAAM,OAAO,QAAQ,CAAC,YAAY,GAAG;AACrC,UAAM;;;EAIV,MAAM,WAAW,QAAuC;GACtD,MAAM,CAAC,WAAW,MAAM,GACrB,OAAO,MAAM,CACb,MAAM,GAAG,MAAM,IAAI,OAAO,GAAG,CAAC,CAC9B,WAAW;AACd,OAAI,CAAC,QAAS;AACd,SAAM,SAAS,OAAO,QAAQ,IAAI;;EAGpC,MAAM,YAAY,QAA8B;GAC9C,MAAM,UAAU,MAAM,GACnB,OAAO,MAAM,CACb,MAAM,QAAQ,MAAM,IAAI,OAAO,IAAI,CAAC,CACpC,UAAU,EACT,KAAK,MAAM,KACZ,CAAC;AACJ,OAAI,QAAQ,WAAW,EAAG;AAC1B,SAAM,QAAQ,IACZ,QAAQ,KAAK,SAAS,SAAS,OAAO,KAAK,IAAI,CAAC,CACjD;;EAGH,MAAM,YAAY,QAEO;GACvB,MAAM,CAAC,WAAW,MAAM,GACrB,OAAO,MAAM,CACb,IAAI,EAAE,UAAU,GAAG,GAAG,MAAM,SAAS,OAAO,CAAC,CAC7C,MACC,IACE,GAAG,MAAM,IAAI,OAAO,GAAG,EACvB,OAAO,UAAU,GAAG,MAAM,SAAS,OAAO,QAAQ,GAAG,OACtD,CACF,CACA,WAAW;AACd,OAAI,CAAC,QACH,OAAM,IAAI,MAAM,iBAAiB;AAEnC,UAAO;IACL,GAAG;IACH,KAAK,KAAK,SAAS,QAAQ;IAC5B;;EAGH,MAAM,aACJ,QACwB;AAWxB,WAVgB,MAAM,GACnB,OAAO,MAAM,CACb,IAAI,EAAE,UAAU,GAAG,GAAG,MAAM,SAAS,OAAO,CAAC,CAC7C,MACC,IACE,QAAQ,MAAM,IAAI,OAAO,IAAI,EAC7B,OAAO,UAAU,GAAG,MAAM,SAAS,OAAO,QAAQ,GAAG,OACtD,CACF,CACA,WAAW,EACC,KAAI,UAAS;IAC1B,GAAG;IACH,KAAK,KAAK,SAAS,KAAK;IACzB,EAAE;;EAGL,MAAM,YAAY,QAA6B;GAC7C,MAAM,CAAC,WAAW,MAAM,GACrB,OAAO,MAAM,CACb,IAAI,EAAE,UAAU,GAAG,GAAG,MAAM,SAAS,OAAO,CAAC,CAC7C,MAAM,GAAG,MAAM,IAAI,OAAO,GAAG,CAAC,CAC9B,WAAW;AACd,OAAI,CAAC,QACH,OAAM,IAAI,MAAM,iBAAiB;AAEnC,OAAI,QAAQ,WAAW,EACrB,OAAM,KAAK,WAAW,EAAE,IAAI,OAAO,IAAI,CAAC;;EAI5C,MAAM,aAAa,QAA8B;GAC/C,MAAM,UAAU,MAAM,GACnB,OAAO,MAAM,CACb,IAAI,EAAE,UAAU,GAAG,GAAG,MAAM,SAAS,OAAO,CAAC,CAC7C,MAAM,QAAQ,MAAM,IAAI,OAAO,IAAI,CAAC,CACpC,UAAU;IACT,IAAI,MAAM;IACV,UAAU,MAAM;IACjB,CAAC;AACJ,OAAI,QAAQ,WAAW,EAAG;GAC1B,MAAM,WAAW,QAAQ,QAAQ,SAAS,KAAK,WAAW,EAAE;AAC5D,OAAI,SAAS,WAAW,EAAG;AAC3B,SAAM,QAAQ,IACZ,SAAS,KAAK,SAAS,KAAK,WAAW,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CACzD;;EAEJ;;;;;ACzNH,SAAgB,kBACd,SAIA;CACA,MAAM,UAAU,cAAc;EAC5B,IAAI,QAAQ;EACZ,IAAI,QAAQ;EACZ,kBAAkB,QAAQ;EAC3B,CAAC;AAKF,QAAO;EACL,QALa,aAAa;GAC1B;GACA,UAAU,QAAQ;GACnB,CAAC;EAGA;EACD"}
@@ -193,32 +193,39 @@ 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
- getUrls(params: ByIds): Promise<Record<string, string>>;
209
+ getUrls(params: ByIds): Promise<{
210
+ id: string;
211
+ url: string | null;
212
+ }[]>;
207
213
  uploadFile(params: {
208
214
  file: File;
209
215
  purpose: string;
210
216
  visibility: "private" | "public";
211
- }): Promise<typeof files.$inferSelect>;
217
+ keepFileName?: boolean | string;
218
+ }): Promise<FileWithUrl>;
212
219
  deleteFile(params: {
213
220
  id: string;
214
221
  }): Promise<void>;
215
222
  deleteFiles(params: ByIds): Promise<void>;
216
223
  acquireFile(params: ById & {
217
224
  purpose?: string;
218
- }): Promise<typeof files.$inferSelect>;
225
+ }): Promise<FileWithUrl>;
219
226
  acquireFiles(params: ByIds & {
220
227
  purpose?: string;
221
- }): Promise<(typeof files.$inferSelect)[]>;
228
+ }): Promise<FileWithUrl[]>;
222
229
  releaseFile(params: ById): Promise<void>;
223
230
  releaseFiles(params: ByIds): Promise<void>;
224
231
  };
@@ -264,7 +271,7 @@ declare function createRouter<const TPurpose extends string>(options: {
264
271
  200: {
265
272
  id: string;
266
273
  name: string | null;
267
- key: string;
274
+ purpose: string;
268
275
  size: number;
269
276
  mimeType: string;
270
277
  createdAt: Date;
@@ -317,4 +324,4 @@ declare function createFilesServer<const TPurpose extends string>(options: Files
317
324
  };
318
325
  //#endregion
319
326
  export { schema_d_exports as a, Visibility as i, createFilesServer as n, PurposePolicy as r, FilesServerOptions as t };
320
- //# sourceMappingURL=server-C_G1kBYN.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.1",
4
+ "version": "0.2.3",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-B1B4vJ67.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<Record<string, string>> {\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 const urls: Record<string, string> = {};\r\n for (const row of rows) {\r\n urls[row.id] = this.buildUrl(row);\r\n }\r\n return urls;\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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"}