@mason-data/cli 0.7.0 → 0.8.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 +90 -71
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog.js +222 -13
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/server.js +1173 -207
- package/dist/src/api/postgres/translate.js +81 -9
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +29 -6
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +1 -0
- package/dist/src/cli/commands.js +622 -183
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/demo.js +19 -10
- package/dist/src/cli/dev.js +351 -111
- package/dist/src/cli/doctor.js +20 -10
- package/dist/src/cli/guides.js +79 -31
- package/dist/src/cli/init.js +119 -58
- package/dist/src/cli/machine.js +12 -11
- package/dist/src/cli/main.js +3 -3
- package/dist/src/cli/mcp.js +29 -20
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/pg-tls.js +13 -3
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +13 -12
- package/dist/src/cli/preflight.js +8 -7
- package/dist/src/cli/project.js +43 -4
- package/dist/src/cli/readiness.js +38 -4
- package/dist/src/cli/registry.js +53 -34
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +584 -0
- package/dist/src/cli/run-server.js +483 -41
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +10 -8
- package/dist/src/cli/serve.js +144 -42
- package/dist/src/cli/share-run.js +286 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +65 -13
- package/dist/src/cli/sources.js +287 -38
- package/dist/src/cli/supervise.js +12 -1
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui.js +9 -6
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user.js +496 -40
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +3 -0
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/events.js +134 -0
- package/dist/src/duck/executor-worker.js +18 -9
- package/dist/src/duck/executor.js +43 -30
- package/dist/src/duck/explain.js +56 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +13 -14
- package/dist/src/duck/gaps.js +17 -8
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/history.js +123 -0
- package/dist/src/duck/kept.js +6 -6
- package/dist/src/duck/plan.js +102 -25
- package/dist/src/duck/publication.js +6 -6
- package/dist/src/duck/requests.js +265 -93
- package/dist/src/duck/rewrite.js +25 -5
- package/dist/src/duck/row-filter.js +649 -0
- package/dist/src/duck/store.js +156 -120
- package/dist/src/duck/stream.js +10 -5
- package/dist/src/duck/sync.js +835 -268
- package/dist/src/duck/types.js +6 -5
- package/dist/src/duck/usage.js +324 -0
- package/dist/src/duck/users.js +64 -15
- package/dist/src/duck/verified.js +3 -0
- package/dist/src/duck/views.js +2 -3
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/log-buffer.js +68 -0
- package/dist/src/lib/pg-numeric.js +5 -2
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/table-sync.js +267 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +327 -410
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +446 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +434 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +218 -0
- package/dist/src/web/shared.js +568 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,60 +1,66 @@
|
|
|
1
1
|
# @mason-data/cli
|
|
2
2
|
|
|
3
|
-
The `mason` command points [Mason](https://
|
|
4
|
-
add, keeps
|
|
3
|
+
The `mason` command points [Mason](https://github.com/embeddable-hq/mason-data) at the Postgres your app already uses, syncs the tables you
|
|
4
|
+
add, keeps them up to date, and serves them to BI tools and your app over the Postgres protocol. Every command works the
|
|
5
5
|
same for you and for a coding agent (Claude Code, Codex, …): it prints for a person, or with `--json` as data, and its
|
|
6
6
|
exit code says how it went ([for coding agents](#for-coding-agents)).
|
|
7
7
|
|
|
8
8
|
There is nothing to clone or build: install this package and run `mason`. It needs Node 22.13 or later and nothing
|
|
9
|
-
else — no Docker, no database of its own
|
|
9
|
+
else — no Docker, no database of its own, and nothing downloaded after `npm install`: Mason's postgres and inet
|
|
10
|
+
extensions come with it, as an optional dependency for your platform (`@mason-data/extensions-<platform>`, 17–31 MiB
|
|
11
|
+
to download; installing with `--omit=optional` leaves them out, and the first sync then downloads them).
|
|
12
|
+
`mason doctor` checks this machine and says what is missing. On a terminal
|
|
10
13
|
its output is coloured and wrapped to the window, and a command that takes a moment shows a spinner; piped,
|
|
11
14
|
redirected, with `--json` or with `NO_COLOR` set, it is plain text.
|
|
12
15
|
|
|
13
16
|
**A Mason project is your app's folder.** `mason init` makes it one: its sources file in `mason/sources/` — the
|
|
14
|
-
database and the tables to
|
|
17
|
+
database and the tables to sync — yours to commit; this machine's synced data and Mason's state in `.mason/`,
|
|
15
18
|
both rebuilt from that file. Every other command works
|
|
16
|
-
anywhere in the folder, as git does. `mason destroy` removes the
|
|
19
|
+
anywhere in the folder, as git does. `mason destroy` removes the synced data and the state. ([Cleaning up](#cleaning-up))
|
|
17
20
|
|
|
18
21
|
```sh
|
|
19
22
|
npm i -g @mason-data/cli
|
|
20
23
|
cd my-app
|
|
21
24
|
mason doctor # Node, memory, ports: what is missing, and what to do
|
|
22
|
-
mason init # this folder becomes a project, on the database
|
|
25
|
+
mason init # this folder becomes a project, on the database .env names (checked first)
|
|
23
26
|
mason tables # the tables: rows, keys, added; a table's columns with `mason tables orders`
|
|
24
27
|
mason add orders customers # add them to the sources file (--columns id,status: those only)
|
|
25
|
-
mason start # Mason in the background,
|
|
26
|
-
mason wait # until every added table is
|
|
28
|
+
mason start # Mason in the background, syncing them, then returns
|
|
29
|
+
mason wait # until every added table is synced and served
|
|
27
30
|
mason user add metabase # a user for a BI tool: its password, printed once, and the URL to paste
|
|
28
|
-
mason ui # the status
|
|
31
|
+
mason ui # the status, tables, users and statements, in your browser
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
`mason init` takes the database's URL as an argument too (`mason init postgres://user:password@localhost:5432/your_app`);
|
|
32
|
-
without one it reads `DATABASE_URL
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
database
|
|
35
|
+
without one it reads `MASON_DATABASE_URL`, else `DATABASE_URL`, each from the folder's `.env.local`, `.env` or the
|
|
36
|
+
shell — set `MASON_DATABASE_URL` to aim Mason at a database other than the one your app's `DATABASE_URL` names. The
|
|
37
|
+
sources file it writes names the variable it read, `url: ${DATABASE_URL}` or `url: ${MASON_DATABASE_URL}` (given a URL
|
|
38
|
+
as an argument, `MASON_DATABASE_URL` when your app's `DATABASE_URL` names another database), and the URL itself stays
|
|
39
|
+
in `.env` (init writes the line there when you gave the URL as an argument or it was only in the shell, and gitignores
|
|
40
|
+
that file): change it there when the password changes. The database is required.
|
|
36
41
|
|
|
37
42
|
**No database at hand?** `mason init demo && mason start`, in a new folder, makes a project on a demo database Mason
|
|
38
43
|
hosts itself — a small shop (`customers`, `products`, `orders`, `order_items`; the first three added, `order_items`
|
|
39
44
|
left for your first `mason add order_items` after `mason start`), kept in `.mason/demo/`, served on a port of this machine, with a new order every few seconds (`MASON_DEMO_CHANGE_MS`, default 5000) so
|
|
40
|
-
`mason status` shows the
|
|
41
|
-
`mason destroy` removes it with the
|
|
45
|
+
`mason status` shows the synced data keeping up. Nothing to install. It is read by polling only (it has no change log), and
|
|
46
|
+
`mason destroy` removes it with the synced data.
|
|
42
47
|
|
|
43
48
|
## The project: its sources file, in your app
|
|
44
49
|
|
|
45
50
|
```
|
|
46
51
|
my-app/
|
|
47
|
-
.env the database's URL (DATABASE_URL
|
|
48
|
-
.mason/ this machine's: Mason's key, the
|
|
49
|
-
mason/sources/app.yml the database and the tables Mason
|
|
52
|
+
.env the database's URL (DATABASE_URL, as your app has it, or MASON_DATABASE_URL) (gitignored)
|
|
53
|
+
.mason/ this machine's: Mason's key, the synced data, its state and logs (gitignored)
|
|
54
|
+
mason/sources/app.yml the database and the tables Mason syncs — commit it
|
|
50
55
|
mason/mason.yml the Mason version the project is written for — commit it: production runs that version
|
|
51
56
|
```
|
|
52
57
|
|
|
53
|
-
The sources file is named for the database (`app.yml` above) and lists what Mason
|
|
58
|
+
The sources file is named for the database (`app.yml` above) and lists what Mason syncs, one entry per table:
|
|
54
59
|
|
|
55
60
|
```yaml
|
|
56
61
|
kind: postgres
|
|
57
62
|
url: ${DATABASE_URL} # a variable from .env.local, .env or the shell; a literal URL is allowed for local dev
|
|
63
|
+
database: production # optional: the database name a tool connects with (none: mason)
|
|
58
64
|
tables:
|
|
59
65
|
orders: # a bare name is in `public`; `billing.invoices` names its schema
|
|
60
66
|
columns: [id, customer_id, status, amount, created_at, payload]
|
|
@@ -62,12 +68,14 @@ tables:
|
|
|
62
68
|
events:
|
|
63
69
|
exclude: [raw_body] # every column but these
|
|
64
70
|
sessions:
|
|
65
|
-
key: [tenant_id, id] # only for a table with no primary key: the unique columns to
|
|
71
|
+
key: [tenant_id, id] # only for a table with no primary key: the unique columns to key it by
|
|
66
72
|
```
|
|
67
73
|
|
|
68
|
-
- An entry is `{}` (every column), `columns:` (those; the primary key is always
|
|
74
|
+
- An entry is `{}` (every column), `columns:` (those; the primary key is always synced), `exclude:` (every column but
|
|
69
75
|
those) — never both — and, for a table with no primary key, `key:`: the columns that tell its rows apart, which it is
|
|
70
|
-
|
|
76
|
+
keyed by (polling only: logical replication cannot apply a change by them). A table with a primary key refuses `key:`.
|
|
77
|
+
- A tool connects with the database name `mason`. A tool moving from your Postgres changes its host, port and database
|
|
78
|
+
name, or `database:` keeps the name it has, so it changes only its host and port.
|
|
71
79
|
- Column types are not in the file: Mason reads them from the database at every start.
|
|
72
80
|
- `url:` is `${NAME}`, read from `.env.local`, `.env` or the shell — or a literal URL, for a database on your machine.
|
|
73
81
|
- One source per project for now, and `postgres` is its only kind.
|
|
@@ -77,11 +85,11 @@ tables:
|
|
|
77
85
|
mistakes on one report, each with its key and the way out (`did you mean …?`).
|
|
78
86
|
|
|
79
87
|
`mason init --folder <dir>` puts `sources/` elsewhere in the folder (`--folder .` for the folder itself). Keep it in
|
|
80
|
-
your app's repo, so a schema change and the tables that
|
|
88
|
+
your app's repo, so a schema change and the tables that sync it travel together. A teammate who clones the repo runs
|
|
81
89
|
`mason init` once: the sources file is kept, and `.mason/` is per machine.
|
|
82
90
|
|
|
83
91
|
**A project an older Mason made** (its tables listed in `mason/models/`) is refused by `mason start`; nothing is
|
|
84
|
-
migrated. `mason init` in it writes the sources file alone — its `.mason/` and
|
|
92
|
+
migrated. `mason init` in it writes the sources file alone — its `.mason/` and synced data kept — then `mason add` each table
|
|
85
93
|
again and remove `mason/models/`.
|
|
86
94
|
|
|
87
95
|
## Commands
|
|
@@ -89,22 +97,24 @@ again and remove `mason/models/`.
|
|
|
89
97
|
| Command | What it does |
|
|
90
98
|
|---|---|
|
|
91
99
|
| `mason help [<command> \| <guide>]` | The commands by step, one command's usage, or one of the guides, offline: `mason help agent` (driving Mason from a coding agent: the commands in order, `--json` and its shapes, exit codes, failures) and `mason help production` (the road from a laptop to production: the image, its settings, Docker Compose, logical replication, upgrades). |
|
|
92
|
-
| `mason doctor` | Checks what Mason needs here — Node 22.13+, memory, Mason's ports — and in a project's folder its database; with Mason running for it, the run itself: whether every added table is
|
|
93
|
-
| `mason init [<postgres url>]` | Makes this folder a project: checks it can read the database (nothing is written when it cannot; `--skip-check` writes anyway), writes `.mason/config.json` (Mason's key, never the URL) and `mason/sources/<database>.yml` (keeping a sources file already there), adds the URL to `.env` when it was not there, and adds `.mason/` and that `.env` to `.gitignore`. The URL from the argument, else `DATABASE_URL
|
|
94
|
-
| `mason start` | Starts Mason for this folder's project in the background and returns once it is up. Run it again any time: it says so when Mason is already running.
|
|
100
|
+
| `mason doctor` | Checks what Mason needs here — Node 22.13+, memory, Mason's ports — and in a project's folder its database; with Mason running for it, the run itself: whether every added table is synced, the statements refused or slow in the last day. Every failing line says what to do; it ends with the next command. |
|
|
101
|
+
| `mason init [<postgres url>]` | Makes this folder a project: checks it can read the database (nothing is written when it cannot; `--skip-check` writes anyway), writes `.mason/config.json` (Mason's key, never the URL) and `mason/sources/<database>.yml` (keeping a sources file already there), adds the URL to `.env` when it was not there, and adds `.mason/` and that `.env` to `.gitignore`. The URL from the argument, else `MASON_DATABASE_URL`, else `DATABASE_URL`, each in `.env.local`, `.env` or the shell (the sources file names the one read). `--name` sets the project's name (default: the folder's), which its replication slot is named for; `--database` the database name a tool connects with (default `mason`, none written); `--folder` where its files go, `--remote` allows a database on another machine. Refuses a folder already in a project, but for one with no sources file (an older Mason's), whose sources file alone it writes. |
|
|
102
|
+
| `mason start` | Starts Mason for this folder's project in the background and returns once it is up. Run it again any time: it says so when Mason is already running. Each table is kept current as its `sync:` in the sources file says (`off`, `poll <interval>` or `replication`). `--self-signed-tls` serves the Postgres API over TLS; `--attach` stays in the foreground. |
|
|
95
103
|
| `mason tables [<table>…] [--all]` | The tables this user can read against the sources file, one line each: rows, primary key, whether added (and with which columns), and why a table cannot be added yet (no primary key: the `--key` to add it by). Name a table (or pass `--all` for every one) for its columns: each one's type, what it references, and whether `mason add` would leave it out as a secret. It reads the project's database itself, so Mason need not be running. |
|
|
96
|
-
| `mason add <table>… [--columns …] [--include …] [--key …]` |
|
|
97
|
-
| `mason remove <table>…` | Takes tables out of the sources file — each one's entry, every other byte as it was — and Mason drops their
|
|
98
|
-
| `mason wait [--timeout <s>]` | Waits until every added table is
|
|
104
|
+
| `mason add <table>… [--columns …] [--include …] [--key …] [--sync …]` | Syncs tables into Mason, each one an entry of the sources file: every column (`orders: {}`), or `--columns id,status,created_at` for those (one table at a time; the key is written with them). Columns that look like secrets (`password`, `token`, `api_key`, `private_key`, …) are left out, named, and written as `exclude:`; `--include password_hash` adds one. `--key tenant_id,id` adds a table with no primary key, keyed by those columns. `--sync "poll 30s"` (or `off`) writes how it is kept current after its first sync on its entry; on a table already added, `--sync` alone changes that line and nothing else. Without it the entry follows the file's `sync:`, but a table over 1,000,000 rows under a file that polls is written `sync: off` and said, and a frequent poll of tens of millions of rows is warned of with an estimate of its cost. Adding a table again with other columns changes its entry. Mason need not be running: the entry is checked against the database as the run checks it, and the first sync begins at `mason start` (with Mason running, at once). |
|
|
105
|
+
| `mason remove <table>…` | Takes tables out of the sources file — each one's entry, every other byte as it was — and Mason drops their synced data: at once when it is running, else at the next `mason start`. Mason need not be running. |
|
|
106
|
+
| `mason wait [--timeout <s>]` | Waits until every added table is synced and served, showing every added table on a line of its own: 100% for the finished ones, in the order they finished, and a bar for each still syncing (rows, %, rate, time left) at the bottom. Exit 0 ready, 1 a first sync failed (printed, with what to do), 124 timed out (default 600 s). |
|
|
99
107
|
| `mason user add\|list\|remove <name>` | The users of the Postgres API: a name and a password that does not expire, as a BI tool's datasource form asks for them or an app's `DATABASE_URL` carries them. `add` prints the password once with the URL; `remove` refuses the next connection at once and ends the sessions it holds at their next statement, without a restart. |
|
|
100
|
-
| `mason mcp [--url <url>]` | An MCP server on stdio for Claude Code, Claude Desktop and other agents (below): the tables Mason serves with each
|
|
101
|
-
| `mason ui` | Opens the running Mason's web page in your browser: what `mason status` says,
|
|
102
|
-
| `mason status` | What Mason is doing: the database and how its changes are read, the project folder, the
|
|
103
|
-
| `mason sync <table>…` |
|
|
104
|
-
| `mason logs [-n <lines>] [-f] [--start] [--requests [--failed]]` | Mason's log: every
|
|
105
|
-
| `mason
|
|
106
|
-
| `mason
|
|
107
|
-
| `mason
|
|
108
|
+
| `mason mcp [--url <url>]` | An MCP server on stdio for Claude Code, Claude Desktop and other agents (below): the tables Mason serves with each one's rows and how current it is, a table's columns, a statement's rows with their Postgres types or Mason's refusal with its way out, and EXPLAIN — connected as a `mason user` (`MASON_URL`, else `--url`), beside `mason start` or a `mason serve` elsewhere. |
|
|
109
|
+
| `mason ui` | Opens the running Mason's web page in your browser: the Overview (what `mason status` says), the Tables (add, remove or sync one), the Users of `mason user` (add one, its password shown once; disable, enable or remove one) and Observability (the statements, usage over time, the sync's events), refreshed while the tab is open. It prints the URL with the run's token, which the page trades for a cookie on the first visit; `--no-open` prints it only, `--json` is `{ url, port }`. `mason start`'s alone: `mason serve` serves no page. |
|
|
110
|
+
| `mason status` | What Mason is doing: the database and how its changes are read, the project folder, the synced data's file, the Postgres API and how it listens, each synced table, the statements of the last day and week (each outcome, the completed ones' average, p50 and p95 time, the rows and bytes sent), the ones answered slowly in the last day and the ones refused or failed, each with its reason. In replication mode, where the stream is. |
|
|
111
|
+
| `mason sync <table>…` | Brings these synced tables up to date now, by one poll each: a table whose `sync:` is `off` is kept current only so (from cron, before a report), a polled one is polled now as well as at its interval. In replication mode the stream keeps every table current, and it says where the stream is. |
|
|
112
|
+
| `mason logs [-n <lines>] [-f] [--start] [--requests [<filters>]] [--events [--failed]] [--running]` | Mason's log: every sync, poll and statement, with warnings and errors and their reasons; `-f` follows it, `--start` shows what `mason start` printed instead. `--requests` is the request log: every statement a tool sent, when, the outcome (ok, refused, failed, cancelled) and SQLSTATE, how long, the rows and bytes sent, the tool and the user; `--failed` only the ones not answered, `--outcome`, `--since` / `--until` (`2h`, `7d`, an ISO time), `--user` and `--client` narrow it, and `--before` reads the page before. `--events` is the sync's events: each first sync, poll, catch-up or stream that failed, the database out of reach, a table current again, each sync asked by hand (mason sync, the page's Sync now), and each restart of the run (`--failed` only the failures). `--running` lists the statements running now, by session. |
|
|
113
|
+
| `mason cancel <session>` | Stops the statement a session of the Postgres API is running, as the client's own cancel would: the client is told 57014, its session goes on, and the request log keeps it as cancelled. `mason logs --running` lists the sessions. |
|
|
114
|
+
| `mason usage [--since 7d] [--step 1h]` | What Mason did over time, from a sample a minute kept `MASON_HISTORY_DAYS` days: the statements and their p95, the rows and bytes sent, the synced data's size and rows, the stalest table, the stream's lag, the memory; then each synced table's own size. |
|
|
115
|
+
| `mason stop` | Stops Mason for this project. The synced data and the state stay: the next `mason start` resumes in seconds. |
|
|
116
|
+
| `mason destroy` | Removes this project's synced data (its file) and Mason's state in `.mason/`, stopping Mason first if it runs — and, when a table was replicated, its slot (and its publication, when Mason made it) on the database. Your files, `.mason/config.json` and the users stay: the next `mason start` syncs the tables again. |
|
|
117
|
+
| `mason serve` | Mason for a server or a container: `mason start --attach` configured by the environment alone, its state in one folder (a volume), its log on stdout, `GET /healthz`, `GET /metrics` (Prometheus, numbers only), a clean stop on SIGTERM. `mason serve --help` lists every setting with its default. |
|
|
108
118
|
|
|
109
119
|
Every command documents itself: `mason --help` lists them, and `mason <command> --help` (or `mason help <command>`) says
|
|
110
120
|
how to use one, with examples. Every command but `help` and `mcp` (whose stdout is the MCP protocol's) takes `--json`. A command means the project of the folder it
|
|
@@ -112,47 +122,56 @@ runs in, or the nearest one above it.
|
|
|
112
122
|
|
|
113
123
|
## What `mason start` does
|
|
114
124
|
|
|
115
|
-
1. **It only reads the database** — the one the sources file names
|
|
116
|
-
|
|
117
|
-
them. When you stop it, nothing is left behind on the database.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
is
|
|
125
|
+
1. **It only reads the database** — the one the sources file names, with `SELECT` statements. It does not need
|
|
126
|
+
`wal_level = logical`, a replication slot, a publication or the `REPLICATION` attribute, and it creates none of
|
|
127
|
+
them. When you stop it, nothing is left behind on the database. After its first sync each table is kept current as
|
|
128
|
+
the sources file's `sync:` says — at the top for every table, on an entry for that one: `off` (the default, and what
|
|
129
|
+
`mason init` writes) reads it again only at `mason sync <table>`; `poll 30s` polls it every 30 seconds, each poll a
|
|
130
|
+
scan of the table, the next one at least four times the last one's time later. Changing a table's `sync:` keeps its
|
|
131
|
+
synced data and catches it up by one poll. `mason status` says each table's sync and when it was last synced, on its line.
|
|
132
|
+
(`sync: replication` is the exception: that table is read from the change log, as production does, through one slot
|
|
133
|
+
Mason makes and a publication carrying the replicated tables alone — its own, or the one `publication:` at the top
|
|
134
|
+
of the file names, never altered. The slot is dropped, verified gone, once no table is replicated.)
|
|
135
|
+
2. **It syncs each added table** into a DuckDB file in `.mason/`, a range of pages at a time, a few in parallel, one
|
|
136
|
+
Postgres backend each, smallest table first; a first sync that stops continues with the ranges it did not commit. The table
|
|
137
|
+
is served the moment its first sync lands; it runs beside the polls, never instead of them.
|
|
123
138
|
3. **It serves the Postgres API** on 5433, or on the next port free on every address when something holds 5433 (often a
|
|
124
139
|
second Postgres; `mason start` says what held it) — and records it: every later start listens on the same port, so a
|
|
125
140
|
tool's saved connection keeps working, and refuses, naming what holds it, rather than move. `MASON_PG_PORT=55433` in
|
|
126
141
|
your shell names the port, or moves the project to it (said). It listens on 127.0.0.1 unless `MASON_PG_HOST` names
|
|
127
142
|
another address; off this machine it needs TLS (`--self-signed-tls`, or `MASON_PG_TLS_CERT` / `MASON_PG_TLS_KEY`).
|
|
128
|
-
4. **It runs in the background**,
|
|
143
|
+
4. **It runs in the background**, syncing what the sources file names: `mason add` and `mason remove` change it at once,
|
|
129
144
|
a hand edit (or an add or remove made while Mason was stopped) at the next `mason start`.
|
|
130
145
|
|
|
131
|
-
Mason's own settings (`
|
|
132
|
-
|
|
146
|
+
Mason's own settings (`MASON_DUCKDB_MEMORY`, `MASON_COPY_PARALLEL`, …) set in your shell reach Mason. How often a
|
|
147
|
+
table is polled is its `sync:`, never a setting: `MASON_POLL_MS` set refuses the start, naming the line to write.
|
|
148
|
+
A first sync reads `MASON_COPY_PARALLEL` chunks at once (default 3), each sized to take about `MASON_COPY_CHUNK_SECONDS` to
|
|
133
149
|
read (default 12) at the rate it measures on your connection, so `mason wait` moves every few seconds even over a slow
|
|
134
|
-
link, and a
|
|
135
|
-
A JSON value your queries keep reading (`payload ->> 'customer_id'`) is stored as a column of the
|
|
150
|
+
link, and a first sync that stops reads at most those chunks again.
|
|
151
|
+
A JSON value your queries keep reading (`payload ->> 'customer_id'`) is stored as a column of the synced table and read from
|
|
136
152
|
there (`mason status` lists them); `MASON_STORE_JSON_PATHS=off` turns that off.
|
|
137
153
|
|
|
138
154
|
## `mason start` options
|
|
139
155
|
|
|
140
156
|
| Option | What it does |
|
|
141
157
|
|---|---|
|
|
142
|
-
| `--replication` | Read the database's changes from its change log (logical replication), as production does, instead of polling. It needs `wal_level = logical` and a user that may replicate, and makes its own publication and slot (`mason_dev_<project>_<source>`, the source the sources file's name; past 63 bytes, a prefix and a hash) on the database. The project keeps the mode until `--poll`; a switch keeps the copies and catches each table up by one poll. `mason stop` leaves the slot (it holds WAL until the next start); `mason destroy` and `--poll` drop it. |
|
|
143
|
-
| `--publication <name>` | With `--replication`: a publication the database owner made (else `PG_PUBLICATION`), read as it is, verified to carry every added table and column, never created, altered or dropped. A read replica needs one. |
|
|
144
|
-
| `--poll` | Read them with `SELECT` again (the default). |
|
|
145
158
|
| `--self-signed-tls` | The Postgres API requires TLS, from a certificate made for this project in `.mason/` (served again by every later start with the flag); clients connect with `sslmode=require`. |
|
|
146
159
|
| `--port <n>` | The port the `mason` commands reach it on (default 4039, or the next free one). |
|
|
147
160
|
| `--attach` | Stay in the foreground: the terminal shows Mason's progress, and Ctrl-C stops it. |
|
|
148
161
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
`--replication`, `--poll` and `--publication` are gone: each answers with the line to write at the top of the sources
|
|
163
|
+
file (`sync: replication`, `sync: poll 5m`, `publication: <name>`) and exit 2, and starts nothing. A project that
|
|
164
|
+
streams, with no `sync:` anywhere in its file, is refused with its slot kept: `sync: replication` keeps streaming,
|
|
165
|
+
`sync: off` or `sync: poll <interval>` confirms the drop. One with a recorded publication needs `publication: <name>`,
|
|
166
|
+
or `publication: null` for Mason's own.
|
|
152
167
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
A database on another machine needs `mason init --remote`, which records that you mean it: the synced data leaves the
|
|
169
|
+
database's network, and each polled table is read whole at its `sync:` interval (`sync: replication` reads only its
|
|
170
|
+
changes). Point it at a read replica, never the primary your app writes to.
|
|
171
|
+
|
|
172
|
+
**When the database moves.** Mason records the Postgres server it synced from. The same database on a new server (a
|
|
173
|
+
restore, a move to another host) is read again at the next `mason start`: every row is compared with the synced data, and only
|
|
174
|
+
what changed is written. A different database is refused: `mason destroy`, then `mason start`, syncs it.
|
|
156
175
|
|
|
157
176
|
## For coding agents
|
|
158
177
|
|
|
@@ -188,10 +207,10 @@ and says `mason user add claude`.
|
|
|
188
207
|
|
|
189
208
|
| Tool | Arguments | What it answers |
|
|
190
209
|
|---|---|---|
|
|
191
|
-
| `tables` | none | each table Mason serves, named as a statement writes it (`public.orders`, `public."Clients"`): `rows_about` (the
|
|
210
|
+
| `tables` | none | each table Mason serves, named as a statement writes it (`public.orders`, `public."Clients"`): `rows_about` (the synced table's estimated row count), `kept` (how it is kept current, as `mason status` says it) and `kept_current_by` (`polling`, `logical replication`, `copying again`, or null when nothing keeps it current now) |
|
|
192
211
|
| `describe` | `table` (as `tables` names it, or a bare name in `public`) | its columns with their Postgres types and NOT NULL, its primary key, its foreign keys, comments, and the same rows and `kept` |
|
|
193
212
|
| `query` | `sql`, `max_rows` (200, at most 10,000), `timeout_ms` (30,000, at most 300,000) | `columns` (name, Postgres type), `rows` as Postgres's text, `truncated` when `max_rows` cut it; a refusal is a tool error with the SQLSTATE, message, DETAIL and HINT (the way out) |
|
|
194
|
-
| `explain` | `sql`, `analyze`, `timeout_ms` | Mason's EXPLAIN: each
|
|
213
|
+
| `explain` | `sql`, `analyze`, `timeout_ms` | Mason's EXPLAIN: each synced table read with its rows and how current it is, the SQL it runs, the plan |
|
|
195
214
|
|
|
196
215
|
Its resources are `mason://help/agent` and `mason://help/mcp` (the two pages `mason help` prints). Every statement it
|
|
197
216
|
runs is in the request log under `application_name` `mason-mcp/<the client's name>` (`mason logs --requests`), a query
|
|
@@ -203,9 +222,9 @@ comes before `--env`, which takes every value after it):
|
|
|
203
222
|
|
|
204
223
|
```sh
|
|
205
224
|
mason user add claude # the URL, printed once
|
|
206
|
-
claude mcp add mason --env MASON_URL="postgres://claude:<password>@127.0.0.1:5433/
|
|
225
|
+
claude mcp add mason --env MASON_URL="postgres://claude:<password>@127.0.0.1:5433/mason" -- mason mcp
|
|
207
226
|
# or on its command line, where ps shows it to this machine's users and claude mcp add prints it
|
|
208
|
-
claude mcp add mason -- mason mcp --url "postgres://claude:<password>@127.0.0.1:5433/
|
|
227
|
+
claude mcp add mason -- mason mcp --url "postgres://claude:<password>@127.0.0.1:5433/mason"
|
|
209
228
|
```
|
|
210
229
|
|
|
211
230
|
Claude Desktop, in `claude_desktop_config.json`. The app often starts a server without your shell's `PATH`, where
|
|
@@ -218,7 +237,7 @@ neither `mason` nor the `node` its first line asks for is found: name both by th
|
|
|
218
237
|
"mason": {
|
|
219
238
|
"command": "/absolute/path/to/node",
|
|
220
239
|
"args": ["/absolute/path/to/mason", "mcp"],
|
|
221
|
-
"env": { "MASON_URL": "postgres://claude:<password>@127.0.0.1:5433/
|
|
240
|
+
"env": { "MASON_URL": "postgres://claude:<password>@127.0.0.1:5433/mason" }
|
|
222
241
|
}
|
|
223
242
|
}
|
|
224
243
|
}
|
|
@@ -226,17 +245,17 @@ neither `mason` nor the `node` its first line asks for is found: name both by th
|
|
|
226
245
|
|
|
227
246
|
## Cleaning up
|
|
228
247
|
|
|
229
|
-
Three things exist once a project has run: your **files** (the sources file, in your app), the **
|
|
248
|
+
Three things exist once a project has run: your **files** (the sources file, in your app), the **synced data** (a
|
|
230
249
|
file in `.mason/`) and **Mason's state** (`.mason/`: its settings, logs and the request log). Each way of cleaning up
|
|
231
250
|
keeps a different set:
|
|
232
251
|
|
|
233
252
|
| You want | Run | Keeps |
|
|
234
253
|
|---|---|---|
|
|
235
254
|
| Pause | `mason stop` | the data, the state, your files: the next `mason start` resumes in seconds |
|
|
236
|
-
|
|
|
255
|
+
| Sync the data again, or free its disk | `mason destroy` | your files, `.mason/config.json` and the users: the next `mason start` syncs everything again |
|
|
237
256
|
|
|
238
257
|
Restarting the machine is a `mason stop` that nobody ran: the next `mason start` resumes. In a demo project
|
|
239
|
-
(`mason init demo`) the demo database is state too: `mason destroy` removes it with the
|
|
258
|
+
(`mason init demo`) the demo database is state too: `mason destroy` removes it with the synced data, and the next `mason start`
|
|
240
259
|
seeds it again.
|
|
241
260
|
|
|
242
261
|
## Requirements
|
|
@@ -245,7 +264,7 @@ seeds it again.
|
|
|
245
264
|
|
|
246
265
|
- Node 22.13 or later. The SQL parser (`libpg-query`), the catalog (`PGlite`) and the engine (DuckDB) ship as
|
|
247
266
|
WebAssembly or prebuilt binaries: no native build, no Python or compiler needed.
|
|
248
|
-
- Memory: the
|
|
267
|
+
- Memory: the engine may use half the container's memory limit by default, else half the machine's RAM
|
|
249
268
|
(`MASON_DUCKDB_MEMORY=4GB` sets it; `mason doctor` says it). Sizing a server:
|
|
250
269
|
[deploy/mason-data/README.md#memory](https://github.com/embeddable-hq/mason-data/blob/main/deploy/mason-data/README.md#memory).
|
|
251
270
|
- Postgres 13 or later. Each table needs a primary key, or a `key:` in the sources file (polling only).
|
|
@@ -256,14 +275,14 @@ seeds it again.
|
|
|
256
275
|
project's `.mason/config.json`, which a browser cannot send — or, for its web page (`mason ui`), the cookie the run's token is traded for on the first visit (HttpOnly, SameSite=Strict, its Host and every change's Origin checked). The Postgres API takes a user's password (`mason user`): a
|
|
257
276
|
MAC over its name keyed by the project's API key, so rotating the key removes every user at once. Off loopback the Postgres API is TLS or refused.
|
|
258
277
|
|
|
259
|
-
What a project keeps on this machine (the database URL, the API key, the run's token, the
|
|
278
|
+
What a project keeps on this machine (the database URL, the API key, the run's token, the synced data, its log and request
|
|
260
279
|
log) is in its `.mason/`, readable by your user only and gitignored by `mason init`.
|
|
261
280
|
|
|
262
281
|
## In production
|
|
263
282
|
|
|
264
283
|
Only the project folder travels — `mason/`, with `mason.yml` naming the Mason version it is written for. Production runs
|
|
265
284
|
it as one container, `ghcr.io/embeddable-hq/mason-data`, with `mason serve`: the same run, configured by the environment,
|
|
266
|
-
its
|
|
285
|
+
its synced data on one volume, each table read from Postgres's change log or polled as its `sync:` says. `mason help
|
|
267
286
|
production` is the road, offline; `deploy/mason-data/` in the repo is the Compose file and its walkthrough.
|
|
268
287
|
|
|
269
288
|
Apache-2.0.
|
package/dist/src/api/error.js
CHANGED
|
@@ -16,3 +16,6 @@ export class ApiError extends Error {
|
|
|
16
16
|
this.code = code;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
/** The code of a DISABLED `mason user`'s refused login or session (src/duck/users.ts): the listener answers it 28000, as
|
|
20
|
+
* Postgres answers a role without LOGIN, never 28P01 — its password was right. */
|
|
21
|
+
export const USER_DISABLED = "user_disabled";
|