@syncular/client 0.0.6-95 → 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.
- package/README.md +713 -11
- package/dist/auth-leases.d.ts +11 -0
- package/dist/auth-leases.d.ts.map +1 -0
- package/dist/auth-leases.js +140 -0
- package/dist/auth-leases.js.map +1 -0
- package/dist/blob-limits.d.ts +15 -0
- package/dist/blob-limits.d.ts.map +1 -0
- package/dist/blob-limits.js +66 -0
- package/dist/blob-limits.js.map +1 -0
- package/dist/bridge-client.d.ts +69 -0
- package/dist/bridge-client.d.ts.map +1 -0
- package/dist/bridge-client.js +282 -0
- package/dist/bridge-client.js.map +1 -0
- package/dist/client-config.d.ts +6 -0
- package/dist/client-config.d.ts.map +1 -0
- package/dist/client-config.js +18 -0
- package/dist/client-config.js.map +1 -0
- package/dist/client.d.ts +65 -397
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +150 -960
- package/dist/client.js.map +1 -1
- package/dist/command-history.d.ts +35 -0
- package/dist/command-history.d.ts.map +1 -0
- package/dist/command-history.js +378 -0
- package/dist/command-history.js.map +1 -0
- package/dist/console-diagnostics.d.ts +33 -0
- package/dist/console-diagnostics.d.ts.map +1 -0
- package/dist/console-diagnostics.js +496 -0
- package/dist/console-diagnostics.js.map +1 -0
- package/dist/crdt-yjs/index.d.ts +4 -0
- package/dist/crdt-yjs/index.d.ts.map +1 -0
- package/dist/crdt-yjs/index.js +4 -0
- package/dist/crdt-yjs/index.js.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts +126 -0
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +1 -0
- package/dist/crdt-yjs/webview-host-facade.js +284 -0
- package/dist/crdt-yjs/webview-host-facade.js.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +153 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js +406 -0
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +73 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +1 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +169 -0
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +1 -0
- package/dist/database.d.ts +83 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +916 -0
- package/dist/database.js.map +1 -0
- package/dist/diagnostics.d.ts +10 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +83 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +211 -0
- package/dist/errors.js.map +1 -0
- package/dist/generated-bridge.d.ts +365 -0
- package/dist/generated-bridge.d.ts.map +1 -0
- package/dist/generated-bridge.js +250 -0
- package/dist/generated-bridge.js.map +1 -0
- package/dist/index.d.ts +13 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -25
- package/dist/index.js.map +1 -1
- package/dist/mutations.d.ts +4 -72
- package/dist/mutations.d.ts.map +1 -1
- package/dist/mutations.js +2 -568
- package/dist/mutations.js.map +1 -1
- package/dist/network.d.ts +3 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +17 -0
- package/dist/network.js.map +1 -0
- package/dist/react/index.d.ts +169 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +628 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react-native/index.d.ts +35 -0
- package/dist/react-native/index.d.ts.map +1 -0
- package/dist/react-native/index.js +49 -0
- package/dist/react-native/index.js.map +1 -0
- package/dist/runtime-contract.d.ts +13 -0
- package/dist/runtime-contract.d.ts.map +1 -0
- package/dist/runtime-contract.js +24 -0
- package/dist/runtime-contract.js.map +1 -0
- package/dist/rust-client.d.ts +106 -0
- package/dist/rust-client.d.ts.map +1 -0
- package/dist/rust-client.js +936 -0
- package/dist/rust-client.js.map +1 -0
- package/dist/sentry.d.ts +35 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +155 -0
- package/dist/sentry.js.map +1 -0
- package/dist/sql-safety.d.ts +3 -0
- package/dist/sql-safety.d.ts.map +1 -0
- package/dist/sql-safety.js +62 -0
- package/dist/sql-safety.js.map +1 -0
- package/dist/syncular-runtime-artifacts.json +61 -0
- package/dist/tauri/index.d.ts +35 -0
- package/dist/tauri/index.d.ts.map +1 -0
- package/dist/tauri/index.js +114 -0
- package/dist/tauri/index.js.map +1 -0
- package/dist/types.d.ts +1059 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm/.syncular-wasm-profile +1 -0
- package/dist/wasm/syncular-runtime-artifact.json +21 -0
- package/dist/wasm/syncular.d.ts +207 -0
- package/dist/wasm/syncular.js +2341 -0
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-bindings/runtime-contract.d.ts +22 -0
- package/dist/wasm-bindings/runtime-contract.d.ts.map +1 -0
- package/dist/wasm-bindings/runtime-contract.js +112 -0
- package/dist/wasm-bindings/runtime-contract.js.map +1 -0
- package/dist/wasm-core/.syncular-wasm-profile +1 -0
- package/dist/wasm-core/syncular-runtime-artifact.json +17 -0
- package/dist/wasm-core/syncular.d.ts +162 -0
- package/dist/wasm-core/syncular.js +1847 -0
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +77 -0
- package/dist/wasm-perf/.syncular-wasm-profile +1 -0
- package/dist/wasm-perf/syncular-runtime-artifact.json +21 -0
- package/dist/wasm-perf/syncular.d.ts +207 -0
- package/dist/wasm-perf/syncular.js +2341 -0
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +97 -0
- package/dist/wasm-runtime.d.ts +18 -0
- package/dist/wasm-runtime.d.ts.map +1 -0
- package/dist/wasm-runtime.js +28 -0
- package/dist/wasm-runtime.js.map +1 -0
- package/dist/worker-client.d.ts +123 -0
- package/dist/worker-client.d.ts.map +1 -0
- package/dist/worker-client.js +1686 -0
- package/dist/worker-client.js.map +1 -0
- package/dist/worker-entry.d.ts +2 -0
- package/dist/worker-entry.d.ts.map +1 -0
- package/dist/worker-entry.js +479 -0
- package/dist/worker-entry.js.map +1 -0
- package/dist/worker-protocol.d.ts +64 -0
- package/dist/worker-protocol.d.ts.map +1 -0
- package/dist/worker-protocol.js +16 -0
- package/dist/worker-protocol.js.map +1 -0
- package/dist/worker-realtime.d.ts +39 -0
- package/dist/worker-realtime.d.ts.map +1 -0
- package/dist/worker-realtime.js +677 -0
- package/dist/worker-realtime.js.map +1 -0
- package/package.json +124 -17
- package/src/auth-leases.ts +251 -0
- package/src/blob-limits.ts +98 -0
- package/src/bridge-client.ts +512 -0
- package/src/client-config.ts +29 -0
- package/src/client.ts +256 -1414
- package/src/command-history.ts +623 -0
- package/src/console-diagnostics.ts +617 -0
- package/src/crdt-yjs/index.ts +3 -0
- package/src/crdt-yjs/webview-host-facade.ts +477 -0
- package/src/crdt-yjs/yjs-document-field-adapter.ts +733 -0
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +272 -0
- package/src/database.ts +1342 -0
- package/src/diagnostics.ts +116 -0
- package/src/errors.ts +269 -0
- package/src/generated-bridge.ts +741 -0
- package/src/index.ts +21 -25
- package/src/mutations.ts +12 -821
- package/src/network.ts +32 -0
- package/src/react/index.ts +1036 -0
- package/src/react-native/index.ts +152 -0
- package/src/runtime-contract.ts +48 -0
- package/src/rust-client.ts +1488 -0
- package/src/sentry.ts +215 -0
- package/src/sql-safety.ts +61 -0
- package/src/tauri/index.ts +211 -0
- package/src/types.ts +1356 -0
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +70 -0
- package/src/wasm-bindings/runtime-contract.ts +158 -0
- package/src/wasm-runtime.ts +76 -0
- package/src/worker-client.ts +2215 -0
- package/src/worker-entry.ts +584 -0
- package/src/worker-protocol.ts +109 -0
- package/src/worker-realtime.ts +843 -0
- package/dist/blobs/index.d.ts +0 -7
- package/dist/blobs/index.d.ts.map +0 -1
- package/dist/blobs/index.js +0 -7
- package/dist/blobs/index.js.map +0 -1
- package/dist/blobs/manager.d.ts +0 -345
- package/dist/blobs/manager.d.ts.map +0 -1
- package/dist/blobs/manager.js +0 -749
- package/dist/blobs/manager.js.map +0 -1
- package/dist/blobs/migrate.d.ts +0 -14
- package/dist/blobs/migrate.d.ts.map +0 -1
- package/dist/blobs/migrate.js +0 -59
- package/dist/blobs/migrate.js.map +0 -1
- package/dist/blobs/types.d.ts +0 -62
- package/dist/blobs/types.d.ts.map +0 -1
- package/dist/blobs/types.js +0 -5
- package/dist/blobs/types.js.map +0 -1
- package/dist/conflicts.d.ts +0 -31
- package/dist/conflicts.d.ts.map +0 -1
- package/dist/conflicts.js +0 -112
- package/dist/conflicts.js.map +0 -1
- package/dist/create-client.d.ts +0 -118
- package/dist/create-client.d.ts.map +0 -1
- package/dist/create-client.js +0 -178
- package/dist/create-client.js.map +0 -1
- package/dist/engine/SyncEngine.d.ts +0 -272
- package/dist/engine/SyncEngine.d.ts.map +0 -1
- package/dist/engine/SyncEngine.js +0 -1860
- package/dist/engine/SyncEngine.js.map +0 -1
- package/dist/engine/index.d.ts +0 -6
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -6
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/types.d.ts +0 -345
- package/dist/engine/types.d.ts.map +0 -1
- package/dist/engine/types.js +0 -7
- package/dist/engine/types.js.map +0 -1
- package/dist/handlers/collection.d.ts +0 -6
- package/dist/handlers/collection.d.ts.map +0 -1
- package/dist/handlers/collection.js +0 -21
- package/dist/handlers/collection.js.map +0 -1
- package/dist/handlers/create-handler.d.ts +0 -120
- package/dist/handlers/create-handler.d.ts.map +0 -1
- package/dist/handlers/create-handler.js +0 -145
- package/dist/handlers/create-handler.js.map +0 -1
- package/dist/handlers/types.d.ts +0 -83
- package/dist/handlers/types.d.ts.map +0 -1
- package/dist/handlers/types.js +0 -5
- package/dist/handlers/types.js.map +0 -1
- package/dist/migrate.d.ts +0 -19
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -238
- package/dist/migrate.js.map +0 -1
- package/dist/outbox.d.ts +0 -112
- package/dist/outbox.d.ts.map +0 -1
- package/dist/outbox.js +0 -294
- package/dist/outbox.js.map +0 -1
- package/dist/plugins/incrementing-version.d.ts +0 -34
- package/dist/plugins/incrementing-version.d.ts.map +0 -1
- package/dist/plugins/incrementing-version.js +0 -83
- package/dist/plugins/incrementing-version.js.map +0 -1
- package/dist/plugins/index.d.ts +0 -3
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/plugins/index.js +0 -3
- package/dist/plugins/index.js.map +0 -1
- package/dist/plugins/types.d.ts +0 -49
- package/dist/plugins/types.d.ts.map +0 -1
- package/dist/plugins/types.js +0 -15
- package/dist/plugins/types.js.map +0 -1
- package/dist/proxy/connection.d.ts +0 -33
- package/dist/proxy/connection.d.ts.map +0 -1
- package/dist/proxy/connection.js +0 -153
- package/dist/proxy/connection.js.map +0 -1
- package/dist/proxy/dialect.d.ts +0 -46
- package/dist/proxy/dialect.d.ts.map +0 -1
- package/dist/proxy/dialect.js +0 -58
- package/dist/proxy/dialect.js.map +0 -1
- package/dist/proxy/driver.d.ts +0 -42
- package/dist/proxy/driver.d.ts.map +0 -1
- package/dist/proxy/driver.js +0 -78
- package/dist/proxy/driver.js.map +0 -1
- package/dist/proxy/index.d.ts +0 -10
- package/dist/proxy/index.d.ts.map +0 -1
- package/dist/proxy/index.js +0 -10
- package/dist/proxy/index.js.map +0 -1
- package/dist/proxy/mutations.d.ts +0 -9
- package/dist/proxy/mutations.d.ts.map +0 -1
- package/dist/proxy/mutations.js +0 -11
- package/dist/proxy/mutations.js.map +0 -1
- package/dist/pull-engine.d.ts +0 -48
- package/dist/pull-engine.d.ts.map +0 -1
- package/dist/pull-engine.js +0 -648
- package/dist/pull-engine.js.map +0 -1
- package/dist/push-engine.d.ts +0 -18
- package/dist/push-engine.d.ts.map +0 -1
- package/dist/push-engine.js +0 -167
- package/dist/push-engine.js.map +0 -1
- package/dist/query/FingerprintCollector.d.ts +0 -18
- package/dist/query/FingerprintCollector.d.ts.map +0 -1
- package/dist/query/FingerprintCollector.js +0 -28
- package/dist/query/FingerprintCollector.js.map +0 -1
- package/dist/query/QueryContext.d.ts +0 -33
- package/dist/query/QueryContext.d.ts.map +0 -1
- package/dist/query/QueryContext.js +0 -16
- package/dist/query/QueryContext.js.map +0 -1
- package/dist/query/fingerprint.d.ts +0 -61
- package/dist/query/fingerprint.d.ts.map +0 -1
- package/dist/query/fingerprint.js +0 -91
- package/dist/query/fingerprint.js.map +0 -1
- package/dist/query/index.d.ts +0 -7
- package/dist/query/index.d.ts.map +0 -1
- package/dist/query/index.js +0 -7
- package/dist/query/index.js.map +0 -1
- package/dist/query/tracked-select.d.ts +0 -18
- package/dist/query/tracked-select.d.ts.map +0 -1
- package/dist/query/tracked-select.js +0 -90
- package/dist/query/tracked-select.js.map +0 -1
- package/dist/schema.d.ts +0 -83
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js +0 -7
- package/dist/schema.js.map +0 -1
- package/dist/subscription-state.d.ts +0 -46
- package/dist/subscription-state.d.ts.map +0 -1
- package/dist/subscription-state.js +0 -185
- package/dist/subscription-state.js.map +0 -1
- package/dist/sync-loop.d.ts +0 -34
- package/dist/sync-loop.d.ts.map +0 -1
- package/dist/sync-loop.js +0 -253
- package/dist/sync-loop.js.map +0 -1
- package/dist/sync.d.ts +0 -32
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -55
- package/dist/sync.js.map +0 -1
- package/dist/utils/id.d.ts +0 -17
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -46
- package/dist/utils/id.js.map +0 -1
- package/src/blobs/index.ts +0 -7
- package/src/blobs/manager.ts +0 -1027
- package/src/blobs/migrate.ts +0 -67
- package/src/blobs/types.ts +0 -84
- package/src/client.test.ts +0 -417
- package/src/conflicts.ts +0 -171
- package/src/create-client.test.ts +0 -83
- package/src/create-client.ts +0 -330
- package/src/engine/SyncEngine.test.ts +0 -254
- package/src/engine/SyncEngine.ts +0 -2375
- package/src/engine/index.ts +0 -6
- package/src/engine/types.ts +0 -419
- package/src/handlers/collection.ts +0 -36
- package/src/handlers/create-handler.ts +0 -330
- package/src/handlers/types.ts +0 -108
- package/src/migrate.ts +0 -318
- package/src/outbox.ts +0 -383
- package/src/plugins/incrementing-version.ts +0 -133
- package/src/plugins/index.ts +0 -2
- package/src/plugins/types.ts +0 -63
- package/src/proxy/connection.ts +0 -191
- package/src/proxy/dialect.ts +0 -76
- package/src/proxy/driver.ts +0 -126
- package/src/proxy/index.ts +0 -10
- package/src/proxy/mutations.ts +0 -18
- package/src/pull-engine.test.ts +0 -151
- package/src/pull-engine.ts +0 -855
- package/src/push-engine.ts +0 -216
- package/src/query/FingerprintCollector.ts +0 -29
- package/src/query/QueryContext.ts +0 -54
- package/src/query/fingerprint.ts +0 -109
- package/src/query/index.ts +0 -10
- package/src/query/tracked-select.ts +0 -139
- package/src/schema.ts +0 -94
- package/src/subscription-state.ts +0 -259
- package/src/sync-loop.ts +0 -379
- package/src/sync.ts +0 -170
- package/src/utils/id.ts +0 -55
package/src/mutations.ts
CHANGED
|
@@ -1,49 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Provides a dynamic `Proxy` mutation interface with Kysely typings:
|
|
5
|
-
* - `mutations.tasks.insert({ ... })` (auto-generates id)
|
|
6
|
-
* - `mutations.tasks.update(id, { ... })`
|
|
7
|
-
* - `mutations.tasks.delete(id)`
|
|
8
|
-
* - `mutations.$commit(async (tx) => { ... })` for batching
|
|
9
|
-
*
|
|
10
|
-
* Under the hood this compiles to sync `upsert/delete` operations.
|
|
11
|
-
*
|
|
12
|
-
* This module is framework-agnostic. `@syncular/client-react` wraps it to add
|
|
13
|
-
* React state and automatic sync triggering.
|
|
14
|
-
*/
|
|
1
|
+
import type { SyncOperation } from '@syncular/core';
|
|
2
|
+
import type { Insertable, Updateable } from 'kysely';
|
|
15
3
|
|
|
16
|
-
import type {
|
|
17
|
-
ColumnCodecDialect,
|
|
18
|
-
ColumnCodecSource,
|
|
19
|
-
SyncOperation,
|
|
20
|
-
SyncPushRequest,
|
|
21
|
-
SyncPushResponse,
|
|
22
|
-
SyncTransport,
|
|
23
|
-
} from '@syncular/core';
|
|
24
|
-
import {
|
|
25
|
-
applyCodecsToDbRow,
|
|
26
|
-
isRecord,
|
|
27
|
-
randomId,
|
|
28
|
-
toTableColumnCodecs,
|
|
29
|
-
} from '@syncular/core';
|
|
30
|
-
import type { Insertable, Kysely, Transaction, Updateable } from 'kysely';
|
|
31
|
-
import { sql } from 'kysely';
|
|
32
|
-
import { enqueueOutboxCommit } from './outbox';
|
|
33
|
-
import type {
|
|
34
|
-
SyncClientPlugin,
|
|
35
|
-
SyncClientPluginContext,
|
|
36
|
-
} from './plugins/types';
|
|
37
|
-
import type { SyncClientDb } from './schema';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Base type for any database schema.
|
|
41
|
-
* Uses an index signature to support runtime-determined table names.
|
|
42
|
-
*/
|
|
43
4
|
type AnyDb = Record<string, Record<string, unknown>>;
|
|
44
5
|
|
|
45
|
-
type SyncOpKind = 'upsert' | 'delete';
|
|
46
|
-
|
|
47
6
|
type ReservedKeys = '$commit' | '$table';
|
|
48
7
|
type KnownKeys<T> = string extends keyof T ? never : keyof T & string;
|
|
49
8
|
type KnownTableKey<DB> = Exclude<KnownKeys<DB>, ReservedKeys>;
|
|
@@ -58,25 +17,17 @@ type UpdatePayload<Row> = Omit<Updateable<Row>, 'id'> & { id?: never };
|
|
|
58
17
|
type BaseVersionOptions = { baseVersion?: number | null };
|
|
59
18
|
|
|
60
19
|
export interface MutationReceipt {
|
|
61
|
-
/**
|
|
62
|
-
* Outbox commit id (when using outbox) or a generated id (when pushing directly).
|
|
63
|
-
*/
|
|
64
20
|
commitId: string;
|
|
65
|
-
/**
|
|
66
|
-
* Protocol-level client commit id (sent to the server in push requests).
|
|
67
|
-
*/
|
|
68
21
|
clientCommitId: string;
|
|
69
22
|
}
|
|
70
23
|
|
|
71
|
-
export interface
|
|
72
|
-
operations: SyncOperation[];
|
|
73
|
-
localMutations: Array<{ table: string; rowId: string; op: SyncOpKind }>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface PushCommitMeta {
|
|
24
|
+
export interface SyncularCommitMeta {
|
|
77
25
|
operations: SyncOperation[];
|
|
78
|
-
localMutations: Array<{
|
|
79
|
-
|
|
26
|
+
localMutations: Array<{
|
|
27
|
+
table: string;
|
|
28
|
+
rowId: string;
|
|
29
|
+
op: 'upsert' | 'delete';
|
|
30
|
+
}>;
|
|
80
31
|
}
|
|
81
32
|
|
|
82
33
|
export type TableMutations<DB, T extends keyof DB & string> = {
|
|
@@ -95,9 +46,6 @@ export type TableMutations<DB, T extends keyof DB & string> = {
|
|
|
95
46
|
id: string,
|
|
96
47
|
options?: BaseVersionOptions
|
|
97
48
|
) => Promise<MutationReceipt>;
|
|
98
|
-
/**
|
|
99
|
-
* Explicit upsert escape hatch. Prefer insert/update for clarity.
|
|
100
|
-
*/
|
|
101
49
|
upsert: (
|
|
102
50
|
id: string,
|
|
103
51
|
patch: UpdatePayload<DB[T]>,
|
|
@@ -124,7 +72,6 @@ export type TableMutationsTx<DB, T extends keyof DB & string> = {
|
|
|
124
72
|
export type MutationsTx<DB> = {
|
|
125
73
|
[T in KnownTableKey<DB>]: TableMutationsTx<DB, T>;
|
|
126
74
|
} & {
|
|
127
|
-
// Index signature for dynamic access via Proxy
|
|
128
75
|
[table: string]: TableMutationsTx<AnyDb, string>;
|
|
129
76
|
};
|
|
130
77
|
|
|
@@ -146,189 +93,10 @@ export type MutationsApi<DB, CommitOptions = unknown> = {
|
|
|
146
93
|
[T in KnownTableKey<DB>]: TableMutations<DB, T>;
|
|
147
94
|
};
|
|
148
95
|
|
|
149
|
-
function sanitizePayload(
|
|
150
|
-
payload: Record<string, unknown>,
|
|
151
|
-
args: { omit: string[] }
|
|
152
|
-
): Record<string, unknown> {
|
|
153
|
-
if (args.omit.length === 0) return payload;
|
|
154
|
-
const omitSet = new Set(args.omit);
|
|
155
|
-
let changed = false;
|
|
156
|
-
const out: Record<string, unknown> = {};
|
|
157
|
-
for (const [k, v] of Object.entries(payload)) {
|
|
158
|
-
if (omitSet.has(k)) {
|
|
159
|
-
changed = true;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
out[k] = v;
|
|
163
|
-
}
|
|
164
|
-
return changed ? out : payload;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function coerceBaseVersion(value: unknown): number | null {
|
|
168
|
-
if (value === null || value === undefined) return null;
|
|
169
|
-
const n = typeof value === 'number' ? value : Number(value);
|
|
170
|
-
if (!Number.isFinite(n)) return null;
|
|
171
|
-
if (n <= 0) return null;
|
|
172
|
-
return n;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function hasOwn(obj: object, key: string): boolean {
|
|
176
|
-
return Object.hasOwn(obj, key);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Valid SQL identifier regex.
|
|
181
|
-
* Allows: alphanumeric, underscore, cannot start with digit
|
|
182
|
-
*/
|
|
183
|
-
const VALID_IDENTIFIER_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Validate table name to prevent SQL injection.
|
|
187
|
-
* Table names are inserted directly into SQL, so they must be validated.
|
|
188
|
-
*/
|
|
189
|
-
function validateTableName(table: string): void {
|
|
190
|
-
if (!VALID_IDENTIFIER_REGEX.test(table)) {
|
|
191
|
-
throw new Error(
|
|
192
|
-
`Invalid table name "${table}". Table names must match /^[a-zA-Z_][a-zA-Z0-9_]*$/`
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Internal helpers for dynamic table operations.
|
|
199
|
-
*
|
|
200
|
-
* Kysely's query builder is typed around compile-time table names. For the
|
|
201
|
-
* Proxy-based mutations API, we need runtime table/column names. We use
|
|
202
|
-
* Kysely's `sql` builder with strict identifier validation to avoid unsafe
|
|
203
|
-
* `any`/`unknown` casts while keeping the public API typed.
|
|
204
|
-
*/
|
|
205
|
-
function validateColumnName(column: string): void {
|
|
206
|
-
if (!VALID_IDENTIFIER_REGEX.test(column)) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`Invalid column name "${column}". Column names must match /^[a-zA-Z_][a-zA-Z0-9_]*$/`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function readBaseVersion<T>(args: {
|
|
214
|
-
trx: Transaction<T>;
|
|
215
|
-
table: string;
|
|
216
|
-
rowId: string;
|
|
217
|
-
idColumn: string;
|
|
218
|
-
versionColumn: string;
|
|
219
|
-
}): Promise<number | null> {
|
|
220
|
-
validateTableName(args.table);
|
|
221
|
-
validateColumnName(args.idColumn);
|
|
222
|
-
validateColumnName(args.versionColumn);
|
|
223
|
-
|
|
224
|
-
const res = await sql<{ v: unknown }>`
|
|
225
|
-
select ${sql.ref(args.versionColumn)} as v
|
|
226
|
-
from ${sql.table(args.table)}
|
|
227
|
-
where ${sql.ref(args.idColumn)} = ${sql.val(args.rowId)}
|
|
228
|
-
limit 1
|
|
229
|
-
`.execute(args.trx);
|
|
230
|
-
|
|
231
|
-
return coerceBaseVersion(res.rows[0]?.v);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function dynamicInsert<T>(
|
|
235
|
-
trx: Transaction<T>,
|
|
236
|
-
table: string,
|
|
237
|
-
values: Record<string, unknown> | Record<string, unknown>[]
|
|
238
|
-
): Promise<void> {
|
|
239
|
-
validateTableName(table);
|
|
240
|
-
const rows = Array.isArray(values) ? values : [values];
|
|
241
|
-
if (rows.length === 0) return;
|
|
242
|
-
|
|
243
|
-
const columnsSet = new Set<string>();
|
|
244
|
-
for (const row of rows) {
|
|
245
|
-
for (const col of Object.keys(row)) {
|
|
246
|
-
validateColumnName(col);
|
|
247
|
-
columnsSet.add(col);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const columns = Array.from(columnsSet);
|
|
252
|
-
if (columns.length === 0) return;
|
|
253
|
-
|
|
254
|
-
await sql`
|
|
255
|
-
insert into ${sql.table(table)} (${sql.join(columns.map((c) => sql.ref(c)))})
|
|
256
|
-
values ${sql.join(
|
|
257
|
-
rows.map(
|
|
258
|
-
(row) => sql`(${sql.join(columns.map((c) => sql.val(row[c] ?? null)))})`
|
|
259
|
-
)
|
|
260
|
-
)}
|
|
261
|
-
`.execute(trx);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function dynamicUpsert<T>(
|
|
265
|
-
trx: Transaction<T>,
|
|
266
|
-
table: string,
|
|
267
|
-
idColumn: string,
|
|
268
|
-
id: string,
|
|
269
|
-
values: Record<string, unknown>
|
|
270
|
-
): Promise<void> {
|
|
271
|
-
validateTableName(table);
|
|
272
|
-
validateColumnName(idColumn);
|
|
273
|
-
|
|
274
|
-
// Check if the row already exists
|
|
275
|
-
const existing = await sql`
|
|
276
|
-
select 1 from ${sql.table(table)}
|
|
277
|
-
where ${sql.ref(idColumn)} = ${sql.val(id)}
|
|
278
|
-
limit 1
|
|
279
|
-
`.execute(trx);
|
|
280
|
-
|
|
281
|
-
if (existing.rows.length > 0) {
|
|
282
|
-
// Row exists: just update the provided columns
|
|
283
|
-
await dynamicUpdate(trx, table, idColumn, id, values);
|
|
284
|
-
} else {
|
|
285
|
-
// Row doesn't exist: insert with all provided columns + id
|
|
286
|
-
await dynamicInsert(trx, table, { ...values, [idColumn]: id });
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async function dynamicUpdate<T>(
|
|
291
|
-
trx: Transaction<T>,
|
|
292
|
-
table: string,
|
|
293
|
-
idColumn: string,
|
|
294
|
-
id: string,
|
|
295
|
-
values: Record<string, unknown>
|
|
296
|
-
): Promise<void> {
|
|
297
|
-
validateTableName(table);
|
|
298
|
-
validateColumnName(idColumn);
|
|
299
|
-
|
|
300
|
-
const setParts = Object.entries(values).map(([col, value]) => {
|
|
301
|
-
validateColumnName(col);
|
|
302
|
-
return sql`${sql.ref(col)} = ${sql.val(value)}`;
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
if (setParts.length === 0) return;
|
|
306
|
-
|
|
307
|
-
await sql`
|
|
308
|
-
update ${sql.table(table)}
|
|
309
|
-
set ${sql.join(setParts)}
|
|
310
|
-
where ${sql.ref(idColumn)} = ${sql.val(id)}
|
|
311
|
-
`.execute(trx);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async function dynamicDelete<T>(
|
|
315
|
-
trx: Transaction<T>,
|
|
316
|
-
table: string,
|
|
317
|
-
idColumn: string,
|
|
318
|
-
id: string
|
|
319
|
-
): Promise<void> {
|
|
320
|
-
validateTableName(table);
|
|
321
|
-
validateColumnName(idColumn);
|
|
322
|
-
await sql`
|
|
323
|
-
delete from ${sql.table(table)}
|
|
324
|
-
where ${sql.ref(idColumn)} = ${sql.val(id)}
|
|
325
|
-
`.execute(trx);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
96
|
export function createMutationsApi<DB, Meta = unknown, CommitOptions = unknown>(
|
|
329
97
|
commit: MutationsCommitFn<DB, Meta, CommitOptions>
|
|
330
98
|
): MutationsApi<DB, CommitOptions> {
|
|
331
|
-
const rootTableCache = new Map<string,
|
|
99
|
+
const rootTableCache = new Map<string, TableMutations<AnyDb, string>>();
|
|
332
100
|
|
|
333
101
|
const apiBase = {
|
|
334
102
|
$commit: async <R>(
|
|
@@ -342,7 +110,7 @@ export function createMutationsApi<DB, Meta = unknown, CommitOptions = unknown>(
|
|
|
342
110
|
const cached = rootTableCache.get(table);
|
|
343
111
|
if (cached) return cached;
|
|
344
112
|
|
|
345
|
-
const tableApi: TableMutations<
|
|
113
|
+
const tableApi: TableMutations<AnyDb, string> = {
|
|
346
114
|
async insert(values) {
|
|
347
115
|
const { result, receipt } = await commit(
|
|
348
116
|
async (tx) => await tx[table]!.insert(values)
|
|
@@ -395,583 +163,6 @@ export function createMutationsApi<DB, Meta = unknown, CommitOptions = unknown>(
|
|
|
395
163
|
}) as MutationsApi<DB, CommitOptions>;
|
|
396
164
|
}
|
|
397
165
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
idColumn?: string;
|
|
401
|
-
versionColumn?: string | null;
|
|
402
|
-
omitColumns?: string[];
|
|
403
|
-
codecs?: ColumnCodecSource;
|
|
404
|
-
codecDialect?: ColumnCodecDialect;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
export function createOutboxCommit<DB extends SyncClientDb>(
|
|
408
|
-
config: OutboxCommitConfig<DB>
|
|
409
|
-
): MutationsCommitFn<DB, OutboxCommitMeta, undefined> {
|
|
410
|
-
const idColumn = config.idColumn ?? 'id';
|
|
411
|
-
const versionColumn = config.versionColumn ?? 'server_version';
|
|
412
|
-
const omitColumns = config.omitColumns ?? [];
|
|
413
|
-
const codecDialect = config.codecDialect ?? 'sqlite';
|
|
414
|
-
|
|
415
|
-
return async (fn) => {
|
|
416
|
-
const operations: SyncOperation[] = [];
|
|
417
|
-
const localMutations: Array<{
|
|
418
|
-
table: string;
|
|
419
|
-
rowId: string;
|
|
420
|
-
op: SyncOpKind;
|
|
421
|
-
}> = [];
|
|
422
|
-
|
|
423
|
-
const { result, receipt } = await config.db
|
|
424
|
-
.transaction()
|
|
425
|
-
.execute(async (trx) => {
|
|
426
|
-
const txTableCache = new Map<string, any>();
|
|
427
|
-
const tableCodecCache = new Map<
|
|
428
|
-
string,
|
|
429
|
-
Map<string, ReturnType<typeof toTableColumnCodecs>>
|
|
430
|
-
>();
|
|
431
|
-
const resolveTableCodecs = (
|
|
432
|
-
table: string,
|
|
433
|
-
row: Record<string, unknown>
|
|
434
|
-
) => {
|
|
435
|
-
const codecs = config.codecs;
|
|
436
|
-
if (!codecs) return {};
|
|
437
|
-
const columns = Object.keys(row);
|
|
438
|
-
if (columns.length === 0) return {};
|
|
439
|
-
|
|
440
|
-
let tableCache = tableCodecCache.get(table);
|
|
441
|
-
if (!tableCache) {
|
|
442
|
-
tableCache = new Map<
|
|
443
|
-
string,
|
|
444
|
-
ReturnType<typeof toTableColumnCodecs>
|
|
445
|
-
>();
|
|
446
|
-
tableCodecCache.set(table, tableCache);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const cacheKey = columns.slice().sort().join('\u0000');
|
|
450
|
-
const cached = tableCache.get(cacheKey);
|
|
451
|
-
if (cached) return cached;
|
|
452
|
-
|
|
453
|
-
const resolved = toTableColumnCodecs(table, codecs, columns, {
|
|
454
|
-
dialect: codecDialect,
|
|
455
|
-
});
|
|
456
|
-
tableCache.set(cacheKey, resolved);
|
|
457
|
-
return resolved;
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
const makeTxTable = (table: string) => {
|
|
461
|
-
const cached = txTableCache.get(table);
|
|
462
|
-
if (cached) return cached;
|
|
463
|
-
|
|
464
|
-
const tableApi: TableMutationsTx<any, any> = {
|
|
465
|
-
async insert(values) {
|
|
466
|
-
const raw = isRecord(values) ? values : {};
|
|
467
|
-
const rawId = raw[idColumn];
|
|
468
|
-
const id =
|
|
469
|
-
typeof rawId === 'string' && rawId ? rawId : randomId();
|
|
470
|
-
|
|
471
|
-
const row = { ...raw, [idColumn]: id };
|
|
472
|
-
const dbRow = applyCodecsToDbRow(
|
|
473
|
-
row,
|
|
474
|
-
resolveTableCodecs(table, row),
|
|
475
|
-
codecDialect
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
await dynamicInsert(trx, table, dbRow);
|
|
479
|
-
|
|
480
|
-
const payload = sanitizePayload(row, {
|
|
481
|
-
omit: [
|
|
482
|
-
idColumn,
|
|
483
|
-
...(versionColumn ? [versionColumn] : []),
|
|
484
|
-
...omitColumns,
|
|
485
|
-
],
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
operations.push({
|
|
489
|
-
table: table,
|
|
490
|
-
row_id: id,
|
|
491
|
-
op: 'upsert',
|
|
492
|
-
payload,
|
|
493
|
-
base_version: null,
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
497
|
-
return id;
|
|
498
|
-
},
|
|
499
|
-
|
|
500
|
-
async insertMany(rows) {
|
|
501
|
-
const ids: string[] = [];
|
|
502
|
-
const toInsert: Record<string, unknown>[] = [];
|
|
503
|
-
|
|
504
|
-
for (const values of rows) {
|
|
505
|
-
const raw = isRecord(values) ? values : {};
|
|
506
|
-
const rawId = raw[idColumn];
|
|
507
|
-
const id =
|
|
508
|
-
typeof rawId === 'string' && rawId ? rawId : randomId();
|
|
509
|
-
ids.push(id);
|
|
510
|
-
toInsert.push({ ...raw, [idColumn]: id });
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const dbRows = toInsert.map((row) =>
|
|
514
|
-
applyCodecsToDbRow(
|
|
515
|
-
row,
|
|
516
|
-
resolveTableCodecs(table, row),
|
|
517
|
-
codecDialect
|
|
518
|
-
)
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
if (dbRows.length > 0) {
|
|
522
|
-
await dynamicInsert(trx, table, dbRows);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
for (let i = 0; i < toInsert.length; i++) {
|
|
526
|
-
const row = toInsert[i]!;
|
|
527
|
-
const id = ids[i]!;
|
|
528
|
-
const payload = sanitizePayload(row, {
|
|
529
|
-
omit: [
|
|
530
|
-
idColumn,
|
|
531
|
-
...(versionColumn ? [versionColumn] : []),
|
|
532
|
-
...omitColumns,
|
|
533
|
-
],
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
operations.push({
|
|
537
|
-
table: table,
|
|
538
|
-
row_id: id,
|
|
539
|
-
op: 'upsert',
|
|
540
|
-
payload,
|
|
541
|
-
base_version: null,
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return ids;
|
|
548
|
-
},
|
|
549
|
-
|
|
550
|
-
async update(id, patch, opts) {
|
|
551
|
-
const rawPatch = isRecord(patch) ? patch : {};
|
|
552
|
-
const sanitized = sanitizePayload(rawPatch, {
|
|
553
|
-
omit: [
|
|
554
|
-
idColumn,
|
|
555
|
-
...(versionColumn ? [versionColumn] : []),
|
|
556
|
-
...omitColumns,
|
|
557
|
-
],
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
const hasExplicitBaseVersion =
|
|
561
|
-
!!opts && hasOwn(opts, 'baseVersion');
|
|
562
|
-
const dbPatch = applyCodecsToDbRow(
|
|
563
|
-
sanitized,
|
|
564
|
-
resolveTableCodecs(table, sanitized),
|
|
565
|
-
codecDialect
|
|
566
|
-
);
|
|
567
|
-
|
|
568
|
-
await dynamicUpdate(trx, table, idColumn, id, dbPatch);
|
|
569
|
-
|
|
570
|
-
const baseVersion = hasExplicitBaseVersion
|
|
571
|
-
? (opts!.baseVersion ?? null)
|
|
572
|
-
: versionColumn
|
|
573
|
-
? await readBaseVersion({
|
|
574
|
-
trx: trx,
|
|
575
|
-
table,
|
|
576
|
-
rowId: id,
|
|
577
|
-
idColumn,
|
|
578
|
-
versionColumn,
|
|
579
|
-
})
|
|
580
|
-
: null;
|
|
581
|
-
|
|
582
|
-
operations.push({
|
|
583
|
-
table: table,
|
|
584
|
-
row_id: id,
|
|
585
|
-
op: 'upsert',
|
|
586
|
-
payload: sanitized,
|
|
587
|
-
base_version: coerceBaseVersion(baseVersion),
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
591
|
-
},
|
|
592
|
-
|
|
593
|
-
async delete(id, opts) {
|
|
594
|
-
const hasExplicitBaseVersion =
|
|
595
|
-
!!opts && hasOwn(opts, 'baseVersion');
|
|
596
|
-
const baseVersion = hasExplicitBaseVersion
|
|
597
|
-
? (opts!.baseVersion ?? null)
|
|
598
|
-
: versionColumn
|
|
599
|
-
? await readBaseVersion({
|
|
600
|
-
trx: trx,
|
|
601
|
-
table,
|
|
602
|
-
rowId: id,
|
|
603
|
-
idColumn,
|
|
604
|
-
versionColumn,
|
|
605
|
-
})
|
|
606
|
-
: null;
|
|
607
|
-
|
|
608
|
-
await dynamicDelete(trx, table, idColumn, id);
|
|
609
|
-
|
|
610
|
-
operations.push({
|
|
611
|
-
table: table,
|
|
612
|
-
row_id: id,
|
|
613
|
-
op: 'delete',
|
|
614
|
-
payload: null,
|
|
615
|
-
base_version: coerceBaseVersion(baseVersion),
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
localMutations.push({ table, rowId: id, op: 'delete' });
|
|
619
|
-
},
|
|
620
|
-
|
|
621
|
-
async upsert(id, patch, opts) {
|
|
622
|
-
const rawPatch = isRecord(patch) ? patch : {};
|
|
623
|
-
const sanitized = sanitizePayload(rawPatch, {
|
|
624
|
-
omit: [
|
|
625
|
-
idColumn,
|
|
626
|
-
...(versionColumn ? [versionColumn] : []),
|
|
627
|
-
...omitColumns,
|
|
628
|
-
],
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
const hasExplicitBaseVersion =
|
|
632
|
-
!!opts && hasOwn(opts, 'baseVersion');
|
|
633
|
-
const dbPatch = applyCodecsToDbRow(
|
|
634
|
-
sanitized,
|
|
635
|
-
resolveTableCodecs(table, sanitized),
|
|
636
|
-
codecDialect
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
await dynamicUpsert(trx, table, idColumn, id, dbPatch);
|
|
640
|
-
|
|
641
|
-
const baseVersion = hasExplicitBaseVersion
|
|
642
|
-
? (opts!.baseVersion ?? null)
|
|
643
|
-
: versionColumn
|
|
644
|
-
? await readBaseVersion({
|
|
645
|
-
trx: trx,
|
|
646
|
-
table,
|
|
647
|
-
rowId: id,
|
|
648
|
-
idColumn,
|
|
649
|
-
versionColumn,
|
|
650
|
-
})
|
|
651
|
-
: null;
|
|
652
|
-
|
|
653
|
-
operations.push({
|
|
654
|
-
table: table,
|
|
655
|
-
row_id: id,
|
|
656
|
-
op: 'upsert',
|
|
657
|
-
payload: sanitized,
|
|
658
|
-
base_version: coerceBaseVersion(baseVersion),
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
662
|
-
},
|
|
663
|
-
};
|
|
664
|
-
|
|
665
|
-
txTableCache.set(table, tableApi);
|
|
666
|
-
return tableApi;
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
const txProxy = new Proxy(
|
|
670
|
-
{},
|
|
671
|
-
{
|
|
672
|
-
get(_target, prop) {
|
|
673
|
-
if (prop === 'then') return undefined;
|
|
674
|
-
if (typeof prop !== 'string') return undefined;
|
|
675
|
-
return makeTxTable(prop);
|
|
676
|
-
},
|
|
677
|
-
}
|
|
678
|
-
) as MutationsTx<DB>;
|
|
679
|
-
|
|
680
|
-
const result = await fn(txProxy);
|
|
681
|
-
|
|
682
|
-
if (operations.length === 0) {
|
|
683
|
-
throw new Error('No mutations were enqueued');
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Enqueue outbox commit within this transaction
|
|
687
|
-
const receipt = await enqueueOutboxCommit(trx, { operations });
|
|
688
|
-
return {
|
|
689
|
-
result,
|
|
690
|
-
receipt: { id: receipt.id, clientCommitId: receipt.clientCommitId },
|
|
691
|
-
};
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
return {
|
|
695
|
-
result,
|
|
696
|
-
receipt: { commitId: receipt.id, clientCommitId: receipt.clientCommitId },
|
|
697
|
-
meta: { operations, localMutations },
|
|
698
|
-
};
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
export function createOutboxMutations<DB extends SyncClientDb>(
|
|
703
|
-
config: OutboxCommitConfig<DB>
|
|
704
|
-
): MutationsApi<DB, undefined> {
|
|
705
|
-
return createMutationsApi(createOutboxCommit(config));
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
export interface PushCommitConfig {
|
|
709
|
-
transport: SyncTransport;
|
|
710
|
-
clientId: string;
|
|
711
|
-
actorId?: string;
|
|
712
|
-
plugins?: SyncClientPlugin[];
|
|
713
|
-
idColumn?: string;
|
|
714
|
-
versionColumn?: string | null;
|
|
715
|
-
omitColumns?: string[];
|
|
716
|
-
/** Client schema version (default: 1) */
|
|
717
|
-
schemaVersion?: number;
|
|
718
|
-
readBaseVersion?: (args: {
|
|
719
|
-
table: string;
|
|
720
|
-
rowId: string;
|
|
721
|
-
idColumn: string;
|
|
722
|
-
versionColumn: string;
|
|
723
|
-
}) => Promise<number | null>;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function clonePushRequest(request: SyncPushRequest): SyncPushRequest {
|
|
727
|
-
if (typeof structuredClone === 'function') return structuredClone(request);
|
|
728
|
-
return JSON.parse(JSON.stringify(request)) as SyncPushRequest;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
export function createPushCommit<DB = AnyDb>(
|
|
732
|
-
config: PushCommitConfig
|
|
733
|
-
): MutationsCommitFn<DB, PushCommitMeta, undefined> {
|
|
734
|
-
const idColumn = config.idColumn ?? 'id';
|
|
735
|
-
const versionColumn = config.versionColumn ?? 'server_version';
|
|
736
|
-
const omitColumns = config.omitColumns ?? [];
|
|
737
|
-
|
|
738
|
-
return async (fn) => {
|
|
739
|
-
const operations: SyncOperation[] = [];
|
|
740
|
-
const localMutations: Array<{
|
|
741
|
-
table: string;
|
|
742
|
-
rowId: string;
|
|
743
|
-
op: SyncOpKind;
|
|
744
|
-
}> = [];
|
|
745
|
-
|
|
746
|
-
const txTableCache = new Map<string, any>();
|
|
747
|
-
const makeTxTable = (table: string) => {
|
|
748
|
-
const cached = txTableCache.get(table);
|
|
749
|
-
if (cached) return cached;
|
|
750
|
-
|
|
751
|
-
const tableApi: TableMutationsTx<any, any> = {
|
|
752
|
-
async insert(values) {
|
|
753
|
-
const raw = isRecord(values) ? values : {};
|
|
754
|
-
const rawId = raw[idColumn];
|
|
755
|
-
const id = typeof rawId === 'string' && rawId ? rawId : randomId();
|
|
756
|
-
|
|
757
|
-
const row = { ...raw, [idColumn]: id } as Record<string, unknown>;
|
|
758
|
-
|
|
759
|
-
const payload = sanitizePayload(row, {
|
|
760
|
-
omit: [
|
|
761
|
-
idColumn,
|
|
762
|
-
...(versionColumn ? [versionColumn] : []),
|
|
763
|
-
...omitColumns,
|
|
764
|
-
],
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
operations.push({
|
|
768
|
-
table: table,
|
|
769
|
-
row_id: id,
|
|
770
|
-
op: 'upsert',
|
|
771
|
-
payload,
|
|
772
|
-
base_version: null,
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
776
|
-
return id;
|
|
777
|
-
},
|
|
778
|
-
|
|
779
|
-
async insertMany(rows) {
|
|
780
|
-
const ids: string[] = [];
|
|
781
|
-
const toUpsert: Record<string, unknown>[] = [];
|
|
782
|
-
|
|
783
|
-
for (const values of rows) {
|
|
784
|
-
const raw = isRecord(values) ? values : {};
|
|
785
|
-
const rawId = raw[idColumn];
|
|
786
|
-
const id = typeof rawId === 'string' && rawId ? rawId : randomId();
|
|
787
|
-
ids.push(id);
|
|
788
|
-
toUpsert.push({ ...raw, [idColumn]: id });
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
for (let i = 0; i < toUpsert.length; i++) {
|
|
792
|
-
const row = toUpsert[i]!;
|
|
793
|
-
const id = ids[i]!;
|
|
794
|
-
const payload = sanitizePayload(row, {
|
|
795
|
-
omit: [
|
|
796
|
-
idColumn,
|
|
797
|
-
...(versionColumn ? [versionColumn] : []),
|
|
798
|
-
...omitColumns,
|
|
799
|
-
],
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
operations.push({
|
|
803
|
-
table: table,
|
|
804
|
-
row_id: id,
|
|
805
|
-
op: 'upsert',
|
|
806
|
-
payload,
|
|
807
|
-
base_version: null,
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
return ids;
|
|
814
|
-
},
|
|
815
|
-
|
|
816
|
-
async update(id, patch, opts) {
|
|
817
|
-
const rawPatch = isRecord(patch) ? patch : {};
|
|
818
|
-
const sanitized = sanitizePayload(rawPatch, {
|
|
819
|
-
omit: [
|
|
820
|
-
idColumn,
|
|
821
|
-
...(versionColumn ? [versionColumn] : []),
|
|
822
|
-
...omitColumns,
|
|
823
|
-
],
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
const hasExplicitBaseVersion = !!opts && hasOwn(opts, 'baseVersion');
|
|
827
|
-
const baseVersion = hasExplicitBaseVersion
|
|
828
|
-
? (opts!.baseVersion ?? null)
|
|
829
|
-
: versionColumn && config.readBaseVersion
|
|
830
|
-
? await config.readBaseVersion({
|
|
831
|
-
table,
|
|
832
|
-
rowId: id,
|
|
833
|
-
idColumn,
|
|
834
|
-
versionColumn,
|
|
835
|
-
})
|
|
836
|
-
: null;
|
|
837
|
-
|
|
838
|
-
operations.push({
|
|
839
|
-
table: table,
|
|
840
|
-
row_id: id,
|
|
841
|
-
op: 'upsert',
|
|
842
|
-
payload: sanitized,
|
|
843
|
-
base_version: coerceBaseVersion(baseVersion),
|
|
844
|
-
});
|
|
845
|
-
|
|
846
|
-
localMutations.push({ table, rowId: id, op: 'upsert' });
|
|
847
|
-
},
|
|
848
|
-
|
|
849
|
-
async delete(id, opts) {
|
|
850
|
-
const hasExplicitBaseVersion = !!opts && hasOwn(opts, 'baseVersion');
|
|
851
|
-
const baseVersion = hasExplicitBaseVersion
|
|
852
|
-
? (opts!.baseVersion ?? null)
|
|
853
|
-
: versionColumn && config.readBaseVersion
|
|
854
|
-
? await config.readBaseVersion({
|
|
855
|
-
table,
|
|
856
|
-
rowId: id,
|
|
857
|
-
idColumn,
|
|
858
|
-
versionColumn,
|
|
859
|
-
})
|
|
860
|
-
: null;
|
|
861
|
-
|
|
862
|
-
operations.push({
|
|
863
|
-
table: table,
|
|
864
|
-
row_id: id,
|
|
865
|
-
op: 'delete',
|
|
866
|
-
payload: null,
|
|
867
|
-
base_version: coerceBaseVersion(baseVersion),
|
|
868
|
-
});
|
|
869
|
-
|
|
870
|
-
localMutations.push({ table, rowId: id, op: 'delete' });
|
|
871
|
-
},
|
|
872
|
-
|
|
873
|
-
async upsert(id, patch, opts) {
|
|
874
|
-
await tableApi.update(id, patch, opts);
|
|
875
|
-
},
|
|
876
|
-
};
|
|
877
|
-
|
|
878
|
-
txTableCache.set(table, tableApi);
|
|
879
|
-
return tableApi;
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
const txProxy = new Proxy(
|
|
883
|
-
{},
|
|
884
|
-
{
|
|
885
|
-
get(_target, prop) {
|
|
886
|
-
if (prop === 'then') return undefined;
|
|
887
|
-
if (typeof prop !== 'string') return undefined;
|
|
888
|
-
return makeTxTable(prop);
|
|
889
|
-
},
|
|
890
|
-
}
|
|
891
|
-
) as MutationsTx<DB>;
|
|
892
|
-
|
|
893
|
-
const result = await fn(txProxy);
|
|
894
|
-
|
|
895
|
-
if (operations.length === 0) {
|
|
896
|
-
throw new Error('No mutations were enqueued');
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const commitId = randomId();
|
|
900
|
-
const clientCommitId = randomId();
|
|
901
|
-
|
|
902
|
-
const request: SyncPushRequest = {
|
|
903
|
-
clientId: config.clientId,
|
|
904
|
-
clientCommitId,
|
|
905
|
-
operations,
|
|
906
|
-
schemaVersion: config.schemaVersion ?? 1,
|
|
907
|
-
};
|
|
908
|
-
|
|
909
|
-
const plugins = config.plugins ?? [];
|
|
910
|
-
// Sort plugins by priority (lower numbers first, default 50)
|
|
911
|
-
const sortedPlugins = [...plugins].sort(
|
|
912
|
-
(a, b) => (a.priority ?? 50) - (b.priority ?? 50)
|
|
913
|
-
);
|
|
914
|
-
const ctx: SyncClientPluginContext = {
|
|
915
|
-
actorId: config.actorId ?? 'unknown',
|
|
916
|
-
clientId: config.clientId,
|
|
917
|
-
};
|
|
918
|
-
|
|
919
|
-
let requestToSend = request;
|
|
920
|
-
if (sortedPlugins.length > 0) {
|
|
921
|
-
requestToSend = clonePushRequest(request);
|
|
922
|
-
for (const plugin of sortedPlugins) {
|
|
923
|
-
if (!plugin.beforePush) continue;
|
|
924
|
-
requestToSend = await plugin.beforePush(ctx, requestToSend);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const combined = await config.transport.sync({
|
|
929
|
-
clientId: requestToSend.clientId,
|
|
930
|
-
push: {
|
|
931
|
-
clientCommitId: requestToSend.clientCommitId,
|
|
932
|
-
operations: requestToSend.operations,
|
|
933
|
-
schemaVersion: requestToSend.schemaVersion,
|
|
934
|
-
},
|
|
935
|
-
});
|
|
936
|
-
if (!combined.push) {
|
|
937
|
-
throw new Error('Server returned no push response');
|
|
938
|
-
}
|
|
939
|
-
const rawResponse = combined.push;
|
|
940
|
-
|
|
941
|
-
let response = rawResponse;
|
|
942
|
-
if (sortedPlugins.length > 0) {
|
|
943
|
-
// Run afterPush in reverse priority order (higher numbers first)
|
|
944
|
-
for (const plugin of [...sortedPlugins].reverse()) {
|
|
945
|
-
if (!plugin.afterPush) continue;
|
|
946
|
-
response = await plugin.afterPush(ctx, {
|
|
947
|
-
request: requestToSend,
|
|
948
|
-
response,
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
if (response.status !== 'applied' && response.status !== 'cached') {
|
|
954
|
-
const conflictCount = response.results.filter(
|
|
955
|
-
(r) => r.status === 'conflict'
|
|
956
|
-
).length;
|
|
957
|
-
const errorCount = response.results.filter(
|
|
958
|
-
(r) => r.status === 'error'
|
|
959
|
-
).length;
|
|
960
|
-
throw new Error(
|
|
961
|
-
`Push rejected (${conflictCount} conflicts, ${errorCount} errors)`
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
return {
|
|
966
|
-
result,
|
|
967
|
-
receipt: { commitId, clientCommitId },
|
|
968
|
-
meta: { operations, localMutations, response },
|
|
969
|
-
};
|
|
970
|
-
};
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
export function createPushMutations<DB = AnyDb>(
|
|
974
|
-
config: PushCommitConfig
|
|
975
|
-
): MutationsApi<DB, undefined> {
|
|
976
|
-
return createMutationsApi(createPushCommit(config));
|
|
166
|
+
function hasOwn(obj: object, key: string): boolean {
|
|
167
|
+
return Object.hasOwn(obj, key);
|
|
977
168
|
}
|