backlex 0.1.0
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/LICENSE +201 -0
- package/README.md +43 -0
- package/dist/chunk-U2MHWV2E.js +44 -0
- package/dist/chunk-UIUS57OR.js +15 -0
- package/dist/index.d.ts +569 -0
- package/dist/index.js +857 -0
- package/dist/types-CbcbXGiA.d.ts +247 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/dist/webhook.d.ts +39 -0
- package/dist/webhook.js +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
import { BacklexError } from './chunk-UIUS57OR.js';
|
|
2
|
+
export { BacklexError } from './chunk-UIUS57OR.js';
|
|
3
|
+
export { verifyWebhook } from './chunk-U2MHWV2E.js';
|
|
4
|
+
|
|
5
|
+
// src/condition.ts
|
|
6
|
+
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7
|
+
var looksLikeComparison = (o) => {
|
|
8
|
+
const keys = Object.keys(o);
|
|
9
|
+
return keys.length > 0 && keys.every((k) => k.startsWith("_"));
|
|
10
|
+
};
|
|
11
|
+
var looksLikeNesting = (o) => {
|
|
12
|
+
const keys = Object.keys(o);
|
|
13
|
+
return keys.length > 0 && keys.every((k) => !k.startsWith("_") && !k.startsWith("$"));
|
|
14
|
+
};
|
|
15
|
+
var flattenNested = (prefix, value, out) => {
|
|
16
|
+
for (const [k, v] of Object.entries(value)) {
|
|
17
|
+
const path = `${prefix}.${k}`;
|
|
18
|
+
if (isPlainObject(v) && looksLikeComparison(v)) {
|
|
19
|
+
out[path] = v;
|
|
20
|
+
} else if (isPlainObject(v) && looksLikeNesting(v)) {
|
|
21
|
+
flattenNested(path, v, out);
|
|
22
|
+
} else {
|
|
23
|
+
out[path] = { _eq: v };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var normalizeCondition = (raw, opts = {}) => {
|
|
28
|
+
if (!isPlainObject(raw)) return raw;
|
|
29
|
+
const rels = opts.relationFields;
|
|
30
|
+
const and = raw.$and ?? raw._and;
|
|
31
|
+
if (Array.isArray(and)) {
|
|
32
|
+
return { $and: and.map((c) => normalizeCondition(c, opts)) };
|
|
33
|
+
}
|
|
34
|
+
const or = raw.$or ?? raw._or;
|
|
35
|
+
if (Array.isArray(or)) {
|
|
36
|
+
return { $or: or.map((c) => normalizeCondition(c, opts)) };
|
|
37
|
+
}
|
|
38
|
+
const not = raw.$not ?? raw._not;
|
|
39
|
+
if (not !== void 0) {
|
|
40
|
+
return { $not: normalizeCondition(not, opts) };
|
|
41
|
+
}
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
44
|
+
if (isPlainObject(value)) {
|
|
45
|
+
if (looksLikeComparison(value)) {
|
|
46
|
+
out[key] = value;
|
|
47
|
+
} else if (looksLikeNesting(value) && (rels ? rels.has(key) : false)) {
|
|
48
|
+
flattenNested(key, value, out);
|
|
49
|
+
} else {
|
|
50
|
+
out[key] = value;
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
out[key] = { _eq: value };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/query.ts
|
|
60
|
+
var prefixKeys = (cond, head) => {
|
|
61
|
+
const c = cond;
|
|
62
|
+
if (Array.isArray(c.$and)) {
|
|
63
|
+
return { $and: c.$and.map((x) => prefixKeys(x, head)) };
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(c.$or)) {
|
|
66
|
+
return { $or: c.$or.map((x) => prefixKeys(x, head)) };
|
|
67
|
+
}
|
|
68
|
+
if (c.$not !== void 0) {
|
|
69
|
+
return { $not: prefixKeys(c.$not, head) };
|
|
70
|
+
}
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [k, v] of Object.entries(c)) out[`${head}.${k}`] = v;
|
|
73
|
+
return out;
|
|
74
|
+
};
|
|
75
|
+
var makeFilterBuilder = () => {
|
|
76
|
+
const leaf = (field, op, value) => ({ [field]: { [op]: value } });
|
|
77
|
+
return {
|
|
78
|
+
eq: (f, v) => leaf(f, "_eq", v),
|
|
79
|
+
neq: (f, v) => leaf(f, "_neq", v),
|
|
80
|
+
gt: (f, v) => leaf(f, "_gt", v),
|
|
81
|
+
gte: (f, v) => leaf(f, "_gte", v),
|
|
82
|
+
lt: (f, v) => leaf(f, "_lt", v),
|
|
83
|
+
lte: (f, v) => leaf(f, "_lte", v),
|
|
84
|
+
in: (f, v) => leaf(f, "_in", v),
|
|
85
|
+
nin: (f, v) => leaf(f, "_nin", v),
|
|
86
|
+
between: (f, lo, hi) => leaf(f, "_between", [lo, hi]),
|
|
87
|
+
isNull: (f, isNull = true) => leaf(f, "_null", isNull),
|
|
88
|
+
empty: (f) => leaf(f, "_empty", true),
|
|
89
|
+
nempty: (f) => leaf(f, "_nempty", true),
|
|
90
|
+
contains: (f, v) => leaf(f, "_contains", v),
|
|
91
|
+
icontains: (f, v) => leaf(f, "_icontains", v),
|
|
92
|
+
startsWith: (f, v) => leaf(f, "_starts_with", v),
|
|
93
|
+
endsWith: (f, v) => leaf(f, "_ends_with", v),
|
|
94
|
+
and: (...conds) => ({ $and: conds }),
|
|
95
|
+
or: (...conds) => ({ $or: conds }),
|
|
96
|
+
not: (cond) => ({ $not: cond }),
|
|
97
|
+
rel: (head, build) => prefixKeys(build(makeFilterBuilder()), head),
|
|
98
|
+
now: (opts = {}) => ({ $now: { ...opts } })
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
var QueryBuilder = class {
|
|
102
|
+
constructor(listFn) {
|
|
103
|
+
this.listFn = listFn;
|
|
104
|
+
}
|
|
105
|
+
_filter;
|
|
106
|
+
_sort = [];
|
|
107
|
+
_fields = [];
|
|
108
|
+
_expand = [];
|
|
109
|
+
_limit;
|
|
110
|
+
_offset;
|
|
111
|
+
_meta;
|
|
112
|
+
_locale;
|
|
113
|
+
_q;
|
|
114
|
+
where(build) {
|
|
115
|
+
this._filter = normalizeCondition(build(makeFilterBuilder()));
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
/** Replace the filter with a raw canonical condition (escape hatch). */
|
|
119
|
+
filter(cond) {
|
|
120
|
+
this._filter = normalizeCondition(cond);
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
select(...fields) {
|
|
124
|
+
this._fields.push(...fields);
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
orderBy(...sorts) {
|
|
128
|
+
this._sort.push(...sorts);
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
/** Inline single-hop relations (replaces each FK with the related object). */
|
|
132
|
+
expand(...rels) {
|
|
133
|
+
this._expand.push(...rels);
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
/** Project `i18n_text` fields to one locale, or `"*"` for the full map. */
|
|
137
|
+
locale(loc) {
|
|
138
|
+
this._locale = loc;
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
/** Free-text search across readable text fields. */
|
|
142
|
+
search(text) {
|
|
143
|
+
this._q = text;
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
limit(n) {
|
|
147
|
+
this._limit = n;
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
offset(n) {
|
|
151
|
+
this._offset = n;
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
withMeta(m) {
|
|
155
|
+
this._meta = m;
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
/** Assemble the plain `ListQuery` — the canonical JSON the REST API takes. */
|
|
159
|
+
toQuery() {
|
|
160
|
+
const q = {};
|
|
161
|
+
if (this._filter) q.filter = this._filter;
|
|
162
|
+
if (this._sort.length) q.sort = this._sort;
|
|
163
|
+
if (this._fields.length) q.fields = this._fields;
|
|
164
|
+
if (this._expand.length) q.expand = this._expand;
|
|
165
|
+
if (this._limit !== void 0) q.limit = this._limit;
|
|
166
|
+
if (this._offset !== void 0) q.offset = this._offset;
|
|
167
|
+
if (this._meta) q.meta = this._meta;
|
|
168
|
+
if (this._locale) q.locale = this._locale;
|
|
169
|
+
if (this._q) q.q = this._q;
|
|
170
|
+
return q;
|
|
171
|
+
}
|
|
172
|
+
list() {
|
|
173
|
+
return this.listFn(this.toQuery());
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// src/sync.ts
|
|
178
|
+
var memoryStore = () => {
|
|
179
|
+
const rows = /* @__PURE__ */ new Map();
|
|
180
|
+
const meta = /* @__PURE__ */ new Map();
|
|
181
|
+
let queue = [];
|
|
182
|
+
return {
|
|
183
|
+
async get(id) {
|
|
184
|
+
return rows.get(id);
|
|
185
|
+
},
|
|
186
|
+
async set(id, row) {
|
|
187
|
+
rows.set(id, row);
|
|
188
|
+
},
|
|
189
|
+
async remove(id) {
|
|
190
|
+
rows.delete(id);
|
|
191
|
+
},
|
|
192
|
+
async all() {
|
|
193
|
+
return [...rows.values()];
|
|
194
|
+
},
|
|
195
|
+
async getMeta(k) {
|
|
196
|
+
return meta.get(k) ?? null;
|
|
197
|
+
},
|
|
198
|
+
async setMeta(k, v) {
|
|
199
|
+
meta.set(k, v);
|
|
200
|
+
},
|
|
201
|
+
async queueGet() {
|
|
202
|
+
return [...queue];
|
|
203
|
+
},
|
|
204
|
+
async queueSet(ops) {
|
|
205
|
+
queue = [...ops];
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
var indexedDbStore = (opts) => {
|
|
210
|
+
const dbName = opts.dbName ?? "backlex-sync";
|
|
211
|
+
const rowsStore = `rows:${opts.collection}`;
|
|
212
|
+
const metaStore = `meta:${opts.collection}`;
|
|
213
|
+
const idb = globalThis.indexedDB;
|
|
214
|
+
if (!idb) throw new Error("indexedDbStore requires a browser with IndexedDB; use memoryStore()");
|
|
215
|
+
let dbp = null;
|
|
216
|
+
const open = () => {
|
|
217
|
+
if (dbp) return dbp;
|
|
218
|
+
dbp = new Promise((resolve, reject) => {
|
|
219
|
+
const req = idb.open(dbName, 1);
|
|
220
|
+
req.onupgradeneeded = () => {
|
|
221
|
+
const db = req.result;
|
|
222
|
+
if (!db.objectStoreNames.contains(rowsStore)) db.createObjectStore(rowsStore);
|
|
223
|
+
if (!db.objectStoreNames.contains(metaStore)) db.createObjectStore(metaStore);
|
|
224
|
+
};
|
|
225
|
+
req.onsuccess = () => resolve(req.result);
|
|
226
|
+
req.onerror = () => reject(req.error);
|
|
227
|
+
});
|
|
228
|
+
return dbp;
|
|
229
|
+
};
|
|
230
|
+
const tx = async (store, mode, fn) => {
|
|
231
|
+
const db = await open();
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const t = db.transaction(store, mode);
|
|
234
|
+
const req = fn(t.objectStore(store));
|
|
235
|
+
req.onsuccess = () => resolve(req.result);
|
|
236
|
+
req.onerror = () => reject(req.error);
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
return {
|
|
240
|
+
get: (id) => tx(rowsStore, "readonly", (s) => s.get(id)),
|
|
241
|
+
set: (id, row) => tx(rowsStore, "readwrite", (s) => s.put(row, id)).then(() => {
|
|
242
|
+
}),
|
|
243
|
+
remove: (id) => tx(rowsStore, "readwrite", (s) => s.delete(id)).then(() => {
|
|
244
|
+
}),
|
|
245
|
+
all: () => tx(rowsStore, "readonly", (s) => s.getAll()),
|
|
246
|
+
getMeta: (k) => tx(metaStore, "readonly", (s) => s.get(k)).then((v) => v ?? null),
|
|
247
|
+
setMeta: (k, v) => tx(metaStore, "readwrite", (s) => s.put(v, k)).then(() => {
|
|
248
|
+
}),
|
|
249
|
+
queueGet: () => tx(metaStore, "readonly", (s) => s.get("__queue")).then((v) => v ? JSON.parse(v) : []),
|
|
250
|
+
queueSet: (ops) => tx(metaStore, "readwrite", (s) => s.put(JSON.stringify(ops), "__queue")).then(() => {
|
|
251
|
+
})
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
var isOnline = () => {
|
|
255
|
+
const nav = globalThis.navigator;
|
|
256
|
+
return nav?.onLine ?? true;
|
|
257
|
+
};
|
|
258
|
+
var createSync = (client, options) => {
|
|
259
|
+
const store = options.store ?? memoryStore();
|
|
260
|
+
const pk = options.pk ?? "id";
|
|
261
|
+
const pageSize = options.pageSize ?? 200;
|
|
262
|
+
const slug = encodeURIComponent(options.collection);
|
|
263
|
+
const notify = () => options.onChange?.();
|
|
264
|
+
const pendingIds = async () => {
|
|
265
|
+
const q = await store.queueGet();
|
|
266
|
+
const s = /* @__PURE__ */ new Set();
|
|
267
|
+
for (const op of q) s.add(op.kind === "create" ? op.tempId : op.id);
|
|
268
|
+
return s;
|
|
269
|
+
};
|
|
270
|
+
const applyRow = async (row, skip) => {
|
|
271
|
+
const id = String(row[pk]);
|
|
272
|
+
if (skip.has(id)) return;
|
|
273
|
+
if (row._deleted === true || row.deleted_at != null) await store.remove(id);
|
|
274
|
+
else await store.set(id, row);
|
|
275
|
+
};
|
|
276
|
+
const pull = async () => {
|
|
277
|
+
let cursor = await store.getMeta("cursor");
|
|
278
|
+
let total = 0;
|
|
279
|
+
const skip = await pendingIds();
|
|
280
|
+
for (; ; ) {
|
|
281
|
+
const qs = `?limit=${pageSize}${cursor ? `&since=${encodeURIComponent(cursor)}` : ""}`;
|
|
282
|
+
const res = await client.request("GET", `/api/items/${slug}/changes${qs}`);
|
|
283
|
+
for (const row of res.data) await applyRow(row, skip);
|
|
284
|
+
total += res.data.length;
|
|
285
|
+
if (res.cursor) {
|
|
286
|
+
cursor = res.cursor;
|
|
287
|
+
await store.setMeta("cursor", cursor);
|
|
288
|
+
}
|
|
289
|
+
if (!res.hasMore) break;
|
|
290
|
+
}
|
|
291
|
+
if (total) notify();
|
|
292
|
+
return total;
|
|
293
|
+
};
|
|
294
|
+
const flush = async () => {
|
|
295
|
+
const queue = await store.queueGet();
|
|
296
|
+
if (queue.length === 0 || !isOnline()) return;
|
|
297
|
+
const operations = queue.map(
|
|
298
|
+
(op) => op.kind === "create" ? { op: "create", data: op.data } : op.kind === "update" ? { op: "update", id: op.id, data: op.data } : { op: "delete", id: op.id }
|
|
299
|
+
);
|
|
300
|
+
const res = await client.request(
|
|
301
|
+
"POST",
|
|
302
|
+
`/api/items/${slug}/batch`,
|
|
303
|
+
{ operations }
|
|
304
|
+
);
|
|
305
|
+
for (let i = 0; i < queue.length; i++) {
|
|
306
|
+
const op = queue[i];
|
|
307
|
+
const r = res.data.results[i];
|
|
308
|
+
if (!op || !r || !r.ok) continue;
|
|
309
|
+
if (op.kind === "create") {
|
|
310
|
+
await store.remove(op.tempId);
|
|
311
|
+
if (r.data) await store.set(String(r.data[pk]), r.data);
|
|
312
|
+
} else if (op.kind === "update" && r.data) {
|
|
313
|
+
await store.set(String(r.data[pk]), r.data);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const remaining = queue.filter((_, i) => !res.data.results[i]?.ok);
|
|
317
|
+
await store.queueSet(remaining);
|
|
318
|
+
notify();
|
|
319
|
+
};
|
|
320
|
+
const enqueue = async (op) => {
|
|
321
|
+
const q = await store.queueGet();
|
|
322
|
+
q.push(op);
|
|
323
|
+
await store.queueSet(q);
|
|
324
|
+
if (isOnline()) await flush().catch(() => {
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
const getAll = () => store.all();
|
|
328
|
+
const get = (id) => store.get(id);
|
|
329
|
+
const create = async (data) => {
|
|
330
|
+
const tempId = `tmp_${Math.abs(hash(JSON.stringify(data) + await store.getMeta("seq"))).toString(36)}_${await bump()}`;
|
|
331
|
+
await store.set(tempId, { ...data, [pk]: tempId, _pending: true });
|
|
332
|
+
await enqueue({ kind: "create", tempId, data });
|
|
333
|
+
notify();
|
|
334
|
+
return tempId;
|
|
335
|
+
};
|
|
336
|
+
const update = async (id, data) => {
|
|
337
|
+
const cur = await store.get(id) ?? {};
|
|
338
|
+
await store.set(id, { ...cur, ...data });
|
|
339
|
+
await enqueue({ kind: "update", id, data });
|
|
340
|
+
notify();
|
|
341
|
+
};
|
|
342
|
+
const remove = async (id) => {
|
|
343
|
+
await store.remove(id);
|
|
344
|
+
await enqueue({ kind: "delete", id });
|
|
345
|
+
notify();
|
|
346
|
+
};
|
|
347
|
+
const bump = async () => {
|
|
348
|
+
const n = Number(await store.getMeta("seq") ?? "0") + 1;
|
|
349
|
+
await store.setMeta("seq", String(n));
|
|
350
|
+
return n;
|
|
351
|
+
};
|
|
352
|
+
let unsub = null;
|
|
353
|
+
let onlineHandler = null;
|
|
354
|
+
const live = () => {
|
|
355
|
+
unsub?.();
|
|
356
|
+
unsub = client.subscribe(`items:${options.collection}`, (e) => {
|
|
357
|
+
const row = e.event === "deleted" ? { ...e.data, _deleted: true } : e.data;
|
|
358
|
+
void applyRow(row, /* @__PURE__ */ new Set()).then(notify);
|
|
359
|
+
});
|
|
360
|
+
return () => {
|
|
361
|
+
unsub?.();
|
|
362
|
+
unsub = null;
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
const start = async () => {
|
|
366
|
+
await pull();
|
|
367
|
+
await flush().catch(() => {
|
|
368
|
+
});
|
|
369
|
+
live();
|
|
370
|
+
const target = globalThis;
|
|
371
|
+
if (target.addEventListener) {
|
|
372
|
+
onlineHandler = () => {
|
|
373
|
+
void flush().then(() => pull()).catch(() => {
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
target.addEventListener("online", onlineHandler);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const stop = () => {
|
|
380
|
+
unsub?.();
|
|
381
|
+
unsub = null;
|
|
382
|
+
const target = globalThis;
|
|
383
|
+
if (onlineHandler && target.removeEventListener) target.removeEventListener("online", onlineHandler);
|
|
384
|
+
onlineHandler = null;
|
|
385
|
+
};
|
|
386
|
+
return { pull, flush, live, start, stop, getAll, get, create, update, remove, store };
|
|
387
|
+
};
|
|
388
|
+
var hash = (s) => {
|
|
389
|
+
let h = 2166136261;
|
|
390
|
+
for (let i = 0; i < s.length; i++) {
|
|
391
|
+
h ^= s.charCodeAt(i);
|
|
392
|
+
h = Math.imul(h, 16777619);
|
|
393
|
+
}
|
|
394
|
+
return h | 0;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
// src/index.ts
|
|
398
|
+
var buildSearch = (q) => {
|
|
399
|
+
if (!q) return "";
|
|
400
|
+
const params = new URLSearchParams();
|
|
401
|
+
if (q.filter) params.set("filter", JSON.stringify(q.filter));
|
|
402
|
+
if (q.sort) {
|
|
403
|
+
params.set("sort", Array.isArray(q.sort) ? q.sort.join(",") : q.sort);
|
|
404
|
+
}
|
|
405
|
+
if (q.fields) {
|
|
406
|
+
params.set("fields", Array.isArray(q.fields) ? q.fields.join(",") : q.fields);
|
|
407
|
+
}
|
|
408
|
+
if (q.expand) {
|
|
409
|
+
params.set("expand", Array.isArray(q.expand) ? q.expand.join(",") : q.expand);
|
|
410
|
+
}
|
|
411
|
+
if (q.limit !== void 0) params.set("limit", String(q.limit));
|
|
412
|
+
if (q.offset !== void 0) params.set("offset", String(q.offset));
|
|
413
|
+
if (q.meta) params.set("meta", q.meta);
|
|
414
|
+
if (q.locale) params.set("locale", q.locale);
|
|
415
|
+
if (q.q) params.set("q", q.q);
|
|
416
|
+
if (q.status) params.set("status", q.status);
|
|
417
|
+
const s = params.toString();
|
|
418
|
+
return s ? `?${s}` : "";
|
|
419
|
+
};
|
|
420
|
+
var buildItemSearch = (q) => {
|
|
421
|
+
if (!q) return "";
|
|
422
|
+
const params = new URLSearchParams();
|
|
423
|
+
if (q.expand) {
|
|
424
|
+
params.set("expand", Array.isArray(q.expand) ? q.expand.join(",") : q.expand);
|
|
425
|
+
}
|
|
426
|
+
if (q.locale) params.set("locale", q.locale);
|
|
427
|
+
const s = params.toString();
|
|
428
|
+
return s ? `?${s}` : "";
|
|
429
|
+
};
|
|
430
|
+
var DEFAULT_CHUNK = 8 * 1024 * 1024;
|
|
431
|
+
var OFFSET_OCTET = "application/offset+octet-stream";
|
|
432
|
+
var b64 = (s) => btoa(String.fromCharCode(...new TextEncoder().encode(s)));
|
|
433
|
+
var normalizeUploadData = (data) => {
|
|
434
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
435
|
+
return { size: data.size, slice: (s, e) => data.slice(s, e) };
|
|
436
|
+
}
|
|
437
|
+
const u = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
438
|
+
return { size: u.byteLength, slice: (s, e) => u.subarray(s, e) };
|
|
439
|
+
};
|
|
440
|
+
var createClient = (opts) => {
|
|
441
|
+
const f = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
442
|
+
let appToken = opts.token ?? null;
|
|
443
|
+
const authBase = opts.workspace ? `/api/t/${encodeURIComponent(opts.workspace)}/auth` : "/api/auth";
|
|
444
|
+
const authHeader = () => {
|
|
445
|
+
if (opts.apiKey) return { authorization: `Bearer ${opts.apiKey}` };
|
|
446
|
+
if (appToken) return { authorization: `Bearer ${appToken}` };
|
|
447
|
+
return {};
|
|
448
|
+
};
|
|
449
|
+
const tenantHeader = () => opts.tenant ? { "x-backlex-tenant": opts.tenant } : {};
|
|
450
|
+
const request = async (method, path, body, extraHeaders) => {
|
|
451
|
+
const headers = {
|
|
452
|
+
"content-type": "application/json",
|
|
453
|
+
...authHeader(),
|
|
454
|
+
...tenantHeader(),
|
|
455
|
+
...extraHeaders ?? {}
|
|
456
|
+
};
|
|
457
|
+
const res = await f(`${opts.url}${path}`, {
|
|
458
|
+
method,
|
|
459
|
+
credentials: "include",
|
|
460
|
+
headers,
|
|
461
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
462
|
+
});
|
|
463
|
+
if (!res.ok) {
|
|
464
|
+
const errBody = await res.json().catch(() => ({}));
|
|
465
|
+
throw new BacklexError(res.status, errBody);
|
|
466
|
+
}
|
|
467
|
+
if (res.status === 204) return void 0;
|
|
468
|
+
return await res.json();
|
|
469
|
+
};
|
|
470
|
+
const requestRaw = async (method, path, rawBody, contentType) => {
|
|
471
|
+
const headers = {
|
|
472
|
+
...authHeader(),
|
|
473
|
+
...tenantHeader()
|
|
474
|
+
};
|
|
475
|
+
if (contentType) headers["content-type"] = contentType;
|
|
476
|
+
const res = await f(`${opts.url}${path}`, {
|
|
477
|
+
method,
|
|
478
|
+
credentials: "include",
|
|
479
|
+
headers,
|
|
480
|
+
body: rawBody
|
|
481
|
+
});
|
|
482
|
+
if (!res.ok) {
|
|
483
|
+
const errBody = await res.json().catch(() => ({}));
|
|
484
|
+
throw new BacklexError(res.status, errBody);
|
|
485
|
+
}
|
|
486
|
+
return res;
|
|
487
|
+
};
|
|
488
|
+
const collection = (slug) => {
|
|
489
|
+
const list = (q) => request("GET", `/api/items/${slug}${buildSearch(q)}`);
|
|
490
|
+
return {
|
|
491
|
+
list,
|
|
492
|
+
/** Fluent, type-safe query builder that compiles to `ListQuery`. */
|
|
493
|
+
query: () => new QueryBuilder(list),
|
|
494
|
+
/** Run a single-function aggregate (count/sum/avg/min/max), optionally grouped. */
|
|
495
|
+
aggregate: (body) => request("POST", `/api/items/${slug}/aggregate`, body),
|
|
496
|
+
/** Relevance search (full-text / vector / hybrid). */
|
|
497
|
+
search: (body) => request("POST", `/api/items/${slug}/search`, body),
|
|
498
|
+
/** Export every readable row as a JSON or CSV string (honors the same
|
|
499
|
+
* read filters as `list`). */
|
|
500
|
+
exportItems: (format = "json") => requestRaw("GET", `/api/items/${slug}/export?format=${format}`).then(
|
|
501
|
+
(r) => r.text()
|
|
502
|
+
),
|
|
503
|
+
/** Bulk-import rows from a JSON array (or raw JSON/CSV string). Each row
|
|
504
|
+
* runs the normal create path; row-level failures land in `errors`. */
|
|
505
|
+
importItems: (body, format = "json") => {
|
|
506
|
+
const raw = typeof body === "string" ? body : JSON.stringify(body);
|
|
507
|
+
const contentType = format === "csv" ? "text/csv" : "application/json";
|
|
508
|
+
return requestRaw(
|
|
509
|
+
"POST",
|
|
510
|
+
`/api/items/${slug}/import?format=${format}`,
|
|
511
|
+
raw,
|
|
512
|
+
contentType
|
|
513
|
+
).then((r) => r.json());
|
|
514
|
+
},
|
|
515
|
+
one: (id, opts2) => request("GET", `/api/items/${slug}/${id}${buildItemSearch(opts2)}`),
|
|
516
|
+
create: (data) => request("POST", `/api/items/${slug}`, data),
|
|
517
|
+
update: (id, patch) => request("PATCH", `/api/items/${slug}/${id}`, patch),
|
|
518
|
+
delete: (id) => request("DELETE", `/api/items/${slug}/${id}`),
|
|
519
|
+
/** Bulk-create rows. `atomic` runs the whole set in one transaction
|
|
520
|
+
* (all-or-nothing; Postgres/SQLite only). Default is partial-success. */
|
|
521
|
+
createMany: (rows, opts2) => request("POST", `/api/items/${slug}/batch`, {
|
|
522
|
+
operations: rows.map((data) => ({ op: "create", data })),
|
|
523
|
+
atomic: opts2?.atomic
|
|
524
|
+
}),
|
|
525
|
+
/** Bulk-update rows by id. */
|
|
526
|
+
updateMany: (updates, opts2) => request("POST", `/api/items/${slug}/batch`, {
|
|
527
|
+
operations: updates.map((u) => ({ op: "update", id: u.id, data: u.data })),
|
|
528
|
+
atomic: opts2?.atomic
|
|
529
|
+
}),
|
|
530
|
+
/** Bulk-delete rows by id. */
|
|
531
|
+
deleteMany: (ids, opts2) => request("POST", `/api/items/${slug}/batch`, {
|
|
532
|
+
operations: ids.map((id) => ({ op: "delete", id })),
|
|
533
|
+
atomic: opts2?.atomic
|
|
534
|
+
}),
|
|
535
|
+
/** Mixed create/update/delete in one request. `atomic` = all-or-nothing. */
|
|
536
|
+
batch: (operations, opts2) => request("POST", `/api/items/${slug}/batch`, {
|
|
537
|
+
operations,
|
|
538
|
+
atomic: opts2?.atomic
|
|
539
|
+
}),
|
|
540
|
+
/** Flip a versioned item to published (`_status`) now. */
|
|
541
|
+
publish: (id) => request("POST", `/api/items/${slug}/${id}/publish`),
|
|
542
|
+
/** Flip a versioned item back to draft (clears any pending schedule). */
|
|
543
|
+
unpublish: (id) => request("POST", `/api/items/${slug}/${id}/publish?unpublish=1`),
|
|
544
|
+
/** Schedule a versioned item to auto-publish at `at` (the cron tick applies
|
|
545
|
+
* it when due). Pass `null` to cancel a pending schedule. Requires the
|
|
546
|
+
* `publish` permission. */
|
|
547
|
+
schedulePublish: (id, at) => request("POST", `/api/items/${slug}/${id}/publish`, {
|
|
548
|
+
publishAt: at == null ? null : at instanceof Date ? at.toISOString() : at
|
|
549
|
+
})
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
const subscribe = (channel, onEvent, onError) => {
|
|
553
|
+
const url = `${opts.url}/api/realtime/${channel}/subscribe`;
|
|
554
|
+
const es = new EventSource(url, { withCredentials: true });
|
|
555
|
+
es.addEventListener("message", (ev) => {
|
|
556
|
+
try {
|
|
557
|
+
onEvent(JSON.parse(ev.data));
|
|
558
|
+
} catch (e) {
|
|
559
|
+
onError?.(e);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
es.addEventListener("error", (e) => onError?.(e));
|
|
563
|
+
return () => es.close();
|
|
564
|
+
};
|
|
565
|
+
const captureToken = (r) => {
|
|
566
|
+
if (opts.workspace && typeof r.token === "string") appToken = r.token;
|
|
567
|
+
return r;
|
|
568
|
+
};
|
|
569
|
+
const auth = {
|
|
570
|
+
/** Email + password sign-up. In app mode this creates a *workspace* end-
|
|
571
|
+
* user (in `app_users`), not a control-plane account. */
|
|
572
|
+
signUp: (input) => request("POST", `${authBase}/sign-up/email`, input).then(captureToken),
|
|
573
|
+
/** Email + password sign-in. */
|
|
574
|
+
signIn: (input) => request("POST", `${authBase}/sign-in/email`, input).then(captureToken),
|
|
575
|
+
/**
|
|
576
|
+
* Begin an OAuth sign-in. Returns `{ url }` — the provider's authorize
|
|
577
|
+
* page — which a browser app should navigate to (`location.href = url`).
|
|
578
|
+
* `provider` must be one of the ids returned by `auth.providers()`.
|
|
579
|
+
*/
|
|
580
|
+
signInSocial: (provider, input) => request("POST", `${authBase}/sign-in/social`, {
|
|
581
|
+
provider,
|
|
582
|
+
...input,
|
|
583
|
+
// ask better-auth for the URL instead of a 302, so the caller controls
|
|
584
|
+
// the navigation.
|
|
585
|
+
disableRedirect: true
|
|
586
|
+
}),
|
|
587
|
+
/** Send a one-time sign-in link by email (requires the `magic` provider
|
|
588
|
+
* to be enabled for the workspace). */
|
|
589
|
+
signInMagicLink: (input) => request("POST", `${authBase}/sign-in/magic-link`, input),
|
|
590
|
+
/** Email a one-time numeric code (requires the `email-otp` provider). `type`
|
|
591
|
+
* defaults to `"sign-in"`; use `"email-verification"` / `"forget-password"`
|
|
592
|
+
* for those flows. Complete a sign-in with `signInEmailOTP`. */
|
|
593
|
+
sendVerificationOTP: (input) => request("POST", `${authBase}/email-otp/send-verification-otp`, {
|
|
594
|
+
type: "sign-in",
|
|
595
|
+
...input
|
|
596
|
+
}),
|
|
597
|
+
/** Complete an email-OTP sign-in with the code from `sendVerificationOTP`. In
|
|
598
|
+
* app mode the returned session token is captured and replayed as a bearer. */
|
|
599
|
+
signInEmailOTP: (input) => request("POST", `${authBase}/sign-in/email-otp`, input).then(captureToken),
|
|
600
|
+
/** Send a password-reset email. `redirectTo` is the link the email points at. */
|
|
601
|
+
requestPasswordReset: (input) => request("POST", `${authBase}/request-password-reset`, input),
|
|
602
|
+
/** Complete a reset with the token from the email and a new password. */
|
|
603
|
+
resetPassword: (input) => request("POST", `${authBase}/reset-password`, input),
|
|
604
|
+
/** Mint a fresh short-lived access JWT from the stored session token (app
|
|
605
|
+
* mode). The SDK's own requests keep using the session token; use this when a
|
|
606
|
+
* downstream service needs a proper access token. */
|
|
607
|
+
refresh: () => request(
|
|
608
|
+
"POST",
|
|
609
|
+
`${authBase}/token/refresh`,
|
|
610
|
+
{ refreshToken: appToken }
|
|
611
|
+
),
|
|
612
|
+
/** Change the signed-in user's password (requires the current password). */
|
|
613
|
+
changePassword: (input) => request("POST", `${authBase}/change-password`, input),
|
|
614
|
+
/** Update the signed-in user's profile (e.g. `{ name, image }`). */
|
|
615
|
+
updateUser: (attributes) => request("POST", `${authBase}/update-user`, attributes),
|
|
616
|
+
/** Send an email-verification link to the signed-in (or named) user. */
|
|
617
|
+
sendVerificationEmail: (input) => request("POST", `${authBase}/send-verification-email`, input),
|
|
618
|
+
signOut: () => request("POST", `${authBase}/sign-out`).then((r) => {
|
|
619
|
+
if (opts.workspace) appToken = null;
|
|
620
|
+
return r;
|
|
621
|
+
}),
|
|
622
|
+
/** Current session, or `{ user: null }`. */
|
|
623
|
+
getSession: () => request("GET", `${authBase}/get-session`),
|
|
624
|
+
/** List the signed-in user's active sessions (one row per device/login). */
|
|
625
|
+
listSessions: () => request("GET", `${authBase}/list-sessions`),
|
|
626
|
+
/** Revoke one session by its `token` (from `listSessions`). */
|
|
627
|
+
revokeSession: (input) => request("POST", `${authBase}/revoke-session`, input),
|
|
628
|
+
/** Revoke every session **except** the current one (sign out other devices). */
|
|
629
|
+
revokeOtherSessions: () => request("POST", `${authBase}/revoke-other-sessions`),
|
|
630
|
+
/** Revoke **all** sessions, including the current one. */
|
|
631
|
+
revokeSessions: () => request("POST", `${authBase}/revoke-sessions`),
|
|
632
|
+
/** Public description of this workspace's auth surface (provider list +
|
|
633
|
+
* policy flags) — what a sign-in screen needs to render. No secrets. */
|
|
634
|
+
providers: () => request("GET", `${authBase}/providers`).then((r) => r.data),
|
|
635
|
+
/** The current workspace session token (app mode) — persist this across
|
|
636
|
+
* reloads and pass it back via `createClient({ token })`. */
|
|
637
|
+
getToken: () => appToken,
|
|
638
|
+
/** Restore a workspace session token (app mode). */
|
|
639
|
+
setToken: (token) => {
|
|
640
|
+
appToken = token;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const safeErr = async (res) => await res.json().catch(() => ({}));
|
|
644
|
+
const headOffset = async (location, signal) => {
|
|
645
|
+
const res = await f(`${opts.url}${location}`, {
|
|
646
|
+
method: "HEAD",
|
|
647
|
+
credentials: "include",
|
|
648
|
+
headers: { ...authHeader(), ...tenantHeader(), "Tus-Resumable": "1.0.0" },
|
|
649
|
+
signal
|
|
650
|
+
});
|
|
651
|
+
if (!res.ok) throw new BacklexError(res.status, void 0);
|
|
652
|
+
return Number(res.headers.get("Upload-Offset") ?? "0");
|
|
653
|
+
};
|
|
654
|
+
const patchLoop = async (location, src, chunkSize, onProgress, signal) => {
|
|
655
|
+
let offset = await headOffset(location, signal);
|
|
656
|
+
let retries = 0;
|
|
657
|
+
while (offset < src.size) {
|
|
658
|
+
const end = Math.min(offset + chunkSize, src.size);
|
|
659
|
+
try {
|
|
660
|
+
const res = await f(`${opts.url}${location}`, {
|
|
661
|
+
method: "PATCH",
|
|
662
|
+
credentials: "include",
|
|
663
|
+
headers: {
|
|
664
|
+
...authHeader(),
|
|
665
|
+
...tenantHeader(),
|
|
666
|
+
"Tus-Resumable": "1.0.0",
|
|
667
|
+
"Upload-Offset": String(offset),
|
|
668
|
+
"content-type": OFFSET_OCTET
|
|
669
|
+
},
|
|
670
|
+
body: src.slice(offset, end),
|
|
671
|
+
signal
|
|
672
|
+
});
|
|
673
|
+
if (res.status === 409) {
|
|
674
|
+
offset = await headOffset(location, signal);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (!res.ok) throw new BacklexError(res.status, await safeErr(res));
|
|
678
|
+
offset = Number(res.headers.get("Upload-Offset") ?? String(end));
|
|
679
|
+
retries = 0;
|
|
680
|
+
onProgress?.(offset, src.size);
|
|
681
|
+
} catch (e) {
|
|
682
|
+
if (signal?.aborted) throw e;
|
|
683
|
+
if (e instanceof BacklexError && e.status >= 400 && e.status < 500 && e.status !== 409) {
|
|
684
|
+
throw e;
|
|
685
|
+
}
|
|
686
|
+
if (++retries > 6) throw e;
|
|
687
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** (retries - 1)));
|
|
688
|
+
offset = await headOffset(location, signal);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
const storage = {
|
|
693
|
+
list: (prefix) => request(
|
|
694
|
+
"GET",
|
|
695
|
+
`/api/storage${prefix ? `?prefix=${encodeURIComponent(prefix)}` : ""}`
|
|
696
|
+
),
|
|
697
|
+
put: async (key, body, contentType, folderId) => {
|
|
698
|
+
const headers = {
|
|
699
|
+
...authHeader(),
|
|
700
|
+
...tenantHeader(),
|
|
701
|
+
...contentType ? { "content-type": contentType } : {}
|
|
702
|
+
};
|
|
703
|
+
const url = `${opts.url}/api/storage/${encodeURIComponent(key)}${folderId ? `?folderId=${folderId}` : ""}`;
|
|
704
|
+
const res = await f(url, {
|
|
705
|
+
method: "PUT",
|
|
706
|
+
credentials: "include",
|
|
707
|
+
headers,
|
|
708
|
+
body
|
|
709
|
+
});
|
|
710
|
+
if (!res.ok) {
|
|
711
|
+
const errBody = await res.json().catch(() => ({}));
|
|
712
|
+
throw new BacklexError(res.status, errBody);
|
|
713
|
+
}
|
|
714
|
+
return res.json();
|
|
715
|
+
},
|
|
716
|
+
download: async (key) => {
|
|
717
|
+
const res = await f(`${opts.url}/api/storage/${encodeURIComponent(key)}`, {
|
|
718
|
+
credentials: "include",
|
|
719
|
+
headers: { ...authHeader(), ...tenantHeader() }
|
|
720
|
+
});
|
|
721
|
+
if (!res.ok) {
|
|
722
|
+
throw new BacklexError(res.status, void 0);
|
|
723
|
+
}
|
|
724
|
+
return res;
|
|
725
|
+
},
|
|
726
|
+
delete: (key) => request(
|
|
727
|
+
"DELETE",
|
|
728
|
+
`/api/storage/${encodeURIComponent(key)}`
|
|
729
|
+
),
|
|
730
|
+
/**
|
|
731
|
+
* Resumable upload (TUS 1.0.0). Splits `data` into chunks and PATCHes them
|
|
732
|
+
* to `/api/uploads`, resuming from the server's committed offset after a
|
|
733
|
+
* transient failure. `data` may be a `Blob`/`File`, `ArrayBuffer`, or
|
|
734
|
+
* `Uint8Array`. Returns the final key + the TUS session `location` (persist
|
|
735
|
+
* it to resume across page reloads via `resumeUpload`). The standard TUS
|
|
736
|
+
* protocol means Uppy / tus-js-client can also target `/api/uploads`.
|
|
737
|
+
*/
|
|
738
|
+
uploadResumable: async (input) => {
|
|
739
|
+
const src = normalizeUploadData(input.data);
|
|
740
|
+
const meta = [`key ${b64(input.key)}`];
|
|
741
|
+
const ct = input.contentType ?? (input.data instanceof Blob ? input.data.type : "");
|
|
742
|
+
if (ct) meta.push(`contentType ${b64(ct)}`);
|
|
743
|
+
if (input.folderId) meta.push(`folderId ${b64(input.folderId)}`);
|
|
744
|
+
const res = await f(`${opts.url}/api/uploads`, {
|
|
745
|
+
method: "POST",
|
|
746
|
+
credentials: "include",
|
|
747
|
+
headers: {
|
|
748
|
+
...authHeader(),
|
|
749
|
+
...tenantHeader(),
|
|
750
|
+
"Tus-Resumable": "1.0.0",
|
|
751
|
+
"Upload-Length": String(src.size),
|
|
752
|
+
"Upload-Metadata": meta.join(",")
|
|
753
|
+
},
|
|
754
|
+
signal: input.signal
|
|
755
|
+
});
|
|
756
|
+
if (!res.ok) throw new BacklexError(res.status, await safeErr(res));
|
|
757
|
+
const location = res.headers.get("Location");
|
|
758
|
+
if (!location) throw new BacklexError(res.status, void 0);
|
|
759
|
+
await patchLoop(location, src, input.chunkSize ?? DEFAULT_CHUNK, input.onProgress, input.signal);
|
|
760
|
+
return { key: input.key, location };
|
|
761
|
+
},
|
|
762
|
+
/** Resume a previously-started resumable upload at the server's offset. */
|
|
763
|
+
resumeUpload: async (location, data, opts2) => {
|
|
764
|
+
await patchLoop(
|
|
765
|
+
location,
|
|
766
|
+
normalizeUploadData(data),
|
|
767
|
+
opts2?.chunkSize ?? DEFAULT_CHUNK,
|
|
768
|
+
opts2?.onProgress,
|
|
769
|
+
opts2?.signal
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
const messaging = {
|
|
774
|
+
/** Register (or refresh) the current user's push device. Re-registering the
|
|
775
|
+
* same token reactivates it and updates last-seen, so call this on every
|
|
776
|
+
* app launch. `web-push` requires `keys` (the VAPID subscription keys). */
|
|
777
|
+
registerDevice: (input) => request("POST", "/api/device-tokens", input),
|
|
778
|
+
/** Remove one of the caller's registered devices by id. */
|
|
779
|
+
unregister: (id) => request("DELETE", `/api/device-tokens/${encodeURIComponent(id)}`),
|
|
780
|
+
/** List the caller's registered devices. */
|
|
781
|
+
listDevices: () => request("GET", "/api/device-tokens"),
|
|
782
|
+
/** Register (or refresh) the current user's phone number for SMS. Number
|
|
783
|
+
* must be E.164 (e.g. "+14155552671"). Re-registering reactivates it. */
|
|
784
|
+
registerPhone: (input) => request("POST", "/api/phone-numbers", input),
|
|
785
|
+
/** Remove one of the caller's registered phone numbers by id. */
|
|
786
|
+
unregisterPhone: (id) => request("DELETE", `/api/phone-numbers/${encodeURIComponent(id)}`),
|
|
787
|
+
/** List the caller's registered phone numbers. */
|
|
788
|
+
listPhones: () => request("GET", "/api/phone-numbers")
|
|
789
|
+
};
|
|
790
|
+
const jobs = {
|
|
791
|
+
/** Enqueue a durable background job. `type` is `function` (run a named
|
|
792
|
+
* function with `payload.name` + `payload.input`) or `webhook.deliver`.
|
|
793
|
+
* Jobs retry with backoff and dead-letter after `maxAttempts`. Pass
|
|
794
|
+
* `runAt` (ISO string) to schedule for later. Admin-scoped. */
|
|
795
|
+
enqueue: (input) => request("POST", "/api/jobs", input),
|
|
796
|
+
/** List jobs (newest first), optionally filtered by queue/status. */
|
|
797
|
+
list: (q) => {
|
|
798
|
+
const params = new URLSearchParams();
|
|
799
|
+
if (q?.queue) params.set("queue", q.queue);
|
|
800
|
+
if (q?.status) params.set("status", q.status);
|
|
801
|
+
if (q?.limit != null) params.set("limit", String(q.limit));
|
|
802
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
803
|
+
return request("GET", `/api/jobs${suffix}`);
|
|
804
|
+
},
|
|
805
|
+
/** Fetch a single job by id. */
|
|
806
|
+
get: (id) => request("GET", `/api/jobs/${encodeURIComponent(id)}`),
|
|
807
|
+
/** Requeue a failed / dead-lettered / cancelled job to run again. */
|
|
808
|
+
retry: (id) => request("POST", `/api/jobs/${encodeURIComponent(id)}/retry`),
|
|
809
|
+
/** Cancel a pending job. */
|
|
810
|
+
cancel: (id) => request("POST", `/api/jobs/${encodeURIComponent(id)}/cancel`),
|
|
811
|
+
/** Delete a job row. */
|
|
812
|
+
remove: (id) => request("DELETE", `/api/jobs/${encodeURIComponent(id)}`)
|
|
813
|
+
};
|
|
814
|
+
let flagsCache = null;
|
|
815
|
+
const fetchFlags = async () => {
|
|
816
|
+
const res = await request("GET", "/api/flags");
|
|
817
|
+
flagsCache = res.data ?? {};
|
|
818
|
+
return flagsCache;
|
|
819
|
+
};
|
|
820
|
+
const flags = {
|
|
821
|
+
/** Fetch + cache the evaluated flag map. */
|
|
822
|
+
all: () => fetchFlags(),
|
|
823
|
+
/** Resolved value for a flag (remote config payload), or `undefined`. Uses
|
|
824
|
+
* the cache if `all()` was already called this session; pass
|
|
825
|
+
* `{ refresh: true }` to force a re-fetch. */
|
|
826
|
+
get: async (key, opts2) => {
|
|
827
|
+
const map = opts2?.refresh || !flagsCache ? await fetchFlags() : flagsCache;
|
|
828
|
+
return map[key]?.value;
|
|
829
|
+
},
|
|
830
|
+
/** Whether a flag is on for the caller. */
|
|
831
|
+
isEnabled: async (key, opts2) => {
|
|
832
|
+
const map = opts2?.refresh || !flagsCache ? await fetchFlags() : flagsCache;
|
|
833
|
+
return Boolean(map[key]?.enabled);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
const sync = (options) => createSync({ request, subscribe }, options);
|
|
837
|
+
return {
|
|
838
|
+
from: collection,
|
|
839
|
+
subscribe,
|
|
840
|
+
auth,
|
|
841
|
+
storage,
|
|
842
|
+
messaging,
|
|
843
|
+
jobs,
|
|
844
|
+
flags,
|
|
845
|
+
sync,
|
|
846
|
+
/** Raw escape hatch — issues a request with auth headers applied. */
|
|
847
|
+
request
|
|
848
|
+
};
|
|
849
|
+
};
|
|
850
|
+
var typedCollections = (client) => {
|
|
851
|
+
const collections = new Proxy({}, {
|
|
852
|
+
get: (_target, slug) => typeof slug === "string" ? client.from(slug) : void 0
|
|
853
|
+
});
|
|
854
|
+
return Object.assign(client, { collections });
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
export { QueryBuilder, createClient, createSync, indexedDbStore, memoryStore, typedCollections };
|