@uploadista/adapters-express 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +5 -0
- package/LICENSE +21 -0
- package/README.md +456 -0
- package/USAGE.md +164 -0
- package/dist/adapter-layer.d.ts +22 -0
- package/dist/adapter-layer.d.ts.map +1 -0
- package/dist/adapter-layer.js +3 -0
- package/dist/error-types.d.ts +24 -0
- package/dist/error-types.d.ts.map +1 -0
- package/dist/error-types.js +65 -0
- package/dist/flow-adapter.d.ts +19 -0
- package/dist/flow-adapter.d.ts.map +1 -0
- package/dist/flow-adapter.js +80 -0
- package/dist/flow-http-handlers.d.ts +9 -0
- package/dist/flow-http-handlers.d.ts.map +1 -0
- package/dist/flow-http-handlers.js +133 -0
- package/dist/http-handlers.d.ts +7 -0
- package/dist/http-handlers.d.ts.map +1 -0
- package/dist/http-handlers.js +78 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/upload-http-handlers.d.ts +9 -0
- package/dist/upload-http-handlers.d.ts.map +1 -0
- package/dist/upload-http-handlers.js +113 -0
- package/dist/uploadista-adapter-layer.d.ts +24 -0
- package/dist/uploadista-adapter-layer.d.ts.map +1 -0
- package/dist/uploadista-adapter-layer.js +4 -0
- package/dist/uploadista-adapter.d.ts +78 -0
- package/dist/uploadista-adapter.d.ts.map +1 -0
- package/dist/uploadista-adapter.js +297 -0
- package/dist/uploadista-websocket-handler.d.ts +9 -0
- package/dist/uploadista-websocket-handler.d.ts.map +1 -0
- package/dist/uploadista-websocket-handler.js +132 -0
- package/dist/websocket-handler.d.ts +8 -0
- package/dist/websocket-handler.d.ts.map +1 -0
- package/dist/websocket-handler.js +82 -0
- package/package.json +40 -0
- package/src/error-types.ts +103 -0
- package/src/flow-http-handlers.ts +184 -0
- package/src/index.ts +14 -0
- package/src/upload-http-handlers.ts +186 -0
- package/src/uploadista-adapter-layer.ts +32 -0
- package/src/uploadista-adapter.ts +626 -0
- package/src/uploadista-websocket-handler.ts +209 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { AdapterError, BadRequestError as BaseBadRequestError, NotFoundError as BaseNotFoundError, ValidationError as BaseValidationError } from "@uploadista/server";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import type { Response } from "express";
|
|
5
|
+
export { AdapterError as ExpressAdapterError, BaseValidationError as ValidationError, BaseNotFoundError as NotFoundError, BaseBadRequestError as BadRequestError, };
|
|
6
|
+
/**
|
|
7
|
+
* Sends error response using Express Response object
|
|
8
|
+
*/
|
|
9
|
+
export declare const sendErrorResponse: (res: Response, error: AdapterError) => void;
|
|
10
|
+
/**
|
|
11
|
+
* Sends UploadistaError response using Express Response object
|
|
12
|
+
*/
|
|
13
|
+
export declare const sendUploadistaErrorResponse: (res: Response, error: UploadistaError) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Sends generic error response using Express Response object
|
|
16
|
+
*/
|
|
17
|
+
export declare const sendGenericErrorResponse: (res: Response, message?: string) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Universal error handler that sends error response via Express Response.
|
|
20
|
+
* Handles AdapterError, UploadistaError, and unknown errors.
|
|
21
|
+
* This is the recommended way to handle errors in HTTP handlers.
|
|
22
|
+
*/
|
|
23
|
+
export declare const handleErrorResponse: (res: Response) => (error: unknown) => Effect.Effect<void, never, never>;
|
|
24
|
+
//# sourceMappingURL=error-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-types.d.ts","sourceRoot":"","sources":["../src/error-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,EACL,YAAY,EACZ,eAAe,IAAI,mBAAmB,EACtC,aAAa,IAAI,iBAAiB,EAClC,eAAe,IAAI,mBAAmB,EAIvC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EACL,YAAY,IAAI,mBAAmB,EACnC,mBAAmB,IAAI,eAAe,EACtC,iBAAiB,IAAI,aAAa,EAClC,mBAAmB,IAAI,eAAe,GACvC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,QAAQ,EAAE,OAAO,YAAY,KAAG,IAEtE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,2BAA2B,GACtC,KAAK,QAAQ,EACb,OAAO,eAAe,KACrB,IAEF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wBAAwB,GACnC,KAAK,QAAQ,EACb,gBAAiC,KAChC,IAEF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,QAAQ,MAAM,OAAO,OAAO,sCAiDpE,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { AdapterError, BadRequestError as BaseBadRequestError, NotFoundError as BaseNotFoundError, ValidationError as BaseValidationError, createErrorResponseBody, createGenericErrorResponseBody, createUploadistaErrorResponseBody, } from "@uploadista/server";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
// Re-export shared error types for backward compatibility
|
|
4
|
+
export { AdapterError as ExpressAdapterError, BaseValidationError as ValidationError, BaseNotFoundError as NotFoundError, BaseBadRequestError as BadRequestError, };
|
|
5
|
+
/**
|
|
6
|
+
* Sends error response using Express Response object
|
|
7
|
+
*/
|
|
8
|
+
export const sendErrorResponse = (res, error) => {
|
|
9
|
+
res.status(error.statusCode).json(createErrorResponseBody(error));
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Sends UploadistaError response using Express Response object
|
|
13
|
+
*/
|
|
14
|
+
export const sendUploadistaErrorResponse = (res, error) => {
|
|
15
|
+
res.status(error.status).json(createUploadistaErrorResponseBody(error));
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Sends generic error response using Express Response object
|
|
19
|
+
*/
|
|
20
|
+
export const sendGenericErrorResponse = (res, message = "Internal server error") => {
|
|
21
|
+
res.status(500).json(createGenericErrorResponseBody(message));
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Universal error handler that sends error response via Express Response.
|
|
25
|
+
* Handles AdapterError, UploadistaError, and unknown errors.
|
|
26
|
+
* This is the recommended way to handle errors in HTTP handlers.
|
|
27
|
+
*/
|
|
28
|
+
export const handleErrorResponse = (res) => (error) => {
|
|
29
|
+
console.error(error);
|
|
30
|
+
// Handle known adapter errors
|
|
31
|
+
if (error instanceof AdapterError) {
|
|
32
|
+
return Effect.sync(() => sendErrorResponse(res, error));
|
|
33
|
+
}
|
|
34
|
+
// Handle UploadistaError
|
|
35
|
+
if (typeof error === "object" &&
|
|
36
|
+
error !== null &&
|
|
37
|
+
"code" in error &&
|
|
38
|
+
"status" in error &&
|
|
39
|
+
"body" in error) {
|
|
40
|
+
return Effect.sync(() => sendUploadistaErrorResponse(res, error));
|
|
41
|
+
}
|
|
42
|
+
// Handle unknown errors - try to extract what we can
|
|
43
|
+
let message = "Internal server error";
|
|
44
|
+
let code = "UNKNOWN_ERROR";
|
|
45
|
+
let status = 500;
|
|
46
|
+
if (typeof error === "object" && error !== null) {
|
|
47
|
+
const errorObj = error;
|
|
48
|
+
if ("message" in errorObj && typeof errorObj.message === "string") {
|
|
49
|
+
message = errorObj.message;
|
|
50
|
+
}
|
|
51
|
+
if ("code" in errorObj && typeof errorObj.code === "string") {
|
|
52
|
+
code = errorObj.code;
|
|
53
|
+
}
|
|
54
|
+
if ("status" in errorObj && typeof errorObj.status === "number") {
|
|
55
|
+
status = errorObj.status;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Effect.sync(() => {
|
|
59
|
+
res.status(status).json({
|
|
60
|
+
error: message,
|
|
61
|
+
code,
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FlowProvider, FlowServer } from "@uploadista/core/flow";
|
|
2
|
+
import { type BaseEventEmitterService, type BaseKvStoreService } from "@uploadista/core/types";
|
|
3
|
+
import { Layer } from "effect";
|
|
4
|
+
import type { Request, Response } from "express";
|
|
5
|
+
export type ExpressFlowAdapterOptions = {
|
|
6
|
+
flowProvider: Layer.Layer<FlowProvider>;
|
|
7
|
+
kvStore: Layer.Layer<BaseKvStoreService>;
|
|
8
|
+
eventEmitter: Layer.Layer<BaseEventEmitterService>;
|
|
9
|
+
withTracing?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type ExpressFlowAdapter = {
|
|
12
|
+
handler: (req: Request, res: Response) => Promise<void>;
|
|
13
|
+
flowServer: Layer.Layer<FlowServer>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Creates an Express flow adapter
|
|
17
|
+
*/
|
|
18
|
+
export declare const createExpressFlowAdapter: ({ flowProvider, kvStore, eventEmitter, withTracing, }: ExpressFlowAdapterOptions) => Promise<ExpressFlowAdapter>;
|
|
19
|
+
//# sourceMappingURL=flow-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-adapter.d.ts","sourceRoot":"","sources":["../src/flow-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,UAAU,EAEX,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EAGxB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAIjD,MAAM,MAAM,yBAAyB,GAAG;IACtC,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACxC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACzC,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IACnD,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;CACrC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,wBAAwB,GAAU,uDAK5C,yBAAyB,KAAG,OAAO,CAAC,kBAAkB,CAoGxD,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { FlowServer, flowServer, } from "@uploadista/core/flow";
|
|
2
|
+
import { flowEventEmitter, flowJobKvStore, } from "@uploadista/core/types";
|
|
3
|
+
import { NodeSdkLive } from "@uploadista/observability";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
/**
|
|
6
|
+
* Creates an Express flow adapter
|
|
7
|
+
*/
|
|
8
|
+
export const createExpressFlowAdapter = async ({ flowProvider, kvStore, eventEmitter, withTracing = false, }) => {
|
|
9
|
+
const flowJobKVStoreLayer = Layer.provide(flowJobKvStore, kvStore);
|
|
10
|
+
const flowEventEmitterLayer = Layer.provide(flowEventEmitter, eventEmitter);
|
|
11
|
+
const baseDependencies = Layer.mergeAll(flowProvider, flowEventEmitterLayer, flowJobKVStoreLayer);
|
|
12
|
+
const flowServerLayer = Layer.provide(flowServer, baseDependencies);
|
|
13
|
+
// Initialize the flow server
|
|
14
|
+
const runProgram = (effect) => {
|
|
15
|
+
if (withTracing) {
|
|
16
|
+
return Effect.runPromise(effect.pipe(Effect.provide(NodeSdkLive)));
|
|
17
|
+
}
|
|
18
|
+
return Effect.runPromise(effect);
|
|
19
|
+
};
|
|
20
|
+
const handler = async (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const program = Effect.gen(function* () {
|
|
23
|
+
const server = yield* FlowServer;
|
|
24
|
+
switch (req.method) {
|
|
25
|
+
case "GET": {
|
|
26
|
+
const id = req.url?.split("/").pop();
|
|
27
|
+
if (!id) {
|
|
28
|
+
return { status: 400, body: { error: "No id" } };
|
|
29
|
+
}
|
|
30
|
+
const flowData = yield* server.getFlowData(id);
|
|
31
|
+
return { status: 200, body: flowData };
|
|
32
|
+
}
|
|
33
|
+
case "POST": {
|
|
34
|
+
const urlSegments = req.url?.split("/") || [];
|
|
35
|
+
const storageId = urlSegments.pop();
|
|
36
|
+
const flowId = urlSegments.pop();
|
|
37
|
+
if (!flowId) {
|
|
38
|
+
return { status: 400, body: { error: "No id" } };
|
|
39
|
+
}
|
|
40
|
+
if (!storageId) {
|
|
41
|
+
return { status: 400, body: { error: "No storage id" } };
|
|
42
|
+
}
|
|
43
|
+
console.log("Flow execution params:", req.body, flowId, storageId);
|
|
44
|
+
const result = yield* server.runFlow(flowId, storageId, req.body?.inputs);
|
|
45
|
+
return { status: 200, body: result };
|
|
46
|
+
}
|
|
47
|
+
default:
|
|
48
|
+
return { status: 405, body: { error: "Method not allowed" } };
|
|
49
|
+
}
|
|
50
|
+
}).pipe(Effect.catchAll((error) => {
|
|
51
|
+
let status = 500;
|
|
52
|
+
let message = "Internal server error";
|
|
53
|
+
if (typeof error === "object" && error !== null) {
|
|
54
|
+
const errorObj = error;
|
|
55
|
+
if ("message" in errorObj && typeof errorObj.message === "string") {
|
|
56
|
+
message = errorObj.message;
|
|
57
|
+
}
|
|
58
|
+
if ("code" in errorObj && errorObj.code === "FILE_NOT_FOUND") {
|
|
59
|
+
status = 404;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Effect.succeed({
|
|
63
|
+
status,
|
|
64
|
+
body: { error: message },
|
|
65
|
+
});
|
|
66
|
+
}));
|
|
67
|
+
const result = await runProgram(Effect.provide(program, flowServerLayer));
|
|
68
|
+
res.status(result.status).json(result.body);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
res.status(500).json({
|
|
72
|
+
error: error instanceof Error ? error.message : "Internal server error",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
handler,
|
|
78
|
+
flowServer: flowServerLayer,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FlowServerShape } from "@uploadista/core/flow";
|
|
2
|
+
import { AuthCacheService, AuthContextService } from "@uploadista/server";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import type { Request, Response } from "express";
|
|
5
|
+
export declare const handleFlowGet: (req: Request, res: Response, flowServer: FlowServerShape) => Effect.Effect<void, never, AuthContextService>;
|
|
6
|
+
export declare const handleFlowPost: <TRequirements = never>(req: Request, res: Response, flowServer: FlowServerShape) => Effect.Effect<void, never, AuthContextService | TRequirements | AuthCacheService>;
|
|
7
|
+
export declare const handleJobStatus: (req: Request, res: Response, flowServer: FlowServerShape) => Effect.Effect<void>;
|
|
8
|
+
export declare const handleContinueFlow: <TRequirements = never>(req: Request, res: Response, flowServer: FlowServerShape) => Effect.Effect<void, never, AuthContextService | TRequirements>;
|
|
9
|
+
//# sourceMappingURL=flow-http-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow-http-handlers.d.ts","sourceRoot":"","sources":["../src/flow-http-handlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAEnB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGjD,eAAO,MAAM,aAAa,GACxB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,YAAY,eAAe,mDAkB5B,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,aAAa,GAAG,KAAK,EAClD,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,YAAY,eAAe,sFAsD5B,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,YAAY,eAAe,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,CAcpB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,aAAa,GAAG,KAAK,EACtD,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,YAAY,eAAe,mEAoE5B,CAAC"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { AuthCacheService, AuthContextService, getLastSegment, } from "@uploadista/server";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { handleErrorResponse } from "./error-types";
|
|
4
|
+
export const handleFlowGet = (req, res, flowServer) => {
|
|
5
|
+
return Effect.gen(function* () {
|
|
6
|
+
// Access auth context if available
|
|
7
|
+
const authService = yield* AuthContextService;
|
|
8
|
+
const clientId = yield* authService.getClientId();
|
|
9
|
+
const url = new URL(req.url, `http://${req.get("host")}`);
|
|
10
|
+
const id = getLastSegment(url.pathname);
|
|
11
|
+
if (!id) {
|
|
12
|
+
res.status(400).json({ error: "No id" });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const flowData = yield* flowServer.getFlowData(id, clientId);
|
|
16
|
+
res.status(200).json(flowData);
|
|
17
|
+
}).pipe(Effect.catchAll(handleErrorResponse(res)));
|
|
18
|
+
};
|
|
19
|
+
export const handleFlowPost = (req, res, flowServer) => {
|
|
20
|
+
return Effect.gen(function* () {
|
|
21
|
+
const authService = yield* AuthContextService;
|
|
22
|
+
const authCache = yield* AuthCacheService;
|
|
23
|
+
const clientId = yield* authService.getClientId();
|
|
24
|
+
const urlSegments = req.url.split("/");
|
|
25
|
+
const storageId = urlSegments.pop();
|
|
26
|
+
const flowId = urlSegments.pop();
|
|
27
|
+
if (!flowId) {
|
|
28
|
+
res.status(400).json({ error: "No id" });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!storageId) {
|
|
32
|
+
res.status(400).json({ error: "No storage id" });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const params = req.body;
|
|
36
|
+
if (clientId) {
|
|
37
|
+
console.log(`[Flow] Executing flow: ${flowId}, storage: ${storageId}, client: ${clientId}`);
|
|
38
|
+
console.log(JSON.stringify(params, null, 2));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.log(`Flow execution params: ${flowId} ${storageId}`);
|
|
42
|
+
console.log(JSON.stringify(params, null, 2));
|
|
43
|
+
}
|
|
44
|
+
// Run flow returns immediately with jobId
|
|
45
|
+
const result = yield* flowServer.runFlow({
|
|
46
|
+
flowId,
|
|
47
|
+
storageId,
|
|
48
|
+
clientId,
|
|
49
|
+
inputs: params.inputs,
|
|
50
|
+
});
|
|
51
|
+
// Cache auth context for subsequent flow operations (continue, status)
|
|
52
|
+
const authContext = yield* authService.getAuthContext();
|
|
53
|
+
if (authContext) {
|
|
54
|
+
yield* authCache.set(result.id, authContext);
|
|
55
|
+
}
|
|
56
|
+
if (clientId) {
|
|
57
|
+
console.log(`[Flow] Flow started with jobId: ${result.id}, client: ${clientId}`);
|
|
58
|
+
}
|
|
59
|
+
res.status(200).json(result);
|
|
60
|
+
}).pipe(Effect.catchAll(handleErrorResponse(res)));
|
|
61
|
+
};
|
|
62
|
+
export const handleJobStatus = (req, res, flowServer) => {
|
|
63
|
+
return Effect.gen(function* () {
|
|
64
|
+
const urlSegments = req.url.split("/");
|
|
65
|
+
const jobId = urlSegments[urlSegments.length - 2]; // .../jobs/:jobId/status
|
|
66
|
+
if (!jobId) {
|
|
67
|
+
res.status(400).json({ error: "No job id" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const result = yield* flowServer.getJobStatus(jobId);
|
|
71
|
+
res.status(200).json(result);
|
|
72
|
+
}).pipe(Effect.catchAll(handleErrorResponse(res)));
|
|
73
|
+
};
|
|
74
|
+
export const handleContinueFlow = (req, res, flowServer) => {
|
|
75
|
+
return Effect.gen(function* () {
|
|
76
|
+
const authService = yield* AuthContextService;
|
|
77
|
+
const clientId = yield* authService.getClientId();
|
|
78
|
+
const url = new URL(req.url, `http://${req.get("host")}`);
|
|
79
|
+
const urlSegments = url.pathname.split("/");
|
|
80
|
+
const jobId = urlSegments[urlSegments.length - 3]; // .../jobs/:jobId/continue/:nodeId
|
|
81
|
+
const nodeId = urlSegments[urlSegments.length - 1]; // .../jobs/:jobId/continue/:nodeId
|
|
82
|
+
if (!jobId) {
|
|
83
|
+
console.error("No job id");
|
|
84
|
+
res.status(400).json({ error: "No job id" });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!nodeId) {
|
|
88
|
+
console.error("No node id");
|
|
89
|
+
res.status(400).json({ error: "No node id" });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const contentType = req.get("Content-Type");
|
|
93
|
+
let newData;
|
|
94
|
+
// Handle different content types
|
|
95
|
+
if (contentType?.includes("application/octet-stream")) {
|
|
96
|
+
// For streaming data, convert Node.js Readable to web ReadableStream
|
|
97
|
+
newData = new ReadableStream({
|
|
98
|
+
start(controller) {
|
|
99
|
+
req.on("data", (chunk) => {
|
|
100
|
+
controller.enqueue(chunk);
|
|
101
|
+
});
|
|
102
|
+
req.on("end", () => {
|
|
103
|
+
controller.close();
|
|
104
|
+
});
|
|
105
|
+
req.on("error", (error) => {
|
|
106
|
+
controller.error(error);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else if (contentType?.includes("application/json")) {
|
|
112
|
+
// For JSON data, use the parsed body
|
|
113
|
+
const body = req.body;
|
|
114
|
+
if (body.newData === undefined) {
|
|
115
|
+
console.error("Missing newData");
|
|
116
|
+
res.status(400).json({ error: "Missing newData" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
newData = body.newData;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
res.status(415).json({ error: "Unsupported Content-Type" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const result = yield* flowServer.continueFlow({
|
|
126
|
+
jobId,
|
|
127
|
+
nodeId,
|
|
128
|
+
newData,
|
|
129
|
+
clientId,
|
|
130
|
+
});
|
|
131
|
+
res.status(200).json(result);
|
|
132
|
+
}).pipe(Effect.catchAll(handleErrorResponse(res)));
|
|
133
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UploadServerShape } from "@uploadista/core/upload";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import type { Request, Response } from "express";
|
|
4
|
+
export declare const handlePost: (req: Request, res: Response, server: UploadServerShape) => Effect.Effect<void>;
|
|
5
|
+
export declare const handleGet: (req: Request, res: Response, server: UploadServerShape) => Effect.Effect<void>;
|
|
6
|
+
export declare const handlePatch: (req: Request, res: Response, server: UploadServerShape) => Effect.Effect<void>;
|
|
7
|
+
//# sourceMappingURL=http-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-handlers.d.ts","sourceRoot":"","sources":["../src/http-handlers.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAUjD,eAAO,MAAM,UAAU,GACrB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,QAAQ,iBAAiB,KACxB,MAAM,CAAC,MAAM,CAAC,IAAI,CAkClB,CAAC;AAEJ,eAAO,MAAM,SAAS,GACpB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,QAAQ,iBAAiB,KACxB,MAAM,CAAC,MAAM,CAAC,IAAI,CA4ClB,CAAC;AAEJ,eAAO,MAAM,WAAW,GACtB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,QAAQ,iBAAiB,KACxB,MAAM,CAAC,MAAM,CAAC,IAAI,CA4BlB,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { UploadistaError } from "@uploadista/core/errors";
|
|
2
|
+
import { inputFileSchema } from "@uploadista/core/types";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { BadRequestError, NotFoundError, sendErrorResponse, sendGenericErrorResponse, sendUploadistaErrorResponse, ValidationError, } from "./error-types";
|
|
5
|
+
export const handlePost = (req, res, server) => Effect.gen(function* () {
|
|
6
|
+
const json = req.body;
|
|
7
|
+
if (!json) {
|
|
8
|
+
return yield* Effect.fail(new BadRequestError("Invalid JSON payload"));
|
|
9
|
+
}
|
|
10
|
+
const parsedInputFile = yield* Effect.sync(() => inputFileSchema.safeParse(json));
|
|
11
|
+
if (!parsedInputFile.success) {
|
|
12
|
+
return yield* Effect.fail(new ValidationError("Invalid input file schema"));
|
|
13
|
+
}
|
|
14
|
+
const fileCreated = yield* server.createUpload(parsedInputFile.data);
|
|
15
|
+
res.status(200).json(fileCreated);
|
|
16
|
+
}).pipe(Effect.catchAll((error) => {
|
|
17
|
+
if (error instanceof ValidationError ||
|
|
18
|
+
error instanceof BadRequestError) {
|
|
19
|
+
return Effect.sync(() => sendErrorResponse(res, error));
|
|
20
|
+
}
|
|
21
|
+
if (error instanceof UploadistaError) {
|
|
22
|
+
return Effect.sync(() => sendUploadistaErrorResponse(res, error));
|
|
23
|
+
}
|
|
24
|
+
return Effect.sync(() => sendGenericErrorResponse(res));
|
|
25
|
+
}));
|
|
26
|
+
export const handleGet = (req, res, server) => Effect.gen(function* () {
|
|
27
|
+
const url = new URL(req.url, `http://${req.get("host")}`);
|
|
28
|
+
const pathSegments = url.pathname.split("/").filter(Boolean);
|
|
29
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
30
|
+
if (lastSegment === "capabilities") {
|
|
31
|
+
const storageId = url.searchParams.get("storageId") ||
|
|
32
|
+
pathSegments[pathSegments.length - 2];
|
|
33
|
+
if (!storageId) {
|
|
34
|
+
return yield* Effect.fail(new BadRequestError("storageId is required for capabilities"));
|
|
35
|
+
}
|
|
36
|
+
const capabilities = yield* server.getCapabilities(storageId);
|
|
37
|
+
res.status(200).json({
|
|
38
|
+
storageId,
|
|
39
|
+
capabilities,
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!lastSegment) {
|
|
45
|
+
return yield* Effect.fail(new BadRequestError("Upload ID is required"));
|
|
46
|
+
}
|
|
47
|
+
const fileResult = yield* server.getUpload(lastSegment);
|
|
48
|
+
res.status(200).json(fileResult);
|
|
49
|
+
}).pipe(Effect.catchAll((error) => {
|
|
50
|
+
if (error instanceof BadRequestError || error instanceof NotFoundError) {
|
|
51
|
+
return Effect.sync(() => sendErrorResponse(res, error));
|
|
52
|
+
}
|
|
53
|
+
if (error instanceof UploadistaError) {
|
|
54
|
+
return Effect.sync(() => sendUploadistaErrorResponse(res, error));
|
|
55
|
+
}
|
|
56
|
+
return Effect.sync(() => sendGenericErrorResponse(res));
|
|
57
|
+
}));
|
|
58
|
+
export const handlePatch = (req, res, server) => Effect.gen(function* () {
|
|
59
|
+
const uploadId = req.url.split("/").pop();
|
|
60
|
+
if (!uploadId) {
|
|
61
|
+
return yield* Effect.fail(new BadRequestError("Upload ID is required"));
|
|
62
|
+
}
|
|
63
|
+
// Convert Express request to ReadableStream
|
|
64
|
+
const body = req;
|
|
65
|
+
if (!body) {
|
|
66
|
+
return yield* Effect.fail(new BadRequestError("Request body is required"));
|
|
67
|
+
}
|
|
68
|
+
const fileResult = yield* server.uploadChunk(uploadId, body);
|
|
69
|
+
res.status(200).json(fileResult);
|
|
70
|
+
}).pipe(Effect.catchAll((error) => {
|
|
71
|
+
if (error instanceof BadRequestError || error instanceof NotFoundError) {
|
|
72
|
+
return Effect.sync(() => sendErrorResponse(res, error));
|
|
73
|
+
}
|
|
74
|
+
if (error instanceof UploadistaError) {
|
|
75
|
+
return Effect.sync(() => sendUploadistaErrorResponse(res, error));
|
|
76
|
+
}
|
|
77
|
+
return Effect.sync(() => sendGenericErrorResponse(res));
|
|
78
|
+
}));
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from "./uploadista-adapter";
|
|
2
|
+
export type { ExpressWebSocketHandler, WebSocketConnection, WebSocketHandlers, } from "./uploadista-adapter-layer";
|
|
3
|
+
export { createUploadistaWebSocketHandler, createWebSocketCloseHandler, createWebSocketErrorHandler, createWebSocketMessageHandler, } from "./uploadista-websocket-handler";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AAGrC,YAAY,EACV,uBAAuB,EACvB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAC3B,2BAA2B,EAC3B,6BAA6B,GAC9B,MAAM,gCAAgC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UploadServerShape } from "@uploadista/core/upload";
|
|
2
|
+
import { MetricsService } from "@uploadista/observability";
|
|
3
|
+
import { AuthCacheService, AuthContextService } from "@uploadista/server";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import type { Request, Response } from "express";
|
|
6
|
+
export declare const handleUploadPost: (req: Request, res: Response, server: UploadServerShape) => Effect.Effect<void | undefined, never, AuthContextService | AuthCacheService>;
|
|
7
|
+
export declare const handleUploadGet: (req: Request, res: Response, server: UploadServerShape) => Effect.Effect<void | undefined, never, AuthContextService>;
|
|
8
|
+
export declare const handleUploadPatch: (req: Request, res: Response, server: UploadServerShape) => Effect.Effect<void | undefined, never, AuthContextService | AuthCacheService | MetricsService>;
|
|
9
|
+
//# sourceMappingURL=upload-http-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-http-handlers.d.ts","sourceRoot":"","sources":["../src/upload-http-handlers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAOjD,eAAO,MAAM,gBAAgB,GAC3B,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,QAAQ,iBAAiB,kFA8CyB,CAAC;AAErD,eAAO,MAAM,eAAe,GAC1B,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,QAAQ,iBAAiB,+DAsCyB,CAAC;AAErD,eAAO,MAAM,iBAAiB,GAC5B,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,QAAQ,iBAAiB,mGA4EyB,CAAC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { inputFileSchema } from "@uploadista/core/types";
|
|
2
|
+
import { MetricsService } from "@uploadista/observability";
|
|
3
|
+
import { AuthCacheService, AuthContextService } from "@uploadista/server";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
import { BadRequestError, handleErrorResponse, ValidationError, } from "./error-types";
|
|
6
|
+
export const handleUploadPost = (req, res, server) => Effect.gen(function* () {
|
|
7
|
+
// Access auth context if available
|
|
8
|
+
const authService = yield* AuthContextService;
|
|
9
|
+
const authCache = yield* AuthCacheService;
|
|
10
|
+
const clientId = yield* authService.getClientId();
|
|
11
|
+
if (clientId) {
|
|
12
|
+
console.log(`[Upload] Creating upload for client: ${clientId}`);
|
|
13
|
+
}
|
|
14
|
+
const json = req.body;
|
|
15
|
+
if (!json) {
|
|
16
|
+
return yield* Effect.fail(new BadRequestError("Invalid JSON payload"));
|
|
17
|
+
}
|
|
18
|
+
const parsedInputFile = yield* Effect.sync(() => inputFileSchema.safeParse(json));
|
|
19
|
+
if (!parsedInputFile.success) {
|
|
20
|
+
return yield* Effect.fail(new ValidationError("Invalid input file schema"));
|
|
21
|
+
}
|
|
22
|
+
const fileCreated = yield* server.createUpload(parsedInputFile.data, clientId);
|
|
23
|
+
// Cache auth context for subsequent chunk uploads
|
|
24
|
+
const authContext = yield* authService.getAuthContext();
|
|
25
|
+
if (authContext) {
|
|
26
|
+
yield* authCache.set(fileCreated.id, authContext);
|
|
27
|
+
}
|
|
28
|
+
if (clientId) {
|
|
29
|
+
console.log(`[Upload] Upload created: ${fileCreated.id} for client: ${clientId}`);
|
|
30
|
+
}
|
|
31
|
+
res.status(200).json(fileCreated);
|
|
32
|
+
}).pipe(Effect.catchAll(handleErrorResponse(res)));
|
|
33
|
+
export const handleUploadGet = (req, res, server) => Effect.gen(function* () {
|
|
34
|
+
const authService = yield* AuthContextService;
|
|
35
|
+
const clientId = yield* authService.getClientId();
|
|
36
|
+
const url = new URL(req.url, `http://${req.get("host")}`);
|
|
37
|
+
const pathSegments = url.pathname.split("/").filter(Boolean);
|
|
38
|
+
const lastSegment = pathSegments[pathSegments.length - 1];
|
|
39
|
+
if (lastSegment === "capabilities") {
|
|
40
|
+
const storageId = url.searchParams.get("storageId") ||
|
|
41
|
+
pathSegments[pathSegments.length - 2];
|
|
42
|
+
if (!storageId) {
|
|
43
|
+
return yield* Effect.fail(new BadRequestError("storageId is required for capabilities"));
|
|
44
|
+
}
|
|
45
|
+
const capabilities = yield* server.getCapabilities(storageId, clientId);
|
|
46
|
+
res.status(200).json({
|
|
47
|
+
storageId,
|
|
48
|
+
capabilities,
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!lastSegment) {
|
|
54
|
+
return yield* Effect.fail(new BadRequestError("Upload ID is required"));
|
|
55
|
+
}
|
|
56
|
+
const fileResult = yield* server.getUpload(lastSegment);
|
|
57
|
+
res.status(200).json(fileResult);
|
|
58
|
+
}).pipe(Effect.catchAll(handleErrorResponse(res)));
|
|
59
|
+
export const handleUploadPatch = (req, res, server) => Effect.gen(function* () {
|
|
60
|
+
// Try to get auth from current request or cached auth
|
|
61
|
+
const authService = yield* AuthContextService;
|
|
62
|
+
const authCache = yield* AuthCacheService;
|
|
63
|
+
const metricsService = yield* MetricsService;
|
|
64
|
+
const uploadId = req.url.split("/").pop();
|
|
65
|
+
if (!uploadId) {
|
|
66
|
+
return yield* Effect.fail(new BadRequestError("Upload ID is required"));
|
|
67
|
+
}
|
|
68
|
+
// Try current auth first, fallback to cached auth
|
|
69
|
+
let clientId = yield* authService.getClientId();
|
|
70
|
+
let authMetadata = yield* authService.getMetadata();
|
|
71
|
+
if (!clientId) {
|
|
72
|
+
const cachedAuth = yield* authCache.get(uploadId);
|
|
73
|
+
clientId = cachedAuth?.clientId ?? null;
|
|
74
|
+
authMetadata = cachedAuth?.metadata ?? {};
|
|
75
|
+
}
|
|
76
|
+
if (clientId) {
|
|
77
|
+
console.log(`[Upload] Uploading chunk for upload: ${uploadId}, client: ${clientId}`);
|
|
78
|
+
}
|
|
79
|
+
// Convert Node.js Readable stream to web ReadableStream
|
|
80
|
+
const body = new ReadableStream({
|
|
81
|
+
start(controller) {
|
|
82
|
+
req.on("data", (chunk) => {
|
|
83
|
+
controller.enqueue(chunk);
|
|
84
|
+
});
|
|
85
|
+
req.on("end", () => {
|
|
86
|
+
controller.close();
|
|
87
|
+
});
|
|
88
|
+
req.on("error", (error) => {
|
|
89
|
+
controller.error(error);
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const fileResult = yield* server.uploadChunk(uploadId, clientId, body);
|
|
94
|
+
// Clear cache and record metrics if upload is complete
|
|
95
|
+
if (fileResult.size && fileResult.offset >= fileResult.size) {
|
|
96
|
+
yield* authCache.delete(uploadId);
|
|
97
|
+
if (clientId) {
|
|
98
|
+
console.log(`[Upload] Upload completed, cleared auth cache: ${uploadId}`);
|
|
99
|
+
}
|
|
100
|
+
// Record upload metrics if we have organization ID
|
|
101
|
+
if (clientId && fileResult.size) {
|
|
102
|
+
console.log(`[Upload] Recording metrics for org: ${clientId}, size: ${fileResult.size}`);
|
|
103
|
+
yield* Effect.forkDaemon(metricsService.recordUpload(clientId, fileResult.size, authMetadata));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.warn(`[Upload] Cannot record metrics - missing organizationId or size`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (clientId) {
|
|
110
|
+
console.log(`[Upload] Chunk uploaded for upload: ${uploadId}, client: ${clientId}`);
|
|
111
|
+
}
|
|
112
|
+
res.status(200).json(fileResult);
|
|
113
|
+
}).pipe(Effect.catchAll(handleErrorResponse(res)));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import { type Effect, Context as EffectContext } from "effect";
|
|
3
|
+
import type { Request, Response } from "express";
|
|
4
|
+
export interface WebSocketConnection {
|
|
5
|
+
id: string;
|
|
6
|
+
send: (data: string) => void;
|
|
7
|
+
close: (code?: number, reason?: string) => void;
|
|
8
|
+
readyState: number;
|
|
9
|
+
}
|
|
10
|
+
export type WebSocketHandlers = {
|
|
11
|
+
onMessage: (message: string) => void;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
onError: (error: Error) => void;
|
|
14
|
+
};
|
|
15
|
+
export type ExpressWebSocketHandler = (req: IncomingMessage, connection: WebSocketConnection) => WebSocketHandlers;
|
|
16
|
+
export type ExpressUploadistaAdapterServiceShape = {
|
|
17
|
+
handler: (req: Request, res: Response) => Effect.Effect<void, never, never>;
|
|
18
|
+
websocketHandler: ExpressWebSocketHandler;
|
|
19
|
+
};
|
|
20
|
+
declare const ExpressUploadistaAdapterService_base: EffectContext.TagClass<ExpressUploadistaAdapterService, "ExpressUploadistaAdapterService", ExpressUploadistaAdapterServiceShape>;
|
|
21
|
+
export declare class ExpressUploadistaAdapterService extends ExpressUploadistaAdapterService_base {
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=uploadista-adapter-layer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploadista-adapter-layer.d.ts","sourceRoot":"","sources":["../src/uploadista-adapter-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,KAAK,MAAM,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,QAAQ,CAAC;AAC/D,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,CACpC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,mBAAmB,KAC5B,iBAAiB,CAAC;AAGvB,MAAM,MAAM,oCAAoC,GAAG;IACjD,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAC5E,gBAAgB,EAAE,uBAAuB,CAAC;CAC3C,CAAC;;AAGF,qBAAa,+BAAgC,SAAQ,oCAEqB;CAAG"}
|