marcidb-client 0.14.1 → 0.16.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/README.md +6 -3
- package/bin/marci-generate-darwin-arm64 +0 -0
- package/bin/marci-generate-linux-x64 +0 -0
- package/bin/marci-generate-win32-x64.exe +0 -0
- package/bin/marci-migrate-darwin-arm64 +0 -0
- package/bin/marci-migrate-linux-x64 +0 -0
- package/bin/marci-migrate-win32-x64.exe +0 -0
- package/bin.js +125 -9
- package/package.json +1 -1
- package/runtime/index.d.ts +48 -5
- package/runtime/index.js +55 -14
package/README.md
CHANGED
|
@@ -34,12 +34,15 @@ marcidb generate
|
|
|
34
34
|
marcidb generate schema.marci node_modules/.marcidb/client
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
3. Start your MarciDB server and connect:
|
|
37
|
+
3. Start your MarciDB server and connect — the database name is the last segment of the URL:
|
|
38
38
|
|
|
39
39
|
```typescript
|
|
40
40
|
import { marcidb } from "marcidb-client";
|
|
41
41
|
|
|
42
|
-
const db = marcidb("http://localhost:3000");
|
|
42
|
+
const db = marcidb("http://localhost:3000/myapp");
|
|
43
|
+
|
|
44
|
+
// a server started with --token (or an embedded app's maintenance port opened with one)
|
|
45
|
+
const secured = marcidb("http://localhost:3000/myapp", { token: process.env.MARCI_TOKEN });
|
|
43
46
|
```
|
|
44
47
|
|
|
45
48
|
## Usage
|
|
@@ -47,7 +50,7 @@ const db = marcidb("http://localhost:3000");
|
|
|
47
50
|
```typescript
|
|
48
51
|
import { marcidb } from "marcidb-client";
|
|
49
52
|
|
|
50
|
-
const db = marcidb("http://localhost:3000");
|
|
53
|
+
const db = marcidb("http://localhost:3000/myapp");
|
|
51
54
|
|
|
52
55
|
// Find — a chainable query; no select = id + every scalar
|
|
53
56
|
const users = await db.user.where({ name: { $startsWith: "A" } }).order("id", "desc").limit(20);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bin.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
5
7
|
import path from "node:path";
|
|
6
8
|
import os from "node:os";
|
|
7
9
|
import fs from "node:fs";
|
|
@@ -26,6 +28,9 @@ function binPath(base) {
|
|
|
26
28
|
return path.join(__dirname, "bin", `${base}-${suffix}`);
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
// Flags that never take a value (so `--yes backup.marcibin` keeps the file as a positional argument)
|
|
32
|
+
const BOOLEAN_FLAGS = new Set(["yes", "no-migrations"]);
|
|
33
|
+
|
|
29
34
|
// Parses positional arguments and flags (--flag value | --flag)
|
|
30
35
|
function parseArgs(args) {
|
|
31
36
|
const positional = [];
|
|
@@ -35,7 +40,7 @@ function parseArgs(args) {
|
|
|
35
40
|
if (a.startsWith("--")) {
|
|
36
41
|
const key = a.slice(2);
|
|
37
42
|
const next = args[i + 1];
|
|
38
|
-
if (next === undefined || next.startsWith("--")) {
|
|
43
|
+
if (BOOLEAN_FLAGS.has(key) || next === undefined || next.startsWith("--")) {
|
|
39
44
|
flags[key] = true;
|
|
40
45
|
} else {
|
|
41
46
|
flags[key] = next;
|
|
@@ -66,14 +71,45 @@ Commands:
|
|
|
66
71
|
--url <url> server url (default: http://localhost:3000)
|
|
67
72
|
--migrations <dir> migrations directory (default: migrations)
|
|
68
73
|
|
|
74
|
+
backup <db> [options] Download a consistent backup of a database (.marcibin)
|
|
75
|
+
--url <url> server url (default: http://localhost:3000)
|
|
76
|
+
--out <file|dir> where to write it (default: <db>-<YYYYMMDD-HHMMSS>.marcibin in cwd)
|
|
77
|
+
|
|
78
|
+
restore <db> <file> [options] Upload a backup; replaces the live database <db>
|
|
79
|
+
--url <url> server url (default: http://localhost:3000)
|
|
80
|
+
--into <name> restore under another name instead (created if missing)
|
|
81
|
+
--yes confirm replacing a database that exists
|
|
82
|
+
|
|
83
|
+
inspect <file|dir> [options] Read a backup, or a database directory no process has open
|
|
84
|
+
--tree <name> list one tree's entries (rows decoded through the schema)
|
|
85
|
+
--limit <n> entries to show (default: 20)
|
|
86
|
+
--id <json> one row of a model tree, by id
|
|
87
|
+
--raw hex keys/values; row headers annotated
|
|
88
|
+
|
|
89
|
+
Every command talking to a server sends --token <token> (or $MARCI_TOKEN) as a bearer token.
|
|
90
|
+
|
|
69
91
|
Examples:
|
|
70
92
|
marcidb generate
|
|
71
93
|
marcidb generate schema.marci --name add_users
|
|
72
94
|
marcidb migrate push myapp --url http://localhost:3000
|
|
73
95
|
marcidb migrate check myapp
|
|
96
|
+
marcidb backup myapp --out backups/
|
|
97
|
+
marcidb restore myapp backups/myapp-20261012-030000.marcibin --into myapp_check
|
|
98
|
+
marcidb inspect backups/myapp-20261012-030000.marcibin --tree User --limit 5
|
|
74
99
|
`);
|
|
75
100
|
}
|
|
76
101
|
|
|
102
|
+
// The server url (--url, default local) without a trailing slash
|
|
103
|
+
function serverUrl(flags) {
|
|
104
|
+
return (flags.url ?? "http://localhost:3000").replace(/\/+$/, "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// `Authorization: Bearer` for a server started with --token / MARCI_TOKEN
|
|
108
|
+
function authHeaders(flags) {
|
|
109
|
+
const token = typeof flags.token === "string" ? flags.token : process.env.MARCI_TOKEN;
|
|
110
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
111
|
+
}
|
|
112
|
+
|
|
77
113
|
function cmdGenerate(args) {
|
|
78
114
|
const { positional, flags } = parseArgs(args);
|
|
79
115
|
const schema = positional[0] ?? "schema.marci";
|
|
@@ -112,8 +148,8 @@ async function cmdMigrate(args) {
|
|
|
112
148
|
}
|
|
113
149
|
|
|
114
150
|
// The server's current snapshot ("" if the DB doesn't exist yet)
|
|
115
|
-
async function fetchServerSnapshot(url, db) {
|
|
116
|
-
const res = await fetch(`${url}/${db}/$snapshot
|
|
151
|
+
async function fetchServerSnapshot(url, db, flags) {
|
|
152
|
+
const res = await fetch(`${url}/${db}/$snapshot`, { headers: authHeaders(flags) });
|
|
117
153
|
if (res.ok) return await res.text();
|
|
118
154
|
if (res.status === 404) return "";
|
|
119
155
|
console.error(`Cannot read server snapshot [${res.status}]: ${await res.text()}`);
|
|
@@ -135,21 +171,21 @@ async function migratePush(rest) {
|
|
|
135
171
|
const db = positional[0];
|
|
136
172
|
if (!db) { console.error("marcidb migrate push: <db> name required"); process.exit(1); }
|
|
137
173
|
|
|
138
|
-
const url = (flags
|
|
174
|
+
const url = serverUrl(flags);
|
|
139
175
|
const dir = flags.migrations ?? "migrations";
|
|
140
176
|
if (!fs.existsSync(dir)) {
|
|
141
177
|
console.error(`marcidb migrate push: migrations directory "${dir}" not found — run "marcidb generate" first`);
|
|
142
178
|
process.exit(1);
|
|
143
179
|
}
|
|
144
180
|
|
|
145
|
-
const serverSnap = await fetchServerSnapshot(url, db);
|
|
181
|
+
const serverSnap = await fetchServerSnapshot(url, db, flags);
|
|
146
182
|
const plan = planActions(dir, serverSnap);
|
|
147
183
|
if (plan.error !== undefined) { console.error(`Migration planning failed: ${plan.error}`); process.exit(1); }
|
|
148
184
|
if (!plan.out.trim()) { console.log(`'${db}' is up to date — nothing to push`); return; }
|
|
149
185
|
|
|
150
186
|
const res = await fetch(`${url}/${db}/$migrate`, {
|
|
151
187
|
method: "POST",
|
|
152
|
-
headers: { "Content-Type": "text/plain" },
|
|
188
|
+
headers: { "Content-Type": "text/plain", ...authHeaders(flags) },
|
|
153
189
|
body: plan.out,
|
|
154
190
|
});
|
|
155
191
|
if (!res.ok) { console.error(`Migration failed [${res.status}]: ${await res.text()}`); process.exit(1); }
|
|
@@ -162,10 +198,10 @@ async function migrateCheck(rest) {
|
|
|
162
198
|
const db = positional[0];
|
|
163
199
|
if (!db) { console.error("marcidb migrate check: <db> name required"); process.exit(1); }
|
|
164
200
|
|
|
165
|
-
const url = (flags
|
|
201
|
+
const url = serverUrl(flags);
|
|
166
202
|
const dir = flags.migrations ?? "migrations";
|
|
167
203
|
|
|
168
|
-
const serverSnap = await fetchServerSnapshot(url, db);
|
|
204
|
+
const serverSnap = await fetchServerSnapshot(url, db, flags);
|
|
169
205
|
const plan = planActions(dir, serverSnap);
|
|
170
206
|
if (plan.error !== undefined) {
|
|
171
207
|
console.error(`'${db}' has DIVERGED — the server schema isn't a point in your migration history.`);
|
|
@@ -179,9 +215,89 @@ async function migrateCheck(rest) {
|
|
|
179
215
|
}
|
|
180
216
|
}
|
|
181
217
|
|
|
218
|
+
// `20261012-120000` for a local timestamp (the server's file name carries the snapshot time instead)
|
|
219
|
+
function stamp() {
|
|
220
|
+
return new Date().toISOString().replace(/\.\d+Z$/, "").replace(/[-:]/g, "").replace("T", "-");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Backup: GET /:db/$backup streamed to <out>.tmp, renamed when complete. A download that breaks off
|
|
224
|
+
// (the server aborts the response when its copy fails) leaves nothing behind.
|
|
225
|
+
async function cmdBackup(args) {
|
|
226
|
+
const { positional, flags } = parseArgs(args);
|
|
227
|
+
const db = positional[0];
|
|
228
|
+
if (!db) { console.error("marcidb backup: <db> name required"); process.exit(1); }
|
|
229
|
+
|
|
230
|
+
const url = serverUrl(flags);
|
|
231
|
+
const res = await fetch(`${url}/${db}/$backup`, { headers: authHeaders(flags) });
|
|
232
|
+
if (!res.ok) { console.error(`Backup failed [${res.status}]: ${await res.text()}`); process.exit(1); }
|
|
233
|
+
|
|
234
|
+
const offered = /filename="([^"]+)"/.exec(res.headers.get("content-disposition") ?? "")?.[1];
|
|
235
|
+
const name = path.basename(offered ?? `${db}-${stamp()}.marcibin`);
|
|
236
|
+
const outFlag = typeof flags.out === "string" ? flags.out : undefined;
|
|
237
|
+
const out = outFlag === undefined ? name
|
|
238
|
+
: fs.existsSync(outFlag) && fs.statSync(outFlag).isDirectory() ? path.join(outFlag, name)
|
|
239
|
+
: outFlag;
|
|
240
|
+
|
|
241
|
+
const tmp = `${out}.tmp`;
|
|
242
|
+
try {
|
|
243
|
+
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(tmp));
|
|
244
|
+
fs.renameSync(tmp, out);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
fs.rmSync(tmp, { force: true });
|
|
247
|
+
console.error(`Backup failed: the download broke off (${e.message ?? e}) — nothing was written`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
console.log(`Backed up '${db}' to ${out} (${fs.statSync(out).size} bytes)`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Restore: POST the file to /:db/$restore[?into=]. Replacing a database that exists needs --yes.
|
|
254
|
+
async function cmdRestore(args) {
|
|
255
|
+
const { positional, flags } = parseArgs(args);
|
|
256
|
+
const [db, file] = positional;
|
|
257
|
+
if (!db || !file) { console.error("marcidb restore: usage: marcidb restore <db> <file.marcibin> [--into <name>] [--yes]"); process.exit(1); }
|
|
258
|
+
if (!fs.existsSync(file)) { console.error(`marcidb restore: "${file}" not found`); process.exit(1); }
|
|
259
|
+
|
|
260
|
+
const url = serverUrl(flags);
|
|
261
|
+
const into = typeof flags.into === "string" ? flags.into : undefined;
|
|
262
|
+
const target = into ?? db;
|
|
263
|
+
if (!flags.yes) {
|
|
264
|
+
// Without --into the live database is replaced; with it, only when that name already exists.
|
|
265
|
+
const exists = into === undefined || (await fetch(`${url}/${into}/$snapshot`, { headers: authHeaders(flags) })).status !== 404;
|
|
266
|
+
if (exists) {
|
|
267
|
+
console.error(
|
|
268
|
+
`This replaces the database '${target}' on ${url} with the contents of ${file}:\n` +
|
|
269
|
+
`everything written to '${target}' after that backup was taken is lost.\n` +
|
|
270
|
+
`Run again with --yes to proceed${into === undefined ? ", or restore under another name with --into <name>" : ""}.`,
|
|
271
|
+
);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const query = into === undefined ? "" : `?into=${encodeURIComponent(into)}`;
|
|
277
|
+
const body = typeof fs.openAsBlob === "function" ? await fs.openAsBlob(file) : fs.readFileSync(file);
|
|
278
|
+
const res = await fetch(`${url}/${db}/$restore${query}`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { "Content-Type": "application/x-marcidb-backup", ...authHeaders(flags) },
|
|
281
|
+
body,
|
|
282
|
+
});
|
|
283
|
+
if (!res.ok) { console.error(`Restore failed [${res.status}]: ${await res.text()}`); process.exit(1); }
|
|
284
|
+
const info = await res.json();
|
|
285
|
+
console.log(`Restored ${file} into '${info.restored}' (${info.trees} trees, ${info.entries} entries, schema version ${info.schemaVersion}, taken ${info.createdAt})`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Inspect runs locally (no server): marci-migrate reads the file or the closed directory.
|
|
289
|
+
function cmdInspect(args) {
|
|
290
|
+
const r = spawnSync(binPath("marci-migrate"), ["inspect", ...args], { stdio: "inherit" });
|
|
291
|
+
if (r.error) { console.error(`marcidb inspect: ${r.error.message}`); process.exit(1); }
|
|
292
|
+
process.exit(r.status ?? 1);
|
|
293
|
+
}
|
|
294
|
+
|
|
182
295
|
const COMMANDS = {
|
|
183
296
|
generate: cmdGenerate,
|
|
184
297
|
migrate: cmdMigrate,
|
|
298
|
+
backup: cmdBackup,
|
|
299
|
+
restore: cmdRestore,
|
|
300
|
+
inspect: cmdInspect,
|
|
185
301
|
};
|
|
186
302
|
|
|
187
303
|
// --- main ---
|
package/package.json
CHANGED
package/runtime/index.d.ts
CHANGED
|
@@ -207,6 +207,8 @@ type MarciOp = {
|
|
|
207
207
|
query?: any;
|
|
208
208
|
data?: any;
|
|
209
209
|
id?: any;
|
|
210
|
+
key?: any;
|
|
211
|
+
onInsert?: any;
|
|
210
212
|
journal?: JournalArgs;
|
|
211
213
|
};
|
|
212
214
|
/** The arguments of a journal action (`$journalOpen` / `$journalRead` / `$journalDrop`). `wait` is in seconds. */
|
|
@@ -275,9 +277,11 @@ interface Query<
|
|
|
275
277
|
readonly [__sel]: Effective<T, Sel>;
|
|
276
278
|
/** Filter (marcidb `$where`). Repeated calls are ANDed. */
|
|
277
279
|
where(where: T["where"]): Query<T, Sel>;
|
|
278
|
-
/** Sort
|
|
280
|
+
/** Sort: `order("age", "desc")` or `order({ age: "desc" })`. A second `order()` adds the next key — ties on
|
|
281
|
+
* the first are broken by it (`order("createdAt", "desc").order("id")`); the keys may also come as an array
|
|
282
|
+
* in one call. Several keys sort in memory. */
|
|
279
283
|
order(field: keyof T["order"] & string, direction?: "asc" | "desc"): Query<T, Sel>;
|
|
280
|
-
order(order: T["order"]): Query<T, Sel>;
|
|
284
|
+
order(order: T["order"] | T["order"][]): Query<T, Sel>;
|
|
281
285
|
limit(n: number): Query<T, Sel>;
|
|
282
286
|
skip(n: number): Query<T, Sel>;
|
|
283
287
|
/** Keyset cursor: rows strictly after this id in the current order (`$cursor`). */
|
|
@@ -302,7 +306,26 @@ interface Query<
|
|
|
302
306
|
}>;
|
|
303
307
|
aggregate<A extends T["aggregate"]>(query: A): Op<AggregateResult<T["model"], A>> & Sub<T["name"], NoInfer<A>>;
|
|
304
308
|
insert(data: T["insert"]): Op<T["id"]>;
|
|
309
|
+
/** The new row by `select`'s shape — its defaults, dates and files as stored: one call, nothing to read back. */
|
|
310
|
+
insert<S extends T["query"]>(data: T["insert"], options: {
|
|
311
|
+
select: S;
|
|
312
|
+
}): Promise<Rows<T, S>>;
|
|
305
313
|
update(id: IdArg<T["id"]>, data: T["update"]): Op<void>;
|
|
314
|
+
/** Writes the row `key` names — the id, or the fields of ONE unique index (a `@unique` field, all the fields of
|
|
315
|
+
* a `@unique(<name>)` group): an update of `data` when it exists, an insert of key + data when not. `data` is
|
|
316
|
+
* in the insert form (plain values, `{ id }` for a relation; no `$` operators). `onInsert` adds fields used
|
|
317
|
+
* only when the row is created. Resolves to the id, like `insert`. */
|
|
318
|
+
upsert<K extends Partial<T["insert"]> | T["id"]>(key: K, data: Omit<T["insert"], keyof K>, options?: {
|
|
319
|
+
onInsert?: Partial<T["insert"]>;
|
|
320
|
+
}): Op<T["id"]>;
|
|
321
|
+
/** The row written, by `select`'s shape — as stored, whichever path the upsert took. */
|
|
322
|
+
upsert<
|
|
323
|
+
K extends Partial<T["insert"]> | T["id"],
|
|
324
|
+
S extends T["query"]
|
|
325
|
+
>(key: K, data: Omit<T["insert"], keyof K>, options: {
|
|
326
|
+
onInsert?: Partial<T["insert"]>;
|
|
327
|
+
select: S;
|
|
328
|
+
}): Promise<Rows<T, S>>;
|
|
306
329
|
/** Applies `data` to every row the chain's `where` matches (all rows without one); resolves to the number of rows. */
|
|
307
330
|
updateMany(data: T["update"]): Op<number>;
|
|
308
331
|
/** @deprecated pass the filter through the chain: `db.user.where(w).updateMany(data)` (removed in the next minor). */
|
|
@@ -376,6 +399,9 @@ type QueryLayerOptions = {
|
|
|
376
399
|
run(op: MarciOp): Promise<any>;
|
|
377
400
|
/** Hook for `insert` payloads (the lecodes SDK generates uuid ids here). */
|
|
378
401
|
prepareInsert?(model: string, data: any): any;
|
|
402
|
+
/** Hook for the `onInsert` part of an `upsert` — the fields used only when the row is created (the lecodes SDK
|
|
403
|
+
* puts a uuid id here). Receives what the caller gave (or undefined), returns what is sent. */
|
|
404
|
+
prepareUpsert?(model: string, onInsert: any): any;
|
|
379
405
|
};
|
|
380
406
|
/**
|
|
381
407
|
* The query builder over a set of models and a `run` function. Returns `op(descriptor)` — a lazy operation
|
|
@@ -386,7 +412,24 @@ declare function createQueryLayer(options: QueryLayerOptions): {
|
|
|
386
412
|
op: (descriptor: MarciOp) => any;
|
|
387
413
|
collection: (model: string) => any;
|
|
388
414
|
};
|
|
389
|
-
|
|
415
|
+
/** The class of a refused request — the server's `kind`: a constraint (`unique`, `foreign_key`, `restrict`,
|
|
416
|
+
* `duplicate_id`), `not_found`, `bad_request`, `unauthorized`, or `internal` for a fault of the server. */
|
|
417
|
+
type MarciErrorKind = "unique" | "foreign_key" | "restrict" | "duplicate_id" | "not_found" | "bad_request" | "unauthorized" | "internal";
|
|
418
|
+
/** A failed request: `kind` is what to switch on, `field` the `Model.field` a constraint is about, the message is for a person. */
|
|
419
|
+
declare class MarciError extends Error {
|
|
420
|
+
name: "MarciError";
|
|
421
|
+
readonly status: number;
|
|
422
|
+
readonly kind: MarciErrorKind;
|
|
423
|
+
readonly field?: string;
|
|
424
|
+
constructor(status: number, message: string, kind: MarciErrorKind, field?: string);
|
|
425
|
+
}
|
|
426
|
+
/** Options of the HTTP transport — the second argument of `marcidb(url, options)`. */
|
|
427
|
+
interface HttpOptions {
|
|
428
|
+
/** Sent as `Authorization: Bearer <token>` on every request: for a server started with `--token`, or an
|
|
429
|
+
* embedded app's maintenance port opened with one (`db.listen({ token })`). */
|
|
430
|
+
token?: string;
|
|
431
|
+
}
|
|
432
|
+
declare function request(method: string, url: string, body?: any, options?: HttpOptions): Promise<any>;
|
|
390
433
|
/**
|
|
391
434
|
* A binary-eligible read over HTTP. Advertises `Accept: <binary>, json` plus the client's schema
|
|
392
435
|
* fingerprint (`X-Marci-Schema`) and inspects the *response* content-type:
|
|
@@ -395,7 +438,7 @@ declare function request(method: string, url: string, body?: any): Promise<any>;
|
|
|
395
438
|
* schema/shape mismatch — so it sent the normal JSON body; no second round-trip needed).
|
|
396
439
|
* A single request handles both outcomes; `run()` in the generated client branches on the return shape.
|
|
397
440
|
*/
|
|
398
|
-
declare function requestBinary(url: string, body: any, schemaHash: string): Promise<Uint8Array | {
|
|
441
|
+
declare function requestBinary(url: string, body: any, schemaHash: string, options?: HttpOptions): Promise<Uint8Array | {
|
|
399
442
|
json: any;
|
|
400
443
|
}>;
|
|
401
444
|
/**
|
|
@@ -408,4 +451,4 @@ declare function createDecoderRegistry(MODELS: Record<string, FieldDesc[]>): {
|
|
|
408
451
|
decodeBuffer(decoder: (reader: any) => any, bytes: Uint8Array, many: boolean): any;
|
|
409
452
|
};
|
|
410
453
|
declare function encodeId(id: any): string;
|
|
411
|
-
export { AggregateKeys, AggregateResult, BareId, Collection, CompareNumValue, CompareRefListValue, CompareRefValue, CompareStrValue, CompareValue, CustomSearch, CustomSearchValue, Effective, FieldDesc, FieldOps, FullTextSearch, GetResult, IdArg, Journal, JournalArgs, JournalEntry, JournalOp, JournalOptions, JsonCondition, JsonPathWhere, JsonType, JsonValue, MarciOp, MarciTransport, ModelTypes, ModelsMeta, Never, NumOps, Only, Op, PrimitiveListUpdate, Query, QueryLayerOptions, RefListUpdate, RefListUpdateOrdered, RefListUpdateStruct, RefUpdate, RefUpdateStruct, Resolve, Rows, ServiceKeys, StrOps, Sub, UpdateNumValue, ValueOps, VectorSearch, WhereValue, createDecoderRegistry, createQueryLayer, encodeId, request, requestBinary };
|
|
454
|
+
export { AggregateKeys, AggregateResult, BareId, Collection, CompareNumValue, CompareRefListValue, CompareRefValue, CompareStrValue, CompareValue, CustomSearch, CustomSearchValue, Effective, FieldDesc, FieldOps, FullTextSearch, GetResult, HttpOptions, IdArg, Journal, JournalArgs, JournalEntry, JournalOp, JournalOptions, JsonCondition, JsonPathWhere, JsonType, JsonValue, MarciError, MarciErrorKind, MarciOp, MarciTransport, ModelTypes, ModelsMeta, Never, NumOps, Only, Op, PrimitiveListUpdate, Query, QueryLayerOptions, RefListUpdate, RefListUpdateOrdered, RefListUpdateStruct, RefUpdate, RefUpdateStruct, Resolve, Rows, ServiceKeys, StrOps, Sub, UpdateNumValue, ValueOps, VectorSearch, WhereValue, createDecoderRegistry, createQueryLayer, encodeId, request, requestBinary };
|
package/runtime/index.js
CHANGED
|
@@ -101,6 +101,11 @@ function createQueryLayer(options) {
|
|
|
101
101
|
return w ? { $where: w } : {};
|
|
102
102
|
};
|
|
103
103
|
const findMany = () => ({ model, action: "findMany", query: build() });
|
|
104
|
+
const readBack = (written, select) => written.then((id) => run({ model, action: "findFirst", query: resolveShape(model, { ...select, $where: id }) })).then((row) => {
|
|
105
|
+
if (row === null)
|
|
106
|
+
throw new Error(`marcidb: ${model}: the row just written is gone`);
|
|
107
|
+
return row;
|
|
108
|
+
});
|
|
104
109
|
return {
|
|
105
110
|
get __op() {
|
|
106
111
|
return findMany();
|
|
@@ -112,7 +117,12 @@ function createQueryLayer(options) {
|
|
|
112
117
|
catch: (onRejected) => run(findMany()).catch(onRejected),
|
|
113
118
|
finally: (onFinally) => run(findMany()).finally(onFinally),
|
|
114
119
|
where: (where) => make({ ...st, where: and(st.where, where) }),
|
|
115
|
-
order: (field, direction) =>
|
|
120
|
+
order: (field, direction) => {
|
|
121
|
+
const given = typeof field === "string" ? [{ [field]: direction ?? "asc" }] : Array.isArray(field) ? field : Object.entries(field).map(([k, v]) => ({ [k]: v }));
|
|
122
|
+
const prev = st.order === undefined ? [] : Array.isArray(st.order) ? st.order : [st.order];
|
|
123
|
+
const keys = [...prev, ...given];
|
|
124
|
+
return make({ ...st, order: keys.length === 1 ? keys[0] : keys });
|
|
125
|
+
},
|
|
116
126
|
limit: (n) => make({ ...st, limit: n }),
|
|
117
127
|
skip: (n) => make({ ...st, skip: n }),
|
|
118
128
|
after: (id) => make({ ...st, cursor: idObject(model, id) }),
|
|
@@ -128,8 +138,16 @@ function createQueryLayer(options) {
|
|
|
128
138
|
delete q.$where;
|
|
129
139
|
return Object.assign(op({ model, action: "aggregate", query: q }), { __select: q });
|
|
130
140
|
},
|
|
131
|
-
insert: (data) =>
|
|
141
|
+
insert: (data, extra) => {
|
|
142
|
+
const inserted = op({ model, action: "insert", data: options.prepareInsert ? options.prepareInsert(model, data) : data });
|
|
143
|
+
return extra?.select ? readBack(inserted, extra.select) : inserted;
|
|
144
|
+
},
|
|
132
145
|
update: (id, data) => op({ model, action: "update", id: idObject(model, id), data }),
|
|
146
|
+
upsert: (key, data, o) => {
|
|
147
|
+
const onInsert = options.prepareUpsert ? options.prepareUpsert(model, o?.onInsert) : o?.onInsert;
|
|
148
|
+
const written = op({ model, action: "upsert", key, data, ...onInsert !== undefined ? { onInsert } : {} });
|
|
149
|
+
return o?.select ? readBack(written, o.select) : written;
|
|
150
|
+
},
|
|
133
151
|
updateMany: (a, b) => b === undefined ? op({ model, action: "updateMany", query: whereOnly(), data: a }) : op({ model, action: "updateMany", query: whereOnly(a), data: b }),
|
|
134
152
|
delete: (id) => op({ model, action: "delete", id: idObject(model, id) }),
|
|
135
153
|
deleteMany: () => {
|
|
@@ -149,33 +167,55 @@ function createQueryLayer(options) {
|
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
// src/index.ts
|
|
152
|
-
|
|
170
|
+
class MarciError extends Error {
|
|
171
|
+
name = "MarciError";
|
|
172
|
+
status;
|
|
173
|
+
kind;
|
|
174
|
+
field;
|
|
175
|
+
constructor(status, message, kind, field) {
|
|
176
|
+
super(message);
|
|
177
|
+
this.status = status;
|
|
178
|
+
this.kind = kind;
|
|
179
|
+
this.field = field;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
var kindOfStatus = (status) => status === 404 ? "not_found" : status === 401 ? "unauthorized" : status >= 500 ? "internal" : "bad_request";
|
|
183
|
+
async function failure(res) {
|
|
184
|
+
const text = await res.text();
|
|
185
|
+
try {
|
|
186
|
+
const body = JSON.parse(text);
|
|
187
|
+
if (body && typeof body.error === "string")
|
|
188
|
+
return new MarciError(res.status, body.error, body.kind ?? kindOfStatus(res.status), body.field);
|
|
189
|
+
} catch {}
|
|
190
|
+
return new MarciError(res.status, text || `HTTP ${res.status}`, kindOfStatus(res.status));
|
|
191
|
+
}
|
|
192
|
+
function optionHeaders(options) {
|
|
193
|
+
return options?.token ? { Authorization: `Bearer ${options.token}` } : {};
|
|
194
|
+
}
|
|
195
|
+
async function request(method, url, body, options) {
|
|
153
196
|
const res = await fetch(url, {
|
|
154
197
|
method,
|
|
155
|
-
headers: { "Content-Type": "application/json" },
|
|
198
|
+
headers: { "Content-Type": "application/json", ...optionHeaders(options) },
|
|
156
199
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
157
200
|
});
|
|
158
|
-
if (!res.ok)
|
|
159
|
-
|
|
160
|
-
throw new Error(`MarciDB [${res.status}]: ${text}`);
|
|
161
|
-
}
|
|
201
|
+
if (!res.ok)
|
|
202
|
+
throw await failure(res);
|
|
162
203
|
return res.json();
|
|
163
204
|
}
|
|
164
205
|
var BINARY_MEDIA_TYPE = "application/x-marcidb-rows";
|
|
165
|
-
async function requestBinary(url, body, schemaHash) {
|
|
206
|
+
async function requestBinary(url, body, schemaHash, options) {
|
|
166
207
|
const res = await fetch(url, {
|
|
167
208
|
method: "POST",
|
|
168
209
|
headers: {
|
|
169
210
|
"Content-Type": "application/json",
|
|
170
211
|
Accept: `${BINARY_MEDIA_TYPE}, application/json`,
|
|
171
|
-
"X-Marci-Schema": schemaHash
|
|
212
|
+
"X-Marci-Schema": schemaHash,
|
|
213
|
+
...optionHeaders(options)
|
|
172
214
|
},
|
|
173
215
|
body: JSON.stringify(body ?? {})
|
|
174
216
|
});
|
|
175
|
-
if (!res.ok)
|
|
176
|
-
|
|
177
|
-
throw new Error(`MarciDB [${res.status}]: ${text}`);
|
|
178
|
-
}
|
|
217
|
+
if (!res.ok)
|
|
218
|
+
throw await failure(res);
|
|
179
219
|
const contentType = res.headers.get("content-type") ?? "";
|
|
180
220
|
if (contentType.includes(BINARY_MEDIA_TYPE)) {
|
|
181
221
|
return new Uint8Array(await res.arrayBuffer());
|
|
@@ -377,6 +417,7 @@ function encodeId(id) {
|
|
|
377
417
|
return String(id);
|
|
378
418
|
}
|
|
379
419
|
export {
|
|
420
|
+
MarciError,
|
|
380
421
|
createDecoderRegistry,
|
|
381
422
|
createQueryLayer,
|
|
382
423
|
encodeId,
|