@foretag/tanstack-db-surrealdb 0.6.0 → 0.6.2

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 CHANGED
@@ -190,6 +190,46 @@ DEFINE INDEX crdt_doc_ts ON crdt_update FIELDS doc, ts;
190
190
 
191
191
  If a single `crdt_update` table is shared across multiple base tables, use a union type such as `record<doc> | record<sheet>`.
192
192
 
193
+ ## Permissions Templates
194
+
195
+ The adapter does not manage Surreal table permissions. Define them in schema.
196
+
197
+ ### E2EE-only table permissions
198
+
199
+ ```sql
200
+ DEFINE TABLE secret_note SCHEMAFULL
201
+ PERMISSIONS
202
+ FOR select, create, update, delete WHERE owner = $auth.id;
203
+ ```
204
+
205
+ ### CRDT updates table permissions (append-only)
206
+
207
+ ```sql
208
+ DEFINE TABLE crdt_update SCHEMAFULL
209
+ PERMISSIONS
210
+ FOR select, create WHERE owner = $auth.id
211
+ FOR update, delete NONE;
212
+
213
+ -- Add owner metadata on update rows for simple ACL checks
214
+ DEFINE FIELD owner ON crdt_update TYPE record<account>;
215
+ DEFINE INDEX crdt_owner_doc_ts ON crdt_update FIELDS owner, doc, ts;
216
+ ```
217
+
218
+ ### CRDT snapshots table permissions
219
+
220
+ ```sql
221
+ DEFINE TABLE crdt_snapshot SCHEMAFULL
222
+ PERMISSIONS
223
+ FOR select WHERE owner = $auth.id
224
+ FOR create, update, delete NONE;
225
+
226
+ -- Common pattern: clients read snapshots; only trusted backend writes/prunes them
227
+ DEFINE FIELD owner ON crdt_snapshot TYPE record<account>;
228
+ DEFINE INDEX snap_owner_doc_ts ON crdt_snapshot FIELDS owner, doc, ts;
229
+ ```
230
+
231
+ If you run snapshot compaction from a trusted backend/service account, grant create/delete to that account only.
232
+
193
233
  ## Usage Snippets
194
234
 
195
235
  ### E2EE-only secret table
package/dist/index.d.mts CHANGED
@@ -136,8 +136,6 @@ type SurrealCollectionOptionsReturn<T extends {
136
136
  utils: UtilsRecord;
137
137
  };
138
138
 
