@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 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: parseParams(tableSchema.columns, configRecord.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.207";
7418
- const WASM_VERSION = "0.0.1-canary.207";
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.207",
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 = parseParams(tableSchema.columns, q.selectQuery.vars ?? {});
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 = parseParams(tableSchema.columns, q.selectQuery.vars ?? {});
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.207",
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.207",
64
- "@spooky-sync/ssp-wasm": "0.0.1-canary.207",
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
- params: parseParams(tableSchema.columns, configRecord.params),
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 { parseParams, encodeRecordId, parseDuration } from './utils/index';
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 = parseParams(tableSchema.columns, q.selectQuery.vars ?? {});
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 = parseParams(tableSchema.columns, q.selectQuery.vars ?? {});
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);
@@ -1,125 +1,54 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { RecordId } from 'surrealdb';
3
- import { parseParams, cleanRecord } from './parser';
4
-
5
- describe('parseParams', () => {
6
- it('passes through plain values unchanged', () => {
7
- const schema = {
8
- name: { type: 'string' as const, optional: false },
9
- age: { type: 'number' as const, optional: false },
10
- };
11
- const result = parseParams(schema, { name: 'Alice', age: 30 });
12
- expect(result).toEqual({ name: 'Alice', age: 30 });
13
- });
14
-
15
- it('converts string to RecordId for recordId columns', () => {
16
- const schema = {
17
- owner: { type: 'string' as const, optional: false, recordId: true },
18
- };
19
- const result = parseParams(schema, { owner: 'user:123' });
20
- expect(result.owner).toBeInstanceOf(RecordId);
21
- expect(result.owner.table.toString()).toBe('user');
22
- expect(result.owner.id).toBe('123');
23
- });
24
-
25
- it('passes through existing RecordId for recordId columns', () => {
26
- const schema = {
27
- owner: { type: 'string' as const, optional: false, recordId: true },
28
- };
29
- const rid = new RecordId('user', '456');
30
- const result = parseParams(schema, { owner: rid });
31
- expect(result.owner).toBe(rid);
32
- });
33
-
34
- it('converts string to Date for dateTime columns', () => {
35
- const schema = {
36
- createdAt: { type: 'string' as const, optional: false, dateTime: true },
37
- };
38
- const result = parseParams(schema, { createdAt: '2024-01-01T00:00:00Z' });
39
- expect(result.createdAt).toBeInstanceOf(Date);
40
- expect(result.createdAt.toISOString()).toBe('2024-01-01T00:00:00.000Z');
41
- });
42
-
43
- it('converts number (timestamp) to Date for dateTime columns', () => {
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('cleanRecord', () => {
90
- const tableSchema = {
91
- name: { type: 'string' as const, optional: false },
92
- age: { type: 'number' as const, optional: false },
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
  });
@@ -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;