ocel 0.0.0 → 0.0.1-alpha.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.
Files changed (74) hide show
  1. package/bin/run.js +39 -0
  2. package/dist/blob/bucket.d.ts +55 -0
  3. package/dist/blob/bucket.js +65 -0
  4. package/dist/blob/index.d.ts +8 -0
  5. package/dist/blob/index.js +13 -0
  6. package/dist/blob/integrations/client.d.ts +25 -0
  7. package/dist/blob/integrations/client.js +71 -0
  8. package/dist/blob/integrations/hono.d.ts +31 -0
  9. package/dist/blob/integrations/hono.js +11 -0
  10. package/dist/blob/mime-types.d.ts +1 -0
  11. package/dist/blob/mime-types.js +1 -0
  12. package/dist/blob/parse.d.ts +16 -0
  13. package/dist/blob/parse.js +69 -0
  14. package/dist/blob/s3-listener.d.ts +7 -0
  15. package/dist/blob/s3-listener.js +35 -0
  16. package/dist/blob/types.d.ts +128 -0
  17. package/dist/blob/types.js +1 -0
  18. package/dist/blob/upload-error.d.ts +35 -0
  19. package/dist/blob/upload-error.js +54 -0
  20. package/dist/blob/uploader.d.ts +67 -0
  21. package/dist/blob/uploader.js +323 -0
  22. package/dist/internal/db/index.d.ts +68 -0
  23. package/dist/internal/db/index.js +73 -0
  24. package/dist/lambda/index.d.ts +2 -0
  25. package/dist/lambda/index.js +4 -0
  26. package/dist/lambda/integrations/express.d.ts +3 -0
  27. package/dist/lambda/integrations/express.js +13 -0
  28. package/dist/lambda/integrations/hono.d.ts +3 -0
  29. package/dist/lambda/integrations/hono.js +13 -0
  30. package/dist/lambda/lambda.d.ts +43 -0
  31. package/dist/lambda/lambda.js +30 -0
  32. package/dist/postgres/index.d.ts +2 -0
  33. package/dist/postgres/index.js +4 -0
  34. package/dist/postgres/pg.d.ts +11 -0
  35. package/dist/postgres/pg.js +24 -0
  36. package/dist/utils/component.d.ts +3 -0
  37. package/dist/utils/component.js +1 -0
  38. package/dist/utils/constants.d.ts +2 -0
  39. package/dist/utils/constants.js +2 -0
  40. package/dist/utils/nanoid.d.ts +1 -0
  41. package/dist/utils/nanoid.js +6 -0
  42. package/dist/utils/rpc.d.ts +11 -0
  43. package/dist/utils/rpc.js +17 -0
  44. package/dist/utils/stack.d.ts +4 -0
  45. package/dist/utils/stack.js +23 -0
  46. package/dist/workflow/index.d.ts +3 -0
  47. package/dist/workflow/index.js +4 -0
  48. package/dist/workflow/workflow.d.ts +34 -0
  49. package/dist/workflow/workflow.js +146 -0
  50. package/package.json +93 -8
  51. package/src/blob/bucket.ts +92 -0
  52. package/src/blob/index.ts +34 -0
  53. package/src/blob/integrations/client.ts +127 -0
  54. package/src/blob/integrations/hono.ts +16 -0
  55. package/src/blob/mime-types.ts +68 -0
  56. package/src/blob/parse.ts +93 -0
  57. package/src/blob/s3-listener.ts +48 -0
  58. package/src/blob/types.ts +142 -0
  59. package/src/blob/upload-error.ts +67 -0
  60. package/src/blob/uploader.ts +426 -0
  61. package/src/internal/db/index.ts +78 -0
  62. package/src/lambda/index.ts +8 -0
  63. package/src/lambda/integrations/express.ts +25 -0
  64. package/src/lambda/integrations/hono.ts +25 -0
  65. package/src/lambda/lambda.ts +94 -0
  66. package/src/postgres/index.ts +5 -0
  67. package/src/postgres/pg.ts +33 -0
  68. package/src/utils/component.ts +3 -0
  69. package/src/utils/constants.ts +2 -0
  70. package/src/utils/nanoid.ts +9 -0
  71. package/src/utils/rpc.ts +28 -0
  72. package/src/utils/stack.ts +26 -0
  73. package/src/workflow/index.ts +9 -0
  74. package/src/workflow/workflow.ts +235 -0
