@absolutejs/sync 0.0.1 → 0.1.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 (68) hide show
  1. package/README.md +264 -24
  2. package/dist/adapters/drizzle/index.d.ts +17 -0
  3. package/dist/adapters/drizzle/index.js +128 -0
  4. package/dist/adapters/drizzle/index.js.map +12 -0
  5. package/dist/adapters/drizzle/read.d.ts +31 -0
  6. package/dist/adapters/drizzle/topics.d.ts +41 -0
  7. package/dist/adapters/drizzle/write.d.ts +69 -0
  8. package/dist/adapters/mysql/index.d.ts +75 -0
  9. package/dist/adapters/mysql/index.js +171 -0
  10. package/dist/adapters/mysql/index.js.map +11 -0
  11. package/dist/adapters/postgres/index.d.ts +53 -0
  12. package/dist/adapters/postgres/index.js +86 -0
  13. package/dist/adapters/postgres/index.js.map +10 -0
  14. package/dist/adapters/prisma/collection.d.ts +39 -0
  15. package/dist/adapters/prisma/index.d.ts +23 -0
  16. package/dist/adapters/prisma/index.js +231 -0
  17. package/dist/adapters/prisma/index.js.map +14 -0
  18. package/dist/adapters/prisma/predicate.d.ts +20 -0
  19. package/dist/adapters/prisma/read.d.ts +28 -0
  20. package/dist/adapters/prisma/topics.d.ts +29 -0
  21. package/dist/adapters/prisma/write.d.ts +65 -0
  22. package/dist/adapters/sqlite/index.d.ts +32 -0
  23. package/dist/adapters/sqlite/index.js +128 -0
  24. package/dist/adapters/sqlite/index.js.map +11 -0
  25. package/dist/angular/index.d.ts +1 -0
  26. package/dist/angular/index.js +347 -0
  27. package/dist/angular/index.js.map +11 -0
  28. package/dist/angular/sync-collection.service.d.ts +20 -0
  29. package/dist/client/index.d.ts +8 -30
  30. package/dist/client/index.js +744 -3
  31. package/dist/client/index.js.map +8 -4
  32. package/dist/client/liveQuery.d.ts +75 -0
  33. package/dist/client/subscriber.d.ts +30 -0
  34. package/dist/client/syncCollection.d.ts +102 -0
  35. package/dist/client/syncStore.d.ts +81 -0
  36. package/dist/engine/aggregate.d.ts +45 -0
  37. package/dist/engine/collection.d.ts +87 -0
  38. package/dist/engine/connection.d.ts +71 -0
  39. package/dist/engine/dataflow.d.ts +109 -0
  40. package/dist/engine/equiJoin.d.ts +51 -0
  41. package/dist/engine/graph.d.ts +85 -0
  42. package/dist/engine/index.d.ts +34 -0
  43. package/dist/engine/index.js +1269 -0
  44. package/dist/engine/index.js.map +20 -0
  45. package/dist/engine/materializedView.d.ts +53 -0
  46. package/dist/engine/mutation.d.ts +30 -0
  47. package/dist/engine/pollingSource.d.ts +42 -0
  48. package/dist/engine/routes.d.ts +40 -0
  49. package/dist/engine/socket.d.ts +64 -0
  50. package/dist/engine/syncEngine.d.ts +100 -0
  51. package/dist/engine/types.d.ts +45 -0
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.js +160 -2
  54. package/dist/index.js.map +7 -5
  55. package/dist/react/index.d.ts +1 -0
  56. package/dist/react/index.js +332 -0
  57. package/dist/react/index.js.map +11 -0
  58. package/dist/react/useSyncCollection.d.ts +16 -0
  59. package/dist/reactiveHub.d.ts +6 -0
  60. package/dist/svelte/createSyncCollectionStore.d.ts +15 -0
  61. package/dist/svelte/index.d.ts +1 -0
  62. package/dist/svelte/index.js +338 -0
  63. package/dist/svelte/index.js.map +11 -0
  64. package/dist/vue/index.d.ts +1 -0
  65. package/dist/vue/index.js +331 -0
  66. package/dist/vue/index.js.map +11 -0
  67. package/dist/vue/useSyncCollection.d.ts +17 -0
  68. package/package.json +102 -6
