bleam 0.0.11 → 0.0.13
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/ai.cjs +2315 -2173
- package/dist/ai.d.cts +227 -322
- package/dist/ai.d.ts +227 -322
- package/dist/ai.js +2310 -2159
- package/dist/app-storage-D8W4n8ey.cjs +39 -0
- package/dist/app-storage-Isi5Bo0R.js +34 -0
- package/dist/cli.cjs +148 -22
- package/dist/cli.d.cts +14 -1
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +148 -22
- package/dist/config.d.cts +1 -1
- package/dist/elements-CFk0QHw0.d.cts +127 -0
- package/dist/{ui-CHc4xEs_.d.ts → elements-ClGQ41Sc.d.ts} +84 -49
- package/dist/{ui.cjs → elements.cjs} +307 -168
- package/dist/elements.d.cts +2 -0
- package/dist/elements.d.ts +2 -0
- package/dist/{ui.js → elements.js} +306 -169
- package/dist/{files-Ds1wT8C2.js → files-DErLhzCB.js} +11 -12
- package/dist/{files-Bo7h9fik.cjs → files-lMk-CpL_.cjs} +16 -11
- package/dist/files.cjs +1 -1
- package/dist/files.d.cts +1 -1
- package/dist/files.js +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/schema-B7ELMpuI.js +226 -0
- package/dist/schema-B7SLUBLN.cjs +286 -0
- package/dist/schema-BWsDPc6c.d.cts +125 -0
- package/dist/schema-LxnzAfgw.d.ts +125 -0
- package/dist/schema.cjs +10 -2
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +2 -2
- package/dist/secrets.cjs +146 -0
- package/dist/secrets.d.cts +14 -0
- package/dist/secrets.d.ts +14 -0
- package/dist/secrets.js +142 -0
- package/dist/state-BZYyrE2-.cjs +936 -0
- package/dist/state-DkaRFkZJ.js +843 -0
- package/dist/state.cjs +17 -12
- package/dist/state.d.cts +144 -140
- package/dist/state.d.ts +145 -140
- package/dist/state.js +4 -3
- package/dist/window.d.cts +1 -1
- package/dist/window.d.ts +3 -3
- package/package.json +13 -6
- package/templates/basic/app/index.tsx +2 -2
- package/templates/foundation-models/app/index.tsx +78 -16
- package/templates/image-generation/app/index.tsx +4 -4
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +46 -46
- package/templates/native/ios/Podfile.lock +173 -173
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +42 -365
- package/templates/state/app/index.tsx +2 -2
- package/templates/text-generation/app/index.tsx +83 -52
- package/templates/updates/README.md +1 -1
- package/dist/schema-Bo5Jvqus.js +0 -90
- package/dist/schema-CYh6n8GS.d.ts +0 -58
- package/dist/schema-oeOrd3i1.d.cts +0 -58
- package/dist/schema-rQ13mrpD.cjs +0 -102
- package/dist/state-Bx0VlTlO.cjs +0 -852
- package/dist/state-CAwe-Vw1.js +0 -767
- package/dist/ui-1WepaMS4.d.cts +0 -92
- package/dist/ui.d.cts +0 -2
- package/dist/ui.d.ts +0 -2
- /package/dist/{config-CufOVJV3.d.cts → config-COcRnn5a.d.cts} +0 -0
- /package/dist/{files-Dt5mbzLq.d.cts → files-DwA7pzr3.d.cts} +0 -0
- /package/dist/{native-sqlite-yQLD5s9i.cjs → native-sqlite-Dw--FI9a.cjs} +0 -0
- /package/dist/{native-sqlite-xcGdamRD.js → native-sqlite-WzRNzCSh.js} +0 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
import { t as crypto } from "./crypto-BB92-Upx.js";
|
|
2
|
+
import { t as currentAppStorageKey } from "./app-storage-Isi5Bo0R.js";
|
|
3
|
+
import { l as schema } from "./schema-B7ELMpuI.js";
|
|
4
|
+
import { useAtom, useAtomValue, useSetAtom } from "jotai/react";
|
|
5
|
+
import { atom, getDefaultStore } from "jotai/vanilla";
|
|
6
|
+
import { atomFamily } from "jotai-family";
|
|
7
|
+
|
|
8
|
+
//#region src/state/atom.ts
|
|
9
|
+
const storedAtomsKey = Symbol.for("bleam.state.storedAtoms.v1");
|
|
10
|
+
function storedAtoms() {
|
|
11
|
+
const scope = globalThis;
|
|
12
|
+
scope[storedAtomsKey] ??= /* @__PURE__ */ new Map();
|
|
13
|
+
return scope[storedAtomsKey];
|
|
14
|
+
}
|
|
15
|
+
function asJotaiAtom(value) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function asJotaiWritableAtom(value) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function asAtom(value) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
function asWritableAtom(value) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function storageKey(key) {
|
|
28
|
+
const trimmed = key.trim();
|
|
29
|
+
if (!trimmed) throw new Error("Atom storage key must not be empty");
|
|
30
|
+
return `bleam.apps.${currentAppStorageKey()}.atoms.${trimmed}`;
|
|
31
|
+
}
|
|
32
|
+
function assertStoredValue(value, seen = /* @__PURE__ */ new Set()) {
|
|
33
|
+
if (value === null) return;
|
|
34
|
+
if (value === void 0) throw new Error("Persisted atom objects cannot contain undefined");
|
|
35
|
+
if (typeof value === "string" || typeof value === "boolean") return;
|
|
36
|
+
if (typeof value === "number") {
|
|
37
|
+
if (!Number.isFinite(value)) throw new Error("Persisted atom numbers must be finite");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value !== "object") throw new Error("Persisted atom values must be JSON-safe");
|
|
41
|
+
if (seen.has(value)) throw new Error("Persisted atom values cannot be cyclic");
|
|
42
|
+
seen.add(value);
|
|
43
|
+
if (Array.isArray(value)) for (const item of value) {
|
|
44
|
+
if (item === void 0) throw new Error("Persisted atom arrays cannot contain undefined");
|
|
45
|
+
assertStoredValue(item, seen);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const prototype = Object.getPrototypeOf(value);
|
|
49
|
+
if (prototype !== Object.prototype && prototype !== null) throw new Error("Persisted atom objects must be plain objects");
|
|
50
|
+
for (const item of Object.values(value)) assertStoredValue(item, seen);
|
|
51
|
+
}
|
|
52
|
+
seen.delete(value);
|
|
53
|
+
}
|
|
54
|
+
function encoded(value) {
|
|
55
|
+
if (value === void 0) return void 0;
|
|
56
|
+
assertStoredValue(value);
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
function persistedAtom(initialValue, key) {
|
|
60
|
+
const resolvedKey = storageKey(key);
|
|
61
|
+
const initial = encoded(initialValue);
|
|
62
|
+
const existing = storedAtoms().get(resolvedKey);
|
|
63
|
+
if (existing) {
|
|
64
|
+
if (process.env.NODE_ENV !== "production" && existing.initial !== initial) console.warn(`Atom storage key "${key}" was declared with different initial values`);
|
|
65
|
+
return existing.atom;
|
|
66
|
+
}
|
|
67
|
+
const defaults = NSUserDefaults.standardUserDefaults;
|
|
68
|
+
const stored = defaults.objectForKey(resolvedKey);
|
|
69
|
+
let value = initialValue;
|
|
70
|
+
if (typeof stored === "string") try {
|
|
71
|
+
value = JSON.parse(stored);
|
|
72
|
+
assertStoredValue(value);
|
|
73
|
+
} catch {
|
|
74
|
+
defaults.removeObjectForKey(resolvedKey);
|
|
75
|
+
value = initialValue;
|
|
76
|
+
}
|
|
77
|
+
const base = atom(value);
|
|
78
|
+
const result = asWritableAtom(atom((get) => get(base), (get, set, update) => {
|
|
79
|
+
const next = typeof update === "function" ? update(get(base)) : update;
|
|
80
|
+
const nextEncoded = encoded(next);
|
|
81
|
+
if (nextEncoded === void 0) defaults.removeObjectForKey(resolvedKey);
|
|
82
|
+
else defaults.setObjectForKey(nextEncoded, resolvedKey);
|
|
83
|
+
set(base, next);
|
|
84
|
+
}));
|
|
85
|
+
storedAtoms().set(resolvedKey, {
|
|
86
|
+
atom: result,
|
|
87
|
+
initial
|
|
88
|
+
});
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
function atom$1(initialOrRead, optionsOrWrite) {
|
|
92
|
+
if (typeof initialOrRead === "function") {
|
|
93
|
+
const read = initialOrRead;
|
|
94
|
+
const wrappedRead = (get) => read((value) => get(asJotaiAtom(value)));
|
|
95
|
+
if (typeof optionsOrWrite === "function") {
|
|
96
|
+
const write = optionsOrWrite;
|
|
97
|
+
return asWritableAtom(atom(wrappedRead, (get, set, ...args) => write((value) => get(asJotaiAtom(value)), (value, ...nextArgs) => set(asJotaiWritableAtom(value), ...nextArgs), ...args)));
|
|
98
|
+
}
|
|
99
|
+
return asAtom(atom(wrappedRead));
|
|
100
|
+
}
|
|
101
|
+
const options = optionsOrWrite;
|
|
102
|
+
return options?.storage ? persistedAtom(initialOrRead, options.storage) : asWritableAtom(atom(initialOrRead));
|
|
103
|
+
}
|
|
104
|
+
function useAtomValue$1(value) {
|
|
105
|
+
return useAtomValue(asJotaiAtom(value));
|
|
106
|
+
}
|
|
107
|
+
function useSetAtom$1(value) {
|
|
108
|
+
return useSetAtom(asJotaiWritableAtom(value));
|
|
109
|
+
}
|
|
110
|
+
function useAtom$1(value) {
|
|
111
|
+
return useAtom(asJotaiWritableAtom(value));
|
|
112
|
+
}
|
|
113
|
+
function internalAtom(value) {
|
|
114
|
+
return asAtom(value);
|
|
115
|
+
}
|
|
116
|
+
function internalWritableAtom(value) {
|
|
117
|
+
return asWritableAtom(value);
|
|
118
|
+
}
|
|
119
|
+
function internalJotaiAtom(value) {
|
|
120
|
+
return asJotaiAtom(value);
|
|
121
|
+
}
|
|
122
|
+
function internalJotaiWritableAtom(value) {
|
|
123
|
+
return asJotaiWritableAtom(value);
|
|
124
|
+
}
|
|
125
|
+
const internalStore = getDefaultStore();
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/state/collection.ts
|
|
129
|
+
var CollectionVersionConflictError = class extends Error {
|
|
130
|
+
name = "CollectionVersionConflictError";
|
|
131
|
+
constructor(collection, id, expectedVersion, actualVersion) {
|
|
132
|
+
super(`Version conflict for "${collection}:${id}": expected ${expectedVersion}, received ${actualVersion}`);
|
|
133
|
+
this.collection = collection;
|
|
134
|
+
this.id = id;
|
|
135
|
+
this.expectedVersion = expectedVersion;
|
|
136
|
+
this.actualVersion = actualVersion;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const schemaVersion = 2;
|
|
140
|
+
const registryKey = Symbol.for("bleam.state.collections.v3");
|
|
141
|
+
function registry() {
|
|
142
|
+
const scope = globalThis;
|
|
143
|
+
scope[registryKey] ??= /* @__PURE__ */ new Map();
|
|
144
|
+
return scope[registryKey];
|
|
145
|
+
}
|
|
146
|
+
const queryPlans = /* @__PURE__ */ new Map();
|
|
147
|
+
const mutationTerminals = /* @__PURE__ */ new Map();
|
|
148
|
+
let databasePromise;
|
|
149
|
+
let initializedPromise;
|
|
150
|
+
let jsonSqlSupportPromise;
|
|
151
|
+
let mutationPipeline = Promise.resolve();
|
|
152
|
+
let transactionCallbackStarting = false;
|
|
153
|
+
function getDatabase() {
|
|
154
|
+
databasePromise ??= import("./native-sqlite-WzRNzCSh.js").then((module) => module.openNativeSQLiteDatabase(`${currentAppStorageKey()}.sqlite3`));
|
|
155
|
+
return databasePromise;
|
|
156
|
+
}
|
|
157
|
+
async function initializedDatabase() {
|
|
158
|
+
initializedPromise ??= (async () => {
|
|
159
|
+
const database = await getDatabase();
|
|
160
|
+
await database.execute("PRAGMA journal_mode = WAL");
|
|
161
|
+
if (numberValue(await database.get("PRAGMA user_version"), "user_version", 0) !== schemaVersion) await database.transaction(async () => {
|
|
162
|
+
await database.execute("DROP TABLE IF EXISTS links");
|
|
163
|
+
await database.execute("DROP TABLE IF EXISTS collections");
|
|
164
|
+
await createTables(database);
|
|
165
|
+
await database.execute(`PRAGMA user_version = ${schemaVersion}`);
|
|
166
|
+
});
|
|
167
|
+
else await createTables(database);
|
|
168
|
+
return database;
|
|
169
|
+
})();
|
|
170
|
+
return initializedPromise;
|
|
171
|
+
}
|
|
172
|
+
async function createTables(database) {
|
|
173
|
+
await database.execute(`CREATE TABLE IF NOT EXISTS collections (
|
|
174
|
+
collection TEXT NOT NULL,
|
|
175
|
+
id TEXT NOT NULL,
|
|
176
|
+
version INTEGER NOT NULL,
|
|
177
|
+
createdAt TEXT NOT NULL,
|
|
178
|
+
updatedAt TEXT NOT NULL,
|
|
179
|
+
data TEXT NOT NULL,
|
|
180
|
+
PRIMARY KEY (collection, id)
|
|
181
|
+
)`);
|
|
182
|
+
await database.execute(`CREATE TABLE IF NOT EXISTS links (
|
|
183
|
+
leftCollection TEXT NOT NULL,
|
|
184
|
+
leftId TEXT NOT NULL,
|
|
185
|
+
rightCollection TEXT NOT NULL,
|
|
186
|
+
rightId TEXT NOT NULL,
|
|
187
|
+
createdAt TEXT NOT NULL,
|
|
188
|
+
PRIMARY KEY (leftCollection, leftId, rightCollection, rightId)
|
|
189
|
+
)`);
|
|
190
|
+
await database.execute("CREATE INDEX IF NOT EXISTS collections_created_idx ON collections(collection, createdAt, id)");
|
|
191
|
+
await database.execute("CREATE INDEX IF NOT EXISTS links_left_idx ON links(leftCollection, leftId, rightCollection)");
|
|
192
|
+
await database.execute("CREATE INDEX IF NOT EXISTS links_right_idx ON links(rightCollection, rightId, leftCollection)");
|
|
193
|
+
}
|
|
194
|
+
function stableStringify(value) {
|
|
195
|
+
if (value === void 0) return "undefined";
|
|
196
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
197
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
198
|
+
return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
|
|
199
|
+
}
|
|
200
|
+
function collectionDefinition(collection) {
|
|
201
|
+
const definition = registry().get(collectionName(collection));
|
|
202
|
+
if (!definition || definition.atom !== collection) throw new Error("Invalid collection atom");
|
|
203
|
+
return definition;
|
|
204
|
+
}
|
|
205
|
+
function collectionName(collection) {
|
|
206
|
+
for (const definition of registry().values()) if (definition.atom === collection) return definition.name;
|
|
207
|
+
throw new Error("Invalid collection atom");
|
|
208
|
+
}
|
|
209
|
+
function stringValue(row, key) {
|
|
210
|
+
const value = row?.[key];
|
|
211
|
+
if (typeof value !== "string") throw new Error(`Expected SQLite ${key} string`);
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
function numberValue(row, key, fallback) {
|
|
215
|
+
const value = row?.[key];
|
|
216
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
217
|
+
if (fallback !== void 0 && value === void 0) return fallback;
|
|
218
|
+
throw new Error(`Expected SQLite ${key} number`);
|
|
219
|
+
}
|
|
220
|
+
function parseRow(definition, row) {
|
|
221
|
+
const parsed = JSON.parse(stringValue(row, "data"));
|
|
222
|
+
return {
|
|
223
|
+
id: stringValue(row, "id"),
|
|
224
|
+
collection: definition.name,
|
|
225
|
+
version: numberValue(row, "version"),
|
|
226
|
+
createdAt: stringValue(row, "createdAt"),
|
|
227
|
+
updatedAt: stringValue(row, "updatedAt"),
|
|
228
|
+
data: definition.validator.parse(parsed)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function assertId(id) {
|
|
232
|
+
if (!id.trim()) throw new TypeError("Record ID must not be empty");
|
|
233
|
+
}
|
|
234
|
+
function assertCollectionName(name) {
|
|
235
|
+
if (!name.trim()) throw new TypeError("Collection name must not be empty");
|
|
236
|
+
}
|
|
237
|
+
function assertFieldPaths(shape, prefix = "") {
|
|
238
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
239
|
+
if (!key || key.includes(".")) throw new TypeError(`Collection field name "${prefix}${key}" must not be empty or contain a dot`);
|
|
240
|
+
if (field.kind === "object") assertFieldPaths(field.shape, `${prefix}${key}.`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function endpointOrder(first, second) {
|
|
244
|
+
if (first.collection === second.collection && first.id === second.id) throw new Error(`Cannot link record "${first.collection}:${first.id}" to itself`);
|
|
245
|
+
return first.collection < second.collection || first.collection === second.collection && first.id < second.id ? [first, second] : [second, first];
|
|
246
|
+
}
|
|
247
|
+
async function linkedIds(database, collection, id, target) {
|
|
248
|
+
return (await database.select(`SELECT leftCollection, leftId, rightCollection, rightId FROM links
|
|
249
|
+
WHERE (leftCollection = ? AND leftId = ? AND rightCollection = ?)
|
|
250
|
+
OR (rightCollection = ? AND rightId = ? AND leftCollection = ?)`, [
|
|
251
|
+
collection,
|
|
252
|
+
id,
|
|
253
|
+
target,
|
|
254
|
+
collection,
|
|
255
|
+
id,
|
|
256
|
+
target
|
|
257
|
+
])).map((row) => stringValue(row, "leftCollection") === collection && stringValue(row, "leftId") === id ? stringValue(row, "rightId") : stringValue(row, "leftId")).sort((left, right) => left.localeCompare(right));
|
|
258
|
+
}
|
|
259
|
+
async function incidentLinks(database, collection, id) {
|
|
260
|
+
return database.select(`SELECT leftCollection, leftId, rightCollection, rightId FROM links
|
|
261
|
+
WHERE (leftCollection = ? AND leftId = ?)
|
|
262
|
+
OR (rightCollection = ? AND rightId = ?)`, [
|
|
263
|
+
collection,
|
|
264
|
+
id,
|
|
265
|
+
collection,
|
|
266
|
+
id
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
async function fullRow(database, definition, persisted, linkNames = []) {
|
|
270
|
+
const links = {};
|
|
271
|
+
for (const target of [...new Set(linkNames)].sort()) links[target] = await linkedIds(database, definition.name, persisted.id, target);
|
|
272
|
+
return {
|
|
273
|
+
...persisted,
|
|
274
|
+
data: persisted.data,
|
|
275
|
+
links
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async function getPersisted(database, definition, id) {
|
|
279
|
+
const row = await database.get(`SELECT id, version, createdAt, updatedAt, data FROM collections
|
|
280
|
+
WHERE collection = ? AND id = ?`, [definition.name, id]);
|
|
281
|
+
return row ? parseRow(definition, row) : null;
|
|
282
|
+
}
|
|
283
|
+
function dataValueAt(data, path) {
|
|
284
|
+
let current = data;
|
|
285
|
+
for (const segment of path.split(".")) {
|
|
286
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) return;
|
|
287
|
+
current = current[segment];
|
|
288
|
+
}
|
|
289
|
+
return current;
|
|
290
|
+
}
|
|
291
|
+
function valueAt(row, path) {
|
|
292
|
+
if (path === "createdAt" || path === "updatedAt") return row[path];
|
|
293
|
+
return dataValueAt(row.data, path);
|
|
294
|
+
}
|
|
295
|
+
function sqliteOperand(value) {
|
|
296
|
+
return typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
297
|
+
}
|
|
298
|
+
function topLevelClauseSql(clause, parameters) {
|
|
299
|
+
if (clause.path.includes(".")) return void 0;
|
|
300
|
+
parameters.push(`$.${clause.path}`, sqliteOperand(clause.operand));
|
|
301
|
+
switch (clause.comparison) {
|
|
302
|
+
case "equals": return "json_extract(data, ?) = ?";
|
|
303
|
+
case "greater-than": return "json_extract(data, ?) > ?";
|
|
304
|
+
case "less-than": return "json_extract(data, ?) < ?";
|
|
305
|
+
case "contains": return "instr(json_extract(data, ?), ?) > 0";
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function supportsJsonSql(database) {
|
|
309
|
+
jsonSqlSupportPromise ??= database.get(`SELECT json_extract('{"value":1}', '$.value') AS value`).then((row) => row?.value === 1).catch(() => false);
|
|
310
|
+
return jsonSqlSupportPromise;
|
|
311
|
+
}
|
|
312
|
+
function matches(value, clause) {
|
|
313
|
+
const operand = clause.operand;
|
|
314
|
+
switch (clause.comparison) {
|
|
315
|
+
case "equals": return typeof value === typeof operand && Object.is(value, operand);
|
|
316
|
+
case "greater-than": return typeof value === "number" && typeof operand === "number" && value > operand;
|
|
317
|
+
case "less-than": return typeof value === "number" && typeof operand === "number" && value < operand;
|
|
318
|
+
case "contains": return typeof value === "string" && typeof operand === "string" && value.includes(operand);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function relatedPath(left, right) {
|
|
322
|
+
return left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
|
|
323
|
+
}
|
|
324
|
+
function sameIds(left, right) {
|
|
325
|
+
return Array.isArray(left) && left.length === right.length && left.every((id, index) => id === right[index]);
|
|
326
|
+
}
|
|
327
|
+
function compareValues(left, right, order) {
|
|
328
|
+
const leftMissing = left === void 0;
|
|
329
|
+
const rightMissing = right === void 0;
|
|
330
|
+
if (leftMissing || rightMissing) {
|
|
331
|
+
if (leftMissing && rightMissing) return 0;
|
|
332
|
+
return leftMissing ? 1 : -1;
|
|
333
|
+
}
|
|
334
|
+
if (Object.is(left, right)) return 0;
|
|
335
|
+
if (typeof left !== "string" && typeof left !== "number" && typeof left !== "boolean" || typeof left !== typeof right) return 0;
|
|
336
|
+
const comparison = left < right ? -1 : 1;
|
|
337
|
+
return order === "desc" ? -comparison : comparison;
|
|
338
|
+
}
|
|
339
|
+
async function selectRows(database, definition, plan) {
|
|
340
|
+
const parameters = [definition.name];
|
|
341
|
+
const predicates = [];
|
|
342
|
+
if (await supportsJsonSql(database)) for (const clause of plan.where) {
|
|
343
|
+
const predicate = topLevelClauseSql(clause, parameters);
|
|
344
|
+
if (predicate) predicates.push(predicate);
|
|
345
|
+
}
|
|
346
|
+
let rows = (await database.select(`SELECT id, version, createdAt, updatedAt, data FROM collections
|
|
347
|
+
WHERE collection = ?${predicates.length > 0 ? ` AND ${predicates.join(" AND ")}` : ""}`, parameters)).map((row) => parseRow(definition, row));
|
|
348
|
+
if (plan.from) {
|
|
349
|
+
const ids = new Set(await linkedIds(database, plan.from.collection, plan.from.id, definition.name));
|
|
350
|
+
rows = rows.filter((row) => ids.has(row.id));
|
|
351
|
+
}
|
|
352
|
+
rows = rows.filter((row) => plan.where.every((clause) => matches(valueAt(row, clause.path), clause)));
|
|
353
|
+
rows.sort((left, right) => {
|
|
354
|
+
return (plan.sort ? compareValues(valueAt(left, plan.sort.path), valueAt(right, plan.sort.path), plan.sort.order) : left.createdAt.localeCompare(right.createdAt)) || left.id.localeCompare(right.id);
|
|
355
|
+
});
|
|
356
|
+
return plan.take === void 0 ? rows : rows.slice(0, plan.take);
|
|
357
|
+
}
|
|
358
|
+
async function executeQuery(database, definition, terminal) {
|
|
359
|
+
const plan = terminal.plan;
|
|
360
|
+
if (plan.terminal === "ids") return (await selectRows(database, definition, plan)).map((row) => row.id);
|
|
361
|
+
const persisted = await getPersisted(database, definition, plan.id);
|
|
362
|
+
if (plan.terminal === "row") return persisted ? await fullRow(database, definition, persisted, plan.links) : null;
|
|
363
|
+
return persisted ? dataValueAt(persisted.data, plan.path) : void 0;
|
|
364
|
+
}
|
|
365
|
+
function fieldAtPath(shape, path) {
|
|
366
|
+
const segments = path.split(".");
|
|
367
|
+
let currentShape = shape;
|
|
368
|
+
let field = currentShape[segments[0]];
|
|
369
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
370
|
+
if (field?.kind !== "object") throw new Error(`Unknown schema field path "${path}"`);
|
|
371
|
+
currentShape = field.shape;
|
|
372
|
+
field = currentShape[segments[index]];
|
|
373
|
+
}
|
|
374
|
+
if (!field) throw new Error(`Unknown schema field path "${path}"`);
|
|
375
|
+
return field;
|
|
376
|
+
}
|
|
377
|
+
function validateScalarPath(definition, path) {
|
|
378
|
+
const field = fieldAtPath(definition.shape, path);
|
|
379
|
+
if (field.kind === "object" || field.kind === "array") throw new TypeError(`Collection query path "${path}" must be scalar`);
|
|
380
|
+
return field;
|
|
381
|
+
}
|
|
382
|
+
function queryBuilder(definition) {
|
|
383
|
+
const plan = {
|
|
384
|
+
terminal: "ids",
|
|
385
|
+
where: []
|
|
386
|
+
};
|
|
387
|
+
const builder = {
|
|
388
|
+
where(filters) {
|
|
389
|
+
for (const [path, value] of Object.entries(filters)) {
|
|
390
|
+
const field = validateScalarPath(definition, path);
|
|
391
|
+
const clauses = Array.isArray(value) ? typeof value[0] === "string" ? [value] : value : [["equals", value]];
|
|
392
|
+
for (const [comparison, operand] of clauses) {
|
|
393
|
+
if (typeof operand === "number" && !Number.isFinite(operand)) throw new TypeError("Collection query numbers must be finite");
|
|
394
|
+
const fieldType = field.kind === "literal" ? typeof field.value : field.kind;
|
|
395
|
+
if (typeof operand !== fieldType) throw new TypeError(`Invalid comparison value for "${path}"`);
|
|
396
|
+
if (comparison !== "equals" && comparison !== "greater-than" && comparison !== "less-than" && comparison !== "contains") throw new TypeError(`Invalid comparison operator "${comparison}"`);
|
|
397
|
+
if (comparison === "contains" && fieldType !== "string" || (comparison === "greater-than" || comparison === "less-than") && fieldType !== "number") throw new TypeError(`Invalid ${comparison} comparison for "${path}"`);
|
|
398
|
+
plan.where.push({
|
|
399
|
+
path,
|
|
400
|
+
comparison,
|
|
401
|
+
operand
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return builder;
|
|
406
|
+
},
|
|
407
|
+
from(other, id) {
|
|
408
|
+
assertId(id);
|
|
409
|
+
plan.from = {
|
|
410
|
+
collection: collectionName(other),
|
|
411
|
+
id
|
|
412
|
+
};
|
|
413
|
+
return builder;
|
|
414
|
+
},
|
|
415
|
+
sortBy(path, order = "asc") {
|
|
416
|
+
if (path !== "createdAt" && path !== "updatedAt") validateScalarPath(definition, path);
|
|
417
|
+
if (order !== "asc" && order !== "desc") throw new TypeError("Invalid sort order");
|
|
418
|
+
plan.sort = {
|
|
419
|
+
path,
|
|
420
|
+
order
|
|
421
|
+
};
|
|
422
|
+
return builder;
|
|
423
|
+
},
|
|
424
|
+
take(count) {
|
|
425
|
+
if (!Number.isInteger(count) || count <= 0) throw new TypeError("Collection query take must be a positive integer");
|
|
426
|
+
plan.take = count;
|
|
427
|
+
return builder;
|
|
428
|
+
},
|
|
429
|
+
ids() {
|
|
430
|
+
return { plan: {
|
|
431
|
+
...plan,
|
|
432
|
+
terminal: "ids"
|
|
433
|
+
} };
|
|
434
|
+
},
|
|
435
|
+
row(id, options) {
|
|
436
|
+
assertId(id);
|
|
437
|
+
return { plan: {
|
|
438
|
+
...plan,
|
|
439
|
+
terminal: "row",
|
|
440
|
+
id,
|
|
441
|
+
links: options?.links?.map(collectionName).sort()
|
|
442
|
+
} };
|
|
443
|
+
},
|
|
444
|
+
field(id, path) {
|
|
445
|
+
assertId(id);
|
|
446
|
+
fieldAtPath(definition.shape, path);
|
|
447
|
+
return { plan: {
|
|
448
|
+
...plan,
|
|
449
|
+
terminal: "field",
|
|
450
|
+
id,
|
|
451
|
+
path
|
|
452
|
+
} };
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
return builder;
|
|
456
|
+
}
|
|
457
|
+
function canonicalPlan(plan) {
|
|
458
|
+
return {
|
|
459
|
+
...plan,
|
|
460
|
+
where: [...plan.where].sort((left, right) => stableStringify(left).localeCompare(stableStringify(right))),
|
|
461
|
+
links: plan.links ? [...new Set(plan.links)].sort() : void 0
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const queryFamily = atomFamily((key) => {
|
|
465
|
+
const registration = queryPlans.get(key);
|
|
466
|
+
if (!registration) throw new Error(`Missing collection query plan "${key}"`);
|
|
467
|
+
return atom(async (get) => {
|
|
468
|
+
get(internalJotaiAtom(registration.revision));
|
|
469
|
+
const result = await executeQuery(await initializedDatabase(), registration.definition, { plan: registration.plan });
|
|
470
|
+
if (registration.hasCache && registration.plan.terminal === "ids" && sameIds(registration.cache, result)) return registration.cache;
|
|
471
|
+
if (registration.hasCache && Object.is(registration.cache, result)) return registration.cache;
|
|
472
|
+
registration.cache = result;
|
|
473
|
+
registration.hasCache = true;
|
|
474
|
+
return result;
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
const mutationFamily = atomFamily((key) => {
|
|
478
|
+
const registration = mutationTerminals.get(key);
|
|
479
|
+
if (!registration) throw new Error(`Missing collection mutation terminal "${key}"`);
|
|
480
|
+
return atom(() => null, (_get, _set, ...args) => enqueueMutation((database, changes) => executeMutation(database, changes, registration.collection, registration.terminal, args)));
|
|
481
|
+
});
|
|
482
|
+
function atomCollection(name, shape) {
|
|
483
|
+
assertCollectionName(name);
|
|
484
|
+
assertFieldPaths(shape);
|
|
485
|
+
const validator = schema(shape);
|
|
486
|
+
const signature = stableStringify(validator.toJSONSchema());
|
|
487
|
+
const existing = registry().get(name);
|
|
488
|
+
if (existing) {
|
|
489
|
+
if (existing.signature !== signature) throw new Error(`Collection "${name}" was registered with a different schema`);
|
|
490
|
+
return existing.atom;
|
|
491
|
+
}
|
|
492
|
+
const definition = {
|
|
493
|
+
name,
|
|
494
|
+
shape,
|
|
495
|
+
signature,
|
|
496
|
+
validator,
|
|
497
|
+
revision: atom$1(0)
|
|
498
|
+
};
|
|
499
|
+
const value = atom$1(async (get) => {
|
|
500
|
+
get(definition.revision);
|
|
501
|
+
const ids = (await selectRows(await initializedDatabase(), definition, {
|
|
502
|
+
terminal: "ids",
|
|
503
|
+
where: []
|
|
504
|
+
})).map((row) => row.id);
|
|
505
|
+
if (sameIds(definition.idsCache, ids)) return definition.idsCache;
|
|
506
|
+
definition.idsCache = ids;
|
|
507
|
+
return ids;
|
|
508
|
+
});
|
|
509
|
+
definition.atom = value;
|
|
510
|
+
registry().set(name, definition);
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
function queryAtom(collection, build) {
|
|
514
|
+
const definition = collectionDefinition(collection);
|
|
515
|
+
const plan = canonicalPlan(build(queryBuilder(definition)).plan);
|
|
516
|
+
const key = stableStringify({
|
|
517
|
+
collection: definition.name,
|
|
518
|
+
plan
|
|
519
|
+
});
|
|
520
|
+
if (!queryPlans.has(key)) queryPlans.set(key, {
|
|
521
|
+
definition,
|
|
522
|
+
plan,
|
|
523
|
+
revision: atom$1(0),
|
|
524
|
+
hasCache: false
|
|
525
|
+
});
|
|
526
|
+
return internalAtom(queryFamily(key));
|
|
527
|
+
}
|
|
528
|
+
function mutationBuilder() {
|
|
529
|
+
return {
|
|
530
|
+
insert: () => ({ kind: "insert" }),
|
|
531
|
+
update: (id) => ({
|
|
532
|
+
kind: "update",
|
|
533
|
+
id,
|
|
534
|
+
args: [],
|
|
535
|
+
result: void 0
|
|
536
|
+
}),
|
|
537
|
+
remove: (id) => ({
|
|
538
|
+
kind: "remove",
|
|
539
|
+
id
|
|
540
|
+
}),
|
|
541
|
+
link: (id, other) => ({
|
|
542
|
+
kind: "link",
|
|
543
|
+
id,
|
|
544
|
+
other
|
|
545
|
+
}),
|
|
546
|
+
unlink: (id, other) => ({
|
|
547
|
+
kind: "unlink",
|
|
548
|
+
id,
|
|
549
|
+
other
|
|
550
|
+
})
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function terminalKey(terminal) {
|
|
554
|
+
return stableStringify({
|
|
555
|
+
kind: terminal.kind,
|
|
556
|
+
id: "id" in terminal ? terminal.id : void 0,
|
|
557
|
+
other: "other" in terminal ? collectionName(terminal.other) : void 0
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
function collectionChange(changes, name) {
|
|
561
|
+
let change = changes.get(name);
|
|
562
|
+
if (!change) {
|
|
563
|
+
change = {
|
|
564
|
+
structural: false,
|
|
565
|
+
links: false,
|
|
566
|
+
records: /* @__PURE__ */ new Map()
|
|
567
|
+
};
|
|
568
|
+
changes.set(name, change);
|
|
569
|
+
}
|
|
570
|
+
return change;
|
|
571
|
+
}
|
|
572
|
+
function markRecord(changes, collection, id, kind, paths = []) {
|
|
573
|
+
const change = collectionChange(changes, collection);
|
|
574
|
+
if (kind === "insert" || kind === "remove") change.structural = true;
|
|
575
|
+
const record = change.records.get(id) ?? {
|
|
576
|
+
kind,
|
|
577
|
+
paths: /* @__PURE__ */ new Set(),
|
|
578
|
+
data: true,
|
|
579
|
+
links: false
|
|
580
|
+
};
|
|
581
|
+
record.kind = kind;
|
|
582
|
+
record.data = true;
|
|
583
|
+
for (const path of paths) record.paths.add(path);
|
|
584
|
+
change.records.set(id, record);
|
|
585
|
+
}
|
|
586
|
+
function markLink(changes, collection, id) {
|
|
587
|
+
const change = collectionChange(changes, collection);
|
|
588
|
+
change.links = true;
|
|
589
|
+
const record = change.records.get(id) ?? {
|
|
590
|
+
kind: "update",
|
|
591
|
+
paths: /* @__PURE__ */ new Set(),
|
|
592
|
+
data: false,
|
|
593
|
+
links: false
|
|
594
|
+
};
|
|
595
|
+
record.links = true;
|
|
596
|
+
change.records.set(id, record);
|
|
597
|
+
}
|
|
598
|
+
function changedDataPaths(previous, next, prefix = "", output = /* @__PURE__ */ new Set()) {
|
|
599
|
+
if (stableStringify(previous) === stableStringify(next)) return output;
|
|
600
|
+
if (previous && next && typeof previous === "object" && typeof next === "object" && !Array.isArray(previous) && !Array.isArray(next)) {
|
|
601
|
+
const keys = new Set([...Object.keys(previous), ...Object.keys(next)]);
|
|
602
|
+
for (const key of keys) changedDataPaths(previous[key], next[key], prefix ? `${prefix}.${key}` : key, output);
|
|
603
|
+
} else if (prefix) output.add(prefix);
|
|
604
|
+
return output;
|
|
605
|
+
}
|
|
606
|
+
function queryAffected(registration, changes) {
|
|
607
|
+
const { definition, plan } = registration;
|
|
608
|
+
const change = changes.get(definition.name);
|
|
609
|
+
if (plan.terminal === "row") {
|
|
610
|
+
const record = change?.records.get(plan.id);
|
|
611
|
+
return Boolean(record && (record.data || record.links && (plan.links?.length ?? 0) > 0));
|
|
612
|
+
}
|
|
613
|
+
if (plan.terminal === "field") {
|
|
614
|
+
const record = change?.records.get(plan.id);
|
|
615
|
+
if (!record?.data) return false;
|
|
616
|
+
if (record.kind !== "update") return true;
|
|
617
|
+
return [...record.paths].some((path) => relatedPath(path, plan.path));
|
|
618
|
+
}
|
|
619
|
+
if (change?.structural) return true;
|
|
620
|
+
if (plan.from && (change?.links || changes.get(plan.from.collection)?.links)) return true;
|
|
621
|
+
const dependencies = [...plan.where.map((clause) => clause.path), ...plan.sort ? [plan.sort.path] : []];
|
|
622
|
+
if (!change || dependencies.length === 0) return false;
|
|
623
|
+
return [...change.records.values()].some((record) => [...record.paths].some((path) => dependencies.some((dependency) => relatedPath(path, dependency))));
|
|
624
|
+
}
|
|
625
|
+
function publishChanges(changes) {
|
|
626
|
+
for (const [name, change] of changes) {
|
|
627
|
+
if (!change.structural) continue;
|
|
628
|
+
const definition = registry().get(name);
|
|
629
|
+
if (definition) internalStore.set(internalJotaiWritableAtom(definition.revision), (value) => value + 1);
|
|
630
|
+
}
|
|
631
|
+
for (const registration of queryPlans.values()) {
|
|
632
|
+
if (!queryAffected(registration, changes)) continue;
|
|
633
|
+
internalStore.set(internalJotaiWritableAtom(registration.revision), (value) => value + 1);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function enqueueMutation(action) {
|
|
637
|
+
let resolve;
|
|
638
|
+
let reject;
|
|
639
|
+
const result = new Promise((onResolve, onReject) => {
|
|
640
|
+
resolve = onResolve;
|
|
641
|
+
reject = onReject;
|
|
642
|
+
});
|
|
643
|
+
mutationPipeline = mutationPipeline.then(async () => {
|
|
644
|
+
const database = await initializedDatabase();
|
|
645
|
+
const changes = /* @__PURE__ */ new Map();
|
|
646
|
+
try {
|
|
647
|
+
const value = await database.transaction(() => action(database, changes));
|
|
648
|
+
publishChanges(changes);
|
|
649
|
+
resolve(value);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
reject(error);
|
|
652
|
+
}
|
|
653
|
+
}).catch(() => void 0);
|
|
654
|
+
return result;
|
|
655
|
+
}
|
|
656
|
+
async function insertRecord(database, changes, definition, input) {
|
|
657
|
+
const data = definition.validator.parse(input);
|
|
658
|
+
const id = crypto.randomUUID();
|
|
659
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
660
|
+
await database.execute(`INSERT INTO collections (collection, id, version, createdAt, updatedAt, data)
|
|
661
|
+
VALUES (?, ?, 1, ?, ?, ?)`, [
|
|
662
|
+
definition.name,
|
|
663
|
+
id,
|
|
664
|
+
timestamp,
|
|
665
|
+
timestamp,
|
|
666
|
+
JSON.stringify(data)
|
|
667
|
+
]);
|
|
668
|
+
markRecord(changes, definition.name, id, "insert");
|
|
669
|
+
return id;
|
|
670
|
+
}
|
|
671
|
+
function validatePatch(shape, patch, path = "patch") {
|
|
672
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError(`${path} must be an object`);
|
|
673
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
674
|
+
const field = shape[key];
|
|
675
|
+
if (!field) throw new TypeError(`Unknown field ${path}.${key}`);
|
|
676
|
+
if (value !== void 0 && field.kind === "object") validatePatch(field.shape, value, `${path}.${key}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function applyPatch(current, patch) {
|
|
680
|
+
const next = { ...current };
|
|
681
|
+
for (const [key, value] of Object.entries(patch)) if (value === void 0) delete next[key];
|
|
682
|
+
else if (value && typeof value === "object" && !Array.isArray(value) && current[key] && typeof current[key] === "object" && !Array.isArray(current[key])) next[key] = applyPatch(current[key], value);
|
|
683
|
+
else next[key] = value;
|
|
684
|
+
return next;
|
|
685
|
+
}
|
|
686
|
+
async function updateRecord(database, changes, definition, id, patch, options) {
|
|
687
|
+
assertId(id);
|
|
688
|
+
validatePatch(definition.shape, patch);
|
|
689
|
+
const previous = await getPersisted(database, definition, id);
|
|
690
|
+
if (!previous) throw new Error(`Cannot update missing record "${definition.name}:${id}"`);
|
|
691
|
+
if (options?.ifVersion !== void 0 && options.ifVersion !== previous.version) throw new CollectionVersionConflictError(definition.name, id, options.ifVersion, previous.version);
|
|
692
|
+
const data = definition.validator.parse(applyPatch(previous.data, patch));
|
|
693
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
694
|
+
const timestamp = now > previous.updatedAt ? now : new Date(Date.parse(previous.updatedAt) + 1).toISOString();
|
|
695
|
+
await database.execute(`UPDATE collections SET data = ?, updatedAt = ?, version = ?
|
|
696
|
+
WHERE collection = ? AND id = ? AND version = ?`, [
|
|
697
|
+
JSON.stringify(data),
|
|
698
|
+
timestamp,
|
|
699
|
+
previous.version + 1,
|
|
700
|
+
definition.name,
|
|
701
|
+
id,
|
|
702
|
+
previous.version
|
|
703
|
+
]);
|
|
704
|
+
const persisted = await getPersisted(database, definition, id);
|
|
705
|
+
if (!persisted) throw new Error(`Cannot update missing record "${definition.name}:${id}"`);
|
|
706
|
+
if (persisted.version !== previous.version + 1 || stableStringify(persisted.data) !== stableStringify(data)) throw new CollectionVersionConflictError(definition.name, id, options?.ifVersion ?? previous.version, persisted.version);
|
|
707
|
+
const paths = changedDataPaths(previous.data, data);
|
|
708
|
+
paths.add("updatedAt");
|
|
709
|
+
markRecord(changes, definition.name, id, "update", paths);
|
|
710
|
+
return fullRow(database, definition, persisted);
|
|
711
|
+
}
|
|
712
|
+
async function removeRecord(database, changes, definition, id) {
|
|
713
|
+
assertId(id);
|
|
714
|
+
if (!await getPersisted(database, definition, id)) return false;
|
|
715
|
+
const links = await incidentLinks(database, definition.name, id);
|
|
716
|
+
for (const link of links) {
|
|
717
|
+
markLink(changes, stringValue(link, "leftCollection"), stringValue(link, "leftId"));
|
|
718
|
+
markLink(changes, stringValue(link, "rightCollection"), stringValue(link, "rightId"));
|
|
719
|
+
}
|
|
720
|
+
await database.execute("DELETE FROM links WHERE (leftCollection = ? AND leftId = ?) OR (rightCollection = ? AND rightId = ?)", [
|
|
721
|
+
definition.name,
|
|
722
|
+
id,
|
|
723
|
+
definition.name,
|
|
724
|
+
id
|
|
725
|
+
]);
|
|
726
|
+
await database.execute("DELETE FROM collections WHERE collection = ? AND id = ?", [definition.name, id]);
|
|
727
|
+
markRecord(changes, definition.name, id, "remove");
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
async function mutateLink(database, changes, kind, leftCollection, leftId, rightCollection, rightId) {
|
|
731
|
+
assertId(leftId);
|
|
732
|
+
assertId(rightId);
|
|
733
|
+
const leftDefinition = collectionDefinition(leftCollection);
|
|
734
|
+
const rightDefinition = collectionDefinition(rightCollection);
|
|
735
|
+
const [left, right] = endpointOrder({
|
|
736
|
+
collection: leftDefinition.name,
|
|
737
|
+
id: leftId
|
|
738
|
+
}, {
|
|
739
|
+
collection: rightDefinition.name,
|
|
740
|
+
id: rightId
|
|
741
|
+
});
|
|
742
|
+
if (!await getPersisted(database, leftDefinition, leftId)) throw new Error(`Cannot link missing record "${leftDefinition.name}:${leftId}"`);
|
|
743
|
+
if (!await getPersisted(database, rightDefinition, rightId)) throw new Error(`Cannot link missing record "${rightDefinition.name}:${rightId}"`);
|
|
744
|
+
const parameters = [
|
|
745
|
+
left.collection,
|
|
746
|
+
left.id,
|
|
747
|
+
right.collection,
|
|
748
|
+
right.id
|
|
749
|
+
];
|
|
750
|
+
const existing = await database.get(`SELECT createdAt FROM links WHERE leftCollection = ? AND leftId = ?
|
|
751
|
+
AND rightCollection = ? AND rightId = ?`, parameters);
|
|
752
|
+
if (kind === "unlink") {
|
|
753
|
+
if (!existing) return false;
|
|
754
|
+
await database.execute(`DELETE FROM links WHERE leftCollection = ? AND leftId = ?
|
|
755
|
+
AND rightCollection = ? AND rightId = ?`, parameters);
|
|
756
|
+
markLink(changes, left.collection, left.id);
|
|
757
|
+
markLink(changes, right.collection, right.id);
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
if (existing) return false;
|
|
761
|
+
await database.execute(`INSERT INTO links (leftCollection, leftId, rightCollection, rightId, createdAt)
|
|
762
|
+
VALUES (?, ?, ?, ?, ?)`, [...parameters, (/* @__PURE__ */ new Date()).toISOString()]);
|
|
763
|
+
markLink(changes, left.collection, left.id);
|
|
764
|
+
markLink(changes, right.collection, right.id);
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
async function executeMutation(database, changes, collection, terminal, args) {
|
|
768
|
+
const definition = collectionDefinition(collection);
|
|
769
|
+
switch (terminal.kind) {
|
|
770
|
+
case "insert": return insertRecord(database, changes, definition, args[0]);
|
|
771
|
+
case "update": return updateRecord(database, changes, definition, terminal.id, args[0], args[1]);
|
|
772
|
+
case "remove": return removeRecord(database, changes, definition, terminal.id);
|
|
773
|
+
case "link":
|
|
774
|
+
case "unlink": return mutateLink(database, changes, terminal.kind, collection, terminal.id, terminal.other, args[0]);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function mutateAtom(collection, build) {
|
|
778
|
+
const definition = collectionDefinition(collection);
|
|
779
|
+
const terminal = build(mutationBuilder());
|
|
780
|
+
if ("id" in terminal) assertId(terminal.id);
|
|
781
|
+
const key = stableStringify({
|
|
782
|
+
collection: definition.name,
|
|
783
|
+
terminal: terminalKey(terminal)
|
|
784
|
+
});
|
|
785
|
+
if (!mutationTerminals.has(key)) mutationTerminals.set(key, {
|
|
786
|
+
collection,
|
|
787
|
+
terminal
|
|
788
|
+
});
|
|
789
|
+
return internalWritableAtom(mutationFamily(key));
|
|
790
|
+
}
|
|
791
|
+
function transactionContext(database, changes) {
|
|
792
|
+
return {
|
|
793
|
+
async get(collection, id, options) {
|
|
794
|
+
const definition = collectionDefinition(collection);
|
|
795
|
+
const persisted = await getPersisted(database, definition, id);
|
|
796
|
+
return persisted ? fullRow(database, definition, persisted, options?.links?.map(collectionName)) : null;
|
|
797
|
+
},
|
|
798
|
+
query(collection, build) {
|
|
799
|
+
const definition = collectionDefinition(collection);
|
|
800
|
+
return executeQuery(database, definition, { plan: canonicalPlan(build(queryBuilder(definition)).plan) });
|
|
801
|
+
},
|
|
802
|
+
insert(collection, value) {
|
|
803
|
+
return insertRecord(database, changes, collectionDefinition(collection), value);
|
|
804
|
+
},
|
|
805
|
+
update(collection, id, patch, options) {
|
|
806
|
+
return updateRecord(database, changes, collectionDefinition(collection), id, patch, options);
|
|
807
|
+
},
|
|
808
|
+
remove(collection, id) {
|
|
809
|
+
return removeRecord(database, changes, collectionDefinition(collection), id);
|
|
810
|
+
},
|
|
811
|
+
link(collection, id, other, otherId) {
|
|
812
|
+
return mutateLink(database, changes, "link", collection, id, other, otherId);
|
|
813
|
+
},
|
|
814
|
+
unlink(collection, id, other, otherId) {
|
|
815
|
+
return mutateLink(database, changes, "unlink", collection, id, other, otherId);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
function transactionAtom(action) {
|
|
820
|
+
return atom$1(() => null, (_get, _set, ...args) => {
|
|
821
|
+
if (transactionCallbackStarting) return Promise.reject(/* @__PURE__ */ new Error("Nested collection transactions are not supported"));
|
|
822
|
+
return enqueueMutation((database, changes) => {
|
|
823
|
+
transactionCallbackStarting = true;
|
|
824
|
+
try {
|
|
825
|
+
return Promise.resolve(action(transactionContext(database, changes), ...args));
|
|
826
|
+
} finally {
|
|
827
|
+
transactionCallbackStarting = false;
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
function useList(collection, build = (builder) => builder.ids()) {
|
|
833
|
+
return useAtomValue$1(queryAtom(collection, build));
|
|
834
|
+
}
|
|
835
|
+
function useRow(collection, id, options) {
|
|
836
|
+
return useAtomValue$1(queryAtom(collection, (builder) => builder.row(id, options)));
|
|
837
|
+
}
|
|
838
|
+
function useField(collection, id, path) {
|
|
839
|
+
return useAtomValue$1(queryAtom(collection, (builder) => builder.field(id, path)));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
//#endregion
|
|
843
|
+
export { transactionAtom as a, useRow as c, internalJotaiWritableAtom as d, internalStore as f, useSetAtom$1 as h, queryAtom as i, atom$1 as l, useAtomValue$1 as m, atomCollection as n, useField as o, useAtom$1 as p, mutateAtom as r, useList as s, CollectionVersionConflictError as t, internalJotaiAtom as u };
|