@mason-data/cli 0.6.2 → 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 +147 -69
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog.js +258 -14
- 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 +1290 -223
- package/dist/src/api/postgres/translate.js +81 -9
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/pg-wire.js +144 -38
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +55 -8
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +5 -3
- package/dist/src/cli/commands.js +843 -272
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/demo.js +158 -0
- package/dist/src/cli/dev.js +458 -125
- package/dist/src/cli/doctor.js +25 -12
- package/dist/src/cli/guides.js +103 -31
- package/dist/src/cli/init.js +159 -58
- package/dist/src/cli/machine.js +18 -12
- package/dist/src/cli/main.js +5 -5
- package/dist/src/cli/mcp.js +735 -0
- 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 +24 -14
- package/dist/src/cli/project.js +48 -7
- package/dist/src/cli/readiness.js +38 -4
- package/dist/src/cli/registry.js +57 -35
- 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 +498 -133
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +10 -8
- package/dist/src/cli/serve.js +147 -40
- package/dist/src/cli/share-run.js +286 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +168 -0
- package/dist/src/cli/sources.js +290 -39
- 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 +240 -0
- package/dist/src/demo/source.js +655 -0
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/distinct-key.js +228 -0
- package/dist/src/duck/events.js +134 -0
- package/dist/src/duck/executor-worker.js +25 -11
- package/dist/src/duck/executor.js +107 -53
- package/dist/src/duck/explain.js +158 -29
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +73 -19
- package/dist/src/duck/gaps.js +25 -11
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/history.js +123 -0
- package/dist/src/duck/jsonb-order.js +175 -0
- package/dist/src/duck/jsonb.js +10 -8
- package/dist/src/duck/kept-route.js +82 -2
- package/dist/src/duck/kept.js +211 -44
- package/dist/src/duck/pgtree.js +40 -0
- package/dist/src/duck/plan.js +120 -30
- package/dist/src/duck/publication.js +6 -6
- package/dist/src/duck/requests.js +265 -93
- package/dist/src/duck/rewrite.js +1193 -73
- package/dist/src/duck/row-filter.js +649 -0
- package/dist/src/duck/store.js +271 -159
- package/dist/src/duck/stream.js +10 -5
- package/dist/src/duck/sync.js +907 -275
- 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 +9 -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/lib/timezone.js +3 -2
- package/dist/src/web/app.css +706 -55
- package/dist/src/web/app.js +330 -258
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +84 -15
- 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/dist/src/cli/version.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* What this Mason is, and where that was read (what a person running it can act on follows from it: `mason serve`'s pin
|
|
3
|
+
* says the image to run, or the npm line): the package's version when installed; else the image's (`MASON_VERSION`,
|
|
4
|
+
* stamped at build by images-publish.yml: `v0.6.1` for a release, said `0.6.1` as the package says it, or `main-<sha>`);
|
|
5
|
+
* a checkout runs as "dev".
|
|
5
6
|
*/
|
|
6
7
|
import * as fs from "node:fs";
|
|
7
8
|
import * as path from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
export function
|
|
10
|
+
export function runningVersion() {
|
|
10
11
|
// src/cli/ in a checkout (the repo's package.json, which is not this one), dist/src/cli/ in the package
|
|
11
12
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
for (const p of [path.join(here, "..", "..", "package.json"), path.join(here, "..", "..", "..", "package.json")]) {
|
|
@@ -14,10 +15,11 @@ export function version() {
|
|
|
14
15
|
continue;
|
|
15
16
|
const pkg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
16
17
|
if (pkg.name === "@mason-data/cli" && pkg.version)
|
|
17
|
-
return pkg.version;
|
|
18
|
+
return { text: pkg.version, from: "package" };
|
|
18
19
|
}
|
|
19
20
|
const image = process.env.MASON_VERSION?.trim();
|
|
20
21
|
if (image)
|
|
21
|
-
return /^v\d+\.\d+\.\d+$/.test(image) ? image.slice(1) : image;
|
|
22
|
-
return "dev";
|
|
22
|
+
return { text: /^v\d+\.\d+\.\d+$/.test(image) ? image.slice(1) : image, from: "image" };
|
|
23
|
+
return { text: "dev", from: "checkout" };
|
|
23
24
|
}
|
|
25
|
+
export const version = () => runningVersion().text;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE DEMO DATABASE'S SCHEMA AND ROWS (`mason init demo`, src/demo/source.ts): a small web shop a BI user recognises at
|
|
3
|
+
* once — `customers`, `products`, `orders`, `order_items` — with primary and foreign keys declared, about 200 / 50 /
|
|
4
|
+
* 5,000 / 15,000 rows, and the orders' dates spread over the twelve months before the seed, so a time-window chart has
|
|
5
|
+
* data in every week.
|
|
6
|
+
*
|
|
7
|
+
* SYNTHETIC ONLY (CLAUDE.md "A real database's data and structures are confidential"): every name below is made up
|
|
8
|
+
* here, from short word lists, and every email is at example.com.
|
|
9
|
+
*
|
|
10
|
+
* DETERMINISTIC: one seeded PRNG (`mulberry32`) draws every value, so every demo holds the same rows; only the dates are
|
|
11
|
+
* anchored to the moment of the seed (`now`), so "the last 30 days" has orders whenever the demo was made. The rounds of
|
|
12
|
+
* changes (`changeSql`) draw from a PRNG seeded by the newest order's id, so they are the same sequence too, across
|
|
13
|
+
* restarts.
|
|
14
|
+
*
|
|
15
|
+
* WRITTEN AS A FEW MULTI-ROW INSERTs (`ROWS_PER_INSERT` a statement), never one per row: PGlite is slow per statement.
|
|
16
|
+
* Prices and totals are counted in integer cents and printed as decimals, so a total is exactly its items' sum.
|
|
17
|
+
*/
|
|
18
|
+
/** A small, fast, seeded PRNG (mulberry32): the same seed, the same sequence, on every machine. */
|
|
19
|
+
export function mulberry32(seed) {
|
|
20
|
+
let a = seed >>> 0;
|
|
21
|
+
return () => {
|
|
22
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
23
|
+
let t = a;
|
|
24
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
25
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
26
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** The demo database's name and its user: what its URL says, and what `current_database()` and `current_user` are. */
|
|
30
|
+
export const DEMO_DATABASE = "demo";
|
|
31
|
+
export const DEMO_USER = "demo";
|
|
32
|
+
/** `postgres://demo@127.0.0.1:<port>/demo`: the demo database's URL, its port the one fact that differs. */
|
|
33
|
+
export function demoUrl(port) {
|
|
34
|
+
return `postgres://${DEMO_USER}@127.0.0.1:${port}/${DEMO_DATABASE}`;
|
|
35
|
+
}
|
|
36
|
+
export const SEED_COUNTS = { customers: 200, products: 50, orders: 5_000 };
|
|
37
|
+
/** the database's four tables, in the order they are made (a foreign key's target first) */
|
|
38
|
+
export const DEMO_TABLES = ["customers", "products", "orders", "order_items"];
|
|
39
|
+
/**
|
|
40
|
+
* THE TABLE `mason init demo` LEAVES OUT of the sources file, for the person to add themselves: the first thing they do
|
|
41
|
+
* after `mason start` is a real `mason add` (or the web page's Add), and they see a copy begin. `order_items`, because
|
|
42
|
+
* its foreign keys' targets (`orders`, `products`) are among the ones added, so it joins cleanly when it arrives. The
|
|
43
|
+
* database holds it regardless, and the writer (`changeSql`) keeps inserting into it.
|
|
44
|
+
*/
|
|
45
|
+
export const DEMO_TABLE_TO_ADD = "order_items";
|
|
46
|
+
/** the tables `mason init demo` writes to the sources file: the database's, but the one left to add */
|
|
47
|
+
export const DEMO_ADDED_TABLES = DEMO_TABLES.filter((t) => t !== DEMO_TABLE_TO_ADD);
|
|
48
|
+
/** how the demo's tables are kept current (the sources file's top `sync:`, src/lib/table-sync.ts): polled every 5 s,
|
|
49
|
+
* so the demo shows a copy keeping up with its writer — where a project `mason init` makes is `off` */
|
|
50
|
+
export const DEMO_SYNC = { mode: "poll", every: "5s", seconds: 5 };
|
|
51
|
+
/** an order's status moves along this path (`changeSql`); `cancelled` is a side exit */
|
|
52
|
+
export const ORDER_STATUSES = ["pending", "paid", "shipped", "delivered"];
|
|
53
|
+
const ROWS_PER_INSERT = 1_000;
|
|
54
|
+
const DAY_MS = 86_400_000;
|
|
55
|
+
const FIRST = ["Ada", "Ben", "Cleo", "Dev", "Eli", "Fay", "Gus", "Hana", "Ivo", "Jun", "Kai", "Lea", "Milo", "Nia", "Otto",
|
|
56
|
+
"Pia", "Quin", "Rae", "Sol", "Tess", "Uma", "Vic", "Wren", "Xavi", "Yara", "Zed"];
|
|
57
|
+
const LAST = ["Abbott", "Brook", "Castell", "Dorn", "Ember", "Fallow", "Grove", "Hollis", "Ingram", "Juniper", "Keel",
|
|
58
|
+
"Lark", "Marsh", "Nettle", "Orchard", "Penn", "Quarry", "Rook", "Sorrel", "Thorne", "Vale", "Wilde"];
|
|
59
|
+
/** weighted: the first countries hold most customers, as a shop's do */
|
|
60
|
+
const COUNTRIES = ["United States", "United States", "United States", "United Kingdom", "United Kingdom", "Germany",
|
|
61
|
+
"Germany", "France", "Canada", "Australia", "Netherlands", "Spain", "Japan", "Brazil", "India"];
|
|
62
|
+
const CATEGORIES = {
|
|
63
|
+
Books: { nouns: ["Novel", "Cookbook", "Atlas", "Field Guide", "Notebook"], cents: [800, 4_500] },
|
|
64
|
+
Electronics: { nouns: ["Headphones", "Speaker", "Charger", "Keyboard", "Webcam"], cents: [1_500, 25_000] },
|
|
65
|
+
Home: { nouns: ["Lamp", "Rug", "Mug", "Throw", "Vase"], cents: [1_200, 12_000] },
|
|
66
|
+
Garden: { nouns: ["Trowel", "Planter", "Hose", "Seed Kit", "Bird Feeder"], cents: [900, 8_000] },
|
|
67
|
+
Sports: { nouns: ["Yoga Mat", "Water Bottle", "Jump Rope", "Backpack", "Cap"], cents: [1_000, 9_000] },
|
|
68
|
+
};
|
|
69
|
+
const ADJECTIVES = ["Oak", "Slate", "Cedar", "Coral", "Indigo", "Amber", "Birch", "Moss", "Pebble", "Saffron"];
|
|
70
|
+
/** a SQL string literal (the seed's own words only, but quoted properly all the same) */
|
|
71
|
+
const lit = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
72
|
+
const money = (cents) => `${Math.floor(cents / 100)}.${String(cents % 100).padStart(2, "0")}`;
|
|
73
|
+
const stamp = (ms) => lit(new Date(ms).toISOString());
|
|
74
|
+
/** `INSERT INTO t (cols) VALUES …` in statements of ROWS_PER_INSERT rows */
|
|
75
|
+
function inserts(table, columns, rows) {
|
|
76
|
+
const out = [];
|
|
77
|
+
for (let i = 0; i < rows.length; i += ROWS_PER_INSERT) {
|
|
78
|
+
out.push(`INSERT INTO ${table} (${columns}) VALUES\n${rows.slice(i, i + ROWS_PER_INSERT).join(",\n")}`);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
const DDL = `CREATE TABLE customers (
|
|
83
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
84
|
+
name text NOT NULL,
|
|
85
|
+
email text NOT NULL UNIQUE,
|
|
86
|
+
country text NOT NULL,
|
|
87
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
88
|
+
);
|
|
89
|
+
CREATE TABLE products (
|
|
90
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
91
|
+
name text NOT NULL,
|
|
92
|
+
category text NOT NULL,
|
|
93
|
+
price numeric(10,2) NOT NULL
|
|
94
|
+
);
|
|
95
|
+
CREATE TABLE orders (
|
|
96
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
97
|
+
customer_id bigint NOT NULL REFERENCES customers (id),
|
|
98
|
+
status text NOT NULL,
|
|
99
|
+
total numeric(12,2) NOT NULL,
|
|
100
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
101
|
+
);
|
|
102
|
+
CREATE TABLE order_items (
|
|
103
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
104
|
+
order_id bigint NOT NULL REFERENCES orders (id),
|
|
105
|
+
product_id bigint NOT NULL REFERENCES products (id),
|
|
106
|
+
quantity integer NOT NULL,
|
|
107
|
+
unit_price numeric(10,2) NOT NULL
|
|
108
|
+
);
|
|
109
|
+
CREATE INDEX order_items_order_id ON order_items (order_id);
|
|
110
|
+
CREATE INDEX orders_customer_id ON orders (customer_id);
|
|
111
|
+
COMMENT ON TABLE orders IS 'One row per checkout; total is the sum of its order_items';`;
|
|
112
|
+
/** The product catalogue: its prices in cents, by id (1-based), which the orders' items read. */
|
|
113
|
+
function products(rand) {
|
|
114
|
+
const names = Object.keys(CATEGORIES);
|
|
115
|
+
const out = [];
|
|
116
|
+
for (let i = 0; i < SEED_COUNTS.products; i++) {
|
|
117
|
+
const category = names[i % names.length];
|
|
118
|
+
const c = CATEGORIES[category];
|
|
119
|
+
const noun = c.nouns[Math.floor(i / names.length) % c.nouns.length];
|
|
120
|
+
const adjective = ADJECTIVES[Math.floor(i / (names.length * c.nouns.length)) % ADJECTIVES.length];
|
|
121
|
+
// a price ending in 99 or 49, as a shop's do
|
|
122
|
+
const cents = Math.floor((c.cents[0] + rand() * (c.cents[1] - c.cents[0])) / 100) * 100 + (rand() < 0.7 ? 99 : 49);
|
|
123
|
+
out.push({ name: `${adjective} ${noun}`, category, cents });
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** The status of an order placed `ageDays` ago: the old ones delivered (some cancelled), the newest still moving. */
|
|
128
|
+
function statusOf(ageDays, rand) {
|
|
129
|
+
if (rand() < 0.04)
|
|
130
|
+
return "cancelled";
|
|
131
|
+
if (ageDays > 10)
|
|
132
|
+
return "delivered";
|
|
133
|
+
if (ageDays > 5)
|
|
134
|
+
return rand() < 0.6 ? "delivered" : "shipped";
|
|
135
|
+
if (ageDays > 2)
|
|
136
|
+
return rand() < 0.5 ? "shipped" : "paid";
|
|
137
|
+
return rand() < 0.5 ? "paid" : "pending";
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The whole seed, as statements to run in order in ONE transaction: the tables, their rows, the identity sequences moved
|
|
141
|
+
* past the ids written, and ANALYZE (run by the caller, outside the transaction) so the catalog's row estimates are the
|
|
142
|
+
* counts. `now` anchors the dates: the orders span the 365 days before it.
|
|
143
|
+
*/
|
|
144
|
+
export function seedStatements(now) {
|
|
145
|
+
const rand = mulberry32(20_261_027);
|
|
146
|
+
const end = now.getTime();
|
|
147
|
+
const start = end - 365 * DAY_MS;
|
|
148
|
+
const customerRows = [];
|
|
149
|
+
const customerSince = [];
|
|
150
|
+
for (let id = 1; id <= SEED_COUNTS.customers; id++) {
|
|
151
|
+
const first = FIRST[Math.floor(rand() * FIRST.length)];
|
|
152
|
+
const last = LAST[Math.floor(rand() * LAST.length)];
|
|
153
|
+
const country = COUNTRIES[Math.floor(rand() * COUNTRIES.length)];
|
|
154
|
+
// customers sign up over the year before the first orders and the year itself
|
|
155
|
+
const since = start - 180 * DAY_MS + Math.floor(rand() * 500 * DAY_MS);
|
|
156
|
+
customerSince.push(Math.min(since, end - DAY_MS));
|
|
157
|
+
customerRows.push(`(${id}, ${lit(`${first} ${last}`)}, ${lit(`${first}.${last}.${id}@example.com`.toLowerCase())}, ${lit(country)}, ${stamp(customerSince[id - 1])})`);
|
|
158
|
+
}
|
|
159
|
+
const catalogue = products(rand);
|
|
160
|
+
const productRows = catalogue.map((p, i) => `(${i + 1}, ${lit(p.name)}, ${lit(p.category)}, ${money(p.cents)})`);
|
|
161
|
+
// the orders' times: more of them as the year goes on (a growing shop), sorted so ids follow time, as an app's do
|
|
162
|
+
const times = [];
|
|
163
|
+
for (let i = 0; i < SEED_COUNTS.orders; i++)
|
|
164
|
+
times.push(start + Math.floor(Math.sqrt(rand()) * (end - start - 60_000)));
|
|
165
|
+
times.sort((a, b) => a - b);
|
|
166
|
+
const orderRows = [];
|
|
167
|
+
const itemRows = [];
|
|
168
|
+
let itemId = 0;
|
|
169
|
+
for (let i = 0; i < times.length; i++) {
|
|
170
|
+
const id = i + 1;
|
|
171
|
+
const at = times[i];
|
|
172
|
+
// a customer who had signed up by then; a few customers order far more often (the square skews to low ids)
|
|
173
|
+
let customer = 1 + Math.floor(rand() * rand() * SEED_COUNTS.customers);
|
|
174
|
+
if (customerSince[customer - 1] > at)
|
|
175
|
+
customer = 1 + Math.floor(rand() * SEED_COUNTS.customers);
|
|
176
|
+
const lines = 1 + Math.floor(rand() * 5); // 1–5 items, about 3 on average
|
|
177
|
+
let total = 0;
|
|
178
|
+
for (let l = 0; l < lines; l++) {
|
|
179
|
+
const product = 1 + Math.floor(rand() * catalogue.length);
|
|
180
|
+
const quantity = rand() < 0.75 ? 1 : 2 + Math.floor(rand() * 3);
|
|
181
|
+
const cents = catalogue[product - 1].cents;
|
|
182
|
+
total += quantity * cents;
|
|
183
|
+
itemRows.push(`(${++itemId}, ${id}, ${product}, ${quantity}, ${money(cents)})`);
|
|
184
|
+
}
|
|
185
|
+
orderRows.push(`(${id}, ${customer}, ${lit(statusOf((end - at) / DAY_MS, rand))}, ${money(total)}, ${stamp(at)})`);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
ddl: DDL,
|
|
189
|
+
rows: [
|
|
190
|
+
...inserts("customers", "id, name, email, country, created_at", customerRows),
|
|
191
|
+
...inserts("products", "id, name, category, price", productRows),
|
|
192
|
+
...inserts("orders", "id, customer_id, status, total, created_at", orderRows),
|
|
193
|
+
...inserts("order_items", "id, order_id, product_id, quantity, unit_price", itemRows),
|
|
194
|
+
],
|
|
195
|
+
// the identity columns continue past the ids written, so a later insert (`changeSql`) takes the next one
|
|
196
|
+
after: DEMO_TABLES.map((t) => `SELECT setval(pg_get_serial_sequence('${t}', 'id'), (SELECT max(id) FROM ${t}))`),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** What one round of changes reads first: the newest order's id, the products' prices, and the oldest order still moving. */
|
|
200
|
+
export const CHANGE_STATE_SQL = `SELECT (SELECT coalesce(max(id), 0) FROM orders)::text AS newest,
|
|
201
|
+
(SELECT count(*) FROM customers)::text AS customers,
|
|
202
|
+
(SELECT coalesce(json_agg(json_build_array(id, (price * 100)::bigint) ORDER BY id), '[]'::json)::text FROM products) AS prices,
|
|
203
|
+
(SELECT json_build_array(id, status)::text FROM orders WHERE status IN ('pending', 'paid', 'shipped') ORDER BY created_at, id LIMIT 1) AS moving`;
|
|
204
|
+
/**
|
|
205
|
+
* ONE ROUND OF CHANGES, as one transaction's statements: a new order (now, `pending`) with 1–4 items and its total, and
|
|
206
|
+
* the oldest order still moving moved one step on its path (pending → paid → shipped → delivered). Its values are drawn
|
|
207
|
+
* from a PRNG seeded by the newest order's id, so the rounds are the same sequence on every demo. Answers the statements
|
|
208
|
+
* (the new order's id read back last, as `id`) and what they did, in one line, given that id.
|
|
209
|
+
*/
|
|
210
|
+
export function changeSql(s) {
|
|
211
|
+
const rand = mulberry32(s.newest * 7_919 + 17);
|
|
212
|
+
const customer = 1 + Math.floor(rand() * Math.max(1, s.customers));
|
|
213
|
+
const lines = 1 + Math.floor(rand() * 4);
|
|
214
|
+
const items = [];
|
|
215
|
+
for (let l = 0; l < lines && s.prices.length > 0; l++) {
|
|
216
|
+
const [product, cents] = s.prices[Math.floor(rand() * s.prices.length)];
|
|
217
|
+
items.push({ product, quantity: rand() < 0.75 ? 1 : 2, cents });
|
|
218
|
+
}
|
|
219
|
+
const total = items.reduce((sum, i) => sum + i.quantity * i.cents, 0);
|
|
220
|
+
const statements = [
|
|
221
|
+
// the order's id is its identity's next, read back by the items through currval
|
|
222
|
+
`INSERT INTO orders (customer_id, status, total, created_at) VALUES (${customer}, 'pending', ${money(total)}, now())`,
|
|
223
|
+
...(items.length === 0 ? [] : [`INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES ` +
|
|
224
|
+
items.map((i) => `(currval(pg_get_serial_sequence('orders', 'id')), ${i.product}, ${i.quantity}, ${money(i.cents)})`).join(", ")]),
|
|
225
|
+
];
|
|
226
|
+
let moved = "";
|
|
227
|
+
if (s.moving !== null) {
|
|
228
|
+
const [id, status] = s.moving;
|
|
229
|
+
const next = ORDER_STATUSES[ORDER_STATUSES.indexOf(status) + 1];
|
|
230
|
+
if (next !== undefined) {
|
|
231
|
+
statements.push(`UPDATE orders SET status = ${lit(next)} WHERE id = ${id}`);
|
|
232
|
+
moved = `; order ${id} ${status} → ${next}`;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
statements.push(`SELECT currval(pg_get_serial_sequence('orders', 'id'))::text AS id`);
|
|
236
|
+
return {
|
|
237
|
+
statements,
|
|
238
|
+
said: (orderId) => `order ${orderId} placed by customer ${customer} (${items.length} item${items.length === 1 ? "" : "s"}, ${money(total)})${moved}`,
|
|
239
|
+
};
|
|
240
|
+
}
|