marcidb-client 0.15.0 → 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 +9 -3
- package/runtime/index.js +8 -4
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
|
@@ -423,7 +423,13 @@ declare class MarciError extends Error {
|
|
|
423
423
|
readonly field?: string;
|
|
424
424
|
constructor(status: number, message: string, kind: MarciErrorKind, field?: string);
|
|
425
425
|
}
|
|
426
|
-
|
|
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>;
|
|
427
433
|
/**
|
|
428
434
|
* A binary-eligible read over HTTP. Advertises `Accept: <binary>, json` plus the client's schema
|
|
429
435
|
* fingerprint (`X-Marci-Schema`) and inspects the *response* content-type:
|
|
@@ -432,7 +438,7 @@ declare function request(method: string, url: string, body?: any): Promise<any>;
|
|
|
432
438
|
* schema/shape mismatch — so it sent the normal JSON body; no second round-trip needed).
|
|
433
439
|
* A single request handles both outcomes; `run()` in the generated client branches on the return shape.
|
|
434
440
|
*/
|
|
435
|
-
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 | {
|
|
436
442
|
json: any;
|
|
437
443
|
}>;
|
|
438
444
|
/**
|
|
@@ -445,4 +451,4 @@ declare function createDecoderRegistry(MODELS: Record<string, FieldDesc[]>): {
|
|
|
445
451
|
decodeBuffer(decoder: (reader: any) => any, bytes: Uint8Array, many: boolean): any;
|
|
446
452
|
};
|
|
447
453
|
declare function encodeId(id: any): string;
|
|
448
|
-
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, 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 };
|
|
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
|
@@ -189,10 +189,13 @@ async function failure(res) {
|
|
|
189
189
|
} catch {}
|
|
190
190
|
return new MarciError(res.status, text || `HTTP ${res.status}`, kindOfStatus(res.status));
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
function optionHeaders(options) {
|
|
193
|
+
return options?.token ? { Authorization: `Bearer ${options.token}` } : {};
|
|
194
|
+
}
|
|
195
|
+
async function request(method, url, body, options) {
|
|
193
196
|
const res = await fetch(url, {
|
|
194
197
|
method,
|
|
195
|
-
headers: { "Content-Type": "application/json" },
|
|
198
|
+
headers: { "Content-Type": "application/json", ...optionHeaders(options) },
|
|
196
199
|
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
197
200
|
});
|
|
198
201
|
if (!res.ok)
|
|
@@ -200,13 +203,14 @@ async function request(method, url, body) {
|
|
|
200
203
|
return res.json();
|
|
201
204
|
}
|
|
202
205
|
var BINARY_MEDIA_TYPE = "application/x-marcidb-rows";
|
|
203
|
-
async function requestBinary(url, body, schemaHash) {
|
|
206
|
+
async function requestBinary(url, body, schemaHash, options) {
|
|
204
207
|
const res = await fetch(url, {
|
|
205
208
|
method: "POST",
|
|
206
209
|
headers: {
|
|
207
210
|
"Content-Type": "application/json",
|
|
208
211
|
Accept: `${BINARY_MEDIA_TYPE}, application/json`,
|
|
209
|
-
"X-Marci-Schema": schemaHash
|
|
212
|
+
"X-Marci-Schema": schemaHash,
|
|
213
|
+
...optionHeaders(options)
|
|
210
214
|
},
|
|
211
215
|
body: JSON.stringify(body ?? {})
|
|
212
216
|
});
|