139
- declare const toRecordKeyString: (rid: RecordId | string) => string;
140
-
141
139
  interface CRDTProfileAdapter<T extends object> {
142
140
  materialize: (doc: LoroDoc, id: string) => T;
143
141
  applyLocalChange: (doc: LoroDoc, change: LocalChange<T>) => void;
@@ -150,6 +148,8 @@ declare const materializeLoroRichtext: <T extends object>(doc: LoroDoc, id: stri
150
148
  declare const applyLoroRichtextChange: <T extends object>(doc: LoroDoc, change: LocalChange<T>) => void;
151
149
  declare const createLoroProfile: <T extends object = Record<string, unknown>>(profile: LoroProfile) => CRDTProfileAdapter<T>;
152
150
 
151
+ declare const toRecordKeyString: (rid: RecordId | string) => string;
152
+
153
153
  type MutationInput<T extends {
154
154
  id: string | RecordId;
155
155
  }> = Omit<T, 'id'> & {
package/dist/index.d.ts CHANGED
@@ -136,8 +136,6 @@ type SurrealCollectionOptionsReturn<T extends {
136
136
  utils: UtilsRecord;
137
137
  };
138
138
 
139
- declare const toRecordKeyString: (rid: RecordId | string) => string;
140
-
141
139
  interface CRDTProfileAdapter<T extends object> {
142
140
  materialize: (doc: LoroDoc, id: string) => T;
143
141
  applyLocalChange: (doc: LoroDoc, change: LocalChange<T>) => void;
@@ -150,6 +148,8 @@ declare const materializeLoroRichtext: <T extends object>(doc: LoroDoc, id: stri
150
148
  declare const applyLoroRichtextChange: <T extends object>(doc: LoroDoc, change: LocalChange<T>) => void;
151
149
  declare const createLoroProfile: <T extends object = Record<string, unknown>>(profile: LoroProfile) => CRDTProfileAdapter<T>;
152
150
 
151
+ declare const toRecordKeyString: (rid: RecordId | string) => string;
152
+
153
153
  type MutationInput<T extends {
154
154
  id: string | RecordId;
155
155
  }> = Omit<T, 'id'> & {
package/dist/index.js CHANGED
@@ -6,6 +6,66 @@ var surrealdb = require('surrealdb');
6
6
  var db = require('@tanstack/db');
7
7
 
8
8
  // src/index.ts
9
+
10
+ // src/crdt.ts
11
+ var toRecord = (value) => typeof value === "object" && value !== null ? value : {};
12
+ var materializeLoroJson = (doc, id) => {
13
+ const root = toRecord(doc.getMap("root").toJSON());
14
+ return {
15
+ id,
16
+ ...root
17
+ };
18
+ };
19
+ var applyLoroJsonChange = (doc, change) => {
20
+ const root = doc.getMap("root");
21
+ if (change.type === "delete") {
22
+ root.set("deleted", true);
23
+ return;
24
+ }
25
+ const value = toRecord(change.value);
26
+ for (const [key, fieldValue] of Object.entries(value)) {
27
+ if (key === "id") continue;
28
+ root.set(key, fieldValue);
29
+ }
30
+ };
31
+ var materializeLoroRichtext = (doc, id) => {
32
+ const metadata = toRecord(doc.getMap("root").toJSON());
33
+ const content = doc.getText("content").toString();
34
+ return {
35
+ id,
36
+ content,
37
+ ...metadata
38
+ };
39
+ };
40
+ var applyLoroRichtextChange = (doc, change) => {
41
+ const text = doc.getText("content");
42
+ const metadata = doc.getMap("root");
43
+ if (change.type === "delete") {
44
+ metadata.set("deleted", true);
45
+ return;
46
+ }
47
+ const value = toRecord(change.value);
48
+ for (const [key, fieldValue] of Object.entries(value)) {
49
+ if (key === "id") continue;
50
+ if (key === "content") {
51
+ if (typeof fieldValue === "string") text.update(fieldValue);
52
+ continue;
53
+ }
54
+ metadata.set(key, fieldValue);
55
+ }
56
+ };
57
+ var createLoroProfile = (profile) => {
58
+ if (profile === "richtext") {
59
+ return {
60
+ materialize: materializeLoroRichtext,
61
+ applyLocalChange: applyLoroRichtextChange
62
+ };
63
+ }
64
+ return {
65
+ materialize: materializeLoroJson,
66
+ applyLocalChange: applyLoroJsonChange
67
+ };
68
+ };
9
69
  var recordIdIdentityPool = /* @__PURE__ */ new Map();
10
70
  var nativeRecordIdPool = /* @__PURE__ */ new Map();
11
71
  var internRecordIdIdentity = (canonical, preferred) => {
@@ -193,66 +253,6 @@ var toRecordId = (tableName, id) => {
193
253
  const key = normalized.startsWith(prefixed) ? normalized.slice(prefixed.length) : normalized;
194
254
  return new surrealdb.RecordId(tableName, key);
195
255
  };
196
-
197
- // src/crdt.ts
198
- var toRecord = (value) => typeof value === "object" && value !== null ? value : {};
199
- var materializeLoroJson = (doc, id) => {
200
- const root = toRecord(doc.getMap("root").toJSON());
201
- return {
202
- id,
203
- ...root
204
- };
205
- };
206
- var applyLoroJsonChange = (doc, change) => {
207
- const root = doc.getMap("root");
208
- if (change.type === "delete") {
209
- root.set("deleted", true);
210
- return;
211
- }
212
- const value = toRecord(change.value);
213
- for (const [key, fieldValue] of Object.entries(value)) {
214
- if (key === "id") continue;
215
- root.set(key, fieldValue);
216
- }
217
- };
218
- var materializeLoroRichtext = (doc, id) => {
219
- const metadata = toRecord(doc.getMap("root").toJSON());
220
- const content = doc.getText("content").toString();
221
- return {
222
- id,
223
- content,
224
- ...metadata
225
- };
226
- };
227
- var applyLoroRichtextChange = (doc, change) => {
228
- const text = doc.getText("content");
229
- const metadata = doc.getMap("root");
230
- if (change.type === "delete") {
231
- metadata.set("deleted", true);
232
- return;
233
- }
234
- const value = toRecord(change.value);
235
- for (const [key, fieldValue] of Object.entries(value)) {
236
- if (key === "id") continue;
237
- if (key === "content") {
238
- if (typeof fieldValue === "string") text.update(fieldValue);
239
- continue;
240
- }
241
- metadata.set(key, fieldValue);
242
- }
243
- };
244
- var createLoroProfile = (profile) => {
245
- if (profile === "richtext") {
246
- return {
247
- materialize: materializeLoroRichtext,
248
- applyLocalChange: applyLoroRichtextChange
249
- };
250
- }
251
- return {
252
- materialize: materializeLoroJson,
253
- applyLocalChange: applyLoroJsonChange
254
- };
255
- };
256
256
  var IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
257
257
  var firstRow = (res) => {
258
258
  if (!res) return void 0;
@@ -291,11 +291,12 @@ var mapRelationPath = (path, relation) => {
291
291
  return path;
292
292
  };
293
293
  var normalizeFilterValue = (value) => {
294
- if (Array.isArray(value)) {
294
+ if (Array.isArray(value))
295
295
  return value.map((item) => normalizeFilterValue(item));
296
- }
297
296
  const native = toNativeRecordIdLikeValue(value);
298
297
  if (native !== value) return native;
298
+ if (value && typeof value === "object")
299
+ return normalizeRecordIdLikeValue(value);
299
300
  return normalizeRecordIdLikeValue(value);
300
301
  };
301
302
  var toSqlFragment = (value) => {
@@ -454,7 +455,8 @@ function manageTable(db, config) {
454
455
  if (action === "KILLED") return;
455
456
  if (action === "CREATE") cb({ type: "insert", row: value });
456
457
  else if (action === "UPDATE") cb({ type: "update", row: value });
457
- else if (action === "DELETE") cb({ type: "delete", row: { id: value.id } });
458
+ else if (action === "DELETE")
459
+ cb({ type: "delete", row: { id: value.id } });
458
460
  };
459
461
  const start = async () => {
460
462
  if (!db.isFeatureSupported(surrealdb.Features.LiveQueries)) return;
@@ -687,7 +689,8 @@ function createInsertSchema(tableName) {
687
689
  const data = normalizeRecordIdLikeFields({
688
690
  ...value
689
691
  });
690
- if (!data.id) data.id = createTempRecordId(tableName);
692
+ if (!data.id)
693
+ data.id = createTempRecordId(tableName);
691
694
  return { value: data };
692
695
  },
693
696
  types: void 0
@@ -942,7 +945,9 @@ function modernSurrealCollectionOptions(config) {
942
945
  const ensureBaseLive = async () => {
943
946
  if (cleanupBaseLive !== NOOP) return;
944
947
  if (!db.isFeatureSupported?.(surrealdb.Features.LiveQueries)) return;
945
- const live = await db.live(tableResource);
948
+ const live = await db.live(
949
+ tableResource
950
+ );
946
951
  if (killed) {
947
952
  await live.kill();
948
953
  return;
@@ -952,14 +957,18 @@ function modernSurrealCollectionOptions(config) {
952
957
  const row = message.value;
953
958
  const id = toRecordKeyString(row.id);
954
959
  const wasVisible = activeOnDemandIds.has(id);
955
- if (isStrictOnDemand && !wasVisible && message.action !== "DELETE") return;
960
+ if (isStrictOnDemand && !wasVisible && message.action !== "DELETE")
961
+ return;
956
962
  if (message.action === "DELETE") {
957
963
  for (const ids of subsetIds.values()) ids.delete(id);
958
964
  updateActiveOnDemandIds();
959
965
  if (isStrictOnDemand && !wasVisible) return;
960
966
  ctx.begin();
961
967
  try {
962
- ctx.write({ type: "delete", key: `${tableName}:${id}` });
968
+ ctx.write({
969
+ type: "delete",
970
+ key: `${tableName}:${id}`
971
+ });
963
972
  } finally {
964
973
  ctx.commit();
965
974
  }
@@ -985,7 +994,9 @@ function modernSurrealCollectionOptions(config) {
985
994
  if (!crdtEnabled || !updatesTable) return;
986
995
  if (cleanupUpdateLive !== NOOP) return;
987
996
  if (!db.isFeatureSupported?.(surrealdb.Features.LiveQueries)) return;
988
- const live = await db.live(updatesTable);
997
+ const live = await db.live(
998
+ updatesTable
999
+ );
989
1000
  if (killed) {
990
1001
  await live.kill();
991
1002
  return;
@@ -1037,7 +1048,13 @@ function modernSurrealCollectionOptions(config) {
1037
1048
  return;
1038
1049
  }
1039
1050
  for (const id of ids) {
1040
- await hydrateCrdtDoc(id, ctx.write, ctx.begin, ctx.commit, "insert");
1051
+ await hydrateCrdtDoc(
1052
+ id,
1053
+ ctx.write,
1054
+ ctx.begin,
1055
+ ctx.commit,
1056
+ "insert"
1057
+ );
1041
1058
  }
1042
1059
  await ensureUpdateLive();
1043
1060
  };
@@ -1085,13 +1102,19 @@ function modernSurrealCollectionOptions(config) {
1085
1102
  }
1086
1103
  if (!crdtEnabled) {
1087
1104
  if (!isOnDemandLike) {
1088
- const rows2 = await toRecordArray(await db.select(tableResource));
1105
+ const rows2 = await toRecordArray(
1106
+ await db.select(tableResource)
1107
+ );
1089
1108
  const decoded2 = await Promise.all(
1090
- rows2.map((row) => decodeBaseRow(row))
1109
+ rows2.map(
1110
+ (row) => decodeBaseRow(row)
1111
+ )
1091
1112
  );
1092
1113
  return decoded2;
1093
1114
  }
1094
- const rows = await tableAccess.loadSubset(meta?.loadSubsetOptions);
1115
+ const rows = await tableAccess.loadSubset(
1116
+ meta?.loadSubsetOptions
1117
+ );
1095
1118
  const decoded = await Promise.all(
1096
1119
  rows.map(
1097
1120
  (row) => decodeBaseRow(row)
@@ -1177,7 +1200,9 @@ function modernSurrealCollectionOptions(config) {
1177
1200
  const writeUtils = getWriteUtils(params.collection.utils);
1178
1201
  for (const mutation of params.transaction.mutations) {
1179
1202
  if (mutation.type !== "update") continue;
1180
- const mutationId = normalizeMutationId(mutation.key);
1203
+ const mutationId = normalizeMutationId(
1204
+ mutation.key
1205
+ );
1181
1206
  const normalizedModified = omitUndefined(
1182
1207
  normalizeRecordIdLikeFields({
1183
1208
  ...mutation.modified
@@ -1209,7 +1234,9 @@ function modernSurrealCollectionOptions(config) {
1209
1234
  toRecordKeyString(mutationId)
1210
1235
  );
1211
1236
  await db.update(mutationId).merge(encoded);
1212
- writeUtils.writeUpsert?.(normalizeRow({ ...decodedCurrent, ...merged }));
1237
+ writeUtils.writeUpsert?.(
1238
+ normalizeRow({ ...decodedCurrent, ...merged })
1239
+ );
1213
1240
  continue;
1214
1241
  }
1215
1242
  const id = toRecordKeyString(mutationId);
@@ -1232,7 +1259,9 @@ function modernSurrealCollectionOptions(config) {
1232
1259
  const writeUtils = getWriteUtils(params.collection.utils);
1233
1260
  for (const mutation of params.transaction.mutations) {
1234
1261
  if (mutation.type !== "delete") continue;
1235
- const mutationId = normalizeMutationId(mutation.key);
1262
+ const mutationId = normalizeMutationId(
1263
+ mutation.key
1264
+ );
1236
1265
  const id = toRecordKeyString(mutationId);
1237
1266
  if (!crdtEnabled) {
1238
1267
  await db.delete(mutationId);
@@ -1259,14 +1288,22 @@ function modernSurrealCollectionOptions(config) {
1259
1288
  const canRunBaseSync = typeof ctx.collection?.on === "function";
1260
1289
  const baseResult = canRunBaseSync ? baseSync(ctx) : void 0;
1261
1290
  const baseCleanup = typeof baseResult === "function" ? baseResult : typeof baseResult === "object" && baseResult && "cleanup" in baseResult && typeof baseResult.cleanup === "function" ? baseResult.cleanup : NOOP;
1262
- const runtime = createSyncRuntime(ctx);
1291
+ const runtime = createSyncRuntime(
1292
+ ctx
1293
+ );
1263
1294
  const start = async () => {
1264
1295
  if (!isOnDemandLike) {
1265
1296
  if (!crdtEnabled) {
1266
1297
  const rows = toRecordArray(
1267
1298
  await db.select(tableResource)
1268
1299
  );
1269
- await hydratePlainRows(rows, ctx.write, ctx.begin, ctx.commit, "insert");
1300
+ await hydratePlainRows(
1301
+ rows,
1302
+ ctx.write,
1303
+ ctx.begin,
1304
+ ctx.commit,
1305
+ "insert"
1306
+ );
1270
1307
  await runtime.startRealtime();
1271
1308
  ctx.markReady();
1272
1309
  return;
@@ -1280,7 +1317,10 @@ function modernSurrealCollectionOptions(config) {
1280
1317
  for (const update of updates) {
1281
1318
  const id = idFromDocRef(update.doc);
1282
1319
  const doc = getDoc(id);
1283
- const bytes = await decodeUpdateBytes(update, "update");
1320
+ const bytes = await decodeUpdateBytes(
1321
+ update,
1322
+ "update"
1323
+ );
1284
1324
  if (!bytes.byteLength) continue;
1285
1325
  doc.import(bytes);
1286
1326
  }
@@ -1289,7 +1329,9 @@ function modernSurrealCollectionOptions(config) {
1289
1329
  for (const [id, doc] of docs.entries()) {
1290
1330
  ctx.write({
1291
1331
  type: "insert",
1292
- value: normalizeRow(materializeCrdt(doc, id))
1332
+ value: normalizeRow(
1333
+ materializeCrdt(doc, id)
1334
+ )
1293
1335
  });
1294
1336
  }
1295
1337
  } finally {