@cortexa/core 0.7.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.
Files changed (39) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +423 -0
  3. package/dist/chunk-6VDDIS65.js +21 -0
  4. package/dist/chunk-6VDDIS65.js.map +1 -0
  5. package/dist/chunk-T4NUBJ7T.js +13 -0
  6. package/dist/chunk-T4NUBJ7T.js.map +1 -0
  7. package/dist/cli/chunk-7HYSAZX4.js +15 -0
  8. package/dist/cli/chunk-7HYSAZX4.js.map +1 -0
  9. package/dist/cli/chunk-HVMZ6P54.js +23 -0
  10. package/dist/cli/chunk-HVMZ6P54.js.map +1 -0
  11. package/dist/cli/index.js +5921 -0
  12. package/dist/cli/index.js.map +1 -0
  13. package/dist/cli/mongodb-WTT5HVQH.js +145 -0
  14. package/dist/cli/mongodb-WTT5HVQH.js.map +1 -0
  15. package/dist/cli/mongodb-stream-Q5OBFWZP.js +93 -0
  16. package/dist/cli/mongodb-stream-Q5OBFWZP.js.map +1 -0
  17. package/dist/cli/mssql-KG7P3R2I.js +79 -0
  18. package/dist/cli/mssql-KG7P3R2I.js.map +1 -0
  19. package/dist/cli/mysql-stream-Z2MZS2KF.js +195 -0
  20. package/dist/cli/mysql-stream-Z2MZS2KF.js.map +1 -0
  21. package/dist/cli/postgres-stream-S4CRICEA.js +237 -0
  22. package/dist/cli/postgres-stream-S4CRICEA.js.map +1 -0
  23. package/dist/index.cjs +5733 -0
  24. package/dist/index.cjs.map +1 -0
  25. package/dist/index.d.cts +670 -0
  26. package/dist/index.d.ts +670 -0
  27. package/dist/index.js +4805 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mongodb-M5UGJZTC.js +143 -0
  30. package/dist/mongodb-M5UGJZTC.js.map +1 -0
  31. package/dist/mongodb-stream-Q23UHLTM.js +92 -0
  32. package/dist/mongodb-stream-Q23UHLTM.js.map +1 -0
  33. package/dist/mssql-I27XEHQ2.js +77 -0
  34. package/dist/mssql-I27XEHQ2.js.map +1 -0
  35. package/dist/mysql-stream-YCNLPPPG.js +194 -0
  36. package/dist/mysql-stream-YCNLPPPG.js.map +1 -0
  37. package/dist/postgres-stream-A7EVYUX2.js +236 -0
  38. package/dist/postgres-stream-A7EVYUX2.js.map +1 -0
  39. package/package.json +105 -0
