routeflow-api 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/adapters/cassandra.cjs +117 -0
- package/dist/adapters/cassandra.cjs.map +1 -0
- package/dist/adapters/cassandra.d.cts +37 -0
- package/dist/adapters/cassandra.d.ts +37 -0
- package/dist/adapters/cassandra.js +90 -0
- package/dist/adapters/cassandra.js.map +1 -0
- package/dist/adapters/dynamodb.cjs +180 -0
- package/dist/adapters/dynamodb.cjs.map +1 -0
- package/dist/adapters/dynamodb.d.cts +48 -0
- package/dist/adapters/dynamodb.d.ts +48 -0
- package/dist/adapters/dynamodb.js +153 -0
- package/dist/adapters/dynamodb.js.map +1 -0
- package/dist/adapters/elasticsearch.cjs +120 -0
- package/dist/adapters/elasticsearch.cjs.map +1 -0
- package/dist/adapters/elasticsearch.d.cts +43 -0
- package/dist/adapters/elasticsearch.d.ts +43 -0
- package/dist/adapters/elasticsearch.js +93 -0
- package/dist/adapters/elasticsearch.js.map +1 -0
- package/dist/adapters/mongodb.cjs +159 -0
- package/dist/adapters/mongodb.cjs.map +1 -0
- package/dist/adapters/mongodb.d.cts +54 -0
- package/dist/adapters/mongodb.d.ts +54 -0
- package/dist/adapters/mongodb.js +132 -0
- package/dist/adapters/mongodb.js.map +1 -0
- package/dist/adapters/mysql.cjs +159 -0
- package/dist/adapters/mysql.cjs.map +1 -0
- package/dist/adapters/mysql.d.cts +63 -0
- package/dist/adapters/mysql.d.ts +63 -0
- package/dist/adapters/mysql.js +132 -0
- package/dist/adapters/mysql.js.map +1 -0
- package/dist/adapters/opensearch.cjs +120 -0
- package/dist/adapters/opensearch.cjs.map +1 -0
- package/dist/adapters/opensearch.d.cts +2 -0
- package/dist/adapters/opensearch.d.ts +2 -0
- package/dist/adapters/opensearch.js +93 -0
- package/dist/adapters/opensearch.js.map +1 -0
- package/dist/adapters/postgres.cjs +271 -0
- package/dist/adapters/postgres.cjs.map +1 -0
- package/dist/adapters/postgres.d.cts +81 -0
- package/dist/adapters/postgres.d.ts +81 -0
- package/dist/adapters/postgres.js +244 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/redis.cjs +153 -0
- package/dist/adapters/redis.cjs.map +1 -0
- package/dist/adapters/redis.d.cts +40 -0
- package/dist/adapters/redis.d.ts +40 -0
- package/dist/adapters/redis.js +126 -0
- package/dist/adapters/redis.js.map +1 -0
- package/dist/adapters/snowflake.cjs +117 -0
- package/dist/adapters/snowflake.cjs.map +1 -0
- package/dist/adapters/snowflake.d.cts +37 -0
- package/dist/adapters/snowflake.d.ts +37 -0
- package/dist/adapters/snowflake.js +90 -0
- package/dist/adapters/snowflake.js.map +1 -0
- package/dist/client/index.cjs +484 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +174 -0
- package/dist/client/index.d.ts +174 -0
- package/dist/client/index.js +455 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.cjs +935 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +190 -0
- package/dist/index.d.ts +190 -0
- package/dist/index.js +890 -0
- package/dist/index.js.map +1 -0
- package/dist/types-tPDla8AE.d.cts +75 -0
- package/dist/types-tPDla8AE.d.ts +75 -0
- package/package.json +157 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# routeflow
|
|
2
|
+
|
|
3
|
+
RouteFlow — REST API with real-time database push subscriptions
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install routeflow
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
For PostgreSQL:
|
|
12
|
+
```bash
|
|
13
|
+
npm install routeflow pg
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### Server
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import 'reflect-metadata'
|
|
22
|
+
import { createApp, MemoryAdapter, Route, Reactive } from 'routeflow'
|
|
23
|
+
import type { Context } from 'routeflow'
|
|
24
|
+
|
|
25
|
+
const adapter = new MemoryAdapter()
|
|
26
|
+
const items = [{ id: 1, name: 'Apple' }]
|
|
27
|
+
|
|
28
|
+
class ItemController {
|
|
29
|
+
@Route('GET', '/items')
|
|
30
|
+
async listItems(_ctx: Context) {
|
|
31
|
+
return items
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Reactive({ watch: 'items' })
|
|
35
|
+
@Route('GET', '/items/live')
|
|
36
|
+
async listLiveItems(_ctx: Context) {
|
|
37
|
+
return items
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const app = createApp({ adapter, port: 3000 })
|
|
42
|
+
app.register(ItemController)
|
|
43
|
+
await app.listen()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Client
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { createClient } from 'routeflow/client'
|
|
50
|
+
|
|
51
|
+
const client = createClient('http://localhost:3000')
|
|
52
|
+
|
|
53
|
+
const snapshot = await client.get('/items')
|
|
54
|
+
|
|
55
|
+
const unsubscribe = client.subscribe('/items/live', (items) => {
|
|
56
|
+
console.log('live update', items)
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Database Adapters
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// PostgreSQL
|
|
64
|
+
import { PostgresAdapter } from 'routeflow/adapters/postgres'
|
|
65
|
+
|
|
66
|
+
// MongoDB
|
|
67
|
+
import { MongoDbAdapter } from 'routeflow/adapters/mongodb'
|
|
68
|
+
|
|
69
|
+
// MySQL
|
|
70
|
+
import { MySqlAdapter } from 'routeflow/adapters/mysql'
|
|
71
|
+
|
|
72
|
+
// Redis
|
|
73
|
+
import { RedisAdapter } from 'routeflow/adapters/redis'
|
|
74
|
+
|
|
75
|
+
// DynamoDB
|
|
76
|
+
import { DynamoDbAdapter } from 'routeflow/adapters/dynamodb'
|
|
77
|
+
|
|
78
|
+
// Elasticsearch
|
|
79
|
+
import { ElasticsearchAdapter } from 'routeflow/adapters/elasticsearch'
|
|
80
|
+
|
|
81
|
+
// OpenSearch
|
|
82
|
+
import { OpenSearchAdapter } from 'routeflow/adapters/opensearch'
|
|
83
|
+
|
|
84
|
+
// Snowflake
|
|
85
|
+
import { SnowflakeAdapter } from 'routeflow/adapters/snowflake'
|
|
86
|
+
|
|
87
|
+
// Cassandra
|
|
88
|
+
import { CassandraAdapter } from 'routeflow/adapters/cassandra'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/cassandra.ts
|
|
21
|
+
var cassandra_exports = {};
|
|
22
|
+
__export(cassandra_exports, {
|
|
23
|
+
CassandraAdapter: () => CassandraAdapter
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(cassandra_exports);
|
|
26
|
+
|
|
27
|
+
// src/core/errors.ts
|
|
28
|
+
var ReactiveApiError = class extends Error {
|
|
29
|
+
/** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */
|
|
30
|
+
code;
|
|
31
|
+
constructor(code, message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "ReactiveApiError";
|
|
34
|
+
this.code = code;
|
|
35
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/adapters/cassandra/cassandra-adapter.ts
|
|
40
|
+
var CassandraAdapter = class {
|
|
41
|
+
source;
|
|
42
|
+
onError;
|
|
43
|
+
listeners = /* @__PURE__ */ new Map();
|
|
44
|
+
handleChangeBound = (event) => {
|
|
45
|
+
this.handleChange(event);
|
|
46
|
+
};
|
|
47
|
+
handleErrorBound = (error) => {
|
|
48
|
+
this.onError?.(error);
|
|
49
|
+
};
|
|
50
|
+
connected = false;
|
|
51
|
+
constructor(options) {
|
|
52
|
+
this.source = options.source;
|
|
53
|
+
this.onError = options.onError;
|
|
54
|
+
}
|
|
55
|
+
async connect() {
|
|
56
|
+
if (this.connected) return;
|
|
57
|
+
this.source.on("change", this.handleChangeBound);
|
|
58
|
+
this.source.on("error", this.handleErrorBound);
|
|
59
|
+
try {
|
|
60
|
+
await this.source.start?.();
|
|
61
|
+
} catch (error) {
|
|
62
|
+
throw new ReactiveApiError(
|
|
63
|
+
"CASSANDRA_SOURCE_START_FAILED",
|
|
64
|
+
`Failed to start Cassandra CDC source: ${errorMessage(error)}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
this.connected = true;
|
|
68
|
+
}
|
|
69
|
+
async disconnect() {
|
|
70
|
+
if (!this.connected) return;
|
|
71
|
+
removeListener(this.source, "change", this.handleChangeBound);
|
|
72
|
+
removeListener(this.source, "error", this.handleErrorBound);
|
|
73
|
+
await this.source.stop?.();
|
|
74
|
+
this.listeners.clear();
|
|
75
|
+
this.connected = false;
|
|
76
|
+
}
|
|
77
|
+
onChange(table, callback) {
|
|
78
|
+
if (!this.listeners.has(table)) {
|
|
79
|
+
this.listeners.set(table, /* @__PURE__ */ new Set());
|
|
80
|
+
}
|
|
81
|
+
this.listeners.get(table).add(callback);
|
|
82
|
+
return () => {
|
|
83
|
+
const callbacks = this.listeners.get(table);
|
|
84
|
+
if (!callbacks) return;
|
|
85
|
+
callbacks.delete(callback);
|
|
86
|
+
if (callbacks.size === 0) {
|
|
87
|
+
this.listeners.delete(table);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
handleChange(change) {
|
|
92
|
+
const callbacks = this.listeners.get(change.table);
|
|
93
|
+
if (!callbacks) return;
|
|
94
|
+
const event = {
|
|
95
|
+
...change,
|
|
96
|
+
timestamp: change.timestamp ?? Date.now()
|
|
97
|
+
};
|
|
98
|
+
for (const callback of callbacks) {
|
|
99
|
+
callback(event);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
function removeListener(source, event, listener) {
|
|
104
|
+
if (source.off) {
|
|
105
|
+
source.off(event, listener);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
source.removeListener?.(event, listener);
|
|
109
|
+
}
|
|
110
|
+
function errorMessage(error) {
|
|
111
|
+
return error instanceof Error ? error.message : String(error);
|
|
112
|
+
}
|
|
113
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
114
|
+
0 && (module.exports = {
|
|
115
|
+
CassandraAdapter
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=cassandra.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/cassandra.ts","../../src/core/errors.ts","../../src/adapters/cassandra/cassandra-adapter.ts"],"sourcesContent":["export { CassandraAdapter } from './cassandra/cassandra-adapter.js'\nexport type { CassandraAdapterOptions } from './cassandra/types.js'\n","/**\n * Base error class for all RouteFlow errors.\n * Always use this instead of plain `Error` throughout the framework.\n */\nexport class ReactiveApiError extends Error {\n /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */\n readonly code: string\n\n constructor(code: string, message: string) {\n super(message)\n this.name = 'ReactiveApiError'\n this.code = code\n // Restore prototype chain (required when extending built-ins in TS)\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n","import type { ChangeEvent, DatabaseAdapter } from '../../core/types.js'\nimport { ReactiveApiError } from '../../core/errors.js'\nimport type {\n CassandraAdapterOptions,\n CassandraCdcEvent,\n CassandraCdcSource,\n} from './types.js'\n\nexport class CassandraAdapter implements DatabaseAdapter {\n private readonly source: CassandraCdcSource\n private readonly onError?: CassandraAdapterOptions['onError']\n private readonly listeners: Map<string, Set<(event: ChangeEvent) => void>> = new Map()\n private readonly handleChangeBound = (event: CassandraCdcEvent) => {\n this.handleChange(event)\n }\n private readonly handleErrorBound = (error: Error) => {\n this.onError?.(error)\n }\n private connected = false\n\n constructor(options: CassandraAdapterOptions) {\n this.source = options.source\n this.onError = options.onError\n }\n\n async connect(): Promise<void> {\n if (this.connected) return\n\n this.source.on('change', this.handleChangeBound)\n this.source.on('error', this.handleErrorBound)\n\n try {\n await this.source.start?.()\n } catch (error) {\n throw new ReactiveApiError(\n 'CASSANDRA_SOURCE_START_FAILED',\n `Failed to start Cassandra CDC source: ${errorMessage(error)}`,\n )\n }\n\n this.connected = true\n }\n\n async disconnect(): Promise<void> {\n if (!this.connected) return\n\n removeListener(this.source, 'change', this.handleChangeBound)\n removeListener(this.source, 'error', this.handleErrorBound)\n await this.source.stop?.()\n this.listeners.clear()\n this.connected = false\n }\n\n onChange(table: string, callback: (event: ChangeEvent) => void): () => void {\n if (!this.listeners.has(table)) {\n this.listeners.set(table, new Set())\n }\n\n this.listeners.get(table)!.add(callback)\n\n return () => {\n const callbacks = this.listeners.get(table)\n if (!callbacks) return\n\n callbacks.delete(callback)\n if (callbacks.size === 0) {\n this.listeners.delete(table)\n }\n }\n }\n\n private handleChange(change: CassandraCdcEvent): void {\n const callbacks = this.listeners.get(change.table)\n if (!callbacks) return\n\n const event: ChangeEvent = {\n ...change,\n timestamp: change.timestamp ?? Date.now(),\n }\n\n for (const callback of callbacks) {\n callback(event)\n }\n }\n}\n\nfunction removeListener(\n source: CassandraCdcSource,\n event: 'change' | 'error',\n listener: (...args: any[]) => void,\n): void {\n if (source.off) {\n source.off(event, listener)\n return\n }\n\n source.removeListener?.(event, listener)\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIO,IAAM,mBAAN,cAA+B,MAAM;AAAA;AAAA,EAEjC;AAAA,EAET,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAEZ,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AACF;;;ACPO,IAAM,mBAAN,MAAkD;AAAA,EACtC;AAAA,EACA;AAAA,EACA,YAA4D,oBAAI,IAAI;AAAA,EACpE,oBAAoB,CAAC,UAA6B;AACjE,SAAK,aAAa,KAAK;AAAA,EACzB;AAAA,EACiB,mBAAmB,CAAC,UAAiB;AACpD,SAAK,UAAU,KAAK;AAAA,EACtB;AAAA,EACQ,YAAY;AAAA,EAEpB,YAAY,SAAkC;AAC5C,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ;AAAA,EACzB;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,UAAW;AAEpB,SAAK,OAAO,GAAG,UAAU,KAAK,iBAAiB;AAC/C,SAAK,OAAO,GAAG,SAAS,KAAK,gBAAgB;AAE7C,QAAI;AACF,YAAM,KAAK,OAAO,QAAQ;AAAA,IAC5B,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR;AAAA,QACA,yCAAyC,aAAa,KAAK,CAAC;AAAA,MAC9D;AAAA,IACF;AAEA,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,aAA4B;AAChC,QAAI,CAAC,KAAK,UAAW;AAErB,mBAAe,KAAK,QAAQ,UAAU,KAAK,iBAAiB;AAC5D,mBAAe,KAAK,QAAQ,SAAS,KAAK,gBAAgB;AAC1D,UAAM,KAAK,OAAO,OAAO;AACzB,SAAK,UAAU,MAAM;AACrB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,SAAS,OAAe,UAAoD;AAC1E,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,oBAAI,IAAI,CAAC;AAAA,IACrC;AAEA,SAAK,UAAU,IAAI,KAAK,EAAG,IAAI,QAAQ;AAEvC,WAAO,MAAM;AACX,YAAM,YAAY,KAAK,UAAU,IAAI,KAAK;AAC1C,UAAI,CAAC,UAAW;AAEhB,gBAAU,OAAO,QAAQ;AACzB,UAAI,UAAU,SAAS,GAAG;AACxB,aAAK,UAAU,OAAO,KAAK;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,QAAiC;AACpD,UAAM,YAAY,KAAK,UAAU,IAAI,OAAO,KAAK;AACjD,QAAI,CAAC,UAAW;AAEhB,UAAM,QAAqB;AAAA,MACzB,GAAG;AAAA,MACH,WAAW,OAAO,aAAa,KAAK,IAAI;AAAA,IAC1C;AAEA,eAAW,YAAY,WAAW;AAChC,eAAS,KAAK;AAAA,IAChB;AAAA,EACF;AACF;AAEA,SAAS,eACP,QACA,OACA,UACM;AACN,MAAI,OAAO,KAAK;AACd,WAAO,IAAI,OAAO,QAAQ;AAC1B;AAAA,EACF;AAEA,SAAO,iBAAiB,OAAO,QAAQ;AACzC;AAEA,SAAS,aAAa,OAAwB;AAC5C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;","names":[]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { C as ChangeEvent, D as DatabaseAdapter } from '../types-tPDla8AE.cjs';
|
|
2
|
+
|
|
3
|
+
interface CassandraCdcEvent<T = unknown> {
|
|
4
|
+
table: string;
|
|
5
|
+
operation: ChangeEvent<T>['operation'];
|
|
6
|
+
newRow: T | null;
|
|
7
|
+
oldRow: T | null;
|
|
8
|
+
timestamp?: number;
|
|
9
|
+
}
|
|
10
|
+
interface CassandraCdcSource {
|
|
11
|
+
on(event: 'change', listener: (event: CassandraCdcEvent) => void): void;
|
|
12
|
+
on(event: 'error', listener: (error: Error) => void): void;
|
|
13
|
+
off?(event: 'change' | 'error', listener: (...args: any[]) => void): void;
|
|
14
|
+
removeListener?(event: 'change' | 'error', listener: (...args: any[]) => void): void;
|
|
15
|
+
start?(): Promise<void> | void;
|
|
16
|
+
stop?(): Promise<void> | void;
|
|
17
|
+
}
|
|
18
|
+
interface CassandraAdapterOptions {
|
|
19
|
+
source: CassandraCdcSource;
|
|
20
|
+
onError?: (error: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare class CassandraAdapter implements DatabaseAdapter {
|
|
24
|
+
private readonly source;
|
|
25
|
+
private readonly onError?;
|
|
26
|
+
private readonly listeners;
|
|
27
|
+
private readonly handleChangeBound;
|
|
28
|
+
private readonly handleErrorBound;
|
|
29
|
+
private connected;
|
|
30
|
+
constructor(options: CassandraAdapterOptions);
|
|
31
|
+
connect(): Promise<void>;
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
|
|
34
|
+
private handleChange;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { CassandraAdapter, type CassandraAdapterOptions };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { C as ChangeEvent, D as DatabaseAdapter } from '../types-tPDla8AE.js';
|
|
2
|
+
|
|
3
|
+
interface CassandraCdcEvent<T = unknown> {
|
|
4
|
+
table: string;
|
|
5
|
+
operation: ChangeEvent<T>['operation'];
|
|
6
|
+
newRow: T | null;
|
|
7
|
+
oldRow: T | null;
|
|
8
|
+
timestamp?: number;
|
|
9
|
+
}
|
|
10
|
+
interface CassandraCdcSource {
|
|
11
|
+
on(event: 'change', listener: (event: CassandraCdcEvent) => void): void;
|
|
12
|
+
on(event: 'error', listener: (error: Error) => void): void;
|
|
13
|
+
off?(event: 'change' | 'error', listener: (...args: any[]) => void): void;
|
|
14
|
+
removeListener?(event: 'change' | 'error', listener: (...args: any[]) => void): void;
|
|
15
|
+
start?(): Promise<void> | void;
|
|
16
|
+
stop?(): Promise<void> | void;
|
|
17
|
+
}
|
|
18
|
+
interface CassandraAdapterOptions {
|
|
19
|
+
source: CassandraCdcSource;
|
|
20
|
+
onError?: (error: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare class CassandraAdapter implements DatabaseAdapter {
|
|
24
|
+
private readonly source;
|
|
25
|
+
private readonly onError?;
|
|
26
|
+
private readonly listeners;
|
|
27
|
+
private readonly handleChangeBound;
|
|
28
|
+
private readonly handleErrorBound;
|
|
29
|
+
private connected;
|
|
30
|
+
constructor(options: CassandraAdapterOptions);
|
|
31
|
+
connect(): Promise<void>;
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
|
|
34
|
+
private handleChange;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { CassandraAdapter, type CassandraAdapterOptions };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var ReactiveApiError = class extends Error {
|
|
3
|
+
/** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ReactiveApiError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/adapters/cassandra/cassandra-adapter.ts
|
|
14
|
+
var CassandraAdapter = class {
|
|
15
|
+
source;
|
|
16
|
+
onError;
|
|
17
|
+
listeners = /* @__PURE__ */ new Map();
|
|
18
|
+
handleChangeBound = (event) => {
|
|
19
|
+
this.handleChange(event);
|
|
20
|
+
};
|
|
21
|
+
handleErrorBound = (error) => {
|
|
22
|
+
this.onError?.(error);
|
|
23
|
+
};
|
|
24
|
+
connected = false;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.source = options.source;
|
|
27
|
+
this.onError = options.onError;
|
|
28
|
+
}
|
|
29
|
+
async connect() {
|
|
30
|
+
if (this.connected) return;
|
|
31
|
+
this.source.on("change", this.handleChangeBound);
|
|
32
|
+
this.source.on("error", this.handleErrorBound);
|
|
33
|
+
try {
|
|
34
|
+
await this.source.start?.();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new ReactiveApiError(
|
|
37
|
+
"CASSANDRA_SOURCE_START_FAILED",
|
|
38
|
+
`Failed to start Cassandra CDC source: ${errorMessage(error)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
this.connected = true;
|
|
42
|
+
}
|
|
43
|
+
async disconnect() {
|
|
44
|
+
if (!this.connected) return;
|
|
45
|
+
removeListener(this.source, "change", this.handleChangeBound);
|
|
46
|
+
removeListener(this.source, "error", this.handleErrorBound);
|
|
47
|
+
await this.source.stop?.();
|
|
48
|
+
this.listeners.clear();
|
|
49
|
+
this.connected = false;
|
|
50
|
+
}
|
|
51
|
+
onChange(table, callback) {
|
|
52
|
+
if (!this.listeners.has(table)) {
|
|
53
|
+
this.listeners.set(table, /* @__PURE__ */ new Set());
|
|
54
|
+
}
|
|
55
|
+
this.listeners.get(table).add(callback);
|
|
56
|
+
return () => {
|
|
57
|
+
const callbacks = this.listeners.get(table);
|
|
58
|
+
if (!callbacks) return;
|
|
59
|
+
callbacks.delete(callback);
|
|
60
|
+
if (callbacks.size === 0) {
|
|
61
|
+
this.listeners.delete(table);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
handleChange(change) {
|
|
66
|
+
const callbacks = this.listeners.get(change.table);
|
|
67
|
+
if (!callbacks) return;
|
|
68
|
+
const event = {
|
|
69
|
+
...change,
|
|
70
|
+
timestamp: change.timestamp ?? Date.now()
|
|
71
|
+
};
|
|
72
|
+
for (const callback of callbacks) {
|
|
73
|
+
callback(event);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
function removeListener(source, event, listener) {
|
|
78
|
+
if (source.off) {
|
|
79
|
+
source.off(event, listener);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
source.removeListener?.(event, listener);
|
|
83
|
+
}
|
|
84
|
+
function errorMessage(error) {
|
|
85
|
+
return error instanceof Error ? error.message : String(error);
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
CassandraAdapter
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=cassandra.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/errors.ts","../../src/adapters/cassandra/cassandra-adapter.ts"],"sourcesContent":["/**\n * Base error class for all RouteFlow errors.\n * Always use this instead of plain `Error` throughout the framework.\n */\nexport class ReactiveApiError extends Error {\n /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */\n readonly code: string\n\n constructor(code: string, message: string) {\n super(message)\n this.name = 'ReactiveApiError'\n this.code = code\n // Restore prototype chain (required when extending built-ins in TS)\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n","import type { ChangeEvent, DatabaseAdapter } from '../../core/types.js'\nimport { ReactiveApiError } from '../../core/errors.js'\nimport type {\n CassandraAdapterOptions,\n CassandraCdcEvent,\n CassandraCdcSource,\n} from './types.js'\n\nexport class CassandraAdapter implements DatabaseAdapter {\n private readonly source: CassandraCdcSource\n private readonly onError?: CassandraAdapterOptions['onError']\n private readonly listeners: Map<string, Set<(event: ChangeEvent) => void>> = new Map()\n private readonly handleChangeBound = (event: CassandraCdcEvent) => {\n this.handleChange(event)\n }\n private readonly handleErrorBound = (error: Error) => {\n this.onError?.(error)\n }\n private connected = false\n\n constructor(options: CassandraAdapterOptions) {\n this.source = options.source\n this.onError = options.onError\n }\n\n async connect(): Promise<void> {\n if (this.connected) return\n\n this.source.on('change', this.handleChangeBound)\n this.source.on('error', this.handleErrorBound)\n\n try {\n await this.source.start?.()\n } catch (error) {\n throw new ReactiveApiError(\n 'CASSANDRA_SOURCE_START_FAILED',\n `Failed to start Cassandra CDC source: ${errorMessage(error)}`,\n )\n }\n\n this.connected = true\n }\n\n async disconnect(): Promise<void> {\n if (!this.connected) return\n\n removeListener(this.source, 'change', this.handleChangeBound)\n removeListener(this.source, 'error', this.handleErrorBound)\n await this.source.stop?.()\n this.listeners.clear()\n this.connected = false\n }\n\n onChange(table: string, callback: (event: ChangeEvent) => void): () => void {\n if (!this.listeners.has(table)) {\n this.listeners.set(table, new Set())\n }\n\n this.listeners.get(table)!.add(callback)\n\n return () => {\n const callbacks = this.listeners.get(table)\n if (!callbacks) return\n\n callbacks.delete(callback)\n if (callbacks.size === 0) {\n this.listeners.delete(table)\n }\n }\n }\n\n private handleChange(change: CassandraCdcEvent): void {\n const callbacks = this.listeners.get(change.table)\n if (!callbacks) return\n\n const event: ChangeEvent = {\n ...change,\n timestamp: change.timestamp ?? Date.now(),\n }\n\n for (const callback of callbacks) {\n callback(event)\n }\n }\n}\n\nfunction removeListener(\n source: CassandraCdcSource,\n event: 'change' | 'error',\n listener: (...args: any[]) => void,\n): void {\n if (source.off) {\n source.off(event, listener)\n return\n }\n\n source.removeListener?.(event, listener)\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n"],"mappings":";AAIO,IAAM,mBAAN,cAA+B,MAAM;AAAA;AAAA,EAEjC;AAAA,EAET,YAAY,MAAc,SAAiB;AACzC,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAEZ,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AACF;;;ACPO,IAAM,mBAAN,MAAkD;AAAA,EACtC;AAAA,EACA;AAAA,EACA,YAA4D,oBAAI,IAAI;AAAA,EACpE,oBAAoB,CAAC,UAA6B;AACjE,SAAK,aAAa,KAAK;AAAA,EACzB;AAAA,EACiB,mBAAmB,CAAC,UAAiB;AACpD,SAAK,UAAU,KAAK;AAAA,EACtB;AAAA,EACQ,YAAY;AAAA,EAEpB,YAAY,SAAkC;AAC5C,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ;AAAA,EACzB;AAAA,EAEA,MAAM,UAAyB;AAC7B,QAAI,KAAK,UAAW;AAEpB,SAAK,OAAO,GAAG,UAAU,KAAK,iBAAiB;AAC/C,SAAK,OAAO,GAAG,SAAS,KAAK,gBAAgB;AAE7C,QAAI;AACF,YAAM,KAAK,OAAO,QAAQ;AAAA,IAC5B,SAAS,OAAO;AACd,YAAM,IAAI;AAAA,QACR;AAAA,QACA,yCAAyC,aAAa,KAAK,CAAC;AAAA,MAC9D;AAAA,IACF;AAEA,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,aAA4B;AAChC,QAAI,CAAC,KAAK,UAAW;AAErB,mBAAe,KAAK,QAAQ,UAAU,KAAK,iBAAiB;AAC5D,mBAAe,KAAK,QAAQ,SAAS,KAAK,gBAAgB;AAC1D,UAAM,KAAK,OAAO,OAAO;AACzB,SAAK,UAAU,MAAM;AACrB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,SAAS,OAAe,UAAoD;AAC1E,QAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9B,WAAK,UAAU,IAAI,OAAO,oBAAI,IAAI,CAAC;AAAA,IACrC;AAEA,SAAK,UAAU,IAAI,KAAK,EAAG,IAAI,QAAQ;AAEvC,WAAO,MAAM;AACX,YAAM,YAAY,KAAK,UAAU,IAAI,KAAK;AAC1C,UAAI,CAAC,UAAW;AAEhB,gBAAU,OAAO,QAAQ;AACzB,UAAI,UAAU,SAAS,GAAG;AACxB,aAAK,UAAU,OAAO,KAAK;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,QAAiC;AACpD,UAAM,YAAY,KAAK,UAAU,IAAI,OAAO,KAAK;AACjD,QAAI,CAAC,UAAW;AAEhB,UAAM,QAAqB;AAAA,MACzB,GAAG;AAAA,MACH,WAAW,OAAO,aAAa,KAAK,IAAI;AAAA,IAC1C;AAEA,eAAW,YAAY,WAAW;AAChC,eAAS,KAAK;AAAA,IAChB;AAAA,EACF;AACF;AAEA,SAAS,eACP,QACA,OACA,UACM;AACN,MAAI,OAAO,KAAK;AACd,WAAO,IAAI,OAAO,QAAQ;AAC1B;AAAA,EACF;AAEA,SAAO,iBAAiB,OAAO,QAAQ;AACzC;AAEA,SAAS,aAAa,OAAwB;AAC5C,SAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;","names":[]}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/adapters/dynamodb.ts
|
|
21
|
+
var dynamodb_exports = {};
|
|
22
|
+
__export(dynamodb_exports, {
|
|
23
|
+
DynamoDbAdapter: () => DynamoDbAdapter
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(dynamodb_exports);
|
|
26
|
+
|
|
27
|
+
// src/core/errors.ts
|
|
28
|
+
var ReactiveApiError = class extends Error {
|
|
29
|
+
/** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */
|
|
30
|
+
code;
|
|
31
|
+
constructor(code, message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "ReactiveApiError";
|
|
34
|
+
this.code = code;
|
|
35
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/adapters/dynamodb/dynamodb-adapter.ts
|
|
40
|
+
var DynamoDbAdapter = class {
|
|
41
|
+
source;
|
|
42
|
+
unmarshall;
|
|
43
|
+
onError;
|
|
44
|
+
listeners = /* @__PURE__ */ new Map();
|
|
45
|
+
handleRecordBound = (record) => {
|
|
46
|
+
this.handleRecord(record);
|
|
47
|
+
};
|
|
48
|
+
handleErrorBound = (error) => {
|
|
49
|
+
this.onError?.(error);
|
|
50
|
+
};
|
|
51
|
+
connected = false;
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this.source = options.source;
|
|
54
|
+
this.unmarshall = options.unmarshall ?? defaultUnmarshall;
|
|
55
|
+
this.onError = options.onError;
|
|
56
|
+
}
|
|
57
|
+
async connect() {
|
|
58
|
+
if (this.connected) return;
|
|
59
|
+
this.source.on("record", this.handleRecordBound);
|
|
60
|
+
this.source.on("error", this.handleErrorBound);
|
|
61
|
+
try {
|
|
62
|
+
await this.source.start?.();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new ReactiveApiError(
|
|
65
|
+
"DYNAMODB_STREAM_START_FAILED",
|
|
66
|
+
`Failed to start DynamoDB stream source: ${errorMessage(error)}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
this.connected = true;
|
|
70
|
+
}
|
|
71
|
+
async disconnect() {
|
|
72
|
+
if (!this.connected) return;
|
|
73
|
+
removeListener(this.source, "record", this.handleRecordBound);
|
|
74
|
+
removeListener(this.source, "error", this.handleErrorBound);
|
|
75
|
+
await this.source.stop?.();
|
|
76
|
+
this.listeners.clear();
|
|
77
|
+
this.connected = false;
|
|
78
|
+
}
|
|
79
|
+
onChange(table, callback) {
|
|
80
|
+
if (!this.listeners.has(table)) {
|
|
81
|
+
this.listeners.set(table, /* @__PURE__ */ new Set());
|
|
82
|
+
}
|
|
83
|
+
this.listeners.get(table).add(callback);
|
|
84
|
+
return () => {
|
|
85
|
+
const callbacks = this.listeners.get(table);
|
|
86
|
+
if (!callbacks) return;
|
|
87
|
+
callbacks.delete(callback);
|
|
88
|
+
if (callbacks.size === 0) {
|
|
89
|
+
this.listeners.delete(table);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
handleRecord(record) {
|
|
94
|
+
const normalised = normaliseRecord(record, this.unmarshall);
|
|
95
|
+
if (!normalised) return;
|
|
96
|
+
const callbacks = this.listeners.get(normalised.table);
|
|
97
|
+
if (!callbacks) return;
|
|
98
|
+
const event = {
|
|
99
|
+
...normalised,
|
|
100
|
+
timestamp: Date.now()
|
|
101
|
+
};
|
|
102
|
+
for (const callback of callbacks) {
|
|
103
|
+
callback(event);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function normaliseRecord(record, unmarshall) {
|
|
108
|
+
const table = extractTableName(record.eventSourceARN);
|
|
109
|
+
if (!table || !record.eventName) return null;
|
|
110
|
+
if (record.eventName === "INSERT") {
|
|
111
|
+
return {
|
|
112
|
+
table,
|
|
113
|
+
operation: "INSERT",
|
|
114
|
+
newRow: unmarshall(record.dynamodb?.NewImage),
|
|
115
|
+
oldRow: null
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (record.eventName === "MODIFY") {
|
|
119
|
+
return {
|
|
120
|
+
table,
|
|
121
|
+
operation: "UPDATE",
|
|
122
|
+
newRow: unmarshall(record.dynamodb?.NewImage),
|
|
123
|
+
oldRow: unmarshall(record.dynamodb?.OldImage)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (record.eventName === "REMOVE") {
|
|
127
|
+
return {
|
|
128
|
+
table,
|
|
129
|
+
operation: "DELETE",
|
|
130
|
+
newRow: null,
|
|
131
|
+
oldRow: unmarshall(record.dynamodb?.OldImage)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function extractTableName(arn) {
|
|
137
|
+
if (!arn) return null;
|
|
138
|
+
const match = arn.match(/table\/([^/]+)/);
|
|
139
|
+
return match?.[1] ?? null;
|
|
140
|
+
}
|
|
141
|
+
function defaultUnmarshall(image) {
|
|
142
|
+
if (!image) return null;
|
|
143
|
+
const result = {};
|
|
144
|
+
for (const [key, value] of Object.entries(image)) {
|
|
145
|
+
result[key] = decodeAttributeValue(value);
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
function decodeAttributeValue(value) {
|
|
150
|
+
if (typeof value !== "object" || value === null) return value;
|
|
151
|
+
const record = value;
|
|
152
|
+
if ("S" in record) return record["S"];
|
|
153
|
+
if ("N" in record) return Number(record["N"]);
|
|
154
|
+
if ("BOOL" in record) return record["BOOL"];
|
|
155
|
+
if ("NULL" in record) return null;
|
|
156
|
+
if ("M" in record && typeof record["M"] === "object" && record["M"] !== null) {
|
|
157
|
+
return defaultUnmarshall(record["M"]);
|
|
158
|
+
}
|
|
159
|
+
if ("L" in record && Array.isArray(record["L"])) {
|
|
160
|
+
return record["L"].map((item) => decodeAttributeValue(item));
|
|
161
|
+
}
|
|
162
|
+
if ("SS" in record) return record["SS"];
|
|
163
|
+
if ("NS" in record) return Array.isArray(record["NS"]) ? record["NS"].map(Number) : record["NS"];
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
function removeListener(source, event, listener) {
|
|
167
|
+
if (source.off) {
|
|
168
|
+
source.off(event, listener);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
source.removeListener?.(event, listener);
|
|
172
|
+
}
|
|
173
|
+
function errorMessage(error) {
|
|
174
|
+
return error instanceof Error ? error.message : String(error);
|
|
175
|
+
}
|
|
176
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
177
|
+
0 && (module.exports = {
|
|
178
|
+
DynamoDbAdapter
|
|
179
|
+
});
|
|
180
|
+
//# sourceMappingURL=dynamodb.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/dynamodb.ts","../../src/core/errors.ts","../../src/adapters/dynamodb/dynamodb-adapter.ts"],"sourcesContent":["export { DynamoDbAdapter } from './dynamodb/dynamodb-adapter.js'\nexport type { DynamoDbAdapterOptions } from './dynamodb/types.js'\n","/**\n * Base error class for all RouteFlow errors.\n * Always use this instead of plain `Error` throughout the framework.\n */\nexport class ReactiveApiError extends Error {\n /** Machine-readable error code (e.g. 'ADAPTER_NOT_CONNECTED', 'INVALID_ROUTE') */\n readonly code: string\n\n constructor(code: string, message: string) {\n super(message)\n this.name = 'ReactiveApiError'\n this.code = code\n // Restore prototype chain (required when extending built-ins in TS)\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n","import type { ChangeEvent, DatabaseAdapter } from '../../core/types.js'\nimport { ReactiveApiError } from '../../core/errors.js'\nimport type {\n DynamoDbAdapterOptions,\n DynamoDbAttributeMap,\n DynamoDbStreamRecord,\n DynamoDbStreamSource,\n NormalisedDynamoDbRecord,\n} from './types.js'\n\n/**\n * DynamoDB adapter for RouteFlow.\n *\n * Consumes DynamoDB Streams-style records from an external source.\n */\nexport class DynamoDbAdapter implements DatabaseAdapter {\n private readonly source: DynamoDbStreamSource\n private readonly unmarshall: NonNullable<DynamoDbAdapterOptions['unmarshall']>\n private readonly onError?: DynamoDbAdapterOptions['onError']\n private readonly listeners: Map<string, Set<(event: ChangeEvent) => void>> = new Map()\n private readonly handleRecordBound = (record: DynamoDbStreamRecord) => {\n this.handleRecord(record)\n }\n private readonly handleErrorBound = (error: Error) => {\n this.onError?.(error)\n }\n private connected = false\n\n constructor(options: DynamoDbAdapterOptions) {\n this.source = options.source\n this.unmarshall = options.unmarshall ?? defaultUnmarshall\n this.onError = options.onError\n }\n\n async connect(): Promise<void> {\n if (this.connected) return\n\n this.source.on('record', this.handleRecordBound)\n this.source.on('error', this.handleErrorBound)\n\n try {\n await this.source.start?.()\n } catch (error) {\n throw new ReactiveApiError(\n 'DYNAMODB_STREAM_START_FAILED',\n `Failed to start DynamoDB stream source: ${errorMessage(error)}`,\n )\n }\n\n this.connected = true\n }\n\n async disconnect(): Promise<void> {\n if (!this.connected) return\n\n removeListener(this.source, 'record', this.handleRecordBound)\n removeListener(this.source, 'error', this.handleErrorBound)\n await this.source.stop?.()\n this.listeners.clear()\n this.connected = false\n }\n\n onChange(table: string, callback: (event: ChangeEvent) => void): () => void {\n if (!this.listeners.has(table)) {\n this.listeners.set(table, new Set())\n }\n\n this.listeners.get(table)!.add(callback)\n\n return () => {\n const callbacks = this.listeners.get(table)\n if (!callbacks) return\n\n callbacks.delete(callback)\n if (callbacks.size === 0) {\n this.listeners.delete(table)\n }\n }\n }\n\n private handleRecord(record: DynamoDbStreamRecord): void {\n const normalised = normaliseRecord(record, this.unmarshall)\n if (!normalised) return\n\n const callbacks = this.listeners.get(normalised.table)\n if (!callbacks) return\n\n const event: ChangeEvent = {\n ...normalised,\n timestamp: Date.now(),\n }\n\n for (const callback of callbacks) {\n callback(event)\n }\n }\n}\n\nfunction normaliseRecord(\n record: DynamoDbStreamRecord,\n unmarshall: (image: DynamoDbAttributeMap | undefined) => Record<string, unknown> | null,\n): NormalisedDynamoDbRecord | null {\n const table = extractTableName(record.eventSourceARN)\n if (!table || !record.eventName) return null\n\n if (record.eventName === 'INSERT') {\n return {\n table,\n operation: 'INSERT',\n newRow: unmarshall(record.dynamodb?.NewImage),\n oldRow: null,\n }\n }\n\n if (record.eventName === 'MODIFY') {\n return {\n table,\n operation: 'UPDATE',\n newRow: unmarshall(record.dynamodb?.NewImage),\n oldRow: unmarshall(record.dynamodb?.OldImage),\n }\n }\n\n if (record.eventName === 'REMOVE') {\n return {\n table,\n operation: 'DELETE',\n newRow: null,\n oldRow: unmarshall(record.dynamodb?.OldImage),\n }\n }\n\n return null\n}\n\nfunction extractTableName(arn: string | undefined): string | null {\n if (!arn) return null\n const match = arn.match(/table\\/([^/]+)/)\n return match?.[1] ?? null\n}\n\nfunction defaultUnmarshall(\n image: DynamoDbAttributeMap | undefined,\n): Record<string, unknown> | null {\n if (!image) return null\n\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(image)) {\n result[key] = decodeAttributeValue(value)\n }\n return result\n}\n\nfunction decodeAttributeValue(value: unknown): unknown {\n if (typeof value !== 'object' || value === null) return value\n\n const record = value as Record<string, unknown>\n if ('S' in record) return record['S']\n if ('N' in record) return Number(record['N'])\n if ('BOOL' in record) return record['BOOL']\n if ('NULL' in record) return null\n if ('M' in record && typeof record['M'] === 'object' && record['M'] !== null) {\n return defaultUnmarshall(record['M'] as DynamoDbAttributeMap)\n }\n if ('L' in record && Array.isArray(record['L'])) {\n return record['L'].map((item) => decodeAttributeValue(item))\n }\n if ('SS' in record) return record['SS']\n if ('NS' in record) return Array.isArray(record['NS']) ? record['NS'].map(Number) : record['NS']\n\n return value\n}\n\nfunction removeListener(\n source: DynamoDbStreamSource,\n event: 'record' | 'error',\n listener: (...args: any[]) => void,\n): void {\n if (source.off) {\n source.off(event, listener)\n return\n }\n\n source.removeListener?.(event, listener)\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { D as DatabaseAdapter, C as ChangeEvent } from '../types-tPDla8AE.cjs';
|
|
2
|
+
|
|
3
|
+
interface DynamoDbAttributeMap {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface DynamoDbStreamRecord {
|
|
7
|
+
eventName?: 'INSERT' | 'MODIFY' | 'REMOVE';
|
|
8
|
+
dynamodb?: {
|
|
9
|
+
NewImage?: DynamoDbAttributeMap;
|
|
10
|
+
OldImage?: DynamoDbAttributeMap;
|
|
11
|
+
};
|
|
12
|
+
eventSourceARN?: string;
|
|
13
|
+
}
|
|
14
|
+
interface DynamoDbStreamSource {
|
|
15
|
+
on(event: 'record', listener: (record: DynamoDbStreamRecord) => void): void;
|
|
16
|
+
on(event: 'error', listener: (error: Error) => void): void;
|
|
17
|
+
off?(event: 'record' | 'error', listener: (...args: any[]) => void): void;
|
|
18
|
+
removeListener?(event: 'record' | 'error', listener: (...args: any[]) => void): void;
|
|
19
|
+
start?(): Promise<void> | void;
|
|
20
|
+
stop?(): Promise<void> | void;
|
|
21
|
+
}
|
|
22
|
+
interface DynamoDbAdapterOptions {
|
|
23
|
+
source: DynamoDbStreamSource;
|
|
24
|
+
unmarshall?: (image: DynamoDbAttributeMap | undefined) => Record<string, unknown> | null;
|
|
25
|
+
onError?: (error: unknown) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* DynamoDB adapter for RouteFlow.
|
|
30
|
+
*
|
|
31
|
+
* Consumes DynamoDB Streams-style records from an external source.
|
|
32
|
+
*/
|
|
33
|
+
declare class DynamoDbAdapter implements DatabaseAdapter {
|
|
34
|
+
private readonly source;
|
|
35
|
+
private readonly unmarshall;
|
|
36
|
+
private readonly onError?;
|
|
37
|
+
private readonly listeners;
|
|
38
|
+
private readonly handleRecordBound;
|
|
39
|
+
private readonly handleErrorBound;
|
|
40
|
+
private connected;
|
|
41
|
+
constructor(options: DynamoDbAdapterOptions);
|
|
42
|
+
connect(): Promise<void>;
|
|
43
|
+
disconnect(): Promise<void>;
|
|
44
|
+
onChange(table: string, callback: (event: ChangeEvent) => void): () => void;
|
|
45
|
+
private handleRecord;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { DynamoDbAdapter, type DynamoDbAdapterOptions };
|