@spooky-sync/core 0.0.1-canary.207 → 0.0.1-canary.208
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/index.js +31 -7
- package/package.json +3 -3
- package/src/modules/data/index.ts +10 -1
- package/src/sp00ky.ts +3 -3
- package/src/utils/parser.test.ts +49 -120
- package/src/utils/parser.ts +30 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as renderOrderSql, c as serializeRow, i as projectedDataSql, l as resolveRelations, o as renderWhereSql, r as project, s as reviveRow, t as PROMOTION_OPEN_OPTIONS, u as stableKey } from "./sqlite-open.js";
|
|
2
2
|
import { DateTime, Duration, RecordId, Surreal, Uuid, applyDiagnostics, createRemoteEngines } from "surrealdb";
|
|
3
|
-
import { QueryBuilder, RecordId as RecordId$1, cyrb53 } from "@spooky-sync/query-builder";
|
|
3
|
+
import { QueryBuilder, RecordId as RecordId$1, baseFieldOfParam, cyrb53 } from "@spooky-sync/query-builder";
|
|
4
4
|
import pino from "pino";
|
|
5
5
|
import { applyPatch } from "fast-json-patch";
|
|
6
6
|
import init, { Sp00kyProcessor } from "@spooky-sync/ssp-wasm";
|
|
@@ -84,6 +84,10 @@ function cleanRecord(tableSchema, record) {
|
|
|
84
84
|
for (const [key, value] of Object.entries(record)) if (key === "id" || key.startsWith("_00_") || key in tableSchema) cleaned[key] = value;
|
|
85
85
|
return cleaned;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse a RECORD's fields against the table schema. Anything the schema does not
|
|
89
|
+
* know is dropped, which is what keeps a stray field out of a write.
|
|
90
|
+
*/
|
|
87
91
|
function parseParams(tableSchema, params) {
|
|
88
92
|
const parsedParams = {};
|
|
89
93
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -92,6 +96,26 @@ function parseParams(tableSchema, params) {
|
|
|
92
96
|
}
|
|
93
97
|
return parsedParams;
|
|
94
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse a QUERY's params. Unlike a record's fields, a param name is not always a
|
|
101
|
+
* column: an `_or` branch binds under a synthetic `white__or0` (see
|
|
102
|
+
* `orParamName` in @spooky-sync/query-builder), and the surql that references it
|
|
103
|
+
* is already written. So resolve the column through the synthetic name, and keep
|
|
104
|
+
* a param we cannot type rather than dropping it.
|
|
105
|
+
*
|
|
106
|
+
* Dropping was the bug: `parseParams` kept only column-named params, so every
|
|
107
|
+
* `_or` query registered with `$or0`/`$or1` unbound and matched no rows at all,
|
|
108
|
+
* silently (rowCount 0, errorCount 0).
|
|
109
|
+
*/
|
|
110
|
+
function parseQueryParams(tableSchema, params) {
|
|
111
|
+
const parsedParams = {};
|
|
112
|
+
for (const [key, value] of Object.entries(params)) {
|
|
113
|
+
if (value === void 0) continue;
|
|
114
|
+
const column = tableSchema[key] ?? tableSchema[baseFieldOfParam(key)];
|
|
115
|
+
parsedParams[key] = column ? parseValue(key, column, value) : value;
|
|
116
|
+
}
|
|
117
|
+
return parsedParams;
|
|
118
|
+
}
|
|
95
119
|
function parseValue(name, column, value) {
|
|
96
120
|
if (column.recordId) {
|
|
97
121
|
if (value instanceof RecordId$1) return value;
|
|
@@ -4735,7 +4759,7 @@ var DataModule = class DataModule {
|
|
|
4735
4759
|
...configRecord,
|
|
4736
4760
|
id: recordId,
|
|
4737
4761
|
plan,
|
|
4738
|
-
params:
|
|
4762
|
+
params: parseQueryParams(tableSchema.columns, params ?? configRecord.params),
|
|
4739
4763
|
membershipKey
|
|
4740
4764
|
};
|
|
4741
4765
|
if (membershipKey && !config.remoteArray?.length) {
|
|
@@ -7414,8 +7438,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
7414
7438
|
|
|
7415
7439
|
//#endregion
|
|
7416
7440
|
//#region src/modules/devtools/index.ts
|
|
7417
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
7418
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
7441
|
+
const CORE_VERSION = "0.0.1-canary.208";
|
|
7442
|
+
const WASM_VERSION = "0.0.1-canary.208";
|
|
7419
7443
|
const SURREAL_VERSION = "3.0.3";
|
|
7420
7444
|
var DevToolsService = class DevToolsService {
|
|
7421
7445
|
eventsHistory = [];
|
|
@@ -12399,7 +12423,7 @@ var Sp00kyClient = class {
|
|
|
12399
12423
|
return new TabsCoordinator({
|
|
12400
12424
|
tabId,
|
|
12401
12425
|
fingerprint: computeTabsFingerprint({
|
|
12402
|
-
coreVersion: "0.0.1-canary.
|
|
12426
|
+
coreVersion: "0.0.1-canary.208",
|
|
12403
12427
|
schemaHash: hash53(this.config.schemaSurql),
|
|
12404
12428
|
endpoint: this.config.database.endpoint ?? "",
|
|
12405
12429
|
namespace: this.config.database.namespace,
|
|
@@ -12836,7 +12860,7 @@ var Sp00kyClient = class {
|
|
|
12836
12860
|
async initQuery(table, q, ttl) {
|
|
12837
12861
|
const tableSchema = this.config.schema.tables.find((t) => t.name === table);
|
|
12838
12862
|
if (!tableSchema) throw new Error(`Table ${table} not found`);
|
|
12839
|
-
const params =
|
|
12863
|
+
const params = parseQueryParams(tableSchema.columns, q.selectQuery.vars ?? {});
|
|
12840
12864
|
const hash = await this.dataModule.query(table, q.selectQuery.query, params, ttl, q.selectQuery.plan);
|
|
12841
12865
|
if (!this.pendingQueryInits.has(hash)) {
|
|
12842
12866
|
const chain = this.finishQueryInit(hash, q, params).finally(() => {
|
|
@@ -12908,7 +12932,7 @@ var Sp00kyClient = class {
|
|
|
12908
12932
|
const tableName = q.tableName;
|
|
12909
12933
|
const tableSchema = this.config.schema.tables.find((t) => t.name === tableName);
|
|
12910
12934
|
if (!tableSchema) throw new Error(`Table ${tableName} not found`);
|
|
12911
|
-
const params =
|
|
12935
|
+
const params = parseQueryParams(tableSchema.columns, q.selectQuery.vars ?? {});
|
|
12912
12936
|
const hashKey = String(q.hash);
|
|
12913
12937
|
const marker = await this.dataModule.getPreloadMarker(hashKey);
|
|
12914
12938
|
if (!marker) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/core",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.208",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
64
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
63
|
+
"@spooky-sync/query-builder": "0.0.1-canary.208",
|
|
64
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.208",
|
|
65
65
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
66
66
|
"@surrealdb/wasm": "^3.0.3",
|
|
67
67
|
"blurhash": "^2.0.5",
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
withRetry,
|
|
39
39
|
surql,
|
|
40
40
|
parseParams,
|
|
41
|
+
parseQueryParams,
|
|
41
42
|
cleanRecord,
|
|
42
43
|
extractTablePart,
|
|
43
44
|
generateId,
|
|
@@ -2230,7 +2231,15 @@ export class DataModule<S extends SchemaStructure> {
|
|
|
2230
2231
|
// In-memory only — carries the engine-neutral plan so non-SurrealQL local
|
|
2231
2232
|
// engines materialize via `select(plan)` instead of parsing `surql`.
|
|
2232
2233
|
plan,
|
|
2233
|
-
|
|
2234
|
+
// The CALLER's params, not the ones read back out of the local store: the
|
|
2235
|
+
// store returns a RecordId as a plain `'table:id'` string, and
|
|
2236
|
+
// `parseQueryParams` can only turn it back into a RecordId for a column
|
|
2237
|
+
// the schema marks `recordId`. A union column (`string | record<x>`)
|
|
2238
|
+
// codegens as a plain string, so its param stayed a string and the view
|
|
2239
|
+
// compared a string against a record and matched nothing. The record id is
|
|
2240
|
+
// the same query either way - `recordId` is the hash of surql + vars - so
|
|
2241
|
+
// the in-memory value is the same one, with its types intact.
|
|
2242
|
+
params: parseQueryParams(tableSchema.columns, params ?? configRecord.params),
|
|
2234
2243
|
membershipKey,
|
|
2235
2244
|
};
|
|
2236
2245
|
|
package/src/sp00ky.ts
CHANGED
|
@@ -52,7 +52,7 @@ import { AppReleaseModule, AppReleaseHandle } from './modules/app-release/index'
|
|
|
52
52
|
import type { AppReleaseOptions } from './modules/app-release/index';
|
|
53
53
|
import { LocalStoragePersistenceClient } from './services/persistence/localstorage';
|
|
54
54
|
import { ANON_USER_ID, bucketIdForUser } from './modules/ref-tables';
|
|
55
|
-
import {
|
|
55
|
+
import { parseQueryParams, encodeRecordId, parseDuration } from './utils/index';
|
|
56
56
|
import { SurrealDBPersistenceClient } from './services/persistence/surrealdb';
|
|
57
57
|
import { ResilientPersistenceClient } from './services/persistence/resilient';
|
|
58
58
|
import { detectSharedTabsSupport } from './services/tabs/support';
|
|
@@ -1358,7 +1358,7 @@ export class Sp00kyClient<S extends SchemaStructure> {
|
|
|
1358
1358
|
throw new Error(`Table ${table} not found`);
|
|
1359
1359
|
}
|
|
1360
1360
|
|
|
1361
|
-
const params =
|
|
1361
|
+
const params = parseQueryParams(tableSchema.columns, q.selectQuery.vars ?? {});
|
|
1362
1362
|
const hash = await this.dataModule.query(
|
|
1363
1363
|
table,
|
|
1364
1364
|
q.selectQuery.query,
|
|
@@ -1470,7 +1470,7 @@ export class Sp00kyClient<S extends SchemaStructure> {
|
|
|
1470
1470
|
if (!tableSchema) {
|
|
1471
1471
|
throw new Error(`Table ${tableName} not found`);
|
|
1472
1472
|
}
|
|
1473
|
-
const params =
|
|
1473
|
+
const params = parseQueryParams(tableSchema.columns, q.selectQuery.vars ?? {});
|
|
1474
1474
|
const hashKey = String(q.hash);
|
|
1475
1475
|
|
|
1476
1476
|
const marker = await this.dataModule.getPreloadMarker(hashKey);
|
package/src/utils/parser.test.ts
CHANGED
|
@@ -1,125 +1,54 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import { RecordId } from '
|
|
3
|
-
import { parseParams,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const schema = {
|
|
45
|
-
createdAt: { type: 'string' as const, optional: false, dateTime: true },
|
|
46
|
-
};
|
|
47
|
-
const ts = 1704067200000; // 2024-01-01T00:00:00.000Z
|
|
48
|
-
const result = parseParams(schema, { createdAt: ts });
|
|
49
|
-
expect(result.createdAt).toBeInstanceOf(Date);
|
|
50
|
-
expect(result.createdAt.getTime()).toBe(ts);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('passes through existing Date for dateTime columns', () => {
|
|
54
|
-
const schema = {
|
|
55
|
-
createdAt: { type: 'string' as const, optional: false, dateTime: true },
|
|
56
|
-
};
|
|
57
|
-
const date = new Date('2024-01-01');
|
|
58
|
-
const result = parseParams(schema, { createdAt: date });
|
|
59
|
-
expect(result.createdAt).toBe(date);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('skips undefined values', () => {
|
|
63
|
-
const schema = {
|
|
64
|
-
name: { type: 'string' as const, optional: false },
|
|
65
|
-
age: { type: 'number' as const, optional: true },
|
|
66
|
-
};
|
|
67
|
-
const result = parseParams(schema, { name: 'Alice', age: undefined });
|
|
68
|
-
expect(result).toEqual({ name: 'Alice' });
|
|
69
|
-
expect('age' in result).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('throws on invalid recordId value', () => {
|
|
73
|
-
const schema = {
|
|
74
|
-
owner: { type: 'string' as const, optional: false, recordId: true },
|
|
75
|
-
};
|
|
76
|
-
expect(() => parseParams(schema, { owner: 12345 })).toThrow('Invalid value for owner');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('throws on invalid dateTime value', () => {
|
|
80
|
-
const schema = {
|
|
81
|
-
createdAt: { type: 'string' as const, optional: false, dateTime: true },
|
|
82
|
-
};
|
|
83
|
-
expect(() => parseParams(schema, { createdAt: true })).toThrow(
|
|
84
|
-
'Invalid value for createdAt'
|
|
85
|
-
);
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { RecordId } from '@spooky-sync/query-builder';
|
|
3
|
+
import { parseParams, parseQueryParams } from './parser';
|
|
4
|
+
|
|
5
|
+
const columns = {
|
|
6
|
+
white: { recordId: true },
|
|
7
|
+
black: { recordId: true },
|
|
8
|
+
owner: { recordId: true },
|
|
9
|
+
created_at: { dateTime: true },
|
|
10
|
+
result: {},
|
|
11
|
+
} as any;
|
|
12
|
+
|
|
13
|
+
describe('parseQueryParams', () => {
|
|
14
|
+
// The regression this exists for: an `_or` group binds under synthetic names,
|
|
15
|
+
// and dropping them left `(white = $white__or0 OR black = $black__or1)` with
|
|
16
|
+
// nothing bound, so every OR query registered and matched no rows at all.
|
|
17
|
+
it('keeps an _or branch param and types it through its field', () => {
|
|
18
|
+
const parsed = parseQueryParams(columns, {
|
|
19
|
+
owner: 'user:abc',
|
|
20
|
+
white__or0: 'player_name:PN_1',
|
|
21
|
+
black__or1: 'player_name:PN_1',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(Object.keys(parsed).sort()).toEqual(['black__or1', 'owner', 'white__or0']);
|
|
25
|
+
expect(parsed.white__or0).toBeInstanceOf(RecordId);
|
|
26
|
+
expect(String(parsed.white__or0)).toBe('player_name:PN_1');
|
|
27
|
+
expect(parsed.black__or1).toBeInstanceOf(RecordId);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('passes a param it cannot resolve through untouched instead of dropping it', () => {
|
|
31
|
+
const parsed = parseQueryParams(columns, { in: 'anything', limit: 30 });
|
|
32
|
+
expect(parsed).toEqual({ in: 'anything', limit: 30 });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('still types and keeps plain column params, and skips undefined', () => {
|
|
36
|
+
const parsed = parseQueryParams(columns, {
|
|
37
|
+
owner: 'user:abc',
|
|
38
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
39
|
+
result: undefined,
|
|
40
|
+
});
|
|
41
|
+
expect(parsed.owner).toBeInstanceOf(RecordId);
|
|
42
|
+
expect(parsed.created_at).toBeInstanceOf(Date);
|
|
43
|
+
expect('result' in parsed).toBe(false);
|
|
86
44
|
});
|
|
87
45
|
});
|
|
88
46
|
|
|
89
|
-
describe('
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
it('keeps schema fields and strips non-schema fields', () => {
|
|
96
|
-
const record = { id: 'user:1', name: 'Alice', age: 30, _internal: true, computed_score: 99 };
|
|
97
|
-
const result = cleanRecord(tableSchema, record);
|
|
98
|
-
expect(result).toEqual({ id: 'user:1', name: 'Alice', age: 30 });
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('always preserves id even though it is not in schema', () => {
|
|
102
|
-
const record = { id: 'user:2', extra: 'gone' };
|
|
103
|
-
const result = cleanRecord(tableSchema, record);
|
|
104
|
-
expect(result).toEqual({ id: 'user:2' });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('does not coerce or transform values', () => {
|
|
108
|
-
const date = new Date('2024-01-01');
|
|
109
|
-
const schema = { created: { type: 'string' as const, optional: false, dateTime: true } };
|
|
110
|
-
const record = { id: 'x:1', created: date };
|
|
111
|
-
const result = cleanRecord(schema, record);
|
|
112
|
-
expect(result.created).toBe(date);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('returns only id when record has no schema fields', () => {
|
|
116
|
-
const record = { id: 'user:3', unknown1: 'a', unknown2: 'b' };
|
|
117
|
-
const result = cleanRecord(tableSchema, record);
|
|
118
|
-
expect(result).toEqual({ id: 'user:3' });
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('handles empty record', () => {
|
|
122
|
-
const result = cleanRecord(tableSchema, {});
|
|
123
|
-
expect(result).toEqual({});
|
|
47
|
+
describe('parseParams', () => {
|
|
48
|
+
// Records keep the old behaviour: a field the schema does not know is dropped
|
|
49
|
+
// rather than written.
|
|
50
|
+
it('drops a field that is not a column', () => {
|
|
51
|
+
const parsed = parseParams(columns, { result: '1-0', nonsense: 'x', white__or0: 'y' });
|
|
52
|
+
expect(parsed).toEqual({ result: '1-0' });
|
|
124
53
|
});
|
|
125
54
|
});
|
package/src/utils/parser.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ColumnSchema} from '@spooky-sync/query-builder';
|
|
2
|
-
import { RecordId } from '@spooky-sync/query-builder';
|
|
2
|
+
import { RecordId, baseFieldOfParam } from '@spooky-sync/query-builder';
|
|
3
3
|
import { parseRecordIdString } from './index';
|
|
4
4
|
import { DateTime } from 'surrealdb';
|
|
5
5
|
|
|
@@ -16,6 +16,10 @@ export function cleanRecord(
|
|
|
16
16
|
return cleaned;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Parse a RECORD's fields against the table schema. Anything the schema does not
|
|
21
|
+
* know is dropped, which is what keeps a stray field out of a write.
|
|
22
|
+
*/
|
|
19
23
|
export function parseParams(
|
|
20
24
|
tableSchema: Record<string, ColumnSchema>,
|
|
21
25
|
params: Record<string, any>
|
|
@@ -31,6 +35,31 @@ export function parseParams(
|
|
|
31
35
|
return parsedParams;
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Parse a QUERY's params. Unlike a record's fields, a param name is not always a
|
|
40
|
+
* column: an `_or` branch binds under a synthetic `white__or0` (see
|
|
41
|
+
* `orParamName` in @spooky-sync/query-builder), and the surql that references it
|
|
42
|
+
* is already written. So resolve the column through the synthetic name, and keep
|
|
43
|
+
* a param we cannot type rather than dropping it.
|
|
44
|
+
*
|
|
45
|
+
* Dropping was the bug: `parseParams` kept only column-named params, so every
|
|
46
|
+
* `_or` query registered with `$or0`/`$or1` unbound and matched no rows at all,
|
|
47
|
+
* silently (rowCount 0, errorCount 0).
|
|
48
|
+
*/
|
|
49
|
+
export function parseQueryParams(
|
|
50
|
+
tableSchema: Record<string, ColumnSchema>,
|
|
51
|
+
params: Record<string, any>
|
|
52
|
+
) {
|
|
53
|
+
const parsedParams: Record<string, any> = {};
|
|
54
|
+
for (const [key, value] of Object.entries(params)) {
|
|
55
|
+
if (value === undefined) continue;
|
|
56
|
+
const column = tableSchema[key] ?? tableSchema[baseFieldOfParam(key)];
|
|
57
|
+
parsedParams[key] = column ? parseValue(key, column, value) : value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parsedParams;
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
function parseValue(name: string, column: ColumnSchema, value: any) {
|
|
35
64
|
if (column.recordId) {
|
|
36
65
|
if (value instanceof RecordId) return value;
|