@@ -0,0 +1,143 @@
1
+ // src/connectors/mongodb.ts
2
+ import { MongoClient } from "mongodb";
3
+ var MongoDBConnector = class {
4
+ client = null;
5
+ db = null;
6
+ dbName = "";
7
+ async connect(config) {
8
+ const url = config.url ?? `mongodb://${config.host ?? "localhost"}:${config.port ?? 27017}`;
9
+ this.dbName = config.database ?? "test";
10
+ this.client = new MongoClient(url);
11
+ await this.client.connect();
12
+ this.db = this.client.db(this.dbName);
13
+ }
14
+ async disconnect() {
15
+ if (this.client) {
16
+ await this.client.close();
17
+ this.client = null;
18
+ this.db = null;
19
+ }
20
+ }
21
+ async getTableCount() {
22
+ this.ensureConnected();
23
+ const collections = await this.db.listCollections().toArray();
24
+ return collections.length;
25
+ }
26
+ async getDatabaseName() {
27
+ return this.dbName;
28
+ }
29
+ async isReadOnly() {
30
+ return true;
31
+ }
32
+ isConnected() {
33
+ return this.client !== null && this.db !== null;
34
+ }
35
+ /**
36
+ * Bridge between SQL-shaped interface and MongoDB's native API.
37
+ * Uses a command protocol for MongoDB operations:
38
+ * - "COLLECTIONS" → listCollections
39
+ * - "SAMPLE:collection:N" → aggregate $sample
40
+ * - "COUNT:collection" → countDocuments
41
+ * - "INDEXES:collection" → collection.indexes()
42
+ * - "FIELDS:collection:N" → sample N docs and union field names
43
+ * - "COMMAND:{...}" → run admin command (e.g. replSetGetStatus)
44
+ */
45
+ async query(sql, _params) {
46
+ this.ensureConnected();
47
+ const trimmed = sql.trim();
48
+ if (trimmed === "COLLECTIONS") {
49
+ const collections = await this.db.listCollections().toArray();
50
+ const rows = collections.map((c) => ({
51
+ table_name: c.name,
52
+ table_schema: this.dbName
53
+ }));
54
+ return { rows, rowCount: rows.length };
55
+ }
56
+ if (trimmed.startsWith("SAMPLE:")) {
57
+ const parts = trimmed.split(":");
58
+ const collectionName = parts[1];
59
+ const limit = parseInt(parts[2] ?? "5", 10);
60
+ const docs = await this.db.collection(collectionName).aggregate([{ $sample: { size: limit } }]).toArray();
61
+ const rows = docs.map((d) => this.flattenDocument(d));
62
+ return { rows, rowCount: rows.length };
63
+ }
64
+ if (trimmed.startsWith("COUNT:")) {
65
+ const collectionName = trimmed.slice(6);
66
+ const count = await this.db.collection(collectionName).countDocuments();
67
+ return { rows: [{ count }], rowCount: 1 };
68
+ }
69
+ if (trimmed.startsWith("INDEXES:")) {
70
+ const collectionName = trimmed.slice(8);
71
+ const indexes = await this.db.collection(collectionName).indexes();
72
+ const rows = indexes.map((idx) => ({
73
+ index_name: idx.name ?? "unknown",
74
+ columns: Object.keys(idx.key),
75
+ is_unique: idx.unique ?? false
76
+ }));
77
+ return { rows, rowCount: rows.length };
78
+ }
79
+ if (trimmed.startsWith("FIELDS:")) {
80
+ const parts = trimmed.split(":");
81
+ const collectionName = parts[1];
82
+ const sampleSize = parseInt(parts[2] ?? "20", 10);
83
+ const docs = await this.db.collection(collectionName).aggregate([{ $sample: { size: sampleSize } }]).toArray();
84
+ const fieldMap = /* @__PURE__ */ new Map();
85
+ for (const doc of docs) {
86
+ this.extractFields(doc, "", fieldMap);
87
+ }
88
+ const rows = Array.from(fieldMap.entries()).map(([name, type]) => ({
89
+ column_name: name,
90
+ data_type: type,
91
+ is_nullable: "YES",
92
+ column_default: null
93
+ }));
94
+ return { rows, rowCount: rows.length };
95
+ }
96
+ if (trimmed.startsWith("COMMAND:")) {
97
+ const cmdJson = trimmed.slice(8);
98
+ const cmd = JSON.parse(cmdJson);
99
+ const result = await this.db.admin().command(cmd);
100
+ return { rows: [result], rowCount: 1 };
101
+ }
102
+ throw new Error(`MongoDB connector does not support raw SQL. Use command protocol (COLLECTIONS, SAMPLE:name:N, COUNT:name, INDEXES:name, FIELDS:name:N, COMMAND:{...}).`);
103
+ }
104
+ ensureConnected() {
105
+ if (!this.db) {
106
+ throw new Error("Not connected. Call connect() first.");
107
+ }
108
+ }
109
+ flattenDocument(doc) {
110
+ const result = {};
111
+ for (const [key, value] of Object.entries(doc)) {
112
+ if (key === "_id") {
113
+ result[key] = String(value);
114
+ } else {
115
+ result[key] = value;
116
+ }
117
+ }
118
+ return result;
119
+ }
120
+ extractFields(doc, prefix, fieldMap) {
121
+ for (const [key, value] of Object.entries(doc)) {
122
+ const fullKey = prefix ? `${prefix}.${key}` : key;
123
+ const bsonType = this.inferBsonType(value);
124
+ if (!fieldMap.has(fullKey)) {
125
+ fieldMap.set(fullKey, bsonType);
126
+ }
127
+ }
128
+ }
129
+ inferBsonType(value) {
130
+ if (value === null || value === void 0) return "null";
131
+ if (typeof value === "string") return "string";
132
+ if (typeof value === "number") return Number.isInteger(value) ? "int" : "double";
133
+ if (typeof value === "boolean") return "bool";
134
+ if (value instanceof Date) return "date";
135
+ if (Array.isArray(value)) return "array";
136
+ if (typeof value === "object") return "object";
137
+ return "unknown";
138
+ }
139
+ };
140
+ export {
141
+ MongoDBConnector
142
+ };
143
+ //# sourceMappingURL=mongodb-M5UGJZTC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connectors/mongodb.ts"],"sourcesContent":["import { MongoClient, type Db, type Document } from 'mongodb';\nimport type { ConnectionConfig } from '../core/config.js';\nimport type { DatabaseConnector, QueryResult } from './types.js';\n\nexport class MongoDBConnector implements DatabaseConnector {\n private client: MongoClient | null = null;\n private db: Db | null = null;\n private dbName: string = '';\n\n async connect(config: ConnectionConfig): Promise<void> {\n const url = config.url ?? `mongodb://${config.host ?? 'localhost'}:${config.port ?? 27017}`;\n this.dbName = config.database ?? 'test';\n this.client = new MongoClient(url);\n await this.client.connect();\n this.db = this.client.db(this.dbName);\n }\n\n async disconnect(): Promise<void> {\n if (this.client) {\n await this.client.close();\n this.client = null;\n this.db = null;\n }\n }\n\n async getTableCount(): Promise<number> {\n this.ensureConnected();\n const collections = await this.db!.listCollections().toArray();\n return collections.length;\n }\n\n async getDatabaseName(): Promise<string> {\n return this.dbName;\n }\n\n async isReadOnly(): Promise<boolean> {\n // Cortexa never writes to the target database\n return true;\n }\n\n isConnected(): boolean {\n return this.client !== null && this.db !== null;\n }\n\n /**\n * Bridge between SQL-shaped interface and MongoDB's native API.\n * Uses a command protocol for MongoDB operations:\n * - \"COLLECTIONS\" → listCollections\n * - \"SAMPLE:collection:N\" → aggregate $sample\n * - \"COUNT:collection\" → countDocuments\n * - \"INDEXES:collection\" → collection.indexes()\n * - \"FIELDS:collection:N\" → sample N docs and union field names\n * - \"COMMAND:{...}\" → run admin command (e.g. replSetGetStatus)\n */\n async query(sql: string, _params?: unknown[]): Promise<QueryResult> {\n this.ensureConnected();\n\n const trimmed = sql.trim();\n\n if (trimmed === 'COLLECTIONS') {\n const collections = await this.db!.listCollections().toArray();\n const rows = collections.map((c) => ({\n table_name: c.name,\n table_schema: this.dbName,\n }));\n return { rows, rowCount: rows.length };\n }\n\n if (trimmed.startsWith('SAMPLE:')) {\n const parts = trimmed.split(':');\n const collectionName = parts[1];\n const limit = parseInt(parts[2] ?? '5', 10);\n const docs = await this.db!.collection(collectionName)\n .aggregate<Document>([{ $sample: { size: limit } }])\n .toArray();\n const rows = docs.map((d) => this.flattenDocument(d));\n return { rows, rowCount: rows.length };\n }\n\n if (trimmed.startsWith('COUNT:')) {\n const collectionName = trimmed.slice(6);\n const count = await this.db!.collection(collectionName).countDocuments();\n return { rows: [{ count }], rowCount: 1 };\n }\n\n if (trimmed.startsWith('INDEXES:')) {\n const collectionName = trimmed.slice(8);\n const indexes = await this.db!.collection(collectionName).indexes();\n const rows = indexes.map((idx) => ({\n index_name: idx.name ?? 'unknown',\n columns: Object.keys(idx.key),\n is_unique: idx.unique ?? false,\n }));\n return { rows: rows as unknown as Record<string, unknown>[], rowCount: rows.length };\n }\n\n if (trimmed.startsWith('FIELDS:')) {\n const parts = trimmed.split(':');\n const collectionName = parts[1];\n const sampleSize = parseInt(parts[2] ?? '20', 10);\n const docs = await this.db!.collection(collectionName)\n .aggregate<Document>([{ $sample: { size: sampleSize } }])\n .toArray();\n const fieldMap = new Map<string, string>();\n for (const doc of docs) {\n this.extractFields(doc, '', fieldMap);\n }\n const rows = Array.from(fieldMap.entries()).map(([name, type]) => ({\n column_name: name,\n data_type: type,\n is_nullable: 'YES',\n column_default: null,\n }));\n return { rows, rowCount: rows.length };\n }\n\n if (trimmed.startsWith('COMMAND:')) {\n const cmdJson = trimmed.slice(8);\n const cmd = JSON.parse(cmdJson) as Document;\n const result = await this.db!.admin().command(cmd);\n return { rows: [result as Record<string, unknown>], rowCount: 1 };\n }\n\n throw new Error(`MongoDB connector does not support raw SQL. Use command protocol (COLLECTIONS, SAMPLE:name:N, COUNT:name, INDEXES:name, FIELDS:name:N, COMMAND:{...}).`);\n }\n\n private ensureConnected(): void {\n if (!this.db) {\n throw new Error('Not connected. Call connect() first.');\n }\n }\n\n private flattenDocument(doc: Document): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(doc)) {\n if (key === '_id') {\n result[key] = String(value);\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n private extractFields(doc: Document, prefix: string, fieldMap: Map<string, string>): void {\n for (const [key, value] of Object.entries(doc)) {\n const fullKey = prefix ? `${prefix}.${key}` : key;\n const bsonType = this.inferBsonType(value);\n if (!fieldMap.has(fullKey)) {\n fieldMap.set(fullKey, bsonType);\n }\n }\n }\n\n private inferBsonType(value: unknown): string {\n if (value === null || value === undefined) return 'null';\n if (typeof value === 'string') return 'string';\n if (typeof value === 'number') return Number.isInteger(value) ? 'int' : 'double';\n if (typeof value === 'boolean') return 'bool';\n if (value instanceof Date) return 'date';\n if (Array.isArray(value)) return 'array';\n if (typeof value === 'object') return 'object';\n return 'unknown';\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,92 @@
1
+ import {
2
+ createLogger
3
+ } from "./chunk-T4NUBJ7T.js";
4
+
5
+ // src/behavioral/streams/mongodb-stream.ts
6
+ import { MongoClient } from "mongodb";
7
+ var MongoDBStream = class {
8
+ connectionConfig;
9
+ logger = createLogger({ name: "mongodb-stream" });
10
+ client = null;
11
+ changeStream = null;
12
+ _running = false;
13
+ constructor(connectionConfig) {
14
+ this.connectionConfig = connectionConfig;
15
+ }
16
+ async start(onChange) {
17
+ const url = this.connectionConfig.url ?? `mongodb://${this.connectionConfig.host ?? "localhost"}:${this.connectionConfig.port ?? 27017}`;
18
+ const dbName = this.connectionConfig.database ?? "test";
19
+ this.client = new MongoClient(url);
20
+ await this.client.connect();
21
+ const db = this.client.db(dbName);
22
+ const stream = db.watch([], { fullDocument: "updateLookup" });
23
+ this._running = true;
24
+ stream.on("change", (event) => {
25
+ const rawChange = this.mapChangeEvent(event);
26
+ if (rawChange) {
27
+ onChange(rawChange);
28
+ }
29
+ });
30
+ stream.on("error", (err) => {
31
+ this.logger.error({ error: err.message }, "MongoDB change stream error");
32
+ this._running = false;
33
+ });
34
+ stream.on("close", () => {
35
+ this._running = false;
36
+ });
37
+ this.changeStream = stream;
38
+ this.logger.info("MongoDB change stream started");
39
+ }
40
+ async stop() {
41
+ if (this.changeStream) {
42
+ await this.changeStream.close();
43
+ this.changeStream = null;
44
+ }
45
+ this._running = false;
46
+ }
47
+ isRunning() {
48
+ return this._running;
49
+ }
50
+ async cleanup() {
51
+ await this.stop();
52
+ if (this.client) {
53
+ await this.client.close();
54
+ this.client = null;
55
+ }
56
+ }
57
+ mapChangeEvent(event) {
58
+ const evt = event;
59
+ const ns = evt.ns;
60
+ if (!ns) return null;
61
+ let operation;
62
+ switch (event.operationType) {
63
+ case "insert":
64
+ operation = "INSERT";
65
+ break;
66
+ case "update":
67
+ case "replace":
68
+ operation = "UPDATE";
69
+ break;
70
+ case "delete":
71
+ operation = "DELETE";
72
+ break;
73
+ default:
74
+ return null;
75
+ }
76
+ const documentKey = evt.documentKey;
77
+ const primaryKey = documentKey?._id ? String(documentKey._id) : "unknown";
78
+ const fullDoc = evt.fullDocument;
79
+ return {
80
+ tableName: ns.coll,
81
+ tableSchema: ns.db,
82
+ operation,
83
+ primaryKey,
84
+ newData: fullDoc ?? void 0,
85
+ detectedAt: /* @__PURE__ */ new Date()
86
+ };
87
+ }
88
+ };
89
+ export {
90
+ MongoDBStream
91
+ };
92
+ //# sourceMappingURL=mongodb-stream-Q23UHLTM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/behavioral/streams/mongodb-stream.ts"],"sourcesContent":["import { MongoClient, type ChangeStreamDocument, type Document } from 'mongodb';\nimport type { ConnectionConfig } from '../../core/config.js';\nimport type { RawChange } from '../types.js';\nimport type { ChangeStream as CortexaChangeStream } from './types.js';\nimport { createLogger } from '../../core/logger.js';\n\nexport class MongoDBStream implements CortexaChangeStream {\n private readonly connectionConfig: ConnectionConfig;\n private readonly logger = createLogger({ name: 'mongodb-stream' });\n private client: MongoClient | null = null;\n private changeStream: ReturnType<MongoClient['db']> extends infer D ? (D extends { watch: (...args: unknown[]) => infer W } ? W : never) : never = null as never;\n private _running = false;\n\n constructor(connectionConfig: ConnectionConfig) {\n this.connectionConfig = connectionConfig;\n }\n\n async start(onChange: (change: RawChange) => void): Promise<void> {\n const url = this.connectionConfig.url ??\n `mongodb://${this.connectionConfig.host ?? 'localhost'}:${this.connectionConfig.port ?? 27017}`;\n const dbName = this.connectionConfig.database ?? 'test';\n\n this.client = new MongoClient(url);\n await this.client.connect();\n const db = this.client.db(dbName);\n\n const stream = db.watch([], { fullDocument: 'updateLookup' });\n this._running = true;\n\n stream.on('change', (event: ChangeStreamDocument<Document>) => {\n const rawChange = this.mapChangeEvent(event);\n if (rawChange) {\n onChange(rawChange);\n }\n });\n\n stream.on('error', (err: Error) => {\n this.logger.error({ error: err.message }, 'MongoDB change stream error');\n this._running = false;\n });\n\n stream.on('close', () => {\n this._running = false;\n });\n\n // Store as unknown to avoid complex type gymnastics\n this.changeStream = stream as never;\n this.logger.info('MongoDB change stream started');\n }\n\n async stop(): Promise<void> {\n if (this.changeStream) {\n await (this.changeStream as unknown as { close: () => Promise<void> }).close();\n this.changeStream = null as never;\n }\n this._running = false;\n }\n\n isRunning(): boolean {\n return this._running;\n }\n\n async cleanup(): Promise<void> {\n await this.stop();\n if (this.client) {\n await this.client.close();\n this.client = null;\n }\n }\n\n private mapChangeEvent(event: ChangeStreamDocument<Document>): RawChange | null {\n const evt = event as unknown as Record<string, unknown>;\n const ns = evt.ns as { db: string; coll: string } | undefined;\n if (!ns) return null;\n\n let operation: 'INSERT' | 'UPDATE' | 'DELETE';\n switch (event.operationType) {\n case 'insert':\n operation = 'INSERT';\n break;\n case 'update':\n case 'replace':\n operation = 'UPDATE';\n break;\n case 'delete':\n operation = 'DELETE';\n break;\n default:\n return null;\n }\n\n const documentKey = evt.documentKey as Record<string, unknown> | undefined;\n const primaryKey = documentKey?._id ? String(documentKey._id) : 'unknown';\n\n const fullDoc = evt.fullDocument as Record<string, unknown> | undefined;\n\n return {\n tableName: ns.coll,\n tableSchema: ns.db,\n operation,\n primaryKey,\n newData: fullDoc ?? undefined,\n detectedAt: new Date(),\n };\n }\n}\n"],"mappings":";;;;;AAAA,SAAS,mBAA6D;AAM/D,IAAM,gBAAN,MAAmD;AAAA,EACvC;AAAA,EACA,SAAS,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAAA,EACzD,SAA6B;AAAA,EAC7B,eAA2I;AAAA,EAC3I,WAAW;AAAA,EAEnB,YAAY,kBAAoC;AAC9C,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EAEA,MAAM,MAAM,UAAsD;AAChE,UAAM,MAAM,KAAK,iBAAiB,OAChC,aAAa,KAAK,iBAAiB,QAAQ,WAAW,IAAI,KAAK,iBAAiB,QAAQ,KAAK;AAC/F,UAAM,SAAS,KAAK,iBAAiB,YAAY;AAEjD,SAAK,SAAS,IAAI,YAAY,GAAG;AACjC,UAAM,KAAK,OAAO,QAAQ;AAC1B,UAAM,KAAK,KAAK,OAAO,GAAG,MAAM;AAEhC,UAAM,SAAS,GAAG,MAAM,CAAC,GAAG,EAAE,cAAc,eAAe,CAAC;AAC5D,SAAK,WAAW;AAEhB,WAAO,GAAG,UAAU,CAAC,UAA0C;AAC7D,YAAM,YAAY,KAAK,eAAe,KAAK;AAC3C,UAAI,WAAW;AACb,iBAAS,SAAS;AAAA,MACpB;AAAA,IACF,CAAC;AAED,WAAO,GAAG,SAAS,CAAC,QAAe;AACjC,WAAK,OAAO,MAAM,EAAE,OAAO,IAAI,QAAQ,GAAG,6BAA6B;AACvE,WAAK,WAAW;AAAA,IAClB,CAAC;AAED,WAAO,GAAG,SAAS,MAAM;AACvB,WAAK,WAAW;AAAA,IAClB,CAAC;AAGD,SAAK,eAAe;AACpB,SAAK,OAAO,KAAK,+BAA+B;AAAA,EAClD;AAAA,EAEA,MAAM,OAAsB;AAC1B,QAAI,KAAK,cAAc;AACrB,YAAO,KAAK,aAA2D,MAAM;AAC7E,WAAK,eAAe;AAAA,IACtB;AACA,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,YAAqB;AACnB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAM,UAAyB;AAC7B,UAAM,KAAK,KAAK;AAChB,QAAI,KAAK,QAAQ;AACf,YAAM,KAAK,OAAO,MAAM;AACxB,WAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA,EAEQ,eAAe,OAAyD;AAC9E,UAAM,MAAM;AACZ,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,GAAI,QAAO;AAEhB,QAAI;AACJ,YAAQ,MAAM,eAAe;AAAA,MAC3B,KAAK;AACH,oBAAY;AACZ;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AACH,oBAAY;AACZ;AAAA,MACF,KAAK;AACH,oBAAY;AACZ;AAAA,MACF;AACE,eAAO;AAAA,IACX;AAEA,UAAM,cAAc,IAAI;AACxB,UAAM,aAAa,aAAa,MAAM,OAAO,YAAY,GAAG,IAAI;AAEhE,UAAM,UAAU,IAAI;AAEpB,WAAO;AAAA,MACL,WAAW,GAAG;AAAA,MACd,aAAa,GAAG;AAAA,MAChB;AAAA,MACA;AAAA,MACA,SAAS,WAAW;AAAA,MACpB,YAAY,oBAAI,KAAK;AAAA,IACvB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,77 @@
1
+ // src/connectors/mssql.ts
2
+ import sql from "mssql";
3
+ var MSSQLConnector = class {
4
+ pool = null;
5
+ async connect(config) {
6
+ if (config.url) {
7
+ this.pool = await sql.connect(config.url);
8
+ } else {
9
+ this.pool = await sql.connect({
10
+ server: config.host ?? "localhost",
11
+ port: config.port ?? 1433,
12
+ database: config.database,
13
+ user: config.user,
14
+ password: config.password,
15
+ options: {
16
+ encrypt: false,
17
+ trustServerCertificate: true
18
+ }
19
+ });
20
+ }
21
+ }
22
+ async disconnect() {
23
+ if (this.pool) {
24
+ await this.pool.close();
25
+ this.pool = null;
26
+ }
27
+ }
28
+ async getTableCount() {
29
+ this.ensureConnected();
30
+ const result = await this.pool.request().query(
31
+ `SELECT COUNT(*) AS count FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'`
32
+ );
33
+ return result.recordset[0]?.count ?? 0;
34
+ }
35
+ async getDatabaseName() {
36
+ this.ensureConnected();
37
+ const result = await this.pool.request().query("SELECT DB_NAME() AS db_name");
38
+ return result.recordset[0]?.db_name ?? "";
39
+ }
40
+ async isReadOnly() {
41
+ this.ensureConnected();
42
+ try {
43
+ await this.pool.request().query("CREATE TABLE #_cortexa_ro_test (id INT); DROP TABLE #_cortexa_ro_test;");
44
+ return false;
45
+ } catch {
46
+ return true;
47
+ }
48
+ }
49
+ isConnected() {
50
+ return this.pool !== null && this.pool.connected;
51
+ }
52
+ async query(sqlText, params) {
53
+ this.ensureConnected();
54
+ const request = this.pool.request();
55
+ let convertedSql = sqlText;
56
+ if (params && params.length > 0) {
57
+ convertedSql = sqlText.replace(/\$(\d+)/g, (_match, num) => `@p${num}`);
58
+ for (let i = 0; i < params.length; i++) {
59
+ request.input(`p${i + 1}`, params[i]);
60
+ }
61
+ }
62
+ const result = await request.query(convertedSql);
63
+ return {
64
+ rows: result.recordset ?? [],
65
+ rowCount: result.rowsAffected?.[0] ?? result.recordset?.length ?? 0
66
+ };
67
+ }
68
+ ensureConnected() {
69
+ if (!this.pool) {
70
+ throw new Error("Not connected. Call connect() first.");
71
+ }
72
+ }
73
+ };
74
+ export {
75
+ MSSQLConnector
76
+ };
77
+ //# sourceMappingURL=mssql-I27XEHQ2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/connectors/mssql.ts"],"sourcesContent":["import sql from 'mssql';\nimport type { ConnectionConfig } from '../core/config.js';\nimport type { DatabaseConnector, QueryResult } from './types.js';\n\nexport class MSSQLConnector implements DatabaseConnector {\n private pool: sql.ConnectionPool | null = null;\n\n async connect(config: ConnectionConfig): Promise<void> {\n if (config.url) {\n this.pool = await sql.connect(config.url);\n } else {\n this.pool = await sql.connect({\n server: config.host ?? 'localhost',\n port: config.port ?? 1433,\n database: config.database,\n user: config.user,\n password: config.password,\n options: {\n encrypt: false,\n trustServerCertificate: true,\n },\n });\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.pool) {\n await this.pool.close();\n this.pool = null;\n }\n }\n\n async getTableCount(): Promise<number> {\n this.ensureConnected();\n const result = await this.pool!.request().query(\n `SELECT COUNT(*) AS count FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'`\n );\n return result.recordset[0]?.count ?? 0;\n }\n\n async getDatabaseName(): Promise<string> {\n this.ensureConnected();\n const result = await this.pool!.request().query('SELECT DB_NAME() AS db_name');\n return result.recordset[0]?.db_name ?? '';\n }\n\n async isReadOnly(): Promise<boolean> {\n this.ensureConnected();\n try {\n await this.pool!.request().query('CREATE TABLE #_cortexa_ro_test (id INT); DROP TABLE #_cortexa_ro_test;');\n return false;\n } catch {\n return true;\n }\n }\n\n isConnected(): boolean {\n return this.pool !== null && this.pool.connected;\n }\n\n async query(sqlText: string, params?: unknown[]): Promise<QueryResult> {\n this.ensureConnected();\n\n const request = this.pool!.request();\n\n // Convert $1, $2 style params to @p1, @p2\n let convertedSql = sqlText;\n if (params && params.length > 0) {\n convertedSql = sqlText.replace(/\\$(\\d+)/g, (_match, num) => `@p${num}`);\n for (let i = 0; i < params.length; i++) {\n request.input(`p${i + 1}`, params[i]);\n }\n }\n\n const result = await request.query(convertedSql);\n return {\n rows: result.recordset as Record<string, unknown>[] ?? [],\n rowCount: result.rowsAffected?.[0] ?? result.recordset?.length ?? 0,\n };\n }\n\n private ensureConnected(): void {\n if (!this.pool) {\n throw new Error('Not connected. Call connect() first.');\n }\n }\n}\n"],"mappings":";AAAA,OAAO,SAAS;AAIT,IAAM,iBAAN,MAAkD;AAAA,EAC/C,OAAkC;AAAA,EAE1C,MAAM,QAAQ,QAAyC;AACrD,QAAI,OAAO,KAAK;AACd,WAAK,OAAO,MAAM,IAAI,QAAQ,OAAO,GAAG;AAAA,IAC1C,OAAO;AACL,WAAK,OAAO,MAAM,IAAI,QAAQ;AAAA,QAC5B,QAAQ,OAAO,QAAQ;AAAA,QACvB,MAAM,OAAO,QAAQ;AAAA,QACrB,UAAU,OAAO;AAAA,QACjB,MAAM,OAAO;AAAA,QACb,UAAU,OAAO;AAAA,QACjB,SAAS;AAAA,UACP,SAAS;AAAA,UACT,wBAAwB;AAAA,QAC1B;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,aAA4B;AAChC,QAAI,KAAK,MAAM;AACb,YAAM,KAAK,KAAK,MAAM;AACtB,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AAAA,EAEA,MAAM,gBAAiC;AACrC,SAAK,gBAAgB;AACrB,UAAM,SAAS,MAAM,KAAK,KAAM,QAAQ,EAAE;AAAA,MACxC;AAAA,IACF;AACA,WAAO,OAAO,UAAU,CAAC,GAAG,SAAS;AAAA,EACvC;AAAA,EAEA,MAAM,kBAAmC;AACvC,SAAK,gBAAgB;AACrB,UAAM,SAAS,MAAM,KAAK,KAAM,QAAQ,EAAE,MAAM,6BAA6B;AAC7E,WAAO,OAAO,UAAU,CAAC,GAAG,WAAW;AAAA,EACzC;AAAA,EAEA,MAAM,aAA+B;AACnC,SAAK,gBAAgB;AACrB,QAAI;AACF,YAAM,KAAK,KAAM,QAAQ,EAAE,MAAM,wEAAwE;AACzG,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,cAAuB;AACrB,WAAO,KAAK,SAAS,QAAQ,KAAK,KAAK;AAAA,EACzC;AAAA,EAEA,MAAM,MAAM,SAAiB,QAA0C;AACrE,SAAK,gBAAgB;AAErB,UAAM,UAAU,KAAK,KAAM,QAAQ;AAGnC,QAAI,eAAe;AACnB,QAAI,UAAU,OAAO,SAAS,GAAG;AAC/B,qBAAe,QAAQ,QAAQ,YAAY,CAAC,QAAQ,QAAQ,KAAK,GAAG,EAAE;AACtE,eAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,gBAAQ,MAAM,IAAI,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC;AAAA,MACtC;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,QAAQ,MAAM,YAAY;AAC/C,WAAO;AAAA,MACL,MAAM,OAAO,aAA0C,CAAC;AAAA,MACxD,UAAU,OAAO,eAAe,CAAC,KAAK,OAAO,WAAW,UAAU;AAAA,IACpE;AAAA,EACF;AAAA,EAEQ,kBAAwB;AAC9B,QAAI,CAAC,KAAK,MAAM;AACd,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,194 @@
1
+ import {
2
+ computeBackoff,
3
+ resolveReconnectOptions
4
+ } from "./chunk-6VDDIS65.js";
5
+ import {
6
+ createLogger
7
+ } from "./chunk-T4NUBJ7T.js";
8
+
9
+ // src/behavioral/streams/mysql-stream.ts
10
+ var logger = createLogger({ name: "mysql-stream" });
11
+ var MySQLStream = class {
12
+ connectionConfig;
13
+ connector;
14
+ running;
15
+ zongji;
16
+ reconnectOpts;
17
+ reconnectAttempts;
18
+ lastOnChange;
19
+ constructor(connectionConfig, connector, reconnect) {
20
+ this.connectionConfig = connectionConfig;
21
+ this.connector = connector;
22
+ this.running = false;
23
+ this.zongji = null;
24
+ this.reconnectOpts = resolveReconnectOptions(reconnect);
25
+ this.reconnectAttempts = 0;
26
+ this.lastOnChange = null;
27
+ }
28
+ async start(onChange) {
29
+ this.lastOnChange = onChange;
30
+ this.reconnectAttempts = 0;
31
+ await this.connectStream(onChange);
32
+ }
33
+ async connectStream(onChange) {
34
+ const { ZongJi } = await import("@powersync/mysql-zongji");
35
+ const connectionOptions = this.buildConnectionOptions();
36
+ const zongji = new ZongJi(connectionOptions);
37
+ this.zongji = zongji;
38
+ this.zongji.on("binlog", (event) => {
39
+ try {
40
+ const binlogEvent = event;
41
+ const eventName = binlogEvent.getEventName().toLowerCase();
42
+ if (eventName === "writerows" || eventName === "updaterows" || eventName === "deleterows") {
43
+ const changes = this.mapBinlogEvent(eventName, binlogEvent);
44
+ for (const change of changes) {
45
+ this.reconnectAttempts = 0;
46
+ onChange(change);
47
+ }
48
+ }
49
+ } catch (err) {
50
+ const message = err instanceof Error ? err.message : String(err);
51
+ logger.error({ err: message }, "Failed to process binlog event");
52
+ }
53
+ });
54
+ this.zongji.on("error", (err) => {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ logger.error({ err: message }, "Binlog stream error");
57
+ this.attemptReconnect();
58
+ });
59
+ this.zongji.start({
60
+ serverId: Math.floor(Math.random() * 1e5) + 1,
61
+ includeEvents: ["tablemap", "writerows", "updaterows", "deleterows"]
62
+ });
63
+ this.running = true;
64
+ logger.info("MySQL binlog stream started");
65
+ }
66
+ attemptReconnect() {
67
+ if (!this.lastOnChange || this.reconnectAttempts >= this.reconnectOpts.maxAttempts) {
68
+ logger.error({ attempts: this.reconnectAttempts }, "Max reconnection attempts reached, stream stopped");
69
+ this.running = false;
70
+ return;
71
+ }
72
+ const delay = computeBackoff(this.reconnectAttempts, this.reconnectOpts);
73
+ this.reconnectAttempts++;
74
+ logger.info({ attempt: this.reconnectAttempts, delayMs: delay }, "Reconnecting binlog stream");
75
+ setTimeout(() => {
76
+ if (!this.lastOnChange) return;
77
+ this.connectStream(this.lastOnChange).catch((err) => {
78
+ const message = err instanceof Error ? err.message : String(err);
79
+ logger.error({ err: message }, "Reconnection failed");
80
+ this.attemptReconnect();
81
+ });
82
+ }, delay);
83
+ }
84
+ async stop() {
85
+ if (this.zongji) {
86
+ this.zongji.stop();
87
+ this.zongji = null;
88
+ }
89
+ this.running = false;
90
+ logger.info("MySQL binlog stream stopped");
91
+ }
92
+ async cleanup() {
93
+ await this.stop();
94
+ }
95
+ isRunning() {
96
+ return this.running;
97
+ }
98
+ mapBinlogEvent(eventType, event) {
99
+ const tableInfo = event.tableMap[event.tableId];
100
+ if (!tableInfo) {
101
+ return [];
102
+ }
103
+ const changes = [];
104
+ switch (eventType) {
105
+ case "writerows": {
106
+ for (const row of event.rows) {
107
+ const primaryKey = this.extractPrimaryKey(row);
108
+ changes.push({
109
+ tableName: tableInfo.tableName,
110
+ tableSchema: tableInfo.parentSchema,
111
+ operation: "INSERT",
112
+ primaryKey,
113
+ newData: row,
114
+ detectedAt: /* @__PURE__ */ new Date()
115
+ });
116
+ }
117
+ break;
118
+ }
119
+ case "updaterows": {
120
+ for (const row of event.rows) {
121
+ const updateRow = row;
122
+ const primaryKey = this.extractPrimaryKey(updateRow.after);
123
+ const changedColumns = this.computeChangedColumns(updateRow.before, updateRow.after);
124
+ changes.push({
125
+ tableName: tableInfo.tableName,
126
+ tableSchema: tableInfo.parentSchema,
127
+ operation: "UPDATE",
128
+ primaryKey,
129
+ oldData: updateRow.before,
130
+ newData: updateRow.after,
131
+ changedColumns,
132
+ detectedAt: /* @__PURE__ */ new Date()
133
+ });
134
+ }
135
+ break;
136
+ }
137
+ case "deleterows": {
138
+ for (const row of event.rows) {
139
+ const primaryKey = this.extractPrimaryKey(row);
140
+ changes.push({
141
+ tableName: tableInfo.tableName,
142
+ tableSchema: tableInfo.parentSchema,
143
+ operation: "DELETE",
144
+ primaryKey,
145
+ oldData: row,
146
+ detectedAt: /* @__PURE__ */ new Date()
147
+ });
148
+ }
149
+ break;
150
+ }
151
+ }
152
+ return changes;
153
+ }
154
+ extractPrimaryKey(data) {
155
+ if (data.id !== void 0) {
156
+ const id = data.id;
157
+ if (typeof id === "number" || typeof id === "string") {
158
+ return id;
159
+ }
160
+ return String(id);
161
+ }
162
+ return "unknown";
163
+ }
164
+ computeChangedColumns(oldData, newData) {
165
+ const changed = [];
166
+ for (const key of Object.keys(newData)) {
167
+ if (oldData[key] !== newData[key]) {
168
+ changed.push(key);
169
+ }
170
+ }
171
+ return changed;
172
+ }
173
+ buildConnectionOptions() {
174
+ if (this.connectionConfig.url) {
175
+ const url = new URL(this.connectionConfig.url);
176
+ return {
177
+ host: url.hostname,
178
+ port: url.port ? parseInt(url.port, 10) : 3306,
179
+ user: decodeURIComponent(url.username),
180
+ password: decodeURIComponent(url.password)
181
+ };
182
+ }
183
+ return {
184
+ host: this.connectionConfig.host ?? "localhost",
185
+ port: this.connectionConfig.port,
186
+ user: this.connectionConfig.user ?? "",
187
+ password: this.connectionConfig.password ?? ""
188
+ };
189
+ }
190
+ };
191
+ export {
192
+ MySQLStream
193
+ };
194
+ //# sourceMappingURL=mysql-stream-YCNLPPPG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/behavioral/streams/mysql-stream.ts"],"sourcesContent":["import type { ConnectionConfig } from '../../core/config.js';\nimport type { DatabaseConnector } from '../../connectors/types.js';\nimport type { RawChange } from '../types.js';\nimport type { ChangeStream, ReconnectOptions } from './types.js';\nimport { resolveReconnectOptions, computeBackoff } from './types.js';\nimport type { ZongjiOptions } from '@powersync/mysql-zongji';\nimport { createLogger } from '../../core/logger.js';\n\nexport interface BinlogTableInfo {\n parentSchema: string;\n tableName: string;\n columns: Array<{ name: string }>;\n}\n\nexport interface BinlogEvent {\n tableMap: Record<number, BinlogTableInfo>;\n tableId: number;\n rows: Array<Record<string, unknown>>;\n}\n\nexport type BinlogEventType = 'writerows' | 'updaterows' | 'deleterows';\n\nconst logger = createLogger({ name: 'mysql-stream' });\n\nexport class MySQLStream implements ChangeStream {\n private readonly connectionConfig: ConnectionConfig;\n private readonly connector: DatabaseConnector;\n private running: boolean;\n private zongji: import('@powersync/mysql-zongji').ZongJi | null;\n private reconnectOpts: Required<ReconnectOptions>;\n private reconnectAttempts: number;\n private lastOnChange: ((change: RawChange) => void) | null;\n\n constructor(connectionConfig: ConnectionConfig, connector: DatabaseConnector, reconnect?: ReconnectOptions) {\n this.connectionConfig = connectionConfig;\n this.connector = connector;\n this.running = false;\n this.zongji = null;\n this.reconnectOpts = resolveReconnectOptions(reconnect);\n this.reconnectAttempts = 0;\n this.lastOnChange = null;\n }\n\n async start(onChange: (change: RawChange) => void): Promise<void> {\n this.lastOnChange = onChange;\n this.reconnectAttempts = 0;\n await this.connectStream(onChange);\n }\n\n private async connectStream(onChange: (change: RawChange) => void): Promise<void> {\n const { ZongJi } = await import('@powersync/mysql-zongji');\n\n const connectionOptions = this.buildConnectionOptions();\n\n const zongji = new ZongJi(connectionOptions);\n\n this.zongji = zongji;\n\n this.zongji.on('binlog', (event: unknown) => {\n try {\n const binlogEvent = event as { getEventName: () => string } & BinlogEvent;\n const eventName = binlogEvent.getEventName().toLowerCase();\n\n if (eventName === 'writerows' || eventName === 'updaterows' || eventName === 'deleterows') {\n const changes = this.mapBinlogEvent(eventName, binlogEvent);\n for (const change of changes) {\n this.reconnectAttempts = 0;\n onChange(change);\n }\n }\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n logger.error({ err: message }, 'Failed to process binlog event');\n }\n });\n\n this.zongji.on('error', (err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n logger.error({ err: message }, 'Binlog stream error');\n this.attemptReconnect();\n });\n\n this.zongji.start({\n serverId: Math.floor(Math.random() * 100000) + 1,\n includeEvents: ['tablemap', 'writerows', 'updaterows', 'deleterows'],\n });\n\n this.running = true;\n logger.info('MySQL binlog stream started');\n }\n\n private attemptReconnect(): void {\n if (!this.lastOnChange || this.reconnectAttempts >= this.reconnectOpts.maxAttempts) {\n logger.error({ attempts: this.reconnectAttempts }, 'Max reconnection attempts reached, stream stopped');\n this.running = false;\n return;\n }\n\n const delay = computeBackoff(this.reconnectAttempts, this.reconnectOpts);\n this.reconnectAttempts++;\n logger.info({ attempt: this.reconnectAttempts, delayMs: delay }, 'Reconnecting binlog stream');\n\n setTimeout(() => {\n if (!this.lastOnChange) return;\n this.connectStream(this.lastOnChange).catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n logger.error({ err: message }, 'Reconnection failed');\n this.attemptReconnect();\n });\n }, delay);\n }\n\n async stop(): Promise<void> {\n if (this.zongji) {\n this.zongji.stop();\n this.zongji = null;\n }\n this.running = false;\n logger.info('MySQL binlog stream stopped');\n }\n\n async cleanup(): Promise<void> {\n await this.stop();\n // MySQL has no replication slot — nothing extra to clean up\n }\n\n isRunning(): boolean {\n return this.running;\n }\n\n mapBinlogEvent(eventType: BinlogEventType, event: BinlogEvent): RawChange[] {\n const tableInfo = event.tableMap[event.tableId];\n if (!tableInfo) {\n return [];\n }\n\n const changes: RawChange[] = [];\n\n switch (eventType) {\n case 'writerows': {\n for (const row of event.rows) {\n const primaryKey = this.extractPrimaryKey(row);\n\n changes.push({\n tableName: tableInfo.tableName,\n tableSchema: tableInfo.parentSchema,\n operation: 'INSERT',\n primaryKey,\n newData: row,\n detectedAt: new Date(),\n });\n }\n break;\n }\n case 'updaterows': {\n for (const row of event.rows) {\n const updateRow = row as unknown as { before: Record<string, unknown>; after: Record<string, unknown> };\n const primaryKey = this.extractPrimaryKey(updateRow.after);\n const changedColumns = this.computeChangedColumns(updateRow.before, updateRow.after);\n\n changes.push({\n tableName: tableInfo.tableName,\n tableSchema: tableInfo.parentSchema,\n operation: 'UPDATE',\n primaryKey,\n oldData: updateRow.before,\n newData: updateRow.after,\n changedColumns,\n detectedAt: new Date(),\n });\n }\n break;\n }\n case 'deleterows': {\n for (const row of event.rows) {\n const primaryKey = this.extractPrimaryKey(row);\n\n changes.push({\n tableName: tableInfo.tableName,\n tableSchema: tableInfo.parentSchema,\n operation: 'DELETE',\n primaryKey,\n oldData: row,\n detectedAt: new Date(),\n });\n }\n break;\n }\n }\n\n return changes;\n }\n\n private extractPrimaryKey(data: Record<string, unknown>): string | number {\n if (data.id !== undefined) {\n const id = data.id;\n if (typeof id === 'number' || typeof id === 'string') {\n return id;\n }\n return String(id);\n }\n\n return 'unknown';\n }\n\n private computeChangedColumns(\n oldData: Record<string, unknown>,\n newData: Record<string, unknown>\n ): string[] {\n const changed: string[] = [];\n for (const key of Object.keys(newData)) {\n if (oldData[key] !== newData[key]) {\n changed.push(key);\n }\n }\n return changed;\n }\n\n private buildConnectionOptions(): ZongjiOptions {\n if (this.connectionConfig.url) {\n const url = new URL(this.connectionConfig.url);\n return {\n host: url.hostname,\n port: url.port ? parseInt(url.port, 10) : 3306,\n user: decodeURIComponent(url.username),\n password: decodeURIComponent(url.password),\n };\n }\n\n return {\n host: this.connectionConfig.host ?? 'localhost',\n port: this.connectionConfig.port,\n user: this.connectionConfig.user ?? '',\n password: this.connectionConfig.password ?? '',\n };\n }\n}\n"],"mappings":"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","names":[]}