@absolutejs/sync 0.0.1 → 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 +281 -24
- package/dist/adapters/drizzle/collection.d.ts +27 -0
- package/dist/adapters/drizzle/index.d.ts +20 -0
- package/dist/adapters/drizzle/index.js +265 -0
- package/dist/adapters/drizzle/index.js.map +14 -0
- package/dist/adapters/drizzle/predicate.d.ts +20 -0
- package/dist/adapters/drizzle/read.d.ts +31 -0
- package/dist/adapters/drizzle/topics.d.ts +41 -0
- package/dist/adapters/drizzle/write.d.ts +69 -0
- package/dist/adapters/mysql/index.d.ts +75 -0
- package/dist/adapters/mysql/index.js +171 -0
- package/dist/adapters/mysql/index.js.map +11 -0
- package/dist/adapters/postgres/index.d.ts +53 -0
- package/dist/adapters/postgres/index.js +86 -0
- package/dist/adapters/postgres/index.js.map +10 -0
- package/dist/adapters/prisma/collection.d.ts +39 -0
- package/dist/adapters/prisma/index.d.ts +23 -0
- package/dist/adapters/prisma/index.js +231 -0
- package/dist/adapters/prisma/index.js.map +14 -0
- package/dist/adapters/prisma/predicate.d.ts +20 -0
- package/dist/adapters/prisma/read.d.ts +28 -0
- package/dist/adapters/prisma/topics.d.ts +29 -0
- package/dist/adapters/prisma/write.d.ts +65 -0
- package/dist/adapters/sqlite/index.d.ts +32 -0
- package/dist/adapters/sqlite/index.js +128 -0
- package/dist/adapters/sqlite/index.js.map +11 -0
- package/dist/angular/index.d.ts +1 -0
- package/dist/angular/index.js +347 -0
- package/dist/angular/index.js.map +11 -0
- package/dist/angular/sync-collection.service.d.ts +20 -0
- package/dist/client/index.d.ts +12 -30
- package/dist/client/index.js +1099 -3
- package/dist/client/index.js.map +10 -4
- package/dist/client/liveQuery.d.ts +75 -0
- package/dist/client/presence.d.ts +37 -0
- package/dist/client/subscriber.d.ts +30 -0
- package/dist/client/syncClient.d.ts +53 -0
- package/dist/client/syncCollection.d.ts +102 -0
- package/dist/client/syncStore.d.ts +81 -0
- package/dist/engine/aggregate.d.ts +45 -0
- package/dist/engine/cluster.d.ts +41 -0
- package/dist/engine/collection.d.ts +87 -0
- package/dist/engine/connection.d.ts +103 -0
- package/dist/engine/dataflow.d.ts +109 -0
- package/dist/engine/equiJoin.d.ts +51 -0
- package/dist/engine/graph.d.ts +85 -0
- package/dist/engine/index.d.ts +40 -0
- package/dist/engine/index.js +1774 -0
- package/dist/engine/index.js.map +23 -0
- package/dist/engine/materializedView.d.ts +53 -0
- package/dist/engine/mutation.d.ts +66 -0
- package/dist/engine/pollingSource.d.ts +42 -0
- package/dist/engine/presence.d.ts +46 -0
- package/dist/engine/reactive.d.ts +67 -0
- package/dist/engine/routes.d.ts +40 -0
- package/dist/engine/socket.d.ts +67 -0
- package/dist/engine/syncEngine.d.ts +132 -0
- package/dist/engine/types.d.ts +45 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +327 -3
- package/dist/index.js.map +8 -5
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +332 -0
- package/dist/react/index.js.map +11 -0
- package/dist/react/useSyncCollection.d.ts +16 -0
- package/dist/reactiveHub.d.ts +6 -0
- package/dist/svelte/createSyncCollectionStore.d.ts +15 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +338 -0
- package/dist/svelte/index.js.map +11 -0
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +331 -0
- package/dist/vue/index.js.map +11 -0
- package/dist/vue/useSyncCollection.d.ts +17 -0
- package/package.json +104 -6
|
@@ -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';
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/adapters/prisma/topics.ts
|
|
3
|
+
var tableTopic = (model) => model;
|
|
4
|
+
var keyTopic = (model, key) => `${model}:${key}`;
|
|
5
|
+
var isRowKey = (value) => typeof value === "string" || typeof value === "number" || typeof value === "bigint";
|
|
6
|
+
var extractKeyFromWhere = (where, keyField) => {
|
|
7
|
+
const fields = Object.keys(where);
|
|
8
|
+
if (fields.length !== 1 || fields[0] !== keyField) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const condition = where[keyField];
|
|
12
|
+
if (isRowKey(condition)) {
|
|
13
|
+
return condition;
|
|
14
|
+
}
|
|
15
|
+
if (condition !== null && typeof condition === "object" && !Array.isArray(condition)) {
|
|
16
|
+
const operators = Object.keys(condition);
|
|
17
|
+
if (operators.length === 1 && operators[0] === "equals") {
|
|
18
|
+
const value = condition.equals;
|
|
19
|
+
if (isRowKey(value)) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
};
|
|
26
|
+
var extractRowKeys = (rows, keyField) => {
|
|
27
|
+
const keys = [];
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const value = row[keyField];
|
|
30
|
+
if (isRowKey(value)) {
|
|
31
|
+
keys.push(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return keys;
|
|
35
|
+
};
|
|
36
|
+
// src/adapters/prisma/read.ts
|
|
37
|
+
var deriveReadTopics = (model, where, options = {}) => {
|
|
38
|
+
const keyField = options.keyField ?? "id";
|
|
39
|
+
const key = where === undefined ? undefined : extractKeyFromWhere(where, keyField);
|
|
40
|
+
if (key === undefined) {
|
|
41
|
+
return { topics: [tableTopic(model)], rowLevel: false };
|
|
42
|
+
}
|
|
43
|
+
return { topics: [keyTopic(model, key)], rowLevel: true };
|
|
44
|
+
};
|
|
45
|
+
// src/adapters/prisma/write.ts
|
|
46
|
+
var publishChange = (hub, model, options = {}) => {
|
|
47
|
+
const name = tableTopic(model);
|
|
48
|
+
const keys = options.keys === undefined ? [] : [...new Set(options.keys)];
|
|
49
|
+
const payload = { table: name, op: options.op, keys };
|
|
50
|
+
const topics = [
|
|
51
|
+
...new Set([name, ...keys.map((key) => keyTopic(model, key))])
|
|
52
|
+
];
|
|
53
|
+
for (const topic of topics) {
|
|
54
|
+
hub.publish(topic, payload);
|
|
55
|
+
}
|
|
56
|
+
return topics;
|
|
57
|
+
};
|
|
58
|
+
var publishRows = (hub, model, rows, options = {}) => {
|
|
59
|
+
const list = Array.isArray(rows) ? rows : [rows];
|
|
60
|
+
return publishChange(hub, model, {
|
|
61
|
+
keys: extractRowKeys(list, options.keyField ?? "id"),
|
|
62
|
+
op: options.op
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
var publishWhere = (hub, model, where, options = {}) => {
|
|
66
|
+
const key = extractKeyFromWhere(where, options.keyField ?? "id");
|
|
67
|
+
return publishChange(hub, model, {
|
|
68
|
+
keys: key === undefined ? [] : [key],
|
|
69
|
+
op: options.op
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
// src/adapters/prisma/predicate.ts
|
|
73
|
+
class UnsupportedFilterError extends Error {
|
|
74
|
+
constructor(operator) {
|
|
75
|
+
super(`Cannot evaluate Prisma filter operator "${operator}" incrementally`);
|
|
76
|
+
this.name = "UnsupportedFilterError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
80
|
+
var equals = (value, operand) => {
|
|
81
|
+
if (operand === null) {
|
|
82
|
+
return value === null || value === undefined;
|
|
83
|
+
}
|
|
84
|
+
if (value instanceof Date && operand instanceof Date) {
|
|
85
|
+
return value.getTime() === operand.getTime();
|
|
86
|
+
}
|
|
87
|
+
return value === operand;
|
|
88
|
+
};
|
|
89
|
+
var order = (value) => value instanceof Date ? value.getTime() : value;
|
|
90
|
+
var compare = (value, operand) => {
|
|
91
|
+
const a = order(value);
|
|
92
|
+
const b = order(operand);
|
|
93
|
+
if (a < b) {
|
|
94
|
+
return -1;
|
|
95
|
+
}
|
|
96
|
+
if (a > b) {
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
};
|
|
101
|
+
var comparable = (value) => value !== null && value !== undefined;
|
|
102
|
+
var FIELD_OPERATORS = new Set([
|
|
103
|
+
"equals",
|
|
104
|
+
"not",
|
|
105
|
+
"in",
|
|
106
|
+
"notIn",
|
|
107
|
+
"lt",
|
|
108
|
+
"lte",
|
|
109
|
+
"gt",
|
|
110
|
+
"gte",
|
|
111
|
+
"contains",
|
|
112
|
+
"startsWith",
|
|
113
|
+
"endsWith"
|
|
114
|
+
]);
|
|
115
|
+
var matchesField = (value, condition) => {
|
|
116
|
+
if (!isPlainObject(condition)) {
|
|
117
|
+
return equals(value, condition);
|
|
118
|
+
}
|
|
119
|
+
for (const operator of Object.keys(condition)) {
|
|
120
|
+
if (!FIELD_OPERATORS.has(operator)) {
|
|
121
|
+
throw new UnsupportedFilterError(operator);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const [operator, operand] of Object.entries(condition)) {
|
|
125
|
+
switch (operator) {
|
|
126
|
+
case "equals":
|
|
127
|
+
if (!equals(value, operand))
|
|
128
|
+
return false;
|
|
129
|
+
break;
|
|
130
|
+
case "not":
|
|
131
|
+
if (isPlainObject(operand)) {
|
|
132
|
+
if (matchesField(value, operand))
|
|
133
|
+
return false;
|
|
134
|
+
} else if (equals(value, operand)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case "in":
|
|
139
|
+
if (!Array.isArray(operand) || !operand.some((item) => equals(value, item)))
|
|
140
|
+
return false;
|
|
141
|
+
break;
|
|
142
|
+
case "notIn":
|
|
143
|
+
if (Array.isArray(operand) && operand.some((item) => equals(value, item)))
|
|
144
|
+
return false;
|
|
145
|
+
break;
|
|
146
|
+
case "lt":
|
|
147
|
+
if (!comparable(value) || compare(value, operand) >= 0)
|
|
148
|
+
return false;
|
|
149
|
+
break;
|
|
150
|
+
case "lte":
|
|
151
|
+
if (!comparable(value) || compare(value, operand) > 0)
|
|
152
|
+
return false;
|
|
153
|
+
break;
|
|
154
|
+
case "gt":
|
|
155
|
+
if (!comparable(value) || compare(value, operand) <= 0)
|
|
156
|
+
return false;
|
|
157
|
+
break;
|
|
158
|
+
case "gte":
|
|
159
|
+
if (!comparable(value) || compare(value, operand) < 0)
|
|
160
|
+
return false;
|
|
161
|
+
break;
|
|
162
|
+
case "contains":
|
|
163
|
+
if (typeof value !== "string" || !value.includes(String(operand)))
|
|
164
|
+
return false;
|
|
165
|
+
break;
|
|
166
|
+
case "startsWith":
|
|
167
|
+
if (typeof value !== "string" || !value.startsWith(String(operand)))
|
|
168
|
+
return false;
|
|
169
|
+
break;
|
|
170
|
+
case "endsWith":
|
|
171
|
+
if (typeof value !== "string" || !value.endsWith(String(operand)))
|
|
172
|
+
return false;
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
throw new UnsupportedFilterError(operator);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
};
|
|
180
|
+
var toConditions = (value) => {
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
return value.filter(isPlainObject);
|
|
183
|
+
}
|
|
184
|
+
return isPlainObject(value) ? [value] : [];
|
|
185
|
+
};
|
|
186
|
+
var matchesWhere = (where, row) => {
|
|
187
|
+
for (const [field, condition] of Object.entries(where)) {
|
|
188
|
+
if (field === "AND") {
|
|
189
|
+
if (!toConditions(condition).every((part) => matchesWhere(part, row)))
|
|
190
|
+
return false;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (field === "OR") {
|
|
194
|
+
const parts = toConditions(condition);
|
|
195
|
+
if (parts.length > 0 && !parts.some((part) => matchesWhere(part, row)))
|
|
196
|
+
return false;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (field === "NOT") {
|
|
200
|
+
if (toConditions(condition).some((part) => matchesWhere(part, row)))
|
|
201
|
+
return false;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!matchesField(row[field], condition)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
};
|
|
210
|
+
// src/adapters/prisma/collection.ts
|
|
211
|
+
var prismaCollection = (options) => ({
|
|
212
|
+
name: options.name,
|
|
213
|
+
hydrate: (params, ctx) => options.find(options.where(params, ctx), params, ctx),
|
|
214
|
+
match: (row, params, ctx) => matchesWhere(options.where(params, ctx), row),
|
|
215
|
+
key: options.key,
|
|
216
|
+
authorize: options.authorize
|
|
217
|
+
});
|
|
218
|
+
export {
|
|
219
|
+
tableTopic,
|
|
220
|
+
publishWhere,
|
|
221
|
+
publishRows,
|
|
222
|
+
publishChange,
|
|
223
|
+
prismaCollection,
|
|
224
|
+
matchesWhere,
|
|
225
|
+
keyTopic,
|
|
226
|
+
deriveReadTopics,
|
|
227
|
+
UnsupportedFilterError
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
//# debugId=96C8D5DF158A4DF864756E2164756E21
|
|
231
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/adapters/prisma/topics.ts", "../src/adapters/prisma/read.ts", "../src/adapters/prisma/write.ts", "../src/adapters/prisma/predicate.ts", "../src/adapters/prisma/collection.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Shared topic vocabulary + key extraction for the Prisma adapter. Operates on\n * Prisma's plain `where` objects and result records — there is no\n * `@prisma/client` import, so it stays generic across every database Prisma\n * supports. Both the read side (derive a query's topics) and the write side\n * (publish a mutation's topics) build on these so the two always agree.\n */\n\n/** A Prisma `where` filter object, e.g. `{ id: 5 }` or `{ id: { gt: 5 } }`. */\nexport type PrismaWhere = Record<string, unknown>;\n\n/** A scalar usable as a row key. */\nexport type RowKey = string | number | bigint;\n\n/** The coarse topic every read/write of `model` touches, e.g. `user`. */\nexport const tableTopic = (model: string): string => model;\n\n/** The row-level topic for one key of `model`, e.g. `user:5`. */\nexport const keyTopic = (model: string, key: RowKey): string =>\n\t`${model}:${key}`;\n\nconst isRowKey = (value: unknown): value is RowKey =>\n\ttypeof value === 'string' ||\n\ttypeof value === 'number' ||\n\ttypeof value === 'bigint';\n\n/**\n * Best-effort: pull a single key-field equality value out of a Prisma `where`.\n * Recognises `{ [keyField]: scalar }` and `{ [keyField]: { equals: scalar } }`\n * only — extra fields, other operators (`gt`/`in`/`not`/…), `AND`/`OR`, or a\n * compound-key object yield `undefined`, so the caller falls back to the table\n * topic (over-invalidating a little rather than missing an update).\n */\nexport const extractKeyFromWhere = (\n\twhere: PrismaWhere,\n\tkeyField: string\n): RowKey | undefined => {\n\tconst fields = Object.keys(where);\n\tif (fields.length !== 1 || fields[0] !== keyField) {\n\t\treturn undefined;\n\t}\n\tconst condition = where[keyField];\n\tif (isRowKey(condition)) {\n\t\treturn condition;\n\t}\n\tif (\n\t\tcondition !== null &&\n\t\ttypeof condition === 'object' &&\n\t\t!Array.isArray(condition)\n\t) {\n\t\tconst operators = Object.keys(condition as Record<string, unknown>);\n\t\tif (operators.length === 1 && operators[0] === 'equals') {\n\t\t\tconst value = (condition as Record<string, unknown>).equals;\n\t\t\tif (isRowKey(value)) {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t}\n\treturn undefined;\n};\n\n/**\n * Read the key value from each result record (e.g. the output of a Prisma\n * `create`/`update`/`delete`), using `keyField`. Records without a scalar key\n * are skipped.\n */\nexport const extractRowKeys = (\n\trows: ReadonlyArray<Record<string, unknown>>,\n\tkeyField: string\n): RowKey[] => {\n\tconst keys: RowKey[] = [];\n\tfor (const row of rows) {\n\t\tconst value = row[keyField];\n\t\tif (isRowKey(value)) {\n\t\t\tkeys.push(value);\n\t\t}\n\t}\n\treturn keys;\n};\n",
|
|
6
|
+
"import { extractKeyFromWhere, keyTopic, tableTopic } from './topics';\nimport type { PrismaWhere } from './topics';\n\nexport type DeriveReadTopicsOptions = {\n\t/**\n\t * Primary-key field name to narrow on. Defaults to `id` (Prisma's\n\t * convention); set it for models keyed by another field.\n\t */\n\tkeyField?: string;\n};\n\nexport type DerivedReadTopics = {\n\t/** Topics this read depends on — subscribe to all of them. */\n\ttopics: string[];\n\t/**\n\t * `true` when derivation narrowed to a specific row (`model:key`); `false`\n\t * when it fell back to the whole-model topic.\n\t */\n\trowLevel: boolean;\n};\n\n/**\n * Derive the reactive topics a read of `model` (optionally filtered by `where`)\n * depends on. A recognised key-field equality narrows to a single `model:key`\n * topic; everything else subscribes to the whole-model topic.\n *\n * @example\n * deriveReadTopics('user'); // { topics: ['user'], rowLevel: false }\n * deriveReadTopics('user', { id: 5 }); // { topics: ['user:5'], rowLevel: true }\n * deriveReadTopics('user', { id: { gt: 5 } }); // { topics: ['user'], rowLevel: false }\n */\nexport const deriveReadTopics = (\n\tmodel: string,\n\twhere?: PrismaWhere,\n\toptions: DeriveReadTopicsOptions = {}\n): DerivedReadTopics => {\n\tconst keyField = options.keyField ?? 'id';\n\tconst key =\n\t\twhere === undefined ? undefined : extractKeyFromWhere(where, keyField);\n\n\tif (key === undefined) {\n\t\treturn { topics: [tableTopic(model)], rowLevel: false };\n\t}\n\treturn { topics: [keyTopic(model, key)], rowLevel: true };\n};\n",
|
|
7
|
+
"import type { ReactiveHub } from '../../reactiveHub';\nimport {\n\textractKeyFromWhere,\n\textractRowKeys,\n\tkeyTopic,\n\ttableTopic\n} from './topics';\nimport type { PrismaWhere, RowKey } from './topics';\n\n/**\n * Prisma write-side topic publishing (Tier 2).\n *\n * The mirror of {@link deriveReadTopics}: after a mutation commits, publish the\n * topics it invalidates so subscribed reads refetch. Every change publishes the\n * **model** topic (so list queries refresh) plus a **row** topic per affected\n * key (so row-level queries refresh) — the exact topics the read side derives.\n *\n * Use the SAME model identifier you pass on the read side; the topic is the\n * model name verbatim.\n */\n\n/** The kind of mutation, forwarded in the change-event payload. */\nexport type ChangeOp = 'insert' | 'update' | 'delete';\n\n/** Payload carried by every change event the write side publishes. */\nexport type ChangePayload = {\n\t/** Name of the model that changed. */\n\ttable: string;\n\t/** Mutation kind, when the caller provided it. */\n\top?: ChangeOp;\n\t/** Affected row keys (empty for a model-wide change). */\n\tkeys: RowKey[];\n};\n\nexport type PublishChangeOptions = {\n\t/** Row keys that changed; each emits a `model:key` topic. */\n\tkeys?: ReadonlyArray<RowKey>;\n\top?: ChangeOp;\n};\n\n/**\n * Publish the reactive topics a change to `model` invalidates: the whole-model\n * topic (always) plus a `model:key` topic per affected row. Call after the\n * mutation resolves. Returns the (de-duplicated) topics published.\n */\nexport const publishChange = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\tmodel: string,\n\toptions: PublishChangeOptions = {}\n): string[] => {\n\tconst name = tableTopic(model);\n\tconst keys = options.keys === undefined ? [] : [...new Set(options.keys)];\n\tconst payload: ChangePayload = { table: name, op: options.op, keys };\n\tconst topics = [\n\t\t...new Set([name, ...keys.map((key) => keyTopic(model, key))])\n\t];\n\tfor (const topic of topics) {\n\t\thub.publish(topic, payload);\n\t}\n\treturn topics;\n};\n\nexport type PublishRowsOptions = {\n\t/** Key field (defaults to `id`). */\n\tkeyField?: string;\n\top?: ChangeOp;\n};\n\n/**\n * Publish change topics for the record(s) a mutation returned. Accepts a single\n * record (Prisma `create`/`update`/`delete`) or an array (`findMany` results),\n * reading each record's `keyField` to emit `model:key` topics.\n *\n * @example\n * const user = await prisma.user.create({ data });\n * publishRows(hub, 'user', user, { op: 'insert' });\n */\nexport const publishRows = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\tmodel: string,\n\trows: Record<string, unknown> | ReadonlyArray<Record<string, unknown>>,\n\toptions: PublishRowsOptions = {}\n): string[] => {\n\tconst list = (Array.isArray(rows) ? rows : [rows]) as ReadonlyArray<\n\t\tRecord<string, unknown>\n\t>;\n\treturn publishChange(hub, model, {\n\t\tkeys: extractRowKeys(list, options.keyField ?? 'id'),\n\t\top: options.op\n\t});\n};\n\nexport type PublishWhereOptions = {\n\t/** Key field (defaults to `id`). */\n\tkeyField?: string;\n\top?: ChangeOp;\n};\n\n/**\n * Publish change topics for an `update`/`delete` identified by a `where` filter.\n * A simple key-field equality narrows to that row's topic; any other filter\n * publishes just the model topic, so every affected subscriber refetches.\n *\n * @example\n * await prisma.user.update({ where: { id }, data });\n * publishWhere(hub, 'user', { id }, { op: 'update' });\n */\nexport const publishWhere = (\n\thub: Pick<ReactiveHub, 'publish'>,\n\tmodel: string,\n\twhere: PrismaWhere,\n\toptions: PublishWhereOptions = {}\n): string[] => {\n\tconst key = extractKeyFromWhere(where, options.keyField ?? 'id');\n\treturn publishChange(hub, model, {\n\t\tkeys: key === undefined ? [] : [key],\n\t\top: options.op\n\t});\n};\n",
|
|
8
|
+
"import type { PrismaWhere } from './topics';\n\n/**\n * Thrown when a Prisma `where` uses an operator the incremental matcher can't\n * evaluate in JS. The sync engine catches it and degrades that subscription to\n * a refetch, so it never produces a wrong result — only a less efficient one.\n */\nexport class UnsupportedFilterError extends Error {\n\tconstructor(operator: string) {\n\t\tsuper(\n\t\t\t`Cannot evaluate Prisma filter operator \"${operator}\" incrementally`\n\t\t);\n\t\tthis.name = 'UnsupportedFilterError';\n\t}\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n\ttypeof value === 'object' &&\n\tvalue !== null &&\n\t!Array.isArray(value) &&\n\t!(value instanceof Date);\n\n/** Prisma equality semantics (a `null` operand means IS NULL). */\nconst equals = (value: unknown, operand: unknown): boolean => {\n\tif (operand === null) {\n\t\treturn value === null || value === undefined;\n\t}\n\tif (value instanceof Date && operand instanceof Date) {\n\t\treturn value.getTime() === operand.getTime();\n\t}\n\treturn value === operand;\n};\n\nconst order = (value: unknown): number | string =>\n\tvalue instanceof Date ? value.getTime() : (value as number | string);\n\nconst compare = (value: unknown, operand: unknown): number => {\n\tconst a = order(value);\n\tconst b = order(operand);\n\tif (a < b) {\n\t\treturn -1;\n\t}\n\tif (a > b) {\n\t\treturn 1;\n\t}\n\treturn 0;\n};\n\nconst comparable = (value: unknown): boolean =>\n\tvalue !== null && value !== undefined;\n\nconst FIELD_OPERATORS = new Set([\n\t'equals',\n\t'not',\n\t'in',\n\t'notIn',\n\t'lt',\n\t'lte',\n\t'gt',\n\t'gte',\n\t'contains',\n\t'startsWith',\n\t'endsWith'\n]);\n\n/** Evaluate a single field's condition (scalar equality or an operator object). */\nconst matchesField = (value: unknown, condition: unknown): boolean => {\n\tif (!isPlainObject(condition)) {\n\t\treturn equals(value, condition);\n\t}\n\t// Validate support up front: an unsupported operator must force a refetch\n\t// even when an earlier operator would short-circuit to false.\n\tfor (const operator of Object.keys(condition)) {\n\t\tif (!FIELD_OPERATORS.has(operator)) {\n\t\t\tthrow new UnsupportedFilterError(operator);\n\t\t}\n\t}\n\tfor (const [operator, operand] of Object.entries(condition)) {\n\t\tswitch (operator) {\n\t\t\tcase 'equals':\n\t\t\t\tif (!equals(value, operand)) return false;\n\t\t\t\tbreak;\n\t\t\tcase 'not':\n\t\t\t\tif (isPlainObject(operand)) {\n\t\t\t\t\tif (matchesField(value, operand)) return false;\n\t\t\t\t} else if (equals(value, operand)) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase 'in':\n\t\t\t\tif (\n\t\t\t\t\t!Array.isArray(operand) ||\n\t\t\t\t\t!operand.some((item) => equals(value, item))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'notIn':\n\t\t\t\tif (\n\t\t\t\t\tArray.isArray(operand) &&\n\t\t\t\t\toperand.some((item) => equals(value, item))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'lt':\n\t\t\t\tif (!comparable(value) || compare(value, operand) >= 0)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'lte':\n\t\t\t\tif (!comparable(value) || compare(value, operand) > 0)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'gt':\n\t\t\t\tif (!comparable(value) || compare(value, operand) <= 0)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'gte':\n\t\t\t\tif (!comparable(value) || compare(value, operand) < 0)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'contains':\n\t\t\t\tif (\n\t\t\t\t\ttypeof value !== 'string' ||\n\t\t\t\t\t!value.includes(String(operand))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'startsWith':\n\t\t\t\tif (\n\t\t\t\t\ttypeof value !== 'string' ||\n\t\t\t\t\t!value.startsWith(String(operand))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tcase 'endsWith':\n\t\t\t\tif (\n\t\t\t\t\ttypeof value !== 'string' ||\n\t\t\t\t\t!value.endsWith(String(operand))\n\t\t\t\t)\n\t\t\t\t\treturn false;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\t// `mode`, relation filters, etc. — bail so the engine refetches.\n\t\t\t\tthrow new UnsupportedFilterError(operator);\n\t\t}\n\t}\n\treturn true;\n};\n\nconst toConditions = (value: unknown): PrismaWhere[] => {\n\tif (Array.isArray(value)) {\n\t\treturn value.filter(isPlainObject);\n\t}\n\treturn isPlainObject(value) ? [value] : [];\n};\n\n/**\n * Evaluate a Prisma `where` object against an in-memory row — the JS mirror of\n * the SQL filter, used for incremental matching. Supports field equality and the\n * `equals/not/in/notIn/lt/lte/gt/gte/contains/startsWith/endsWith` operators\n * plus `AND`/`OR`/`NOT`. Anything else throws {@link UnsupportedFilterError}, so\n * the engine falls back to a refetch.\n *\n * @example\n * matchesWhere({ userId: 5, status: { not: 'archived' } }, row)\n */\nexport const matchesWhere = (\n\twhere: PrismaWhere,\n\trow: Record<string, unknown>\n): boolean => {\n\tfor (const [field, condition] of Object.entries(where)) {\n\t\tif (field === 'AND') {\n\t\t\tif (\n\t\t\t\t!toConditions(condition).every((part) =>\n\t\t\t\t\tmatchesWhere(part, row)\n\t\t\t\t)\n\t\t\t)\n\t\t\t\treturn false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (field === 'OR') {\n\t\t\tconst parts = toConditions(condition);\n\t\t\tif (\n\t\t\t\tparts.length > 0 &&\n\t\t\t\t!parts.some((part) => matchesWhere(part, row))\n\t\t\t)\n\t\t\t\treturn false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (field === 'NOT') {\n\t\t\tif (toConditions(condition).some((part) => matchesWhere(part, row)))\n\t\t\t\treturn false;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!matchesField(row[field], condition)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\treturn true;\n};\n",
|
|
9
|
+
"import type {\n\tCollectionContext,\n\tCollectionDefinition\n} from '../../engine/collection';\nimport type { RowKey } from '../../engine/types';\nimport { matchesWhere } from './predicate';\nimport type { PrismaWhere } from './topics';\n\nexport type PrismaCollectionOptions<T, P, Ctx> = {\n\t/** Collection name (change-feed key and topic root). */\n\tname: string;\n\t/**\n\t * The query filter, written once. Used both to hydrate from the database\n\t * (passed to `find`) and, mirrored in JS, to match changed rows\n\t * incrementally. Receives the subscription's params and context.\n\t */\n\twhere: (params: P, ctx: Ctx) => PrismaWhere;\n\t/**\n\t * Run the database read for a given `where` — your Prisma call, e.g.\n\t * `(where) => prisma.order.findMany({ where })`.\n\t */\n\tfind: (\n\t\twhere: PrismaWhere,\n\t\tparams: P,\n\t\tctx: Ctx\n\t) => Promise<Iterable<T>> | Iterable<T>;\n\t/** Row identity. Defaults to `row.id`. */\n\tkey?: (row: T) => RowKey;\n\t/** Access control; return false (or throw) to deny. */\n\tauthorize?: (params: P, ctx: Ctx) => boolean | Promise<boolean>;\n};\n\n/**\n * Build a syncable {@link CollectionDefinition} for Prisma from a single filter:\n * `where` is written once and powers both `hydrate` (via your `find`) and the\n * incremental `match` (via {@link matchesWhere}) — no restating the WHERE.\n *\n * If the filter uses an operator the JS matcher can't evaluate, that change\n * degrades to a refetch (handled by the engine), so the result stays correct.\n *\n * @example\n * prismaCollection({\n * name: 'orders',\n * where: (p) => ({ userId: p.userId, status: 'open' }),\n * find: (where) => prisma.order.findMany({ where }),\n * authorize: (p, ctx) => p.userId === ctx.userId\n * });\n */\nexport const prismaCollection = <T, P = void, Ctx = CollectionContext>(\n\toptions: PrismaCollectionOptions<T, P, Ctx>\n): CollectionDefinition<T, P, Ctx> => ({\n\tname: options.name,\n\thydrate: (params, ctx) =>\n\t\toptions.find(options.where(params, ctx), params, ctx),\n\tmatch: (row, params, ctx) =>\n\t\tmatchesWhere(\n\t\t\toptions.where(params, ctx),\n\t\t\trow as Record<string, unknown>\n\t\t),\n\tkey: options.key,\n\tauthorize: options.authorize\n});\n"
|
|
10
|
+
],
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "96C8D5DF158A4DF864756E2164756E21",
|
|
13
|
+
"names": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PrismaWhere } from './topics';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a Prisma `where` uses an operator the incremental matcher can't
|
|
4
|
+
* evaluate in JS. The sync engine catches it and degrades that subscription to
|
|
5
|
+
* a refetch, so it never produces a wrong result — only a less efficient one.
|
|
6
|
+
*/
|
|
7
|
+
export declare class UnsupportedFilterError extends Error {
|
|
8
|
+
constructor(operator: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Evaluate a Prisma `where` object against an in-memory row — the JS mirror of
|
|
12
|
+
* the SQL filter, used for incremental matching. Supports field equality and the
|
|
13
|
+
* `equals/not/in/notIn/lt/lte/gt/gte/contains/startsWith/endsWith` operators
|
|
14
|
+
* plus `AND`/`OR`/`NOT`. Anything else throws {@link UnsupportedFilterError}, so
|
|
15
|
+
* the engine falls back to a refetch.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* matchesWhere({ userId: 5, status: { not: 'archived' } }, row)
|
|
19
|
+
*/
|
|
20
|
+
export declare const matchesWhere: (where: PrismaWhere, row: Record<string, unknown>) => boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { PrismaWhere } from './topics';
|
|
2
|
+
export type DeriveReadTopicsOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Primary-key field name to narrow on. Defaults to `id` (Prisma's
|
|
5
|
+
* convention); set it for models keyed by another field.
|
|
6
|
+
*/
|
|
7
|
+
keyField?: string;
|
|
8
|
+
};
|
|
9
|
+
export type DerivedReadTopics = {
|
|
10
|
+
/** Topics this read depends on — subscribe to all of them. */
|
|
11
|
+
topics: string[];
|
|
12
|
+
/**
|
|
13
|
+
* `true` when derivation narrowed to a specific row (`model:key`); `false`
|
|
14
|
+
* when it fell back to the whole-model topic.
|
|
15
|
+
*/
|
|
16
|
+
rowLevel: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Derive the reactive topics a read of `model` (optionally filtered by `where`)
|
|
20
|
+
* depends on. A recognised key-field equality narrows to a single `model:key`
|
|
21
|
+
* topic; everything else subscribes to the whole-model topic.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* deriveReadTopics('user'); // { topics: ['user'], rowLevel: false }
|
|
25
|
+
* deriveReadTopics('user', { id: 5 }); // { topics: ['user:5'], rowLevel: true }
|
|
26
|
+
* deriveReadTopics('user', { id: { gt: 5 } }); // { topics: ['user'], rowLevel: false }
|
|
27
|
+
*/
|
|
28
|
+
export declare const deriveReadTopics: (model: string, where?: PrismaWhere, options?: DeriveReadTopicsOptions) => DerivedReadTopics;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared topic vocabulary + key extraction for the Prisma adapter. Operates on
|
|
3
|
+
* Prisma's plain `where` objects and result records — there is no
|
|
4
|
+
* `@prisma/client` import, so it stays generic across every database Prisma
|
|
5
|
+
* supports. Both the read side (derive a query's topics) and the write side
|
|
6
|
+
* (publish a mutation's topics) build on these so the two always agree.
|
|
7
|
+
*/
|
|
8
|
+
/** A Prisma `where` filter object, e.g. `{ id: 5 }` or `{ id: { gt: 5 } }`. */
|
|
9
|
+
export type PrismaWhere = Record<string, unknown>;
|
|
10
|
+
/** A scalar usable as a row key. */
|
|
11
|
+
export type RowKey = string | number | bigint;
|
|
12
|
+
/** The coarse topic every read/write of `model` touches, e.g. `user`. */
|
|
13
|
+
export declare const tableTopic: (model: string) => string;
|
|
14
|
+
/** The row-level topic for one key of `model`, e.g. `user:5`. */
|
|
15
|
+
export declare const keyTopic: (model: string, key: RowKey) => string;
|
|
16
|
+
/**
|
|
17
|
+
* Best-effort: pull a single key-field equality value out of a Prisma `where`.
|
|
18
|
+
* Recognises `{ [keyField]: scalar }` and `{ [keyField]: { equals: scalar } }`
|
|
19
|
+
* only — extra fields, other operators (`gt`/`in`/`not`/…), `AND`/`OR`, or a
|
|
20
|
+
* compound-key object yield `undefined`, so the caller falls back to the table
|
|
21
|
+
* topic (over-invalidating a little rather than missing an update).
|
|
22
|
+
*/
|
|
23
|
+
export declare const extractKeyFromWhere: (where: PrismaWhere, keyField: string) => RowKey | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Read the key value from each result record (e.g. the output of a Prisma
|
|
26
|
+
* `create`/`update`/`delete`), using `keyField`. Records without a scalar key
|
|
27
|
+
* are skipped.
|
|
28
|
+
*/
|
|
29
|
+
export declare const extractRowKeys: (rows: ReadonlyArray<Record<string, unknown>>, keyField: string) => RowKey[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactiveHub } from '../../reactiveHub';
|
|
2
|
+
import type { PrismaWhere, RowKey } from './topics';
|
|
3
|
+
/**
|
|
4
|
+
* Prisma write-side topic publishing (Tier 2).
|
|
5
|
+
*
|
|
6
|
+
* The mirror of {@link deriveReadTopics}: after a mutation commits, publish the
|
|
7
|
+
* topics it invalidates so subscribed reads refetch. Every change publishes the
|
|
8
|
+
* **model** topic (so list queries refresh) plus a **row** topic per affected
|
|
9
|
+
* key (so row-level queries refresh) — the exact topics the read side derives.
|
|
10
|
+
*
|
|
11
|
+
* Use the SAME model identifier you pass on the read side; the topic is the
|
|
12
|
+
* model name verbatim.
|
|
13
|
+
*/
|
|
14
|
+
/** The kind of mutation, forwarded in the change-event payload. */
|
|
15
|
+
export type ChangeOp = 'insert' | 'update' | 'delete';
|
|
16
|
+
/** Payload carried by every change event the write side publishes. */
|
|
17
|
+
export type ChangePayload = {
|
|
18
|
+
/** Name of the model that changed. */
|
|
19
|
+
table: string;
|
|
20
|
+
/** Mutation kind, when the caller provided it. */
|
|
21
|
+
op?: ChangeOp;
|
|
22
|
+
/** Affected row keys (empty for a model-wide change). */
|
|
23
|
+
keys: RowKey[];
|
|
24
|
+
};
|
|
25
|
+
export type PublishChangeOptions = {
|
|
26
|
+
/** Row keys that changed; each emits a `model:key` topic. */
|
|
27
|
+
keys?: ReadonlyArray<RowKey>;
|
|
28
|
+
op?: ChangeOp;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Publish the reactive topics a change to `model` invalidates: the whole-model
|
|
32
|
+
* topic (always) plus a `model:key` topic per affected row. Call after the
|
|
33
|
+
* mutation resolves. Returns the (de-duplicated) topics published.
|
|
34
|
+
*/
|
|
35
|
+
export declare const publishChange: (hub: Pick<ReactiveHub, "publish">, model: string, options?: PublishChangeOptions) => string[];
|
|
36
|
+
export type PublishRowsOptions = {
|
|
37
|
+
/** Key field (defaults to `id`). */
|
|
38
|
+
keyField?: string;
|
|
39
|
+
op?: ChangeOp;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Publish change topics for the record(s) a mutation returned. Accepts a single
|
|
43
|
+
* record (Prisma `create`/`update`/`delete`) or an array (`findMany` results),
|
|
44
|
+
* reading each record's `keyField` to emit `model:key` topics.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const user = await prisma.user.create({ data });
|
|
48
|
+
* publishRows(hub, 'user', user, { op: 'insert' });
|
|
49
|
+
*/
|
|
50
|
+
export declare const publishRows: (hub: Pick<ReactiveHub, "publish">, model: string, rows: Record<string, unknown> | ReadonlyArray<Record<string, unknown>>, options?: PublishRowsOptions) => string[];
|
|
51
|
+
export type PublishWhereOptions = {
|
|
52
|
+
/** Key field (defaults to `id`). */
|
|
53
|
+
keyField?: string;
|
|
54
|
+
op?: ChangeOp;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Publish change topics for an `update`/`delete` identified by a `where` filter.
|
|
58
|
+
* A simple key-field equality narrows to that row's topic; any other filter
|
|
59
|
+
* publishes just the model topic, so every affected subscriber refetches.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* await prisma.user.update({ where: { id }, data });
|
|
63
|
+
* publishWhere(hub, 'user', { id }, { op: 'update' });
|
|
64
|
+
*/
|
|
65
|
+
export declare const publishWhere: (hub: Pick<ReactiveHub, "publish">, model: string, where: PrismaWhere, options?: PublishWhereOptions) => string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite CDC adapter for @absolutejs/sync (Tier 3, M5).
|
|
3
|
+
*
|
|
4
|
+
* SQLite has no `LISTEN/NOTIFY`, and its `update_hook` isn't reachable from the
|
|
5
|
+
* JS runtimes we target, so out-of-band writes are caught with the portable
|
|
6
|
+
* changelog (outbox) pattern: install triggers that append every row change to a
|
|
7
|
+
* changelog table, then tail it with {@link createPollingChangeSource}. Polling a
|
|
8
|
+
* local SQLite table is cheap (same process, no network).
|
|
9
|
+
*
|
|
10
|
+
* Dependency-free — it only generates SQL; bring your own client (`bun:sqlite`,
|
|
11
|
+
* better-sqlite3, …) to run it and to back the poll query.
|
|
12
|
+
*/
|
|
13
|
+
export { createPollingChangeSource, parseOutboxRow } from '../../engine/pollingSource';
|
|
14
|
+
export type { OutboxRow, PollingChangeSourceOptions } from '../../engine/pollingSource';
|
|
15
|
+
export type SqliteChangelogOptions = {
|
|
16
|
+
/** Table name → the column names to capture in the change payload. */
|
|
17
|
+
tables: Record<string, string[]>;
|
|
18
|
+
/** Changelog table name. Defaults to `absolute_sync_changelog`. */
|
|
19
|
+
changelogTable?: string;
|
|
20
|
+
/** Trigger name prefix. Defaults to `absolute_sync`. */
|
|
21
|
+
prefix?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Generate the SQL that installs the changelog table and per-table
|
|
25
|
+
* insert/update/delete triggers — run it once (e.g. in a migration). Each
|
|
26
|
+
* trigger appends `{ tbl, op, payload }` (payload built with `json_object` from
|
|
27
|
+
* the listed columns) to the changelog for {@link createPollingChangeSource}.
|
|
28
|
+
*
|
|
29
|
+
* The statements are `;`-separated; run them as a script, or split on `;` if your
|
|
30
|
+
* driver executes one statement per call.
|
|
31
|
+
*/
|
|
32
|
+
export declare const sqliteChangelogSchema: (options: SqliteChangelogOptions) => string;
|