@edkstack/files 0.1.11 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client/index.d.mts +4 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/server/index.d.mts +2 -2
- package/dist/server/index.mjs +2 -2
- package/dist/{server-BDKHu5Yk.mjs → server-B1B4vJ67.mjs} +79 -42
- package/dist/server-B1B4vJ67.mjs.map +1 -0
- package/dist/{server-DYtdHDUx.d.mts → server-C_G1kBYN.d.mts} +34 -20
- package/package.json +1 -1
- package/dist/server-BDKHu5Yk.mjs.map +0 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) {
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (!no_symbols) {
|
|
12
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
}
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { __exportAll as t };
|
package/dist/client/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as createFilesServer } from "../server-
|
|
1
|
+
import { n as createFilesServer } from "../server-C_G1kBYN.mjs";
|
|
2
2
|
import "../server/index.mjs";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
@@ -8,7 +8,7 @@ import * as _tanstack_react_query0 from "@tanstack/react-query";
|
|
|
8
8
|
import { InferMutationOptions } from "eden2query";
|
|
9
9
|
|
|
10
10
|
//#region src/client/client.d.ts
|
|
11
|
-
declare function createFilesClient<TServer extends ReturnType<typeof createFilesServer<any>>>(domain: string, config?: Treaty.Config): Treaty.Create<TServer["
|
|
11
|
+
declare function createFilesClient<TServer extends ReturnType<typeof createFilesServer<any>>>(domain: string, config?: Treaty.Config): Treaty.Create<TServer["router"]>;
|
|
12
12
|
type FilesClient = ReturnType<typeof createFilesClient>;
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/client/provider.d.ts
|
|
@@ -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: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/client/provider.tsx","../../src/client/client.ts","../../src/client/hooks.ts"],"sourcesContent":["import { createContext, useContext, type ReactNode } from \"react\";\r\nimport type { FilesClient } from \"./client\";\r\n\r\n\r\nconst FilesClientContext = createContext<FilesClient | null>(null);\r\n\r\nexport function FilesClientProvider(props: {\r\n filesClient: FilesClient;\r\n children: ReactNode;\r\n}) {\r\n return (\r\n <FilesClientContext.Provider value={props.filesClient}>\r\n {props.children}\r\n </FilesClientContext.Provider>\r\n );\r\n}\r\n\r\nexport function useFilesClient() {\r\n const context = useContext(FilesClientContext);\r\n if (!context) {\r\n throw new Error(\"useFilesClient must be used within a FilesClientProvider\");\r\n }\r\n return context;\r\n}\r\n","import { treaty, type Treaty } from \"@elysiajs/eden\";\r\nimport type { createFilesServer } from \"../server\";\r\n\r\nexport function createFilesClient<\r\n TServer extends ReturnType<typeof createFilesServer<any>>\r\n>(\r\n domain: string,\r\n config?: Treaty.Config\r\n): Treaty.Create<TServer[\"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/client/provider.tsx","../../src/client/client.ts","../../src/client/hooks.ts"],"sourcesContent":["import { createContext, useContext, type ReactNode } from \"react\";\r\nimport type { FilesClient } from \"./client\";\r\n\r\n\r\nconst FilesClientContext = createContext<FilesClient | null>(null);\r\n\r\nexport function FilesClientProvider(props: {\r\n filesClient: FilesClient;\r\n children: ReactNode;\r\n}) {\r\n return (\r\n <FilesClientContext.Provider value={props.filesClient}>\r\n {props.children}\r\n </FilesClientContext.Provider>\r\n );\r\n}\r\n\r\nexport function useFilesClient() {\r\n const context = useContext(FilesClientContext);\r\n if (!context) {\r\n throw new Error(\"useFilesClient must be used within a FilesClientProvider\");\r\n }\r\n return context;\r\n}\r\n","import { treaty, type Treaty } from \"@elysiajs/eden\";\r\nimport type { createFilesServer } from \"../server\";\r\n\r\nexport function createFilesClient<\r\n TServer extends ReturnType<typeof createFilesServer<any>>\r\n>(\r\n domain: string,\r\n config?: Treaty.Config\r\n): Treaty.Create<TServer[\"router\"]> {\r\n return treaty<TServer[\"router\"]>(domain, config);\r\n}\r\n\r\nexport type FilesClient = ReturnType<typeof createFilesClient>;","import { useMutation, type UseMutationOptions } from \"@tanstack/react-query\";\r\nimport type { FilesClient } from \"./client\";\r\nimport { edenMutationOptions, type InferMutationOptions } from \"eden2query\";\r\nimport { useFilesClient } from \"./provider\";\r\n\r\nexport function useUpload(\r\n options: InferMutationOptions<FilesClient[\"api\"][\"files\"][\"upload\"][\"post\"]>\r\n) {\r\n const filesClient = useFilesClient() ;\r\n return useMutation({\r\n ...options,\r\n ...edenMutationOptions(\r\n filesClient.api.files.upload.post,\r\n ),\r\n });\r\n}"],"mappings":";;;;;;;AAIA,MAAM,qBAAqB,cAAkC,KAAK;AAElE,SAAgB,oBAAoB,OAGjC;AACD,QACE,oBAAC,mBAAmB;EAAS,OAAO,MAAM;YACvC,MAAM;GACqB;;AAIlC,SAAgB,iBAAiB;CAC/B,MAAM,UAAU,WAAW,mBAAmB;AAC9C,KAAI,CAAC,QACH,OAAM,IAAI,MAAM,2DAA2D;AAE7E,QAAO;;;;;ACnBT,SAAgB,kBAGd,QACA,QACkC;AAClC,QAAO,OAA0B,QAAQ,OAAO;;;;;ACJlD,SAAgB,UACd,SACA;CACA,MAAM,cAAc,gBAAgB;AACpC,QAAO,YAAY;EACjB,GAAG;EACH,GAAG,oBACD,YAAY,IAAI,MAAM,OAAO,KAC9B;EACF,CAAC"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as schema_d_exports, i as Visibility, n as createFilesServer, r as PurposePolicy, t as FilesServerOptions } from "./server-C_G1kBYN.mjs";
|
|
2
2
|
import "./server/index.mjs";
|
|
3
|
-
export {
|
|
3
|
+
export { FilesServerOptions, PurposePolicy, Visibility, createFilesServer, schema_d_exports as schema };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createFilesServer } from "./server-
|
|
1
|
+
import { n as schema_exports, t as createFilesServer } from "./server-B1B4vJ67.mjs";
|
|
2
2
|
import "./server/index.mjs";
|
|
3
3
|
|
|
4
|
-
export { createFilesServer };
|
|
4
|
+
export { createFilesServer, schema_exports as schema };
|
package/dist/server/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export {
|
|
1
|
+
import { a as schema_d_exports, i as Visibility, n as createFilesServer, r as PurposePolicy, t as FilesServerOptions } from "../server-C_G1kBYN.mjs";
|
|
2
|
+
export { FilesServerOptions, type PurposePolicy, type Visibility, createFilesServer, schema_d_exports as schema };
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { t as createFilesServer } from "../server-
|
|
1
|
+
import { n as schema_exports, t as createFilesServer } from "../server-B1B4vJ67.mjs";
|
|
2
2
|
|
|
3
|
-
export { createFilesServer };
|
|
3
|
+
export { createFilesServer, schema_exports as schema };
|
|
@@ -1,25 +1,43 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import { index, integer, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
1
4
|
import { Elysia, t } from "elysia";
|
|
2
5
|
import { S3Client } from "bun";
|
|
3
|
-
import { nanoid } from "nanoid";
|
|
4
6
|
import { extname } from "path";
|
|
5
|
-
import { and, eq, sql } from "drizzle-orm";
|
|
6
|
-
import { index, integer, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
7
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
7
8
|
|
|
8
|
-
//#region src/server/
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
//#region src/server/schema.ts
|
|
10
|
+
var schema_exports = /* @__PURE__ */ __exportAll({ files: () => files });
|
|
11
|
+
const files = pgTable("files", {
|
|
12
|
+
id: text("id").primaryKey().$defaultFn(() => `file_${nanoid()}`),
|
|
13
|
+
purpose: text("purpose").notNull(),
|
|
14
|
+
name: text("name"),
|
|
15
|
+
key: text("key").notNull().unique(),
|
|
16
|
+
size: integer("size").notNull(),
|
|
17
|
+
mimeType: text("mime_type").notNull().default("application/octet-stream"),
|
|
18
|
+
refCount: integer("ref_count").notNull().default(0),
|
|
19
|
+
visibility: pgEnum("visibility", ["private", "public"])().notNull().default("private"),
|
|
20
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
21
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow()
|
|
22
|
+
}, (table) => [index("files_key_idx").on(table.key), index("files_created_at_idx").on(table.createdAt)]);
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/server/router.ts
|
|
26
|
+
function createRouter(options) {
|
|
27
|
+
const { service, policies } = options;
|
|
11
28
|
return new Elysia({ prefix: "/api" }).post("/files/upload", async ({ status, body }) => {
|
|
12
29
|
const { file, purpose } = body;
|
|
13
30
|
const policy = policies[purpose];
|
|
14
31
|
if (!policy) return status(400, { message: "Purpose not supported" });
|
|
15
32
|
if (policy.maxSize !== void 0 && file.size > policy.maxSize) return status(400, { message: "File size exceeds the maximum allowed size" });
|
|
16
33
|
if (policy.allowedMimeTypes !== void 0 && !policy.allowedMimeTypes.includes(file.type)) return status(400, { message: "File type not allowed" });
|
|
17
|
-
const uploaded = await
|
|
34
|
+
const uploaded = await service.uploadFile({
|
|
18
35
|
file,
|
|
19
36
|
purpose,
|
|
20
37
|
visibility: policy.visibility ?? "private"
|
|
21
38
|
});
|
|
22
39
|
return status(200, {
|
|
40
|
+
url: await service.getUrl({ id: uploaded.id }) ?? "",
|
|
23
41
|
id: uploaded.id,
|
|
24
42
|
name: uploaded.name,
|
|
25
43
|
key: uploaded.key,
|
|
@@ -44,6 +62,7 @@ function UploadRequest(purposes) {
|
|
|
44
62
|
}
|
|
45
63
|
const ErrorResponse = t.Object({ message: t.String() });
|
|
46
64
|
const FileResponse = t.Object({
|
|
65
|
+
url: t.String(),
|
|
47
66
|
id: t.String(),
|
|
48
67
|
name: t.Nullable(t.String()),
|
|
49
68
|
key: t.String(),
|
|
@@ -53,54 +72,54 @@ const FileResponse = t.Object({
|
|
|
53
72
|
});
|
|
54
73
|
|
|
55
74
|
//#endregion
|
|
56
|
-
//#region src/server/
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
key: text("key").notNull().unique(),
|
|
62
|
-
size: integer("size").notNull(),
|
|
63
|
-
mimeType: text("mime_type").notNull().default("application/octet-stream"),
|
|
64
|
-
refCount: integer("ref_count").notNull().default(0),
|
|
65
|
-
visibility: pgEnum("visibility", ["private", "public"])().notNull().default("private"),
|
|
66
|
-
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
67
|
-
updatedAt: timestamp("updated_at").notNull().defaultNow()
|
|
68
|
-
}, (table) => [index("files_key_idx").on(table.key), index("files_created_at_idx").on(table.createdAt)]);
|
|
69
|
-
|
|
70
|
-
//#endregion
|
|
71
|
-
//#region src/server/services.ts
|
|
72
|
-
function createServices(options) {
|
|
73
|
-
const { db, s3Options, keyPrefix = "files", presignExpiresIn = 3600 } = options;
|
|
74
|
-
const { endpoint } = s3Options;
|
|
75
|
-
const s3Client = new S3Client(s3Options);
|
|
75
|
+
//#region src/server/service.ts
|
|
76
|
+
function createService(options) {
|
|
77
|
+
const { db, s3, keyPrefix = "files", presignExpiresIn = 3600 } = options;
|
|
78
|
+
const { endpoint } = s3;
|
|
79
|
+
const s3Client = new S3Client(s3);
|
|
76
80
|
return {
|
|
81
|
+
async listFiles(params) {
|
|
82
|
+
return await db.select().from(files).where(inArray(files.id, params.ids)).limit(params.ids.length);
|
|
83
|
+
},
|
|
77
84
|
async getFile(params) {
|
|
78
85
|
const [found] = await db.select().from(files).where(eq(files.id, params.id)).limit(1);
|
|
79
|
-
|
|
80
|
-
|
|
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 });
|
|
81
91
|
},
|
|
82
92
|
async getUrl(params) {
|
|
83
93
|
const [found] = await db.select({
|
|
84
94
|
key: files.key,
|
|
85
95
|
visibility: files.visibility
|
|
86
96
|
}).from(files).where(eq(files.id, params.id)).limit(1);
|
|
87
|
-
if (!found)
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
const urls = {};
|
|
107
|
+
for (const row of rows) urls[row.id] = this.buildUrl(row);
|
|
108
|
+
return urls;
|
|
90
109
|
},
|
|
91
110
|
async uploadFile(params) {
|
|
92
111
|
const id = nanoid();
|
|
93
112
|
const ext = extname(params.file.name);
|
|
94
113
|
const key = [
|
|
95
114
|
keyPrefix,
|
|
96
|
-
params.visibility,
|
|
97
115
|
params.purpose,
|
|
98
116
|
`${id}${ext}`
|
|
99
117
|
].join("/");
|
|
100
118
|
const s3file = s3Client.file(key);
|
|
101
119
|
await s3file.write(params.file, {
|
|
102
120
|
type: params.file.type,
|
|
103
|
-
acl: params.visibility === "public" ? "public-read" : "private"
|
|
121
|
+
acl: params.visibility === "public" ? "public-read" : "private",
|
|
122
|
+
contentDisposition: `attachment; filename="${params.file.name}"`
|
|
104
123
|
});
|
|
105
124
|
try {
|
|
106
125
|
const [created] = await db.insert(files).values({
|
|
@@ -114,7 +133,7 @@ function createServices(options) {
|
|
|
114
133
|
if (!created) throw new Error("Failed to create file record");
|
|
115
134
|
return created;
|
|
116
135
|
} catch (error) {
|
|
117
|
-
await s3file.delete().catch();
|
|
136
|
+
await s3file.delete().catch(() => {});
|
|
118
137
|
throw error;
|
|
119
138
|
}
|
|
120
139
|
},
|
|
@@ -123,15 +142,33 @@ function createServices(options) {
|
|
|
123
142
|
if (!deleted) return;
|
|
124
143
|
await s3Client.delete(deleted.key);
|
|
125
144
|
},
|
|
145
|
+
async deleteFiles(params) {
|
|
146
|
+
const deleted = await db.delete(files).where(inArray(files.id, params.ids)).returning({ key: files.key });
|
|
147
|
+
if (deleted.length === 0) return;
|
|
148
|
+
await Promise.all(deleted.map((file) => s3Client.delete(file.key)));
|
|
149
|
+
},
|
|
126
150
|
async acquireFile(params) {
|
|
127
151
|
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();
|
|
128
152
|
if (!updated) throw new Error("File not found");
|
|
129
153
|
return updated;
|
|
130
154
|
},
|
|
155
|
+
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();
|
|
157
|
+
},
|
|
131
158
|
async releaseFile(params) {
|
|
132
159
|
const [updated] = await db.update(files).set({ refCount: sql`${files.refCount} - 1` }).where(eq(files.id, params.id)).returning();
|
|
133
160
|
if (!updated) throw new Error("File not found");
|
|
134
161
|
if (updated.refCount < 1) await this.deleteFile({ id: params.id });
|
|
162
|
+
},
|
|
163
|
+
async releaseFiles(params) {
|
|
164
|
+
const updated = await db.update(files).set({ refCount: sql`${files.refCount} - 1` }).where(inArray(files.id, params.ids)).returning({
|
|
165
|
+
id: files.id,
|
|
166
|
+
refCount: files.refCount
|
|
167
|
+
});
|
|
168
|
+
if (updated.length === 0) return;
|
|
169
|
+
const toDelete = updated.filter((file) => file.refCount < 1);
|
|
170
|
+
if (toDelete.length === 0) return;
|
|
171
|
+
await Promise.all(toDelete.map((file) => this.deleteFile({ id: file.id })));
|
|
135
172
|
}
|
|
136
173
|
};
|
|
137
174
|
}
|
|
@@ -139,20 +176,20 @@ function createServices(options) {
|
|
|
139
176
|
//#endregion
|
|
140
177
|
//#region src/server/server.ts
|
|
141
178
|
function createFilesServer(options) {
|
|
142
|
-
const
|
|
179
|
+
const service = createService({
|
|
143
180
|
db: options.db,
|
|
144
|
-
|
|
181
|
+
s3: options.s3,
|
|
145
182
|
presignExpiresIn: options.presignExpiresIn
|
|
146
183
|
});
|
|
147
184
|
return {
|
|
148
|
-
|
|
149
|
-
|
|
185
|
+
router: createRouter({
|
|
186
|
+
service,
|
|
150
187
|
policies: options.policies
|
|
151
188
|
}),
|
|
152
|
-
|
|
189
|
+
service
|
|
153
190
|
};
|
|
154
191
|
}
|
|
155
192
|
|
|
156
193
|
//#endregion
|
|
157
|
-
export { createFilesServer as t };
|
|
158
|
-
//# sourceMappingURL=server-
|
|
194
|
+
export { schema_exports as n, createFilesServer as t };
|
|
195
|
+
//# sourceMappingURL=server-B1B4vJ67.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Elysia } from "elysia";
|
|
2
|
-
import { S3Options } from "bun";
|
|
3
1
|
import * as drizzle_orm_pg_core0 from "drizzle-orm/pg-core";
|
|
4
2
|
import { PgDatabase } from "drizzle-orm/pg-core";
|
|
3
|
+
import { Elysia } from "elysia";
|
|
4
|
+
import { S3Options } from "bun";
|
|
5
5
|
|
|
6
|
-
//#region src/server/
|
|
7
|
-
|
|
6
|
+
//#region src/server/schema.d.ts
|
|
7
|
+
declare namespace schema_d_exports {
|
|
8
|
+
export { files };
|
|
9
|
+
}
|
|
8
10
|
declare const files: drizzle_orm_pg_core0.PgTableWithColumns<{
|
|
9
11
|
name: "files";
|
|
10
12
|
schema: undefined;
|
|
@@ -183,42 +185,53 @@ declare const files: drizzle_orm_pg_core0.PgTableWithColumns<{
|
|
|
183
185
|
dialect: "pg";
|
|
184
186
|
}>;
|
|
185
187
|
//#endregion
|
|
186
|
-
//#region src/server/
|
|
187
|
-
type
|
|
188
|
+
//#region src/server/service.d.ts
|
|
189
|
+
type Service = ReturnType<typeof createService>;
|
|
188
190
|
type ById = {
|
|
189
191
|
id: string;
|
|
190
192
|
};
|
|
191
|
-
|
|
193
|
+
type ByIds = {
|
|
194
|
+
ids: string[];
|
|
195
|
+
};
|
|
196
|
+
declare function createService(options: {
|
|
192
197
|
db: PgDatabase<any, any, any>;
|
|
193
|
-
|
|
198
|
+
s3: S3Options;
|
|
194
199
|
keyPrefix?: string;
|
|
195
200
|
presignExpiresIn?: number;
|
|
196
201
|
}): {
|
|
197
|
-
|
|
198
|
-
|
|
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<Record<string, string>>;
|
|
199
207
|
uploadFile(params: {
|
|
200
208
|
file: File;
|
|
201
209
|
purpose: string;
|
|
202
210
|
visibility: "private" | "public";
|
|
203
|
-
}): Promise<
|
|
211
|
+
}): Promise<typeof files.$inferSelect>;
|
|
204
212
|
deleteFile(params: {
|
|
205
213
|
id: string;
|
|
206
214
|
}): Promise<void>;
|
|
215
|
+
deleteFiles(params: ByIds): Promise<void>;
|
|
207
216
|
acquireFile(params: ById & {
|
|
208
217
|
purpose?: string;
|
|
209
|
-
}): Promise<
|
|
218
|
+
}): Promise<typeof files.$inferSelect>;
|
|
219
|
+
acquireFiles(params: ByIds & {
|
|
220
|
+
purpose?: string;
|
|
221
|
+
}): Promise<(typeof files.$inferSelect)[]>;
|
|
210
222
|
releaseFile(params: ById): Promise<void>;
|
|
223
|
+
releaseFiles(params: ByIds): Promise<void>;
|
|
211
224
|
};
|
|
212
225
|
//#endregion
|
|
213
|
-
//#region src/server/
|
|
226
|
+
//#region src/server/router.d.ts
|
|
214
227
|
type Visibility = "private" | "public";
|
|
215
228
|
interface PurposePolicy {
|
|
216
229
|
maxSize?: number;
|
|
217
230
|
allowedMimeTypes?: string[];
|
|
218
231
|
visibility?: Visibility;
|
|
219
232
|
}
|
|
220
|
-
declare function
|
|
221
|
-
|
|
233
|
+
declare function createRouter<const TPurpose extends string>(options: {
|
|
234
|
+
service: Service;
|
|
222
235
|
policies: Record<TPurpose, PurposePolicy>;
|
|
223
236
|
}): Elysia<"/api", {
|
|
224
237
|
decorator: {};
|
|
@@ -255,6 +268,7 @@ declare function createRoutes<const TPurpose extends string>(options: {
|
|
|
255
268
|
size: number;
|
|
256
269
|
mimeType: string;
|
|
257
270
|
createdAt: Date;
|
|
271
|
+
url: string;
|
|
258
272
|
};
|
|
259
273
|
400: {
|
|
260
274
|
message: string;
|
|
@@ -293,14 +307,14 @@ declare function createRoutes<const TPurpose extends string>(options: {
|
|
|
293
307
|
//#region src/server/server.d.ts
|
|
294
308
|
interface FilesServerOptions<TPurposes extends string> {
|
|
295
309
|
db: PgDatabase<any, any, any>;
|
|
296
|
-
|
|
310
|
+
s3: S3Options;
|
|
297
311
|
policies: Record<TPurposes, PurposePolicy>;
|
|
298
312
|
presignExpiresIn?: number;
|
|
299
313
|
}
|
|
300
314
|
declare function createFilesServer<const TPurpose extends string>(options: FilesServerOptions<TPurpose>): {
|
|
301
|
-
readonly
|
|
302
|
-
readonly
|
|
315
|
+
readonly router: ReturnType<typeof createRouter<TPurpose>>;
|
|
316
|
+
readonly service: Service;
|
|
303
317
|
};
|
|
304
318
|
//#endregion
|
|
305
|
-
export {
|
|
306
|
-
//# sourceMappingURL=server-
|
|
319
|
+
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
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server-BDKHu5Yk.mjs","names":[],"sources":["../src/server/routes.ts","../src/server/schemas.ts","../src/server/services.ts","../src/server/server.ts"],"sourcesContent":["import { Elysia, t } from \"elysia\";\r\nimport type { Services } from \"./services\";\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 createRoutes<const TPurpose extends string>(\r\n options: {\r\n services: Services;\r\n policies: Record<TPurpose, PurposePolicy>\r\n }\r\n) {\r\n \r\n const { \r\n services, \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 services.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 { nanoid } from \"nanoid\";\r\nimport { index, integer, pgTable, text, timestamp, pgEnum } from \"drizzle-orm/pg-core\";\r\n\r\nexport type FileRecord = typeof files[\"$inferSelect\"];\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 { S3Client, type S3Options } from \"bun\";\r\nimport { nanoid } from \"nanoid\";\r\nimport { extname } from \"path\";\r\nimport { eq, sql, lt, and } from \"drizzle-orm\";\r\nimport type { FileRecord } from \"./schemas\";\r\nimport type { PgDatabase } from \"drizzle-orm/pg-core\";\r\nimport { files } from \"./schemas\";\r\n\r\nexport type Services = ReturnType<typeof createServices>;\r\n\r\ntype ById = { id: string };\r\n\r\nexport function createServices(\r\n options: {\r\n db: PgDatabase<any, any, any>;\r\n s3Options: S3Options;\r\n keyPrefix?: string;\r\n presignExpiresIn?: number;\r\n }\r\n) {\r\n const { \r\n db, \r\n 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<FileRecord> {\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<FileRecord> {\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<FileRecord> {\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 { createRoutes, type PurposePolicy } from \"./routes\";\r\nimport { createServices, type Services } from \"./services\";\r\nimport type { S3Options } from \"bun\";\r\n\r\nexport interface FilesServerOptions<TPurposes extends string> {\r\n db: PgDatabase<any, any, any>;\r\n s3Options: 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 routes: ReturnType<typeof createRoutes<TPurpose>>;\r\n readonly services: Services;\r\n} {\r\n const services = createServices({\r\n db: options.db,\r\n s3Options: options.s3Options,\r\n presignExpiresIn: options.presignExpiresIn,\r\n });\r\n const routes = createRoutes({\r\n services,\r\n policies: options.policies,\r\n });\r\n return {\r\n routes,\r\n services,\r\n };\r\n}"],"mappings":"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"}
|