ocel 0.0.1-alpha.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +98 -0
- package/dist/binding/binding.d.ts +22 -0
- package/dist/binding/binding.js +60 -0
- package/dist/binding/unprovisioned.d.ts +17 -0
- package/dist/binding/unprovisioned.js +25 -0
- package/dist/bucket/bucket-client.d.ts +4 -0
- package/dist/bucket/bucket-client.js +5 -0
- package/dist/bucket/bucket-context.d.ts +8 -0
- package/dist/bucket/bucket-context.js +10 -0
- package/dist/bucket/bucket.d.ts +60 -0
- package/dist/bucket/bucket.js +91 -0
- package/dist/bucket/client.d.ts +49 -0
- package/dist/bucket/client.js +95 -0
- package/dist/bucket/errors.d.ts +12 -0
- package/dist/bucket/errors.js +20 -0
- package/dist/bucket/express.d.ts +10 -0
- package/dist/bucket/express.js +24 -0
- package/dist/bucket/hono.d.ts +18 -0
- package/dist/bucket/hono.js +13 -0
- package/dist/bucket/index.d.ts +8 -0
- package/dist/bucket/index.js +6 -0
- package/dist/bucket/keys.d.ts +5 -0
- package/dist/bucket/keys.js +24 -0
- package/dist/bucket/metadata.d.ts +6 -0
- package/dist/bucket/metadata.js +6 -0
- package/dist/bucket/next.d.ts +15 -0
- package/dist/bucket/next.js +11 -0
- package/dist/bucket/objects.d.ts +130 -0
- package/dist/bucket/objects.js +350 -0
- package/dist/bucket/route.d.ts +13 -0
- package/dist/bucket/route.js +247 -0
- package/dist/bucket/server-only.js +5 -0
- package/dist/bucket/types.d.ts +63 -0
- package/dist/bucket/uploader.d.ts +3 -0
- package/dist/bucket/uploader.js +6 -0
- package/dist/build-env.d.ts +17 -0
- package/dist/build-env.js +41 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.js +9 -0
- package/dist/declaration/callsite.d.ts +15 -0
- package/dist/declaration/callsite.js +43 -0
- package/dist/declaration/component.d.ts +4 -0
- package/dist/declaration/component.js +1 -0
- package/dist/declaration/defer.d.ts +5 -0
- package/dist/declaration/defer.js +9 -0
- package/dist/delivery/duration.d.ts +13 -0
- package/dist/delivery/duration.js +36 -0
- package/dist/delivery/lane.d.ts +4 -0
- package/dist/delivery/lane.js +15 -0
- package/dist/delivery/payload.d.ts +6 -0
- package/dist/delivery/payload.js +14 -0
- package/dist/delivery/retry.d.ts +16 -0
- package/dist/delivery/retry.js +10 -0
- package/dist/delivery/schema.d.ts +10 -0
- package/dist/delivery/schema.js +20 -0
- package/dist/dns.d.ts +11 -0
- package/dist/dns.js +4 -0
- package/dist/edge.d.ts +4 -0
- package/dist/edge.js +4 -0
- package/dist/env/access.d.ts +22 -0
- package/dist/env/access.js +49 -0
- package/dist/env/client.d.ts +5 -0
- package/dist/env/client.js +10 -0
- package/dist/env/declare.d.ts +2 -0
- package/dist/env/declare.js +83 -0
- package/dist/env/definition.d.ts +46 -0
- package/dist/env/definition.js +164 -0
- package/dist/env/deployment.d.ts +23 -0
- package/dist/env/deployment.js +13 -0
- package/dist/env/edge.d.ts +10 -0
- package/dist/env/edge.js +30 -0
- package/dist/env/errors.d.ts +9 -0
- package/dist/env/errors.js +9 -0
- package/dist/env/file.d.ts +1 -0
- package/dist/env/file.js +27 -0
- package/dist/env/index.d.ts +10 -0
- package/dist/env/index.js +63 -0
- package/dist/env/live.d.ts +3 -0
- package/dist/env/live.js +12 -0
- package/dist/env/schema.d.ts +27 -0
- package/dist/env/schema.js +43 -0
- package/dist/env/scope.d.ts +7 -0
- package/dist/env/scope.js +45 -0
- package/dist/env/standard.d.ts +10 -0
- package/dist/env/standard.js +21 -0
- package/dist/env/value.d.ts +5 -0
- package/dist/env/value.js +26 -0
- package/dist/env-source.d.ts +17 -0
- package/dist/env-source.js +16 -0
- package/dist/gen/proto/app/bucket/v1/bucket_pb.d.ts +911 -0
- package/dist/gen/proto/app/bucket/v1/bucket_pb.js +253 -0
- package/dist/gen/proto/app/realtime/v1/realtime_pb.d.ts +50 -0
- package/dist/gen/proto/app/realtime/v1/realtime_pb.js +23 -0
- package/dist/gen/proto/app/resources/v1/resources_pb.d.ts +554 -0
- package/dist/gen/proto/app/resources/v1/resources_pb.js +214 -0
- package/dist/gen/proto/app/resources/v1/variables_pb.d.ts +231 -0
- package/dist/gen/proto/app/resources/v1/variables_pb.js +101 -0
- package/dist/gen/proto/app/task/v1/task_pb.d.ts +498 -0
- package/dist/gen/proto/app/task/v1/task_pb.js +151 -0
- package/dist/gen/proto/app/topic/v1/topic_pb.d.ts +456 -0
- package/dist/gen/proto/app/topic/v1/topic_pb.js +125 -0
- package/dist/gen/proto/buf/validate/validate_pb.d.ts +4833 -0
- package/dist/gen/proto/buf/validate/validate_pb.js +356 -0
- package/dist/gen/proto/common/bindings/v1/bindings_pb.d.ts +378 -0
- package/dist/gen/proto/common/bindings/v1/bindings_pb.js +140 -0
- package/dist/generated/config.d.ts +428 -0
- package/dist/generated/config.js +2 -0
- package/dist/kv/entries.d.ts +193 -0
- package/dist/kv/entries.js +325 -0
- package/dist/kv/errors.d.ts +12 -0
- package/dist/kv/errors.js +14 -0
- package/dist/kv/index.d.ts +6 -0
- package/dist/kv/index.js +3 -0
- package/dist/kv/ioredis.d.ts +2 -0
- package/dist/kv/ioredis.js +20 -0
- package/dist/kv/kv.d.ts +84 -0
- package/dist/kv/kv.js +177 -0
- package/dist/kv/pattern.d.ts +27 -0
- package/dist/kv/pattern.js +73 -0
- package/dist/kv/ttl.d.ts +22 -0
- package/dist/kv/ttl.js +21 -0
- package/dist/postgres/index.d.ts +21 -2
- package/dist/postgres/index.js +73 -2
- package/dist/postgres/pg.d.ts +5 -4
- package/dist/postgres/pg.js +16 -12
- package/dist/providers/aws/dns.d.ts +11 -0
- package/dist/providers/aws/dns.js +4 -0
- package/dist/providers/aws/edge.d.ts +6 -0
- package/dist/providers/aws/edge.js +8 -0
- package/dist/providers/aws/index.d.ts +4 -0
- package/dist/providers/aws/index.js +4 -0
- package/dist/providers/gcp/edge.d.ts +14 -0
- package/dist/providers/gcp/edge.js +14 -0
- package/dist/providers/gcp/index.d.ts +9 -0
- package/dist/providers/gcp/index.js +9 -0
- package/dist/providers/vps/index.d.ts +4 -0
- package/dist/providers/vps/index.js +4 -0
- package/dist/realtime/client-error.d.ts +30 -0
- package/dist/realtime/client-error.js +17 -0
- package/dist/realtime/client-event-socket.d.ts +8 -0
- package/dist/realtime/client-event-socket.js +169 -0
- package/dist/realtime/client-transport.d.ts +29 -0
- package/dist/realtime/client-transport.js +13 -0
- package/dist/realtime/client.d.ts +150 -0
- package/dist/realtime/client.js +345 -0
- package/dist/realtime/express.d.ts +18 -0
- package/dist/realtime/express.js +83 -0
- package/dist/realtime/handler.d.ts +56 -0
- package/dist/realtime/handler.js +287 -0
- package/dist/realtime/hono.d.ts +19 -0
- package/dist/realtime/hono.js +24 -0
- package/dist/realtime/index.d.ts +2 -0
- package/dist/realtime/index.js +2 -0
- package/dist/realtime/next.d.ts +26 -0
- package/dist/realtime/next.js +16 -0
- package/dist/realtime/realtime.d.ts +181 -0
- package/dist/realtime/realtime.js +205 -0
- package/dist/realtime/token.d.ts +20 -0
- package/dist/realtime/token.js +45 -0
- package/dist/realtime/transport.d.ts +11 -0
- package/dist/realtime/transport.js +15 -0
- package/dist/realtime/wire.d.ts +30 -0
- package/dist/realtime/wire.js +104 -0
- package/dist/runtime/dev-server.d.ts +8 -0
- package/dist/runtime/dev-server.js +8 -0
- package/dist/runtime/rpc.d.ts +23 -0
- package/dist/runtime/rpc.js +24 -0
- package/dist/runtime/transport.d.ts +11 -0
- package/dist/runtime/transport.js +35 -0
- package/dist/task/errors.d.ts +7 -0
- package/dist/task/errors.js +7 -0
- package/dist/task/hooks.d.ts +57 -0
- package/dist/task/hooks.js +1 -0
- package/dist/task/index.d.ts +9 -0
- package/dist/task/index.js +4 -0
- package/dist/task/runs.d.ts +74 -0
- package/dist/task/runs.js +82 -0
- package/dist/task/task.d.ts +117 -0
- package/dist/task/task.js +109 -0
- package/dist/topic/dead-letters.d.ts +53 -0
- package/dist/topic/dead-letters.js +62 -0
- package/dist/topic/index.d.ts +8 -0
- package/dist/topic/index.js +4 -0
- package/dist/topic/topic.d.ts +85 -0
- package/dist/topic/topic.js +117 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/worker/context.d.ts +36 -0
- package/dist/worker/context.js +1 -0
- package/dist/worker/deliver.d.ts +13 -0
- package/dist/worker/deliver.js +258 -0
- package/dist/worker/index.d.ts +4 -0
- package/dist/worker/index.js +2 -0
- package/dist/worker/registry.d.ts +28 -0
- package/dist/worker/registry.js +22 -0
- package/dist/worker/semaphore.d.ts +7 -0
- package/dist/worker/semaphore.js +36 -0
- package/dist/worker/worker.d.ts +21 -0
- package/dist/worker/worker.js +32 -0
- package/package.json +158 -57
- package/bin/run.js +0 -39
- package/dist/blob/bucket.d.ts +0 -55
- package/dist/blob/bucket.js +0 -65
- package/dist/blob/index.d.ts +0 -8
- package/dist/blob/index.js +0 -13
- package/dist/blob/integrations/client.d.ts +0 -25
- package/dist/blob/integrations/client.js +0 -71
- package/dist/blob/integrations/hono.d.ts +0 -31
- package/dist/blob/integrations/hono.js +0 -11
- package/dist/blob/mime-types.d.ts +0 -1
- package/dist/blob/parse.d.ts +0 -16
- package/dist/blob/parse.js +0 -69
- package/dist/blob/s3-listener.d.ts +0 -7
- package/dist/blob/s3-listener.js +0 -35
- package/dist/blob/types.d.ts +0 -128
- package/dist/blob/types.js +0 -1
- package/dist/blob/upload-error.d.ts +0 -35
- package/dist/blob/upload-error.js +0 -54
- package/dist/blob/uploader.d.ts +0 -67
- package/dist/blob/uploader.js +0 -323
- package/dist/internal/db/index.d.ts +0 -68
- package/dist/internal/db/index.js +0 -73
- package/dist/lambda/index.d.ts +0 -2
- package/dist/lambda/index.js +0 -4
- package/dist/lambda/integrations/express.d.ts +0 -3
- package/dist/lambda/integrations/express.js +0 -13
- package/dist/lambda/integrations/hono.d.ts +0 -3
- package/dist/lambda/integrations/hono.js +0 -13
- package/dist/lambda/lambda.d.ts +0 -43
- package/dist/lambda/lambda.js +0 -30
- package/dist/utils/component.d.ts +0 -3
- package/dist/utils/constants.d.ts +0 -2
- package/dist/utils/constants.js +0 -2
- package/dist/utils/nanoid.d.ts +0 -1
- package/dist/utils/nanoid.js +0 -6
- package/dist/utils/rpc.d.ts +0 -11
- package/dist/utils/rpc.js +0 -17
- package/dist/utils/stack.d.ts +0 -4
- package/dist/utils/stack.js +0 -23
- package/dist/workflow/index.d.ts +0 -3
- package/dist/workflow/index.js +0 -4
- package/dist/workflow/workflow.d.ts +0 -34
- package/dist/workflow/workflow.js +0 -146
- package/src/blob/bucket.ts +0 -92
- package/src/blob/index.ts +0 -34
- package/src/blob/integrations/client.ts +0 -127
- package/src/blob/integrations/hono.ts +0 -16
- package/src/blob/mime-types.ts +0 -68
- package/src/blob/parse.ts +0 -93
- package/src/blob/s3-listener.ts +0 -48
- package/src/blob/types.ts +0 -142
- package/src/blob/upload-error.ts +0 -67
- package/src/blob/uploader.ts +0 -426
- package/src/internal/db/index.ts +0 -78
- package/src/lambda/index.ts +0 -8
- package/src/lambda/integrations/express.ts +0 -25
- package/src/lambda/integrations/hono.ts +0 -25
- package/src/lambda/lambda.ts +0 -94
- package/src/postgres/index.ts +0 -5
- package/src/postgres/pg.ts +0 -33
- package/src/utils/component.ts +0 -3
- package/src/utils/constants.ts +0 -2
- package/src/utils/nanoid.ts +0 -9
- package/src/utils/rpc.ts +0 -28
- package/src/utils/stack.ts +0 -26
- package/src/workflow/index.ts +0 -9
- package/src/workflow/workflow.ts +0 -235
- /package/dist/{blob/mime-types.js → bucket/server-only.d.ts} +0 -0
- /package/dist/{utils/component.js → bucket/types.js} +0 -0
package/dist/workflow/index.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
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/src/blob/bucket.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
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
|
-
}
|
package/src/blob/index.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import type z from "zod";
|
|
2
|
-
import type { Bucket } from "../bucket";
|
|
3
|
-
import type { PresignUploadBody, PresignUploadResponse } from "../types";
|
|
4
|
-
|
|
5
|
-
type PollResponse = {
|
|
6
|
-
files: Array<{
|
|
7
|
-
fileKey: string;
|
|
8
|
-
status: "PENDING" | "SUCCESS" | "FAILED";
|
|
9
|
-
file?: { path: string; contentDisposition: string; contentType: string };
|
|
10
|
-
metadata?: unknown;
|
|
11
|
-
}>;
|
|
12
|
-
completed: boolean;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type InferReturn<TRouter, K extends keyof TRouter> = TRouter[K] extends {
|
|
16
|
-
_def: { output: infer R };
|
|
17
|
-
}
|
|
18
|
-
? R
|
|
19
|
-
: never;
|
|
20
|
-
|
|
21
|
-
type InferInput<TRouter, K extends keyof TRouter> = TRouter[K] extends {
|
|
22
|
-
_def: { input: infer R };
|
|
23
|
-
}
|
|
24
|
-
? R
|
|
25
|
-
: never;
|
|
26
|
-
|
|
27
|
-
export function createUploadClient<TBucket extends Bucket<any>>(opts: {
|
|
28
|
-
url: string;
|
|
29
|
-
}) {
|
|
30
|
-
return {
|
|
31
|
-
upload: async <K extends keyof TBucket["_shape"]>(
|
|
32
|
-
uploader: K,
|
|
33
|
-
args: InferInput<TBucket["_shape"], K> extends never
|
|
34
|
-
? {
|
|
35
|
-
files: File[];
|
|
36
|
-
onClientUploadComplete?: (
|
|
37
|
-
res: InferReturn<TBucket["_shape"], K>,
|
|
38
|
-
) => void;
|
|
39
|
-
}
|
|
40
|
-
: {
|
|
41
|
-
files: File[];
|
|
42
|
-
input: z.infer<InferInput<TBucket["_shape"], K>>;
|
|
43
|
-
onClientUploadComplete?: (
|
|
44
|
-
res: InferReturn<TBucket["_shape"], K>,
|
|
45
|
-
) => void;
|
|
46
|
-
},
|
|
47
|
-
) => {
|
|
48
|
-
const url = opts.url;
|
|
49
|
-
const searchParams = new URLSearchParams();
|
|
50
|
-
searchParams.append("action", "presign");
|
|
51
|
-
searchParams.append("uploader", String(uploader));
|
|
52
|
-
|
|
53
|
-
// get presign urls
|
|
54
|
-
const result = (await fetch(`${url}?${searchParams.toString()}`, {
|
|
55
|
-
method: "POST",
|
|
56
|
-
headers: {
|
|
57
|
-
"Content-Type": "application/json",
|
|
58
|
-
},
|
|
59
|
-
body: JSON.stringify({
|
|
60
|
-
files: args.files.map((f) => {
|
|
61
|
-
return {
|
|
62
|
-
name: f.name,
|
|
63
|
-
size: f.size,
|
|
64
|
-
type: f.type,
|
|
65
|
-
lastModified: f.lastModified,
|
|
66
|
-
mimeType: f.type,
|
|
67
|
-
};
|
|
68
|
-
}),
|
|
69
|
-
...("input" in args ? { input: args.input } : {}),
|
|
70
|
-
} satisfies PresignUploadBody),
|
|
71
|
-
}).then((r) => r.json())) as PresignUploadResponse;
|
|
72
|
-
|
|
73
|
-
// upload files to presign urls (sessionId required for S3 listener to update DynamoDB)
|
|
74
|
-
await Promise.all(
|
|
75
|
-
args.files.map((file, index) => {
|
|
76
|
-
if (!result.files[index]?.url) {
|
|
77
|
-
throw new Error(`No presign URL returned for file index ${index}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return fetch(result.files[index].url, {
|
|
81
|
-
method: "PUT",
|
|
82
|
-
headers: {
|
|
83
|
-
"Content-Type": file.type,
|
|
84
|
-
"Content-Disposition": "inline",
|
|
85
|
-
},
|
|
86
|
-
body: file,
|
|
87
|
-
});
|
|
88
|
-
}),
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
// poll for completion (S3 listener Lambda updates DynamoDB when upload lands)
|
|
92
|
-
const pollParams = new URLSearchParams();
|
|
93
|
-
pollParams.append("action", "poll");
|
|
94
|
-
pollParams.append("uploader", String(uploader));
|
|
95
|
-
pollParams.append("sessionId", result.sessionId);
|
|
96
|
-
|
|
97
|
-
const pollUntilComplete = async (): Promise<PollResponse> => {
|
|
98
|
-
const res = await fetch(`${url}?${pollParams.toString()}`, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: { "Content-Type": "application/json" },
|
|
101
|
-
body: JSON.stringify({}),
|
|
102
|
-
});
|
|
103
|
-
const data = (await res.json()) as PollResponse;
|
|
104
|
-
if (data.completed) return data;
|
|
105
|
-
await new Promise((r) => setTimeout(r, 1000));
|
|
106
|
-
return pollUntilComplete();
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const pollResult = await pollUntilComplete();
|
|
110
|
-
|
|
111
|
-
if (args.onClientUploadComplete && pollResult.files) {
|
|
112
|
-
const completedFiles = pollResult.files.filter(
|
|
113
|
-
(f) => f.status === "SUCCESS" && f.file,
|
|
114
|
-
);
|
|
115
|
-
const results = completedFiles.map((f) => ({
|
|
116
|
-
file: f.file!,
|
|
117
|
-
metadata: f.metadata ?? {},
|
|
118
|
-
}));
|
|
119
|
-
if (results.length > 0) {
|
|
120
|
-
args.onClientUploadComplete(
|
|
121
|
-
results.length === 1 ? results[0] : (results as any),
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Context } from "hono";
|
|
2
|
-
import type { Bucket } from "../bucket";
|
|
3
|
-
|
|
4
|
-
export function createRouteHandler(bucket: Bucket<any>) {
|
|
5
|
-
return async (c: Context) => {
|
|
6
|
-
const req = c.req.raw;
|
|
7
|
-
const body = (await req.json()) as any;
|
|
8
|
-
|
|
9
|
-
const result = await bucket.handleUpload({
|
|
10
|
-
body,
|
|
11
|
-
request: req,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
return c.json(result);
|
|
15
|
-
};
|
|
16
|
-
}
|
package/src/blob/mime-types.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
export type KnownMimeType =
|
|
2
|
-
// --- APPLICATION ---
|
|
3
|
-
| "application/json"
|
|
4
|
-
| "application/xml"
|
|
5
|
-
| "application/octet-stream" // Generic binary data
|
|
6
|
-
| "application/zip"
|
|
7
|
-
| "application/gzip"
|
|
8
|
-
| "application/x-tar"
|
|
9
|
-
| "application/wasm"
|
|
10
|
-
| "application/x-7z-compressed" // 7z files
|
|
11
|
-
| "application/x-sh" // Shell script
|
|
12
|
-
| "application/*"
|
|
13
|
-
|
|
14
|
-
// --- DOCUMENTS / TEXT ---
|
|
15
|
-
| "text/plain"
|
|
16
|
-
| "text/csv"
|
|
17
|
-
| "text/html"
|
|
18
|
-
| "text/css"
|
|
19
|
-
| "text/javascript" // Use application/javascript in production
|
|
20
|
-
| "application/javascript"
|
|
21
|
-
| "application/typescript"
|
|
22
|
-
| "application/pdf"
|
|
23
|
-
| "application/rtf"
|
|
24
|
-
| "text/*"
|
|
25
|
-
|
|
26
|
-
// --- IMAGES ---
|
|
27
|
-
| "image/jpeg"
|
|
28
|
-
| "image/png"
|
|
29
|
-
| "image/gif"
|
|
30
|
-
| "image/webp"
|
|
31
|
-
| "image/svg+xml"
|
|
32
|
-
| "image/bmp"
|
|
33
|
-
| "image/tiff"
|
|
34
|
-
| "image/avif"
|
|
35
|
-
| "image/vnd.microsoft.icon" // ICO files
|
|
36
|
-
| "image/*"
|
|
37
|
-
|
|
38
|
-
// --- AUDIO ---
|
|
39
|
-
| "audio/mpeg" // MP3
|
|
40
|
-
| "audio/wav"
|
|
41
|
-
| "audio/ogg"
|
|
42
|
-
| "audio/aac"
|
|
43
|
-
| "audio/flac"
|
|
44
|
-
| "audio/*"
|
|
45
|
-
|
|
46
|
-
// --- VIDEO ---
|
|
47
|
-
| "video/mp4"
|
|
48
|
-
| "video/webm"
|
|
49
|
-
| "video/ogg"
|
|
50
|
-
| "video/quicktime" // MOV
|
|
51
|
-
| "video/x-msvideo" // AVI
|
|
52
|
-
| "video/*"
|
|
53
|
-
|
|
54
|
-
// --- MICROSOFT OFFICE / OPEN OFFICE (Crucial for Business Apps) ---
|
|
55
|
-
| "application/msword" // .doc
|
|
56
|
-
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document" // .docx
|
|
57
|
-
| "application/vnd.ms-excel" // .xls
|
|
58
|
-
| "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" // .xlsx
|
|
59
|
-
| "application/vnd.ms-powerpoint" // .ppt
|
|
60
|
-
| "application/vnd.openxmlformats-officedocument.presentationml.presentation" // .pptx
|
|
61
|
-
| "application/vnd.oasis.opendocument.text" // .odt
|
|
62
|
-
| "application/vnd.oasis.opendocument.spreadsheet" // .ods
|
|
63
|
-
|
|
64
|
-
// --- FONT ---
|
|
65
|
-
| "font/otf"
|
|
66
|
-
| "font/ttf"
|
|
67
|
-
| "font/woff"
|
|
68
|
-
| "font/woff2";
|
package/src/blob/parse.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage } from "node:http";
|
|
2
|
-
import type { FileSize } from "./types";
|
|
3
|
-
import { UploadError } from "./upload-error";
|
|
4
|
-
|
|
5
|
-
export function parseReq(req: Request | IncomingMessage): {
|
|
6
|
-
headers: Record<string, string>;
|
|
7
|
-
method: string;
|
|
8
|
-
url: string;
|
|
9
|
-
path: string;
|
|
10
|
-
query: URLSearchParams;
|
|
11
|
-
} {
|
|
12
|
-
if (req instanceof Request) {
|
|
13
|
-
const headers: Record<string, string> = {};
|
|
14
|
-
req.headers.forEach((value, key) => {
|
|
15
|
-
headers[key] = value;
|
|
16
|
-
});
|
|
17
|
-
return {
|
|
18
|
-
headers,
|
|
19
|
-
method: req.method,
|
|
20
|
-
url: req.url,
|
|
21
|
-
path: new URL(req.url).pathname,
|
|
22
|
-
query: new URL(req.url).searchParams,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const headers: Record<string, string> = {};
|
|
27
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
28
|
-
if (typeof value === "string") {
|
|
29
|
-
headers[key] = value;
|
|
30
|
-
} else if (Array.isArray(value)) {
|
|
31
|
-
headers[key] = value.join(", ");
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const url = new URL(req.url || "", "http://localhost");
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
headers,
|
|
39
|
-
method: req.method || "GET",
|
|
40
|
-
url: req.url || "",
|
|
41
|
-
path: url.pathname,
|
|
42
|
-
query: url.searchParams,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Converts a human-readable file size string (e.g., "10MB", "2.5GB") into bytes.
|
|
48
|
-
* Throws an error for invalid input formats.
|
|
49
|
-
* * @param size The file size represented as a number (bytes) or a string with units (KB, MB, GB, B).
|
|
50
|
-
* @returns The size in bytes (number).
|
|
51
|
-
*/
|
|
52
|
-
export function parseFileSize(size: FileSize): number {
|
|
53
|
-
if (typeof size === "number") {
|
|
54
|
-
if (size < 0) {
|
|
55
|
-
throw UploadError.badRequest("File size cannot be negative.");
|
|
56
|
-
}
|
|
57
|
-
return size;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Convert the input to uppercase to handle 'mb', 'MB', 'Mb' uniformly
|
|
61
|
-
const cleanSize = size.toUpperCase().trim();
|
|
62
|
-
|
|
63
|
-
// Regular expression to extract the numeric value and the unit
|
|
64
|
-
// Captures: (1) The number part, (2) The unit part (B, KB, MB, GB)
|
|
65
|
-
const match = cleanSize.match(/^(\d+(\.\d+)?)([A-Z]+)$/);
|
|
66
|
-
|
|
67
|
-
if (!match) {
|
|
68
|
-
throw UploadError.badRequest(
|
|
69
|
-
`Invalid file size format: "${size}". Expected formats: "10MB", "2GB", "512KB", or raw bytes.`
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const numericValue = Number.parseFloat(match[1] ?? "0");
|
|
74
|
-
const unit = match[3] ?? "";
|
|
75
|
-
|
|
76
|
-
const byteConversionMap: Record<string, number> = {
|
|
77
|
-
B: 1,
|
|
78
|
-
KB: 1024,
|
|
79
|
-
MB: 1024 ** 2, // 1,048,576
|
|
80
|
-
GB: 1024 ** 3, // 1,073,741,824
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const multiplier = byteConversionMap[unit];
|
|
84
|
-
|
|
85
|
-
if (!multiplier) {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`Invalid file size unit: "${unit}". Must be B, KB, MB, or GB.`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Return the size rounded to the nearest integer byte
|
|
92
|
-
return Math.round(numericValue * multiplier);
|
|
93
|
-
}
|
package/src/blob/s3-listener.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S3 event listener Lambda for ocel/blob.
|
|
3
|
-
* Receives ObjectCreated events, reads x-ocel-session-id from object metadata,
|
|
4
|
-
* and updates the UploadSession in DynamoDB to SUCCESS.
|
|
5
|
-
*/
|
|
6
|
-
import type { S3Handler, S3Event } from "aws-lambda";
|
|
7
|
-
import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
8
|
-
import { UpdateItemCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
9
|
-
|
|
10
|
-
const s3 = new S3Client({});
|
|
11
|
-
const dynamo = new DynamoDBClient({});
|
|
12
|
-
const TABLE_NAME = process.env.OCEL_TABLE_NAME || "OcelTable";
|
|
13
|
-
|
|
14
|
-
export const handler: S3Handler = async (event: S3Event) => {
|
|
15
|
-
for (const record of event.Records) {
|
|
16
|
-
const bucket = record.s3.bucket.name;
|
|
17
|
-
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const head = await s3.send(
|
|
21
|
-
new HeadObjectCommand({ Bucket: bucket, Key: key }),
|
|
22
|
-
);
|
|
23
|
-
const sessionId = head.Metadata?.["x-ocel-session-id"];
|
|
24
|
-
if (!sessionId) continue;
|
|
25
|
-
|
|
26
|
-
const pk = `SESSION#${sessionId}`.toLowerCase();
|
|
27
|
-
const sk = `FILE#${bucket}#${key}`.toLowerCase();
|
|
28
|
-
|
|
29
|
-
await dynamo.send(
|
|
30
|
-
new UpdateItemCommand({
|
|
31
|
-
TableName: TABLE_NAME,
|
|
32
|
-
Key: {
|
|
33
|
-
pk: { S: pk },
|
|
34
|
-
sk: { S: sk },
|
|
35
|
-
},
|
|
36
|
-
UpdateExpression: "SET #status = :status, updatedAt = :now",
|
|
37
|
-
ExpressionAttributeNames: { "#status": "status" },
|
|
38
|
-
ExpressionAttributeValues: {
|
|
39
|
-
":status": { S: "SUCCESS" },
|
|
40
|
-
":now": { S: new Date().toISOString() },
|
|
41
|
-
},
|
|
42
|
-
}),
|
|
43
|
-
);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
console.error(`Failed to update session for ${bucket}/${key}:`, err);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
};
|