@syncular/client 0.0.6-239 → 0.0.6-241
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/dist/query-public.d.ts +2 -1
- package/dist/query-public.d.ts.map +1 -1
- package/dist/query-public.js +54 -12
- package/dist/query-public.js.map +1 -1
- package/package.json +3 -3
- package/src/query-public.test.ts +162 -0
- package/src/query-public.ts +101 -13
package/dist/query-public.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This wrapper keeps query-builder tracking isolated per chain so branching a
|
|
5
5
|
* base Kysely builder does not leak joined tables into sibling branches.
|
|
6
6
|
*/
|
|
7
|
+
import { type ColumnCodecDialect, type ColumnCodecSource } from '@syncular/core';
|
|
7
8
|
import type { Kysely } from 'kysely';
|
|
8
9
|
import type { FingerprintCollector } from './query/FingerprintCollector';
|
|
9
10
|
import { type MutationTimestampSource } from './query/fingerprint';
|
|
@@ -15,5 +16,5 @@ type TrackedSelectFrom<DB> = Kysely<DB>['selectFrom'];
|
|
|
15
16
|
export interface QueryContext<DB extends SyncClientDb = SyncClientDb> {
|
|
16
17
|
selectFrom: TrackedSelectFrom<DB>;
|
|
17
18
|
}
|
|
18
|
-
export declare function createQueryContext<DB extends SyncClientDb>(db: Kysely<DB>, scopeCollector: Set<string>, fingerprintCollector: FingerprintCollector, engine: MutationTimestampSource, keyField?: string, fingerprintMode?: FingerprintMode): QueryContext<DB>;
|
|
19
|
+
export declare function createQueryContext<DB extends SyncClientDb>(db: Kysely<DB>, scopeCollector: Set<string>, fingerprintCollector: FingerprintCollector, engine: MutationTimestampSource, keyField?: string, fingerprintMode?: FingerprintMode, codecSource?: ColumnCodecSource, codecDialect?: ColumnCodecDialect): QueryContext<DB>;
|
|
19
20
|
//# sourceMappingURL=query-public.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-public.d.ts","sourceRoot":"","sources":["../src/query-public.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"query-public.d.ts","sourceRoot":"","sources":["../src/query-public.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EAGvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAAqB,MAAM,QAAQ,CAAC;AACxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACzE,OAAO,EAIL,KAAK,uBAAuB,EAC7B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EACL,cAAc,EACd,kBAAkB,GACnB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,OAAO,CAAC;AAE/C,KAAK,iBAAiB,CAAC,EAAE,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;AAiTtD,MAAM,WAAW,YAAY,CAAC,EAAE,SAAS,YAAY,GAAG,YAAY;IAClE,UAAU,EAAE,iBAAiB,CAAC,EAAE,CAAC,CAAC;CACnC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,YAAY,EACxD,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,EAC3B,oBAAoB,EAAE,oBAAoB,EAC1C,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,SAAO,EACf,eAAe,GAAE,eAAwB,EACzC,WAAW,CAAC,EAAE,iBAAiB,EAC/B,YAAY,GAAE,kBAA6B,GAC1C,YAAY,CAAC,EAAE,CAAC,CAalB"}
|
package/dist/query-public.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* This wrapper keeps query-builder tracking isolated per chain so branching a
|
|
5
5
|
* base Kysely builder does not leak joined tables into sibling branches.
|
|
6
6
|
*/
|
|
7
|
+
import { applyQueryResultPlan, buildQueryResultPlan, createTableColumnCodecsResolver, } from '@syncular/core';
|
|
7
8
|
import { computeRowFingerprint, computeValueFingerprint, hasKeyField, } from './query/fingerprint.js';
|
|
8
9
|
export { FingerprintCollector } from './query/FingerprintCollector.js';
|
|
9
10
|
export { canFingerprint, computeFingerprint, } from './query/fingerprint.js';
|
|
@@ -62,15 +63,51 @@ function addTrackedTablesToScopeCollector(scopeCollector, trackedTables) {
|
|
|
62
63
|
scopeCollector.add(trackedTable);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
|
-
function
|
|
66
|
+
function isOperationNodeSource(value) {
|
|
67
|
+
if (!isRecord(value))
|
|
68
|
+
return false;
|
|
69
|
+
return typeof Reflect.get(value, 'toOperationNode') === 'function';
|
|
70
|
+
}
|
|
71
|
+
function decodeRows(rows, builder, resolveCodecs, codecDialect) {
|
|
72
|
+
if (!resolveCodecs || !builder)
|
|
73
|
+
return rows;
|
|
74
|
+
const plan = buildQueryResultPlan(builder.toOperationNode());
|
|
75
|
+
if (!plan)
|
|
76
|
+
return rows;
|
|
77
|
+
if (Array.isArray(rows)) {
|
|
78
|
+
return rows.map((row) => {
|
|
79
|
+
if (!isRecord(row))
|
|
80
|
+
return row;
|
|
81
|
+
return applyQueryResultPlan({
|
|
82
|
+
row,
|
|
83
|
+
plan,
|
|
84
|
+
resolveTableCodecs: resolveCodecs,
|
|
85
|
+
dialect: codecDialect,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (!isRecord(rows))
|
|
90
|
+
return rows;
|
|
91
|
+
return applyQueryResultPlan({
|
|
92
|
+
row: rows,
|
|
93
|
+
plan,
|
|
94
|
+
resolveTableCodecs: resolveCodecs,
|
|
95
|
+
dialect: codecDialect,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function createExecuteProxy(builder, primaryTable, trackedTables, scopeCollector, collector, engine, keyField, fingerprintMode, resolveCodecs, codecDialect) {
|
|
99
|
+
const operationNodeSource = isOperationNodeSource(builder)
|
|
100
|
+
? builder
|
|
101
|
+
: undefined;
|
|
66
102
|
return new Proxy(builder, {
|
|
67
103
|
get(target, prop, receiver) {
|
|
68
104
|
if (prop === 'execute') {
|
|
69
105
|
return async () => {
|
|
70
106
|
const rows = await target.execute();
|
|
107
|
+
const decoded = decodeRows(rows, operationNodeSource, resolveCodecs, codecDialect);
|
|
71
108
|
addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
|
|
72
109
|
addFingerprint({
|
|
73
|
-
rows,
|
|
110
|
+
rows: decoded,
|
|
74
111
|
primaryTable,
|
|
75
112
|
trackedTables,
|
|
76
113
|
collector,
|
|
@@ -78,15 +115,16 @@ function createExecuteProxy(builder, primaryTable, trackedTables, scopeCollector
|
|
|
78
115
|
keyField,
|
|
79
116
|
fingerprintMode,
|
|
80
117
|
});
|
|
81
|
-
return
|
|
118
|
+
return decoded;
|
|
82
119
|
};
|
|
83
120
|
}
|
|
84
121
|
if (prop === 'executeTakeFirst') {
|
|
85
122
|
return async () => {
|
|
86
123
|
const row = await target.executeTakeFirst();
|
|
124
|
+
const decoded = decodeRows(row, operationNodeSource, resolveCodecs, codecDialect);
|
|
87
125
|
addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
|
|
88
126
|
addFingerprint({
|
|
89
|
-
rows:
|
|
127
|
+
rows: decoded,
|
|
90
128
|
primaryTable,
|
|
91
129
|
trackedTables,
|
|
92
130
|
collector,
|
|
@@ -94,15 +132,16 @@ function createExecuteProxy(builder, primaryTable, trackedTables, scopeCollector
|
|
|
94
132
|
keyField,
|
|
95
133
|
fingerprintMode,
|
|
96
134
|
});
|
|
97
|
-
return
|
|
135
|
+
return decoded;
|
|
98
136
|
};
|
|
99
137
|
}
|
|
100
138
|
if (prop === 'executeTakeFirstOrThrow') {
|
|
101
139
|
return async () => {
|
|
102
140
|
const row = await target.executeTakeFirstOrThrow();
|
|
141
|
+
const decoded = decodeRows(row, operationNodeSource, resolveCodecs, codecDialect);
|
|
103
142
|
addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
|
|
104
143
|
addFingerprint({
|
|
105
|
-
rows:
|
|
144
|
+
rows: decoded,
|
|
106
145
|
primaryTable,
|
|
107
146
|
trackedTables,
|
|
108
147
|
collector,
|
|
@@ -110,7 +149,7 @@ function createExecuteProxy(builder, primaryTable, trackedTables, scopeCollector
|
|
|
110
149
|
keyField,
|
|
111
150
|
fingerprintMode,
|
|
112
151
|
});
|
|
113
|
-
return
|
|
152
|
+
return decoded;
|
|
114
153
|
};
|
|
115
154
|
}
|
|
116
155
|
const value = Reflect.get(target, prop, receiver);
|
|
@@ -130,23 +169,26 @@ function createExecuteProxy(builder, primaryTable, trackedTables, scopeCollector
|
|
|
130
169
|
if (!isExecutableQuery(result)) {
|
|
131
170
|
return result;
|
|
132
171
|
}
|
|
133
|
-
return createExecuteProxy(result, primaryTable, nextTrackedTables, scopeCollector, collector, engine, keyField, fingerprintMode);
|
|
172
|
+
return createExecuteProxy(result, primaryTable, nextTrackedTables, scopeCollector, collector, engine, keyField, fingerprintMode, resolveCodecs, codecDialect);
|
|
134
173
|
};
|
|
135
174
|
},
|
|
136
175
|
});
|
|
137
176
|
}
|
|
138
|
-
function createTrackedSelectFrom(db, scopeCollector, fingerprintCollector, engine, keyField = 'id', fingerprintMode = 'auto') {
|
|
177
|
+
function createTrackedSelectFrom(db, scopeCollector, fingerprintCollector, engine, keyField = 'id', fingerprintMode = 'auto', codecSource, codecDialect = 'sqlite') {
|
|
178
|
+
const resolveCodecs = codecSource
|
|
179
|
+
? createTableColumnCodecsResolver(codecSource, { dialect: codecDialect })
|
|
180
|
+
: undefined;
|
|
139
181
|
const selectFrom = (...args) => {
|
|
140
182
|
const trackedTables = new Set(extractTrackedTableNames(args[0]));
|
|
141
183
|
const primaryTable = Array.from(trackedTables)[0] ?? null;
|
|
142
184
|
const builder = db.selectFrom(...args);
|
|
143
|
-
return createExecuteProxy(builder, primaryTable, trackedTables, scopeCollector, fingerprintCollector, engine, keyField, fingerprintMode);
|
|
185
|
+
return createExecuteProxy(builder, primaryTable, trackedTables, scopeCollector, fingerprintCollector, engine, keyField, fingerprintMode, resolveCodecs, codecDialect);
|
|
144
186
|
};
|
|
145
187
|
return selectFrom;
|
|
146
188
|
}
|
|
147
|
-
export function createQueryContext(db, scopeCollector, fingerprintCollector, engine, keyField = 'id', fingerprintMode = 'auto') {
|
|
189
|
+
export function createQueryContext(db, scopeCollector, fingerprintCollector, engine, keyField = 'id', fingerprintMode = 'auto', codecSource, codecDialect = 'sqlite') {
|
|
148
190
|
return {
|
|
149
|
-
selectFrom: createTrackedSelectFrom(db, scopeCollector, fingerprintCollector, engine, keyField, fingerprintMode),
|
|
191
|
+
selectFrom: createTrackedSelectFrom(db, scopeCollector, fingerprintCollector, engine, keyField, fingerprintMode, codecSource, codecDialect),
|
|
150
192
|
};
|
|
151
193
|
}
|
|
152
194
|
//# sourceMappingURL=query-public.js.map
|
package/dist/query-public.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-public.js","sourceRoot":"","sources":["../src/query-public.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"query-public.js","sourceRoot":"","sources":["../src/query-public.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/client",
|
|
3
|
-
"version": "0.0.6-
|
|
3
|
+
"version": "0.0.6-241",
|
|
4
4
|
"description": "Client-side sync engine with offline-first support, outbox, and conflict resolution",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -119,8 +119,8 @@
|
|
|
119
119
|
"release": "bunx syncular-publish"
|
|
120
120
|
},
|
|
121
121
|
"dependencies": {
|
|
122
|
-
"@syncular/core": "0.0.6-
|
|
123
|
-
"@syncular/transport-http": "0.0.6-
|
|
122
|
+
"@syncular/core": "0.0.6-241",
|
|
123
|
+
"@syncular/transport-http": "0.0.6-241"
|
|
124
124
|
},
|
|
125
125
|
"peerDependencies": {
|
|
126
126
|
"kysely": "*"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { codecs, createDatabase } from '@syncular/core';
|
|
3
|
+
import type { Kysely } from 'kysely';
|
|
4
|
+
import { createBunSqliteDialect } from '../../dialect-bun-sqlite/src';
|
|
5
|
+
import { createQueryContext, FingerprintCollector } from './query-public';
|
|
6
|
+
import type { SyncClientDb } from './schema';
|
|
7
|
+
|
|
8
|
+
interface TasksTable {
|
|
9
|
+
id: string;
|
|
10
|
+
enabled: number | boolean;
|
|
11
|
+
metadata: string | { tags: string[] };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FlagsTable {
|
|
15
|
+
id: string;
|
|
16
|
+
enabled: number | boolean;
|
|
17
|
+
metadata: string | { tags: string[] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TestDb extends SyncClientDb {
|
|
21
|
+
tasks: TasksTable;
|
|
22
|
+
flags: FlagsTable;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const engine = {
|
|
26
|
+
getMutationTimestamp(): number {
|
|
27
|
+
return 0;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function createCodecs() {
|
|
32
|
+
return (column: { table: string; column: string }) => {
|
|
33
|
+
if (column.column === 'enabled') {
|
|
34
|
+
return codecs.numberBoolean();
|
|
35
|
+
}
|
|
36
|
+
if (column.column === 'metadata') {
|
|
37
|
+
return codecs.stringJson<{ tags: string[] }>();
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('createQueryContext codec decoding', () => {
|
|
44
|
+
let db: Kysely<TestDb>;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
db = createDatabase<TestDb>({
|
|
48
|
+
dialect: createBunSqliteDialect({ path: ':memory:' }),
|
|
49
|
+
family: 'sqlite',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await db.schema
|
|
53
|
+
.createTable('tasks')
|
|
54
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
55
|
+
.addColumn('enabled', 'integer', (col) => col.notNull())
|
|
56
|
+
.addColumn('metadata', 'text', (col) => col.notNull())
|
|
57
|
+
.execute();
|
|
58
|
+
|
|
59
|
+
await db.schema
|
|
60
|
+
.createTable('flags')
|
|
61
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
62
|
+
.addColumn('enabled', 'integer', (col) => col.notNull())
|
|
63
|
+
.addColumn('metadata', 'text', (col) => col.notNull())
|
|
64
|
+
.execute();
|
|
65
|
+
|
|
66
|
+
await db
|
|
67
|
+
.insertInto('tasks')
|
|
68
|
+
.values({
|
|
69
|
+
id: 'task-1',
|
|
70
|
+
enabled: 1,
|
|
71
|
+
metadata: JSON.stringify({ tags: ['task'] }),
|
|
72
|
+
})
|
|
73
|
+
.execute();
|
|
74
|
+
|
|
75
|
+
await db
|
|
76
|
+
.insertInto('flags')
|
|
77
|
+
.values({
|
|
78
|
+
id: 'task-1',
|
|
79
|
+
enabled: 0,
|
|
80
|
+
metadata: JSON.stringify({ tags: ['flag'] }),
|
|
81
|
+
})
|
|
82
|
+
.execute();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(async () => {
|
|
86
|
+
await db.destroy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('decodes non-aliased columns from the primary table', async () => {
|
|
90
|
+
const ctx = createQueryContext(
|
|
91
|
+
db,
|
|
92
|
+
new Set(),
|
|
93
|
+
new FingerprintCollector(),
|
|
94
|
+
engine,
|
|
95
|
+
'id',
|
|
96
|
+
'value',
|
|
97
|
+
createCodecs(),
|
|
98
|
+
'sqlite'
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const row = await ctx
|
|
102
|
+
.selectFrom('tasks')
|
|
103
|
+
.selectAll()
|
|
104
|
+
.where('id', '=', 'task-1')
|
|
105
|
+
.executeTakeFirstOrThrow();
|
|
106
|
+
|
|
107
|
+
expect(row).toEqual({
|
|
108
|
+
id: 'task-1',
|
|
109
|
+
enabled: true,
|
|
110
|
+
metadata: { tags: ['task'] },
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('decodes aliased columns selected from the primary table', async () => {
|
|
115
|
+
const ctx = createQueryContext(
|
|
116
|
+
db,
|
|
117
|
+
new Set(),
|
|
118
|
+
new FingerprintCollector(),
|
|
119
|
+
engine,
|
|
120
|
+
'id',
|
|
121
|
+
'value',
|
|
122
|
+
createCodecs(),
|
|
123
|
+
'sqlite'
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const row = await ctx
|
|
127
|
+
.selectFrom('tasks as t')
|
|
128
|
+
.select(['t.enabled as isEnabled', 't.metadata as meta'])
|
|
129
|
+
.where('t.id', '=', 'task-1')
|
|
130
|
+
.executeTakeFirstOrThrow();
|
|
131
|
+
|
|
132
|
+
expect(row).toEqual({
|
|
133
|
+
isEnabled: true,
|
|
134
|
+
meta: { tags: ['task'] },
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('decodes aliased columns selected from joined tables', async () => {
|
|
139
|
+
const ctx = createQueryContext(
|
|
140
|
+
db,
|
|
141
|
+
new Set(),
|
|
142
|
+
new FingerprintCollector(),
|
|
143
|
+
engine,
|
|
144
|
+
'id',
|
|
145
|
+
'value',
|
|
146
|
+
createCodecs(),
|
|
147
|
+
'sqlite'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const row = await ctx
|
|
151
|
+
.selectFrom('tasks')
|
|
152
|
+
.innerJoin('flags', 'flags.id', 'tasks.id')
|
|
153
|
+
.select(['flags.enabled as flagEnabled', 'flags.metadata as flagMeta'])
|
|
154
|
+
.where('tasks.id', '=', 'task-1')
|
|
155
|
+
.executeTakeFirstOrThrow();
|
|
156
|
+
|
|
157
|
+
expect(row).toEqual({
|
|
158
|
+
flagEnabled: false,
|
|
159
|
+
flagMeta: { tags: ['flag'] },
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
package/src/query-public.ts
CHANGED
|
@@ -5,7 +5,15 @@
|
|
|
5
5
|
* base Kysely builder does not leak joined tables into sibling branches.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
applyQueryResultPlan,
|
|
10
|
+
buildQueryResultPlan,
|
|
11
|
+
type ColumnCodecDialect,
|
|
12
|
+
type ColumnCodecSource,
|
|
13
|
+
createTableColumnCodecsResolver,
|
|
14
|
+
type TableColumnCodecs,
|
|
15
|
+
} from '@syncular/core';
|
|
16
|
+
import type { Kysely, RootOperationNode } from 'kysely';
|
|
9
17
|
import type { FingerprintCollector } from './query/FingerprintCollector';
|
|
10
18
|
import {
|
|
11
19
|
computeRowFingerprint,
|
|
@@ -33,6 +41,10 @@ type ExecutableQuery = {
|
|
|
33
41
|
executeTakeFirstOrThrow: () => Promise<unknown>;
|
|
34
42
|
};
|
|
35
43
|
|
|
44
|
+
type OperationNodeSource = {
|
|
45
|
+
toOperationNode: () => RootOperationNode;
|
|
46
|
+
};
|
|
47
|
+
|
|
36
48
|
const JOIN_METHODS = new Set([
|
|
37
49
|
'innerJoin',
|
|
38
50
|
'leftJoin',
|
|
@@ -123,6 +135,42 @@ function addTrackedTablesToScopeCollector(
|
|
|
123
135
|
}
|
|
124
136
|
}
|
|
125
137
|
|
|
138
|
+
function isOperationNodeSource(value: unknown): value is OperationNodeSource {
|
|
139
|
+
if (!isRecord(value)) return false;
|
|
140
|
+
return typeof Reflect.get(value, 'toOperationNode') === 'function';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function decodeRows(
|
|
144
|
+
rows: unknown,
|
|
145
|
+
builder: OperationNodeSource | undefined,
|
|
146
|
+
resolveCodecs:
|
|
147
|
+
| ((table: string, row: Record<string, unknown>) => TableColumnCodecs)
|
|
148
|
+
| undefined,
|
|
149
|
+
codecDialect: ColumnCodecDialect
|
|
150
|
+
): unknown {
|
|
151
|
+
if (!resolveCodecs || !builder) return rows;
|
|
152
|
+
const plan = buildQueryResultPlan(builder.toOperationNode());
|
|
153
|
+
if (!plan) return rows;
|
|
154
|
+
if (Array.isArray(rows)) {
|
|
155
|
+
return rows.map((row) => {
|
|
156
|
+
if (!isRecord(row)) return row;
|
|
157
|
+
return applyQueryResultPlan({
|
|
158
|
+
row,
|
|
159
|
+
plan,
|
|
160
|
+
resolveTableCodecs: resolveCodecs,
|
|
161
|
+
dialect: codecDialect,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (!isRecord(rows)) return rows;
|
|
166
|
+
return applyQueryResultPlan({
|
|
167
|
+
row: rows,
|
|
168
|
+
plan,
|
|
169
|
+
resolveTableCodecs: resolveCodecs,
|
|
170
|
+
dialect: codecDialect,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
126
174
|
function createExecuteProxy<B extends ExecutableQuery>(
|
|
127
175
|
builder: B,
|
|
128
176
|
primaryTable: string | null,
|
|
@@ -131,16 +179,30 @@ function createExecuteProxy<B extends ExecutableQuery>(
|
|
|
131
179
|
collector: FingerprintCollector,
|
|
132
180
|
engine: MutationTimestampSource,
|
|
133
181
|
keyField: string,
|
|
134
|
-
fingerprintMode: FingerprintMode
|
|
182
|
+
fingerprintMode: FingerprintMode,
|
|
183
|
+
resolveCodecs:
|
|
184
|
+
| ((table: string, row: Record<string, unknown>) => TableColumnCodecs)
|
|
185
|
+
| undefined,
|
|
186
|
+
codecDialect: ColumnCodecDialect
|
|
135
187
|
): B {
|
|
188
|
+
const operationNodeSource = isOperationNodeSource(builder)
|
|
189
|
+
? builder
|
|
190
|
+
: undefined;
|
|
191
|
+
|
|
136
192
|
return new Proxy(builder, {
|
|
137
193
|
get(target, prop, receiver) {
|
|
138
194
|
if (prop === 'execute') {
|
|
139
195
|
return async () => {
|
|
140
196
|
const rows = await target.execute();
|
|
197
|
+
const decoded = decodeRows(
|
|
198
|
+
rows,
|
|
199
|
+
operationNodeSource,
|
|
200
|
+
resolveCodecs,
|
|
201
|
+
codecDialect
|
|
202
|
+
);
|
|
141
203
|
addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
|
|
142
204
|
addFingerprint({
|
|
143
|
-
rows,
|
|
205
|
+
rows: decoded,
|
|
144
206
|
primaryTable,
|
|
145
207
|
trackedTables,
|
|
146
208
|
collector,
|
|
@@ -148,16 +210,22 @@ function createExecuteProxy<B extends ExecutableQuery>(
|
|
|
148
210
|
keyField,
|
|
149
211
|
fingerprintMode,
|
|
150
212
|
});
|
|
151
|
-
return
|
|
213
|
+
return decoded;
|
|
152
214
|
};
|
|
153
215
|
}
|
|
154
216
|
|
|
155
217
|
if (prop === 'executeTakeFirst') {
|
|
156
218
|
return async () => {
|
|
157
219
|
const row = await target.executeTakeFirst();
|
|
220
|
+
const decoded = decodeRows(
|
|
221
|
+
row,
|
|
222
|
+
operationNodeSource,
|
|
223
|
+
resolveCodecs,
|
|
224
|
+
codecDialect
|
|
225
|
+
);
|
|
158
226
|
addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
|
|
159
227
|
addFingerprint({
|
|
160
|
-
rows:
|
|
228
|
+
rows: decoded,
|
|
161
229
|
primaryTable,
|
|
162
230
|
trackedTables,
|
|
163
231
|
collector,
|
|
@@ -165,16 +233,22 @@ function createExecuteProxy<B extends ExecutableQuery>(
|
|
|
165
233
|
keyField,
|
|
166
234
|
fingerprintMode,
|
|
167
235
|
});
|
|
168
|
-
return
|
|
236
|
+
return decoded;
|
|
169
237
|
};
|
|
170
238
|
}
|
|
171
239
|
|
|
172
240
|
if (prop === 'executeTakeFirstOrThrow') {
|
|
173
241
|
return async () => {
|
|
174
242
|
const row = await target.executeTakeFirstOrThrow();
|
|
243
|
+
const decoded = decodeRows(
|
|
244
|
+
row,
|
|
245
|
+
operationNodeSource,
|
|
246
|
+
resolveCodecs,
|
|
247
|
+
codecDialect
|
|
248
|
+
);
|
|
175
249
|
addTrackedTablesToScopeCollector(scopeCollector, trackedTables);
|
|
176
250
|
addFingerprint({
|
|
177
|
-
rows:
|
|
251
|
+
rows: decoded,
|
|
178
252
|
primaryTable,
|
|
179
253
|
trackedTables,
|
|
180
254
|
collector,
|
|
@@ -182,7 +256,7 @@ function createExecuteProxy<B extends ExecutableQuery>(
|
|
|
182
256
|
keyField,
|
|
183
257
|
fingerprintMode,
|
|
184
258
|
});
|
|
185
|
-
return
|
|
259
|
+
return decoded;
|
|
186
260
|
};
|
|
187
261
|
}
|
|
188
262
|
|
|
@@ -217,7 +291,9 @@ function createExecuteProxy<B extends ExecutableQuery>(
|
|
|
217
291
|
collector,
|
|
218
292
|
engine,
|
|
219
293
|
keyField,
|
|
220
|
-
fingerprintMode
|
|
294
|
+
fingerprintMode,
|
|
295
|
+
resolveCodecs,
|
|
296
|
+
codecDialect
|
|
221
297
|
);
|
|
222
298
|
};
|
|
223
299
|
},
|
|
@@ -230,8 +306,14 @@ function createTrackedSelectFrom<DB extends SyncClientDb>(
|
|
|
230
306
|
fingerprintCollector: FingerprintCollector,
|
|
231
307
|
engine: MutationTimestampSource,
|
|
232
308
|
keyField = 'id',
|
|
233
|
-
fingerprintMode: FingerprintMode = 'auto'
|
|
309
|
+
fingerprintMode: FingerprintMode = 'auto',
|
|
310
|
+
codecSource?: ColumnCodecSource,
|
|
311
|
+
codecDialect: ColumnCodecDialect = 'sqlite'
|
|
234
312
|
): TrackedSelectFrom<DB> {
|
|
313
|
+
const resolveCodecs = codecSource
|
|
314
|
+
? createTableColumnCodecsResolver(codecSource, { dialect: codecDialect })
|
|
315
|
+
: undefined;
|
|
316
|
+
|
|
235
317
|
const selectFrom = (...args: SelectFromArgs<DB>) => {
|
|
236
318
|
const trackedTables = new Set<string>(extractTrackedTableNames(args[0]));
|
|
237
319
|
const primaryTable = Array.from(trackedTables)[0] ?? null;
|
|
@@ -245,7 +327,9 @@ function createTrackedSelectFrom<DB extends SyncClientDb>(
|
|
|
245
327
|
fingerprintCollector,
|
|
246
328
|
engine,
|
|
247
329
|
keyField,
|
|
248
|
-
fingerprintMode
|
|
330
|
+
fingerprintMode,
|
|
331
|
+
resolveCodecs,
|
|
332
|
+
codecDialect
|
|
249
333
|
) as SelectFromResult<DB>;
|
|
250
334
|
};
|
|
251
335
|
|
|
@@ -262,7 +346,9 @@ export function createQueryContext<DB extends SyncClientDb>(
|
|
|
262
346
|
fingerprintCollector: FingerprintCollector,
|
|
263
347
|
engine: MutationTimestampSource,
|
|
264
348
|
keyField = 'id',
|
|
265
|
-
fingerprintMode: FingerprintMode = 'auto'
|
|
349
|
+
fingerprintMode: FingerprintMode = 'auto',
|
|
350
|
+
codecSource?: ColumnCodecSource,
|
|
351
|
+
codecDialect: ColumnCodecDialect = 'sqlite'
|
|
266
352
|
): QueryContext<DB> {
|
|
267
353
|
return {
|
|
268
354
|
selectFrom: createTrackedSelectFrom(
|
|
@@ -271,7 +357,9 @@ export function createQueryContext<DB extends SyncClientDb>(
|
|
|
271
357
|
fingerprintCollector,
|
|
272
358
|
engine,
|
|
273
359
|
keyField,
|
|
274
|
-
fingerprintMode
|
|
360
|
+
fingerprintMode,
|
|
361
|
+
codecSource,
|
|
362
|
+
codecDialect
|
|
275
363
|
),
|
|
276
364
|
};
|
|
277
365
|
}
|