@homeflare/alchemy 0.22.0 → 0.24.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 +15 -1
- package/dist/litellm/client.d.ts +76 -0
- package/dist/litellm/client.d.ts.map +1 -0
- package/dist/litellm/credentials.d.ts +48 -0
- package/dist/litellm/credentials.d.ts.map +1 -0
- package/dist/litellm/fake-litellm.d.ts +36 -0
- package/dist/litellm/fake-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-stack.d.ts +30 -0
- package/dist/litellm/fake-stack.d.ts.map +1 -0
- package/dist/litellm/generated/pass-through.d.ts +71 -0
- package/dist/litellm/generated/pass-through.d.ts.map +1 -0
- package/dist/litellm/index.d.ts +13 -0
- package/dist/litellm/index.d.ts.map +1 -0
- package/dist/litellm/index.js +270 -0
- package/dist/litellm/index.js.map +14 -0
- package/dist/litellm/pass-through-endpoint.d.ts +94 -0
- package/dist/litellm/pass-through-endpoint.d.ts.map +1 -0
- package/dist/litellm/pass-through-form.d.ts +40 -0
- package/dist/litellm/pass-through-form.d.ts.map +1 -0
- package/dist/litellm/providers.d.ts +16 -0
- package/dist/litellm/providers.d.ts.map +1 -0
- package/dist/postgres/connection.d.ts +50 -0
- package/dist/postgres/connection.d.ts.map +1 -0
- package/dist/postgres/database-attrs.d.ts +79 -0
- package/dist/postgres/database-attrs.d.ts.map +1 -0
- package/dist/postgres/database-sql.d.ts +73 -0
- package/dist/postgres/database-sql.d.ts.map +1 -0
- package/dist/postgres/database.d.ts +63 -0
- package/dist/postgres/database.d.ts.map +1 -0
- package/dist/postgres/errors.d.ts +86 -0
- package/dist/postgres/errors.d.ts.map +1 -0
- package/dist/postgres/fake-sql.d.ts +22 -0
- package/dist/postgres/fake-sql.d.ts.map +1 -0
- package/dist/postgres/index.d.ts +16 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +238 -0
- package/dist/postgres/index.js.map +15 -0
- package/dist/postgres/providers.d.ts +14 -0
- package/dist/postgres/providers.d.ts.map +1 -0
- package/dist/proxmox/api-token-form.d.ts +12 -1
- package/dist/proxmox/api-token-form.d.ts.map +1 -1
- package/dist/proxmox/api-token.d.ts +8 -3
- package/dist/proxmox/api-token.d.ts.map +1 -1
- package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts +13 -13
- package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts.map +1 -1
- package/dist/proxmox/index.d.ts +9 -2
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +17 -9
- package/dist/proxmox/index.js.map +8 -7
- package/dist/proxmox/zfs-pool-spec.d.ts +27 -0
- package/dist/proxmox/zfs-pool-spec.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-write.d.ts +48 -7
- package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool.d.ts +52 -22
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/dist/release/archive.d.ts +6 -2
- package/dist/release/archive.d.ts.map +1 -1
- package/dist/release/binary-form.d.ts +14 -9
- package/dist/release/binary-form.d.ts.map +1 -1
- package/dist/release/binary-pins.d.ts +17 -0
- package/dist/release/binary-pins.d.ts.map +1 -0
- package/dist/release/catalog.d.ts.map +1 -1
- package/dist/release/index.js +90 -40
- package/dist/release/index.js.map +8 -7
- package/dist/release/tar.d.ts +9 -2
- package/dist/release/tar.d.ts.map +1 -1
- package/docs/adopted-deploys.md +14 -0
- package/docs/litellm.md +110 -0
- package/docs/peers.md +8 -1
- package/docs/postgres.md +125 -0
- package/docs/release-binary-catalogs.md +40 -4
- package/docs/release-binary-refusals.md +22 -8
- package/docs/release-binary-upstream.md +33 -15
- package/package.json +10 -1
- package/src/litellm/client.ts +195 -0
- package/src/litellm/credentials.ts +73 -0
- package/src/litellm/fake-litellm.ts +114 -0
- package/src/litellm/fake-stack.ts +100 -0
- package/src/litellm/generated/pass-through.ts +73 -0
- package/src/litellm/index.ts +42 -0
- package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +66 -0
- package/src/litellm/pass-through-endpoint-replace.test.ts +84 -0
- package/src/litellm/pass-through-endpoint.test.ts +208 -0
- package/src/litellm/pass-through-endpoint.ts +258 -0
- package/src/litellm/pass-through-form.test.ts +128 -0
- package/src/litellm/pass-through-form.ts +136 -0
- package/src/litellm/providers.ts +19 -0
- package/src/postgres/connection.ts +62 -0
- package/src/postgres/database-attrs.ts +87 -0
- package/src/postgres/database-bigint-serialization.test.ts +140 -0
- package/src/postgres/database-lifecycle.test.ts +113 -0
- package/src/postgres/database-provenance.test.ts +118 -0
- package/src/postgres/database-refusals.test.ts +135 -0
- package/src/postgres/database-sql.ts +174 -0
- package/src/postgres/database.ts +193 -0
- package/src/postgres/errors.ts +131 -0
- package/src/postgres/fake-sql.ts +123 -0
- package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +26 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +67 -0
- package/src/postgres/index.ts +30 -0
- package/src/postgres/providers.ts +16 -0
- package/src/proxmox/api-token-adopt.test.ts +138 -0
- package/src/proxmox/api-token-form.ts +19 -2
- package/src/proxmox/api-token.ts +8 -3
- package/src/proxmox/generated/pve/cluster-sdn-fabrics.ts +23 -13
- package/src/proxmox/index.test.ts +5 -2
- package/src/proxmox/index.ts +9 -2
- package/src/proxmox/zfs-pool-adopt.test.ts +141 -0
- package/src/proxmox/zfs-pool-spec.ts +76 -0
- package/src/proxmox/zfs-pool-write.ts +63 -9
- package/src/proxmox/zfs-pool.ts +55 -97
- package/src/release/archive.ts +10 -4
- package/src/release/binary-form.ts +24 -76
- package/src/release/binary-pins.ts +95 -0
- package/src/release/binary-root.test.ts +122 -0
- package/src/release/catalog.ts +10 -2
- package/src/release/tar-root.test.ts +114 -0
- package/src/release/tar.ts +78 -12
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Postgres.Database`'s failures as typed tags (S21).
|
|
3
|
+
*
|
|
4
|
+
* ⛔ NONE OF THESE IS A STATUS-CODE OR MESSAGE MATCH. Every one is raised by this family's own
|
|
5
|
+
* code from a fact it already checked (a byte count, a live catalog row, a role lookup) — never
|
|
6
|
+
* from sniffing a driver error's text. The one live-SQLSTATE check this family makes —
|
|
7
|
+
* `isDuplicateDatabaseRace` in `database-sql.ts`, for the `42P04` create race — reads the raw
|
|
8
|
+
* code `@effect/sql-pg` classified onto `SqlSyntaxError.cause.code` (measured in
|
|
9
|
+
* `node_modules/@effect/sql-pg/src/internal/sqlError.ts#classifySqlState`: every `42xxx` code,
|
|
10
|
+
* `42P04` included, becomes `SqlSyntaxError` — there is no more specific tag for it), and it
|
|
11
|
+
* never produces one of these tags: a race converges instead of failing.
|
|
12
|
+
*/
|
|
13
|
+
import * as Data from 'effect/Data';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A declared name would be silently truncated by the server (`NAMEDATALEN` 64, so 63 UTF-8
|
|
17
|
+
* bytes is the last one that survives whole) — refused at plan, before any statement runs.
|
|
18
|
+
*
|
|
19
|
+
* ⛔ WHY THIS IS A PLAN-TIME REFUSAL AND NOT A SERVER ERROR. `truncate_identifier`
|
|
20
|
+
* (`scansup.c@REL_18_6` line 93) clips a long name to 63 bytes and raises only a `NOTICE`
|
|
21
|
+
* (`errcode(ERRCODE_NAME_TOO_LONG)`), never an error. A create would succeed against the
|
|
22
|
+
* TRUNCATED name, so the next plan's read (`WHERE datname = $1`, the name exactly as
|
|
23
|
+
* declared) would find nothing and try to create it again — forever. See
|
|
24
|
+
* `doc/postgres.md#name`.
|
|
25
|
+
*/
|
|
26
|
+
export class PostgresDatabaseNameRefused extends Data.TaggedError('PostgresDatabaseNameRefused')<{
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly byteLength: number;
|
|
29
|
+
readonly limit: number;
|
|
30
|
+
}> {
|
|
31
|
+
override get message(): string {
|
|
32
|
+
return (
|
|
33
|
+
`Postgres.Database "${this.name}": ${String(this.byteLength)} UTF-8 bytes, over the ` +
|
|
34
|
+
`${String(this.limit)}-byte limit (NAMEDATALEN 64, minus the terminator). PostgreSQL would ` +
|
|
35
|
+
'accept the CREATE and silently truncate the name with only a NOTICE, so this plan refuses ' +
|
|
36
|
+
'it instead of creating a database no later read will ever find again.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A logical id's `name` changed. A database's name is its identity; there is no `ALTER … RENAME`
|
|
42
|
+
* in this family's vocabulary (see `docs/postgres.md`), and `diff` never turns this into a
|
|
43
|
+
* `replace` — a replace here is `DROP DATABASE` before `CREATE DATABASE`, on data. */
|
|
44
|
+
export class PostgresDatabaseRenameRefused extends Data.TaggedError(
|
|
45
|
+
'PostgresDatabaseRenameRefused',
|
|
46
|
+
)<{
|
|
47
|
+
readonly from: string;
|
|
48
|
+
readonly to: string;
|
|
49
|
+
}> {
|
|
50
|
+
override get message(): string {
|
|
51
|
+
return (
|
|
52
|
+
`Postgres.Database: the declared name changed from "${this.from}" to "${this.to}". A ` +
|
|
53
|
+
'rename is refused at plan — this provider never replaces a database (that is DROP then ' +
|
|
54
|
+
'CREATE) and never runs ALTER … RENAME. Declare a new logical id for the new name, and ' +
|
|
55
|
+
'remove the old one once its data has moved.'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `OWNER` names a role `pg_roles` does not have, checked before the `CREATE DATABASE` that
|
|
61
|
+
* would otherwise fail on it. Postgres itself refuses the same case with `ERROR: role "…" does
|
|
62
|
+
* not exist` (`createdb()`, `dbcommands.c`) — this tag exists so the refusal is typed and the
|
|
63
|
+
* check runs before any statement, not caught from the driver's text. */
|
|
64
|
+
export class PostgresDatabaseOwnerMissing extends Data.TaggedError('PostgresDatabaseOwnerMissing')<{
|
|
65
|
+
readonly database: string;
|
|
66
|
+
readonly owner: string;
|
|
67
|
+
}> {
|
|
68
|
+
override get message(): string {
|
|
69
|
+
return `Postgres.Database "${this.database}": OWNER "${this.owner}" is not a role in pg_roles.`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A live, asserted property no longer matches the declaration. `reconcile` never issues an
|
|
74
|
+
* `ALTER DATABASE` to fix this — see `docs/postgres.md` for why each asserted prop stops at
|
|
75
|
+
* create. */
|
|
76
|
+
export class PostgresDatabaseDrift extends Data.TaggedError('PostgresDatabaseDrift')<{
|
|
77
|
+
readonly database: string;
|
|
78
|
+
readonly prop: string;
|
|
79
|
+
readonly declared: unknown;
|
|
80
|
+
readonly live: unknown;
|
|
81
|
+
}> {
|
|
82
|
+
override get message(): string {
|
|
83
|
+
return (
|
|
84
|
+
`Postgres.Database "${this.database}": ${this.prop} is declared as ` +
|
|
85
|
+
`${JSON.stringify(this.declared)} but the live database has ${JSON.stringify(this.live)}. ` +
|
|
86
|
+
'This provider never issues ALTER DATABASE — update the declaration to match the live ' +
|
|
87
|
+
'value, or change it by hand and re-plan.'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** `delete` never drops a database. `DROP DATABASE` is destructive and irreversible on data
|
|
93
|
+
* this family did not create from nothing (most declared databases here are adopted, not
|
|
94
|
+
* created); the decision to remove one stays a human act on the host. */
|
|
95
|
+
export class PostgresDatabaseDropRefused extends Data.TaggedError('PostgresDatabaseDropRefused')<{
|
|
96
|
+
readonly database: string;
|
|
97
|
+
}> {
|
|
98
|
+
override get message(): string {
|
|
99
|
+
return (
|
|
100
|
+
`Postgres.Database "${this.database}": delete is refused. This provider never issues DROP ` +
|
|
101
|
+
'DATABASE. Drop it by hand over the trust socket if you mean to remove it, then remove the ' +
|
|
102
|
+
'declaration.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `CREATE DATABASE` raised no error, but the immediate re-read (S10: never trust the write's
|
|
108
|
+
* own report) still finds nothing. Measured to have no real trigger in a correctly speaking
|
|
109
|
+
* client, but S20 forbids `Effect.die` here regardless — an invariant violation is a typed
|
|
110
|
+
* failure the caller can see and retry, never a defect that crashes the whole engine. */
|
|
111
|
+
export class PostgresDatabaseCreateVanished extends Data.TaggedError(
|
|
112
|
+
'PostgresDatabaseCreateVanished',
|
|
113
|
+
)<{
|
|
114
|
+
readonly database: string;
|
|
115
|
+
}> {
|
|
116
|
+
override get message(): string {
|
|
117
|
+
return (
|
|
118
|
+
`Postgres.Database "${this.database}": CREATE DATABASE raised no error, but the database ` +
|
|
119
|
+
'is still absent on the immediate re-read. Re-plan — this is not a state this provider ' +
|
|
120
|
+
'expects to reach.'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type PostgresDatabaseError =
|
|
126
|
+
| PostgresDatabaseNameRefused
|
|
127
|
+
| PostgresDatabaseRenameRefused
|
|
128
|
+
| PostgresDatabaseOwnerMissing
|
|
129
|
+
| PostgresDatabaseDrift
|
|
130
|
+
| PostgresDatabaseDropRefused
|
|
131
|
+
| PostgresDatabaseCreateVanished;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A recording fake `PgExecutor` (S28: "Tests use `bun:test`… a test never trusts the deploy's
|
|
3
|
+
* own report" — here read back from the fake's own catalog, not from the statement list) for
|
|
4
|
+
* every lifecycle test in this family. No socket, no `@effect/sql-pg` client — it is small
|
|
5
|
+
* enough to re-derive the one thing this provider ever asks of a real one: three statement
|
|
6
|
+
* shapes, matched by text, against an in-memory `pg_roles` set and `pg_database` map.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ IT PARSES ITS OWN OUTPUT, NOT SQL IN GENERAL. `parseCreate` below understands exactly the
|
|
9
|
+
* text `buildCreateDatabaseSql` (`database-sql.ts`) produces — quoted with `quoteIdent` /
|
|
10
|
+
* `quoteStringLiteral` — because that is the only `CREATE DATABASE` this family ever issues. A
|
|
11
|
+
* general SQL parser would hide a quoting bug instead of tripping over it.
|
|
12
|
+
*/
|
|
13
|
+
import * as Effect from 'effect/Effect';
|
|
14
|
+
import { SqlError, SqlSyntaxError } from 'effect/unstable/sql/SqlError';
|
|
15
|
+
import type { PostgresDatabaseAttributes } from './database-attrs.ts';
|
|
16
|
+
import type { PgExecutor } from './database-sql.ts';
|
|
17
|
+
|
|
18
|
+
export interface RecordedStatement {
|
|
19
|
+
readonly text: string;
|
|
20
|
+
readonly params: ReadonlyArray<unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FakeSql extends PgExecutor {
|
|
24
|
+
readonly statements: ReadonlyArray<RecordedStatement>;
|
|
25
|
+
/** Mutable on purpose: a test seeds a row a "competing" statement would have produced (the
|
|
26
|
+
* `42P04` race case) before the fake ever sees it. */
|
|
27
|
+
readonly databases: Map<string, PostgresDatabaseAttributes>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FakeSqlOptions {
|
|
31
|
+
readonly roles?: ReadonlyArray<string>;
|
|
32
|
+
readonly databases?: ReadonlyArray<PostgresDatabaseAttributes>;
|
|
33
|
+
/** Fail the NEXT `CREATE DATABASE` with SQLSTATE `42P04` (duplicate_database), once, the way a
|
|
34
|
+
* concurrent creator racing this reconcile would — classified exactly as
|
|
35
|
+
* `@effect/sql-pg`'s own driver classifies it (`database-sql.ts`'s header). */
|
|
36
|
+
readonly raceNextCreate?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const unquoteIdent = (raw: string): string => raw.replace(/""/g, '"');
|
|
40
|
+
const unquoteLiteral = (raw: string): string => raw.replace(/''/g, "'");
|
|
41
|
+
|
|
42
|
+
/** Pull every field back out of exactly the text `buildCreateDatabaseSql` writes. */
|
|
43
|
+
const parseCreate = (text: string): PostgresDatabaseAttributes => {
|
|
44
|
+
const name = /^CREATE DATABASE "((?:[^"]|"")*)" WITH /.exec(text);
|
|
45
|
+
const owner = /OWNER "((?:[^"]|"")*)"/.exec(text);
|
|
46
|
+
const encoding = /ENCODING '((?:[^']|'')*)'/.exec(text);
|
|
47
|
+
const localeProvider = /LOCALE_PROVIDER '((?:[^']|'')*)'/.exec(text);
|
|
48
|
+
const lcCollate = /LC_COLLATE '((?:[^']|'')*)'/.exec(text);
|
|
49
|
+
const lcCtype = /LC_CTYPE '((?:[^']|'')*)'/.exec(text);
|
|
50
|
+
const tablespace = /TABLESPACE "((?:[^"]|"")*)"/.exec(text);
|
|
51
|
+
const allowConnections = /ALLOW_CONNECTIONS (true|false)/.exec(text);
|
|
52
|
+
const connectionLimit = /CONNECTION LIMIT (-?\d+)/.exec(text);
|
|
53
|
+
const isTemplate = /IS_TEMPLATE (true|false)/.exec(text);
|
|
54
|
+
if (name === null || owner === null) {
|
|
55
|
+
throw new Error(`fake-sql: could not parse a generated CREATE DATABASE statement: ${text}`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
name: unquoteIdent(name[1] as string),
|
|
59
|
+
oid: 0,
|
|
60
|
+
owner: unquoteIdent(owner[1] as string),
|
|
61
|
+
encoding: encoding === null ? 'UTF8' : unquoteLiteral(encoding[1] as string),
|
|
62
|
+
localeProvider:
|
|
63
|
+
localeProvider === null ? 'libc' : (unquoteLiteral(localeProvider[1] as string) as 'libc'),
|
|
64
|
+
collate: lcCollate === null ? 'C' : unquoteLiteral(lcCollate[1] as string),
|
|
65
|
+
ctype: lcCtype === null ? 'C' : unquoteLiteral(lcCtype[1] as string),
|
|
66
|
+
tablespace: tablespace === null ? 'pg_default' : unquoteIdent(tablespace[1] as string),
|
|
67
|
+
allowConnections: allowConnections === null ? true : allowConnections[1] === 'true',
|
|
68
|
+
connectionLimit: connectionLimit === null ? -1 : Number(connectionLimit[1]),
|
|
69
|
+
isTemplate: isTemplate === null ? false : isTemplate[1] === 'true',
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const makeFakeSql = (options: FakeSqlOptions = {}): FakeSql => {
|
|
74
|
+
const statements: RecordedStatement[] = [];
|
|
75
|
+
const roles = new Set(options.roles ?? []);
|
|
76
|
+
const databases = new Map(options.databases?.map((d) => [d.name, d] as const) ?? []);
|
|
77
|
+
let raceRemaining = options.raceNextCreate === true ? 1 : 0;
|
|
78
|
+
let oidCounter = 20000;
|
|
79
|
+
|
|
80
|
+
const unsafe = <A extends object>(
|
|
81
|
+
text: string,
|
|
82
|
+
params: ReadonlyArray<unknown> = [],
|
|
83
|
+
): Effect.Effect<ReadonlyArray<A>, SqlError> =>
|
|
84
|
+
Effect.suspend(() => {
|
|
85
|
+
statements.push({ text, params });
|
|
86
|
+
|
|
87
|
+
if (text.startsWith('SELECT 1 AS present FROM pg_roles')) {
|
|
88
|
+
const role = params[0] as string;
|
|
89
|
+
return Effect.succeed(
|
|
90
|
+
(roles.has(role) ? [{ present: 1 }] : []) as unknown as ReadonlyArray<A>,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (text.includes('FROM pg_database')) {
|
|
95
|
+
const name = params[0] as string;
|
|
96
|
+
const row = databases.get(name);
|
|
97
|
+
return Effect.succeed((row === undefined ? [] : [row]) as unknown as ReadonlyArray<A>);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (text.startsWith('CREATE DATABASE')) {
|
|
101
|
+
if (raceRemaining > 0) {
|
|
102
|
+
raceRemaining -= 1;
|
|
103
|
+
return Effect.fail(
|
|
104
|
+
new SqlError({
|
|
105
|
+
reason: new SqlSyntaxError({
|
|
106
|
+
cause: Object.assign(new Error('database "x" already exists'), { code: '42P04' }),
|
|
107
|
+
message: 'duplicate_database (fake race)',
|
|
108
|
+
operation: 'CREATE DATABASE',
|
|
109
|
+
}),
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const row = { ...parseCreate(text), oid: oidCounter };
|
|
114
|
+
oidCounter += 1;
|
|
115
|
+
databases.set(row.name, row);
|
|
116
|
+
return Effect.succeed([] as unknown as ReadonlyArray<A>);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error(`fake-sql: unrecognised statement: ${text}`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return { unsafe, statements, databases };
|
|
123
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# path: doc/src/sgml/ref/create_database.sgml
|
|
2
|
+
# blob: 4da8aeebb50a29e89bd9f68a8deac4b1a3ec7b18
|
|
3
|
+
# tag: REL_18_6 (commit 724edf9bde9d356724ad384a2e196edc3c9f80f7)
|
|
4
|
+
# fetched: 2026-09-23 via gh api repos/postgres/postgres/contents/...?ref=724edf9bde9d356724ad384a2e196edc3c9f80f7
|
|
5
|
+
# excerpt: the <synopsis> block only (lines 23-42 of the full file), cut per house doc-size convention
|
|
6
|
+
#
|
|
7
|
+
<synopsis>
|
|
8
|
+
CREATE DATABASE <replaceable class="parameter">name</replaceable>
|
|
9
|
+
[ WITH ] [ OWNER [=] <replaceable class="parameter">user_name</replaceable> ]
|
|
10
|
+
[ TEMPLATE [=] <replaceable class="parameter">template</replaceable> ]
|
|
11
|
+
[ ENCODING [=] <replaceable class="parameter">encoding</replaceable> ]
|
|
12
|
+
[ STRATEGY [=] <replaceable class="parameter">strategy</replaceable> ]
|
|
13
|
+
[ LOCALE [=] <replaceable class="parameter">locale</replaceable> ]
|
|
14
|
+
[ LC_COLLATE [=] <replaceable class="parameter">lc_collate</replaceable> ]
|
|
15
|
+
[ LC_CTYPE [=] <replaceable class="parameter">lc_ctype</replaceable> ]
|
|
16
|
+
[ BUILTIN_LOCALE [=] <replaceable class="parameter">builtin_locale</replaceable> ]
|
|
17
|
+
[ ICU_LOCALE [=] <replaceable class="parameter">icu_locale</replaceable> ]
|
|
18
|
+
[ ICU_RULES [=] <replaceable class="parameter">icu_rules</replaceable> ]
|
|
19
|
+
[ LOCALE_PROVIDER [=] <replaceable class="parameter">locale_provider</replaceable> ]
|
|
20
|
+
[ COLLATION_VERSION = <replaceable>collation_version</replaceable> ]
|
|
21
|
+
[ TABLESPACE [=] <replaceable class="parameter">tablespace_name</replaceable> ]
|
|
22
|
+
[ ALLOW_CONNECTIONS [=] <replaceable class="parameter">allowconn</replaceable> ]
|
|
23
|
+
[ CONNECTION LIMIT [=] <replaceable class="parameter">connlimit</replaceable> ]
|
|
24
|
+
[ IS_TEMPLATE [=] <replaceable class="parameter">istemplate</replaceable> ]
|
|
25
|
+
[ OID [=] <replaceable class="parameter">oid</replaceable> ]
|
|
26
|
+
</synopsis>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# path: src/include/catalog/pg_database.h
|
|
2
|
+
# blob: 54f0d38c9c9e1cd4d419b05b296b8f84875e9768
|
|
3
|
+
# tag: REL_18_6 (commit 724edf9bde9d356724ad384a2e196edc3c9f80f7)
|
|
4
|
+
# fetched: 2026-09-23 via gh api repos/postgres/postgres/contents/...?ref=724edf9bde9d356724ad384a2e196edc3c9f80f7
|
|
5
|
+
# excerpt: the CATALOG(pg_database...) { ... } FormData_pg_database struct only (lines 29-89 of the full file)
|
|
6
|
+
#
|
|
7
|
+
CATALOG(pg_database,1262,DatabaseRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(1248,DatabaseRelation_Rowtype_Id) BKI_SCHEMA_MACRO
|
|
8
|
+
{
|
|
9
|
+
/* oid */
|
|
10
|
+
Oid oid;
|
|
11
|
+
|
|
12
|
+
/* database name */
|
|
13
|
+
NameData datname;
|
|
14
|
+
|
|
15
|
+
/* owner of database */
|
|
16
|
+
Oid datdba BKI_DEFAULT(POSTGRES) BKI_LOOKUP(pg_authid);
|
|
17
|
+
|
|
18
|
+
/* character encoding */
|
|
19
|
+
int32 encoding;
|
|
20
|
+
|
|
21
|
+
/* locale provider, see pg_collation.collprovider */
|
|
22
|
+
char datlocprovider;
|
|
23
|
+
|
|
24
|
+
/* allowed as CREATE DATABASE template? */
|
|
25
|
+
bool datistemplate;
|
|
26
|
+
|
|
27
|
+
/* new connections allowed? */
|
|
28
|
+
bool datallowconn;
|
|
29
|
+
|
|
30
|
+
/* database has login event triggers? */
|
|
31
|
+
bool dathasloginevt;
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* Max connections allowed. Negative values have special meaning, see
|
|
35
|
+
* DATCONNLIMIT_* defines below.
|
|
36
|
+
*/
|
|
37
|
+
int32 datconnlimit;
|
|
38
|
+
|
|
39
|
+
/* all Xids < this are frozen in this DB */
|
|
40
|
+
TransactionId datfrozenxid;
|
|
41
|
+
|
|
42
|
+
/* all multixacts in the DB are >= this */
|
|
43
|
+
TransactionId datminmxid;
|
|
44
|
+
|
|
45
|
+
/* default table space for this DB */
|
|
46
|
+
Oid dattablespace BKI_LOOKUP(pg_tablespace);
|
|
47
|
+
|
|
48
|
+
#ifdef CATALOG_VARLEN /* variable-length fields start here */
|
|
49
|
+
/* LC_COLLATE setting */
|
|
50
|
+
text datcollate BKI_FORCE_NOT_NULL;
|
|
51
|
+
|
|
52
|
+
/* LC_CTYPE setting */
|
|
53
|
+
text datctype BKI_FORCE_NOT_NULL;
|
|
54
|
+
|
|
55
|
+
/* ICU locale ID */
|
|
56
|
+
text datlocale;
|
|
57
|
+
|
|
58
|
+
/* ICU collation rules */
|
|
59
|
+
text daticurules;
|
|
60
|
+
|
|
61
|
+
/* provider-dependent version of collation data */
|
|
62
|
+
text datcollversion BKI_DEFAULT(_null_);
|
|
63
|
+
|
|
64
|
+
/* access permissions */
|
|
65
|
+
aclitem datacl[1];
|
|
66
|
+
#endif
|
|
67
|
+
} FormData_pg_database;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL providers for Alchemy — `Postgres.Database`, create-and-assert over a self-hosted
|
|
3
|
+
* cluster, walked against PostgreSQL 18.6. See `docs/postgres.md`.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ THIS BARREL IS THE PUBLIC API, deliberately smaller than the directory: `fake-sql.ts` is a
|
|
6
|
+
* test double, not something a consuming stack should import.
|
|
7
|
+
*/
|
|
8
|
+
export type { PostgresConnectionConfig } from './connection.ts';
|
|
9
|
+
export { PostgresConnection, postgresConnection, withPg } from './connection.ts';
|
|
10
|
+
export type { PostgresDatabaseAttributes, PostgresDatabaseProps } from './database-attrs.ts';
|
|
11
|
+
export { POSTGRES_NAME_MAX_BYTES, nameByteRefusal, utf8ByteLength } from './database-attrs.ts';
|
|
12
|
+
export {
|
|
13
|
+
buildCreateDatabaseSql,
|
|
14
|
+
firstDrift,
|
|
15
|
+
isDuplicateDatabaseRace,
|
|
16
|
+
quoteIdent,
|
|
17
|
+
quoteStringLiteral,
|
|
18
|
+
roleExists,
|
|
19
|
+
selectDatabase,
|
|
20
|
+
} from './database-sql.ts';
|
|
21
|
+
export { PostgresDatabase, PostgresDatabaseProvider, isPostgresDatabase } from './database.ts';
|
|
22
|
+
export {
|
|
23
|
+
PostgresDatabaseDrift,
|
|
24
|
+
PostgresDatabaseDropRefused,
|
|
25
|
+
PostgresDatabaseNameRefused,
|
|
26
|
+
PostgresDatabaseOwnerMissing,
|
|
27
|
+
PostgresDatabaseRenameRefused,
|
|
28
|
+
type PostgresDatabaseError,
|
|
29
|
+
} from './errors.ts';
|
|
30
|
+
export { postgresProviders } from './providers.ts';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Postgres.Database`'s provider, pre-wired to one cluster:
|
|
3
|
+
*
|
|
4
|
+
* const providers = postgresProviders({ host: '/opt/homeflare/postgres/sockets', database: 'postgres', username: 'tim' });
|
|
5
|
+
*
|
|
6
|
+
* ★ THE CLUSTER IS A PARAMETER, NOT A DEFAULT (S15, and `docs/postgres.md#path`). This kit is
|
|
7
|
+
* PUBLIC; the mini's socket directory, its user and its port are the CONSUMING stack's values,
|
|
8
|
+
* never baked in here. A stack with more than one cluster calls this more than once and merges
|
|
9
|
+
* the results under whatever logical grouping it wants.
|
|
10
|
+
*/
|
|
11
|
+
import * as Layer from 'effect/Layer';
|
|
12
|
+
import { type PostgresConnectionConfig, postgresConnection } from './connection.ts';
|
|
13
|
+
import { PostgresDatabaseProvider } from './database.ts';
|
|
14
|
+
|
|
15
|
+
export const postgresProviders = (config: PostgresConnectionConfig) =>
|
|
16
|
+
PostgresDatabaseProvider().pipe(Layer.provide(postgresConnection(config)));
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.ApiToken` adopted, undeclared and drifted through Alchemy's own Plan and Apply, over a
|
|
3
|
+
* fake cluster — pinning the metadata-only shape decision 9 (2026-09-23) builds: adopt and manage
|
|
4
|
+
* `comment`/`expire`/`privsep`, never mint a token this resource cannot hand anybody the secret
|
|
5
|
+
* for (api-token.ts's header).
|
|
6
|
+
*
|
|
7
|
+
* ★ FOUR LIVE SHAPES: `metrics@pve!exporter` and `iac@pve!apply` are the no-comment / commented
|
|
8
|
+
* shapes named in api-token.ts's own header; `svc@pve!lease1` and `svc@pve!lease2` are
|
|
9
|
+
* PLACEHOLDERS (never an estate mint-user account — see the ⛔ on declaring under one in
|
|
10
|
+
* api-token.ts's header) exercising a real non-zero `expire` offered as BOTH a JSON number and
|
|
11
|
+
* a JSON string, which PVE does depending on path — `int()` (values.ts) must not care which.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from 'bun:test';
|
|
14
|
+
import * as Effect from 'effect/Effect';
|
|
15
|
+
import * as Layer from 'effect/Layer';
|
|
16
|
+
import { engineOver } from '../verify/fake-engine.ts';
|
|
17
|
+
import { type ApiTokenProps, ProxmoxApiToken, ProxmoxApiTokenProvider } from './api-token.ts';
|
|
18
|
+
import { shape } from './api-token-form.ts';
|
|
19
|
+
import { FAKE_TARGET, type PveCall, fakePve, withoutBao } from './fake-pve.ts';
|
|
20
|
+
import type {
|
|
21
|
+
AccessUsersUseridTokenTokenidPostParams,
|
|
22
|
+
AccessUsersUseridTokenTokenidPutParams,
|
|
23
|
+
} from './generated/pve.ts';
|
|
24
|
+
|
|
25
|
+
type Live = { comment?: string; expire: number | string; privsep: number };
|
|
26
|
+
|
|
27
|
+
/** `access/users/{userid}/token/{tokenid}`, mutable so the PUT test can watch it change. */
|
|
28
|
+
const tokens = new Map<string, Live>([
|
|
29
|
+
['access/users/metrics@pve/token/exporter', { expire: 0, privsep: 0 }],
|
|
30
|
+
['access/users/iac@pve/token/apply', { comment: 'automation', expire: 0, privsep: 0 }],
|
|
31
|
+
['access/users/svc@pve/token/lease1', { expire: 1789327175, privsep: 0 }],
|
|
32
|
+
['access/users/svc@pve/token/lease2', { comment: 'lease', expire: '1789327175', privsep: 0 }],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const cluster = () =>
|
|
36
|
+
fakePve((call: PveCall) => {
|
|
37
|
+
if (call.method === 'GET') return tokens.get(call.path);
|
|
38
|
+
if (call.method === 'PUT') {
|
|
39
|
+
const live = tokens.get(call.path);
|
|
40
|
+
if (live !== undefined) tokens.set(call.path, { ...live, ...call.form });
|
|
41
|
+
return call.form;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** The declared props matching a live fixture exactly — `expire` always as a `number` prop. */
|
|
47
|
+
const propsFor = (
|
|
48
|
+
userid: string,
|
|
49
|
+
tokenid: string,
|
|
50
|
+
over: { comment?: string; privsep?: boolean },
|
|
51
|
+
): ApiTokenProps => ({
|
|
52
|
+
expire: Number(tokens.get(`access/users/${userid}/token/${tokenid}`)?.expire ?? 0),
|
|
53
|
+
privsep: over.privsep ?? false,
|
|
54
|
+
target: FAKE_TARGET,
|
|
55
|
+
tokenid,
|
|
56
|
+
userid,
|
|
57
|
+
...(over.comment === undefined ? {} : { comment: over.comment }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const token = (userid: string, tokenid: string, over: { comment?: string; privsep?: boolean }) =>
|
|
61
|
+
ProxmoxApiToken(`${userid.replace('@', '-')}-${tokenid}`, propsFor(userid, tokenid, over));
|
|
62
|
+
|
|
63
|
+
const engineFor = (fake: ReturnType<typeof cluster>) =>
|
|
64
|
+
engineOver(ProxmoxApiTokenProvider().pipe(Layer.provideMerge(fake.layer)));
|
|
65
|
+
|
|
66
|
+
const declared = () =>
|
|
67
|
+
Effect.all([
|
|
68
|
+
token('metrics@pve', 'exporter', {}),
|
|
69
|
+
token('iac@pve', 'apply', { comment: 'automation' }),
|
|
70
|
+
token('svc@pve', 'lease1', {}),
|
|
71
|
+
token('svc@pve', 'lease2', { comment: 'lease' }),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
describe('adopting a metadata-only ApiToken declaration', () => {
|
|
75
|
+
test('no comment, commented, and a real expire as both a number and a string: noop, adopted, no writes', async () => {
|
|
76
|
+
const fake = cluster();
|
|
77
|
+
await withoutBao(async () => {
|
|
78
|
+
const engine = engineFor(fake);
|
|
79
|
+
const report = await engine.verify(declared());
|
|
80
|
+
expect(report.rows).toHaveLength(4);
|
|
81
|
+
for (const row of report.rows) {
|
|
82
|
+
expect(row).toMatchObject({ changed: [], diff: 'noop', ok: true, planned: 'adopted' });
|
|
83
|
+
}
|
|
84
|
+
const planned = await engine.deploy(declared());
|
|
85
|
+
expect(new Set(Object.values(planned))).toEqual(new Set(['adopted']));
|
|
86
|
+
expect(Object.keys(planned)).toHaveLength(4);
|
|
87
|
+
});
|
|
88
|
+
expect(fake.writes()).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('undeclaring all four sends no DELETE (retain is the default)', async () => {
|
|
92
|
+
const fake = cluster();
|
|
93
|
+
await withoutBao(async () => {
|
|
94
|
+
const engine = engineFor(fake);
|
|
95
|
+
await engine.deploy(declared());
|
|
96
|
+
expect(await engine.deploy(Effect.void)).toEqual({});
|
|
97
|
+
});
|
|
98
|
+
expect(fake.writes()).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('an absent token: the deploy refuses with the mint-elsewhere message, and writes nothing', async () => {
|
|
102
|
+
const fake = cluster();
|
|
103
|
+
const gone = () =>
|
|
104
|
+
ProxmoxApiToken('gone', {
|
|
105
|
+
expire: 0,
|
|
106
|
+
privsep: false,
|
|
107
|
+
target: FAKE_TARGET,
|
|
108
|
+
tokenid: 'missing',
|
|
109
|
+
userid: 'nope@pve',
|
|
110
|
+
});
|
|
111
|
+
await withoutBao(async () => {
|
|
112
|
+
const engine = engineFor(fake);
|
|
113
|
+
await expect(engine.deploy(gone())).rejects.toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
expect(fake.writes()).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a privsep flip: verify shows changed, and the deploy sends exactly one PUT of comment/expire/privsep', async () => {
|
|
119
|
+
const fake = cluster();
|
|
120
|
+
const flipped = () => token('iac@pve', 'apply', { comment: 'automation', privsep: true });
|
|
121
|
+
await withoutBao(async () => {
|
|
122
|
+
const engine = engineFor(fake);
|
|
123
|
+
const report = await engine.verify(flipped());
|
|
124
|
+
expect(report.rows[0]).toMatchObject({ changed: ['privsep'], diff: 'update', ok: false });
|
|
125
|
+
expect(await engine.deploy(flipped())).toEqual({ 'iac-pve-apply': 'adopted' });
|
|
126
|
+
});
|
|
127
|
+
const put = fake.calls.find((call) => call.method === 'PUT');
|
|
128
|
+
expect(fake.writes()).toEqual(['PUT access/users/iac@pve/token/apply']);
|
|
129
|
+
expect(put?.pairs.map(([name]) => name)).toEqual(['comment', 'expire', 'privsep']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('the form keys typecheck against both generated PVE params, with no cast', () => {
|
|
133
|
+
const props = propsFor('iac@pve', 'apply', { comment: 'automation' });
|
|
134
|
+
const post: AccessUsersUseridTokenTokenidPostParams = shape(props);
|
|
135
|
+
const put: AccessUsersUseridTokenTokenidPutParams = shape(props);
|
|
136
|
+
expect(post).toEqual(put);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -16,6 +16,10 @@ import type { ApiTokenAttributes, ApiTokenProps } from './api-token.ts';
|
|
|
16
16
|
* that costs a forever-diff: the schema gives `expire` the default "same as user", and the code
|
|
17
17
|
* never implements it. api-token.ts records what that means.
|
|
18
18
|
*/
|
|
19
|
+
import type {
|
|
20
|
+
AccessUsersUseridTokenTokenidPostParams,
|
|
21
|
+
AccessUsersUseridTokenTokenidPutParams,
|
|
22
|
+
} from './generated/pve.ts';
|
|
19
23
|
import type { PveSpec } from './resource.ts';
|
|
20
24
|
import { bool, int, text } from './values.ts';
|
|
21
25
|
|
|
@@ -56,10 +60,23 @@ export const fullTokenid = (props: ApiTokenProps) => `${props.userid}!${props.to
|
|
|
56
60
|
* required prop precisely so that it is never undeclared, so there is nothing for it to do and
|
|
57
61
|
* its `string | undefined` return would have to be asserted away to fit `Record<string,string>`.
|
|
58
62
|
* An assertion here would be a claim about the prop that the type already makes properly.
|
|
63
|
+
*
|
|
64
|
+
* ★ TYPED AGAINST THE GENERATED PUT AND POST PARAMS, NOT `Record<string, string>`, since
|
|
65
|
+
* 2026-09-23 — the `notification-matcher-form.ts` pattern: `Pick` rather than the whole type,
|
|
66
|
+
* because neither `AccessUsersUseridTokenTokenidPutParams` nor …`PostParams` (generated/pve.ts)
|
|
67
|
+
* carries `userid`/`tokenid` (they are path segments — see the ⛔ above) and Put alone also
|
|
68
|
+
* carries `delete`/`regenerate`, which this family never sends. A schema drift on any of the
|
|
69
|
+
* three fields this function DOES send fails `tsc` here rather than surfacing as a 400 on a live
|
|
70
|
+
* cluster; api-token.test.ts pins the assignment both ways (Put and Post) without a cast.
|
|
71
|
+
* `expire` is written as a template literal, not `String(...)`, because the generated type is
|
|
72
|
+
* `` `${number}` ``, not `string` — `String(n)` widens to plain `string` and would not typecheck.
|
|
59
73
|
*/
|
|
60
|
-
export const shape = (
|
|
74
|
+
export const shape = (
|
|
75
|
+
props: ApiTokenProps,
|
|
76
|
+
): Pick<AccessUsersUseridTokenTokenidPutParams, 'comment' | 'expire' | 'privsep'> &
|
|
77
|
+
Pick<AccessUsersUseridTokenTokenidPostParams, 'comment' | 'expire' | 'privsep'> => ({
|
|
61
78
|
comment: props.comment ?? '',
|
|
62
|
-
expire:
|
|
79
|
+
expire: `${props.expire}`,
|
|
63
80
|
privsep: props.privsep ? '1' : '0',
|
|
64
81
|
});
|
|
65
82
|
|
package/src/proxmox/api-token.ts
CHANGED
|
@@ -20,9 +20,14 @@
|
|
|
20
20
|
* whole of a token's policy, and declaring them is real work: it is how `expire` stops being
|
|
21
21
|
* whatever somebody typed in 2024. Mint NEW tokens where the secret can be caught — OpenBao's
|
|
22
22
|
* `proxmox-c1` mount, or a human at `pveum user token add`.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* ⛔ ★ CORRECTED 2026-09-23 — THIS PARAGRAPH PREVIOUSLY SAID "THE CREATE PATH IS LEFT REACHABLE
|
|
24
|
+
* RATHER THAN STUBBED… the honest arrangement is a create that works". THAT CONTRADICTED THE
|
|
25
|
+
* `reconcile` BELOW, WHICH REFUSES: `handlers.reconcile` calls `Effect.die` by name when `read`
|
|
26
|
+
* answers absent, before any POST — metadata-only per decision 9 (2026-09-23), exported on the
|
|
27
|
+
* barrel as an adopt-and-manage-only Resource. A create that silently did nothing WOULD be the
|
|
28
|
+
* lie resource.ts's ★ describes for a stubbed delete; refusing loudly, by name, with the two
|
|
29
|
+
* ways to get a usable token, is the honest arrangement instead — see the ★ further down and
|
|
30
|
+
* api-token-adopt.test.ts's absent-token row, which pins the refusal and zero writes.
|
|
26
31
|
*
|
|
27
32
|
* ⛔ THERE IS NO `regenerate` PROP, AND IT IS NOT AN OVERSIGHT. PVE's own description: "All users
|
|
28
33
|
* of the previous secret will lose access after this operation." A prop for it would revoke a
|