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,426 @@
1
+ import slugify from "@sindresorhus/slugify";
2
+ import type {
3
+ CallbackBody,
4
+ HandleUploadBody,
5
+ PathConfig,
6
+ PathGenerator,
7
+ PresignUploadBody,
8
+ TUploaderConfig,
9
+ UploadFileBody,
10
+ } from "./types";
11
+ import { UploadError } from "./upload-error";
12
+ import type { IncomingMessage } from "node:http";
13
+ import path from "node:path";
14
+ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
15
+ import { getNanoid } from "../utils/nanoid";
16
+ import { UploadSession } from "../internal/db";
17
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
18
+ import type { ZodType } from "zod";
19
+ import { parseFileSize, parseReq } from "./parse";
20
+ import type { Bucket } from "./bucket";
21
+
22
+ export class Uploader<TMeta = unknown> {
23
+ private pathGenerator: PathGenerator<TMeta> | null = null;
24
+
25
+ constructor(
26
+ private bucket: Bucket<any>,
27
+ private name: string,
28
+ private config: TUploaderConfig<any, TMeta>
29
+ ) {
30
+ const path = this.config.path;
31
+ if (path) {
32
+ if (typeof path === "function") {
33
+ this.pathGenerator = path;
34
+ } else {
35
+ this.pathGenerator = this.createConfigBasedGenerator(path);
36
+ }
37
+ }
38
+ }
39
+
40
+ async handleUpload({
41
+ action,
42
+ body,
43
+ request,
44
+ sessionId,
45
+ }: {
46
+ request: Request | IncomingMessage;
47
+ body: HandleUploadBody;
48
+ action: "presign" | "callback" | "poll";
49
+ sessionId?: string;
50
+ }) {
51
+ try {
52
+ const req = parseReq(request);
53
+
54
+ switch (action) {
55
+ case "presign": {
56
+ if (!("files" in body)) {
57
+ throw UploadError.badRequest("Invalid presign body");
58
+ }
59
+
60
+ const presignBody = body as PresignUploadBody;
61
+ const rawInput = presignBody.input || {};
62
+ const parser = this.config.input as ZodType | undefined;
63
+
64
+ let parsedInput: any;
65
+
66
+ // TODO: error handling
67
+ if (parser) {
68
+ const parsed = parser.safeParse(rawInput);
69
+ if (!parsed.success) {
70
+ throw UploadError.badRequest(parsed.error.issues[0]?.message);
71
+ }
72
+
73
+ parsedInput = parsed.data;
74
+ }
75
+
76
+ const middleware = this.config.onBeforeUpload;
77
+ const customMeta = await middleware?.({
78
+ req: request,
79
+ input: parsedInput,
80
+ files: presignBody.files,
81
+ });
82
+
83
+ const metadata = {
84
+ "x-ocel-path": req.path,
85
+ "x-ocel-uploader": this.name,
86
+ ...(customMeta
87
+ ? { "x-ocel-metadata": JSON.stringify(customMeta) }
88
+ : {}),
89
+ };
90
+
91
+ // TODO: validate files against uploader config (size, count, types, etc)
92
+ this.validateFiles(presignBody.files, customMeta);
93
+
94
+ const files = body.files.map((f) => {
95
+ const name = slugify(f.name, { preserveCharacters: ["."] });
96
+
97
+ return {
98
+ ...f,
99
+ key:
100
+ this.pathGenerator?.({
101
+ fileNameWithExt: name,
102
+ metadata: customMeta as any,
103
+ fileNameWithoutExt: name.replace(path.extname(name), ""),
104
+ ext: path.extname(name),
105
+ } as any) || name,
106
+ disposition: "inline",
107
+ metadata: {
108
+ ...metadata,
109
+ "x-ocel-original-filename": f.name,
110
+ },
111
+ };
112
+ }) satisfies UploadFileBody[];
113
+
114
+ const provider = this.getProvider();
115
+
116
+ return provider.presign(files);
117
+ }
118
+ case "poll": {
119
+ if (!sessionId) {
120
+ throw UploadError.badRequest("sessionId is required for poll");
121
+ }
122
+
123
+ const result = await UploadSession.query
124
+ .sessions({ sessionId })
125
+ .go();
126
+
127
+ const files = result.data.map((record) => {
128
+ let parsedMeta: TMeta = {} as TMeta;
129
+ try {
130
+ if (record.metadata) {
131
+ parsedMeta = JSON.parse(record.metadata) as TMeta;
132
+ }
133
+ } catch {
134
+ // ignore parse errors
135
+ }
136
+ return {
137
+ fileKey: record.fileKey,
138
+ status: record.status,
139
+ file:
140
+ record.status === "SUCCESS"
141
+ ? {
142
+ path: record.fileKey,
143
+ contentDisposition:
144
+ record.contentDisposition || "inline",
145
+ contentType: record.contentType,
146
+ }
147
+ : undefined,
148
+ metadata: parsedMeta,
149
+ };
150
+ });
151
+
152
+ const allComplete = files.every(
153
+ (f) => f.status === "SUCCESS" || f.status === "FAILED"
154
+ );
155
+
156
+ return {
157
+ files,
158
+ completed: allComplete,
159
+ };
160
+ }
161
+ case "callback": {
162
+ // TODO: validate callback body
163
+ if (!("metadata" in body)) {
164
+ throw UploadError.badRequest("Invalid callback body");
165
+ }
166
+
167
+ const data = body as CallbackBody<TMeta>;
168
+
169
+ await this.config.onUploadComplete?.(data);
170
+
171
+ return {
172
+ type: "UPLOAD_COMPLETED" as const,
173
+ response: "ok" as const,
174
+ };
175
+ }
176
+ default:
177
+ throw UploadError.badRequest(`Unhandled action type: ${action}`);
178
+ }
179
+ } catch (err) {
180
+ if (err instanceof UploadError) {
181
+ this.config.onUploadError?.({ error: err });
182
+
183
+ return {
184
+ error: {
185
+ message: err.message,
186
+ code: err.code, // e.g. "FORBIDDEN"
187
+ status: err.statusCode, // e.g. 403
188
+ },
189
+ };
190
+ }
191
+
192
+ return {
193
+ error: {
194
+ message: "Internal Server Error",
195
+ code: "INTERNAL_SERVER_ERROR",
196
+ status: 500,
197
+ },
198
+ };
199
+ }
200
+ }
201
+
202
+ private startPolling(sessionId: string): void {
203
+ const POLL_INTERVAL_MS = 1000;
204
+ const MAX_DURATION_MS = 10 * 60 * 1000; // 10 minutes
205
+ const startTime = Date.now();
206
+
207
+ const tick = async (): Promise<boolean> => {
208
+ if (Date.now() - startTime > MAX_DURATION_MS) {
209
+ return true;
210
+ }
211
+
212
+ const result = await UploadSession.query
213
+ .sessions({ sessionId })
214
+ .go();
215
+
216
+ const allComplete = result.data.every(
217
+ (r) => r.status === "SUCCESS" || r.status === "FAILED"
218
+ );
219
+
220
+ if (!allComplete) {
221
+ return false;
222
+ }
223
+
224
+ // Session complete - invoke callbacks
225
+ for (const record of result.data) {
226
+ let parsedMeta: TMeta = {} as TMeta;
227
+ try {
228
+ if (record.metadata) {
229
+ parsedMeta = JSON.parse(record.metadata) as TMeta;
230
+ }
231
+ } catch {
232
+ // ignore
233
+ }
234
+
235
+ if (record.status === "SUCCESS") {
236
+ await this.config.onUploadComplete?.({
237
+ metadata: parsedMeta,
238
+ file: {
239
+ path: record.fileKey,
240
+ contentDisposition: record.contentDisposition || "inline",
241
+ contentType: record.contentType,
242
+ },
243
+ });
244
+ } else if (record.status === "FAILED" && this.config.onUploadError) {
245
+ this.config.onUploadError({
246
+ error: new UploadError(
247
+ `Upload failed for ${record.fileKey}`,
248
+ 500,
249
+ "UPLOAD_FAILED"
250
+ ),
251
+ });
252
+ }
253
+ }
254
+
255
+ return true;
256
+ };
257
+
258
+ const intervalId = setInterval(async () => {
259
+ try {
260
+ const done = await tick();
261
+ if (done) {
262
+ clearInterval(intervalId);
263
+ }
264
+ } catch {
265
+ clearInterval(intervalId);
266
+ }
267
+ }, POLL_INTERVAL_MS);
268
+ }
269
+
270
+ getProvider() {
271
+ const bucket = this.bucket;
272
+ const bucketId = bucket.__name();
273
+
274
+ return {
275
+ presign: async (files: UploadFileBody[]) => {
276
+ const s3Client = new S3Client();
277
+ const sessionId = getNanoid(8);
278
+
279
+ const presignedFiles = await Promise.all(
280
+ files.map(async (file) => {
281
+ const metadataWithSession = {
282
+ ...file.metadata,
283
+ "x-ocel-session-id": sessionId,
284
+ };
285
+
286
+ await UploadSession.create({
287
+ sessionId,
288
+ bucketName: bucketId,
289
+ fileKey: file.key,
290
+ createdAt: new Date().toISOString(),
291
+ status: "PENDING",
292
+ contentType: file.mimeType,
293
+ contentDisposition: file.disposition,
294
+ fileSize: file.size,
295
+ metadata: file.metadata
296
+ ? JSON.stringify(file.metadata)
297
+ : undefined,
298
+ }).go();
299
+
300
+ const url = await getSignedUrl(
301
+ s3Client,
302
+ new PutObjectCommand({
303
+ Key: file.key,
304
+ Bucket: bucketId,
305
+ ContentType: file.mimeType,
306
+ Metadata: metadataWithSession,
307
+ }),
308
+ { expiresIn: 3600 }
309
+ );
310
+
311
+ return {
312
+ url,
313
+ key: file.key,
314
+ name: file.name,
315
+ };
316
+ })
317
+ );
318
+
319
+ this.startPolling(sessionId);
320
+
321
+ return {
322
+ files: presignedFiles,
323
+ sessionId,
324
+ };
325
+ },
326
+ };
327
+ }
328
+
329
+ createConfigBasedGenerator(config: PathConfig): PathGenerator<any> {
330
+ return (ctx) => {
331
+ let key = ctx.fileNameWithoutExt;
332
+
333
+ // random suffix
334
+ if (config.randomSuffix) {
335
+ key += `-${Math.random().toString(36).slice(2, 10)}`;
336
+ }
337
+
338
+ // static suffix
339
+ else if (config.suffix) {
340
+ key += `-${config.suffix}`;
341
+ }
342
+
343
+ // add extension
344
+ key += ctx.ext;
345
+
346
+ // prefix
347
+ if (config.prefix) {
348
+ return `${config.prefix.replace(/\/$/, "")}/${key}`;
349
+ }
350
+
351
+ return key;
352
+ };
353
+ }
354
+
355
+ private validateFiles(
356
+ files: PresignUploadBody["files"],
357
+ metadata?: TMeta
358
+ ): void {
359
+ const { accept, limits } = this.config;
360
+
361
+ if (limits?.maxFileCount !== undefined) {
362
+ const max =
363
+ typeof limits.maxFileCount === "function"
364
+ ? limits.maxFileCount({ metadata })
365
+ : limits.maxFileCount;
366
+
367
+ if (files.length > max) {
368
+ throw UploadError.badRequest(
369
+ `File limit exceeded. Maximum files allowed: ${max}.`
370
+ );
371
+ }
372
+ }
373
+ if (limits?.minFileCount !== undefined) {
374
+ const min =
375
+ typeof limits.minFileCount === "function"
376
+ ? limits.minFileCount({ metadata })
377
+ : limits.minFileCount;
378
+
379
+ if (files.length < min) {
380
+ throw UploadError.badRequest(
381
+ `File limit not met. Minimum files required: ${min}.`
382
+ );
383
+ }
384
+ }
385
+
386
+ // 2. Per-File Validation (Size and MIME Type)
387
+ for (const file of files) {
388
+ // MIME Type Validation
389
+ if (accept && accept.length > 0) {
390
+ const isAccepted = accept.some((allowedType) => {
391
+ // Simple check for wildcards (*/*) and direct matches (image/png)
392
+ if (allowedType.endsWith("/*")) {
393
+ const typeGroup = allowedType.slice(0, -2);
394
+ return file.mimeType.startsWith(typeGroup);
395
+ }
396
+ return file.mimeType === allowedType;
397
+ });
398
+
399
+ if (!isAccepted) {
400
+ throw UploadError.badRequest(
401
+ `File type not allowed: ${
402
+ file.mimeType
403
+ }. Must be one of: ${accept.join(", ")}.`
404
+ );
405
+ }
406
+ }
407
+
408
+ // Max File Size Validation
409
+ if (limits?.maxFileSize !== undefined) {
410
+ const sizeValue =
411
+ typeof limits.maxFileSize === "function"
412
+ ? limits.maxFileSize({ metadata })
413
+ : limits.maxFileSize;
414
+
415
+ const maxSizeInBytes = parseFileSize(sizeValue);
416
+
417
+ if (file.size > maxSizeInBytes) {
418
+ // You might want to convert maxSize from bytes to KB/MB for a better error message
419
+ throw UploadError.badRequest(
420
+ `File size too large. Maximum size: ${maxSizeInBytes} bytes.`
421
+ );
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
@@ -0,0 +1,78 @@
1
+ import { Entity } from "electrodb";
2
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
+
4
+ const client = new DynamoDBClient({});
5
+
6
+ export const UploadSession = new Entity(
7
+ {
8
+ model: {
9
+ entity: "upload-session",
10
+ version: "1",
11
+ service: "blob",
12
+ },
13
+ attributes: {
14
+ sessionId: {
15
+ type: "string",
16
+ required: true,
17
+ },
18
+ bucketName: {
19
+ type: "string",
20
+ required: true,
21
+ },
22
+ fileKey: {
23
+ type: "string",
24
+ required: true,
25
+ },
26
+ contentType: {
27
+ type: "string",
28
+ required: true,
29
+ },
30
+ contentDisposition: {
31
+ type: "string",
32
+ required: false,
33
+ },
34
+ fileSize: {
35
+ type: "number",
36
+ required: true,
37
+ },
38
+ createdAt: {
39
+ type: "string",
40
+ required: true,
41
+ default: () => new Date().toISOString(),
42
+ },
43
+ updatedAt: {
44
+ type: "string",
45
+ required: true,
46
+ default: () => new Date().toISOString(),
47
+ set: () => new Date().toISOString(),
48
+ },
49
+ status: {
50
+ type: ["PENDING", "SUCCESS", "FAILED"] as const,
51
+ required: true,
52
+ default: "PENDING",
53
+ },
54
+ metadata: {
55
+ type: "string",
56
+ required: false,
57
+ },
58
+ },
59
+ indexes: {
60
+ sessions: {
61
+ pk: {
62
+ field: "pk",
63
+ composite: ["sessionId"],
64
+ template: "SESSION#${sessionId}",
65
+ },
66
+ sk: {
67
+ field: "sk",
68
+ composite: ["bucketName", "fileKey"],
69
+ template: "FILE#${bucketName}#${fileKey}",
70
+ },
71
+ },
72
+ },
73
+ },
74
+ {
75
+ client,
76
+ table: "OcelTable", // required for all ocel logic
77
+ }
78
+ );
@@ -0,0 +1,8 @@
1
+ import { Lambda, type LambdaConfig, type TriggerMap } from "./lambda";
2
+
3
+ export function lambda<T extends keyof TriggerMap>(
4
+ id: string,
5
+ config: LambdaConfig<T>
6
+ ) {
7
+ return new Lambda(id, config);
8
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ Lambda,
3
+ type LambdaConfig,
4
+ type TriggerMap,
5
+ type TriggerUnion,
6
+ } from "../lambda";
7
+ import sls from "serverless-http";
8
+ import type { Express } from "express";
9
+
10
+ export function lambda<T extends keyof TriggerMap>(
11
+ id: string,
12
+ app: Express,
13
+ config?: Omit<LambdaConfig<T>, "trigger" | "handler">,
14
+ ) {
15
+ const handler = sls(app);
16
+
17
+ return new Lambda(id, {
18
+ ...config,
19
+ handler: handler as any,
20
+ trigger: {
21
+ type: "url",
22
+ config: {},
23
+ } as Extract<TriggerUnion, { type: T }>,
24
+ });
25
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ Lambda,
3
+ type LambdaConfig,
4
+ type TriggerMap,
5
+ type TriggerUnion,
6
+ } from "../lambda";
7
+ import type { Hono } from "hono";
8
+ import { handle } from "hono/aws-lambda";
9
+
10
+ export function lambda<T extends keyof TriggerMap>(
11
+ id: string,
12
+ app: Hono,
13
+ config?: Omit<LambdaConfig<T>, "trigger" | "handler">,
14
+ ) {
15
+ const handler = handle(app);
16
+
17
+ return new Lambda(id, {
18
+ ...config,
19
+ handler: handler as any,
20
+ trigger: {
21
+ type: "url",
22
+ config: {},
23
+ } as Extract<TriggerUnion, { type: T }>,
24
+ });
25
+ }
@@ -0,0 +1,94 @@
1
+ import type { Component } from "../utils/component";
2
+ import { rpc } from "../utils/rpc";
3
+ import { getCallerFile } from "../utils/stack";
4
+ import type {
5
+ Context,
6
+ APIGatewayEvent,
7
+ S3Event,
8
+ LambdaFunctionURLEvent,
9
+ } from "aws-lambda";
10
+
11
+ export type TriggerMap = {
12
+ s3: {
13
+ bucket: string;
14
+ events: string[];
15
+ };
16
+ cron: {
17
+ schedule: string;
18
+ };
19
+ url: {
20
+ streaming?: boolean;
21
+ };
22
+ api: {
23
+ id?: string;
24
+ path: string;
25
+ method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "*";
26
+ };
27
+ };
28
+
29
+ export type TriggerUnion = {
30
+ [K in keyof TriggerMap]: { type: K; config: TriggerMap[K] };
31
+ }[keyof TriggerMap];
32
+
33
+ export type LambdaHandlerOutput<T extends keyof TriggerMap> = T extends "api"
34
+ ? {
35
+ statusCode: number;
36
+ headers: Record<string, string>;
37
+ body: string;
38
+ }
39
+ : any;
40
+
41
+ export type LambdaEvent<T extends keyof TriggerMap> = T extends "s3"
42
+ ? S3Event
43
+ : T extends "api"
44
+ ? APIGatewayEvent
45
+ : T extends "url"
46
+ ? LambdaFunctionURLEvent
47
+ : any;
48
+
49
+ export interface LambdaConfig<T extends keyof TriggerMap> {
50
+ handler: (
51
+ event: LambdaEvent<T>,
52
+ context: Context,
53
+ ) => Promise<LambdaHandlerOutput<T>> | LambdaHandlerOutput<T>;
54
+
55
+ trigger: Extract<TriggerUnion, { type: T }>;
56
+ link?: Component[];
57
+ }
58
+
59
+ export class Lambda<T extends keyof TriggerMap> implements Component {
60
+ constructor(
61
+ public id: string,
62
+ config: LambdaConfig<T>,
63
+ ) {
64
+ // remove non-serializable stuff
65
+ const { handler, link, ...rest } = config;
66
+ const links = link?.map((component) => component.__id()) || [];
67
+
68
+ // @ts-expect-error - accessed by cli during build
69
+ this.__handler = handler;
70
+ // @ts-expect-error - accessed by cli during build
71
+ this[Symbol.for("ocel:lambda:id")] = id;
72
+
73
+ if (process.env.OCEL_PHASE === "discovery") {
74
+ const source = getCallerFile();
75
+
76
+ if (!source) {
77
+ throw new Error(
78
+ "Could not determine caller file for lambda registration",
79
+ );
80
+ }
81
+
82
+ rpc.register({
83
+ id: this.id,
84
+ type: "lambda",
85
+ source,
86
+ config: { ...rest, links },
87
+ });
88
+ }
89
+ }
90
+
91
+ __id(): string {
92
+ return this.id;
93
+ }
94
+ }
@@ -0,0 +1,5 @@
1
+ import { Postgres, type PostgresConfig } from "./pg";
2
+
3
+ export function postgres(id: string, config: PostgresConfig) {
4
+ return new Postgres(id, config);
5
+ }
@@ -0,0 +1,33 @@
1
+ import type { Component } from "../utils/component";
2
+ import { rpc } from "../utils/rpc";
3
+ import { getCallerFile } from "../utils/stack";
4
+
5
+ export interface PostgresConfig {
6
+ version?: string;
7
+ migrations?: string;
8
+ }
9
+
10
+ export class Postgres implements Component {
11
+ constructor(public id: string, config: PostgresConfig) {
12
+ // remove non-serializable stuff
13
+
14
+ if (process.env.OCEL_PHASE === "discovery") {
15
+ const source = getCallerFile();
16
+
17
+ rpc.register({
18
+ id: this.id,
19
+ type: "postgres",
20
+ source: source || "unknown",
21
+ config: {},
22
+ });
23
+ }
24
+ }
25
+
26
+ __id(): string {
27
+ return this.id;
28
+ }
29
+
30
+ async sql(query: string) {
31
+ return {};
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ export interface Component {
2
+ __id(): string;
3
+ }
@@ -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";