silosdk 0.0.6 → 0.0.7
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 +67 -10
- package/dist/source.cjs +239 -28
- package/dist/source.d.cts +26 -1
- package/dist/source.d.mts +26 -1
- package/dist/source.mjs +239 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,14 +25,14 @@ TanStack owns:
|
|
|
25
25
|
yarn add silosdk @tanstack/react-db @tanstack/react-query expo-crypto expo-sqlite expo-file-system
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Datastores
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
collections.
|
|
30
|
+
Datastores define SQLite-backed document collections and a link collection for
|
|
31
|
+
relationships between them. They produce options for TanStack DB collections.
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
34
|
import { createCollection } from '@tanstack/react-db'
|
|
35
|
-
import {
|
|
35
|
+
import { createDatastore, createID } from 'silosdk/source'
|
|
36
36
|
|
|
37
37
|
type Post = {
|
|
38
38
|
title: string
|
|
@@ -41,14 +41,28 @@ type Post = {
|
|
|
41
41
|
coverImage: string | null
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
type Tag = {
|
|
45
|
+
name: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const datastore = createDatastore<{
|
|
49
|
+
posts: Post
|
|
50
|
+
tags: Tag
|
|
51
|
+
}>({
|
|
52
|
+
posts: {
|
|
53
|
+
title: '',
|
|
54
|
+
body: '',
|
|
55
|
+
published: false,
|
|
56
|
+
coverImage: null,
|
|
57
|
+
},
|
|
58
|
+
tags: {
|
|
59
|
+
name: '',
|
|
60
|
+
},
|
|
49
61
|
})
|
|
50
62
|
|
|
51
|
-
export const posts = createCollection(
|
|
63
|
+
export const posts = createCollection(datastore.collectionOptions('posts'))
|
|
64
|
+
export const tags = createCollection(datastore.collectionOptions('tags'))
|
|
65
|
+
export const links = createCollection(datastore.linkCollectionOptions())
|
|
52
66
|
```
|
|
53
67
|
|
|
54
68
|
Query and mutate with TanStack DB directly:
|
|
@@ -95,6 +109,49 @@ Source rows are intentionally flat. Field values must be:
|
|
|
95
109
|
type FieldValue = string | number | boolean | null
|
|
96
110
|
```
|
|
97
111
|
|
|
112
|
+
Create links with the link collection utilities:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
await links.utils.link('posts', postId, 'tags', tagId)
|
|
116
|
+
await links.utils.unlink('posts', postId, 'tags', tagId)
|
|
117
|
+
|
|
118
|
+
const isLinked = links.utils.has('posts', postId, 'tags', tagId)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Link storage is flat and directionless. Collection names are sorted
|
|
122
|
+
lexicographically and IDs are stored in the same order. Query rows expose nested
|
|
123
|
+
paths for each collection pair:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { eq, useLiveQuery } from '@tanstack/react-db'
|
|
127
|
+
|
|
128
|
+
const { data: taggedPosts = [] } = useLiveQuery((q) =>
|
|
129
|
+
q
|
|
130
|
+
.from({ posts })
|
|
131
|
+
.join({ links }, ({ posts, links }) =>
|
|
132
|
+
eq(posts.id, links.tags.posts.id),
|
|
133
|
+
)
|
|
134
|
+
.join({ tags }, ({ links, tags }) =>
|
|
135
|
+
eq(links.posts.tags.id, tags.id),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For a `posts` to `tags` link:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
links.tags.posts.id // post id
|
|
144
|
+
links.posts.tags.id // tag id
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Same-collection links are not supported. Define another collection when you need
|
|
148
|
+
a different semantic relationship.
|
|
149
|
+
|
|
150
|
+
## Sources
|
|
151
|
+
|
|
152
|
+
The lower-level `source()` API remains available when you only need one
|
|
153
|
+
collection definition.
|
|
154
|
+
|
|
98
155
|
## Settings
|
|
99
156
|
|
|
100
157
|
Settings are small persisted primitive values backed by `expo-sqlite/kv-store`.
|
package/dist/source.cjs
CHANGED
|
@@ -14,49 +14,205 @@ const deletedDocs = /* @__PURE__ */ new WeakSet();
|
|
|
14
14
|
function createID() {
|
|
15
15
|
return (0, expo_crypto.randomUUID)();
|
|
16
16
|
}
|
|
17
|
+
function createDatastore(defaults) {
|
|
18
|
+
const names = Object.keys(defaults);
|
|
19
|
+
const nameSet = new Set(names);
|
|
20
|
+
for (const name of names) {
|
|
21
|
+
assertSourceName(name);
|
|
22
|
+
assertFlatDefaults(name, defaults[name]);
|
|
23
|
+
}
|
|
24
|
+
if (new Set(names).size !== names.length) throw new Error("Datastore collection names must be unique.");
|
|
25
|
+
return {
|
|
26
|
+
names,
|
|
27
|
+
collectionOptions(name, options = {}) {
|
|
28
|
+
assertDatastoreCollectionName(nameSet, name);
|
|
29
|
+
return createSourceCollectionOptions(name, defaults[name], options);
|
|
30
|
+
},
|
|
31
|
+
linkCollectionOptions(options = {}) {
|
|
32
|
+
return createLinkCollectionOptions(names, nameSet, options);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
17
36
|
function source(name, defaults) {
|
|
18
37
|
assertSourceName(name);
|
|
19
38
|
assertFlatDefaults(name, defaults);
|
|
20
39
|
if (sourceNames.has(name)) throw new Error(`Source "${name}" is already registered. Define each source once and import the existing source instead.`);
|
|
21
40
|
sourceNames.add(name);
|
|
22
|
-
const schema = createSourceSchema(name, defaults);
|
|
23
41
|
return {
|
|
24
42
|
name,
|
|
25
43
|
defaults,
|
|
26
|
-
schema,
|
|
44
|
+
schema: createSourceSchema(name, defaults),
|
|
27
45
|
collectionOptions(options = {}) {
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
return createSourceCollectionOptions(name, defaults, options);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function createSourceCollectionOptions(name, defaults, options = {}) {
|
|
51
|
+
return {
|
|
52
|
+
id: name,
|
|
53
|
+
getKey: (item) => item.id,
|
|
54
|
+
schema: createSourceSchema(name, defaults),
|
|
55
|
+
startSync: options.startSync,
|
|
56
|
+
sync: { sync({ begin, write, commit, markReady, truncate }) {
|
|
57
|
+
const rows = loadRows(name);
|
|
58
|
+
begin();
|
|
59
|
+
truncate();
|
|
60
|
+
for (const row of rows) write({
|
|
61
|
+
type: "insert",
|
|
62
|
+
value: row
|
|
63
|
+
});
|
|
64
|
+
commit();
|
|
65
|
+
markReady();
|
|
66
|
+
} },
|
|
67
|
+
onInsert: async ({ transaction }) => {
|
|
68
|
+
await insertRows(name, transaction.mutations.map((mutation) => mutation.modified));
|
|
69
|
+
},
|
|
70
|
+
onUpdate: async ({ transaction }) => {
|
|
71
|
+
await updateRows(name, transaction.mutations);
|
|
72
|
+
},
|
|
73
|
+
onDelete: async ({ transaction }) => {
|
|
74
|
+
await deleteRows(name, transaction.mutations);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function createLinkCollectionOptions(names, nameSet, options = {}) {
|
|
79
|
+
const schema = createLinkSchema();
|
|
80
|
+
let syncBegin;
|
|
81
|
+
let syncWrite;
|
|
82
|
+
let syncCommit;
|
|
83
|
+
const writeSyncedLink = (type, row) => {
|
|
84
|
+
if (!syncBegin || !syncWrite || !syncCommit) return;
|
|
85
|
+
syncBegin();
|
|
86
|
+
syncWrite({
|
|
87
|
+
type,
|
|
88
|
+
value: row
|
|
89
|
+
});
|
|
90
|
+
syncCommit();
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
id: "silo-links",
|
|
94
|
+
getKey: (item) => item.id,
|
|
95
|
+
schema,
|
|
96
|
+
startSync: options.startSync,
|
|
97
|
+
sync: { sync({ begin, write, commit, markReady, truncate }) {
|
|
98
|
+
syncBegin = begin;
|
|
99
|
+
syncWrite = write;
|
|
100
|
+
syncCommit = commit;
|
|
101
|
+
const rows = loadLinkRows().map((row) => createLinkRow(names, row));
|
|
102
|
+
begin();
|
|
103
|
+
truncate();
|
|
104
|
+
for (const row of rows) write({
|
|
105
|
+
type: "insert",
|
|
106
|
+
value: row
|
|
107
|
+
});
|
|
108
|
+
commit();
|
|
109
|
+
markReady();
|
|
110
|
+
} },
|
|
111
|
+
onInsert: async ({ transaction }) => {
|
|
112
|
+
for (const mutation of transaction.mutations) insertLinkRow(extractStoredLinkRow(mutation.modified));
|
|
113
|
+
},
|
|
114
|
+
onDelete: async ({ transaction }) => {
|
|
115
|
+
for (const mutation of transaction.mutations) deleteLinkRow(String(mutation.key));
|
|
116
|
+
},
|
|
117
|
+
utils: {
|
|
118
|
+
async link(collectionA, idA, collectionB, idB) {
|
|
119
|
+
const row = createStoredLinkRow(nameSet, collectionA, idA, collectionB, idB);
|
|
120
|
+
const exists = hasLinkRow(row.id);
|
|
121
|
+
insertLinkRow(row);
|
|
122
|
+
if (!exists) writeSyncedLink("insert", createLinkRow(names, row));
|
|
123
|
+
},
|
|
124
|
+
async unlink(collectionA, idA, collectionB, idB) {
|
|
125
|
+
const row = createStoredLinkRow(nameSet, collectionA, idA, collectionB, idB);
|
|
126
|
+
const exists = hasLinkRow(row.id);
|
|
127
|
+
deleteLinkRow(row.id);
|
|
128
|
+
if (exists) writeSyncedLink("delete", createLinkRow(names, row));
|
|
129
|
+
},
|
|
130
|
+
has(collectionA, idA, collectionB, idB) {
|
|
131
|
+
return hasLinkRow(createStoredLinkRow(nameSet, collectionA, idA, collectionB, idB).id);
|
|
132
|
+
}
|
|
54
133
|
}
|
|
55
134
|
};
|
|
56
135
|
}
|
|
57
136
|
function assertSourceName(name) {
|
|
58
137
|
if (!sourceNamePattern.test(name)) throw new Error(`Source names must start with a letter and contain only letters, numbers, underscores, or hyphens. Received "${name}".`);
|
|
59
138
|
}
|
|
139
|
+
function assertDatastoreCollectionName(nameSet, name) {
|
|
140
|
+
if (!nameSet.has(name)) throw new Error(`Datastore collection "${name}" is not registered.`);
|
|
141
|
+
}
|
|
142
|
+
function assertLinkCollectionName(nameSet, name) {
|
|
143
|
+
if (!nameSet.has(name)) throw new Error(`Cannot link unknown collection "${name}".`);
|
|
144
|
+
}
|
|
145
|
+
function createStoredLinkRow(nameSet, collectionA, idA, collectionB, idB) {
|
|
146
|
+
assertLinkCollectionName(nameSet, collectionA);
|
|
147
|
+
assertLinkCollectionName(nameSet, collectionB);
|
|
148
|
+
if (collectionA === collectionB) throw new Error(`Links between the same collection are not supported. Received "${collectionA}".`);
|
|
149
|
+
const [firstCollection, firstId, secondCollection, secondId] = collectionA < collectionB ? [
|
|
150
|
+
collectionA,
|
|
151
|
+
idA,
|
|
152
|
+
collectionB,
|
|
153
|
+
idB
|
|
154
|
+
] : [
|
|
155
|
+
collectionB,
|
|
156
|
+
idB,
|
|
157
|
+
collectionA,
|
|
158
|
+
idA
|
|
159
|
+
];
|
|
160
|
+
const collections = encodePair(firstCollection, secondCollection);
|
|
161
|
+
const ids = encodePair(firstId, secondId);
|
|
162
|
+
return {
|
|
163
|
+
id: encodePair(collections, ids),
|
|
164
|
+
collections,
|
|
165
|
+
ids,
|
|
166
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function createLinkRow(names, row) {
|
|
170
|
+
const [firstCollection, secondCollection] = decodePair(row.collections);
|
|
171
|
+
const [firstId, secondId] = decodePair(row.ids);
|
|
172
|
+
const result = {
|
|
173
|
+
id: row.id,
|
|
174
|
+
collections: row.collections,
|
|
175
|
+
ids: row.ids,
|
|
176
|
+
createdAt: row.createdAt
|
|
177
|
+
};
|
|
178
|
+
for (const other of names) {
|
|
179
|
+
const byCurrent = {};
|
|
180
|
+
for (const current of names) {
|
|
181
|
+
let id = null;
|
|
182
|
+
if (other !== current) {
|
|
183
|
+
if (current === firstCollection && other === secondCollection) id = firstId;
|
|
184
|
+
else if (current === secondCollection && other === firstCollection) id = secondId;
|
|
185
|
+
}
|
|
186
|
+
byCurrent[current] = { id };
|
|
187
|
+
}
|
|
188
|
+
result[other] = byCurrent;
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function extractStoredLinkRow(row) {
|
|
193
|
+
if (!isRecord(row)) throw new Error("Expected a link row object.");
|
|
194
|
+
if (typeof row.id !== "string" || typeof row.collections !== "string" || typeof row.ids !== "string" || typeof row.createdAt !== "string") throw new Error("Link rows must include string id, collections, ids, and createdAt fields.");
|
|
195
|
+
return {
|
|
196
|
+
id: row.id,
|
|
197
|
+
collections: row.collections,
|
|
198
|
+
ids: row.ids,
|
|
199
|
+
createdAt: row.createdAt
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function encodePair(first, second) {
|
|
203
|
+
return `${encodePart(first)}:${encodePart(second)}`;
|
|
204
|
+
}
|
|
205
|
+
function decodePair(value) {
|
|
206
|
+
const parts = value.split(":");
|
|
207
|
+
if (parts.length !== 2) throw new Error(`Invalid encoded link pair "${value}".`);
|
|
208
|
+
return [decodePart(parts[0]), decodePart(parts[1])];
|
|
209
|
+
}
|
|
210
|
+
function encodePart(value) {
|
|
211
|
+
return encodeURIComponent(value);
|
|
212
|
+
}
|
|
213
|
+
function decodePart(value) {
|
|
214
|
+
return decodeURIComponent(value);
|
|
215
|
+
}
|
|
60
216
|
function assertFlatDefaults(sourceName, defaults) {
|
|
61
217
|
for (const [field, defaultValue] of Object.entries(defaults)) {
|
|
62
218
|
if (reservedDocumentFields.has(field)) throw new Error(`Source "${sourceName}" field "${field}" is reserved by Silo documents.`);
|
|
@@ -85,6 +241,20 @@ function createSourceSchema(sourceName, defaults) {
|
|
|
85
241
|
}
|
|
86
242
|
} };
|
|
87
243
|
}
|
|
244
|
+
function createLinkSchema() {
|
|
245
|
+
return { "~standard": {
|
|
246
|
+
version: 1,
|
|
247
|
+
vendor: "silo",
|
|
248
|
+
validate(value) {
|
|
249
|
+
try {
|
|
250
|
+
extractStoredLinkRow(value);
|
|
251
|
+
return { value };
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return failure(error instanceof Error ? error.message : "Expected a link row.");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} };
|
|
257
|
+
}
|
|
88
258
|
function createDoc(id, data) {
|
|
89
259
|
const doc = {};
|
|
90
260
|
Object.defineProperty(doc, "id", {
|
|
@@ -160,6 +330,19 @@ function getDatabase() {
|
|
|
160
330
|
version INTEGER NOT NULL DEFAULT 1,
|
|
161
331
|
PRIMARY KEY (source, id)
|
|
162
332
|
);
|
|
333
|
+
|
|
334
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
335
|
+
id TEXT PRIMARY KEY,
|
|
336
|
+
collections TEXT NOT NULL,
|
|
337
|
+
ids TEXT NOT NULL,
|
|
338
|
+
createdAt TEXT NOT NULL
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
CREATE UNIQUE INDEX IF NOT EXISTS links_collections_ids_idx
|
|
342
|
+
ON links(collections, ids);
|
|
343
|
+
|
|
344
|
+
CREATE INDEX IF NOT EXISTS links_collections_idx
|
|
345
|
+
ON links(collections);
|
|
163
346
|
`);
|
|
164
347
|
}
|
|
165
348
|
return database;
|
|
@@ -211,11 +394,39 @@ function deleteRows(sourceName, mutations) {
|
|
|
211
394
|
if (mutation.original) deletedDocs.add(mutation.original);
|
|
212
395
|
deletedDocs.add(mutation.modified);
|
|
213
396
|
db.runSync(`DELETE FROM sources WHERE source = ? AND id = ?`, [sourceName, String(mutation.key)]);
|
|
397
|
+
deleteLinksForSourceRow(sourceName, String(mutation.key));
|
|
214
398
|
}
|
|
215
399
|
});
|
|
216
400
|
return Promise.resolve();
|
|
217
401
|
}
|
|
402
|
+
function loadLinkRows() {
|
|
403
|
+
return getDatabase().getAllSync(`SELECT id, collections, ids, createdAt FROM links`);
|
|
404
|
+
}
|
|
405
|
+
function insertLinkRow(row) {
|
|
406
|
+
getDatabase().runSync(`INSERT OR IGNORE INTO links (id, collections, ids, createdAt)
|
|
407
|
+
VALUES (?, ?, ?, ?)`, [
|
|
408
|
+
row.id,
|
|
409
|
+
row.collections,
|
|
410
|
+
row.ids,
|
|
411
|
+
row.createdAt
|
|
412
|
+
]);
|
|
413
|
+
}
|
|
414
|
+
function deleteLinkRow(id) {
|
|
415
|
+
getDatabase().runSync(`DELETE FROM links WHERE id = ?`, [id]);
|
|
416
|
+
}
|
|
417
|
+
function hasLinkRow(id) {
|
|
418
|
+
return !!getDatabase().getFirstSync(`SELECT id FROM links WHERE id = ?`, [id]);
|
|
419
|
+
}
|
|
420
|
+
function deleteLinksForSourceRow(sourceName, id) {
|
|
421
|
+
const rows = loadLinkRows();
|
|
422
|
+
for (const row of rows) {
|
|
423
|
+
const [firstCollection, secondCollection] = decodePair(row.collections);
|
|
424
|
+
const [firstId, secondId] = decodePair(row.ids);
|
|
425
|
+
if (firstCollection === sourceName && firstId === id || secondCollection === sourceName && secondId === id) deleteLinkRow(row.id);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
218
428
|
|
|
219
429
|
//#endregion
|
|
430
|
+
exports.createDatastore = createDatastore;
|
|
220
431
|
exports.createID = createID;
|
|
221
432
|
exports.source = source;
|
package/dist/source.d.cts
CHANGED
|
@@ -22,6 +22,30 @@ type SourceRow<T extends object> = Doc<T>;
|
|
|
22
22
|
type SourceCollectionOptions<T extends object> = {
|
|
23
23
|
startSync?: boolean;
|
|
24
24
|
};
|
|
25
|
+
type DatastoreDefaults<TCollections extends Record<string, object>> = { [Name in keyof TCollections]: SourceDefaults<TCollections[Name]> };
|
|
26
|
+
type LinkRow<TName extends string = string> = {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly collections: string;
|
|
29
|
+
readonly ids: string;
|
|
30
|
+
readonly createdAt: string;
|
|
31
|
+
} & { readonly [Other in TName]: { readonly [Current in TName]: {
|
|
32
|
+
readonly id: string | null;
|
|
33
|
+
} } };
|
|
34
|
+
type LinkUtils<TName extends string = string> = {
|
|
35
|
+
link(collectionA: TName, idA: string, collectionB: TName, idB: string): Promise<void>;
|
|
36
|
+
unlink(collectionA: TName, idA: string, collectionB: TName, idB: string): Promise<void>;
|
|
37
|
+
has(collectionA: TName, idA: string, collectionB: TName, idB: string): boolean;
|
|
38
|
+
};
|
|
39
|
+
type Datastore<TCollections extends Record<string, object>> = {
|
|
40
|
+
readonly names: ReadonlyArray<keyof TCollections & string>;
|
|
41
|
+
collectionOptions<Name$1 extends keyof TCollections & string>(name: Name$1, options?: SourceCollectionOptions<TCollections[Name$1]>): CollectionConfig<SourceRow<TCollections[Name$1]>, string, StandardSchemaV1<any, SourceRow<TCollections[Name$1]>>> & {
|
|
42
|
+
schema: StandardSchemaV1<any, SourceRow<TCollections[Name$1]>>;
|
|
43
|
+
};
|
|
44
|
+
linkCollectionOptions(options?: SourceCollectionOptions<LinkRow<keyof TCollections & string>>): CollectionConfig<LinkRow<keyof TCollections & string>, string, StandardSchemaV1<any, LinkRow<keyof TCollections & string>>, LinkUtils<keyof TCollections & string>> & {
|
|
45
|
+
schema: StandardSchemaV1<any, LinkRow<keyof TCollections & string>>;
|
|
46
|
+
utils: LinkUtils<keyof TCollections & string>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
25
49
|
type Source<T extends object> = {
|
|
26
50
|
readonly name: string;
|
|
27
51
|
readonly defaults: SourceDefaults<T>;
|
|
@@ -31,6 +55,7 @@ type Source<T extends object> = {
|
|
|
31
55
|
};
|
|
32
56
|
};
|
|
33
57
|
declare function createID(): string;
|
|
58
|
+
declare function createDatastore<TCollections extends Record<string, object>>(defaults: DatastoreDefaults<TCollections>): Datastore<TCollections>;
|
|
34
59
|
declare function source<T extends object>(name: string, defaults: SourceDefaults<T>): Source<T>;
|
|
35
60
|
//#endregion
|
|
36
|
-
export { DefaultValue, Doc, DocumentData, FieldValue, Source, SourceCollectionOptions, SourceDefaults, SourceInput, SourceRow, createID, source };
|
|
61
|
+
export { Datastore, DatastoreDefaults, DefaultValue, Doc, DocumentData, FieldValue, LinkRow, LinkUtils, Source, SourceCollectionOptions, SourceDefaults, SourceInput, SourceRow, createDatastore, createID, source };
|
package/dist/source.d.mts
CHANGED
|
@@ -22,6 +22,30 @@ type SourceRow<T extends object> = Doc<T>;
|
|
|
22
22
|
type SourceCollectionOptions<T extends object> = {
|
|
23
23
|
startSync?: boolean;
|
|
24
24
|
};
|
|
25
|
+
type DatastoreDefaults<TCollections extends Record<string, object>> = { [Name in keyof TCollections]: SourceDefaults<TCollections[Name]> };
|
|
26
|
+
type LinkRow<TName extends string = string> = {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly collections: string;
|
|
29
|
+
readonly ids: string;
|
|
30
|
+
readonly createdAt: string;
|
|
31
|
+
} & { readonly [Other in TName]: { readonly [Current in TName]: {
|
|
32
|
+
readonly id: string | null;
|
|
33
|
+
} } };
|
|
34
|
+
type LinkUtils<TName extends string = string> = {
|
|
35
|
+
link(collectionA: TName, idA: string, collectionB: TName, idB: string): Promise<void>;
|
|
36
|
+
unlink(collectionA: TName, idA: string, collectionB: TName, idB: string): Promise<void>;
|
|
37
|
+
has(collectionA: TName, idA: string, collectionB: TName, idB: string): boolean;
|
|
38
|
+
};
|
|
39
|
+
type Datastore<TCollections extends Record<string, object>> = {
|
|
40
|
+
readonly names: ReadonlyArray<keyof TCollections & string>;
|
|
41
|
+
collectionOptions<Name$1 extends keyof TCollections & string>(name: Name$1, options?: SourceCollectionOptions<TCollections[Name$1]>): CollectionConfig<SourceRow<TCollections[Name$1]>, string, StandardSchemaV1<any, SourceRow<TCollections[Name$1]>>> & {
|
|
42
|
+
schema: StandardSchemaV1<any, SourceRow<TCollections[Name$1]>>;
|
|
43
|
+
};
|
|
44
|
+
linkCollectionOptions(options?: SourceCollectionOptions<LinkRow<keyof TCollections & string>>): CollectionConfig<LinkRow<keyof TCollections & string>, string, StandardSchemaV1<any, LinkRow<keyof TCollections & string>>, LinkUtils<keyof TCollections & string>> & {
|
|
45
|
+
schema: StandardSchemaV1<any, LinkRow<keyof TCollections & string>>;
|
|
46
|
+
utils: LinkUtils<keyof TCollections & string>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
25
49
|
type Source<T extends object> = {
|
|
26
50
|
readonly name: string;
|
|
27
51
|
readonly defaults: SourceDefaults<T>;
|
|
@@ -31,6 +55,7 @@ type Source<T extends object> = {
|
|
|
31
55
|
};
|
|
32
56
|
};
|
|
33
57
|
declare function createID(): string;
|
|
58
|
+
declare function createDatastore<TCollections extends Record<string, object>>(defaults: DatastoreDefaults<TCollections>): Datastore<TCollections>;
|
|
34
59
|
declare function source<T extends object>(name: string, defaults: SourceDefaults<T>): Source<T>;
|
|
35
60
|
//#endregion
|
|
36
|
-
export { DefaultValue, Doc, DocumentData, FieldValue, Source, SourceCollectionOptions, SourceDefaults, SourceInput, SourceRow, createID, source };
|
|
61
|
+
export { Datastore, DatastoreDefaults, DefaultValue, Doc, DocumentData, FieldValue, LinkRow, LinkUtils, Source, SourceCollectionOptions, SourceDefaults, SourceInput, SourceRow, createDatastore, createID, source };
|
package/dist/source.mjs
CHANGED
|
@@ -13,49 +13,205 @@ const deletedDocs = /* @__PURE__ */ new WeakSet();
|
|
|
13
13
|
function createID() {
|
|
14
14
|
return randomUUID();
|
|
15
15
|
}
|
|
16
|
+
function createDatastore(defaults) {
|
|
17
|
+
const names = Object.keys(defaults);
|
|
18
|
+
const nameSet = new Set(names);
|
|
19
|
+
for (const name of names) {
|
|
20
|
+
assertSourceName(name);
|
|
21
|
+
assertFlatDefaults(name, defaults[name]);
|
|
22
|
+
}
|
|
23
|
+
if (new Set(names).size !== names.length) throw new Error("Datastore collection names must be unique.");
|
|
24
|
+
return {
|
|
25
|
+
names,
|
|
26
|
+
collectionOptions(name, options = {}) {
|
|
27
|
+
assertDatastoreCollectionName(nameSet, name);
|
|
28
|
+
return createSourceCollectionOptions(name, defaults[name], options);
|
|
29
|
+
},
|
|
30
|
+
linkCollectionOptions(options = {}) {
|
|
31
|
+
return createLinkCollectionOptions(names, nameSet, options);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
16
35
|
function source(name, defaults) {
|
|
17
36
|
assertSourceName(name);
|
|
18
37
|
assertFlatDefaults(name, defaults);
|
|
19
38
|
if (sourceNames.has(name)) throw new Error(`Source "${name}" is already registered. Define each source once and import the existing source instead.`);
|
|
20
39
|
sourceNames.add(name);
|
|
21
|
-
const schema = createSourceSchema(name, defaults);
|
|
22
40
|
return {
|
|
23
41
|
name,
|
|
24
42
|
defaults,
|
|
25
|
-
schema,
|
|
43
|
+
schema: createSourceSchema(name, defaults),
|
|
26
44
|
collectionOptions(options = {}) {
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
return createSourceCollectionOptions(name, defaults, options);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createSourceCollectionOptions(name, defaults, options = {}) {
|
|
50
|
+
return {
|
|
51
|
+
id: name,
|
|
52
|
+
getKey: (item) => item.id,
|
|
53
|
+
schema: createSourceSchema(name, defaults),
|
|
54
|
+
startSync: options.startSync,
|
|
55
|
+
sync: { sync({ begin, write, commit, markReady, truncate }) {
|
|
56
|
+
const rows = loadRows(name);
|
|
57
|
+
begin();
|
|
58
|
+
truncate();
|
|
59
|
+
for (const row of rows) write({
|
|
60
|
+
type: "insert",
|
|
61
|
+
value: row
|
|
62
|
+
});
|
|
63
|
+
commit();
|
|
64
|
+
markReady();
|
|
65
|
+
} },
|
|
66
|
+
onInsert: async ({ transaction }) => {
|
|
67
|
+
await insertRows(name, transaction.mutations.map((mutation) => mutation.modified));
|
|
68
|
+
},
|
|
69
|
+
onUpdate: async ({ transaction }) => {
|
|
70
|
+
await updateRows(name, transaction.mutations);
|
|
71
|
+
},
|
|
72
|
+
onDelete: async ({ transaction }) => {
|
|
73
|
+
await deleteRows(name, transaction.mutations);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function createLinkCollectionOptions(names, nameSet, options = {}) {
|
|
78
|
+
const schema = createLinkSchema();
|
|
79
|
+
let syncBegin;
|
|
80
|
+
let syncWrite;
|
|
81
|
+
let syncCommit;
|
|
82
|
+
const writeSyncedLink = (type, row) => {
|
|
83
|
+
if (!syncBegin || !syncWrite || !syncCommit) return;
|
|
84
|
+
syncBegin();
|
|
85
|
+
syncWrite({
|
|
86
|
+
type,
|
|
87
|
+
value: row
|
|
88
|
+
});
|
|
89
|
+
syncCommit();
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
id: "silo-links",
|
|
93
|
+
getKey: (item) => item.id,
|
|
94
|
+
schema,
|
|
95
|
+
startSync: options.startSync,
|
|
96
|
+
sync: { sync({ begin, write, commit, markReady, truncate }) {
|
|
97
|
+
syncBegin = begin;
|
|
98
|
+
syncWrite = write;
|
|
99
|
+
syncCommit = commit;
|
|
100
|
+
const rows = loadLinkRows().map((row) => createLinkRow(names, row));
|
|
101
|
+
begin();
|
|
102
|
+
truncate();
|
|
103
|
+
for (const row of rows) write({
|
|
104
|
+
type: "insert",
|
|
105
|
+
value: row
|
|
106
|
+
});
|
|
107
|
+
commit();
|
|
108
|
+
markReady();
|
|
109
|
+
} },
|
|
110
|
+
onInsert: async ({ transaction }) => {
|
|
111
|
+
for (const mutation of transaction.mutations) insertLinkRow(extractStoredLinkRow(mutation.modified));
|
|
112
|
+
},
|
|
113
|
+
onDelete: async ({ transaction }) => {
|
|
114
|
+
for (const mutation of transaction.mutations) deleteLinkRow(String(mutation.key));
|
|
115
|
+
},
|
|
116
|
+
utils: {
|
|
117
|
+
async link(collectionA, idA, collectionB, idB) {
|
|
118
|
+
const row = createStoredLinkRow(nameSet, collectionA, idA, collectionB, idB);
|
|
119
|
+
const exists = hasLinkRow(row.id);
|
|
120
|
+
insertLinkRow(row);
|
|
121
|
+
if (!exists) writeSyncedLink("insert", createLinkRow(names, row));
|
|
122
|
+
},
|
|
123
|
+
async unlink(collectionA, idA, collectionB, idB) {
|
|
124
|
+
const row = createStoredLinkRow(nameSet, collectionA, idA, collectionB, idB);
|
|
125
|
+
const exists = hasLinkRow(row.id);
|
|
126
|
+
deleteLinkRow(row.id);
|
|
127
|
+
if (exists) writeSyncedLink("delete", createLinkRow(names, row));
|
|
128
|
+
},
|
|
129
|
+
has(collectionA, idA, collectionB, idB) {
|
|
130
|
+
return hasLinkRow(createStoredLinkRow(nameSet, collectionA, idA, collectionB, idB).id);
|
|
131
|
+
}
|
|
53
132
|
}
|
|
54
133
|
};
|
|
55
134
|
}
|
|
56
135
|
function assertSourceName(name) {
|
|
57
136
|
if (!sourceNamePattern.test(name)) throw new Error(`Source names must start with a letter and contain only letters, numbers, underscores, or hyphens. Received "${name}".`);
|
|
58
137
|
}
|
|
138
|
+
function assertDatastoreCollectionName(nameSet, name) {
|
|
139
|
+
if (!nameSet.has(name)) throw new Error(`Datastore collection "${name}" is not registered.`);
|
|
140
|
+
}
|
|
141
|
+
function assertLinkCollectionName(nameSet, name) {
|
|
142
|
+
if (!nameSet.has(name)) throw new Error(`Cannot link unknown collection "${name}".`);
|
|
143
|
+
}
|
|
144
|
+
function createStoredLinkRow(nameSet, collectionA, idA, collectionB, idB) {
|
|
145
|
+
assertLinkCollectionName(nameSet, collectionA);
|
|
146
|
+
assertLinkCollectionName(nameSet, collectionB);
|
|
147
|
+
if (collectionA === collectionB) throw new Error(`Links between the same collection are not supported. Received "${collectionA}".`);
|
|
148
|
+
const [firstCollection, firstId, secondCollection, secondId] = collectionA < collectionB ? [
|
|
149
|
+
collectionA,
|
|
150
|
+
idA,
|
|
151
|
+
collectionB,
|
|
152
|
+
idB
|
|
153
|
+
] : [
|
|
154
|
+
collectionB,
|
|
155
|
+
idB,
|
|
156
|
+
collectionA,
|
|
157
|
+
idA
|
|
158
|
+
];
|
|
159
|
+
const collections = encodePair(firstCollection, secondCollection);
|
|
160
|
+
const ids = encodePair(firstId, secondId);
|
|
161
|
+
return {
|
|
162
|
+
id: encodePair(collections, ids),
|
|
163
|
+
collections,
|
|
164
|
+
ids,
|
|
165
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function createLinkRow(names, row) {
|
|
169
|
+
const [firstCollection, secondCollection] = decodePair(row.collections);
|
|
170
|
+
const [firstId, secondId] = decodePair(row.ids);
|
|
171
|
+
const result = {
|
|
172
|
+
id: row.id,
|
|
173
|
+
collections: row.collections,
|
|
174
|
+
ids: row.ids,
|
|
175
|
+
createdAt: row.createdAt
|
|
176
|
+
};
|
|
177
|
+
for (const other of names) {
|
|
178
|
+
const byCurrent = {};
|
|
179
|
+
for (const current of names) {
|
|
180
|
+
let id = null;
|
|
181
|
+
if (other !== current) {
|
|
182
|
+
if (current === firstCollection && other === secondCollection) id = firstId;
|
|
183
|
+
else if (current === secondCollection && other === firstCollection) id = secondId;
|
|
184
|
+
}
|
|
185
|
+
byCurrent[current] = { id };
|
|
186
|
+
}
|
|
187
|
+
result[other] = byCurrent;
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
function extractStoredLinkRow(row) {
|
|
192
|
+
if (!isRecord(row)) throw new Error("Expected a link row object.");
|
|
193
|
+
if (typeof row.id !== "string" || typeof row.collections !== "string" || typeof row.ids !== "string" || typeof row.createdAt !== "string") throw new Error("Link rows must include string id, collections, ids, and createdAt fields.");
|
|
194
|
+
return {
|
|
195
|
+
id: row.id,
|
|
196
|
+
collections: row.collections,
|
|
197
|
+
ids: row.ids,
|
|
198
|
+
createdAt: row.createdAt
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function encodePair(first, second) {
|
|
202
|
+
return `${encodePart(first)}:${encodePart(second)}`;
|
|
203
|
+
}
|
|
204
|
+
function decodePair(value) {
|
|
205
|
+
const parts = value.split(":");
|
|
206
|
+
if (parts.length !== 2) throw new Error(`Invalid encoded link pair "${value}".`);
|
|
207
|
+
return [decodePart(parts[0]), decodePart(parts[1])];
|
|
208
|
+
}
|
|
209
|
+
function encodePart(value) {
|
|
210
|
+
return encodeURIComponent(value);
|
|
211
|
+
}
|
|
212
|
+
function decodePart(value) {
|
|
213
|
+
return decodeURIComponent(value);
|
|
214
|
+
}
|
|
59
215
|
function assertFlatDefaults(sourceName, defaults) {
|
|
60
216
|
for (const [field, defaultValue] of Object.entries(defaults)) {
|
|
61
217
|
if (reservedDocumentFields.has(field)) throw new Error(`Source "${sourceName}" field "${field}" is reserved by Silo documents.`);
|
|
@@ -84,6 +240,20 @@ function createSourceSchema(sourceName, defaults) {
|
|
|
84
240
|
}
|
|
85
241
|
} };
|
|
86
242
|
}
|
|
243
|
+
function createLinkSchema() {
|
|
244
|
+
return { "~standard": {
|
|
245
|
+
version: 1,
|
|
246
|
+
vendor: "silo",
|
|
247
|
+
validate(value) {
|
|
248
|
+
try {
|
|
249
|
+
extractStoredLinkRow(value);
|
|
250
|
+
return { value };
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return failure(error instanceof Error ? error.message : "Expected a link row.");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} };
|
|
256
|
+
}
|
|
87
257
|
function createDoc(id, data) {
|
|
88
258
|
const doc = {};
|
|
89
259
|
Object.defineProperty(doc, "id", {
|
|
@@ -159,6 +329,19 @@ function getDatabase() {
|
|
|
159
329
|
version INTEGER NOT NULL DEFAULT 1,
|
|
160
330
|
PRIMARY KEY (source, id)
|
|
161
331
|
);
|
|
332
|
+
|
|
333
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
334
|
+
id TEXT PRIMARY KEY,
|
|
335
|
+
collections TEXT NOT NULL,
|
|
336
|
+
ids TEXT NOT NULL,
|
|
337
|
+
createdAt TEXT NOT NULL
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
CREATE UNIQUE INDEX IF NOT EXISTS links_collections_ids_idx
|
|
341
|
+
ON links(collections, ids);
|
|
342
|
+
|
|
343
|
+
CREATE INDEX IF NOT EXISTS links_collections_idx
|
|
344
|
+
ON links(collections);
|
|
162
345
|
`);
|
|
163
346
|
}
|
|
164
347
|
return database;
|
|
@@ -210,10 +393,37 @@ function deleteRows(sourceName, mutations) {
|
|
|
210
393
|
if (mutation.original) deletedDocs.add(mutation.original);
|
|
211
394
|
deletedDocs.add(mutation.modified);
|
|
212
395
|
db.runSync(`DELETE FROM sources WHERE source = ? AND id = ?`, [sourceName, String(mutation.key)]);
|
|
396
|
+
deleteLinksForSourceRow(sourceName, String(mutation.key));
|
|
213
397
|
}
|
|
214
398
|
});
|
|
215
399
|
return Promise.resolve();
|
|
216
400
|
}
|
|
401
|
+
function loadLinkRows() {
|
|
402
|
+
return getDatabase().getAllSync(`SELECT id, collections, ids, createdAt FROM links`);
|
|
403
|
+
}
|
|
404
|
+
function insertLinkRow(row) {
|
|
405
|
+
getDatabase().runSync(`INSERT OR IGNORE INTO links (id, collections, ids, createdAt)
|
|
406
|
+
VALUES (?, ?, ?, ?)`, [
|
|
407
|
+
row.id,
|
|
408
|
+
row.collections,
|
|
409
|
+
row.ids,
|
|
410
|
+
row.createdAt
|
|
411
|
+
]);
|
|
412
|
+
}
|
|
413
|
+
function deleteLinkRow(id) {
|
|
414
|
+
getDatabase().runSync(`DELETE FROM links WHERE id = ?`, [id]);
|
|
415
|
+
}
|
|
416
|
+
function hasLinkRow(id) {
|
|
417
|
+
return !!getDatabase().getFirstSync(`SELECT id FROM links WHERE id = ?`, [id]);
|
|
418
|
+
}
|
|
419
|
+
function deleteLinksForSourceRow(sourceName, id) {
|
|
420
|
+
const rows = loadLinkRows();
|
|
421
|
+
for (const row of rows) {
|
|
422
|
+
const [firstCollection, secondCollection] = decodePair(row.collections);
|
|
423
|
+
const [firstId, secondId] = decodePair(row.ids);
|
|
424
|
+
if (firstCollection === sourceName && firstId === id || secondCollection === sourceName && secondId === id) deleteLinkRow(row.id);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
217
427
|
|
|
218
428
|
//#endregion
|
|
219
|
-
export { createID, source };
|
|
429
|
+
export { createDatastore, createID, source };
|