dzql 0.11.0 → 0.12.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/package.json +4 -3
- package/src/cli/index.ts +2 -2
- package/src/compile/rows.ts +10 -2
- package/src/postgres/auth-link.ts +53 -0
- package/src/postgres/cli.ts +44 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dzql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "delta's compiled Postgres step: an app's delta definitions compiled to SQL",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"./postgres": "./src/postgres/index.ts",
|
|
19
19
|
"./pglite": "./src/postgres/pglite.ts",
|
|
20
20
|
"./auth-jwt": "./src/postgres/auth-jwt.ts",
|
|
21
|
+
"./auth-link": "./src/postgres/auth-link.ts",
|
|
21
22
|
"./compile": "./src/compile/opens.ts"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
@@ -63,11 +64,11 @@
|
|
|
63
64
|
"pinia": "^3.0.4",
|
|
64
65
|
"typescript": "^5.0.0",
|
|
65
66
|
"vue": "^3.5.26",
|
|
66
|
-
"@blueshed/delta": "^1.
|
|
67
|
+
"@blueshed/delta": "^1.1.0"
|
|
67
68
|
},
|
|
68
69
|
"peerDependencies": {
|
|
69
70
|
"typescript": "^5.0.0",
|
|
70
|
-
"@blueshed/delta": "^1.
|
|
71
|
+
"@blueshed/delta": "^1.1.0"
|
|
71
72
|
},
|
|
72
73
|
"engines": {
|
|
73
74
|
"bun": ">=1.0.0"
|
package/src/cli/index.ts
CHANGED
|
@@ -15,8 +15,8 @@ const VERSION = packageJson.version;
|
|
|
15
15
|
|
|
16
16
|
const args = process.argv.slice(2);
|
|
17
17
|
|
|
18
|
-
// delta's Postgres commands (dzql is delta's Postgres step): sql, init, migrate
|
|
19
|
-
if (["sql", "init", "migrate"].includes(args[0] ?? "")) {
|
|
18
|
+
// delta's Postgres commands (dzql is delta's Postgres step): sql, init, migrate, link
|
|
19
|
+
if (["sql", "init", "migrate", "link"].includes(args[0] ?? "")) {
|
|
20
20
|
const { main } = await import("../postgres/cli.ts");
|
|
21
21
|
await main(args);
|
|
22
22
|
process.exit(0);
|
package/src/compile/rows.ts
CHANGED
|
@@ -44,8 +44,16 @@ RETURNS JSONB AS $$
|
|
|
44
44
|
DECLARE
|
|
45
45
|
v_row JSONB;
|
|
46
46
|
BEGIN
|
|
47
|
-
${dispatch(schema, (_, t) =>
|
|
48
|
-
|
|
47
|
+
${dispatch(schema, (_, t) => {
|
|
48
|
+
const standing = `t.id = p_id${t.temporal ? " AND t.valid_to IS NULL" : ""}`;
|
|
49
|
+
// A write that waited on another's lock finds the row it waited on stored again (a new version,
|
|
50
|
+
// or put again): the row it locked is not the standing one. Asked afresh (each statement sees what
|
|
51
|
+
// has committed since), a standing row is locked in its turn; none standing is not there.
|
|
52
|
+
return `LOOP
|
|
53
|
+
SELECT to_jsonb(t.*) INTO v_row FROM ${q(t.name)} t WHERE ${standing} FOR UPDATE;
|
|
54
|
+
IF v_row IS NOT NULL OR NOT EXISTS (SELECT 1 FROM ${q(t.name)} t WHERE ${standing}) THEN RETURN v_row; END IF;
|
|
55
|
+
END LOOP;`;
|
|
56
|
+
}, noCollection("p_coll"))}
|
|
49
57
|
END;
|
|
50
58
|
$$ LANGUAGE plpgsql;`;
|
|
51
59
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link sign-in on Postgres: the `LinkStore` delta's `auth-link` keeps its links and sessions in
|
|
3
|
+
* (`@blueshed/delta/auth-link`; Workshop's docs/link-sign-in-plan.md). The tables are its own,
|
|
4
|
+
* `_auth_links` and `_auth_sessions`, beside delta's `_delta_*`: hashes, the person's id, times. A
|
|
5
|
+
* link is used in one statement, so two taps at once make one session, on one server or several.
|
|
6
|
+
*
|
|
7
|
+
* import { linkAuth } from "@blueshed/delta/auth-link";
|
|
8
|
+
* import { postgresLinkStore } from "dzql/auth-link";
|
|
9
|
+
*
|
|
10
|
+
* const auth = linkAuth({ store: await postgresLinkStore(pool), personOf, people });
|
|
11
|
+
*/
|
|
12
|
+
import type { Pool } from "pg";
|
|
13
|
+
import type { KeptLink, KeptSession, LinkStore } from "@blueshed/delta/auth-link";
|
|
14
|
+
|
|
15
|
+
export { linkAuth, makeLink } from "@blueshed/delta/auth-link";
|
|
16
|
+
|
|
17
|
+
/** The links and sessions in Postgres, in tables of their own, made here (once: IF NOT EXISTS). */
|
|
18
|
+
export async function postgresLinkStore(pool: Pool): Promise<LinkStore> {
|
|
19
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS _auth_links (hash TEXT PRIMARY KEY, person BIGINT NOT NULL, opens TEXT, expires TIMESTAMPTZ NOT NULL, used TIMESTAMPTZ)`);
|
|
20
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS _auth_sessions (hash TEXT PRIMARY KEY, person BIGINT NOT NULL, expires TIMESTAMPTZ NOT NULL)`);
|
|
21
|
+
// Times as delta's store has them: ISO strings; a person's id a number (pg reads BIGINT as text)
|
|
22
|
+
const iso = (t: Date | string | null) => (t === null ? null : new Date(t).toISOString());
|
|
23
|
+
const SESSION = "SELECT person::int AS person, expires FROM _auth_sessions WHERE hash = $1";
|
|
24
|
+
return {
|
|
25
|
+
async addLink(hash, l) {
|
|
26
|
+
await pool.query("INSERT INTO _auth_links (hash, person, opens, expires) VALUES ($1, $2, $3, $4)", [hash, l.person, l.opens, l.expires]);
|
|
27
|
+
},
|
|
28
|
+
async useLink(hash, now) {
|
|
29
|
+
const { rows } = await pool.query(
|
|
30
|
+
"UPDATE _auth_links SET used = $1 WHERE hash = $2 AND used IS NULL AND expires > $1 RETURNING person::int AS person, opens",
|
|
31
|
+
[now, hash],
|
|
32
|
+
);
|
|
33
|
+
return rows[0] ?? null;
|
|
34
|
+
},
|
|
35
|
+
async link(hash): Promise<KeptLink | null> {
|
|
36
|
+
const { rows } = await pool.query("SELECT person::int AS person, opens, expires, used FROM _auth_links WHERE hash = $1", [hash]);
|
|
37
|
+
return rows[0] ? { ...rows[0], expires: iso(rows[0].expires)!, used: iso(rows[0].used) } : null;
|
|
38
|
+
},
|
|
39
|
+
async addSession(hash, s) {
|
|
40
|
+
await pool.query("INSERT INTO _auth_sessions (hash, person, expires) VALUES ($1, $2, $3)", [hash, s.person, s.expires]);
|
|
41
|
+
},
|
|
42
|
+
async session(hash): Promise<KeptSession | null> {
|
|
43
|
+
const { rows } = await pool.query(SESSION, [hash]);
|
|
44
|
+
return rows[0] ? { person: rows[0].person, expires: iso(rows[0].expires)! } : null;
|
|
45
|
+
},
|
|
46
|
+
async renewSession(hash, expires) {
|
|
47
|
+
await pool.query("UPDATE _auth_sessions SET expires = $1 WHERE hash = $2", [expires, hash]);
|
|
48
|
+
},
|
|
49
|
+
async endSession(hash) {
|
|
50
|
+
await pool.query("DELETE FROM _auth_sessions WHERE hash = $1", [hash]);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/postgres/cli.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* dzql's Postgres commands, as delta's CLI had them: `dzql sql`, `dzql init
|
|
4
|
-
*
|
|
5
|
-
* `dzql <command>` (src/cli/index.ts hands these
|
|
3
|
+
* dzql's Postgres commands, as delta's CLI had them: `dzql sql`, `dzql init`,
|
|
4
|
+
* `dzql migrate` (a postgres:// URL or a PGlite directory), and `dzql link`. Run through
|
|
5
|
+
* `dzql <command>` (src/cli/index.ts hands these four here).
|
|
6
6
|
*/
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
import { resolve, basename, join, dirname } from "node:path";
|
|
@@ -26,6 +26,8 @@ const USAGE =
|
|
|
26
26
|
` dzql init <dir> [--with-auth] [--upgrade]\n` +
|
|
27
27
|
` dzql migrate <database> [--app <name>] [--dir <migrations>] [--status] [--json]\n` +
|
|
28
28
|
` <database>: a PGlite directory or a postgres:// URL; --dir defaults to ./migrations\n` +
|
|
29
|
+
` dzql link <database> <person> [--origin <url>] [--opens <path>] [--json]\n` +
|
|
30
|
+
` a link to sign in as someone (link sign-in): the first person's, on the server\n` +
|
|
29
31
|
`\n` +
|
|
30
32
|
`Exit codes: 0 done, 1 an error, 2 a bad argument.\n`;
|
|
31
33
|
|
|
@@ -299,6 +301,41 @@ async function cmdMigrate(target: string | undefined, values: Record<string, unk
|
|
|
299
301
|
if (!r.fresh && r.ran.length === 0) process.stdout.write("nothing to run\n");
|
|
300
302
|
}
|
|
301
303
|
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// dzql link
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* A link to sign in as `person` (link sign-in, `dzql/auth-link`), made in the app's Postgres: how
|
|
310
|
+
* the first person signs in, before anyone can send them one. The site is --origin, else ORIGIN,
|
|
311
|
+
* else Railway's domain. The database as `migrate` takes it. A SQLite file is delta's (`delta link`).
|
|
312
|
+
*/
|
|
313
|
+
async function cmdLink(target: string | undefined, person: string | undefined, values: Record<string, unknown>) {
|
|
314
|
+
if (!target || !person) usageError("link takes a <database> and a <person>: dzql link postgres://… 1", true);
|
|
315
|
+
if (!/^\d+$/.test(person)) usageError(`<person> is a person's id, a number: ${person}`);
|
|
316
|
+
const isUrl = /^postgres(ql)?:\/\//.test(target);
|
|
317
|
+
const path = resolve(process.cwd(), target);
|
|
318
|
+
if (!isUrl && (!existsSync(path) || !statSync(path).isDirectory())) usageError(`no PGlite database at ${target} (a SQLite file is delta's: bunx @blueshed/delta link)`);
|
|
319
|
+
const { makeLink, siteOrigin } = await import("@blueshed/delta/auth-link");
|
|
320
|
+
let origin: string;
|
|
321
|
+
try { origin = (values.origin as string | undefined) ?? siteOrigin(); }
|
|
322
|
+
catch (err) { usageError(`${(err as Error).message}, or pass --origin`); }
|
|
323
|
+
const { postgresLinkStore } = await import("./auth-link");
|
|
324
|
+
let pool: any;
|
|
325
|
+
if (isUrl) {
|
|
326
|
+
const { Pool } = await import("pg");
|
|
327
|
+
pool = new Pool({ connectionString: target });
|
|
328
|
+
} else {
|
|
329
|
+
const { openPglite } = await import("./pglite");
|
|
330
|
+
pool = await openPglite(path);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const made = await makeLink(await postgresLinkStore(pool), Number(person), { origin, opens: values.opens as string | undefined });
|
|
334
|
+
if (json) return print(made);
|
|
335
|
+
process.stdout.write(`${made.url}\n(used once, until ${made.expires})\n`);
|
|
336
|
+
} finally { await pool.end(); }
|
|
337
|
+
}
|
|
338
|
+
|
|
302
339
|
// ---------------------------------------------------------------------------
|
|
303
340
|
// main
|
|
304
341
|
// ---------------------------------------------------------------------------
|
|
@@ -318,6 +355,8 @@ export async function main(argv: string[]) {
|
|
|
318
355
|
dir: { type: "string" },
|
|
319
356
|
app: { type: "string" },
|
|
320
357
|
status: { type: "boolean", default: false },
|
|
358
|
+
origin: { type: "string" },
|
|
359
|
+
opens: { type: "string" },
|
|
321
360
|
help: { type: "boolean", short: "h", default: false },
|
|
322
361
|
},
|
|
323
362
|
allowPositionals: true,
|
|
@@ -327,11 +366,12 @@ export async function main(argv: string[]) {
|
|
|
327
366
|
const { values, positionals } = parsed;
|
|
328
367
|
json = !!values.json;
|
|
329
368
|
if (values.help) { process.stdout.write(USAGE); return; }
|
|
330
|
-
const [cmd, arg] = positionals;
|
|
369
|
+
const [cmd, arg, arg2] = positionals;
|
|
331
370
|
switch (cmd) {
|
|
332
371
|
case "sql": return cmdSql(arg, values);
|
|
333
372
|
case "init": return cmdInit(arg, values);
|
|
334
373
|
case "migrate": return cmdMigrate(arg, values);
|
|
374
|
+
case "link": return cmdLink(arg, arg2, values);
|
|
335
375
|
default: usageError(cmd ? `no command ${cmd}` : "a command, please", true);
|
|
336
376
|
}
|
|
337
377
|
}
|