@@ -0,0 +1,30 @@
1
+ import { rpc } from "../utils/rpc";
2
+ import { getCallerFile } from "../utils/stack";
3
+ export class Lambda {
4
+ id;
5
+ constructor(id, config) {
6
+ this.id = id;
7
+ // remove non-serializable stuff
8
+ const { handler, link, ...rest } = config;
9
+ const links = link?.map((component) => component.__id()) || [];
10
+ // @ts-expect-error - accessed by cli during build
11
+ this.__handler = handler;
12
+ // @ts-expect-error - accessed by cli during build
13
+ this[Symbol.for("ocel:lambda:id")] = id;
14
+ if (process.env.OCEL_PHASE === "discovery") {
15
+ const source = getCallerFile();
16
+ if (!source) {
17
+ throw new Error("Could not determine caller file for lambda registration");
18
+ }
19
+ rpc.register({
20
+ id: this.id,
21
+ type: "lambda",
22
+ source,
23
+ config: { ...rest, links },
24
+ });
25
+ }
26
+ }
27
+ __id() {
28
+ return this.id;
29
+ }
30
+ }
@@ -0,0 +1,2 @@
1
+ import { Postgres, type PostgresConfig } from "./pg";
2
+ export declare function postgres(id: string, config: PostgresConfig): Postgres;
@@ -0,0 +1,4 @@
1
+ import { Postgres } from "./pg";
2
+ export function postgres(id, config) {
3
+ return new Postgres(id, config);
4
+ }
@@ -0,0 +1,11 @@
1
+ import type { Component } from "../utils/component";
2
+ export interface PostgresConfig {
3
+ version?: string;
4
+ migrations?: string;
5
+ }
6
+ export declare class Postgres implements Component {
7
+ id: string;
8
+ constructor(id: string, config: PostgresConfig);
9
+ __id(): string;
10
+ sql(query: string): Promise<{}>;
11
+ }
@@ -0,0 +1,24 @@
1
+ import { rpc } from "../utils/rpc";
2
+ import { getCallerFile } from "../utils/stack";
3
+ export class Postgres {
4
+ id;
5
+ constructor(id, config) {
6
+ // remove non-serializable stuff
7
+ this.id = id;
8
+ if (process.env.OCEL_PHASE === "discovery") {
9
+ const source = getCallerFile();
10
+ rpc.register({
11
+ id: this.id,
12
+ type: "postgres",
13
+ source: source || "unknown",
14
+ config: {},
15
+ });
16
+ }
17
+ }
18
+ __id() {
19
+ return this.id;
20
+ }
21
+ async sql(query) {
22
+ return {};
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ export interface Component {
2
+ __id(): string;
3
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export declare const IS_DISCOVERY_PHASE: boolean;
2
+ export declare const IS_DEV_MODE: boolean;
@@ -0,0 +1,2 @@
1
+ export const IS_DISCOVERY_PHASE = process.env.OCEL_PHASE === "discovery";
2
+ export const IS_DEV_MODE = process.env.OCEL_DEV === "1";
@@ -0,0 +1 @@
1
+ export declare function getNanoid(length?: number): string;
@@ -0,0 +1,6 @@
1
+ import { customAlphabet } from "nanoid";
2
+ export function getNanoid(length = 7) {
3
+ const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
4
+ const nanoid = customAlphabet(alphabet, length);
5
+ return nanoid();
6
+ }
@@ -0,0 +1,11 @@
1
+ declare global {
2
+ var __ocelRegister: Promise<unknown>[];
3
+ }
4
+ export declare const rpc: {
5
+ register: (args: {
6
+ id: string;
7
+ type: "lambda" | "bucket" | "workflow" | "postgres";
8
+ source: string;
9
+ config?: Record<string, unknown>;
10
+ }) => Promise<void>;
11
+ };
@@ -0,0 +1,17 @@
1
+ export const rpc = {
2
+ register: async (args) => {
3
+ const SERVER_URL = process.env.OCEL_SERVER;
4
+ if (!SERVER_URL) {
5
+ throw new Error("OCEL_SERVER environment variable is not set");
6
+ }
7
+ const p = fetch(`${SERVER_URL}/register`, {
8
+ method: "POST",
9
+ headers: {
10
+ "Content-Type": "application/json",
11
+ },
12
+ body: JSON.stringify(args),
13
+ });
14
+ globalThis.__ocelRegister ??= [];
15
+ globalThis.__ocelRegister.push(p);
16
+ },
17
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Returns absolute path of the caller file.
3
+ */
4
+ export declare function getCallerFile(depth?: number): string | null;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Returns absolute path of the caller file.
3
+ */
4
+ export function getCallerFile(depth = 1) {
5
+ const original = Error.prepareStackTrace;
6
+ try {
7
+ Error.prepareStackTrace = (_, stack) => stack;
8
+ const err = new Error();
9
+ const frames = err.stack;
10
+ // index 0 = getCallerFile
11
+ // index 1 = the function that called getCallerFile
12
+ return (frames[2 + depth]
13
+ ?.getFileName()
14
+ ?.replace("file://", "")
15
+ ?.split("?")?.[0] ?? null);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ finally {
21
+ Error.prepareStackTrace = original;
22
+ }
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { ZodType } from "zod";
2
+ import { Workflow, type WorkflowConfig } from "./workflow";
3
+ export declare function workflow<TInput extends ZodType = never>(id: string, config: WorkflowConfig<TInput>): Workflow<TInput>;
@@ -0,0 +1,4 @@
1
+ import { Workflow } from "./workflow";
2
+ export function workflow(id, config) {
3
+ return new Workflow(id, config);
4
+ }
@@ -0,0 +1,34 @@
1
+ import type { z, ZodType } from "zod";
2
+ import type { Component } from "../utils/component";
3
+ import { type DurableContext } from "@aws/durable-execution-sdk-js";
4
+ export interface WorkflowConfig<TInput extends ZodType = never> {
5
+ input?: TInput;
6
+ queue?: {
7
+ concurrencyLimit?: number;
8
+ };
9
+ cron?: string;
10
+ run: [TInput] extends [never] ? (args: {
11
+ ctx: DurableContext;
12
+ }) => Promise<void> : (args: {
13
+ input: z.infer<TInput>;
14
+ ctx: DurableContext;
15
+ }) => Promise<void>;
16
+ link?: Component[];
17
+ retry?: {
18
+ maxAttempts: number;
19
+ backoffRate?: number;
20
+ intervalSeconds?: number;
21
+ maxTimeoutSeconds?: number;
22
+ } | false;
23
+ }
24
+ export declare class Workflow<TInput extends ZodType = never> implements Component {
25
+ private id;
26
+ constructor(id: string, config: WorkflowConfig<TInput>);
27
+ __id(): string;
28
+ trigger(payload: [TInput] extends [never] ? {} : z.infer<TInput>, opts?: {
29
+ idempotencyKey?: string;
30
+ }): void;
31
+ batchTrigger(): void;
32
+ triggerAndWait(): void;
33
+ batchTriggerAndWait(): void;
34
+ }
@@ -0,0 +1,146 @@
1
+ import { withDurableExecution, } from "@aws/durable-execution-sdk-js";
2
+ import { getCallerFile } from "../utils/stack";
3
+ import { rpc } from "../utils/rpc";
4
+ import { SpanStatusCode, trace } from "@opentelemetry/api";
5
+ export class Workflow {
6
+ id;
7
+ constructor(id, config) {
8
+ this.id = id;
9
+ const { run, input, link, ...rest } = config;
10
+ const links = link?.map((component) => component.__id()) || [];
11
+ const tracer = trace.getTracer("ocel/workflow");
12
+ const handler = (originalEvent, originalFnContext) => {
13
+ return withDurableExecution(async (event, durableCtx) => {
14
+ const ctx = durableCtx;
15
+ const isReplay = ctx.durableExecutionMode === "ReplayMode";
16
+ const ops = originalEvent.InitialExecutionState?.Operations || [];
17
+ const stepCompleted = (name) => {
18
+ return (isReplay &&
19
+ ops.some((op) => op.Name === name && op.Status === "SUCCEEDED"));
20
+ };
21
+ return tracer.startActiveSpan(isReplay ? `workflow-${id}-replay` : `workflow-${id}`, {
22
+ attributes: { workflow_id: id },
23
+ }, async (rootSpan) => {
24
+ // override context to add tracing
25
+ const instrumentedCtx = new Proxy(durableCtx, {
26
+ get(target, prop, receiver) {
27
+ if (prop === "step") {
28
+ return async (name, fn, opts) => {
29
+ if (stepCompleted(name)) {
30
+ return target.step(name, fn, opts);
31
+ }
32
+ return tracer.startActiveSpan(`step: ${name}`, async (stepSpan) => {
33
+ try {
34
+ console.info(`stepSpan isRecording=${stepSpan.isRecording()} spanContext=${JSON.stringify(stepSpan.spanContext())}`);
35
+ const result = await target.step(name, fn, opts);
36
+ stepSpan.setStatus({ code: SpanStatusCode.OK });
37
+ return result;
38
+ }
39
+ catch (err) {
40
+ // Only record legitimate errors, not Durable Suspend/Sleep interruptions
41
+ // (You can filter specifically if you know the Suspend error type)
42
+ stepSpan.recordException(err);
43
+ stepSpan.setStatus({ code: SpanStatusCode.ERROR });
44
+ throw err;
45
+ }
46
+ finally {
47
+ stepSpan.end();
48
+ }
49
+ });
50
+ };
51
+ }
52
+ if (prop === "wait") {
53
+ return async (name, opts) => {
54
+ if (stepCompleted(name)) {
55
+ return target.wait(name, opts);
56
+ }
57
+ return tracer.startActiveSpan(`wait: ${name}`, async (waitSpan) => {
58
+ console.info(`waitSpan isRecording=${waitSpan.isRecording()} spanContext=${JSON.stringify(waitSpan.spanContext())}`);
59
+ waitSpan.setAttribute("duration_seconds", opts.seconds);
60
+ waitSpan.setAttribute("workflow.status", "suspending");
61
+ console.log("[OTEL-FIX] Closing spans before suspend...");
62
+ waitSpan.setStatus({ code: SpanStatusCode.OK });
63
+ waitSpan.end();
64
+ rootSpan.setAttribute("workflow.suspend_trigger", name);
65
+ rootSpan.setStatus({ code: SpanStatusCode.OK });
66
+ rootSpan.end();
67
+ const provider = trace.getTracerProvider();
68
+ // @ts-ignore
69
+ if (typeof provider.forceFlush === "function") {
70
+ // @ts-ignore
71
+ await provider.forceFlush();
72
+ console.log("[OTEL-FIX] Flush complete.");
73
+ }
74
+ await target.wait(name, opts);
75
+ });
76
+ };
77
+ }
78
+ // Default: Return the original property (e.g., ctx.runId, ctx.now)
79
+ return Reflect.get(target, prop, receiver);
80
+ },
81
+ });
82
+ console.log("eventss", {
83
+ originalEvent,
84
+ originalFnContext,
85
+ });
86
+ const executionArn = originalEvent.DurableExecutionArn;
87
+ const parts = executionArn.split("/");
88
+ const executionId = `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
89
+ console.log("execution details", {
90
+ event,
91
+ originalCtx: durableCtx,
92
+ executionArn,
93
+ executionId,
94
+ });
95
+ rootSpan.setAttribute("execution_id", executionId);
96
+ try {
97
+ console.info(`startRootSpan isRecording=${rootSpan.isRecording()} spanContext=${JSON.stringify(rootSpan.spanContext())}`);
98
+ if (input) {
99
+ const parsed = input.parse(event);
100
+ await run({ input: parsed, ctx: instrumentedCtx });
101
+ }
102
+ else {
103
+ await run({ ctx: instrumentedCtx, input: event });
104
+ }
105
+ rootSpan.setStatus({ code: 1 });
106
+ }
107
+ catch (error) {
108
+ console.log("Error in workflow:", error);
109
+ throw error;
110
+ }
111
+ finally {
112
+ console.log("Ending span for workflow:", id);
113
+ console.log("Span details:", rootSpan);
114
+ if (rootSpan.isRecording()) {
115
+ console.log("Ending root span (normal exit)");
116
+ rootSpan.end();
117
+ }
118
+ }
119
+ });
120
+ })(originalEvent, originalFnContext);
121
+ };
122
+ // @ts-expect-error - accessed by cli during build
123
+ this.__handler = handler;
124
+ // @ts-expect-error - accessed by cli during build
125
+ this[Symbol.for("ocel:lambda:id")] = id;
126
+ if (process.env.OCEL_PHASE === "discovery") {
127
+ const source = getCallerFile();
128
+ if (!source) {
129
+ throw new Error("Could not determine caller file for workflow registration");
130
+ }
131
+ rpc.register({
132
+ id: this.id,
133
+ type: "workflow",
134
+ source,
135
+ config: { ...rest, links },
136
+ });
137
+ }
138
+ }
139
+ __id() {
140
+ return this.id;
141
+ }
142
+ trigger(payload, opts) { }
143
+ batchTrigger() { }
144
+ triggerAndWait() { }
145
+ batchTriggerAndWait() { }
146
+ }
package/package.json CHANGED
@@ -1,13 +1,98 @@
1
1
  {
2
2
  "name": "ocel",
3
- "version": "0.0.0",
4
- "description": "",
5
- "main": "index.js",
3
+ "version": "0.0.1-alpha.1",
4
+ "type": "module",
5
+ "description": "The power of AWS with the DX of Vercel",
6
+ "bin": {
7
+ "ocel": "./bin/run.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "dist/",
12
+ "src/",
13
+ "README.md"
14
+ ],
15
+ "exports": {
16
+ "./blob": {
17
+ "import": "./dist/blob/index.js",
18
+ "types": "./dist/blob/index.d.ts"
19
+ },
20
+ "./blob/client": {
21
+ "import": "./dist/blob/integrations/client.js",
22
+ "types": "./dist/blob/integrations/client.d.ts"
23
+ },
24
+ "./blob/*": {
25
+ "import": "./dist/blob/integrations/*.js",
26
+ "types": "./dist/blob/integrations/*.d.ts"
27
+ },
28
+ "./lambda": {
29
+ "import": "./dist/lambda/index.js",
30
+ "types": "./dist/lambda/index.d.ts"
31
+ },
32
+ "./lambda/*": {
33
+ "import": "./dist/lambda/integrations/*.js",
34
+ "types": "./dist/lambda/integrations/*.d.ts"
35
+ },
36
+ "./workflow": {
37
+ "import": "./dist/workflow/index.js",
38
+ "types": "./dist/workflow/index.d.ts"
39
+ },
40
+ "./postgres": {
41
+ "import": "./dist/postgres/index.js",
42
+ "types": "./dist/postgres/index.d.ts"
43
+ }
44
+ },
6
45
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
46
+ "build": "tsc",
47
+ "build:listener": "bun build src/blob/s3-listener.ts --outfile=dist/blob-listener/index.js --target=node --format=cjs --minify"
48
+ },
49
+ "license": "MIT",
50
+ "packageManager": "pnpm@10.18.3",
51
+ "optionalDependencies": {
52
+ "@ocel/linux-x64": "^0.0.1-alpha.1",
53
+ "@ocel/win32-x64": "^0.0.1-alpha.1",
54
+ "@ocel/darwin-arm64": "^0.0.1-alpha.1",
55
+ "@ocel/darwin-x64": "^0.0.1-alpha.1"
56
+ },
57
+ "devDependencies": {
58
+ "@types/aws-lambda": "^8.10.160",
59
+ "@types/express": "~5.0.0",
60
+ "express": "~5.0.0",
61
+ "hono": "~4.11.1",
62
+ "typescript": "^5.9.3"
63
+ },
64
+ "dependencies": {
65
+ "@aws-sdk/client-dynamodb": "^3.958.0",
66
+ "@aws-sdk/client-lambda": "^3.958.0",
67
+ "@aws-sdk/client-s3": "^3.958.0",
68
+ "@aws-sdk/client-xray": "^3.958.0",
69
+ "@aws-sdk/s3-request-presigner": "^3.958.0",
70
+ "@aws/durable-execution-sdk-js": "^1.0.1",
71
+ "@opentelemetry/api": "^1.9.0",
72
+ "@sindresorhus/slugify": "^3.0.0",
73
+ "electrodb": "^3.5.0",
74
+ "nanoid": "^5.1.6",
75
+ "serverless-http": "^4.0.0",
76
+ "zod": "^4.2.1"
77
+ },
78
+ "peerDependencies": {
79
+ "hono": ">=4.11.1",
80
+ "@types/express": ">=5.0.0",
81
+ "express": ">=5.0.0"
82
+ },
83
+ "peerDependenciesMeta": {
84
+ "hono": {
85
+ "optional": true
86
+ },
87
+ "@types/express": {
88
+ "optional": true
89
+ },
90
+ "express": {
91
+ "optional": true
92
+ }
8
93
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "packageManager": "pnpm@10.18.3"
94
+ "publishConfig": {
95
+ "access": "public",
96
+ "registry": "https://registry.npmjs.org/"
97
+ }
13
98
  }
@@ -0,0 +1,92 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import z from "zod";
3
+ import { getCallerFile } from "../utils/stack";
4
+ import { rpc } from "../utils/rpc";
5
+ import type { HandleUploadBody, TBucketConfig, TUploaderConfig } from "./types";
6
+ import { Uploader } from "./uploader";
7
+ import { parseReq } from "./parse";
8
+ import type { Component } from "../utils/component";
9
+
10
+ export class Bucket<
11
+ TUploaderShape extends Record<string, TUploaderConfig<any, any>>
12
+ > implements Component
13
+ {
14
+ public readonly _shape: TUploaderShape = {} as any;
15
+ private uploaders: Record<string, Uploader<any>> = {};
16
+
17
+ constructor(
18
+ public readonly name: string,
19
+ args: TBucketConfig<TUploaderShape>
20
+ ) {
21
+ if (process.env.OCEL_PHASE === "discovery") {
22
+ rpc.register({
23
+ id: name,
24
+ type: "bucket",
25
+ source: getCallerFile() || "unknown",
26
+ // TODO:
27
+ config: {},
28
+ });
29
+ }
30
+
31
+ this._shape = args.uploaders;
32
+
33
+ Object.entries(args.uploaders).forEach(([key, config]) => {
34
+ this.uploaders[key] = new Uploader(this, key, config);
35
+ });
36
+ }
37
+
38
+ __id() {
39
+ return this.name;
40
+ }
41
+
42
+ __name() {
43
+ const id = process.env[`RESOURCE_${this.name}_BUCKET_NAME`];
44
+ if (!id) {
45
+ throw new Error(
46
+ `Bucket ID for bucket "${this.name}" is not defined. Make sure the bucket is properly configured in your Ocel project.`
47
+ );
48
+ }
49
+ return id;
50
+ }
51
+
52
+ upload = async () => {};
53
+
54
+ uploadExternal = async () => {};
55
+
56
+ handleUpload = async ({
57
+ body,
58
+ request,
59
+ }: {
60
+ request: Request | IncomingMessage;
61
+ body: HandleUploadBody;
62
+ }) => {
63
+ const parsedReq = parseReq(request);
64
+ const hasUploaders = Object.keys(this._shape).length > 0;
65
+ if (!hasUploaders) {
66
+ throw new Error("No uploaders configured for this bucket");
67
+ }
68
+
69
+ const querySchema = z.object({
70
+ action: z.enum(["presign", "callback", "poll"]),
71
+ uploader: z.enum(Object.keys(this._shape) as [string, ...string[]]),
72
+ sessionId: z.string().optional(),
73
+ });
74
+ const queryRes = querySchema.safeParse(Object.fromEntries(parsedReq.query));
75
+ if (!queryRes.success) {
76
+ throw new Error(`Invalid query parameters: ${queryRes.error.message}`);
77
+ }
78
+
79
+ const query = queryRes.data;
80
+ const uploader = this.uploaders[query.uploader];
81
+ if (!uploader) {
82
+ throw new Error(`Uploader not found: ${query.uploader}`);
83
+ }
84
+
85
+ return uploader.handleUpload({
86
+ body,
87
+ request,
88
+ action: query.action,
89
+ sessionId: query.sessionId,
90
+ });
91
+ };
92
+ }
@@ -0,0 +1,34 @@
1
+ import { type ZodType } from "zod";
2
+ import type { TBucketConfig, TUploaderConfig } from "./types";
3
+ import { Bucket } from "./bucket";
4
+
5
+ export function bucket<
6
+ TUploaderShape extends Record<string, TUploaderConfig<any, any>>
7
+ >(name: string, args: TBucketConfig<TUploaderShape>) {
8
+ return new Bucket<TUploaderShape>(name, args);
9
+ }
10
+
11
+ export function uploader<
12
+ TInput extends ZodType = never,
13
+ TMeta = never,
14
+ TReturn = void
15
+ >(
16
+ {
17
+ middleware,
18
+ input,
19
+ }: {
20
+ input?: TInput;
21
+ middleware: TUploaderConfig<TInput, TMeta, TReturn>["onBeforeUpload"];
22
+ },
23
+ config: Omit<
24
+ TUploaderConfig<TInput, TMeta, TReturn>,
25
+ "onBeforeUpload" | "_def" | "input"
26
+ >
27
+ ) {
28
+ return {
29
+ ...config,
30
+ input,
31
+ onBeforeUpload: middleware,
32
+ _def: {} as any,
33
+ } as TUploaderConfig<TInput, TMeta, TReturn>;
34
+ }