@mason-data/cli 0.9.0 → 0.11.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 +295 -159
- package/dist/src/cli/deploy.js +134 -28
- package/dist/src/cli/dev.js +21 -5
- 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 +25 -2
- 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 +70 -31
- package/dist/src/cli/secret-input.js +193 -0
- package/dist/src/cli/security.js +7 -2
- package/dist/src/cli/serve.js +119 -9
- 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-read-only.js +23 -0
- package/dist/src/cli/ui-run-file.js +247 -0
- package/dist/src/cli/ui-socket.js +183 -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/dist/src/web/app.css +5 -0
- package/dist/src/web/app.js +4 -4
- package/dist/src/web/index.html +5 -8
- package/dist/src/web/pages/queries.js +3 -3
- package/dist/src/web/pages/status.js +23 -17
- package/dist/src/web/pages/syncs.js +3 -3
- package/dist/src/web/pages/tables.js +18 -15
- package/dist/src/web/pages/usage.js +16 -12
- package/dist/src/web/pages/users.js +45 -14
- package/dist/src/web/shared.js +10 -4
- 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,75 @@ 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), pageReadOnly (why the page changes nothing: mason serve with
|
|
331
|
+
MASON_UI_READ_ONLY=1, which refuses every change the page sends — or null), pageSocket (the Unix socket the
|
|
332
|
+
page is served on too: mason serve with MASON_UI_SOCKET — or null), next }.
|
|
333
|
+
Each table's kept is how it is kept current now (absent for a table the sync does not hold):
|
|
334
|
+
{ by: poll, sync: { mode: off } or { mode: poll, every (30s), seconds } (its entry's sync:, else the file's,
|
|
335
|
+
else off), syncedAt (its last whole read or poll), nextAt (when its next poll is due; null: off, or not polled
|
|
336
|
+
now), pending (only when it applies: why it is not kept current now, its phase included: it waits for
|
|
337
|
+
its first sync, it is being read whole, its first sync failed, it is read again whole, caught up first) }
|
|
338
|
+
or { by: recopy, copiedAt, syncedAt, pending (only when it applies, as the poll's) }
|
|
339
|
+
or { by: replication, slot, state (streamed, catching-up, waiting, left-out), why, position, lastCommit,
|
|
340
|
+
notRunning (why the stream is not running, null when it is) }; each with promisedSeconds (the age of
|
|
341
|
+
its synced data past which it is behind: poll, its interval plus the larger of 60 s and twice its last poll;
|
|
342
|
+
replication, 120 s; off, null) and lateWhy (what would make it late now: the pass's or its own poll's
|
|
343
|
+
time, which Mason waits four times, the stream not running); with Mason stopped: { running: false, project, source: { name,
|
|
344
|
+
file, url, error (the sources file's problem, or null) }, engine, tables: [{ table }], lastRun, lastStop:
|
|
345
|
+
{ at, how } or null, next }; hung: the same with hung: { pid, problem, why },
|
|
346
|
+
exit 1. In replication mode stream: { slot, slotWal: { walHeldBytes (the WAL the slot holds on the
|
|
347
|
+
database), walStatus (reserved, extended, unreserved, lost), safeWalSizeBytes (the WAL left before Postgres
|
|
348
|
+
invalidates it; null: max_slot_wal_keep_size unset, nothing caps it), at } or { error }, publication, running, failure, position,
|
|
349
|
+
applied, database: { lsn, at } or { error }, lag: { bytes, seconds }, lastBatch: { appliedAt, committedAt },
|
|
350
|
+
tables: [{ table, state (streamed, catching-up, waiting, left-out), why, catchUp: { phase (queued, polling,
|
|
351
|
+
applying), from, startedAt, held, retryAt }, applied: { inserts, updates, deletes, truncates } }] }.
|
|
352
|
+
jsonPaths: [{ table, column, steps, either, canonical, asText (false: a path's -> and ->> share it), name,
|
|
353
|
+
state (waiting, backfilling, ready, dropped), rowsTotal, rowsDone, marked, statements, slowestMs, totalMs,
|
|
354
|
+
fill: { rows, bytesPerRow, seconds, basis (measured: from the last fill of its column; disk or text: modelled from
|
|
355
|
+
the column's bytes a row on disk, or its text's before it is on disk), rowsPerSecond } (the fill's estimate it
|
|
356
|
+
was decided on), filled: { rows, seconds } (what its own fill measured), queued (a backfill waiting for the fill
|
|
357
|
+
under way), keptAt, readyAt, why }]. generatedTables: [{ table, file, kind, method (whole: its SQL run again),
|
|
358
|
+
state (refused, waiting, building, failed, current, behind), targetLag, behindSeconds (across its chain: the
|
|
359
|
+
oldest base change below it not in it), behindSince (when that change was committed; null: none), missingTarget, serving,
|
|
360
|
+
lastRun: { at, seconds, rows }, error (the last run's, no value of a row in it), retryAt,
|
|
361
|
+
refusals: [{ key, message, why, options, text }], waitingFor, warnings }]. roles (a run with a roles file, mason/roles.yml: what a user added
|
|
362
|
+
with --role may read): { file, roles: [name], problem (what is wrong with the file now, every statement of a
|
|
363
|
+
user with a role refused while there is; null), default (the role of a user added without --role; null: the
|
|
364
|
+
file names none, and such a user reads nothing), all (the roles of tables: all) }
|
|
358
365
|
`;
|
|
359
366
|
export const STATUS = {
|
|
360
367
|
needs: "project",
|
|
@@ -363,13 +370,16 @@ export const STATUS = {
|
|
|
363
370
|
usage: STATUS_USAGE,
|
|
364
371
|
options: { json: { type: "boolean" } },
|
|
365
372
|
json: ["running", "pid", "postgres", "source", "warnings", "folder", "project", "tables", "totals", "engine", "disk", "sessions", "stream", "jsonPaths", "generatedTables", "queries", "slowQueries", "refusedQueries",
|
|
366
|
-
"refusedTotal", "unsupported", "recentErrors", "readiness", "ui", "served", "sourcesReadOnly",
|
|
373
|
+
"refusedTotal", "unsupported", "recentErrors", "readiness", "ui", "served", "sourcesReadOnly", "pageReadOnly", "pageSocket",
|
|
367
374
|
"roles", "share", "shareEnded", "lastRun", "lastStop", "hung", "next"],
|
|
368
375
|
};
|
|
369
376
|
export async function status(argv) {
|
|
370
377
|
const f = parseCommand(STATUS, argv);
|
|
371
378
|
if (typeof f === "number")
|
|
372
379
|
return f;
|
|
380
|
+
const extra = noArguments(STATUS, f.positionals, f.values.json === true);
|
|
381
|
+
if (extra !== null)
|
|
382
|
+
return extra;
|
|
373
383
|
const r0 = await theRun(process.cwd());
|
|
374
384
|
// the first line goes straight to the point (Status: running); when the service is not running, the same rows and
|
|
375
385
|
// tables show, with "not running" for each service that is not
|
|
@@ -440,7 +450,8 @@ export async function status(argv) {
|
|
|
440
450
|
["Listener", listenerText(run.postgresListener, postgresLink(run.postgresUrl).port)],
|
|
441
451
|
...shareRows(s.share),
|
|
442
452
|
// the web page (mason ui), without its one-time token: mason ui prints the URL that carries it, and opens it
|
|
443
|
-
...(s.ui === true ? [["Web UI", s.served === true ? "from your machine: mason ui --ssh <user@server>" : `${run.control.url}/ (mason ui opens it)`
|
|
453
|
+
...(s.ui === true ? [["Web UI", (s.served === true ? "from your machine: mason ui --ssh <user@server>" : `${run.control.url}/ (mason ui opens it)`) +
|
|
454
|
+
(typeof s.pageSocket === "string" ? `; on this machine, the Unix socket ${s.pageSocket} too` : "")]] : []),
|
|
444
455
|
// the roles file as the listener reads it now (mason user add --role): its roles, or what is wrong with it
|
|
445
456
|
...(s.roles && s.roles.file !== null
|
|
446
457
|
? [["Roles", s.roles.problem !== null ? st.red(`${s.roles.problem} (every statement of a user with a role is refused until it is fixed)`)
|
|
@@ -788,10 +799,12 @@ const TABLES_USAGE = `usage: mason tables [<table>…] [--all] [--json]
|
|
|
788
799
|
mason tables --all every table with its columns, at once (long)
|
|
789
800
|
|
|
790
801
|
--all every table with its columns, not only the named ones
|
|
791
|
-
--json as data:
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
802
|
+
--json as data: its shape is at the end
|
|
803
|
+
|
|
804
|
+
The --json answer: { database, source: { name (the source's: its sources file's), file (the sources file) },
|
|
805
|
+
tables: [{ table, rows, bytes, primaryKey, comment, addable,
|
|
806
|
+
reason, added, addedColumns (the columns it syncs: null when not added), copyKey (the key: the sources
|
|
807
|
+
file keys it by, null: its primary key), columns: [{ name, type, nullable, comment, references }] }], next }
|
|
795
808
|
`;
|
|
796
809
|
export const TABLES = {
|
|
797
810
|
// 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 +991,22 @@ async function viaRun(run, route, body) {
|
|
|
978
991
|
const ADD_USAGE = `usage: mason add <table>… [--columns <column>,…] [--include <column>,…] [--key <column>,…] [--sync <sync>] [--json]
|
|
979
992
|
|
|
980
993
|
Syncs tables into Mason: each one is added to the project's sources file (mason/sources/<name>.yml), under tables:
|
|
994
|
+
|
|
981
995
|
mason add orders every column: orders: {}
|
|
982
996
|
mason add orders --columns id,status,total those columns: orders: { columns: [id, status, total] }
|
|
983
997
|
mason add orders customers several tables, every column of each
|
|
984
998
|
mason add users --include password_hash every column, and one that looks like a secret
|
|
985
999
|
mason add event_log --key tenant_id,id a table with no primary key, keyed by those columns
|
|
986
1000
|
mason add orders --sync "poll 30s" polled every 30 seconds: orders: { sync: poll 30s }
|
|
1001
|
+
|
|
1002
|
+
--columns <a,b,…> only these columns (one table at a time: the list is that table's); a list flag takes a comma
|
|
1003
|
+
list or the flag repeated, the same: --columns id,status is --columns id --columns status
|
|
1004
|
+
--include <a,b,…> these columns as well as every other one — a column that looks like a secret, named on purpose
|
|
1005
|
+
--key <a,b,…> for a table with no primary key: the columns that tell its rows apart, which it is keyed by (one
|
|
1006
|
+
table at a time; polled or off: logical replication cannot apply a change by them)
|
|
1007
|
+
--sync <sync> how it is kept current after its first sync, written on its entry: off, "poll 30s" or replication
|
|
1008
|
+
--json as data: its shape is at the end
|
|
1009
|
+
|
|
987
1010
|
A table is its name (orders) or schema.table (billing.invoices). Its primary key is always synced. The rest of the
|
|
988
1011
|
file — your comments, other tables — stays as it was.
|
|
989
1012
|
|
|
@@ -1007,19 +1030,14 @@ const ADD_USAGE = `usage: mason add <table>… [--columns <column>,…] [--inclu
|
|
|
1007
1030
|
and the table is synced again whole with them. A project deployed as committed (MASON_PROJECT_READONLY=1 where Mason
|
|
1008
1031
|
runs) is refused, its files left as they are: add the table where the project is committed, then deploy.
|
|
1009
1032
|
|
|
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
|
|
1033
|
+
The --json answer: { added: [{ table, entry, columns (null: every one), exclude, key, addedKey (key columns
|
|
1034
|
+
added to --columns), skippedSecrets, already (the file said so already), sync: { value (off, poll
|
|
1035
|
+
30s), from (entry: its own sync:, file: the file's, default: off, as the file writes none) },
|
|
1036
|
+
bigOff (it is over ${BIG_TABLE_ROWS.toLocaleString("en-US")} rows and its entry was written sync: off), notReplicated (why
|
|
1037
|
+
its entry was written sync: poll 5m under a file whose sync: is replication, or null), pollCost (an estimate of
|
|
1038
|
+
what a frequent poll of a big table costs your database, or null) } or { table, error }],
|
|
1039
|
+
file (the sources file), error (when a table was not added: { code, message, why }), next };
|
|
1040
|
+
exit 1 when a table could not be added
|
|
1023
1041
|
`;
|
|
1024
1042
|
export const ADD = {
|
|
1025
1043
|
// 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 +1045,7 @@ export const ADD = {
|
|
|
1027
1045
|
name: "add",
|
|
1028
1046
|
summary: "sync tables into Mason, adding them to the sources file: mason add orders (--columns id,status: those only)",
|
|
1029
1047
|
usage: ADD_USAGE,
|
|
1030
|
-
options: { json: { type: "boolean" }, columns: { type: "string" }, include: { type: "string" }, key: { type: "string" }, sync: { type: "string" } },
|
|
1048
|
+
options: { json: { type: "boolean" }, columns: { type: "string", multiple: true }, include: { type: "string", multiple: true }, key: { type: "string", multiple: true }, sync: { type: "string" } },
|
|
1031
1049
|
json: ["added", "file", "error", "next"],
|
|
1032
1050
|
};
|
|
1033
1051
|
/** `--sync`, read by the sources file's own grammar (src/lib/table-sync.ts parseSync): a sync, or the usage's problem */
|
|
@@ -1070,7 +1088,7 @@ export async function add(argv) {
|
|
|
1070
1088
|
if (typeof f === "number")
|
|
1071
1089
|
return f;
|
|
1072
1090
|
const json = f.values.json === true;
|
|
1073
|
-
const specs = addSpecs(f.positionals, f.values.columns, f.values.include, f.values.key);
|
|
1091
|
+
const specs = addSpecs(f.positionals, listFlag(f.values.columns), listFlag(f.values.include), listFlag(f.values.key));
|
|
1074
1092
|
if (typeof specs === "string")
|
|
1075
1093
|
return failed("add", { message: specs, code: "usage", usage: ADD_USAGE }, { json });
|
|
1076
1094
|
const sync = addSync(f.values.sync);
|
|
@@ -1213,7 +1231,7 @@ export function addedLines(b, file) {
|
|
|
1213
1231
|
] };
|
|
1214
1232
|
}
|
|
1215
1233
|
// ------------------------------------------------------------------ mason remove
|
|
1216
|
-
const REMOVE_USAGE = `usage: mason remove <table>… [--json]
|
|
1234
|
+
const REMOVE_USAGE = `usage: mason remove <table>… [--dry-run] [--json]
|
|
1217
1235
|
|
|
1218
1236
|
Takes tables out of the project: each one's entry in the sources file (mason/sources/<name>.yml) is removed, the
|
|
1219
1237
|
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 +1241,12 @@ const REMOVE_USAGE = `usage: mason remove <table>… [--json]
|
|
|
1223
1241
|
|
|
1224
1242
|
mason remove customers
|
|
1225
1243
|
mason remove customers billing.invoices several at once
|
|
1244
|
+
mason remove customers --dry-run what it would remove, changing nothing
|
|
1226
1245
|
|
|
1227
|
-
--
|
|
1246
|
+
--dry-run say which entries would go, and change nothing: the file is read, never written, and neither Mason nor
|
|
1247
|
+
the database is asked
|
|
1248
|
+
--json as data: { dryRun, removedTables (the tables, schema.table; on a dry run, the ones that would go), removed
|
|
1249
|
+
(the same, kept until 1.0; not on a dry run), file (the sources file), next }
|
|
1228
1250
|
`;
|
|
1229
1251
|
export const REMOVE = {
|
|
1230
1252
|
// the project, not the run, as mason add
|
|
@@ -1232,8 +1254,8 @@ export const REMOVE = {
|
|
|
1232
1254
|
name: "remove",
|
|
1233
1255
|
summary: "take tables out of the sources file: mason remove customers (its synced data is dropped)",
|
|
1234
1256
|
usage: REMOVE_USAGE,
|
|
1235
|
-
options: { json: { type: "boolean" } },
|
|
1236
|
-
json: ["removed", "file", "next"],
|
|
1257
|
+
options: { json: { type: "boolean" }, "dry-run": { type: "boolean" } },
|
|
1258
|
+
json: ["dryRun", "removedTables", "removed", "file", "next"],
|
|
1237
1259
|
};
|
|
1238
1260
|
export async function remove(argv) {
|
|
1239
1261
|
const f = parseCommand(REMOVE, argv);
|
|
@@ -1251,6 +1273,8 @@ export async function remove(argv) {
|
|
|
1251
1273
|
const readOnly = projectWriteRefusal(where.project.root, process.env);
|
|
1252
1274
|
if (readOnly !== null)
|
|
1253
1275
|
return failed("remove", readOnly, { json });
|
|
1276
|
+
if (f.values["dry-run"] === true)
|
|
1277
|
+
return removeDryRun(where.project, where.state === "up", names, json);
|
|
1254
1278
|
if (where.state === "up") {
|
|
1255
1279
|
// Mason up: the run removes it and drops the copy at once (src/cli/run-server.ts removeTables)
|
|
1256
1280
|
const run = where.run;
|
|
@@ -1272,13 +1296,53 @@ function removedAnswer(r, names, running, json) {
|
|
|
1272
1296
|
const next = running ? [["mason status", "the tables left"]]
|
|
1273
1297
|
: [["mason start", "Mason in the background, for this project: it syncs the tables left"], ["mason status", "the tables left"]];
|
|
1274
1298
|
if (json) {
|
|
1275
|
-
out(jsonText(REMOVE, { removed: b.removed, file: b.file }, next));
|
|
1299
|
+
out(jsonText(REMOVE, { dryRun: false, removed: b.removed, removedTables: b.removed, file: b.file }, next));
|
|
1276
1300
|
return 0;
|
|
1277
1301
|
}
|
|
1278
1302
|
const rel = path.relative(process.cwd(), b.file);
|
|
1279
1303
|
out(reportText({ ...removedReport(b.removed, rel.startsWith("..") ? b.file : rel, { running }), next }, styleOut()));
|
|
1280
1304
|
return 0;
|
|
1281
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* `mason remove --dry-run`: the entries the sources file would lose, CHANGING NOTHING — read as the edit reads the file
|
|
1308
|
+
* (`sourceToEdit`: its refusals said the same), each name found as the edit finds it (`findEntry`); neither the run nor
|
|
1309
|
+
* the database is asked, so the file's other entries are not checked against the database here, as the edit does.
|
|
1310
|
+
*/
|
|
1311
|
+
async function removeDryRun(p, up, names, json) {
|
|
1312
|
+
if (!up) {
|
|
1313
|
+
const st = await runState(p);
|
|
1314
|
+
// a hung run refuses the edit itself (withoutRun): a dry run says what the edit would
|
|
1315
|
+
if (st.kind === "hung")
|
|
1316
|
+
return failed("remove", { ...asFailure(notAnswering(p, st.run)), code: "unreachable" }, { json });
|
|
1317
|
+
}
|
|
1318
|
+
const r = edited(() => {
|
|
1319
|
+
const at = sourceToEdit(p.folder, p.root, () => projectSource(p));
|
|
1320
|
+
const tables = names.map((n) => {
|
|
1321
|
+
const e = findEntry(at.parsed, n);
|
|
1322
|
+
if ("problem" in e)
|
|
1323
|
+
throw new Refusal(e.usage ? 400 : 404, e.problem);
|
|
1324
|
+
return e.table;
|
|
1325
|
+
});
|
|
1326
|
+
return { removed: [...new Set(tables)], file: at.file };
|
|
1327
|
+
});
|
|
1328
|
+
if (!("ok" in r)) {
|
|
1329
|
+
return failed("remove", { message: `${names.join(", ")} would not be removed`, code: r.status === 404 ? "not_found" : r.status === 400 ? "usage" : "refused", why: r.error,
|
|
1330
|
+
fix: "nothing was changed (a dry run)", next: [["mason status", "the tables added"]] }, { json });
|
|
1331
|
+
}
|
|
1332
|
+
const b = r.ok;
|
|
1333
|
+
const next = [[`mason remove ${names.join(" ")}`, "remove them"]];
|
|
1334
|
+
if (json) {
|
|
1335
|
+
out(jsonText(REMOVE, { dryRun: true, removedTables: b.removed, file: b.file }, next));
|
|
1336
|
+
return 0;
|
|
1337
|
+
}
|
|
1338
|
+
const rel = path.relative(process.cwd(), b.file);
|
|
1339
|
+
const one = b.removed.length === 1;
|
|
1340
|
+
out(reportText({ headline: `mason remove would remove ${b.removed.join(", ")}; nothing was changed`, details: [["from", rel.startsWith("..") ? b.file : rel]],
|
|
1341
|
+
tips: [up ? `Mason would stop syncing ${one ? "it" : "them"} and drop ${one ? "its" : "their"} synced data at once`
|
|
1342
|
+
: `what an earlier run synced of ${one ? "it" : "them"} would be dropped at the next mason start`,
|
|
1343
|
+
"mason remove also checks the file's other entries against the database, which a dry run does not ask"], next }, styleOut()));
|
|
1344
|
+
return 0;
|
|
1345
|
+
}
|
|
1282
1346
|
/** What `mason remove` prints for the tables removed, `file` the sources file as the reader is shown it (the web page
|
|
1283
1347
|
* says the same, src/web/app.js; tests/web-page.test.ts holds the two equal). */
|
|
1284
1348
|
export function removedReport(removed, file, o = {}) {
|
|
@@ -1294,30 +1358,22 @@ export function removedReport(removed, file, o = {}) {
|
|
|
1294
1358
|
export function waitTable(t) {
|
|
1295
1359
|
return { table: t.table, copy: t.copy, error: t.error, ...(t.kept ? { kept: t.kept } : {}), ...(t.sync ? { sync: t.sync } : {}) };
|
|
1296
1360
|
}
|
|
1297
|
-
const WAIT_USAGE = `usage: mason wait [<
|
|
1361
|
+
const WAIT_USAGE = `usage: mason wait [<table>…] [--timeout <duration>] [--json]
|
|
1298
1362
|
|
|
1299
1363
|
Waits until every added table is synced and served, showing every added table on a line of its own: 100% for the
|
|
1300
1364
|
finished (in the order they finished), a bar for the ones still syncing, at the bottom. Run it after mason add.
|
|
1301
|
-
Named
|
|
1365
|
+
Named synced tables: waits for those alone, until the first sync of each is done and it is served. Named, a
|
|
1366
|
+
generated table (alone) instead: waits until it holds every change Mason had synced when the wait began, across its
|
|
1302
1367
|
chain (the tables it is generated from, and theirs), so what is read from it next includes them.
|
|
1303
1368
|
|
|
1304
1369
|
mason add orders && mason wait
|
|
1305
|
-
mason wait --timeout
|
|
1370
|
+
mason wait --timeout 1h a large table: wait up to an hour
|
|
1371
|
+
mason wait orders customers these synced tables only
|
|
1306
1372
|
mason wait daily_requests a generated table: until it holds what is synced now
|
|
1307
1373
|
|
|
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)
|
|
1374
|
+
--timeout <duration> how long to wait (default 600, seconds; 10m, 1h); the sync goes on after, and mason wait
|
|
1375
|
+
again picks it up
|
|
1376
|
+
--json as data: its shape is at the end
|
|
1321
1377
|
|
|
1322
1378
|
Exit status: 0 ready (a table behind what its sync: promises is said on its line, and ready); 1 a first sync failed,
|
|
1323
1379
|
or the sync keeping a synced table current failed (its poll, its catch-up, the stream, or a named publication that
|
|
@@ -1325,6 +1381,19 @@ const WAIT_USAGE = `usage: mason wait [<generated table>] [--timeout <seconds>]
|
|
|
1325
1381
|
before it was built (printed, with what to do; Mason tries a failed one again by itself); 124 timed out.
|
|
1326
1382
|
A generated table that serves and holds everything synced is ready (0) though its last run failed: after editing its
|
|
1327
1383
|
file, read mason status for whether the new definition was refused or failed.
|
|
1384
|
+
|
|
1385
|
+
The --json answer, once it ends: { ready, failed, waiting, timedOut, tables: [{ table, copy, error,
|
|
1386
|
+
kept }] (copy: { phase, rowsCopied, progress: the chunks done, reading and for how long, the
|
|
1387
|
+
rate, the seconds left, while its chunks are read }; kept as mason status --json says it —
|
|
1388
|
+
{ by: poll, sync: { mode: off } or { mode: poll, every, seconds }, syncedAt (its last whole read
|
|
1389
|
+
or poll), nextAt (its next poll; null: off) }; kept's pending, when it applies, says why
|
|
1390
|
+
it is not kept current now, its phase included; sync as mason status --json says it: its state —
|
|
1391
|
+
synced, syncing, behind, failed, waiting — asOf, promisedSeconds, behindBySeconds, behindWhy),
|
|
1392
|
+
stream (replication mode: where the stream is, as mason status --json says it), error (on a
|
|
1393
|
+
failure or a timeout: { code, message, next }), next }
|
|
1394
|
+
Named, a generated table: the same ready, failed, waiting, timedOut, error and next, tables
|
|
1395
|
+
empty, and generatedTable (as mason status --json's generatedTables says it: its state,
|
|
1396
|
+
behindSeconds, behindSince — when the oldest change not in it was committed, null: none)
|
|
1328
1397
|
`;
|
|
1329
1398
|
/** how often a wait not drawn in place says its block again though nothing in it changed, so a long one never goes
|
|
1330
1399
|
* quiet: the copies' wait and a generated table's */
|
|
@@ -1337,7 +1406,7 @@ export function sayAgain(what, said, saidAt, now, every = SAY_AGAIN_MS) {
|
|
|
1337
1406
|
export const WAIT = {
|
|
1338
1407
|
needs: "run",
|
|
1339
1408
|
name: "wait",
|
|
1340
|
-
summary: "wait until every added table is synced, or a generated table holds what is synced now (1: a failure)",
|
|
1409
|
+
summary: "wait until every added table (or those named) is synced, or a generated table holds what is synced now (1: a failure)",
|
|
1341
1410
|
usage: WAIT_USAGE,
|
|
1342
1411
|
options: { json: { type: "boolean" }, timeout: { type: "string" } },
|
|
1343
1412
|
json: ["ready", "failed", "waiting", "timedOut", "tables", "stream", "generatedTable", "error", "next"],
|
|
@@ -1347,18 +1416,26 @@ export async function wait(argv) {
|
|
|
1347
1416
|
if (typeof f === "number")
|
|
1348
1417
|
return f;
|
|
1349
1418
|
const json = f.values.json === true;
|
|
1350
|
-
const
|
|
1351
|
-
if (
|
|
1352
|
-
return failed("wait", { message: `--timeout
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
const named = f.positionals[0];
|
|
1419
|
+
const read = parseDuration(typeof f.values.timeout === "string" ? f.values.timeout : "600");
|
|
1420
|
+
if ("problem" in read)
|
|
1421
|
+
return failed("wait", { message: `--timeout ${read.problem}`, code: "usage", usage: WAIT_USAGE }, { json });
|
|
1422
|
+
const timeout = read.seconds;
|
|
1423
|
+
const named = [...new Set(f.positionals)];
|
|
1356
1424
|
const run = await runOr(WAIT, json);
|
|
1357
1425
|
if (!run)
|
|
1358
1426
|
return 1;
|
|
1359
|
-
if (named !== undefined)
|
|
1360
|
-
return waitGenerated(run, named, timeout, json);
|
|
1361
1427
|
return guarded("wait", json, async () => {
|
|
1428
|
+
// tables named: a generated table alone waits for its chain; synced tables for their first syncs alone
|
|
1429
|
+
let only = null;
|
|
1430
|
+
if (named.length) {
|
|
1431
|
+
const pick = waitPick(await readState(run), named);
|
|
1432
|
+
if ("message" in pick)
|
|
1433
|
+
return failed("wait", pick, { json });
|
|
1434
|
+
if ("generated" in pick)
|
|
1435
|
+
return waitGenerated(run, pick.generated, timeout, json);
|
|
1436
|
+
only = new Set(pick.tables);
|
|
1437
|
+
}
|
|
1438
|
+
const again = named.length ? `mason wait ${named.join(" ")}` : "mason wait";
|
|
1362
1439
|
const until = Date.now() + timeout * 1000;
|
|
1363
1440
|
// a terminal gets the block — every added table on its line (tableLines), then the stream — redrawn in place;
|
|
1364
1441
|
// 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 +1467,18 @@ export async function wait(argv) {
|
|
|
1390
1467
|
};
|
|
1391
1468
|
for (;;) {
|
|
1392
1469
|
// a copy's rate and progress are the run's own, measured per chunk (src/duck/sync.ts copyProgressNow)
|
|
1393
|
-
const
|
|
1470
|
+
const all = await readState(run);
|
|
1471
|
+
// named tables: the wait reads them alone, so its lines, its readiness and its answer are theirs
|
|
1472
|
+
const s = only === null ? all : { ...all, tables: all.tables.filter((t) => only.has(t.table)) };
|
|
1394
1473
|
const r = readiness(s);
|
|
1395
1474
|
const done = r.ready || r.failed.length > 0;
|
|
1396
1475
|
const picked = s.tables.filter((t) => t.added);
|
|
1476
|
+
// a table named and removed while it is waited for (mason remove, the page) has nothing left to wait for
|
|
1477
|
+
if (only !== null && picked.length < only.size) {
|
|
1478
|
+
const gone = [...only].filter((n) => !picked.some((t) => t.table === n));
|
|
1479
|
+
return failed("wait", { message: `${gone.join(", ")} ${gone.length === 1 ? "is" : "are"} not added any more`, code: "not_found",
|
|
1480
|
+
next: [["mason status", "the added tables"]] }, { json });
|
|
1481
|
+
}
|
|
1397
1482
|
// the outcome and what to run next, the same for a person and under --json
|
|
1398
1483
|
const readyNext = picked.length ? [...READY_STEPS] : addSteps(s.tables);
|
|
1399
1484
|
const failure = r.failed.length
|
|
@@ -1402,7 +1487,7 @@ export async function wait(argv) {
|
|
|
1402
1487
|
next: [["mason logs", "the detail of each failure"], ["mason status", "the rest of the project"]] }
|
|
1403
1488
|
: !done && Date.now() >= until
|
|
1404
1489
|
? { message: `not ready after ${timeout} s`, code: "timeout", why: r.waiting.join("\n"), fix: "nothing stopped: the syncs go on",
|
|
1405
|
-
next: [[
|
|
1490
|
+
next: [[again, "keep waiting (--timeout <duration> for longer)"], ["mason status", "what Mason is doing"]] }
|
|
1406
1491
|
: null;
|
|
1407
1492
|
if (json && (done || failure)) {
|
|
1408
1493
|
out(jsonText(WAIT, { ...r, timedOut: !done, tables: picked.map(waitTable),
|
|
@@ -1414,7 +1499,9 @@ export async function wait(argv) {
|
|
|
1414
1499
|
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
1500
|
show([...tableLines(s), ...(s.stream ? ["Stream:", ...streamLines(s.stream).map((l) => ` ${l}`)] : [])], meanwhile); // done: the meanwhile Next is dropped
|
|
1416
1501
|
if (r.ready) {
|
|
1417
|
-
const head = !picked.length ? "Nothing to wait for: no table added yet."
|
|
1502
|
+
const head = !picked.length ? "Nothing to wait for: no table added yet."
|
|
1503
|
+
: only !== null ? `Ready: ${picked.map((t) => t.table).join(", ")} ${picked.length === 1 ? "is" : "are"} synced and served.`
|
|
1504
|
+
: "Ready: every added table is synced and served.";
|
|
1418
1505
|
if (!json)
|
|
1419
1506
|
out(withEnd(picked.length ? styleOut().green(head) : head, { next: readyNext }));
|
|
1420
1507
|
return 0;
|
|
@@ -1425,6 +1512,44 @@ export async function wait(argv) {
|
|
|
1425
1512
|
}
|
|
1426
1513
|
});
|
|
1427
1514
|
}
|
|
1515
|
+
/**
|
|
1516
|
+
* WHAT `mason wait <table>…` WAITS FOR: a generated table named alone (its chain: `waitGenerated`), or synced tables, each
|
|
1517
|
+
* found among the added ones as `mason sync` finds them (`findTable`) — a table of the database not added, or a name
|
|
1518
|
+
* nothing has, refused with the way out.
|
|
1519
|
+
*/
|
|
1520
|
+
export function waitPick(s, names) {
|
|
1521
|
+
const generated = s.generatedTables ?? [];
|
|
1522
|
+
const added = s.tables.filter((t) => t.added);
|
|
1523
|
+
const tables = [];
|
|
1524
|
+
for (const name of names) {
|
|
1525
|
+
const g = generated.find((t) => t.table === name || t.table === `public.${name}`);
|
|
1526
|
+
if (g !== undefined) {
|
|
1527
|
+
if (names.length > 1) {
|
|
1528
|
+
return { message: `${entryName(g.table)} is a generated table, which is waited for alone`, code: "usage",
|
|
1529
|
+
fix: `mason wait ${entryName(g.table)}, then mason wait ${names.filter((n) => n !== name).join(" ")}`, next: [["mason wait --help", "what a wait waits for"]] };
|
|
1530
|
+
}
|
|
1531
|
+
return { generated: name };
|
|
1532
|
+
}
|
|
1533
|
+
const t = findTable(added, name);
|
|
1534
|
+
if (!("message" in t)) {
|
|
1535
|
+
tables.push(t.table);
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
// a table of the database that is not added is told so, with the command that adds it
|
|
1539
|
+
const db = findTable(s.tables, name);
|
|
1540
|
+
if ("message" in db && db.code === "usage")
|
|
1541
|
+
return db;
|
|
1542
|
+
if (!("message" in db)) {
|
|
1543
|
+
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"]] };
|
|
1544
|
+
}
|
|
1545
|
+
const choices = [...added.flatMap((x) => [x.table, tableArg(x.table)]), ...generated.map((x) => entryName(x.table))];
|
|
1546
|
+
const named = [...added.map((x) => tableArg(x.table)), ...generated.map((x) => entryName(x.table))];
|
|
1547
|
+
return { message: `there is no added or generated table ${JSON.stringify(name)}${didYouMean(name, choices)}`, code: "not_found",
|
|
1548
|
+
...(named.length ? { fix: `name one of: ${named.join(", ")}` } : {}),
|
|
1549
|
+
next: [["mason status", "the added tables and the generated ones"], ["mason tables", "the database's tables"]] };
|
|
1550
|
+
}
|
|
1551
|
+
return { tables: [...new Set(tables)] };
|
|
1552
|
+
}
|
|
1428
1553
|
/** `mason wait <generated table>`: until it holds every change synced when the wait began (src/cli/readiness.ts
|
|
1429
1554
|
* generatedReadiness), a line when what it waits for changes; the same outcome and exit codes as the wait for copies */
|
|
1430
1555
|
async function waitGenerated(run, named, timeout, json) {
|
|
@@ -1446,7 +1571,7 @@ async function waitGenerated(run, named, timeout, json) {
|
|
|
1446
1571
|
? { 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
1572
|
: !r.ready && Date.now() >= until
|
|
1448
1573
|
? { 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 <
|
|
1574
|
+
next: [[`mason wait ${entryName(g.table)}`, "keep waiting (--timeout <duration> for longer)"], ...next] }
|
|
1450
1575
|
: null;
|
|
1451
1576
|
if (json && (r.ready || failure)) {
|
|
1452
1577
|
out(jsonText(WAIT, { ...r, timedOut: !r.ready && failure?.code === "timeout", tables: [], generatedTable: g, ...(failure ? { error: errorJson(failure) } : {}) }, failure ? failure.next ?? [] : next));
|
|
@@ -1476,17 +1601,7 @@ export const EVENTS_LEAVE_OUT = "A scheduled poll that succeeds is not listed: t
|
|
|
1476
1601
|
const LOGS_USAGE = `usage: mason logs [-n <lines>] [-f] [--start] [--requests [<filters>]] [--events [--failed]] [--running] [--json]
|
|
1477
1602
|
|
|
1478
1603
|
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).
|
|
1604
|
+
running or not.
|
|
1490
1605
|
|
|
1491
1606
|
mason logs
|
|
1492
1607
|
mason logs -f keep printing new lines as Mason works, until Ctrl-C
|
|
@@ -1505,7 +1620,8 @@ const LOGS_USAGE = `usage: mason logs [-n <lines>] [-f] [--start] [--requests [<
|
|
|
1505
1620
|
--failed with --requests: only the ones not answered (refused, failed or cancelled); with --events: only
|
|
1506
1621
|
the failures
|
|
1507
1622
|
--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)
|
|
1623
|
+
the machine's), cancelled (a cancel or a statement timeout, SQLSTATE 57014); a comma list or
|
|
1624
|
+
the flag repeated, the same
|
|
1509
1625
|
--since <time> with --requests: started at or after it — an age (30m, 2h, 7d) or an ISO 8601 date or time with
|
|
1510
1626
|
its zone (2026-10-28, 2026-10-28T09:00:00Z)
|
|
1511
1627
|
--until <time> with --requests: started before it (as --since)
|
|
@@ -1515,18 +1631,32 @@ const LOGS_USAGE = `usage: mason logs [-n <lines>] [-f] [--start] [--requests [<
|
|
|
1515
1631
|
--search <text> with --requests: only the statements whose text has it, in any case (at most ${MAX_SEARCH} characters)
|
|
1516
1632
|
--before <pos> with --requests: the page before: the position a page answered with (its last Next line)
|
|
1517
1633
|
--running the statements running now (Mason must be running)
|
|
1518
|
-
--json as data:
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1634
|
+
--json as data: its shape is at the end
|
|
1635
|
+
|
|
1636
|
+
With --requests, the run's request log instead: every statement a tool sent the
|
|
1637
|
+
Postgres API that Mason answered, and every one it refused, failed or cancelled, a line each, newest last — when, the
|
|
1638
|
+
outcome and SQLSTATE, how long, the rows sent, the client's application_name (and the mason user's name), the
|
|
1639
|
+
statement and the error's first line; the filters narrow it, and a page that has older rows ends with the line that
|
|
1640
|
+
reads them (--before). With --events, the sync's events: each first sync that failed or finished, each poll, catch-up or
|
|
1641
|
+
stream that failed, the database out of reach, a table current again, each sync asked by hand (mason sync, the
|
|
1642
|
+
page's Sync now) and what it did, and each restart of the run — a line each, newest last (a failure repeated word for
|
|
1643
|
+
word is said once an hour). A scheduled poll that succeeds is not listed: the last one's result is each table's last
|
|
1644
|
+
sync (mason status, the Tables page's Last sync). Both are kept MASON_HISTORY_DAYS days
|
|
1645
|
+
(default 7) in the run's synced data, and read through the running Mason. With --running, the statements running now
|
|
1646
|
+
instead, one per session, each with its number (mason cancel <session> --statement <n> stops one).
|
|
1647
|
+
|
|
1648
|
+
The --json answer: { lines: [{ at, text }] } (the log's entries, oldest first; at is null for a line with no
|
|
1649
|
+
time); with --requests { requests: [{ requestId, startedAt, endedAt, session, applicationName,
|
|
1650
|
+
clientAddress (the client's address; behind a TCP proxy, the one its PROXY header carries),
|
|
1651
|
+
tokenName, user (the mason user), role, attributes (the user's values its role's row filters
|
|
1652
|
+
read, or null), method, outcome (ok, refused, failed, cancelled), status
|
|
1653
|
+
(completed, failed, cancelled: what a reader shows), sqlstate, message, detail, hint, rows,
|
|
1654
|
+
bytesSent, durationMs, query, queryTruncated }], keptDays, before (the position of the page before,
|
|
1655
|
+
or null: none older) }; with --events { events: [{ at, kind (copy_failed, poll_failed,
|
|
1656
|
+
catch_up_failed, stream_failed, source_unreachable, restart, copied, recovered, synced (a sync
|
|
1657
|
+
asked by hand), sync_changed (a table's sync: changed)), failed, table (null: the whole sync),
|
|
1658
|
+
message }], keptDays }; with --running { running: [{ session, user,
|
|
1659
|
+
applicationName, method, startedAt, runningMs, rows (sent so far), query, queryTruncated }] }; next
|
|
1530
1660
|
`;
|
|
1531
1661
|
export const LOGS = {
|
|
1532
1662
|
needs: "project",
|
|
@@ -1534,7 +1664,7 @@ export const LOGS = {
|
|
|
1534
1664
|
summary: "Mason's log (-f follows it); --requests: the Postgres API's statements; --events: the sync's; --running: running now",
|
|
1535
1665
|
usage: LOGS_USAGE,
|
|
1536
1666
|
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" },
|
|
1667
|
+
requests: { type: "boolean" }, events: { type: "boolean" }, failed: { type: "boolean" }, outcome: { type: "string", multiple: true }, since: { type: "string" },
|
|
1538
1668
|
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
1669
|
json: ["lines", "requests", "events", "keptDays", "before", "running", "next"],
|
|
1540
1670
|
};
|
|
@@ -1674,7 +1804,7 @@ async function runningLogs(json) {
|
|
|
1674
1804
|
/** A refusal the run answered, in the Failure shape it carries (`failure`, src/cli/run-server.ts sendFailure). */
|
|
1675
1805
|
export function runFailure(reply, next) {
|
|
1676
1806
|
const f = reply.body.failure;
|
|
1677
|
-
const code =
|
|
1807
|
+
const code = failureCode(f?.code, reply.status === 400 ? "usage" : reply.status === 404 ? "not_found" : reply.status === 409 ? "refused" : "failed");
|
|
1678
1808
|
return { message: typeof f?.message === "string" ? f.message : String(reply.body.error ?? `the run answered ${reply.status}`), code,
|
|
1679
1809
|
...(typeof f?.why === "string" ? { why: f.why } : {}), ...(typeof f?.fix === "string" ? { fix: f.fix } : {}), next };
|
|
1680
1810
|
}
|
|
@@ -1682,6 +1812,9 @@ export async function logs(argv) {
|
|
|
1682
1812
|
const f = parseCommand(LOGS, argv);
|
|
1683
1813
|
if (typeof f === "number")
|
|
1684
1814
|
return f;
|
|
1815
|
+
const extra = noArguments(LOGS, f.positionals, f.values.json === true);
|
|
1816
|
+
if (extra !== null)
|
|
1817
|
+
return extra;
|
|
1685
1818
|
const json = f.values.json === true;
|
|
1686
1819
|
if (f.values.requests && f.values.events)
|
|
1687
1820
|
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 +1844,7 @@ export async function logs(argv) {
|
|
|
1711
1844
|
if (f.values.requests) {
|
|
1712
1845
|
// the filters, read as the run reads its query string (src/cli/request-filter.ts): a mistake is said here, exit 2
|
|
1713
1846
|
const text = (v) => typeof v === "string" ? v : undefined;
|
|
1714
|
-
const q = requestFilter({ limit: text(f.values.lines), failed: f.values.failed === true, outcome:
|
|
1847
|
+
const q = requestFilter({ limit: text(f.values.lines), failed: f.values.failed === true, outcome: listFlag(f.values.outcome), since: text(f.values.since),
|
|
1715
1848
|
until: text(f.values.until), user: text(f.values.user), client: text(f.values.client),
|
|
1716
1849
|
slow: f.values.slow === true, search: text(f.values.search), before: text(f.values.before) }, Date.now(), flagOf);
|
|
1717
1850
|
if ("message" in q)
|
|
@@ -1775,9 +1908,9 @@ const STOP_USAGE = `usage: mason stop [--json]
|
|
|
1775
1908
|
|
|
1776
1909
|
mason stop
|
|
1777
1910
|
|
|
1778
|
-
mason destroy removes the synced data and the state too (your files stay), to free the disk or begin again.
|
|
1779
|
-
|
|
1780
1911
|
--json as data: { stopped: [name], killed (a run that did not answer and ignored SIGTERM), supervisor (its pid, when one was stopped), next }
|
|
1912
|
+
|
|
1913
|
+
mason destroy removes the synced data and the state too (your files stay), to free the disk or begin again.
|
|
1781
1914
|
`;
|
|
1782
1915
|
export const STOP = {
|
|
1783
1916
|
needs: "project",
|
|
@@ -1791,6 +1924,9 @@ export async function stop(argv) {
|
|
|
1791
1924
|
const f = parseCommand(STOP, argv);
|
|
1792
1925
|
if (typeof f === "number")
|
|
1793
1926
|
return f;
|
|
1927
|
+
const extra = noArguments(STOP, f.positionals, f.values.json === true);
|
|
1928
|
+
if (extra !== null)
|
|
1929
|
+
return extra;
|
|
1794
1930
|
const json = f.values.json === true;
|
|
1795
1931
|
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
1932
|
const r = await theRun(process.cwd());
|