@mason-data/cli 0.9.0 → 0.10.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 -4
- package/dist/src/api/error.js +4 -0
- package/dist/src/api/postgres/proxy.js +6 -160
- package/dist/src/api/postgres/server.js +22 -4
- package/dist/src/cli/command.js +169 -5
- package/dist/src/cli/commands.js +290 -157
- package/dist/src/cli/deploy.js +134 -28
- package/dist/src/cli/doctor.js +14 -11
- package/dist/src/cli/guides.js +20 -11
- package/dist/src/cli/hosted.js +1 -1
- package/dist/src/cli/init.js +110 -25
- package/dist/src/cli/login.js +20 -17
- package/dist/src/cli/machine.js +125 -40
- package/dist/src/cli/main.js +51 -8
- package/dist/src/cli/mcp.js +15 -12
- package/dist/src/cli/project.js +1 -1
- package/dist/src/cli/registry.js +80 -35
- package/dist/src/cli/replica-hosted.js +761 -102
- package/dist/src/cli/replica.js +43 -5
- package/dist/src/cli/replication-mode.js +5 -0
- package/dist/src/cli/request-filter.js +16 -6
- package/dist/src/cli/run-server.js +25 -2
- package/dist/src/cli/secret-input.js +193 -0
- package/dist/src/cli/serve.js +2 -2
- package/dist/src/cli/share.js +20 -15
- package/dist/src/cli/source-role.js +14 -15
- package/dist/src/cli/sources.js +20 -3
- package/dist/src/cli/style.js +14 -0
- package/dist/src/cli/sync.js +17 -8
- package/dist/src/cli/token-hosted.js +30 -24
- package/dist/src/cli/tunnel.js +1 -1
- package/dist/src/cli/ui-hosted.js +163 -0
- package/dist/src/cli/ui-ssh.js +3 -3
- package/dist/src/cli/ui.js +67 -26
- package/dist/src/cli/usage.js +19 -15
- package/dist/src/cli/user-hosted.js +78 -13
- package/dist/src/cli/user.js +168 -50
- package/dist/src/duck/users.js +105 -9
- package/dist/src/lib/cidr.js +177 -0
- package/dist/src/lib/table-sync.js +69 -19
- package/package.json +6 -6
package/dist/src/cli/commands.js
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* mason tables [<table>…] [--json] the database's tables, their columns, keys and foreign keys
|
|
9
9
|
* mason add <table>… [--columns …] copy tables into Mason: every column, or the ones --columns names
|
|
10
10
|
* mason remove <table>… take tables out of the sources file; their copies are dropped
|
|
11
|
-
* mason wait [--timeout <
|
|
11
|
+
* mason wait [--timeout <duration>] [--json] until every added table is synced: 0 ready, 1 a copy failed (said), 124 timed out
|
|
12
12
|
* mason logs [-n <lines>] [-f] [--start] [--requests] [--events] the run's log; --start, the start's terminal; --requests, its request log; --events, the sync's events
|
|
13
13
|
* mason stop stop Mason for this folder's project (the copy stays)
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
-
import { asFailure, endText, errorJson, failed, jsonText, nextBlock, parseCommand, reportText, withEnd } from "./command.js";
|
|
17
|
+
import { asFailure, endText, errorJson, failed, failureCode, jsonText, listFlag, nextBlock, noArguments, parseCommand, reportText, withEnd } from "./command.js";
|
|
18
18
|
import { err as styleErr, out as styleOut, stepRenderer, wrap } from "./style.js";
|
|
19
19
|
import { alive, endRun, END_GRACE_MS, notAnswering, runState, theRun } from "./runs.js";
|
|
20
20
|
import { version } from "./version.js";
|
|
@@ -25,7 +25,7 @@ import { withPlaceholders, postgresFacts, postgresLink } from "./pg-url.js";
|
|
|
25
25
|
import { listenerText } from "./pg-tls.js";
|
|
26
26
|
import { entriesOfLines, logLineText } from "../lib/log-buffer.js";
|
|
27
27
|
import { LogReader, logPath } from "../lib/log-read.js";
|
|
28
|
-
import { catalogTable, entryName, looksSecret, tableNameProblem } from "./sources.js";
|
|
28
|
+
import { catalogTable, entryName, findEntry, looksSecret, tableNameProblem } from "./sources.js";
|
|
29
29
|
import { addEntry, Refusal, removeEntries, sourceToEdit } from "./source-edit.js";
|
|
30
30
|
import { connectSource, maskedUrl, parseSource, readSourceFacts } from "./preflight.js";
|
|
31
31
|
import { reach } from "./state.js";
|
|
@@ -33,7 +33,8 @@ import { copyingText, copyProgress, duration, generatedReadiness, msText, readin
|
|
|
33
33
|
import { pathText } from "../duck/kept-route.js";
|
|
34
34
|
import { DEFAULT_REQUEST_LIMIT, eventLimit, MAX_SEARCH, SLOW_MS, requestFilter, requestFilterParams } from "./request-filter.js";
|
|
35
35
|
import { countsText } from "../duck/stream.js";
|
|
36
|
-
import { agoText, BIG_TABLE_ROWS, parseSync, syncText } from "../lib/table-sync.js";
|
|
36
|
+
import { agoText, BIG_TABLE_ROWS, parseDuration, parseSync, syncText } from "../lib/table-sync.js";
|
|
37
|
+
import { didYouMean } from "../lib/did-you-mean.js";
|
|
37
38
|
const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
38
39
|
const err = (m) => { process.stderr.write(`${m}\n`); };
|
|
39
40
|
// ------------------------------------------------------------------ shared
|
|
@@ -265,7 +266,14 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
265
266
|
|
|
266
267
|
Status: running, not running, or hung — then this project's facts, one a line: its folder, its database and how
|
|
267
268
|
its changes are read, the run and the synced data's file and the synced tables in all (synced, rows, the last sync), the Postgres API
|
|
268
|
-
and how it listens.
|
|
269
|
+
and how it listens.
|
|
270
|
+
|
|
271
|
+
mason status
|
|
272
|
+
mason status --json
|
|
273
|
+
|
|
274
|
+
--json as data: its shape is at the end
|
|
275
|
+
|
|
276
|
+
Then each added table as mason wait shows it, with how it is kept current and when it was last synced
|
|
269
277
|
(sync: off, last synced 3 days ago; polled every 30s, last 4 s ago); the sync's latest failures (mason logs --events
|
|
270
278
|
lists every event) and the latest statements refused, failed or cancelled (mason logs --requests --failed); the Postgres API's statements of the last day and the last week (each outcome, the completed
|
|
271
279
|
ones' average, p50 and p95 time, the rows and bytes sent); the ones it answered slowly in the last day; and the ones it refused or
|
|
@@ -273,76 +281,6 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
273
281
|
running", how the last run ended, and the tables are the ones the sources file names. Hung is a run that is alive and does
|
|
274
282
|
not answer its commands (a statement may be holding its main thread): the same rows, and mason stop ends it (exit 1).
|
|
275
283
|
|
|
276
|
-
mason status
|
|
277
|
-
mason status --json
|
|
278
|
-
|
|
279
|
-
--json as data: { running, pid, postgres: { url, host, port, database, sslmode (require when it is TLS), shown (the URL with
|
|
280
|
-
<user>:<password> in the credentials' place), psql (its psql line), listener:
|
|
281
|
-
{ host (what it is bound to), tls (certificate, self-signed or null), fingerprint } }, source: { name (the
|
|
282
|
-
source's: its sources file's), file (the sources file), shownFile (it from the project's root), url, database, remote, version, standby, changes,
|
|
283
|
-
replication, demo (the demo database this run hosts), textOrder (what reading the database's text order left to say), sync: { default (every table's sync unless its entry
|
|
284
|
-
says its own: off, poll 30s), written (the file's top sync: says it; false: the default) } }, warnings, folder, project, tables: [{ table, rows,
|
|
285
|
-
bytes, key, added, addedColumns, copy, error (why its sync failed: its first sync, or the poll, catch-up, stream or
|
|
286
|
-
publication check keeping a synced table current; null otherwise), kept, sync: { state (synced, syncing,
|
|
287
|
-
behind: served and kept but older than its sync: promises, failed, waiting), syncedAt (when
|
|
288
|
-
it was last brought current), syncSeconds (how long that took), syncedBy (copy, poll, catch-up, stream),
|
|
289
|
-
ageSeconds (how long ago that was), asOf (the time the synced data holds the database as of: kept's syncedAt),
|
|
290
|
-
promisedSeconds (the age its sync: promises not to pass; null: sync: off promises nothing), behindBySeconds
|
|
291
|
-
(how far past it, 0 when not; null when nothing is promised), behindWhy (why, while behind; else null) }, … }], totals: { added, copied, failed, rows (the synced tables' rows),
|
|
292
|
-
syncedAt (the latest sync of any) }, engine: { file, bytes, keptDays (MASON_HISTORY_DAYS), version (the run's), startedAt,
|
|
293
|
-
uptimeSeconds, restarts (the times its supervisor started the run again: { count, last: { at, why, upSeconds }
|
|
294
|
-
or null }, or { error }) }, disk: { path (where the synced data's file is), freeBytes, copyBytes (the file),
|
|
295
|
-
projectedBytes (what the first syncs running are projected to add; null: none, or too early to say), error },
|
|
296
|
-
sessions: { open (the Postgres API's sessions logged in), byClient: [{ user ('' a token's), client (its
|
|
297
|
-
application_name), open, running }] }, share: (null, or what mason share has open) { port, fingerprint, sessions,
|
|
298
|
-
refusedLogins, droppedConnections, shares: [{ user, from (command: mason share; page: the web page), role, reads, openedAt, via, host, port, endsAt, state }] }, shareEnded: (null, or the last share that ended) { user, from, why, at }, queries: { keptDays, day, week } (the last day's and the last 7
|
|
299
|
-
days' statements, each { days, covered (the days of it the request log keeps), statements, completed, refused,
|
|
300
|
-
failed, cancelled, rows, bytesSent, durationMs: { avg, p50, p95 } (the completed ones') or null }; { error }
|
|
301
|
-
when it could not be read), slowQueries, refusedQueries, refusedTotal (how many in all), unsupported: [{ gap
|
|
302
|
-
(its entry of docs/SQL-SUPPORT.md), title (what it is; null for an entry this Mason no longer has), runs
|
|
303
|
-
(the statements it refused in the last day), users, clients (application_name), last, example (the latest,
|
|
304
|
-
cut at 1,000 characters) }] (the most refused first: GET /api/unsupported) or { error },
|
|
305
|
-
recentErrors: { sync: [{ at, kind, failed, table, message }] (the latest failures of the sync and restarts,
|
|
306
|
-
newest last: mason logs --events), error (why they could not be read, or null), queries: [{ requestId,
|
|
307
|
-
startedAt, outcome, sqlstate, message, query, … }] (the latest statements refused, failed or cancelled, newest
|
|
308
|
-
last, as mason logs --requests --json shows each: mason logs --requests --failed), queriesError (or null) },
|
|
309
|
-
readiness (what mason wait waits on), ui (whether this run serves the web page: mason ui), served (this is
|
|
310
|
-
mason serve's run: mason ui --ssh opens its page), sourcesReadOnly (why the page offers no Add or Remove — the
|
|
311
|
-
project's files deployed read-only, MASON_PROJECT_READONLY, which mason add and mason remove refuse too, or a
|
|
312
|
-
server's sources file it cannot write — or null), next }.
|
|
313
|
-
Each table's kept is how it is kept current now (absent for a table the sync does not hold):
|
|
314
|
-
{ by: poll, sync: { mode: off } or { mode: poll, every (30s), seconds } (its entry's sync:, else the file's,
|
|
315
|
-
else off), syncedAt (its last whole read or poll), nextAt (when its next poll is due; null: off, or not polled
|
|
316
|
-
now), pending (only when it applies: why it is not kept current now, its phase included: it waits for
|
|
317
|
-
its first sync, it is being read whole, its first sync failed, it is read again whole, caught up first) }
|
|
318
|
-
or { by: recopy, copiedAt, syncedAt, pending (only when it applies, as the poll's) }
|
|
319
|
-
or { by: replication, slot, state (streamed, catching-up, waiting, left-out), why, position, lastCommit,
|
|
320
|
-
notRunning (why the stream is not running, null when it is) }; each with promisedSeconds (the age of
|
|
321
|
-
its synced data past which it is behind: poll, its interval plus the larger of 60 s and twice its last poll;
|
|
322
|
-
replication, 120 s; off, null) and lateWhy (what would make it late now: the pass's or its own poll's
|
|
323
|
-
time, which Mason waits four times, the stream not running); with Mason stopped: { running: false, project, source: { name,
|
|
324
|
-
file, url, error (the sources file's problem, or null) }, engine, tables: [{ table }], lastRun, lastStop:
|
|
325
|
-
{ at, how } or null, next }; hung: the same with hung: { pid, problem, why },
|
|
326
|
-
exit 1. In replication mode stream: { slot, slotWal: { walHeldBytes (the WAL the slot holds on the
|
|
327
|
-
database), walStatus (reserved, extended, unreserved, lost), safeWalSizeBytes (the WAL left before Postgres
|
|
328
|
-
invalidates it; null: max_slot_wal_keep_size unset, nothing caps it), at } or { error }, publication, running, failure, position,
|
|
329
|
-
applied, database: { lsn, at } or { error }, lag: { bytes, seconds }, lastBatch: { appliedAt, committedAt },
|
|
330
|
-
tables: [{ table, state (streamed, catching-up, waiting, left-out), why, catchUp: { phase (queued, polling,
|
|
331
|
-
applying), from, startedAt, held, retryAt }, applied: { inserts, updates, deletes, truncates } }] }.
|
|
332
|
-
jsonPaths: [{ table, column, steps, either, canonical, asText (false: a path's -> and ->> share it), name,
|
|
333
|
-
state (waiting, backfilling, ready, dropped), rowsTotal, rowsDone, marked, statements, slowestMs, totalMs,
|
|
334
|
-
fill: { rows, bytesPerRow, seconds, basis (measured: from the last fill of its column; disk or text: modelled from
|
|
335
|
-
the column's bytes a row on disk, or its text's before it is on disk), rowsPerSecond } (the fill's estimate it
|
|
336
|
-
was decided on), filled: { rows, seconds } (what its own fill measured), queued (a backfill waiting for the fill
|
|
337
|
-
under way), keptAt, readyAt, why }]. generatedTables: [{ table, file, kind, method (whole: its SQL run again),
|
|
338
|
-
state (refused, waiting, building, failed, current, behind), targetLag, behindSeconds (across its chain: the
|
|
339
|
-
oldest base change below it not in it), behindSince (when that change was committed; null: none), missingTarget, serving,
|
|
340
|
-
lastRun: { at, seconds, rows }, error (the last run's, no value of a row in it), retryAt,
|
|
341
|
-
refusals: [{ key, message, why, options, text }], waitingFor, warnings }]. roles (a run with a roles file, mason/roles.yml: what a user added
|
|
342
|
-
with --role may read): { file, roles: [name], problem (what is wrong with the file now, every statement of a
|
|
343
|
-
user with a role refused while there is; null), default (the role of a user added without --role; null: the
|
|
344
|
-
file names none, and such a user reads nothing), all (the roles of tables: all) }
|
|
345
|
-
|
|
346
284
|
In replication mode a Stream section says where the stream is: its position against the database's (bytes behind),
|
|
347
285
|
when the newest change applied was committed and how long after it was applied, and each added table — streamed,
|
|
348
286
|
with the changes applied this run; being caught up by one poll; not streamed yet; or left out — and why.
|
|
@@ -355,6 +293,73 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
355
293
|
waiting (its fill's cost holds it back: the line says the estimate and what would store it), queued (its column
|
|
356
294
|
added, its fill after the one under way), backfilling (its existing rows being filled, N %) or ready (read from its
|
|
357
295
|
column). MASON_STORE_JSON_PATHS=off turns it off.
|
|
296
|
+
|
|
297
|
+
The --json answer: { running, pid, postgres: { url, host, port, database, sslmode (require when it is TLS), shown (the URL with
|
|
298
|
+
<user>:<password> in the credentials' place), psql (its psql line), listener:
|
|
299
|
+
{ host (what it is bound to), tls (certificate, self-signed or null), fingerprint } }, source: { name (the
|
|
300
|
+
source's: its sources file's), file (the sources file), shownFile (it from the project's root), url, database, remote, version, standby, changes,
|
|
301
|
+
replication, demo (the demo database this run hosts), textOrder (what reading the database's text order left to say), sync: { default (every table's sync unless its entry
|
|
302
|
+
says its own: off, poll 30s), written (the file's top sync: says it; false: the default) } }, warnings, folder, project, tables: [{ table, rows,
|
|
303
|
+
bytes, key, added, addedColumns, copy, error (why its sync failed: its first sync, or the poll, catch-up, stream or
|
|
304
|
+
publication check keeping a synced table current; null otherwise), kept, sync: { state (synced, syncing,
|
|
305
|
+
behind: served and kept but older than its sync: promises, failed, waiting), syncedAt (when
|
|
306
|
+
it was last brought current), syncSeconds (how long that took), syncedBy (copy, poll, catch-up, stream),
|
|
307
|
+
ageSeconds (how long ago that was), asOf (the time the synced data holds the database as of: kept's syncedAt),
|
|
308
|
+
promisedSeconds (the age its sync: promises not to pass; null: sync: off promises nothing), behindBySeconds
|
|
309
|
+
(how far past it, 0 when not; null when nothing is promised), behindWhy (why, while behind; else null) }, … }], totals: { added, copied, failed, rows (the synced tables' rows),
|
|
310
|
+
syncedAt (the latest sync of any) }, engine: { file, bytes, keptDays (MASON_HISTORY_DAYS), version (the run's), startedAt,
|
|
311
|
+
uptimeSeconds, restarts (the times its supervisor started the run again: { count, last: { at, why, upSeconds }
|
|
312
|
+
or null }, or { error }) }, disk: { path (where the synced data's file is), freeBytes, copyBytes (the file),
|
|
313
|
+
projectedBytes (what the first syncs running are projected to add; null: none, or too early to say), error },
|
|
314
|
+
sessions: { open (the Postgres API's sessions logged in), byClient: [{ user ('' a token's), client (its
|
|
315
|
+
application_name), open, running }] }, share: (null, or what mason share has open) { port, fingerprint, sessions,
|
|
316
|
+
refusedLogins, droppedConnections, shares: [{ user, from (command: mason share; page: the web page), role, reads, openedAt, via, host, port, endsAt, state }] }, shareEnded: (null, or the last share that ended) { user, from, why, at }, queries: { keptDays, day, week } (the last day's and the last 7
|
|
317
|
+
days' statements, each { days, covered (the days of it the request log keeps), statements, completed, refused,
|
|
318
|
+
failed, cancelled, rows, bytesSent, durationMs: { avg, p50, p95 } (the completed ones') or null }; { error }
|
|
319
|
+
when it could not be read), slowQueries, refusedQueries, refusedTotal (how many in all), unsupported: [{ gap
|
|
320
|
+
(its entry of docs/SQL-SUPPORT.md), title (what it is; null for an entry this Mason no longer has), runs
|
|
321
|
+
(the statements it refused in the last day), users, clients (application_name), last, example (the latest,
|
|
322
|
+
cut at 1,000 characters) }] (the most refused first: GET /api/unsupported) or { error },
|
|
323
|
+
recentErrors: { sync: [{ at, kind, failed, table, message }] (the latest failures of the sync and restarts,
|
|
324
|
+
newest last: mason logs --events), error (why they could not be read, or null), queries: [{ requestId,
|
|
325
|
+
startedAt, outcome, sqlstate, message, query, … }] (the latest statements refused, failed or cancelled, newest
|
|
326
|
+
last, as mason logs --requests --json shows each: mason logs --requests --failed), queriesError (or null) },
|
|
327
|
+
readiness (what mason wait waits on), ui (whether this run serves the web page: mason ui), served (this is
|
|
328
|
+
mason serve's run: mason ui --ssh opens its page), sourcesReadOnly (why the page offers no Add or Remove — the
|
|
329
|
+
project's files deployed read-only, MASON_PROJECT_READONLY, which mason add and mason remove refuse too, or a
|
|
330
|
+
server's sources file it cannot write — or null), next }.
|
|
331
|
+
Each table's kept is how it is kept current now (absent for a table the sync does not hold):
|
|
332
|
+
{ by: poll, sync: { mode: off } or { mode: poll, every (30s), seconds } (its entry's sync:, else the file's,
|
|
333
|
+
else off), syncedAt (its last whole read or poll), nextAt (when its next poll is due; null: off, or not polled
|
|
334
|
+
now), pending (only when it applies: why it is not kept current now, its phase included: it waits for
|
|
335
|
+
its first sync, it is being read whole, its first sync failed, it is read again whole, caught up first) }
|
|
336
|
+
or { by: recopy, copiedAt, syncedAt, pending (only when it applies, as the poll's) }
|
|
337
|
+
or { by: replication, slot, state (streamed, catching-up, waiting, left-out), why, position, lastCommit,
|
|
338
|
+
notRunning (why the stream is not running, null when it is) }; each with promisedSeconds (the age of
|
|
339
|
+
its synced data past which it is behind: poll, its interval plus the larger of 60 s and twice its last poll;
|
|
340
|
+
replication, 120 s; off, null) and lateWhy (what would make it late now: the pass's or its own poll's
|
|
341
|
+
time, which Mason waits four times, the stream not running); with Mason stopped: { running: false, project, source: { name,
|
|
342
|
+
file, url, error (the sources file's problem, or null) }, engine, tables: [{ table }], lastRun, lastStop:
|
|
343
|
+
{ at, how } or null, next }; hung: the same with hung: { pid, problem, why },
|
|
344
|
+
exit 1. In replication mode stream: { slot, slotWal: { walHeldBytes (the WAL the slot holds on the
|
|
345
|
+
database), walStatus (reserved, extended, unreserved, lost), safeWalSizeBytes (the WAL left before Postgres
|
|
346
|
+
invalidates it; null: max_slot_wal_keep_size unset, nothing caps it), at } or { error }, publication, running, failure, position,
|
|
347
|
+
applied, database: { lsn, at } or { error }, lag: { bytes, seconds }, lastBatch: { appliedAt, committedAt },
|
|
348
|
+
tables: [{ table, state (streamed, catching-up, waiting, left-out), why, catchUp: { phase (queued, polling,
|
|
349
|
+
applying), from, startedAt, held, retryAt }, applied: { inserts, updates, deletes, truncates } }] }.
|
|
350
|
+
jsonPaths: [{ table, column, steps, either, canonical, asText (false: a path's -> and ->> share it), name,
|
|
351
|
+
state (waiting, backfilling, ready, dropped), rowsTotal, rowsDone, marked, statements, slowestMs, totalMs,
|
|
352
|
+
fill: { rows, bytesPerRow, seconds, basis (measured: from the last fill of its column; disk or text: modelled from
|
|
353
|
+
the column's bytes a row on disk, or its text's before it is on disk), rowsPerSecond } (the fill's estimate it
|
|
354
|
+
was decided on), filled: { rows, seconds } (what its own fill measured), queued (a backfill waiting for the fill
|
|
355
|
+
under way), keptAt, readyAt, why }]. generatedTables: [{ table, file, kind, method (whole: its SQL run again),
|
|
356
|
+
state (refused, waiting, building, failed, current, behind), targetLag, behindSeconds (across its chain: the
|
|
357
|
+
oldest base change below it not in it), behindSince (when that change was committed; null: none), missingTarget, serving,
|
|
358
|
+
lastRun: { at, seconds, rows }, error (the last run's, no value of a row in it), retryAt,
|
|
359
|
+
refusals: [{ key, message, why, options, text }], waitingFor, warnings }]. roles (a run with a roles file, mason/roles.yml: what a user added
|
|
360
|
+
with --role may read): { file, roles: [name], problem (what is wrong with the file now, every statement of a
|
|
361
|
+
user with a role refused while there is; null), default (the role of a user added without --role; null: the
|
|
362
|
+
file names none, and such a user reads nothing), all (the roles of tables: all) }
|
|
358
363
|
`;
|
|
359
364
|
export const STATUS = {
|
|
360
365
|
needs: "project",
|
|
@@ -370,6 +375,9 @@ export async function status(argv) {
|
|
|
370
375
|
const f = parseCommand(STATUS, argv);
|
|
371
376
|
if (typeof f === "number")
|
|
372
377
|
return f;
|
|
378
|
+
const extra = noArguments(STATUS, f.positionals, f.values.json === true);
|
|
379
|
+
if (extra !== null)
|
|
380
|
+
return extra;
|
|
373
381
|
const r0 = await theRun(process.cwd());
|
|
374
382
|
// the first line goes straight to the point (Status: running); when the service is not running, the same rows and
|
|
375
383
|
// tables show, with "not running" for each service that is not
|
|
@@ -788,10 +796,12 @@ const TABLES_USAGE = `usage: mason tables [<table>…] [--all] [--json]
|
|
|
788
796
|
mason tables --all every table with its columns, at once (long)
|
|
789
797
|
|
|
790
798
|
--all every table with its columns, not only the named ones
|
|
791
|
-
--json as data:
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
799
|
+
--json as data: its shape is at the end
|
|
800
|
+
|
|
801
|
+
The --json answer: { database, source: { name (the source's: its sources file's), file (the sources file) },
|
|
802
|
+
tables: [{ table, rows, bytes, primaryKey, comment, addable,
|
|
803
|
+
reason, added, addedColumns (the columns it syncs: null when not added), copyKey (the key: the sources
|
|
804
|
+
file keys it by, null: its primary key), columns: [{ name, type, nullable, comment, references }] }], next }
|
|
795
805
|
`;
|
|
796
806
|
export const TABLES = {
|
|
797
807
|
// the project, not the run (the hands-on review of #451, item 15): what it lists is the database's catalog and the
|
|
@@ -978,12 +988,22 @@ async function viaRun(run, route, body) {
|
|
|
978
988
|
const ADD_USAGE = `usage: mason add <table>… [--columns <column>,…] [--include <column>,…] [--key <column>,…] [--sync <sync>] [--json]
|
|
979
989
|
|
|
980
990
|
Syncs tables into Mason: each one is added to the project's sources file (mason/sources/<name>.yml), under tables:
|
|
991
|
+
|
|
981
992
|
mason add orders every column: orders: {}
|
|
982
993
|
mason add orders --columns id,status,total those columns: orders: { columns: [id, status, total] }
|
|
983
994
|
mason add orders customers several tables, every column of each
|
|
984
995
|
mason add users --include password_hash every column, and one that looks like a secret
|
|
985
996
|
mason add event_log --key tenant_id,id a table with no primary key, keyed by those columns
|
|
986
997
|
mason add orders --sync "poll 30s" polled every 30 seconds: orders: { sync: poll 30s }
|
|
998
|
+
|
|
999
|
+
--columns <a,b,…> only these columns (one table at a time: the list is that table's); a list flag takes a comma
|
|
1000
|
+
list or the flag repeated, the same: --columns id,status is --columns id --columns status
|
|
1001
|
+
--include <a,b,…> these columns as well as every other one — a column that looks like a secret, named on purpose
|
|
1002
|
+
--key <a,b,…> for a table with no primary key: the columns that tell its rows apart, which it is keyed by (one
|
|
1003
|
+
table at a time; polled or off: logical replication cannot apply a change by them)
|
|
1004
|
+
--sync <sync> how it is kept current after its first sync, written on its entry: off, "poll 30s" or replication
|
|
1005
|
+
--json as data: its shape is at the end
|
|
1006
|
+
|
|
987
1007
|
A table is its name (orders) or schema.table (billing.invoices). Its primary key is always synced. The rest of the
|
|
988
1008
|
file — your comments, other tables — stays as it was.
|
|
989
1009
|
|
|
@@ -1007,19 +1027,14 @@ const ADD_USAGE = `usage: mason add <table>… [--columns <column>,…] [--inclu
|
|
|
1007
1027
|
and the table is synced again whole with them. A project deployed as committed (MASON_PROJECT_READONLY=1 where Mason
|
|
1008
1028
|
runs) is refused, its files left as they are: add the table where the project is committed, then deploy.
|
|
1009
1029
|
|
|
1010
|
-
--
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
bigOff (it is over ${BIG_TABLE_ROWS.toLocaleString("en-US")} rows and its entry was written sync: off), notReplicated (why
|
|
1019
|
-
its entry was written sync: poll 5m under a file whose sync: is replication, or null), pollCost (an estimate of
|
|
1020
|
-
what a frequent poll of a big table costs your database, or null) } or { table, error }],
|
|
1021
|
-
file (the sources file), error (when a table was not added: { code, message, why }), next };
|
|
1022
|
-
exit 1 when a table could not be added
|
|
1030
|
+
The --json answer: { added: [{ table, entry, columns (null: every one), exclude, key, addedKey (key columns
|
|
1031
|
+
added to --columns), skippedSecrets, already (the file said so already), sync: { value (off, poll
|
|
1032
|
+
30s), from (entry: its own sync:, file: the file's, default: off, as the file writes none) },
|
|
1033
|
+
bigOff (it is over ${BIG_TABLE_ROWS.toLocaleString("en-US")} rows and its entry was written sync: off), notReplicated (why
|
|
1034
|
+
its entry was written sync: poll 5m under a file whose sync: is replication, or null), pollCost (an estimate of
|
|
1035
|
+
what a frequent poll of a big table costs your database, or null) } or { table, error }],
|
|
1036
|
+
file (the sources file), error (when a table was not added: { code, message, why }), next };
|
|
1037
|
+
exit 1 when a table could not be added
|
|
1023
1038
|
`;
|
|
1024
1039
|
export const ADD = {
|
|
1025
1040
|
// the project, not the run: the sources file is the list, so a table is added before mason start as well as after
|
|
@@ -1027,7 +1042,7 @@ export const ADD = {
|
|
|
1027
1042
|
name: "add",
|
|
1028
1043
|
summary: "sync tables into Mason, adding them to the sources file: mason add orders (--columns id,status: those only)",
|
|
1029
1044
|
usage: ADD_USAGE,
|
|
1030
|
-
options: { json: { type: "boolean" }, columns: { type: "string" }, include: { type: "string" }, key: { type: "string" }, sync: { type: "string" } },
|
|
1045
|
+
options: { json: { type: "boolean" }, columns: { type: "string", multiple: true }, include: { type: "string", multiple: true }, key: { type: "string", multiple: true }, sync: { type: "string" } },
|
|
1031
1046
|
json: ["added", "file", "error", "next"],
|
|
1032
1047
|
};
|
|
1033
1048
|
/** `--sync`, read by the sources file's own grammar (src/lib/table-sync.ts parseSync): a sync, or the usage's problem */
|
|
@@ -1070,7 +1085,7 @@ export async function add(argv) {
|
|
|
1070
1085
|
if (typeof f === "number")
|
|
1071
1086
|
return f;
|
|
1072
1087
|
const json = f.values.json === true;
|
|
1073
|
-
const specs = addSpecs(f.positionals, f.values.columns, f.values.include, f.values.key);
|
|
1088
|
+
const specs = addSpecs(f.positionals, listFlag(f.values.columns), listFlag(f.values.include), listFlag(f.values.key));
|
|
1074
1089
|
if (typeof specs === "string")
|
|
1075
1090
|
return failed("add", { message: specs, code: "usage", usage: ADD_USAGE }, { json });
|
|
1076
1091
|
const sync = addSync(f.values.sync);
|
|
@@ -1213,7 +1228,7 @@ export function addedLines(b, file) {
|
|
|
1213
1228
|
] };
|
|
1214
1229
|
}
|
|
1215
1230
|
// ------------------------------------------------------------------ mason remove
|
|
1216
|
-
const REMOVE_USAGE = `usage: mason remove <table>… [--json]
|
|
1231
|
+
const REMOVE_USAGE = `usage: mason remove <table>… [--dry-run] [--json]
|
|
1217
1232
|
|
|
1218
1233
|
Takes tables out of the project: each one's entry in the sources file (mason/sources/<name>.yml) is removed, the
|
|
1219
1234
|
rest of the file as it was. With Mason running it stops syncing the table and drops its synced data at once; with Mason
|
|
@@ -1223,8 +1238,12 @@ const REMOVE_USAGE = `usage: mason remove <table>… [--json]
|
|
|
1223
1238
|
|
|
1224
1239
|
mason remove customers
|
|
1225
1240
|
mason remove customers billing.invoices several at once
|
|
1241
|
+
mason remove customers --dry-run what it would remove, changing nothing
|
|
1226
1242
|
|
|
1227
|
-
--
|
|
1243
|
+
--dry-run say which entries would go, and change nothing: the file is read, never written, and neither Mason nor
|
|
1244
|
+
the database is asked
|
|
1245
|
+
--json as data: { dryRun, removedTables (the tables, schema.table; on a dry run, the ones that would go), removed
|
|
1246
|
+
(the same, kept until 1.0; not on a dry run), file (the sources file), next }
|
|
1228
1247
|
`;
|
|
1229
1248
|
export const REMOVE = {
|
|
1230
1249
|
// the project, not the run, as mason add
|
|
@@ -1232,8 +1251,8 @@ export const REMOVE = {
|
|
|
1232
1251
|
name: "remove",
|
|
1233
1252
|
summary: "take tables out of the sources file: mason remove customers (its synced data is dropped)",
|
|
1234
1253
|
usage: REMOVE_USAGE,
|
|
1235
|
-
options: { json: { type: "boolean" } },
|
|
1236
|
-
json: ["removed", "file", "next"],
|
|
1254
|
+
options: { json: { type: "boolean" }, "dry-run": { type: "boolean" } },
|
|
1255
|
+
json: ["dryRun", "removedTables", "removed", "file", "next"],
|
|
1237
1256
|
};
|
|
1238
1257
|
export async function remove(argv) {
|
|
1239
1258
|
const f = parseCommand(REMOVE, argv);
|
|
@@ -1251,6 +1270,8 @@ export async function remove(argv) {
|
|
|
1251
1270
|
const readOnly = projectWriteRefusal(where.project.root, process.env);
|
|
1252
1271
|
if (readOnly !== null)
|
|
1253
1272
|
return failed("remove", readOnly, { json });
|
|
1273
|
+
if (f.values["dry-run"] === true)
|
|
1274
|
+
return removeDryRun(where.project, where.state === "up", names, json);
|
|
1254
1275
|
if (where.state === "up") {
|
|
1255
1276
|
// Mason up: the run removes it and drops the copy at once (src/cli/run-server.ts removeTables)
|
|
1256
1277
|
const run = where.run;
|
|
@@ -1272,13 +1293,53 @@ function removedAnswer(r, names, running, json) {
|
|
|
1272
1293
|
const next = running ? [["mason status", "the tables left"]]
|
|
1273
1294
|
: [["mason start", "Mason in the background, for this project: it syncs the tables left"], ["mason status", "the tables left"]];
|
|
1274
1295
|
if (json) {
|
|
1275
|
-
out(jsonText(REMOVE, { removed: b.removed, file: b.file }, next));
|
|
1296
|
+
out(jsonText(REMOVE, { dryRun: false, removed: b.removed, removedTables: b.removed, file: b.file }, next));
|
|
1276
1297
|
return 0;
|
|
1277
1298
|
}
|
|
1278
1299
|
const rel = path.relative(process.cwd(), b.file);
|
|
1279
1300
|
out(reportText({ ...removedReport(b.removed, rel.startsWith("..") ? b.file : rel, { running }), next }, styleOut()));
|
|
1280
1301
|
return 0;
|
|
1281
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* `mason remove --dry-run`: the entries the sources file would lose, CHANGING NOTHING — read as the edit reads the file
|
|
1305
|
+
* (`sourceToEdit`: its refusals said the same), each name found as the edit finds it (`findEntry`); neither the run nor
|
|
1306
|
+
* the database is asked, so the file's other entries are not checked against the database here, as the edit does.
|
|
1307
|
+
*/
|
|
1308
|
+
async function removeDryRun(p, up, names, json) {
|
|
1309
|
+
if (!up) {
|
|
1310
|
+
const st = await runState(p);
|
|
1311
|
+
// a hung run refuses the edit itself (withoutRun): a dry run says what the edit would
|
|
1312
|
+
if (st.kind === "hung")
|
|
1313
|
+
return failed("remove", { ...asFailure(notAnswering(p, st.run)), code: "unreachable" }, { json });
|
|
1314
|
+
}
|
|
1315
|
+
const r = edited(() => {
|
|
1316
|
+
const at = sourceToEdit(p.folder, p.root, () => projectSource(p));
|
|
1317
|
+
const tables = names.map((n) => {
|
|
1318
|
+
const e = findEntry(at.parsed, n);
|
|
1319
|
+
if ("problem" in e)
|
|
1320
|
+
throw new Refusal(e.usage ? 400 : 404, e.problem);
|
|
1321
|
+
return e.table;
|
|
1322
|
+
});
|
|
1323
|
+
return { removed: [...new Set(tables)], file: at.file };
|
|
1324
|
+
});
|
|
1325
|
+
if (!("ok" in r)) {
|
|
1326
|
+
return failed("remove", { message: `${names.join(", ")} would not be removed`, code: r.status === 404 ? "not_found" : r.status === 400 ? "usage" : "refused", why: r.error,
|
|
1327
|
+
fix: "nothing was changed (a dry run)", next: [["mason status", "the tables added"]] }, { json });
|
|
1328
|
+
}
|
|
1329
|
+
const b = r.ok;
|
|
1330
|
+
const next = [[`mason remove ${names.join(" ")}`, "remove them"]];
|
|
1331
|
+
if (json) {
|
|
1332
|
+
out(jsonText(REMOVE, { dryRun: true, removedTables: b.removed, file: b.file }, next));
|
|
1333
|
+
return 0;
|
|
1334
|
+
}
|
|
1335
|
+
const rel = path.relative(process.cwd(), b.file);
|
|
1336
|
+
const one = b.removed.length === 1;
|
|
1337
|
+
out(reportText({ headline: `mason remove would remove ${b.removed.join(", ")}; nothing was changed`, details: [["from", rel.startsWith("..") ? b.file : rel]],
|
|
1338
|
+
tips: [up ? `Mason would stop syncing ${one ? "it" : "them"} and drop ${one ? "its" : "their"} synced data at once`
|
|
1339
|
+
: `what an earlier run synced of ${one ? "it" : "them"} would be dropped at the next mason start`,
|
|
1340
|
+
"mason remove also checks the file's other entries against the database, which a dry run does not ask"], next }, styleOut()));
|
|
1341
|
+
return 0;
|
|
1342
|
+
}
|
|
1282
1343
|
/** What `mason remove` prints for the tables removed, `file` the sources file as the reader is shown it (the web page
|
|
1283
1344
|
* says the same, src/web/app.js; tests/web-page.test.ts holds the two equal). */
|
|
1284
1345
|
export function removedReport(removed, file, o = {}) {
|
|
@@ -1294,30 +1355,22 @@ export function removedReport(removed, file, o = {}) {
|
|
|
1294
1355
|
export function waitTable(t) {
|
|
1295
1356
|
return { table: t.table, copy: t.copy, error: t.error, ...(t.kept ? { kept: t.kept } : {}), ...(t.sync ? { sync: t.sync } : {}) };
|
|
1296
1357
|
}
|
|
1297
|
-
const WAIT_USAGE = `usage: mason wait [<
|
|
1358
|
+
const WAIT_USAGE = `usage: mason wait [<table>…] [--timeout <duration>] [--json]
|
|
1298
1359
|
|
|
1299
1360
|
Waits until every added table is synced and served, showing every added table on a line of its own: 100% for the
|
|
1300
1361
|
finished (in the order they finished), a bar for the ones still syncing, at the bottom. Run it after mason add.
|
|
1301
|
-
Named
|
|
1362
|
+
Named synced tables: waits for those alone, until the first sync of each is done and it is served. Named, a
|
|
1363
|
+
generated table (alone) instead: waits until it holds every change Mason had synced when the wait began, across its
|
|
1302
1364
|
chain (the tables it is generated from, and theirs), so what is read from it next includes them.
|
|
1303
1365
|
|
|
1304
1366
|
mason add orders && mason wait
|
|
1305
|
-
mason wait --timeout
|
|
1367
|
+
mason wait --timeout 1h a large table: wait up to an hour
|
|
1368
|
+
mason wait orders customers these synced tables only
|
|
1306
1369
|
mason wait daily_requests a generated table: until it holds what is synced now
|
|
1307
1370
|
|
|
1308
|
-
--timeout <
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
rate, the seconds left, while its chunks are read }; kept as mason status --json says it —
|
|
1312
|
-
{ by: poll, sync: { mode: off } or { mode: poll, every, seconds }, syncedAt (its last whole read
|
|
1313
|
-
or poll), nextAt (its next poll; null: off) }; kept's pending, when it applies, says why
|
|
1314
|
-
it is not kept current now, its phase included; sync as mason status --json says it: its state —
|
|
1315
|
-
synced, syncing, behind, failed, waiting — asOf, promisedSeconds, behindBySeconds, behindWhy),
|
|
1316
|
-
stream (replication mode: where the stream is, as mason status --json says it), error (on a
|
|
1317
|
-
failure or a timeout: { code, message, next }), next }
|
|
1318
|
-
Named, a generated table: the same ready, failed, waiting, timedOut, error and next, tables
|
|
1319
|
-
empty, and generatedTable (as mason status --json's generatedTables says it: its state,
|
|
1320
|
-
behindSeconds, behindSince — when the oldest change not in it was committed, null: none)
|
|
1371
|
+
--timeout <duration> how long to wait (default 600, seconds; 10m, 1h); the sync goes on after, and mason wait
|
|
1372
|
+
again picks it up
|
|
1373
|
+
--json as data: its shape is at the end
|
|
1321
1374
|
|
|
1322
1375
|
Exit status: 0 ready (a table behind what its sync: promises is said on its line, and ready); 1 a first sync failed,
|
|
1323
1376
|
or the sync keeping a synced table current failed (its poll, its catch-up, the stream, or a named publication that
|
|
@@ -1325,6 +1378,19 @@ const WAIT_USAGE = `usage: mason wait [<generated table>] [--timeout <seconds>]
|
|
|
1325
1378
|
before it was built (printed, with what to do; Mason tries a failed one again by itself); 124 timed out.
|
|
1326
1379
|
A generated table that serves and holds everything synced is ready (0) though its last run failed: after editing its
|
|
1327
1380
|
file, read mason status for whether the new definition was refused or failed.
|
|
1381
|
+
|
|
1382
|
+
The --json answer, once it ends: { ready, failed, waiting, timedOut, tables: [{ table, copy, error,
|
|
1383
|
+
kept }] (copy: { phase, rowsCopied, progress: the chunks done, reading and for how long, the
|
|
1384
|
+
rate, the seconds left, while its chunks are read }; kept as mason status --json says it —
|
|
1385
|
+
{ by: poll, sync: { mode: off } or { mode: poll, every, seconds }, syncedAt (its last whole read
|
|
1386
|
+
or poll), nextAt (its next poll; null: off) }; kept's pending, when it applies, says why
|
|
1387
|
+
it is not kept current now, its phase included; sync as mason status --json says it: its state —
|
|
1388
|
+
synced, syncing, behind, failed, waiting — asOf, promisedSeconds, behindBySeconds, behindWhy),
|
|
1389
|
+
stream (replication mode: where the stream is, as mason status --json says it), error (on a
|
|
1390
|
+
failure or a timeout: { code, message, next }), next }
|
|
1391
|
+
Named, a generated table: the same ready, failed, waiting, timedOut, error and next, tables
|
|
1392
|
+
empty, and generatedTable (as mason status --json's generatedTables says it: its state,
|
|
1393
|
+
behindSeconds, behindSince — when the oldest change not in it was committed, null: none)
|
|
1328
1394
|
`;
|
|
1329
1395
|
/** how often a wait not drawn in place says its block again though nothing in it changed, so a long one never goes
|
|
1330
1396
|
* quiet: the copies' wait and a generated table's */
|
|
@@ -1337,7 +1403,7 @@ export function sayAgain(what, said, saidAt, now, every = SAY_AGAIN_MS) {
|
|
|
1337
1403
|
export const WAIT = {
|
|
1338
1404
|
needs: "run",
|
|
1339
1405
|
name: "wait",
|
|
1340
|
-
summary: "wait until every added table is synced, or a generated table holds what is synced now (1: a failure)",
|
|
1406
|
+
summary: "wait until every added table (or those named) is synced, or a generated table holds what is synced now (1: a failure)",
|
|
1341
1407
|
usage: WAIT_USAGE,
|
|
1342
1408
|
options: { json: { type: "boolean" }, timeout: { type: "string" } },
|
|
1343
1409
|
json: ["ready", "failed", "waiting", "timedOut", "tables", "stream", "generatedTable", "error", "next"],
|
|
@@ -1347,18 +1413,26 @@ export async function wait(argv) {
|
|
|
1347
1413
|
if (typeof f === "number")
|
|
1348
1414
|
return f;
|
|
1349
1415
|
const json = f.values.json === true;
|
|
1350
|
-
const
|
|
1351
|
-
if (
|
|
1352
|
-
return failed("wait", { message: `--timeout
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const named = f.positionals[0];
|
|
1416
|
+
const read = parseDuration(typeof f.values.timeout === "string" ? f.values.timeout : "600");
|
|
1417
|
+
if ("problem" in read)
|
|
1418
|
+
return failed("wait", { message: `--timeout ${read.problem}`, code: "usage", usage: WAIT_USAGE }, { json });
|
|
1419
|
+
const timeout = read.seconds;
|
|
1420
|
+
const named = [...new Set(f.positionals)];
|
|
1356
1421
|
const run = await runOr(WAIT, json);
|
|
1357
1422
|
if (!run)
|
|
1358
1423
|
return 1;
|
|
1359
|
-
if (named !== undefined)
|
|
1360
|
-
return waitGenerated(run, named, timeout, json);
|
|
1361
1424
|
return guarded("wait", json, async () => {
|
|
1425
|
+
// tables named: a generated table alone waits for its chain; synced tables for their first syncs alone
|
|
1426
|
+
let only = null;
|
|
1427
|
+
if (named.length) {
|
|
1428
|
+
const pick = waitPick(await readState(run), named);
|
|
1429
|
+
if ("message" in pick)
|
|
1430
|
+
return failed("wait", pick, { json });
|
|
1431
|
+
if ("generated" in pick)
|
|
1432
|
+
return waitGenerated(run, pick.generated, timeout, json);
|
|
1433
|
+
only = new Set(pick.tables);
|
|
1434
|
+
}
|
|
1435
|
+
const again = named.length ? `mason wait ${named.join(" ")}` : "mason wait";
|
|
1362
1436
|
const until = Date.now() + timeout * 1000;
|
|
1363
1437
|
// a terminal gets the block — every added table on its line (tableLines), then the stream — redrawn in place;
|
|
1364
1438
|
// anything else (a coding agent's shell, a log) the block when what is under way changes, and every 30 s, so a
|
|
@@ -1390,10 +1464,18 @@ export async function wait(argv) {
|
|
|
1390
1464
|
};
|
|
1391
1465
|
for (;;) {
|
|
1392
1466
|
// a copy's rate and progress are the run's own, measured per chunk (src/duck/sync.ts copyProgressNow)
|
|
1393
|
-
const
|
|
1467
|
+
const all = await readState(run);
|
|
1468
|
+
// named tables: the wait reads them alone, so its lines, its readiness and its answer are theirs
|
|
1469
|
+
const s = only === null ? all : { ...all, tables: all.tables.filter((t) => only.has(t.table)) };
|
|
1394
1470
|
const r = readiness(s);
|
|
1395
1471
|
const done = r.ready || r.failed.length > 0;
|
|
1396
1472
|
const picked = s.tables.filter((t) => t.added);
|
|
1473
|
+
// a table named and removed while it is waited for (mason remove, the page) has nothing left to wait for
|
|
1474
|
+
if (only !== null && picked.length < only.size) {
|
|
1475
|
+
const gone = [...only].filter((n) => !picked.some((t) => t.table === n));
|
|
1476
|
+
return failed("wait", { message: `${gone.join(", ")} ${gone.length === 1 ? "is" : "are"} not added any more`, code: "not_found",
|
|
1477
|
+
next: [["mason status", "the added tables"]] }, { json });
|
|
1478
|
+
}
|
|
1397
1479
|
// the outcome and what to run next, the same for a person and under --json
|
|
1398
1480
|
const readyNext = picked.length ? [...READY_STEPS] : addSteps(s.tables);
|
|
1399
1481
|
const failure = r.failed.length
|
|
@@ -1402,7 +1484,7 @@ export async function wait(argv) {
|
|
|
1402
1484
|
next: [["mason logs", "the detail of each failure"], ["mason status", "the rest of the project"]] }
|
|
1403
1485
|
: !done && Date.now() >= until
|
|
1404
1486
|
? { message: `not ready after ${timeout} s`, code: "timeout", why: r.waiting.join("\n"), fix: "nothing stopped: the syncs go on",
|
|
1405
|
-
next: [[
|
|
1487
|
+
next: [[again, "keep waiting (--timeout <duration> for longer)"], ["mason status", "what Mason is doing"]] }
|
|
1406
1488
|
: null;
|
|
1407
1489
|
if (json && (done || failure)) {
|
|
1408
1490
|
out(jsonText(WAIT, { ...r, timedOut: !done, tables: picked.map(waitTable),
|
|
@@ -1414,7 +1496,9 @@ export async function wait(argv) {
|
|
|
1414
1496
|
const meanwhile = done ? [] : [...(copied ? [["mason user add <name>", "a user for your BI tool or psql: a synced table can be queried now"]] : []), ["mason status", "what Mason is doing"]];
|
|
1415
1497
|
show([...tableLines(s), ...(s.stream ? ["Stream:", ...streamLines(s.stream).map((l) => ` ${l}`)] : [])], meanwhile); // done: the meanwhile Next is dropped
|
|
1416
1498
|
if (r.ready) {
|
|
1417
|
-
const head = !picked.length ? "Nothing to wait for: no table added yet."
|
|
1499
|
+
const head = !picked.length ? "Nothing to wait for: no table added yet."
|
|
1500
|
+
: only !== null ? `Ready: ${picked.map((t) => t.table).join(", ")} ${picked.length === 1 ? "is" : "are"} synced and served.`
|
|
1501
|
+
: "Ready: every added table is synced and served.";
|
|
1418
1502
|
if (!json)
|
|
1419
1503
|
out(withEnd(picked.length ? styleOut().green(head) : head, { next: readyNext }));
|
|
1420
1504
|
return 0;
|
|
@@ -1425,6 +1509,44 @@ export async function wait(argv) {
|
|
|
1425
1509
|
}
|
|
1426
1510
|
});
|
|
1427
1511
|
}
|
|
1512
|
+
/**
|
|
1513
|
+
* WHAT `mason wait <table>…` WAITS FOR: a generated table named alone (its chain: `waitGenerated`), or synced tables, each
|
|
1514
|
+
* found among the added ones as `mason sync` finds them (`findTable`) — a table of the database not added, or a name
|
|
1515
|
+
* nothing has, refused with the way out.
|
|
1516
|
+
*/
|
|
1517
|
+
export function waitPick(s, names) {
|
|
1518
|
+
const generated = s.generatedTables ?? [];
|
|
1519
|
+
const added = s.tables.filter((t) => t.added);
|
|
1520
|
+
const tables = [];
|
|
1521
|
+
for (const name of names) {
|
|
1522
|
+
const g = generated.find((t) => t.table === name || t.table === `public.${name}`);
|
|
1523
|
+
if (g !== undefined) {
|
|
1524
|
+
if (names.length > 1) {
|
|
1525
|
+
return { message: `${entryName(g.table)} is a generated table, which is waited for alone`, code: "usage",
|
|
1526
|
+
fix: `mason wait ${entryName(g.table)}, then mason wait ${names.filter((n) => n !== name).join(" ")}`, next: [["mason wait --help", "what a wait waits for"]] };
|
|
1527
|
+
}
|
|
1528
|
+
return { generated: name };
|
|
1529
|
+
}
|
|
1530
|
+
const t = findTable(added, name);
|
|
1531
|
+
if (!("message" in t)) {
|
|
1532
|
+
tables.push(t.table);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
// a table of the database that is not added is told so, with the command that adds it
|
|
1536
|
+
const db = findTable(s.tables, name);
|
|
1537
|
+
if ("message" in db && db.code === "usage")
|
|
1538
|
+
return db;
|
|
1539
|
+
if (!("message" in db)) {
|
|
1540
|
+
return { message: `${db.table} is not added, so Mason does not sync it`, code: "not_found", fix: `mason add ${tableArg(db.table)}`, next: [["mason status", "the added tables"]] };
|
|
1541
|
+
}
|
|
1542
|
+
const choices = [...added.flatMap((x) => [x.table, tableArg(x.table)]), ...generated.map((x) => entryName(x.table))];
|
|
1543
|
+
const named = [...added.map((x) => tableArg(x.table)), ...generated.map((x) => entryName(x.table))];
|
|
1544
|
+
return { message: `there is no added or generated table ${JSON.stringify(name)}${didYouMean(name, choices)}`, code: "not_found",
|
|
1545
|
+
...(named.length ? { fix: `name one of: ${named.join(", ")}` } : {}),
|
|
1546
|
+
next: [["mason status", "the added tables and the generated ones"], ["mason tables", "the database's tables"]] };
|
|
1547
|
+
}
|
|
1548
|
+
return { tables: [...new Set(tables)] };
|
|
1549
|
+
}
|
|
1428
1550
|
/** `mason wait <generated table>`: until it holds every change synced when the wait began (src/cli/readiness.ts
|
|
1429
1551
|
* generatedReadiness), a line when what it waits for changes; the same outcome and exit codes as the wait for copies */
|
|
1430
1552
|
async function waitGenerated(run, named, timeout, json) {
|
|
@@ -1446,7 +1568,7 @@ async function waitGenerated(run, named, timeout, json) {
|
|
|
1446
1568
|
? { message: r.failed[0], code: "failed", fix: "Mason tries a failed run again by itself; mason logs says why it failed", next: [["mason logs", "the detail of the failure"], ...next] }
|
|
1447
1569
|
: !r.ready && Date.now() >= until
|
|
1448
1570
|
? { message: `${g.table} does not hold what was synced ${timeout} s ago yet`, code: "timeout", why: r.waiting.join("\n"), fix: "nothing stopped: its runs go on",
|
|
1449
|
-
next: [[`mason wait ${entryName(g.table)}`, "keep waiting (--timeout <
|
|
1571
|
+
next: [[`mason wait ${entryName(g.table)}`, "keep waiting (--timeout <duration> for longer)"], ...next] }
|
|
1450
1572
|
: null;
|
|
1451
1573
|
if (json && (r.ready || failure)) {
|
|
1452
1574
|
out(jsonText(WAIT, { ...r, timedOut: !r.ready && failure?.code === "timeout", tables: [], generatedTable: g, ...(failure ? { error: errorJson(failure) } : {}) }, failure ? failure.next ?? [] : next));
|
|
@@ -1476,17 +1598,7 @@ export const EVENTS_LEAVE_OUT = "A scheduled poll that succeeds is not listed: t
|
|
|
1476
1598
|
const LOGS_USAGE = `usage: mason logs [-n <lines>] [-f] [--start] [--requests [<filters>]] [--events [--failed]] [--running] [--json]
|
|
1477
1599
|
|
|
1478
1600
|
The last lines of Mason's log: every sync, poll and statement it ran, warnings and errors with their reasons —
|
|
1479
|
-
running or not.
|
|
1480
|
-
Postgres API that Mason answered, and every one it refused, failed or cancelled, a line each, newest last — when, the
|
|
1481
|
-
outcome and SQLSTATE, how long, the rows sent, the client's application_name (and the mason user's name), the
|
|
1482
|
-
statement and the error's first line; the filters narrow it, and a page that has older rows ends with the line that
|
|
1483
|
-
reads them (--before). With --events, the sync's events: each first sync that failed or finished, each poll, catch-up or
|
|
1484
|
-
stream that failed, the database out of reach, a table current again, each sync asked by hand (mason sync, the
|
|
1485
|
-
page's Sync now) and what it did, and each restart of the run — a line each, newest last (a failure repeated word for
|
|
1486
|
-
word is said once an hour). A scheduled poll that succeeds is not listed: the last one's result is each table's last
|
|
1487
|
-
sync (mason status, the Tables page's Last sync). Both are kept MASON_HISTORY_DAYS days
|
|
1488
|
-
(default 7) in the run's synced data, and read through the running Mason. With --running, the statements running now
|
|
1489
|
-
instead, one per session, each with its number (mason cancel <session> --statement <n> stops one).
|
|
1601
|
+
running or not.
|
|
1490
1602
|
|
|
1491
1603
|
mason logs
|
|
1492
1604
|
mason logs -f keep printing new lines as Mason works, until Ctrl-C
|
|
@@ -1505,7 +1617,8 @@ const LOGS_USAGE = `usage: mason logs [-n <lines>] [-f] [--start] [--requests [<
|
|
|
1505
1617
|
--failed with --requests: only the ones not answered (refused, failed or cancelled); with --events: only
|
|
1506
1618
|
the failures
|
|
1507
1619
|
--outcome <o,…> with --requests: only these outcomes — ok, refused (the statement's own fault), failed (Mason's or
|
|
1508
|
-
the machine's), cancelled (a cancel or a statement timeout, SQLSTATE 57014)
|
|
1620
|
+
the machine's), cancelled (a cancel or a statement timeout, SQLSTATE 57014); a comma list or
|
|
1621
|
+
the flag repeated, the same
|
|
1509
1622
|
--since <time> with --requests: started at or after it — an age (30m, 2h, 7d) or an ISO 8601 date or time with
|
|
1510
1623
|
its zone (2026-10-28, 2026-10-28T09:00:00Z)
|
|
1511
1624
|
--until <time> with --requests: started before it (as --since)
|
|
@@ -1515,18 +1628,32 @@ const LOGS_USAGE = `usage: mason logs [-n <lines>] [-f] [--start] [--requests [<
|
|
|
1515
1628
|
--search <text> with --requests: only the statements whose text has it, in any case (at most ${MAX_SEARCH} characters)
|
|
1516
1629
|
--before <pos> with --requests: the page before: the position a page answered with (its last Next line)
|
|
1517
1630
|
--running the statements running now (Mason must be running)
|
|
1518
|
-
--json as data:
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1631
|
+
--json as data: its shape is at the end
|
|
1632
|
+
|
|
1633
|
+
With --requests, the run's request log instead: every statement a tool sent the
|
|
1634
|
+
Postgres API that Mason answered, and every one it refused, failed or cancelled, a line each, newest last — when, the
|
|
1635
|
+
outcome and SQLSTATE, how long, the rows sent, the client's application_name (and the mason user's name), the
|
|
1636
|
+
statement and the error's first line; the filters narrow it, and a page that has older rows ends with the line that
|
|
1637
|
+
reads them (--before). With --events, the sync's events: each first sync that failed or finished, each poll, catch-up or
|
|
1638
|
+
stream that failed, the database out of reach, a table current again, each sync asked by hand (mason sync, the
|
|
1639
|
+
page's Sync now) and what it did, and each restart of the run — a line each, newest last (a failure repeated word for
|
|
1640
|
+
word is said once an hour). A scheduled poll that succeeds is not listed: the last one's result is each table's last
|
|
1641
|
+
sync (mason status, the Tables page's Last sync). Both are kept MASON_HISTORY_DAYS days
|
|
1642
|
+
(default 7) in the run's synced data, and read through the running Mason. With --running, the statements running now
|
|
1643
|
+
instead, one per session, each with its number (mason cancel <session> --statement <n> stops one).
|
|
1644
|
+
|
|
1645
|
+
The --json answer: { lines: [{ at, text }] } (the log's entries, oldest first; at is null for a line with no
|
|
1646
|
+
time); with --requests { requests: [{ requestId, startedAt, endedAt, session, applicationName,
|
|
1647
|
+
clientAddress (the client's address; behind a TCP proxy, the one its PROXY header carries),
|
|
1648
|
+
tokenName, user (the mason user), role, attributes (the user's values its role's row filters
|
|
1649
|
+
read, or null), method, outcome (ok, refused, failed, cancelled), status
|
|
1650
|
+
(completed, failed, cancelled: what a reader shows), sqlstate, message, detail, hint, rows,
|
|
1651
|
+
bytesSent, durationMs, query, queryTruncated }], keptDays, before (the position of the page before,
|
|
1652
|
+
or null: none older) }; with --events { events: [{ at, kind (copy_failed, poll_failed,
|
|
1653
|
+
catch_up_failed, stream_failed, source_unreachable, restart, copied, recovered, synced (a sync
|
|
1654
|
+
asked by hand), sync_changed (a table's sync: changed)), failed, table (null: the whole sync),
|
|
1655
|
+
message }], keptDays }; with --running { running: [{ session, user,
|
|
1656
|
+
applicationName, method, startedAt, runningMs, rows (sent so far), query, queryTruncated }] }; next
|
|
1530
1657
|
`;
|
|
1531
1658
|
export const LOGS = {
|
|
1532
1659
|
needs: "project",
|
|
@@ -1534,7 +1661,7 @@ export const LOGS = {
|
|
|
1534
1661
|
summary: "Mason's log (-f follows it); --requests: the Postgres API's statements; --events: the sync's; --running: running now",
|
|
1535
1662
|
usage: LOGS_USAGE,
|
|
1536
1663
|
options: { lines: { type: "string", short: "n" }, follow: { type: "boolean", short: "f" }, start: { type: "boolean" },
|
|
1537
|
-
requests: { type: "boolean" }, events: { type: "boolean" }, failed: { type: "boolean" }, outcome: { type: "string" }, since: { type: "string" },
|
|
1664
|
+
requests: { type: "boolean" }, events: { type: "boolean" }, failed: { type: "boolean" }, outcome: { type: "string", multiple: true }, since: { type: "string" },
|
|
1538
1665
|
until: { type: "string" }, user: { type: "string" }, client: { type: "string" }, slow: { type: "boolean" }, search: { type: "string" }, before: { type: "string" }, running: { type: "boolean" }, json: { type: "boolean" } },
|
|
1539
1666
|
json: ["lines", "requests", "events", "keptDays", "before", "running", "next"],
|
|
1540
1667
|
};
|
|
@@ -1674,7 +1801,7 @@ async function runningLogs(json) {
|
|
|
1674
1801
|
/** A refusal the run answered, in the Failure shape it carries (`failure`, src/cli/run-server.ts sendFailure). */
|
|
1675
1802
|
export function runFailure(reply, next) {
|
|
1676
1803
|
const f = reply.body.failure;
|
|
1677
|
-
const code =
|
|
1804
|
+
const code = failureCode(f?.code, reply.status === 400 ? "usage" : reply.status === 404 ? "not_found" : reply.status === 409 ? "refused" : "failed");
|
|
1678
1805
|
return { message: typeof f?.message === "string" ? f.message : String(reply.body.error ?? `the run answered ${reply.status}`), code,
|
|
1679
1806
|
...(typeof f?.why === "string" ? { why: f.why } : {}), ...(typeof f?.fix === "string" ? { fix: f.fix } : {}), next };
|
|
1680
1807
|
}
|
|
@@ -1682,6 +1809,9 @@ export async function logs(argv) {
|
|
|
1682
1809
|
const f = parseCommand(LOGS, argv);
|
|
1683
1810
|
if (typeof f === "number")
|
|
1684
1811
|
return f;
|
|
1812
|
+
const extra = noArguments(LOGS, f.positionals, f.values.json === true);
|
|
1813
|
+
if (extra !== null)
|
|
1814
|
+
return extra;
|
|
1685
1815
|
const json = f.values.json === true;
|
|
1686
1816
|
if (f.values.requests && f.values.events)
|
|
1687
1817
|
return failed("logs", { message: "--requests and --events are two lists: ask for one", code: "usage", fix: "mason logs --requests, or mason logs --events" }, { json });
|
|
@@ -1711,7 +1841,7 @@ export async function logs(argv) {
|
|
|
1711
1841
|
if (f.values.requests) {
|
|
1712
1842
|
// the filters, read as the run reads its query string (src/cli/request-filter.ts): a mistake is said here, exit 2
|
|
1713
1843
|
const text = (v) => typeof v === "string" ? v : undefined;
|
|
1714
|
-
const q = requestFilter({ limit: text(f.values.lines), failed: f.values.failed === true, outcome:
|
|
1844
|
+
const q = requestFilter({ limit: text(f.values.lines), failed: f.values.failed === true, outcome: listFlag(f.values.outcome), since: text(f.values.since),
|
|
1715
1845
|
until: text(f.values.until), user: text(f.values.user), client: text(f.values.client),
|
|
1716
1846
|
slow: f.values.slow === true, search: text(f.values.search), before: text(f.values.before) }, Date.now(), flagOf);
|
|
1717
1847
|
if ("message" in q)
|
|
@@ -1775,9 +1905,9 @@ const STOP_USAGE = `usage: mason stop [--json]
|
|
|
1775
1905
|
|
|
1776
1906
|
mason stop
|
|
1777
1907
|
|
|
1778
|
-
mason destroy removes the synced data and the state too (your files stay), to free the disk or begin again.
|
|
1779
|
-
|
|
1780
1908
|
--json as data: { stopped: [name], killed (a run that did not answer and ignored SIGTERM), supervisor (its pid, when one was stopped), next }
|
|
1909
|
+
|
|
1910
|
+
mason destroy removes the synced data and the state too (your files stay), to free the disk or begin again.
|
|
1781
1911
|
`;
|
|
1782
1912
|
export const STOP = {
|
|
1783
1913
|
needs: "project",
|
|
@@ -1791,6 +1921,9 @@ export async function stop(argv) {
|
|
|
1791
1921
|
const f = parseCommand(STOP, argv);
|
|
1792
1922
|
if (typeof f === "number")
|
|
1793
1923
|
return f;
|
|
1924
|
+
const extra = noArguments(STOP, f.positionals, f.values.json === true);
|
|
1925
|
+
if (extra !== null)
|
|
1926
|
+
return extra;
|
|
1794
1927
|
const json = f.values.json === true;
|
|
1795
1928
|
const next = [["mason start", "resume, in seconds (in the project's folder)"], ["mason destroy", "remove its synced data and its state; your files stay"]];
|
|
1796
1929
|
const r = await theRun(process.cwd());
|