@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 +40 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +121 -79
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +121 -79
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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")
|
|
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)
|
|
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(
|
|
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")
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
1105
|
+
const rows2 = await toRecordArray(
|
|
1106
|
+
await db.select(tableResource)
|
|
1107
|
+
);
|
|
1089
1108
|
const decoded2 = await Promise.all(
|
|
1090
|
-
rows2.map(
|
|
1109
|
+
rows2.map(
|
|
1110
|
+
(row) => decodeBaseRow(row)
|
|
1111
|
+
)
|
|
1091
1112
|
);
|
|
1092
1113
|
return decoded2;
|
|
1093
1114
|
}
|
|
1094
|
-
const rows = await tableAccess.loadSubset(
|
|
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(
|
|
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?.(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1332
|
+
value: normalizeRow(
|
|
1333
|
+
materializeCrdt(doc, id)
|
|
1334
|
+
)
|
|
1293
1335
|
});
|
|
1294
1336
|
}
|
|
1295
1337
|
} finally {
|