@@ -0,0 +1,75 @@
1
+ import type { ChangeSource, ParsedChange } from '../../engine/types';
2
+ /**
3
+ * MySQL CDC adapter for @absolutejs/sync (Tier 3, M5).
4
+ *
5
+ * MySQL has no `LISTEN/NOTIFY`, so two pluggable strategies catch out-of-band
6
+ * writes — both behind the engine's {@link ChangeSource} seam:
7
+ *
8
+ * - **Changelog + poll (portable).** Install triggers with
9
+ * {@link mysqlChangelogSchema} and tail the changelog with
10
+ * {@link createPollingChangeSource}. Works anywhere, no extra privileges.
11
+ * - **Binlog (higher throughput).** Wire a binlog reader (e.g. zongji) into
12
+ * {@link mysqlBinlogChangeSource}; it normalizes row events into the change
13
+ * feed. Catches writes without the per-write changelog overhead.
14
+ *
15
+ * Dependency-free — you bring the MySQL client / binlog reader.
16
+ */
17
+ export { createPollingChangeSource, parseOutboxRow } from '../../engine/pollingSource';
18
+ export type { OutboxRow, PollingChangeSourceOptions } from '../../engine/pollingSource';
19
+ export type MysqlChangelogOptions = {
20
+ /** Table name → the column names to capture in the change payload. */
21
+ tables: Record<string, string[]>;
22
+ /** Changelog table name. Defaults to `absolute_sync_changelog`. */
23
+ changelogTable?: string;
24
+ /** Trigger name prefix. Defaults to `absolute_sync`. */
25
+ prefix?: string;
26
+ };
27
+ /**
28
+ * Generate the SQL that installs the changelog table and per-table
29
+ * insert/update/delete triggers — run it once (e.g. in a migration). Each
30
+ * trigger appends `{ tbl, op, payload }` (payload built with `JSON_OBJECT` from
31
+ * the listed columns) to the changelog for {@link createPollingChangeSource}.
32
+ *
33
+ * Each `CREATE TRIGGER` body is a single statement (no `DELIMITER` needed). The
34
+ * statements are `;`-separated; run them as a script, or split on `;` if your
35
+ * driver executes one statement per call.
36
+ */
37
+ export declare const mysqlChangelogSchema: (options: MysqlChangelogOptions) => string;
38
+ /** A row-level binlog event (the subset {@link normalizeBinlogEvent} needs). */
39
+ export type BinlogRowEvent = {
40
+ /**
41
+ * Event kind — zongji's `writerows` / `updaterows` / `deleterows`
42
+ * (case-insensitive).
43
+ */
44
+ type: string;
45
+ /** The affected table. */
46
+ table: string;
47
+ /**
48
+ * Affected rows. Insert/delete: each entry is the row object. Update: each
49
+ * entry is `{ before, after }` (zongji's shape).
50
+ */
51
+ rows: unknown[];
52
+ };
53
+ /**
54
+ * Normalize a binlog row event into engine changes — one per affected row. For
55
+ * updates it takes the `after` image; for deletes the row (or its `before`
56
+ * image). Rows that aren't objects are skipped. Pure, so it's easy to test and
57
+ * to swap for a different reader's shape.
58
+ */
59
+ export declare const normalizeBinlogEvent: (event: BinlogRowEvent) => ParsedChange[];
60
+ export type MysqlBinlogChangeSourceOptions = {
61
+ /**
62
+ * Subscribe to row events from a binlog reader; return a function that stops
63
+ * it. e.g. with zongji:
64
+ * `(onEvent) => { zongji.on('binlog', e => onEvent({ type: e.getEventName(), table: e.tableMap[e.tableId].tableName, rows: e.rows })); zongji.start(...); return () => zongji.stop(); }`.
65
+ */
66
+ subscribe: (onEvent: (event: BinlogRowEvent) => void) => Promise<() => void | Promise<void>> | (() => void | Promise<void>);
67
+ /** Override the event normalizer (defaults to {@link normalizeBinlogEvent}). */
68
+ normalize?: (event: BinlogRowEvent) => ParsedChange[];
69
+ };
70
+ /**
71
+ * A {@link ChangeSource} backed by the MySQL binlog. Each row event is
72
+ * normalized and emitted into the engine. Connect with
73
+ * `engine.connectSource(...)`.
74
+ */
75
+ export declare const mysqlBinlogChangeSource: (options: MysqlBinlogChangeSourceOptions) => ChangeSource;
@@ -0,0 +1,171 @@
1
+ // @bun
2
+ // src/engine/pollingSource.ts
3
+ var OP_BY_NAME = {
4
+ insert: "insert",
5
+ INSERT: "insert",
6
+ update: "update",
7
+ UPDATE: "update",
8
+ delete: "delete",
9
+ DELETE: "delete"
10
+ };
11
+ var parseOutboxRow = (row) => {
12
+ if (typeof row.tbl !== "string") {
13
+ return;
14
+ }
15
+ const op = OP_BY_NAME[row.op];
16
+ if (op === undefined) {
17
+ return;
18
+ }
19
+ let payload = row.payload;
20
+ if (typeof payload === "string") {
21
+ try {
22
+ payload = JSON.parse(payload);
23
+ } catch {
24
+ return;
25
+ }
26
+ }
27
+ if (typeof payload !== "object" || payload === null) {
28
+ return;
29
+ }
30
+ return { table: row.tbl, change: { op, row: payload } };
31
+ };
32
+ var createPollingChangeSource = (options) => {
33
+ const intervalMs = options.intervalMs ?? 1000;
34
+ const parse = options.parse ?? parseOutboxRow;
35
+ const onError = options.onError ?? ((error) => {
36
+ console.warn("[sync] polling change source error:", error);
37
+ });
38
+ let cursor = options.startSeq ?? 0;
39
+ let running = false;
40
+ let timer;
41
+ const tick = async (emit) => {
42
+ if (!running) {
43
+ return;
44
+ }
45
+ try {
46
+ const rows = await options.poll(cursor);
47
+ for (const row of rows) {
48
+ const parsed = parse(row);
49
+ if (parsed !== undefined) {
50
+ await emit(parsed.table, parsed.change);
51
+ }
52
+ if (typeof row.seq === "number" && row.seq > cursor) {
53
+ cursor = row.seq;
54
+ }
55
+ }
56
+ if (rows.length > 0) {
57
+ await options.onProcessed?.(cursor);
58
+ }
59
+ } catch (error) {
60
+ onError(error);
61
+ }
62
+ if (running) {
63
+ timer = setTimeout(() => {
64
+ tick(emit);
65
+ }, intervalMs);
66
+ }
67
+ };
68
+ return {
69
+ start: async (emit) => {
70
+ if (running) {
71
+ return;
72
+ }
73
+ running = true;
74
+ await tick(emit);
75
+ },
76
+ stop: () => {
77
+ running = false;
78
+ if (timer !== undefined) {
79
+ clearTimeout(timer);
80
+ timer = undefined;
81
+ }
82
+ }
83
+ };
84
+ };
85
+
86
+ // src/adapters/mysql/index.ts
87
+ var DEFAULT_CHANGELOG = "absolute_sync_changelog";
88
+ var DEFAULT_PREFIX = "absolute_sync";
89
+ var OPS = ["insert", "update", "delete"];
90
+ var mysqlChangelogSchema = (options) => {
91
+ const changelog = options.changelogTable ?? DEFAULT_CHANGELOG;
92
+ const prefix = options.prefix ?? DEFAULT_PREFIX;
93
+ const createTable = [
94
+ `CREATE TABLE IF NOT EXISTS \`${changelog}\` (`,
95
+ "\tseq BIGINT AUTO_INCREMENT PRIMARY KEY,",
96
+ "\ttbl VARCHAR(255) NOT NULL,",
97
+ "\top VARCHAR(16) NOT NULL,",
98
+ "\tpayload JSON NOT NULL,",
99
+ "\tcreated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP",
100
+ ");"
101
+ ].join(`
102
+ `);
103
+ const jsonObject = (columns, ref) => `JSON_OBJECT(${columns.map((column) => `'${column}', ${ref}.\`${column}\``).join(", ")})`;
104
+ const triggers = Object.entries(options.tables).flatMap(([table, columns]) => OPS.map((op) => {
105
+ const ref = op === "delete" ? "OLD" : "NEW";
106
+ const name = `${prefix}_${table}_${op}`;
107
+ return [
108
+ `DROP TRIGGER IF EXISTS \`${name}\`;`,
109
+ `CREATE TRIGGER \`${name}\` AFTER ${op.toUpperCase()} ON \`${table}\` FOR EACH ROW`,
110
+ `INSERT INTO \`${changelog}\` (tbl, op, payload)`,
111
+ `VALUES ('${table}', '${op}', ${jsonObject(columns, ref)});`
112
+ ].join(`
113
+ `);
114
+ }));
115
+ return [createTable, ...triggers].join(`
116
+
117
+ `);
118
+ };
119
+ var BINLOG_OP = {
120
+ writerows: "insert",
121
+ updaterows: "update",
122
+ deleterows: "delete"
123
+ };
124
+ var normalizeBinlogEvent = (event) => {
125
+ const op = BINLOG_OP[event.type.toLowerCase()];
126
+ if (op === undefined || typeof event.table !== "string") {
127
+ return [];
128
+ }
129
+ const changes = [];
130
+ for (const entry of event.rows) {
131
+ let row = entry;
132
+ if (entry !== null && typeof entry === "object") {
133
+ if (op === "update" && "after" in entry) {
134
+ row = entry.after;
135
+ } else if (op === "delete" && "before" in entry) {
136
+ row = entry.before;
137
+ }
138
+ }
139
+ if (typeof row === "object" && row !== null) {
140
+ changes.push({ table: event.table, change: { op, row } });
141
+ }
142
+ }
143
+ return changes;
144
+ };
145
+ var mysqlBinlogChangeSource = (options) => {
146
+ const normalize = options.normalize ?? normalizeBinlogEvent;
147
+ let unsubscribe;
148
+ return {
149
+ start: async (emit) => {
150
+ unsubscribe = await options.subscribe((event) => {
151
+ for (const parsed of normalize(event)) {
152
+ emit(parsed.table, parsed.change);
153
+ }
154
+ });
155
+ },
156
+ stop: async () => {
157
+ await unsubscribe?.();
158
+ unsubscribe = undefined;
159
+ }
160
+ };
161
+ };
162
+ export {
163
+ parseOutboxRow,
164
+ normalizeBinlogEvent,
165
+ mysqlChangelogSchema,
166
+ mysqlBinlogChangeSource,
167
+ createPollingChangeSource
168
+ };
169
+
170
+ //# debugId=48EE1E12A50EB47064756E2164756E21
171
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/engine/pollingSource.ts", "../src/adapters/mysql/index.ts"],
4
+ "sourcesContent": [
5
+ "import type { ChangeSource, EmitChange, ParsedChange, RowOp } from './types';\n\n/**\n * A database-agnostic CDC {@link ChangeSource} that tails an append-only\n * changelog (outbox) table and emits its rows into the engine.\n *\n * This is the portable way to catch out-of-band writes on databases without a\n * native push channel — MySQL (no `LISTEN/NOTIFY`) and SQLite (no update hook\n * reachable from the JS runtime). Install per-table triggers that append to the\n * changelog (see `@absolutejs/sync/mysql` and `/sqlite`), then poll it here. It\n * is also a fine fallback for Postgres when you'd rather not run `LISTEN`.\n *\n * Driver-agnostic: you supply `poll(sinceSeq)` — a single\n * `SELECT seq, tbl, op, payload FROM <changelog> WHERE seq > ? ORDER BY seq` run\n * with your client — and the adapter tracks the cursor, parses each row, emits,\n * and advances. Delivery is at-least-once across crashes (a row may re-emit if\n * the process dies mid-batch); the engine's per-key last-write-wins makes that\n * safe. Use `onProcessed` to prune the changelog once a watermark is durable.\n */\n\nconst OP_BY_NAME: Record<string, RowOp> = {\n\tinsert: 'insert',\n\tINSERT: 'insert',\n\tupdate: 'update',\n\tUPDATE: 'update',\n\tdelete: 'delete',\n\tDELETE: 'delete'\n};\n\n/** One changelog row, as returned by your `poll` query. */\nexport type OutboxRow = {\n\t/** Monotonic sequence (the cursor advances to the max seen). */\n\tseq: number;\n\t/** Source table the change happened on. */\n\ttbl: string;\n\t/** `insert` | `update` | `delete` (upper- or lower-case). */\n\top: string;\n\t/** The row's captured values — a JSON string or an already-parsed object. */\n\tpayload: unknown;\n};\n\n/**\n * Default changelog-row parser: normalizes `op`, JSON-parses a string `payload`,\n * and returns `{ table, change }`. Returns `undefined` for a malformed row so it\n * is skipped (and its `seq` still advances the cursor) rather than wedging the\n * feed.\n */\nexport const parseOutboxRow = (row: OutboxRow): ParsedChange | undefined => {\n\tif (typeof row.tbl !== 'string') {\n\t\treturn undefined;\n\t}\n\tconst op = OP_BY_NAME[row.op];\n\tif (op === undefined) {\n\t\treturn undefined;\n\t}\n\tlet payload: unknown = row.payload;\n\tif (typeof payload === 'string') {\n\t\ttry {\n\t\t\tpayload = JSON.parse(payload);\n\t\t} catch {\n\t\t\treturn undefined;\n\t\t}\n\t}\n\tif (typeof payload !== 'object' || payload === null) {\n\t\treturn undefined;\n\t}\n\treturn { table: row.tbl, change: { op, row: payload } };\n};\n\nexport type PollingChangeSourceOptions = {\n\t/**\n\t * Fetch changelog rows with `seq > sinceSeq`, ordered by `seq` ascending.\n\t * e.g. `(since) => sql\\`SELECT seq, tbl, op, payload FROM absolute_sync_changelog WHERE seq > ${since} ORDER BY seq\\``.\n\t */\n\tpoll: (sinceSeq: number) => Promise<OutboxRow[]> | OutboxRow[];\n\t/** Poll interval in ms. Defaults to 1000. */\n\tintervalMs?: number;\n\t/** Resume cursor (highest `seq` already processed). Defaults to 0. */\n\tstartSeq?: number;\n\t/** Override the row parser (defaults to {@link parseOutboxRow}). */\n\tparse?: (row: OutboxRow) => ParsedChange | undefined;\n\t/** Called after a non-empty batch with the new watermark — prune here. */\n\tonProcessed?: (uptoSeq: number) => void | Promise<void>;\n\t/** Called if a poll throws (the loop keeps running). Defaults to a warning. */\n\tonError?: (error: unknown) => void;\n};\n\n/**\n * Create a polling {@link ChangeSource} over a changelog table. Connect it with\n * `engine.connectSource(...)`; `start` runs an immediate first poll (draining any\n * backlog) and then polls every `intervalMs` until `stop`.\n */\nexport const createPollingChangeSource = (\n\toptions: PollingChangeSourceOptions\n): ChangeSource => {\n\tconst intervalMs = options.intervalMs ?? 1000;\n\tconst parse = options.parse ?? parseOutboxRow;\n\tconst onError =\n\t\toptions.onError ??\n\t\t((error: unknown) => {\n\t\t\tconsole.warn('[sync] polling change source error:', error);\n\t\t});\n\tlet cursor = options.startSeq ?? 0;\n\tlet running = false;\n\tlet timer: ReturnType<typeof setTimeout> | undefined;\n\n\tconst tick = async (emit: EmitChange): Promise<void> => {\n\t\tif (!running) {\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tconst rows = await options.poll(cursor);\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst parsed = parse(row);\n\t\t\t\tif (parsed !== undefined) {\n\t\t\t\t\tawait emit(parsed.table, parsed.change);\n\t\t\t\t}\n\t\t\t\tif (typeof row.seq === 'number' && row.seq > cursor) {\n\t\t\t\t\tcursor = row.seq;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (rows.length > 0) {\n\t\t\t\tawait options.onProcessed?.(cursor);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tonError(error);\n\t\t}\n\t\tif (running) {\n\t\t\ttimer = setTimeout(() => {\n\t\t\t\tvoid tick(emit);\n\t\t\t}, intervalMs);\n\t\t}\n\t};\n\n\treturn {\n\t\tstart: async (emit) => {\n\t\t\tif (running) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trunning = true;\n\t\t\tawait tick(emit);\n\t\t},\n\t\tstop: () => {\n\t\t\trunning = false;\n\t\t\tif (timer !== undefined) {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\ttimer = undefined;\n\t\t\t}\n\t\t}\n\t};\n};\n",
6
+ "import type { ChangeSource, ParsedChange, RowOp } from '../../engine/types';\n\n/**\n * MySQL CDC adapter for @absolutejs/sync (Tier 3, M5).\n *\n * MySQL has no `LISTEN/NOTIFY`, so two pluggable strategies catch out-of-band\n * writes — both behind the engine's {@link ChangeSource} seam:\n *\n * - **Changelog + poll (portable).** Install triggers with\n * {@link mysqlChangelogSchema} and tail the changelog with\n * {@link createPollingChangeSource}. Works anywhere, no extra privileges.\n * - **Binlog (higher throughput).** Wire a binlog reader (e.g. zongji) into\n * {@link mysqlBinlogChangeSource}; it normalizes row events into the change\n * feed. Catches writes without the per-write changelog overhead.\n *\n * Dependency-free — you bring the MySQL client / binlog reader.\n */\n\nexport {\n\tcreatePollingChangeSource,\n\tparseOutboxRow\n} from '../../engine/pollingSource';\nexport type {\n\tOutboxRow,\n\tPollingChangeSourceOptions\n} from '../../engine/pollingSource';\n\nconst DEFAULT_CHANGELOG = 'absolute_sync_changelog';\nconst DEFAULT_PREFIX = 'absolute_sync';\nconst OPS = ['insert', 'update', 'delete'] as const;\n\nexport type MysqlChangelogOptions = {\n\t/** Table name → the column names to capture in the change payload. */\n\ttables: Record<string, string[]>;\n\t/** Changelog table name. Defaults to `absolute_sync_changelog`. */\n\tchangelogTable?: string;\n\t/** Trigger name prefix. Defaults to `absolute_sync`. */\n\tprefix?: string;\n};\n\n/**\n * Generate the SQL that installs the changelog table and per-table\n * insert/update/delete triggers — run it once (e.g. in a migration). Each\n * trigger appends `{ tbl, op, payload }` (payload built with `JSON_OBJECT` from\n * the listed columns) to the changelog for {@link createPollingChangeSource}.\n *\n * Each `CREATE TRIGGER` body is a single statement (no `DELIMITER` needed). The\n * statements are `;`-separated; run them as a script, or split on `;` if your\n * driver executes one statement per call.\n */\nexport const mysqlChangelogSchema = (\n\toptions: MysqlChangelogOptions\n): string => {\n\tconst changelog = options.changelogTable ?? DEFAULT_CHANGELOG;\n\tconst prefix = options.prefix ?? DEFAULT_PREFIX;\n\n\tconst createTable = [\n\t\t`CREATE TABLE IF NOT EXISTS \\`${changelog}\\` (`,\n\t\t'\\tseq BIGINT AUTO_INCREMENT PRIMARY KEY,',\n\t\t'\\ttbl VARCHAR(255) NOT NULL,',\n\t\t'\\top VARCHAR(16) NOT NULL,',\n\t\t'\\tpayload JSON NOT NULL,',\n\t\t'\\tcreated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP',\n\t\t');'\n\t].join('\\n');\n\n\tconst jsonObject = (columns: string[], ref: 'NEW' | 'OLD'): string =>\n\t\t`JSON_OBJECT(${columns\n\t\t\t.map((column) => `'${column}', ${ref}.\\`${column}\\``)\n\t\t\t.join(', ')})`;\n\n\tconst triggers = Object.entries(options.tables).flatMap(\n\t\t([table, columns]) =>\n\t\t\tOPS.map((op) => {\n\t\t\t\tconst ref = op === 'delete' ? 'OLD' : 'NEW';\n\t\t\t\tconst name = `${prefix}_${table}_${op}`;\n\t\t\t\treturn [\n\t\t\t\t\t`DROP TRIGGER IF EXISTS \\`${name}\\`;`,\n\t\t\t\t\t`CREATE TRIGGER \\`${name}\\` AFTER ${op.toUpperCase()} ON \\`${table}\\` FOR EACH ROW`,\n\t\t\t\t\t`INSERT INTO \\`${changelog}\\` (tbl, op, payload)`,\n\t\t\t\t\t`VALUES ('${table}', '${op}', ${jsonObject(columns, ref)});`\n\t\t\t\t].join('\\n');\n\t\t\t})\n\t);\n\n\treturn [createTable, ...triggers].join('\\n\\n');\n};\n\nconst BINLOG_OP: Record<string, RowOp> = {\n\twriterows: 'insert',\n\tupdaterows: 'update',\n\tdeleterows: 'delete'\n};\n\n/** A row-level binlog event (the subset {@link normalizeBinlogEvent} needs). */\nexport type BinlogRowEvent = {\n\t/**\n\t * Event kind — zongji's `writerows` / `updaterows` / `deleterows`\n\t * (case-insensitive).\n\t */\n\ttype: string;\n\t/** The affected table. */\n\ttable: string;\n\t/**\n\t * Affected rows. Insert/delete: each entry is the row object. Update: each\n\t * entry is `{ before, after }` (zongji's shape).\n\t */\n\trows: unknown[];\n};\n\n/**\n * Normalize a binlog row event into engine changes — one per affected row. For\n * updates it takes the `after` image; for deletes the row (or its `before`\n * image). Rows that aren't objects are skipped. Pure, so it's easy to test and\n * to swap for a different reader's shape.\n */\nexport const normalizeBinlogEvent = (event: BinlogRowEvent): ParsedChange[] => {\n\tconst op = BINLOG_OP[event.type.toLowerCase()];\n\tif (op === undefined || typeof event.table !== 'string') {\n\t\treturn [];\n\t}\n\tconst changes: ParsedChange[] = [];\n\tfor (const entry of event.rows) {\n\t\tlet row: unknown = entry;\n\t\tif (entry !== null && typeof entry === 'object') {\n\t\t\tif (op === 'update' && 'after' in entry) {\n\t\t\t\trow = (entry as { after: unknown }).after;\n\t\t\t} else if (op === 'delete' && 'before' in entry) {\n\t\t\t\trow = (entry as { before: unknown }).before;\n\t\t\t}\n\t\t}\n\t\tif (typeof row === 'object' && row !== null) {\n\t\t\tchanges.push({ table: event.table, change: { op, row } });\n\t\t}\n\t}\n\treturn changes;\n};\n\nexport type MysqlBinlogChangeSourceOptions = {\n\t/**\n\t * Subscribe to row events from a binlog reader; return a function that stops\n\t * it. e.g. with zongji:\n\t * `(onEvent) => { zongji.on('binlog', e => onEvent({ type: e.getEventName(), table: e.tableMap[e.tableId].tableName, rows: e.rows })); zongji.start(...); return () => zongji.stop(); }`.\n\t */\n\tsubscribe: (\n\t\tonEvent: (event: BinlogRowEvent) => void\n\t) => Promise<() => void | Promise<void>> | (() => void | Promise<void>);\n\t/** Override the event normalizer (defaults to {@link normalizeBinlogEvent}). */\n\tnormalize?: (event: BinlogRowEvent) => ParsedChange[];\n};\n\n/**\n * A {@link ChangeSource} backed by the MySQL binlog. Each row event is\n * normalized and emitted into the engine. Connect with\n * `engine.connectSource(...)`.\n */\nexport const mysqlBinlogChangeSource = (\n\toptions: MysqlBinlogChangeSourceOptions\n): ChangeSource => {\n\tconst normalize = options.normalize ?? normalizeBinlogEvent;\n\tlet unsubscribe: (() => void | Promise<void>) | undefined;\n\n\treturn {\n\t\tstart: async (emit) => {\n\t\t\tunsubscribe = await options.subscribe((event) => {\n\t\t\t\tfor (const parsed of normalize(event)) {\n\t\t\t\t\tvoid emit(parsed.table, parsed.change);\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tstop: async () => {\n\t\t\tawait unsubscribe?.();\n\t\t\tunsubscribe = undefined;\n\t\t}\n\t};\n};\n"
7
+ ],
8
+ "mappings": "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",
9
+ "debugId": "48EE1E12A50EB47064756E2164756E21",
10
+ "names": []
11
+ }
@@ -0,0 +1,53 @@
1
+ import type { ChangeSource, ParsedChange } from '../../engine/types';
2
+ /** A parsed change ready to feed the engine. */
3
+ export type ParsedNotification = ParsedChange;
4
+ /**
5
+ * Default NOTIFY-payload parser: expects the JSON the trigger from
6
+ * {@link postgresNotifyTrigger} sends — `{ table, op, row }` where `op` is
7
+ * `INSERT`/`UPDATE`/`DELETE`. Returns `undefined` for anything malformed so a
8
+ * bad payload is skipped rather than throwing.
9
+ */
10
+ export declare const parseNotification: (payload: string) => ParsedNotification | undefined;
11
+ export type PostgresChangeSourceOptions = {
12
+ /**
13
+ * Subscribe to a Postgres NOTIFY channel; return a function that stops
14
+ * listening. The wiring is yours, e.g. with porsager/postgres:
15
+ * `(channel, onNotify) => { const s = await sql.listen(channel, onNotify); return s.unlisten; }`.
16
+ */
17
+ listen: (channel: string, onNotify: (payload: string) => void) => Promise<() => void | Promise<void>> | (() => void | Promise<void>);
18
+ /** NOTIFY channel; must match the trigger's. Defaults to `absolute_sync`. */
19
+ channel?: string;
20
+ /** Override the payload parser (defaults to {@link parseNotification}). */
21
+ parse?: (payload: string) => ParsedNotification | undefined;
22
+ };
23
+ /**
24
+ * A {@link ChangeSource} backed by Postgres `LISTEN/NOTIFY`. Each notification
25
+ * is parsed to `(table, change)` and emitted into the engine.
26
+ *
27
+ * @example
28
+ * const disconnect = await engine.connectSource(
29
+ * postgresChangeSource({
30
+ * listen: async (channel, onNotify) =>
31
+ * (await sql.listen(channel, onNotify)).unlisten
32
+ * })
33
+ * );
34
+ */
35
+ export declare const postgresChangeSource: (options: PostgresChangeSourceOptions) => ChangeSource;
36
+ export type PostgresNotifyTriggerOptions = {
37
+ /** Tables to emit changes for. */
38
+ tables: string[];
39
+ /** NOTIFY channel; must match the change source's. Defaults to `absolute_sync`. */
40
+ channel?: string;
41
+ /** Trigger function name. Defaults to `absolute_sync_notify`. */
42
+ functionName?: string;
43
+ };
44
+ /**
45
+ * Generate the SQL that installs a NOTIFY trigger on each table — run it once
46
+ * (e.g. in a migration). On every insert/update/delete it sends
47
+ * `{ table, op, row }` JSON on the channel for {@link postgresChangeSource}.
48
+ *
49
+ * Note: `pg_notify` payloads are capped at 8000 bytes, so very wide rows can be
50
+ * truncated (the parser then skips them). For large rows or high throughput,
51
+ * prefer a logical-replication source behind the same {@link ChangeSource} seam.
52
+ */
53
+ export declare const postgresNotifyTrigger: (options: PostgresNotifyTriggerOptions) => string;
@@ -0,0 +1,86 @@
1
+ // @bun
2
+ // src/adapters/postgres/index.ts
3
+ var DEFAULT_CHANNEL = "absolute_sync";
4
+ var DEFAULT_FUNCTION = "absolute_sync_notify";
5
+ var OP_BY_TG = {
6
+ INSERT: "insert",
7
+ UPDATE: "update",
8
+ DELETE: "delete"
9
+ };
10
+ var parseNotification = (payload) => {
11
+ let data;
12
+ try {
13
+ data = JSON.parse(payload);
14
+ } catch {
15
+ return;
16
+ }
17
+ if (typeof data !== "object" || data === null) {
18
+ return;
19
+ }
20
+ const { table, op, row } = data;
21
+ if (typeof table !== "string") {
22
+ return;
23
+ }
24
+ const rowOp = typeof op === "string" ? OP_BY_TG[op] : undefined;
25
+ if (rowOp === undefined) {
26
+ return;
27
+ }
28
+ if (typeof row !== "object" || row === null) {
29
+ return;
30
+ }
31
+ return { table, change: { op: rowOp, row } };
32
+ };
33
+ var postgresChangeSource = (options) => {
34
+ const channel = options.channel ?? DEFAULT_CHANNEL;
35
+ const parse = options.parse ?? parseNotification;
36
+ let unlisten;
37
+ return {
38
+ start: async (emit) => {
39
+ unlisten = await options.listen(channel, (payload) => {
40
+ const parsed = parse(payload);
41
+ if (parsed !== undefined) {
42
+ emit(parsed.table, parsed.change);
43
+ }
44
+ });
45
+ },
46
+ stop: async () => {
47
+ await unlisten?.();
48
+ unlisten = undefined;
49
+ }
50
+ };
51
+ };
52
+ var postgresNotifyTrigger = (options) => {
53
+ const channel = options.channel ?? DEFAULT_CHANNEL;
54
+ const fn = options.functionName ?? DEFAULT_FUNCTION;
55
+ const functionSql = [
56
+ `CREATE OR REPLACE FUNCTION ${fn}() RETURNS trigger AS $$`,
57
+ "BEGIN",
58
+ ` PERFORM pg_notify('${channel}', json_build_object(`,
59
+ ` 'table', TG_TABLE_NAME,`,
60
+ ` 'op', TG_OP,`,
61
+ ` 'row', row_to_json(COALESCE(NEW, OLD))`,
62
+ " )::text);",
63
+ " RETURN COALESCE(NEW, OLD);",
64
+ "END;",
65
+ "$$ LANGUAGE plpgsql;"
66
+ ].join(`
67
+ `);
68
+ const triggerSql = options.tables.map((table) => [
69
+ `DROP TRIGGER IF EXISTS ${fn}_${table} ON ${table};`,
70
+ `CREATE TRIGGER ${fn}_${table}`,
71
+ `AFTER INSERT OR UPDATE OR DELETE ON ${table}`,
72
+ `FOR EACH ROW EXECUTE FUNCTION ${fn}();`
73
+ ].join(`
74
+ `));
75
+ return [functionSql, ...triggerSql].join(`
76
+
77
+ `);
78
+ };
79
+ export {
80
+ postgresNotifyTrigger,
81
+ postgresChangeSource,
82
+ parseNotification
83
+ };
84
+
85
+ //# debugId=0625BCD90123273B64756E2164756E21
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/adapters/postgres/index.ts"],
4
+ "sourcesContent": [
5
+ "import type { ChangeSource, ParsedChange, RowOp } from '../../engine/types';\n\n/**\n * Postgres CDC adapter for @absolutejs/sync (Tier 3, M5).\n *\n * Catches writes that didn't go through the mutation API by turning Postgres\n * `LISTEN/NOTIFY` into the engine's change feed. Install the triggers once with\n * {@link postgresNotifyTrigger}, then connect {@link postgresChangeSource} via\n * `engine.connectSource(...)`.\n *\n * Client-agnostic: you supply how to `listen` on a channel, so it works with\n * porsager/postgres (`sql.listen`), node-postgres, or `Bun.sql` — and the\n * adapter itself has no database dependency.\n */\n\nconst DEFAULT_CHANNEL = 'absolute_sync';\nconst DEFAULT_FUNCTION = 'absolute_sync_notify';\n\nconst OP_BY_TG: Record<string, RowOp> = {\n\tINSERT: 'insert',\n\tUPDATE: 'update',\n\tDELETE: 'delete'\n};\n\n/** A parsed change ready to feed the engine. */\nexport type ParsedNotification = ParsedChange;\n\n/**\n * Default NOTIFY-payload parser: expects the JSON the trigger from\n * {@link postgresNotifyTrigger} sends — `{ table, op, row }` where `op` is\n * `INSERT`/`UPDATE`/`DELETE`. Returns `undefined` for anything malformed so a\n * bad payload is skipped rather than throwing.\n */\nexport const parseNotification = (\n\tpayload: string\n): ParsedNotification | undefined => {\n\tlet data: unknown;\n\ttry {\n\t\tdata = JSON.parse(payload);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (typeof data !== 'object' || data === null) {\n\t\treturn undefined;\n\t}\n\tconst { table, op, row } = data as {\n\t\ttable?: unknown;\n\t\top?: unknown;\n\t\trow?: unknown;\n\t};\n\tif (typeof table !== 'string') {\n\t\treturn undefined;\n\t}\n\tconst rowOp = typeof op === 'string' ? OP_BY_TG[op] : undefined;\n\tif (rowOp === undefined) {\n\t\treturn undefined;\n\t}\n\tif (typeof row !== 'object' || row === null) {\n\t\treturn undefined;\n\t}\n\treturn { table, change: { op: rowOp, row } };\n};\n\nexport type PostgresChangeSourceOptions = {\n\t/**\n\t * Subscribe to a Postgres NOTIFY channel; return a function that stops\n\t * listening. The wiring is yours, e.g. with porsager/postgres:\n\t * `(channel, onNotify) => { const s = await sql.listen(channel, onNotify); return s.unlisten; }`.\n\t */\n\tlisten: (\n\t\tchannel: string,\n\t\tonNotify: (payload: string) => void\n\t) => Promise<() => void | Promise<void>> | (() => void | Promise<void>);\n\t/** NOTIFY channel; must match the trigger's. Defaults to `absolute_sync`. */\n\tchannel?: string;\n\t/** Override the payload parser (defaults to {@link parseNotification}). */\n\tparse?: (payload: string) => ParsedNotification | undefined;\n};\n\n/**\n * A {@link ChangeSource} backed by Postgres `LISTEN/NOTIFY`. Each notification\n * is parsed to `(table, change)` and emitted into the engine.\n *\n * @example\n * const disconnect = await engine.connectSource(\n * postgresChangeSource({\n * listen: async (channel, onNotify) =>\n * (await sql.listen(channel, onNotify)).unlisten\n * })\n * );\n */\nexport const postgresChangeSource = (\n\toptions: PostgresChangeSourceOptions\n): ChangeSource => {\n\tconst channel = options.channel ?? DEFAULT_CHANNEL;\n\tconst parse = options.parse ?? parseNotification;\n\tlet unlisten: (() => void | Promise<void>) | undefined;\n\n\treturn {\n\t\tstart: async (emit) => {\n\t\t\tunlisten = await options.listen(channel, (payload) => {\n\t\t\t\tconst parsed = parse(payload);\n\t\t\t\tif (parsed !== undefined) {\n\t\t\t\t\tvoid emit(parsed.table, parsed.change);\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\tstop: async () => {\n\t\t\tawait unlisten?.();\n\t\t\tunlisten = undefined;\n\t\t}\n\t};\n};\n\nexport type PostgresNotifyTriggerOptions = {\n\t/** Tables to emit changes for. */\n\ttables: string[];\n\t/** NOTIFY channel; must match the change source's. Defaults to `absolute_sync`. */\n\tchannel?: string;\n\t/** Trigger function name. Defaults to `absolute_sync_notify`. */\n\tfunctionName?: string;\n};\n\n/**\n * Generate the SQL that installs a NOTIFY trigger on each table — run it once\n * (e.g. in a migration). On every insert/update/delete it sends\n * `{ table, op, row }` JSON on the channel for {@link postgresChangeSource}.\n *\n * Note: `pg_notify` payloads are capped at 8000 bytes, so very wide rows can be\n * truncated (the parser then skips them). For large rows or high throughput,\n * prefer a logical-replication source behind the same {@link ChangeSource} seam.\n */\nexport const postgresNotifyTrigger = (\n\toptions: PostgresNotifyTriggerOptions\n): string => {\n\tconst channel = options.channel ?? DEFAULT_CHANNEL;\n\tconst fn = options.functionName ?? DEFAULT_FUNCTION;\n\n\tconst functionSql = [\n\t\t`CREATE OR REPLACE FUNCTION ${fn}() RETURNS trigger AS $$`,\n\t\t'BEGIN',\n\t\t` PERFORM pg_notify('${channel}', json_build_object(`,\n\t\t` 'table', TG_TABLE_NAME,`,\n\t\t` 'op', TG_OP,`,\n\t\t` 'row', row_to_json(COALESCE(NEW, OLD))`,\n\t\t' )::text);',\n\t\t' RETURN COALESCE(NEW, OLD);',\n\t\t'END;',\n\t\t'$$ LANGUAGE plpgsql;'\n\t].join('\\n');\n\n\tconst triggerSql = options.tables.map((table) =>\n\t\t[\n\t\t\t`DROP TRIGGER IF EXISTS ${fn}_${table} ON ${table};`,\n\t\t\t`CREATE TRIGGER ${fn}_${table}`,\n\t\t\t`AFTER INSERT OR UPDATE OR DELETE ON ${table}`,\n\t\t\t`FOR EACH ROW EXECUTE FUNCTION ${fn}();`\n\t\t].join('\\n')\n\t);\n\n\treturn [functionSql, ...triggerSql].join('\\n\\n');\n};\n"
6
+ ],
7
+ "mappings": ";;AAeA,IAAM,kBAAkB;AACxB,IAAM,mBAAmB;AAEzB,IAAM,WAAkC;AAAA,EACvC,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,QAAQ;AACT;AAWO,IAAM,oBAAoB,CAChC,YACoC;AAAA,EACpC,IAAI;AAAA,EACJ,IAAI;AAAA,IACH,OAAO,KAAK,MAAM,OAAO;AAAA,IACxB,MAAM;AAAA,IACP;AAAA;AAAA,EAED,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAAA,IAC9C;AAAA,EACD;AAAA,EACA,QAAQ,OAAO,IAAI,QAAQ;AAAA,EAK3B,IAAI,OAAO,UAAU,UAAU;AAAA,IAC9B;AAAA,EACD;AAAA,EACA,MAAM,QAAQ,OAAO,OAAO,WAAW,SAAS,MAAM;AAAA,EACtD,IAAI,UAAU,WAAW;AAAA,IACxB;AAAA,EACD;AAAA,EACA,IAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAAA,IAC5C;AAAA,EACD;AAAA,EACA,OAAO,EAAE,OAAO,QAAQ,EAAE,IAAI,OAAO,IAAI,EAAE;AAAA;AA+BrC,IAAM,uBAAuB,CACnC,YACkB;AAAA,EAClB,MAAM,UAAU,QAAQ,WAAW;AAAA,EACnC,MAAM,QAAQ,QAAQ,SAAS;AAAA,EAC/B,IAAI;AAAA,EAEJ,OAAO;AAAA,IACN,OAAO,OAAO,SAAS;AAAA,MACtB,WAAW,MAAM,QAAQ,OAAO,SAAS,CAAC,YAAY;AAAA,QACrD,MAAM,SAAS,MAAM,OAAO;AAAA,QAC5B,IAAI,WAAW,WAAW;AAAA,UACpB,KAAK,OAAO,OAAO,OAAO,MAAM;AAAA,QACtC;AAAA,OACA;AAAA;AAAA,IAEF,MAAM,YAAY;AAAA,MACjB,MAAM,WAAW;AAAA,MACjB,WAAW;AAAA;AAAA,EAEb;AAAA;AAqBM,IAAM,wBAAwB,CACpC,YACY;AAAA,EACZ,MAAM,UAAU,QAAQ,WAAW;AAAA,EACnC,MAAM,KAAK,QAAQ,gBAAgB;AAAA,EAEnC,MAAM,cAAc;AAAA,IACnB,8BAA8B;AAAA,IAC9B;AAAA,IACA,wBAAwB;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD,EAAE,KAAK;AAAA,CAAI;AAAA,EAEX,MAAM,aAAa,QAAQ,OAAO,IAAI,CAAC,UACtC;AAAA,IACC,0BAA0B,MAAM,YAAY;AAAA,IAC5C,kBAAkB,MAAM;AAAA,IACxB,uCAAuC;AAAA,IACvC,iCAAiC;AAAA,EAClC,EAAE,KAAK;AAAA,CAAI,CACZ;AAAA,EAEA,OAAO,CAAC,aAAa,GAAG,UAAU,EAAE,KAAK;AAAA;AAAA,CAAM;AAAA;",
8
+ "debugId": "0625BCD90123273B64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,39 @@
1
+ import type { CollectionContext, CollectionDefinition } from '../../engine/collection';
2
+ import type { RowKey } from '../../engine/types';
3
+ import type { PrismaWhere } from './topics';
4
+ export type PrismaCollectionOptions<T, P, Ctx> = {
5
+ /** Collection name (change-feed key and topic root). */
6
+ name: string;
7
+ /**
8
+ * The query filter, written once. Used both to hydrate from the database
9
+ * (passed to `find`) and, mirrored in JS, to match changed rows
10
+ * incrementally. Receives the subscription's params and context.
11
+ */
12
+ where: (params: P, ctx: Ctx) => PrismaWhere;
13
+ /**
14
+ * Run the database read for a given `where` — your Prisma call, e.g.
15
+ * `(where) => prisma.order.findMany({ where })`.
16
+ */
17
+ find: (where: PrismaWhere, params: P, ctx: Ctx) => Promise<Iterable<T>> | Iterable<T>;
18
+ /** Row identity. Defaults to `row.id`. */
19
+ key?: (row: T) => RowKey;
20
+ /** Access control; return false (or throw) to deny. */
21
+ authorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;
22
+ };
23
+ /**
24
+ * Build a syncable {@link CollectionDefinition} for Prisma from a single filter:
25
+ * `where` is written once and powers both `hydrate` (via your `find`) and the
26
+ * incremental `match` (via {@link matchesWhere}) — no restating the WHERE.
27
+ *
28
+ * If the filter uses an operator the JS matcher can't evaluate, that change
29
+ * degrades to a refetch (handled by the engine), so the result stays correct.
30
+ *
31
+ * @example
32
+ * prismaCollection({
33
+ * name: 'orders',
34
+ * where: (p) => ({ userId: p.userId, status: 'open' }),
35
+ * find: (where) => prisma.order.findMany({ where }),
36
+ * authorize: (p, ctx) => p.userId === ctx.userId
37
+ * });
38
+ */
39
+ export declare const prismaCollection: <T, P = void, Ctx = CollectionContext>(options: PrismaCollectionOptions<T, P, Ctx>) => CollectionDefinition<T, P, Ctx>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Prisma adapter for @absolutejs/sync (Tier 2 — ORM auto-reactivity).
3
+ *
4
+ * The Prisma counterpart of the Drizzle adapter: derive the topics a query
5
+ * depends on ({@link deriveReadTopics}) and publish the topics a mutation
6
+ * invalidates ({@link publishChange} and friends). It identifies a model by name
7
+ * and reads Prisma's plain `where` objects and result records, so it needs no
8
+ * `@prisma/client` import and works on every database Prisma supports.
9
+ *
10
+ * Use the SAME model identifier on the read and write sides — the topic is the
11
+ * model name verbatim (e.g. `'user'` -> topic `user`, row topic `user:5`).
12
+ * Granularity is table-level by default, narrowing to a single row when a filter
13
+ * is a simple key-field equality.
14
+ */
15
+ export { keyTopic, tableTopic } from './topics';
16
+ export type { PrismaWhere, RowKey } from './topics';
17
+ export { deriveReadTopics } from './read';
18
+ export type { DeriveReadTopicsOptions, DerivedReadTopics } from './read';
19
+ export { publishChange, publishRows, publishWhere } from './write';
20
+ export type { ChangeOp, ChangePayload, PublishChangeOptions, PublishRowsOptions, PublishWhereOptions } from './write';
21
+ export { matchesWhere, UnsupportedFilterError } from './predicate';
22
+ export { prismaCollection } from './collection';
23
+ export type { PrismaCollectionOptions } from './collection';