@jarenjs/db 0.56.0 → 0.67.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/ARCHITECTURE.md +412 -56
- package/README.md +600 -57
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +293 -45
- package/docs/LIVE-FORMAT.md +169 -20
- package/docs/MIGRATION-FORMAT.md +142 -17
- package/docs/MODEL-FORMAT.md +752 -64
- package/docs/REPLICATION-FORMAT.md +208 -0
- package/package.json +21 -7
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
- package/schemas/jaren-replication-snapshot.schema.json +83 -0
- package/schemas/jaren-replication.draft-07.schema.json +82 -0
- package/schemas/jaren-replication.schema.json +82 -0
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +230 -47
- package/src/cli.js +165 -59
- package/src/cursor.js +417 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +102 -8
- package/src/dialect.js +268 -113
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +244 -11
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +448 -61
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +17 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit.js +339 -48
- package/src/entity.js +20 -22
- package/src/errors.js +430 -19
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -17
- package/src/introspect.js +583 -0
- package/src/jobs.js +843 -107
- package/src/json-bytes.js +58 -0
- package/src/live-join.js +250 -0
- package/src/live-nested.js +120 -0
- package/src/live.js +18 -4
- package/src/logical-rows.js +90 -0
- package/src/maintenance.js +175 -0
- package/src/migrate.js +248 -181
- package/src/model.js +68 -0
- package/src/plan.js +1119 -138
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1634 -323
- package/src/replication-format.js +115 -0
- package/src/replication.js +332 -0
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1567 -273
- package/src/tracker.js +203 -29
- package/src/udf.js +88 -7
- package/types/index.d.ts +1158 -27
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +69 -2
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +27 -4
- package/types/wasm.d.ts +14 -0
package/src/cli.js
CHANGED
|
@@ -11,16 +11,20 @@
|
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import * as readline from 'readline';
|
|
14
|
-
import { pathToFileURL } from 'url';
|
|
15
14
|
|
|
16
|
-
import {
|
|
15
|
+
import { loadDocument as loadDocumentFile, isDocumentFile } from '@jarenjs/json/node';
|
|
17
16
|
|
|
18
17
|
import {
|
|
19
18
|
planModelMigration, migrate, migrationStatus, shapeHash, compareShapeToModel,
|
|
20
19
|
sqliteDialect, normalizeModel, normalizeEntities, explainMapping,
|
|
21
20
|
planCollection, planEntity, planJoinTable, HISTORY_TABLE, entityEmitModel,
|
|
21
|
+
migrateDocuments, streamDocuments, classifyAssertion,
|
|
22
22
|
} from './index.js';
|
|
23
23
|
import { nodeDriver } from './drivers/node.js';
|
|
24
|
+
import {
|
|
25
|
+
readDocuments, openAtomicTarget, openStreamTarget, openNullTarget,
|
|
26
|
+
formatOf, DOCUMENT_FORMATS,
|
|
27
|
+
} from './document-files.js';
|
|
24
28
|
|
|
25
29
|
const USAGE = `jaren-db — model-driven SQLite migrations
|
|
26
30
|
|
|
@@ -28,10 +32,12 @@ Usage:
|
|
|
28
32
|
jaren-db plan --from <model> --to <model> [--store <db>] [--id <name>] [--out <file>]
|
|
29
33
|
jaren-db plan --model <model> [--snapshot <file>] [--store <db>] [--id <name>] --out <file>
|
|
30
34
|
jaren-db snapshot --model <model> [--snapshot <file>] [--types <file>]
|
|
31
|
-
jaren-db status --model <model> --store <db>
|
|
35
|
+
jaren-db status --model <model> --store <db> [--migrations <dir>] [--snapshot <file>]
|
|
32
36
|
jaren-db apply --store <db> --baseline <model> --migrations <dir> [--model <m>] [--dry-run] [--yes]
|
|
33
|
-
jaren-db check --model <model> --store <db>
|
|
37
|
+
jaren-db check --model <model> --store <db> [--migrations <dir>] [--snapshot <file>]
|
|
34
38
|
jaren-db shape --model <model>
|
|
39
|
+
jaren-db documents --migrations <dir> --in <file|-> (--out <file|-> | --in-place --yes | --check)
|
|
40
|
+
[--format json|jsonl] [--out-format json|jsonl] [--collection <name>] [--batch-size <n>]
|
|
35
41
|
|
|
36
42
|
A <model> or a migration is a .json file, or a MODULE (.js, .mjs, .cjs —
|
|
37
43
|
or .ts where Node strips types) whose default export, or its 'model' /
|
|
@@ -57,6 +63,16 @@ apply Print every statement, then apply. Destructive steps (drop
|
|
|
57
63
|
check The CI command: exit 1 on an unplanned model change, pending
|
|
58
64
|
migrations or drift.
|
|
59
65
|
shape Print the physical mapping a model produces.
|
|
66
|
+
documents Run a migration's DOCUMENT steps (jslt, query) over a file of
|
|
67
|
+
documents instead of a database — a JSON array or JSONL, a
|
|
68
|
+
path or stdio. A step that needs tables (ddl, sql, rebuild,
|
|
69
|
+
derive) is refused by name before the first document is read.
|
|
70
|
+
--out writes a new file, --in-place replaces the input (a
|
|
71
|
+
sibling temporary is renamed over it only once every document
|
|
72
|
+
has survived every step; any failure leaves the original byte
|
|
73
|
+
for byte), and --check transforms and validates everything
|
|
74
|
+
while writing nothing. Exit: 0 applicable and valid, 1 a
|
|
75
|
+
migration or source failure, 2 a misuse of this command line.
|
|
60
76
|
`;
|
|
61
77
|
|
|
62
78
|
function fail(message) {
|
|
@@ -64,11 +80,24 @@ function fail(message) {
|
|
|
64
80
|
process.exit(1);
|
|
65
81
|
}
|
|
66
82
|
|
|
83
|
+
/**
|
|
84
|
+
* A command line that cannot be obeyed — a missing flag, two flags that
|
|
85
|
+
* contradict, a value that names nothing. Distinct from `fail`, because
|
|
86
|
+
* a caller scripting this command has to tell "you asked for the wrong
|
|
87
|
+
* thing" (2) from "what you asked for did not hold" (1).
|
|
88
|
+
*/
|
|
89
|
+
function misuse(message) {
|
|
90
|
+
console.error(`jaren-db: ${message}`);
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
|
|
67
94
|
function parseArgs(argv) {
|
|
68
95
|
const options = {
|
|
69
96
|
command: argv[2], from: null, to: null, model: null, store: null,
|
|
70
97
|
baseline: null, migrations: null, id: null, out: null,
|
|
71
98
|
snapshot: null, types: null,
|
|
99
|
+
in: null, format: null, outFormat: null, collection: null,
|
|
100
|
+
batchSize: null, inPlace: false, check: false,
|
|
72
101
|
dryRun: false, yes: false, help: false,
|
|
73
102
|
};
|
|
74
103
|
for (let i = 3; i < argv.length; i++) {
|
|
@@ -83,15 +112,26 @@ function parseArgs(argv) {
|
|
|
83
112
|
case '--out': options.out = argv[++i]; break;
|
|
84
113
|
case '--snapshot': options.snapshot = argv[++i]; break;
|
|
85
114
|
case '--types': options.types = argv[++i]; break;
|
|
115
|
+
case '--in': options.in = argv[++i]; break;
|
|
116
|
+
case '--out-format': options.outFormat = argv[++i]; break;
|
|
117
|
+
case '--format': options.format = argv[++i]; break;
|
|
118
|
+
case '--collection': options.collection = argv[++i]; break;
|
|
119
|
+
case '--batch-size': options.batchSize = argv[++i]; break;
|
|
120
|
+
case '--in-place': options.inPlace = true; break;
|
|
121
|
+
case '--check': options.check = true; break;
|
|
86
122
|
case '--dry-run': options.dryRun = true; break;
|
|
87
123
|
case '--yes': options.yes = true; break;
|
|
88
124
|
case '--help': case '-h': options.help = true; break;
|
|
89
|
-
|
|
125
|
+
// an unrecognised flag is a misuse of the command line, which is
|
|
126
|
+
// what `documents` promises exit 2 for; the five older commands
|
|
127
|
+
// keep the exit 1 their published contract has always used
|
|
128
|
+
default: (options.command === 'documents' ? misuse : fail)(`unknown option: ${argv[i]}`);
|
|
90
129
|
}
|
|
91
130
|
}
|
|
92
131
|
return options;
|
|
93
132
|
}
|
|
94
133
|
|
|
134
|
+
/** A committed snapshot: JSON on disk, read as it is. */
|
|
95
135
|
const readJson = (file, what) => {
|
|
96
136
|
try {
|
|
97
137
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -101,68 +141,28 @@ const readJson = (file, what) => {
|
|
|
101
141
|
}
|
|
102
142
|
};
|
|
103
143
|
|
|
104
|
-
const MODULE_EXT = /\.(?:m?js|cjs|m?ts|cts)$/;
|
|
105
|
-
const TS_EXT = /\.[mc]?ts$/;
|
|
106
|
-
let loads = 0;
|
|
107
|
-
|
|
108
|
-
/** The document a module exports — `default`, or the named export — as
|
|
109
|
-
* its JSON emission (a pen builder's `toJSON()`), or a named failure. */
|
|
110
|
-
const emissionOf = (mod, exportName, file) => {
|
|
111
|
-
const value = mod.default !== undefined ? mod.default : mod[exportName];
|
|
112
|
-
if (value === null || typeof value !== 'object') {
|
|
113
|
-
return fail(`module '${file}' exports neither a default nor a '${exportName}' document`);
|
|
114
|
-
}
|
|
115
|
-
let doc;
|
|
116
|
-
try {
|
|
117
|
-
doc = JSON.parse(JSON.stringify(value));
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
return fail(`module '${file}': the ${exportName} emission is not JSON (${error.message})`);
|
|
121
|
-
}
|
|
122
|
-
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
123
|
-
return fail(`module '${file}': the ${exportName} emission is not a document`);
|
|
124
|
-
}
|
|
125
|
-
return doc;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
144
|
/**
|
|
129
145
|
* A model or migration document: a `.json` file, or a module loaded
|
|
130
146
|
* TWICE — an emission that changes between loads is not pure (a clock,
|
|
131
147
|
* the environment, randomness), and a migration that hashes differently
|
|
132
|
-
* per load can never match its own history.
|
|
148
|
+
* per load can never match its own history. The loader is the suite's
|
|
149
|
+
* one (`@jarenjs/json/node`, shared with `jaren-contract`); every
|
|
150
|
+
* refusal it names exits here under this CLI's prefix.
|
|
133
151
|
*/
|
|
134
152
|
async function loadDocument(file, what, exportName) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const url = pathToFileURL(path.resolve(file)).href;
|
|
141
|
-
const load = async () => {
|
|
142
|
-
try {
|
|
143
|
-
return await import(`${url}?jaren-db-load=${++loads}`);
|
|
144
|
-
}
|
|
145
|
-
catch (error) {
|
|
146
|
-
return fail(`cannot load ${what} module '${file}': ${error.message}`
|
|
147
|
-
+ (TS_EXT.test(file)
|
|
148
|
-
? ' — a .ts module loads only where Node strips types (Node >= 24 does by default; --no-strip-types turns it off)'
|
|
149
|
-
: ''));
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
const first = emissionOf(await load(), exportName, file);
|
|
153
|
-
const second = emissionOf(await load(), exportName, file);
|
|
154
|
-
if (canonicalizeJson(first) !== canonicalizeJson(second)) {
|
|
155
|
-
return fail(`the ${what} module '${file}' is not pure — two loads emitted different documents; `
|
|
156
|
-
+ 'no clock, no env, no randomness in a model or migration module');
|
|
153
|
+
try {
|
|
154
|
+
return await loadDocumentFile(file, { what, exportName, impure: 'no clock, no env, no randomness in a model or migration module' });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return fail(error.message);
|
|
157
158
|
}
|
|
158
|
-
return first;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
/** The migrations directory, sorted by file name — the full ordered chain. */
|
|
162
162
|
const loadMigrationsDir = async (dir) => {
|
|
163
163
|
if (dir === null) return [];
|
|
164
164
|
const files = fs.readdirSync(dir)
|
|
165
|
-
.filter((file) => (file
|
|
165
|
+
.filter((file) => isDocumentFile(file))
|
|
166
166
|
.sort();
|
|
167
167
|
const migrations = [];
|
|
168
168
|
for (const file of files) migrations.push(await loadDocument(path.join(dir, file), 'migration', 'migration'));
|
|
@@ -267,12 +267,14 @@ async function commandPlan(options) {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
async function commandStatus(options, { asCheck }) {
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
// the history and the model are what a status read needs; the
|
|
271
|
+
// baseline anchors an APPLY (the chain's first shape), not a read —
|
|
272
|
+
// `--baseline` is still accepted so an existing invocation keeps working
|
|
273
|
+
if (options.store === null)
|
|
274
|
+
fail(`${asCheck ? 'check' : 'status'} needs --store`);
|
|
272
275
|
// `check` without the model verified nothing and printed "in sync"
|
|
273
276
|
if (asCheck && options.model === null)
|
|
274
277
|
fail('check needs --model — drift is measured against the model the code carries');
|
|
275
|
-
const baseline = await loadDocument(options.baseline, 'baseline model', 'model');
|
|
276
278
|
const model = options.model !== null ? await loadDocument(options.model, 'model', 'model') : undefined;
|
|
277
279
|
const migrations = await loadMigrationsDir(options.migrations);
|
|
278
280
|
// the snapshot discipline, when it is in use: a model that moved
|
|
@@ -291,7 +293,7 @@ async function commandStatus(options, { asCheck }) {
|
|
|
291
293
|
let status;
|
|
292
294
|
try {
|
|
293
295
|
status = await migrationStatus({ driver: nodeDriver(), path: options.store },
|
|
294
|
-
migrations, {
|
|
296
|
+
migrations, { model });
|
|
295
297
|
}
|
|
296
298
|
catch (error) {
|
|
297
299
|
return fail(error.message);
|
|
@@ -331,7 +333,7 @@ async function commandApply(options) {
|
|
|
331
333
|
const migrations = await loadMigrationsDir(options.migrations);
|
|
332
334
|
const target = { driver: nodeDriver(), path: options.store };
|
|
333
335
|
|
|
334
|
-
const status = await migrationStatus(target, migrations
|
|
336
|
+
const status = await migrationStatus(target, migrations)
|
|
335
337
|
.catch((error) => fail(error.message));
|
|
336
338
|
if (status.pending.length === 0) {
|
|
337
339
|
console.log('nothing to apply — up to date');
|
|
@@ -422,6 +424,109 @@ async function commandShape(options) {
|
|
|
422
424
|
console.log(`-- history rides in '${HISTORY_TABLE}'`);
|
|
423
425
|
}
|
|
424
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Run a migration's document steps over a file of documents.
|
|
429
|
+
*
|
|
430
|
+
* The runner is chosen by what the migration asks for, and named in the
|
|
431
|
+
* report: a cross-document assertion needs every document at once, so
|
|
432
|
+
* its collection is read into memory; anything else streams, holding one
|
|
433
|
+
* batch. Reading a rewindable FILE for the first is not a compromise —
|
|
434
|
+
* it is the same source, read twice.
|
|
435
|
+
*/
|
|
436
|
+
async function commandDocuments(options) {
|
|
437
|
+
if (options.migrations === null) misuse('documents needs --migrations <dir>');
|
|
438
|
+
if (options.in === null) misuse('documents needs --in <file> (or - for standard input)');
|
|
439
|
+
const sinks = [options.out !== null, options.inPlace, options.check].filter(Boolean).length;
|
|
440
|
+
if (sinks === 0)
|
|
441
|
+
misuse('documents needs one of --out <file>, --in-place or --check');
|
|
442
|
+
if (sinks > 1)
|
|
443
|
+
misuse('documents takes exactly one of --out, --in-place and --check');
|
|
444
|
+
if (options.inPlace && options.in === '-')
|
|
445
|
+
misuse('--in-place needs a file to replace, not standard input');
|
|
446
|
+
if (options.inPlace && !options.yes)
|
|
447
|
+
misuse('--in-place rewrites the input file — pass --yes to confirm, or --out to write elsewhere');
|
|
448
|
+
const batchSize = options.batchSize === null ? 500 : Number(options.batchSize);
|
|
449
|
+
if (!Number.isInteger(batchSize) || batchSize < 1)
|
|
450
|
+
misuse(`--batch-size must be a positive integer, not '${options.batchSize}'`);
|
|
451
|
+
|
|
452
|
+
const fromStdin = options.in === '-';
|
|
453
|
+
const inFormat = options.format ?? (fromStdin ? 'jsonl' : formatOf(options.in));
|
|
454
|
+
if (!DOCUMENT_FORMATS.includes(inFormat))
|
|
455
|
+
misuse(`--format must be one of ${DOCUMENT_FORMATS.join(', ')}, not '${inFormat}'`);
|
|
456
|
+
const target = options.inPlace ? options.in : options.out;
|
|
457
|
+
const toStdout = target === '-';
|
|
458
|
+
const outFormat = options.outFormat
|
|
459
|
+
?? (options.check || toStdout ? inFormat : formatOf(/** @type {string} */ (target)));
|
|
460
|
+
if (!DOCUMENT_FORMATS.includes(outFormat))
|
|
461
|
+
misuse(`--out-format must be one of ${DOCUMENT_FORMATS.join(', ')}, not '${outFormat}'`);
|
|
462
|
+
if (!fromStdin && !fs.existsSync(options.in)) fail(`no such file: '${options.in}'`);
|
|
463
|
+
|
|
464
|
+
const migrations = await loadMigrationsDir(options.migrations);
|
|
465
|
+
if (migrations.length === 0) fail(`no migration documents in '${options.migrations}'`);
|
|
466
|
+
|
|
467
|
+
// A document file holds ONE collection. The migrations say which:
|
|
468
|
+
// every document step must name it, or this chain cannot be applied to
|
|
469
|
+
// a file at all — running only the steps that match would leave the
|
|
470
|
+
// rest silently unapplied, which is the one outcome a migration runner
|
|
471
|
+
// may never produce.
|
|
472
|
+
const documentSteps = migrations.flatMap((migration) => migration.steps)
|
|
473
|
+
.filter((step) => step.kind === 'jslt' || step.kind === 'query');
|
|
474
|
+
const named = [...new Set(documentSteps.map((step) => step.collection))];
|
|
475
|
+
if (named.length > 1) {
|
|
476
|
+
fail(`a document file holds one collection, and these migrations touch ${named.length} `
|
|
477
|
+
+ `(${named.join(', ')}) — run them against a store, or split the chain so each `
|
|
478
|
+
+ 'migration touches the collection its file holds');
|
|
479
|
+
}
|
|
480
|
+
if (options.collection !== null && named.length === 1 && options.collection !== named[0]) {
|
|
481
|
+
misuse(`--collection names '${options.collection}', but these migrations touch `
|
|
482
|
+
+ `'${named[0]}' — is this the right file for them?`);
|
|
483
|
+
}
|
|
484
|
+
// with no document step at all there is no collection to infer; the
|
|
485
|
+
// run still proceeds, because a physical step must be REFUSED by name
|
|
486
|
+
// rather than reported as a missing collection
|
|
487
|
+
const collection = named[0] ?? options.collection ?? 'documents';
|
|
488
|
+
|
|
489
|
+
// only a MATERIALIZING assertion needs the collection at once; a
|
|
490
|
+
// per-document predicate and an associative aggregate are both
|
|
491
|
+
// answered one batch at a time, so they stream
|
|
492
|
+
const materializes = documentSteps.some((step) => step.collection === collection
|
|
493
|
+
&& step.kind === 'query' && classifyAssertion(step.assert).strategy === 'materialize');
|
|
494
|
+
|
|
495
|
+
const source = () => (fromStdin ? process.stdin : options.in);
|
|
496
|
+
let sink;
|
|
497
|
+
if (options.check) sink = openNullTarget();
|
|
498
|
+
else if (toStdout) sink = openStreamTarget(process.stdout, outFormat);
|
|
499
|
+
else sink = await openAtomicTarget(/** @type {string} */ (target), outFormat);
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
let report;
|
|
503
|
+
if (materializes) {
|
|
504
|
+
const documents = [];
|
|
505
|
+
for await (const document of readDocuments(source(), inFormat)) documents.push(document);
|
|
506
|
+
const out = await migrateDocuments({ [collection]: documents }, migrations, { batchSize });
|
|
507
|
+
report = out.report;
|
|
508
|
+
for (const document of out.documents[collection]) await sink.write(document);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
report = await streamDocuments({ [collection]: readDocuments(source(), inFormat) },
|
|
512
|
+
migrations, { batchSize, write: (name, document) => sink.write(document) });
|
|
513
|
+
}
|
|
514
|
+
const written = await sink.commit();
|
|
515
|
+
const counts = report.counts[collection] ?? { read: 0, transformed: 0, asserted: 0 };
|
|
516
|
+
console.log(`${options.check ? 'checked' : 'migrated'} '${collection}': `
|
|
517
|
+
+ `${counts.read} read, ${counts.transformed} transformed, ${counts.asserted} asserted `
|
|
518
|
+
+ `(${report.strategy[collection]})`);
|
|
519
|
+
console.log(`applied: ${report.applied.join(', ')}`);
|
|
520
|
+
if (options.check) console.log('checked only — nothing was written');
|
|
521
|
+
else if (toStdout) console.log(`wrote ${written.documents} document(s) to standard output`);
|
|
522
|
+
else console.log(`wrote ${written.documents} document(s) to ${target} (${written.bytes} bytes)`);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
await sink.abort();
|
|
526
|
+
return fail(error.message);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
425
530
|
async function main() {
|
|
426
531
|
const options = parseArgs(process.argv);
|
|
427
532
|
if (options.help || options.command === '--help' || options.command === '-h'
|
|
@@ -436,6 +541,7 @@ async function main() {
|
|
|
436
541
|
case 'check': return commandStatus(options, { asCheck: true });
|
|
437
542
|
case 'apply': return commandApply(options);
|
|
438
543
|
case 'shape': return commandShape(options);
|
|
544
|
+
case 'documents': return commandDocuments(options);
|
|
439
545
|
default: return fail(`unknown command '${options.command}' — try --help`);
|
|
440
546
|
}
|
|
441
547
|
}
|