@uplift-io/uplift 1.0.0 → 1.1.0

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Uplift contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Uplift contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,85 +1,102 @@
1
- # Uplift
2
-
3
- [![npm version](https://img.shields.io/npm/v/@uplift-io/uplift?color=0f766e)](https://www.npmjs.com/package/@uplift-io/uplift)
4
- [![npm downloads](https://img.shields.io/npm/dm/@uplift-io/uplift?color=2563eb)](https://www.npmjs.com/package/@uplift-io/uplift)
5
- [![npm downloads total](https://img.shields.io/npm/dt/@uplift-io/uplift?color=7c3aed)](https://www.npmjs.com/package/@uplift-io/uplift)
6
- [![CI](https://github.com/Itzfeminisce/uplift/actions/workflows/ci.yml/badge.svg)](https://github.com/Itzfeminisce/uplift/actions/workflows/ci.yml)
7
- [![bundle size](https://img.shields.io/badge/bundle-measured%20locally-14b8a6)](https://github.com/Itzfeminisce/uplift/blob/main/docs/BUNDLE_SIZE.md)
8
- [![license](https://img.shields.io/npm/l/@uplift-io/uplift)](https://github.com/Itzfeminisce/uplift/blob/main/LICENSE)
9
-
10
- Dead-simple, type-safe file uploads for TypeScript applications.
11
-
12
- Define upload routes once on the server. Get a typed client on the frontend.
13
-
14
- ```ts
15
- await upload.avatar(file);
16
- await upload.gallery(files);
17
- ```
18
-
19
- ## Install
20
-
21
- Install core plus the adapters you use:
22
-
23
- ```bash
24
- pnpm add @uplift-io/uplift @uplift-io/next @uplift-io/s3
25
- ```
26
-
27
- ## Quick Start
28
-
29
- ```ts
30
- import { image, uplift } from "@uplift-io/uplift";
31
- import { s3 } from "@uplift-io/s3";
32
-
33
- export const uploads = uplift({
34
- storage: s3({
35
- bucket: process.env.S3_BUCKET!,
36
- region: "us-east-1",
37
- accessKeyId: process.env.S3_ACCESS_KEY_ID!,
38
- secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!
39
- }),
40
- routes: {
41
- avatar: image()
42
- .max("2mb")
43
- .auth(async ({ req }) => ({ id: req.headers.get("x-user-id")! }))
44
- .key(({ user }) => `avatars/${user.id}.png`)
45
- .done(async ({ file }) => {
46
- console.log(file.url);
47
- }),
48
- gallery: image().max("8mb").multiple(10)
49
- }
50
- });
51
-
52
- export type Uploads = typeof uploads;
53
- ```
54
-
55
- ```ts
56
- import { createUploadClient } from "@uplift-io/uplift/client";
57
- import type { Uploads } from "./uploads";
58
-
59
- export const upload = createUploadClient<Uploads>("/api/upload");
60
-
61
- const avatar = await upload.avatar(file);
62
- const gallery = await upload.gallery(fileList);
63
- ```
64
-
65
- ## More
66
-
67
- - Full docs: [itzfeminisce.github.io/uplift](https://itzfeminisce.github.io/uplift/)
68
- - Bundle size report: [docs/BUNDLE_SIZE.md](https://github.com/Itzfeminisce/uplift/blob/main/docs/BUNDLE_SIZE.md)
69
- - Next local example: [examples/next-local](https://github.com/Itzfeminisce/uplift/tree/main/examples/next-local)
70
- - GitHub: [github.com/Itzfeminisce/uplift](https://github.com/Itzfeminisce/uplift)
71
- - License: MIT
72
-
73
- ## Storage
74
-
75
- Uplift publishes S3, R2, Bunny, Cloudinary, local, memory, and UploadThing-compatible adapters as separate packages. The UploadThing adapter accepts a server-side uploader compatible with `UTApi.uploadFiles()` and keeps UploadThing optional:
76
-
77
- ```ts
78
- import { uploadthing } from "@uplift-io/uploadthing";
79
- import { UTApi } from "uploadthing/server";
80
-
81
- const utapi = new UTApi();
82
- const storage = uploadthing({
83
- uploader: (file) => utapi.uploadFiles(file)
84
- });
85
- ```
1
+ # Uplift
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@uplift-io/uplift?color=0f766e)](https://www.npmjs.com/package/@uplift-io/uplift)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@uplift-io/uplift?color=2563eb)](https://www.npmjs.com/package/@uplift-io/uplift)
5
+ [![npm downloads total](https://img.shields.io/npm/dt/@uplift-io/uplift?color=7c3aed)](https://www.npmjs.com/package/@uplift-io/uplift)
6
+ [![CI](https://github.com/Itzfeminisce/uplift/actions/workflows/ci.yml/badge.svg)](https://github.com/Itzfeminisce/uplift/actions/workflows/ci.yml)
7
+ [![bundle size](https://img.shields.io/badge/bundle-measured%20locally-14b8a6)](https://github.com/Itzfeminisce/uplift/blob/main/docs/BUNDLE_SIZE.md)
8
+ [![license](https://img.shields.io/npm/l/@uplift-io/uplift)](https://github.com/Itzfeminisce/uplift/blob/main/LICENSE)
9
+
10
+ Dead-simple, type-safe file uploads for TypeScript applications.
11
+
12
+ Define upload routes once on the server. Get a typed client on the frontend.
13
+
14
+ ```ts
15
+ await upload.avatar(file);
16
+ await upload.gallery(files);
17
+ ```
18
+
19
+ ## Install
20
+
21
+ Install core plus the adapters you use:
22
+
23
+ ```bash
24
+ pnpm add @uplift-io/uplift @uplift-io/next @uplift-io/s3
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```ts
30
+ import { image, uplift } from "@uplift-io/uplift";
31
+ import { s3 } from "@uplift-io/s3";
32
+
33
+ export const uploads = uplift({
34
+ storage: s3({
35
+ bucket: process.env.S3_BUCKET!,
36
+ region: "us-east-1",
37
+ accessKeyId: process.env.S3_ACCESS_KEY_ID!,
38
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!
39
+ }),
40
+ routes: {
41
+ avatar: image()
42
+ .max("2mb")
43
+ .auth(async ({ req }) => ({ id: req.headers.get("x-user-id")! }))
44
+ .key(({ user }) => `avatars/${user.id}.png`)
45
+ .done(async ({ file }) => {
46
+ console.log(file.url);
47
+ }),
48
+ gallery: image().max("8mb").multiple(10)
49
+ }
50
+ });
51
+
52
+ export type Uploads = typeof uploads;
53
+ ```
54
+
55
+ ```ts
56
+ import { createUploadClient } from "@uplift-io/uplift/client";
57
+ import type { Uploads } from "./uploads";
58
+
59
+ export const upload = createUploadClient<Uploads>("/api/upload");
60
+
61
+ const avatar = await upload.avatar(file);
62
+ const gallery = await upload.gallery(fileList);
63
+ ```
64
+
65
+ ## Media Transforms
66
+
67
+ Core exports the typed `.transform()` and `.outputs()` pipeline, while optional domain packages own media behavior and dependencies:
68
+
69
+ ```ts
70
+ import { image } from "@uplift-io/uplift";
71
+ import { resize, convert, variant } from "@uplift-io/image";
72
+
73
+ const avatar = image()
74
+ .transform(resize({ width: 512, height: 512 }), convert("webp"))
75
+ .outputs(variant("thumb", resize({ width: 96 }), convert("webp")));
76
+ ```
77
+
78
+ The frontend call remains `upload.avatar(file)`. Declared outputs are available with `uploaded.output("thumb")`.
79
+
80
+ Core stays free of Sharp and ffmpeg. Media packages own those runtime dependencies, and storage adapters may implement `delete(key)` so core can roll back already-written files when a later derived output write fails.
81
+
82
+ ## More
83
+
84
+ - Full docs: [itzfeminisce.github.io/uplift](https://itzfeminisce.github.io/uplift/)
85
+ - Bundle size report: [docs/BUNDLE_SIZE.md](https://github.com/Itzfeminisce/uplift/blob/main/docs/BUNDLE_SIZE.md)
86
+ - Next local example: [examples/next-local](https://github.com/Itzfeminisce/uplift/tree/main/examples/next-local)
87
+ - GitHub: [github.com/Itzfeminisce/uplift](https://github.com/Itzfeminisce/uplift)
88
+ - License: MIT
89
+
90
+ ## Storage
91
+
92
+ Uplift publishes S3, R2, Bunny, Cloudinary, local, memory, and UploadThing-compatible adapters as separate packages. The UploadThing adapter accepts a server-side uploader compatible with `UTApi.uploadFiles()` and keeps UploadThing optional:
93
+
94
+ ```ts
95
+ import { uploadthing } from "@uplift-io/uploadthing";
96
+ import { UTApi } from "uploadthing/server";
97
+
98
+ const utapi = new UTApi();
99
+ const storage = uploadthing({
100
+ uploader: (file) => utapi.uploadFiles(file)
101
+ });
102
+ ```
package/dist/client.cjs CHANGED
@@ -62,7 +62,7 @@ function createUploadClient(baseUrl, options = {}) {
62
62
  throw new UploadError(body.error?.code ?? "UNKNOWN", body.error?.message ?? "Upload failed.");
63
63
  }
64
64
  options.onProgress?.(property, 100);
65
- return body.result;
65
+ return attachOutputGetters(body.result);
66
66
  };
67
67
  }
68
68
  });
@@ -88,13 +88,34 @@ function uploadWithXhr(url, form, route, onProgress) {
88
88
  return;
89
89
  }
90
90
  onProgress?.(route, 100);
91
- resolve(body.result);
91
+ resolve(attachOutputGetters(body.result));
92
92
  };
93
93
  xhr.onerror = () => reject(new UploadError("UPLOAD_FAILED", "Upload request failed."));
94
94
  onProgress?.(route, 0);
95
95
  xhr.send(form);
96
96
  });
97
97
  }
98
+ function attachOutputGetters(result) {
99
+ if (Array.isArray(result)) return result.map((item) => attachOutputGetters(item));
100
+ if (!isUploadedFileLike(result)) return result;
101
+ if (!Object.prototype.hasOwnProperty.call(result, "output")) {
102
+ Object.defineProperty(result, "output", {
103
+ enumerable: false,
104
+ value(name) {
105
+ const output = result.outputs?.[name];
106
+ if (!output) throw new UploadError("VALIDATION_FAILED", `Unknown output: ${name}`);
107
+ return attachOutputGetters(output);
108
+ }
109
+ });
110
+ }
111
+ if (result.outputs) {
112
+ for (const output of Object.values(result.outputs)) attachOutputGetters(output);
113
+ }
114
+ return result;
115
+ }
116
+ function isUploadedFileLike(value) {
117
+ return typeof value === "object" && value !== null;
118
+ }
98
119
  // Annotate the CommonJS export names for ESM import in node:
99
120
  0 && (module.exports = {
100
121
  createUploadClient
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/types.ts"],"sourcesContent":["import { UploadError, type UpliftApp, type UploadClient } from \"./types\";\r\n\r\nexport type UploadProgressHandler = (progress: number) => void;\r\n\r\nexport function createUploadClient<TApp extends UpliftApp>(\r\n baseUrl: string,\r\n options: { fetch?: typeof fetch; onProgress?: (route: string, progress: number) => void } = {}\r\n): UploadClient<TApp> {\r\n const fetcher = options.fetch ?? fetch;\r\n\r\n return new Proxy({}, {\r\n get(_target, property) {\r\n if (typeof property !== \"string\") return undefined;\r\n return async (input: File | File[] | FileList) => {\r\n const files = input instanceof File ? [input] : Array.from(input);\r\n const form = new FormData();\r\n const field = files.length === 1 ? \"file\" : \"files\";\r\n for (const file of files) form.append(field, file);\r\n\r\n const url = routeUrl(baseUrl, property);\r\n if (!options.fetch && typeof XMLHttpRequest !== \"undefined\") {\r\n return uploadWithXhr(url, form, property, options.onProgress);\r\n }\r\n\r\n options.onProgress?.(property, 0);\r\n const response = await fetcher(url, { method: \"POST\", body: form });\r\n const body = await response.json() as { result?: unknown; error?: { code: string; message: string } };\r\n if (!response.ok) {\r\n throw new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\");\r\n }\r\n options.onProgress?.(property, 100);\r\n return body.result;\r\n };\r\n }\r\n }) as UploadClient<TApp>;\r\n}\r\n\r\nfunction routeUrl(baseUrl: string, route: string): string {\r\n const url = new URL(baseUrl, globalThis.location?.href ?? \"http://localhost\");\r\n url.searchParams.set(\"route\", route);\r\n const value = url.toString();\r\n return baseUrl.startsWith(\"/\") ? `${url.pathname}${url.search}` : value;\r\n}\r\n\r\nfunction uploadWithXhr(\r\n url: string,\r\n form: FormData,\r\n route: string,\r\n onProgress?: (route: string, progress: number) => void\r\n): Promise<unknown> {\r\n return new Promise((resolve, reject) => {\r\n const xhr = new XMLHttpRequest();\r\n xhr.open(\"POST\", url);\r\n xhr.upload.onprogress = (event) => {\r\n if (!event.lengthComputable) return;\r\n onProgress?.(route, Math.round((event.loaded / event.total) * 100));\r\n };\r\n xhr.onload = () => {\r\n const body = JSON.parse(xhr.responseText || \"{}\") as {\r\n result?: unknown;\r\n error?: { code: string; message: string };\r\n };\r\n if (xhr.status < 200 || xhr.status >= 300) {\r\n reject(new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\"));\r\n return;\r\n }\r\n onProgress?.(route, 100);\r\n resolve(body.result);\r\n };\r\n xhr.onerror = () => reject(new UploadError(\"UPLOAD_FAILED\", \"Upload request failed.\"));\r\n onProgress?.(route, 0);\r\n xhr.send(form);\r\n });\r\n}\r\n","export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\r\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\r\n\r\nexport type UploadInputFile = {\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string;\r\n file?: File;\r\n};\r\n\r\nexport type UploadedFile = {\r\n url: string;\r\n key: string;\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string | undefined;\r\n provider: string;\r\n};\r\n\r\nexport type UploadErrorCode =\r\n | \"FILE_TOO_LARGE\"\r\n | \"FILE_TOO_SMALL\"\r\n | \"INVALID_TYPE\"\r\n | \"AUTH_FAILED\"\r\n | \"VALIDATION_FAILED\"\r\n | \"UPLOAD_FAILED\"\r\n | \"UNKNOWN\";\r\n\r\nexport class UploadError extends Error {\r\n readonly code: UploadErrorCode;\r\n\r\n constructor(code: UploadErrorCode, message: string) {\r\n super(message);\r\n this.name = \"UploadError\";\r\n this.code = code;\r\n }\r\n\r\n toJSON() {\r\n return {\r\n message: this.message,\r\n code: this.code\r\n };\r\n }\r\n}\r\n\r\nexport type StandardSchema<T = unknown> = {\r\n parse(input: unknown): T;\r\n};\r\n\r\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: TAuth;\r\n meta: TMeta;\r\n};\r\n\r\nexport type DoneContext<\r\n TAuth = unknown,\r\n TMeta = unknown,\r\n TMultiple extends boolean = false\r\n> = TMultiple extends true\r\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\r\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\r\n\r\nexport type StoragePutInput = {\r\n key: string;\r\n file: UploadInputFile;\r\n body: File;\r\n};\r\n\r\nexport type StorageAdapter = {\r\n provider: string;\r\n put(input: StoragePutInput): Promise<UploadedFile>;\r\n};\r\n\r\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\r\n\r\nexport type UploadKind =\r\n | \"any\"\r\n | \"image\"\r\n | \"pdf\"\r\n | \"video\"\r\n | \"audio\"\r\n | \"text\"\r\n | \"json\"\r\n | \"csv\"\r\n | \"custom\";\r\n\r\nexport type UploadRouteDefinition = {\r\n kind: UploadKind;\r\n maxBytes?: number;\r\n minBytes?: number;\r\n multiple: boolean;\r\n multipleLimit?: number;\r\n auth?: Middleware<unknown>;\r\n overrideAuth: boolean;\r\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\r\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\r\n validate?: (ctx: {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: unknown;\r\n meta: unknown;\r\n }) => true | string | Promise<true | string>;\r\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\r\n extensions?: string[];\r\n mimeTypes?: string[];\r\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\r\n requireSquare?: boolean;\r\n aspectRatio?: `${number}:${number}`;\r\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\r\n schema?: StandardSchema;\r\n headers?: string[];\r\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\r\n pageRule?: { min?: number; max?: number };\r\n encrypted?: boolean;\r\n durationRule?: { min?: DurationValue; max?: DurationValue };\r\n};\r\n\r\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\r\n\r\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\r\n storage: StorageAdapter;\r\n routes: TRoutes;\r\n middleware?: Middleware<unknown> | undefined;\r\n onUploadComplete?: ((ctx: {\r\n route: keyof TRoutes & string;\r\n result: UploadedFile | UploadedFile[];\r\n user: unknown;\r\n }) => void | Promise<void>) | undefined;\r\n};\r\n\r\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\r\n ? TMultiple extends true\r\n ? true\r\n : false\r\n : false;\r\n\r\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\r\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? UploadedFile[] : UploadedFile;\r\n\r\nexport type UploadClient<TApp extends UpliftApp> = {\r\n [TRouteName in keyof TApp[\"routes\"] & string]: (\r\n input: ClientInput<TApp[\"routes\"][TRouteName]>\r\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC8BO,IAAM,cAAN,cAA0B,MAAM;AAAA,EAC5B;AAAA,EAET,YAAY,MAAuB,SAAiB;AAClD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,SAAS;AACP,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,IACb;AAAA,EACF;AACF;;;ADzCO,SAAS,mBACd,SACA,UAA4F,CAAC,GACzE;AACpB,QAAM,UAAU,QAAQ,SAAS;AAEjC,SAAO,IAAI,MAAM,CAAC,GAAG;AAAA,IACnB,IAAI,SAAS,UAAU;AACrB,UAAI,OAAO,aAAa,SAAU,QAAO;AACzC,aAAO,OAAO,UAAoC;AAChD,cAAM,QAAQ,iBAAiB,OAAO,CAAC,KAAK,IAAI,MAAM,KAAK,KAAK;AAChE,cAAM,OAAO,IAAI,SAAS;AAC1B,cAAM,QAAQ,MAAM,WAAW,IAAI,SAAS;AAC5C,mBAAW,QAAQ,MAAO,MAAK,OAAO,OAAO,IAAI;AAEjD,cAAM,MAAM,SAAS,SAAS,QAAQ;AACtC,YAAI,CAAC,QAAQ,SAAS,OAAO,mBAAmB,aAAa;AAC3D,iBAAO,cAAc,KAAK,MAAM,UAAU,QAAQ,UAAU;AAAA,QAC9D;AAEA,gBAAQ,aAAa,UAAU,CAAC;AAChC,cAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,QAAQ,QAAQ,MAAM,KAAK,CAAC;AAClE,cAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAI,CAAC,SAAS,IAAI;AAChB,gBAAM,IAAI,YAAa,KAAK,OAAO,QAAQ,WAAqB,KAAK,OAAO,WAAW,gBAAgB;AAAA,QACzG;AACA,gBAAQ,aAAa,UAAU,GAAG;AAClC,eAAO,KAAK;AAAA,MACd;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,SAAS,SAAiB,OAAuB;AACxD,QAAM,MAAM,IAAI,IAAI,SAAS,WAAW,UAAU,QAAQ,kBAAkB;AAC5E,MAAI,aAAa,IAAI,SAAS,KAAK;AACnC,QAAM,QAAQ,IAAI,SAAS;AAC3B,SAAO,QAAQ,WAAW,GAAG,IAAI,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM,KAAK;AACpE;AAEA,SAAS,cACP,KACA,MACA,OACA,YACkB;AAClB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,MAAM,IAAI,eAAe;AAC/B,QAAI,KAAK,QAAQ,GAAG;AACpB,QAAI,OAAO,aAAa,CAAC,UAAU;AACjC,UAAI,CAAC,MAAM,iBAAkB;AAC7B,mBAAa,OAAO,KAAK,MAAO,MAAM,SAAS,MAAM,QAAS,GAAG,CAAC;AAAA,IACpE;AACA,QAAI,SAAS,MAAM;AACjB,YAAM,OAAO,KAAK,MAAM,IAAI,gBAAgB,IAAI;AAIhD,UAAI,IAAI,SAAS,OAAO,IAAI,UAAU,KAAK;AACzC,eAAO,IAAI,YAAa,KAAK,OAAO,QAAQ,WAAqB,KAAK,OAAO,WAAW,gBAAgB,CAAC;AACzG;AAAA,MACF;AACA,mBAAa,OAAO,GAAG;AACvB,cAAQ,KAAK,MAAM;AAAA,IACrB;AACA,QAAI,UAAU,MAAM,OAAO,IAAI,YAAY,iBAAiB,wBAAwB,CAAC;AACrF,iBAAa,OAAO,CAAC;AACrB,QAAI,KAAK,IAAI;AAAA,EACf,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/types.ts"],"sourcesContent":["import { UploadError, type UpliftApp, type UploadClient } from \"./types\";\n\nexport type UploadProgressHandler = (progress: number) => void;\n\nexport function createUploadClient<TApp extends UpliftApp>(\n baseUrl: string,\n options: { fetch?: typeof fetch; onProgress?: (route: string, progress: number) => void } = {}\n): UploadClient<TApp> {\n const fetcher = options.fetch ?? fetch;\n\n return new Proxy({}, {\n get(_target, property) {\n if (typeof property !== \"string\") return undefined;\n return async (input: File | File[] | FileList) => {\n const files = input instanceof File ? [input] : Array.from(input);\n const form = new FormData();\n const field = files.length === 1 ? \"file\" : \"files\";\n for (const file of files) form.append(field, file);\n\n const url = routeUrl(baseUrl, property);\n if (!options.fetch && typeof XMLHttpRequest !== \"undefined\") {\n return uploadWithXhr(url, form, property, options.onProgress);\n }\n\n options.onProgress?.(property, 0);\n const response = await fetcher(url, { method: \"POST\", body: form });\n const body = await response.json() as { result?: unknown; error?: { code: string; message: string } };\n if (!response.ok) {\n throw new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\");\n }\n options.onProgress?.(property, 100);\n return attachOutputGetters(body.result);\n };\n }\n }) as UploadClient<TApp>;\n}\n\nfunction routeUrl(baseUrl: string, route: string): string {\n const url = new URL(baseUrl, globalThis.location?.href ?? \"http://localhost\");\n url.searchParams.set(\"route\", route);\n const value = url.toString();\n return baseUrl.startsWith(\"/\") ? `${url.pathname}${url.search}` : value;\n}\n\nfunction uploadWithXhr(\n url: string,\n form: FormData,\n route: string,\n onProgress?: (route: string, progress: number) => void\n): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(\"POST\", url);\n xhr.upload.onprogress = (event) => {\n if (!event.lengthComputable) return;\n onProgress?.(route, Math.round((event.loaded / event.total) * 100));\n };\n xhr.onload = () => {\n const body = JSON.parse(xhr.responseText || \"{}\") as {\n result?: unknown;\n error?: { code: string; message: string };\n };\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\"));\n return;\n }\n onProgress?.(route, 100);\n resolve(attachOutputGetters(body.result));\n };\n xhr.onerror = () => reject(new UploadError(\"UPLOAD_FAILED\", \"Upload request failed.\"));\n onProgress?.(route, 0);\n xhr.send(form);\n });\n}\n\nfunction attachOutputGetters(result: unknown): unknown {\n if (Array.isArray(result)) return result.map((item) => attachOutputGetters(item));\n if (!isUploadedFileLike(result)) return result;\n if (!Object.prototype.hasOwnProperty.call(result, \"output\")) {\n Object.defineProperty(result, \"output\", {\n enumerable: false,\n value(name: string) {\n const output = result.outputs?.[name];\n if (!output) throw new UploadError(\"VALIDATION_FAILED\", `Unknown output: ${name}`);\n return attachOutputGetters(output);\n }\n });\n }\n if (result.outputs) {\n for (const output of Object.values(result.outputs)) attachOutputGetters(output);\n }\n return result;\n}\n\nfunction isUploadedFileLike(value: unknown): value is {\n outputs?: Record<string, unknown>;\n output?: (name: string) => unknown;\n} {\n return typeof value === \"object\" && value !== null;\n}\n","export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\n\nexport type UploadInputFile = {\n name: string;\n type: string;\n size: number;\n extension?: string;\n file?: File;\n};\n\nexport type UploadedFile = {\n url: string;\n key: string;\n name: string;\n type: string;\n size: number;\n extension?: string | undefined;\n provider: string;\n outputs?: Record<string, UploadedFile> | undefined;\n};\n\nexport type ClientUploadedFile<TOutputNames extends string = never> = UploadedFile & (\n [TOutputNames] extends [never]\n ? object\n : {\n output<TName extends TOutputNames>(name: TName): UploadedFile;\n }\n);\n\nexport type UploadErrorCode =\n | \"FILE_TOO_LARGE\"\n | \"FILE_TOO_SMALL\"\n | \"INVALID_TYPE\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_FAILED\"\n | \"UPLOAD_FAILED\"\n | \"UNKNOWN\";\n\nexport class UploadError extends Error {\n readonly code: UploadErrorCode;\n\n constructor(code: UploadErrorCode, message: string) {\n super(message);\n this.name = \"UploadError\";\n this.code = code;\n }\n\n toJSON() {\n return {\n message: this.message,\n code: this.code\n };\n }\n}\n\nexport type StandardSchema<T = unknown> = {\n parse(input: unknown): T;\n};\n\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\n req: Request;\n file: UploadInputFile;\n user: TAuth;\n meta: TMeta;\n};\n\nexport type DoneContext<\n TAuth = unknown,\n TMeta = unknown,\n TMultiple extends boolean = false\n> = TMultiple extends true\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\n\nexport type StoragePutInput = {\n key: string;\n file: UploadInputFile;\n body: File;\n};\n\nexport type StorageAdapter = {\n provider: string;\n put(input: StoragePutInput): Promise<UploadedFile>;\n delete?(key: string): Promise<void>;\n};\n\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\n\nexport type UploadKind =\n | \"any\"\n | \"image\"\n | \"pdf\"\n | \"video\"\n | \"audio\"\n | \"text\"\n | \"json\"\n | \"csv\"\n | \"custom\";\n\nexport type PreparedUploadFile = {\n file: UploadInputFile;\n body: File;\n};\n\nexport type TransformContext = PreparedUploadFile;\n\nexport type UploadTransform<TKind extends UploadKind = UploadKind> = {\n readonly __kind?: TKind | undefined;\n transform(ctx: TransformContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type UploadTransformFunction<TKind extends UploadKind = UploadKind> = ((\n ctx: TransformContext\n) => File | PreparedUploadFile | Promise<File | PreparedUploadFile>) & {\n readonly __kind?: TKind | undefined;\n};\n\nexport type CompatibleTransform<TKind extends UploadKind> = TKind extends \"any\" | \"custom\"\n ? UploadTransform<UploadKind> | UploadTransformFunction<UploadKind>\n : UploadTransform<TKind> | UploadTransform<\"any\"> | UploadTransformFunction<TKind> | UploadTransformFunction<\"any\">;\n\nexport type OutputContext = PreparedUploadFile & {\n primary: UploadedFile;\n};\n\nexport type UploadOutput<TKind extends UploadKind = UploadKind, TName extends string = string> = {\n readonly __kind?: TKind | undefined;\n name: TName;\n produce(ctx: OutputContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type CompatibleOutput<TKind extends UploadKind, TName extends string = string> = TKind extends \"any\" | \"custom\"\n ? UploadOutput<UploadKind, TName>\n : UploadOutput<TKind, TName> | UploadOutput<\"any\", TName>;\n\nexport type UploadRouteDefinition = {\n kind: UploadKind;\n maxBytes?: number;\n minBytes?: number;\n multiple: boolean;\n multipleLimit?: number;\n auth?: Middleware<unknown>;\n overrideAuth: boolean;\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\n validate?: (ctx: {\n req: Request;\n file: UploadInputFile;\n user: unknown;\n meta: unknown;\n }) => true | string | Promise<true | string>;\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\n extensions?: string[];\n mimeTypes?: string[];\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\n requireSquare?: boolean;\n aspectRatio?: `${number}:${number}`;\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\n schema?: StandardSchema;\n headers?: string[];\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\n pageRule?: { min?: number; max?: number };\n encrypted?: boolean;\n durationRule?: { min?: DurationValue; max?: DurationValue };\n transforms?: Array<UploadTransform | UploadTransformFunction>;\n outputs?: Array<UploadOutput>;\n};\n\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\n\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\n storage: StorageAdapter;\n routes: TRoutes;\n middleware?: Middleware<unknown> | undefined;\n onUploadComplete?: ((ctx: {\n route: keyof TRoutes & string;\n result: UploadedFile | UploadedFile[];\n user: unknown;\n }) => void | Promise<void>) | undefined;\n};\n\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\n ? TMultiple extends true\n ? true\n : false\n : false;\n\nexport type OutputNames<TRoute> = TRoute extends { __outputs?: infer TOutputNames }\n ? TOutputNames extends string\n ? TOutputNames\n : never\n : never;\n\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true\n ? Array<ClientUploadedFile<OutputNames<TRoute>>>\n : ClientUploadedFile<OutputNames<TRoute>>;\n\nexport type UploadClient<TApp extends UpliftApp> = {\n [TRouteName in keyof TApp[\"routes\"] & string]: (\n input: ClientInput<TApp[\"routes\"][TRouteName]>\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACuCO,IAAM,cAAN,cAA0B,MAAM;AAAA,EAC5B;AAAA,EAET,YAAY,MAAuB,SAAiB;AAClD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,SAAS;AACP,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,IACb;AAAA,EACF;AACF;;;ADlDO,SAAS,mBACd,SACA,UAA4F,CAAC,GACzE;AACpB,QAAM,UAAU,QAAQ,SAAS;AAEjC,SAAO,IAAI,MAAM,CAAC,GAAG;AAAA,IACnB,IAAI,SAAS,UAAU;AACrB,UAAI,OAAO,aAAa,SAAU,QAAO;AACzC,aAAO,OAAO,UAAoC;AAChD,cAAM,QAAQ,iBAAiB,OAAO,CAAC,KAAK,IAAI,MAAM,KAAK,KAAK;AAChE,cAAM,OAAO,IAAI,SAAS;AAC1B,cAAM,QAAQ,MAAM,WAAW,IAAI,SAAS;AAC5C,mBAAW,QAAQ,MAAO,MAAK,OAAO,OAAO,IAAI;AAEjD,cAAM,MAAM,SAAS,SAAS,QAAQ;AACtC,YAAI,CAAC,QAAQ,SAAS,OAAO,mBAAmB,aAAa;AAC3D,iBAAO,cAAc,KAAK,MAAM,UAAU,QAAQ,UAAU;AAAA,QAC9D;AAEA,gBAAQ,aAAa,UAAU,CAAC;AAChC,cAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,QAAQ,QAAQ,MAAM,KAAK,CAAC;AAClE,cAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAI,CAAC,SAAS,IAAI;AAChB,gBAAM,IAAI,YAAa,KAAK,OAAO,QAAQ,WAAqB,KAAK,OAAO,WAAW,gBAAgB;AAAA,QACzG;AACA,gBAAQ,aAAa,UAAU,GAAG;AAClC,eAAO,oBAAoB,KAAK,MAAM;AAAA,MACxC;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,SAAS,SAAiB,OAAuB;AACxD,QAAM,MAAM,IAAI,IAAI,SAAS,WAAW,UAAU,QAAQ,kBAAkB;AAC5E,MAAI,aAAa,IAAI,SAAS,KAAK;AACnC,QAAM,QAAQ,IAAI,SAAS;AAC3B,SAAO,QAAQ,WAAW,GAAG,IAAI,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM,KAAK;AACpE;AAEA,SAAS,cACP,KACA,MACA,OACA,YACkB;AAClB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,MAAM,IAAI,eAAe;AAC/B,QAAI,KAAK,QAAQ,GAAG;AACpB,QAAI,OAAO,aAAa,CAAC,UAAU;AACjC,UAAI,CAAC,MAAM,iBAAkB;AAC7B,mBAAa,OAAO,KAAK,MAAO,MAAM,SAAS,MAAM,QAAS,GAAG,CAAC;AAAA,IACpE;AACA,QAAI,SAAS,MAAM;AACjB,YAAM,OAAO,KAAK,MAAM,IAAI,gBAAgB,IAAI;AAIhD,UAAI,IAAI,SAAS,OAAO,IAAI,UAAU,KAAK;AACzC,eAAO,IAAI,YAAa,KAAK,OAAO,QAAQ,WAAqB,KAAK,OAAO,WAAW,gBAAgB,CAAC;AACzG;AAAA,MACF;AACA,mBAAa,OAAO,GAAG;AACvB,cAAQ,oBAAoB,KAAK,MAAM,CAAC;AAAA,IAC1C;AACA,QAAI,UAAU,MAAM,OAAO,IAAI,YAAY,iBAAiB,wBAAwB,CAAC;AACrF,iBAAa,OAAO,CAAC;AACrB,QAAI,KAAK,IAAI;AAAA,EACf,CAAC;AACH;AAEA,SAAS,oBAAoB,QAA0B;AACrD,MAAI,MAAM,QAAQ,MAAM,EAAG,QAAO,OAAO,IAAI,CAAC,SAAS,oBAAoB,IAAI,CAAC;AAChF,MAAI,CAAC,mBAAmB,MAAM,EAAG,QAAO;AACxC,MAAI,CAAC,OAAO,UAAU,eAAe,KAAK,QAAQ,QAAQ,GAAG;AAC3D,WAAO,eAAe,QAAQ,UAAU;AAAA,MACtC,YAAY;AAAA,MACZ,MAAM,MAAc;AAClB,cAAM,SAAS,OAAO,UAAU,IAAI;AACpC,YAAI,CAAC,OAAQ,OAAM,IAAI,YAAY,qBAAqB,mBAAmB,IAAI,EAAE;AACjF,eAAO,oBAAoB,MAAM;AAAA,MACnC;AAAA,IACF,CAAC;AAAA,EACH;AACA,MAAI,OAAO,SAAS;AAClB,eAAW,UAAU,OAAO,OAAO,OAAO,OAAO,EAAG,qBAAoB,MAAM;AAAA,EAChF;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,OAG1B;AACA,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;","names":[]}
package/dist/client.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as UpliftApp, j as UploadClient } from './types-BdcszAj8.cjs';
1
+ import { h as UpliftApp, n as UploadClient } from './types-Cw4fuNs_.cjs';
2
2
 
3
3
  type UploadProgressHandler = (progress: number) => void;
4
4
  declare function createUploadClient<TApp extends UpliftApp>(baseUrl: string, options?: {
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { g as UpliftApp, j as UploadClient } from './types-BdcszAj8.js';
1
+ import { h as UpliftApp, n as UploadClient } from './types-Cw4fuNs_.js';
2
2
 
3
3
  type UploadProgressHandler = (progress: number) => void;
4
4
  declare function createUploadClient<TApp extends UpliftApp>(baseUrl: string, options?: {
package/dist/client.js CHANGED
@@ -36,7 +36,7 @@ function createUploadClient(baseUrl, options = {}) {
36
36
  throw new UploadError(body.error?.code ?? "UNKNOWN", body.error?.message ?? "Upload failed.");
37
37
  }
38
38
  options.onProgress?.(property, 100);
39
- return body.result;
39
+ return attachOutputGetters(body.result);
40
40
  };
41
41
  }
42
42
  });
@@ -62,13 +62,34 @@ function uploadWithXhr(url, form, route, onProgress) {
62
62
  return;
63
63
  }
64
64
  onProgress?.(route, 100);
65
- resolve(body.result);
65
+ resolve(attachOutputGetters(body.result));
66
66
  };
67
67
  xhr.onerror = () => reject(new UploadError("UPLOAD_FAILED", "Upload request failed."));
68
68
  onProgress?.(route, 0);
69
69
  xhr.send(form);
70
70
  });
71
71
  }
72
+ function attachOutputGetters(result) {
73
+ if (Array.isArray(result)) return result.map((item) => attachOutputGetters(item));
74
+ if (!isUploadedFileLike(result)) return result;
75
+ if (!Object.prototype.hasOwnProperty.call(result, "output")) {
76
+ Object.defineProperty(result, "output", {
77
+ enumerable: false,
78
+ value(name) {
79
+ const output = result.outputs?.[name];
80
+ if (!output) throw new UploadError("VALIDATION_FAILED", `Unknown output: ${name}`);
81
+ return attachOutputGetters(output);
82
+ }
83
+ });
84
+ }
85
+ if (result.outputs) {
86
+ for (const output of Object.values(result.outputs)) attachOutputGetters(output);
87
+ }
88
+ return result;
89
+ }
90
+ function isUploadedFileLike(value) {
91
+ return typeof value === "object" && value !== null;
92
+ }
72
93
  export {
73
94
  createUploadClient
74
95
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts","../src/client.ts"],"sourcesContent":["export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\r\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\r\n\r\nexport type UploadInputFile = {\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string;\r\n file?: File;\r\n};\r\n\r\nexport type UploadedFile = {\r\n url: string;\r\n key: string;\r\n name: string;\r\n type: string;\r\n size: number;\r\n extension?: string | undefined;\r\n provider: string;\r\n};\r\n\r\nexport type UploadErrorCode =\r\n | \"FILE_TOO_LARGE\"\r\n | \"FILE_TOO_SMALL\"\r\n | \"INVALID_TYPE\"\r\n | \"AUTH_FAILED\"\r\n | \"VALIDATION_FAILED\"\r\n | \"UPLOAD_FAILED\"\r\n | \"UNKNOWN\";\r\n\r\nexport class UploadError extends Error {\r\n readonly code: UploadErrorCode;\r\n\r\n constructor(code: UploadErrorCode, message: string) {\r\n super(message);\r\n this.name = \"UploadError\";\r\n this.code = code;\r\n }\r\n\r\n toJSON() {\r\n return {\r\n message: this.message,\r\n code: this.code\r\n };\r\n }\r\n}\r\n\r\nexport type StandardSchema<T = unknown> = {\r\n parse(input: unknown): T;\r\n};\r\n\r\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: TAuth;\r\n meta: TMeta;\r\n};\r\n\r\nexport type DoneContext<\r\n TAuth = unknown,\r\n TMeta = unknown,\r\n TMultiple extends boolean = false\r\n> = TMultiple extends true\r\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\r\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\r\n\r\nexport type StoragePutInput = {\r\n key: string;\r\n file: UploadInputFile;\r\n body: File;\r\n};\r\n\r\nexport type StorageAdapter = {\r\n provider: string;\r\n put(input: StoragePutInput): Promise<UploadedFile>;\r\n};\r\n\r\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\r\n\r\nexport type UploadKind =\r\n | \"any\"\r\n | \"image\"\r\n | \"pdf\"\r\n | \"video\"\r\n | \"audio\"\r\n | \"text\"\r\n | \"json\"\r\n | \"csv\"\r\n | \"custom\";\r\n\r\nexport type UploadRouteDefinition = {\r\n kind: UploadKind;\r\n maxBytes?: number;\r\n minBytes?: number;\r\n multiple: boolean;\r\n multipleLimit?: number;\r\n auth?: Middleware<unknown>;\r\n overrideAuth: boolean;\r\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\r\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\r\n validate?: (ctx: {\r\n req: Request;\r\n file: UploadInputFile;\r\n user: unknown;\r\n meta: unknown;\r\n }) => true | string | Promise<true | string>;\r\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\r\n extensions?: string[];\r\n mimeTypes?: string[];\r\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\r\n requireSquare?: boolean;\r\n aspectRatio?: `${number}:${number}`;\r\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\r\n schema?: StandardSchema;\r\n headers?: string[];\r\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\r\n pageRule?: { min?: number; max?: number };\r\n encrypted?: boolean;\r\n durationRule?: { min?: DurationValue; max?: DurationValue };\r\n};\r\n\r\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\r\n\r\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\r\n storage: StorageAdapter;\r\n routes: TRoutes;\r\n middleware?: Middleware<unknown> | undefined;\r\n onUploadComplete?: ((ctx: {\r\n route: keyof TRoutes & string;\r\n result: UploadedFile | UploadedFile[];\r\n user: unknown;\r\n }) => void | Promise<void>) | undefined;\r\n};\r\n\r\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\r\n ? TMultiple extends true\r\n ? true\r\n : false\r\n : false;\r\n\r\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\r\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true ? UploadedFile[] : UploadedFile;\r\n\r\nexport type UploadClient<TApp extends UpliftApp> = {\r\n [TRouteName in keyof TApp[\"routes\"] & string]: (\r\n input: ClientInput<TApp[\"routes\"][TRouteName]>\r\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\r\n};\r\n","import { UploadError, type UpliftApp, type UploadClient } from \"./types\";\r\n\r\nexport type UploadProgressHandler = (progress: number) => void;\r\n\r\nexport function createUploadClient<TApp extends UpliftApp>(\r\n baseUrl: string,\r\n options: { fetch?: typeof fetch; onProgress?: (route: string, progress: number) => void } = {}\r\n): UploadClient<TApp> {\r\n const fetcher = options.fetch ?? fetch;\r\n\r\n return new Proxy({}, {\r\n get(_target, property) {\r\n if (typeof property !== \"string\") return undefined;\r\n return async (input: File | File[] | FileList) => {\r\n const files = input instanceof File ? [input] : Array.from(input);\r\n const form = new FormData();\r\n const field = files.length === 1 ? \"file\" : \"files\";\r\n for (const file of files) form.append(field, file);\r\n\r\n const url = routeUrl(baseUrl, property);\r\n if (!options.fetch && typeof XMLHttpRequest !== \"undefined\") {\r\n return uploadWithXhr(url, form, property, options.onProgress);\r\n }\r\n\r\n options.onProgress?.(property, 0);\r\n const response = await fetcher(url, { method: \"POST\", body: form });\r\n const body = await response.json() as { result?: unknown; error?: { code: string; message: string } };\r\n if (!response.ok) {\r\n throw new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\");\r\n }\r\n options.onProgress?.(property, 100);\r\n return body.result;\r\n };\r\n }\r\n }) as UploadClient<TApp>;\r\n}\r\n\r\nfunction routeUrl(baseUrl: string, route: string): string {\r\n const url = new URL(baseUrl, globalThis.location?.href ?? \"http://localhost\");\r\n url.searchParams.set(\"route\", route);\r\n const value = url.toString();\r\n return baseUrl.startsWith(\"/\") ? `${url.pathname}${url.search}` : value;\r\n}\r\n\r\nfunction uploadWithXhr(\r\n url: string,\r\n form: FormData,\r\n route: string,\r\n onProgress?: (route: string, progress: number) => void\r\n): Promise<unknown> {\r\n return new Promise((resolve, reject) => {\r\n const xhr = new XMLHttpRequest();\r\n xhr.open(\"POST\", url);\r\n xhr.upload.onprogress = (event) => {\r\n if (!event.lengthComputable) return;\r\n onProgress?.(route, Math.round((event.loaded / event.total) * 100));\r\n };\r\n xhr.onload = () => {\r\n const body = JSON.parse(xhr.responseText || \"{}\") as {\r\n result?: unknown;\r\n error?: { code: string; message: string };\r\n };\r\n if (xhr.status < 200 || xhr.status >= 300) {\r\n reject(new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\"));\r\n return;\r\n }\r\n onProgress?.(route, 100);\r\n resolve(body.result);\r\n };\r\n xhr.onerror = () => reject(new UploadError(\"UPLOAD_FAILED\", \"Upload request failed.\"));\r\n onProgress?.(route, 0);\r\n xhr.send(form);\r\n });\r\n}\r\n"],"mappings":";AA8BO,IAAM,cAAN,cAA0B,MAAM;AAAA,EAC5B;AAAA,EAET,YAAY,MAAuB,SAAiB;AAClD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,SAAS;AACP,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,IACb;AAAA,EACF;AACF;;;ACzCO,SAAS,mBACd,SACA,UAA4F,CAAC,GACzE;AACpB,QAAM,UAAU,QAAQ,SAAS;AAEjC,SAAO,IAAI,MAAM,CAAC,GAAG;AAAA,IACnB,IAAI,SAAS,UAAU;AACrB,UAAI,OAAO,aAAa,SAAU,QAAO;AACzC,aAAO,OAAO,UAAoC;AAChD,cAAM,QAAQ,iBAAiB,OAAO,CAAC,KAAK,IAAI,MAAM,KAAK,KAAK;AAChE,cAAM,OAAO,IAAI,SAAS;AAC1B,cAAM,QAAQ,MAAM,WAAW,IAAI,SAAS;AAC5C,mBAAW,QAAQ,MAAO,MAAK,OAAO,OAAO,IAAI;AAEjD,cAAM,MAAM,SAAS,SAAS,QAAQ;AACtC,YAAI,CAAC,QAAQ,SAAS,OAAO,mBAAmB,aAAa;AAC3D,iBAAO,cAAc,KAAK,MAAM,UAAU,QAAQ,UAAU;AAAA,QAC9D;AAEA,gBAAQ,aAAa,UAAU,CAAC;AAChC,cAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,QAAQ,QAAQ,MAAM,KAAK,CAAC;AAClE,cAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAI,CAAC,SAAS,IAAI;AAChB,gBAAM,IAAI,YAAa,KAAK,OAAO,QAAQ,WAAqB,KAAK,OAAO,WAAW,gBAAgB;AAAA,QACzG;AACA,gBAAQ,aAAa,UAAU,GAAG;AAClC,eAAO,KAAK;AAAA,MACd;AAAA,IACF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,SAAS,SAAiB,OAAuB;AACxD,QAAM,MAAM,IAAI,IAAI,SAAS,WAAW,UAAU,QAAQ,kBAAkB;AAC5E,MAAI,aAAa,IAAI,SAAS,KAAK;AACnC,QAAM,QAAQ,IAAI,SAAS;AAC3B,SAAO,QAAQ,WAAW,GAAG,IAAI,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM,KAAK;AACpE;AAEA,SAAS,cACP,KACA,MACA,OACA,YACkB;AAClB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,MAAM,IAAI,eAAe;AAC/B,QAAI,KAAK,QAAQ,GAAG;AACpB,QAAI,OAAO,aAAa,CAAC,UAAU;AACjC,UAAI,CAAC,MAAM,iBAAkB;AAC7B,mBAAa,OAAO,KAAK,MAAO,MAAM,SAAS,MAAM,QAAS,GAAG,CAAC;AAAA,IACpE;AACA,QAAI,SAAS,MAAM;AACjB,YAAM,OAAO,KAAK,MAAM,IAAI,gBAAgB,IAAI;AAIhD,UAAI,IAAI,SAAS,OAAO,IAAI,UAAU,KAAK;AACzC,eAAO,IAAI,YAAa,KAAK,OAAO,QAAQ,WAAqB,KAAK,OAAO,WAAW,gBAAgB,CAAC;AACzG;AAAA,MACF;AACA,mBAAa,OAAO,GAAG;AACvB,cAAQ,KAAK,MAAM;AAAA,IACrB;AACA,QAAI,UAAU,MAAM,OAAO,IAAI,YAAY,iBAAiB,wBAAwB,CAAC;AACrF,iBAAa,OAAO,CAAC;AACrB,QAAI,KAAK,IAAI;AAAA,EACf,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts","../src/client.ts"],"sourcesContent":["export type SizeValue = `${number}b` | `${number}kb` | `${number}mb` | `${number}gb`;\nexport type DurationValue = `${number}s` | `${number}m` | `${number}h`;\n\nexport type UploadInputFile = {\n name: string;\n type: string;\n size: number;\n extension?: string;\n file?: File;\n};\n\nexport type UploadedFile = {\n url: string;\n key: string;\n name: string;\n type: string;\n size: number;\n extension?: string | undefined;\n provider: string;\n outputs?: Record<string, UploadedFile> | undefined;\n};\n\nexport type ClientUploadedFile<TOutputNames extends string = never> = UploadedFile & (\n [TOutputNames] extends [never]\n ? object\n : {\n output<TName extends TOutputNames>(name: TName): UploadedFile;\n }\n);\n\nexport type UploadErrorCode =\n | \"FILE_TOO_LARGE\"\n | \"FILE_TOO_SMALL\"\n | \"INVALID_TYPE\"\n | \"AUTH_FAILED\"\n | \"VALIDATION_FAILED\"\n | \"UPLOAD_FAILED\"\n | \"UNKNOWN\";\n\nexport class UploadError extends Error {\n readonly code: UploadErrorCode;\n\n constructor(code: UploadErrorCode, message: string) {\n super(message);\n this.name = \"UploadError\";\n this.code = code;\n }\n\n toJSON() {\n return {\n message: this.message,\n code: this.code\n };\n }\n}\n\nexport type StandardSchema<T = unknown> = {\n parse(input: unknown): T;\n};\n\nexport type KeyContext<TAuth = unknown, TMeta = unknown> = {\n req: Request;\n file: UploadInputFile;\n user: TAuth;\n meta: TMeta;\n};\n\nexport type DoneContext<\n TAuth = unknown,\n TMeta = unknown,\n TMultiple extends boolean = false\n> = TMultiple extends true\n ? { req: Request; files: UploadedFile[]; user: TAuth; meta: TMeta[] }\n : { req: Request; file: UploadedFile; user: TAuth; meta: TMeta };\n\nexport type StoragePutInput = {\n key: string;\n file: UploadInputFile;\n body: File;\n};\n\nexport type StorageAdapter = {\n provider: string;\n put(input: StoragePutInput): Promise<UploadedFile>;\n delete?(key: string): Promise<void>;\n};\n\nexport type Middleware<TUser = unknown> = (ctx: { req: Request }) => TUser | Promise<TUser>;\n\nexport type UploadKind =\n | \"any\"\n | \"image\"\n | \"pdf\"\n | \"video\"\n | \"audio\"\n | \"text\"\n | \"json\"\n | \"csv\"\n | \"custom\";\n\nexport type PreparedUploadFile = {\n file: UploadInputFile;\n body: File;\n};\n\nexport type TransformContext = PreparedUploadFile;\n\nexport type UploadTransform<TKind extends UploadKind = UploadKind> = {\n readonly __kind?: TKind | undefined;\n transform(ctx: TransformContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type UploadTransformFunction<TKind extends UploadKind = UploadKind> = ((\n ctx: TransformContext\n) => File | PreparedUploadFile | Promise<File | PreparedUploadFile>) & {\n readonly __kind?: TKind | undefined;\n};\n\nexport type CompatibleTransform<TKind extends UploadKind> = TKind extends \"any\" | \"custom\"\n ? UploadTransform<UploadKind> | UploadTransformFunction<UploadKind>\n : UploadTransform<TKind> | UploadTransform<\"any\"> | UploadTransformFunction<TKind> | UploadTransformFunction<\"any\">;\n\nexport type OutputContext = PreparedUploadFile & {\n primary: UploadedFile;\n};\n\nexport type UploadOutput<TKind extends UploadKind = UploadKind, TName extends string = string> = {\n readonly __kind?: TKind | undefined;\n name: TName;\n produce(ctx: OutputContext): File | PreparedUploadFile | Promise<File | PreparedUploadFile>;\n};\n\nexport type CompatibleOutput<TKind extends UploadKind, TName extends string = string> = TKind extends \"any\" | \"custom\"\n ? UploadOutput<UploadKind, TName>\n : UploadOutput<TKind, TName> | UploadOutput<\"any\", TName>;\n\nexport type UploadRouteDefinition = {\n kind: UploadKind;\n maxBytes?: number;\n minBytes?: number;\n multiple: boolean;\n multipleLimit?: number;\n auth?: Middleware<unknown>;\n overrideAuth: boolean;\n key?: (ctx: KeyContext<unknown, unknown>) => string | Promise<string>;\n meta?: (ctx: { req: Request; file: UploadInputFile; user: unknown }) => unknown | Promise<unknown>;\n validate?: (ctx: {\n req: Request;\n file: UploadInputFile;\n user: unknown;\n meta: unknown;\n }) => true | string | Promise<true | string>;\n done?: (ctx: DoneContext<unknown, unknown, boolean>) => void | Promise<void>;\n extensions?: string[];\n mimeTypes?: string[];\n dimensionRule?: { minWidth?: number; minHeight?: number; maxWidth?: number; maxHeight?: number };\n requireSquare?: boolean;\n aspectRatio?: `${number}:${number}`;\n encoding?: \"utf-8\" | \"utf-16\" | \"ascii\";\n schema?: StandardSchema;\n headers?: string[];\n delimiter?: \",\" | \";\" | \"\\t\" | \"|\";\n pageRule?: { min?: number; max?: number };\n encrypted?: boolean;\n durationRule?: { min?: DurationValue; max?: DurationValue };\n transforms?: Array<UploadTransform | UploadTransformFunction>;\n outputs?: Array<UploadOutput>;\n};\n\nexport type UploadRoutes = Record<string, { _def: UploadRouteDefinition }>;\n\nexport type UpliftApp<TRoutes extends UploadRoutes = UploadRoutes> = {\n storage: StorageAdapter;\n routes: TRoutes;\n middleware?: Middleware<unknown> | undefined;\n onUploadComplete?: ((ctx: {\n route: keyof TRoutes & string;\n result: UploadedFile | UploadedFile[];\n user: unknown;\n }) => void | Promise<void>) | undefined;\n};\n\nexport type IsMultiple<TRoute> = TRoute extends { __multiple?: infer TMultiple }\n ? TMultiple extends true\n ? true\n : false\n : false;\n\nexport type OutputNames<TRoute> = TRoute extends { __outputs?: infer TOutputNames }\n ? TOutputNames extends string\n ? TOutputNames\n : never\n : never;\n\nexport type ClientInput<TRoute> = IsMultiple<TRoute> extends true ? File[] | FileList : File;\nexport type ClientOutput<TRoute> = IsMultiple<TRoute> extends true\n ? Array<ClientUploadedFile<OutputNames<TRoute>>>\n : ClientUploadedFile<OutputNames<TRoute>>;\n\nexport type UploadClient<TApp extends UpliftApp> = {\n [TRouteName in keyof TApp[\"routes\"] & string]: (\n input: ClientInput<TApp[\"routes\"][TRouteName]>\n ) => Promise<ClientOutput<TApp[\"routes\"][TRouteName]>>;\n};\n","import { UploadError, type UpliftApp, type UploadClient } from \"./types\";\n\nexport type UploadProgressHandler = (progress: number) => void;\n\nexport function createUploadClient<TApp extends UpliftApp>(\n baseUrl: string,\n options: { fetch?: typeof fetch; onProgress?: (route: string, progress: number) => void } = {}\n): UploadClient<TApp> {\n const fetcher = options.fetch ?? fetch;\n\n return new Proxy({}, {\n get(_target, property) {\n if (typeof property !== \"string\") return undefined;\n return async (input: File | File[] | FileList) => {\n const files = input instanceof File ? [input] : Array.from(input);\n const form = new FormData();\n const field = files.length === 1 ? \"file\" : \"files\";\n for (const file of files) form.append(field, file);\n\n const url = routeUrl(baseUrl, property);\n if (!options.fetch && typeof XMLHttpRequest !== \"undefined\") {\n return uploadWithXhr(url, form, property, options.onProgress);\n }\n\n options.onProgress?.(property, 0);\n const response = await fetcher(url, { method: \"POST\", body: form });\n const body = await response.json() as { result?: unknown; error?: { code: string; message: string } };\n if (!response.ok) {\n throw new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\");\n }\n options.onProgress?.(property, 100);\n return attachOutputGetters(body.result);\n };\n }\n }) as UploadClient<TApp>;\n}\n\nfunction routeUrl(baseUrl: string, route: string): string {\n const url = new URL(baseUrl, globalThis.location?.href ?? \"http://localhost\");\n url.searchParams.set(\"route\", route);\n const value = url.toString();\n return baseUrl.startsWith(\"/\") ? `${url.pathname}${url.search}` : value;\n}\n\nfunction uploadWithXhr(\n url: string,\n form: FormData,\n route: string,\n onProgress?: (route: string, progress: number) => void\n): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(\"POST\", url);\n xhr.upload.onprogress = (event) => {\n if (!event.lengthComputable) return;\n onProgress?.(route, Math.round((event.loaded / event.total) * 100));\n };\n xhr.onload = () => {\n const body = JSON.parse(xhr.responseText || \"{}\") as {\n result?: unknown;\n error?: { code: string; message: string };\n };\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(new UploadError((body.error?.code ?? \"UNKNOWN\") as never, body.error?.message ?? \"Upload failed.\"));\n return;\n }\n onProgress?.(route, 100);\n resolve(attachOutputGetters(body.result));\n };\n xhr.onerror = () => reject(new UploadError(\"UPLOAD_FAILED\", \"Upload request failed.\"));\n onProgress?.(route, 0);\n xhr.send(form);\n });\n}\n\nfunction attachOutputGetters(result: unknown): unknown {\n if (Array.isArray(result)) return result.map((item) => attachOutputGetters(item));\n if (!isUploadedFileLike(result)) return result;\n if (!Object.prototype.hasOwnProperty.call(result, \"output\")) {\n Object.defineProperty(result, \"output\", {\n enumerable: false,\n value(name: string) {\n const output = result.outputs?.[name];\n if (!output) throw new UploadError(\"VALIDATION_FAILED\", `Unknown output: ${name}`);\n return attachOutputGetters(output);\n }\n });\n }\n if (result.outputs) {\n for (const output of Object.values(result.outputs)) attachOutputGetters(output);\n }\n return result;\n}\n\nfunction isUploadedFileLike(value: unknown): value is {\n outputs?: Record<string, unknown>;\n output?: (name: string) => unknown;\n} {\n return typeof value === \"object\" && value !== null;\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -73,6 +73,7 @@ var UploadBuilder = class {
73
73
  __meta;
74
74
  __multiple;
75
75
  __kind;
76
+ __outputs;
76
77
  _def;
77
78
  constructor(kind, mimeTypes = [], extensions = []) {
78
79
  this._def = {
@@ -166,6 +167,14 @@ var UploadBuilder = class {
166
167
  this._def.durationRule = rule;
167
168
  return this;
168
169
  }
170
+ transform(...transforms) {
171
+ this._def.transforms = [...this._def.transforms ?? [], ...transforms];
172
+ return this;
173
+ }
174
+ outputs(...outputs) {
175
+ this._def.outputs = [...this._def.outputs ?? [], ...outputs];
176
+ return this;
177
+ }
169
178
  };
170
179
  function any() {
171
180
  return new UploadBuilder("any");
@@ -218,7 +227,7 @@ function createUploadClient(baseUrl, options = {}) {
218
227
  throw new UploadError(body.error?.code ?? "UNKNOWN", body.error?.message ?? "Upload failed.");
219
228
  }
220
229
  options.onProgress?.(property, 100);
221
- return body.result;
230
+ return attachOutputGetters(body.result);
222
231
  };
223
232
  }
224
233
  });
@@ -244,13 +253,34 @@ function uploadWithXhr(url, form, route, onProgress) {
244
253
  return;
245
254
  }
246
255
  onProgress?.(route, 100);
247
- resolve(body.result);
256
+ resolve(attachOutputGetters(body.result));
248
257
  };
249
258
  xhr.onerror = () => reject(new UploadError("UPLOAD_FAILED", "Upload request failed."));
250
259
  onProgress?.(route, 0);
251
260
  xhr.send(form);
252
261
  });
253
262
  }
263
+ function attachOutputGetters(result) {
264
+ if (Array.isArray(result)) return result.map((item) => attachOutputGetters(item));
265
+ if (!isUploadedFileLike(result)) return result;
266
+ if (!Object.prototype.hasOwnProperty.call(result, "output")) {
267
+ Object.defineProperty(result, "output", {
268
+ enumerable: false,
269
+ value(name) {
270
+ const output = result.outputs?.[name];
271
+ if (!output) throw new UploadError("VALIDATION_FAILED", `Unknown output: ${name}`);
272
+ return attachOutputGetters(output);
273
+ }
274
+ });
275
+ }
276
+ if (result.outputs) {
277
+ for (const output of Object.values(result.outputs)) attachOutputGetters(output);
278
+ }
279
+ return result;
280
+ }
281
+ function isUploadedFileLike(value) {
282
+ return typeof value === "object" && value !== null;
283
+ }
254
284
 
255
285
  // src/index.ts
256
286
  function uplift(config) {