@volter/twin-planetscale 0.1.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/LICENSE +202 -0
- package/README.md +473 -0
- package/api/src/fetch.ts +50 -0
- package/api/src/generated/surface.gen.json +1 -0
- package/api/src/generated/ui.gen.json +1 -0
- package/api/src/index.ts +19 -0
- package/api/src/manifest.ts +136 -0
- package/api/src/screens/deploy-request.tsx +111 -0
- package/api/src/screens/service-tokens.tsx +141 -0
- package/api/src/screens/session.tsx +117 -0
- package/api/src/semantics/audit.ts +82 -0
- package/api/src/semantics/backups.ts +258 -0
- package/api/src/semantics/branches.ts +201 -0
- package/api/src/semantics/deploy-requests.ts +493 -0
- package/api/src/semantics/index.ts +371 -0
- package/api/src/semantics/shared.ts +141 -0
- package/api/src/semantics/time.ts +77 -0
- package/api/src/token-gate.ts +96 -0
- package/dist/api/src/fetch.d.ts +8 -0
- package/dist/api/src/fetch.js +51 -0
- package/dist/api/src/fetch.ts +50 -0
- package/dist/api/src/generated/surface.gen.json +1 -0
- package/dist/api/src/generated/ui.gen.json +1 -0
- package/dist/api/src/index.ts +19 -0
- package/dist/api/src/manifest.d.ts +2 -0
- package/dist/api/src/manifest.js +113 -0
- package/dist/api/src/manifest.ts +136 -0
- package/dist/api/src/screens/deploy-request.d.ts +7 -0
- package/dist/api/src/screens/deploy-request.js +106 -0
- package/dist/api/src/screens/deploy-request.tsx +111 -0
- package/dist/api/src/screens/service-tokens.d.ts +3 -0
- package/dist/api/src/screens/service-tokens.js +134 -0
- package/dist/api/src/screens/service-tokens.tsx +141 -0
- package/dist/api/src/screens/session.d.ts +11 -0
- package/dist/api/src/screens/session.js +108 -0
- package/dist/api/src/screens/session.tsx +117 -0
- package/dist/api/src/semantics/audit.d.ts +31 -0
- package/dist/api/src/semantics/audit.js +80 -0
- package/dist/api/src/semantics/audit.ts +82 -0
- package/dist/api/src/semantics/backups.d.ts +37 -0
- package/dist/api/src/semantics/backups.js +264 -0
- package/dist/api/src/semantics/backups.ts +258 -0
- package/dist/api/src/semantics/branches.d.ts +53 -0
- package/dist/api/src/semantics/branches.js +197 -0
- package/dist/api/src/semantics/branches.ts +201 -0
- package/dist/api/src/semantics/deploy-requests.d.ts +47 -0
- package/dist/api/src/semantics/deploy-requests.js +491 -0
- package/dist/api/src/semantics/deploy-requests.ts +493 -0
- package/dist/api/src/semantics/index.d.ts +20 -0
- package/dist/api/src/semantics/index.js +381 -0
- package/dist/api/src/semantics/index.ts +371 -0
- package/dist/api/src/semantics/shared.d.ts +36 -0
- package/dist/api/src/semantics/shared.js +132 -0
- package/dist/api/src/semantics/shared.ts +141 -0
- package/dist/api/src/semantics/time.d.ts +2 -0
- package/dist/api/src/semantics/time.js +81 -0
- package/dist/api/src/semantics/time.ts +77 -0
- package/dist/api/src/token-gate.d.ts +9 -0
- package/dist/api/src/token-gate.js +97 -0
- package/dist/api/src/token-gate.ts +96 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +61 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/index.d.ts +26 -0
- package/dist/src/index.js +156 -0
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +41 -0
- package/dist/src/planetscale-budget.d.ts +78 -0
- package/dist/src/planetscale-budget.js +305 -0
- package/dist/src/planetscale-capabilities.d.ts +10 -0
- package/dist/src/planetscale-capabilities.js +3977 -0
- package/dist/src/planetscale-collation-weights.gen.d.ts +4 -0
- package/dist/src/planetscale-collation-weights.gen.js +12 -0
- package/dist/src/planetscale-collation.d.ts +70 -0
- package/dist/src/planetscale-collation.js +391 -0
- package/dist/src/planetscale-conformance.d.ts +8 -0
- package/dist/src/planetscale-conformance.js +213 -0
- package/dist/src/planetscale-connector.d.ts +150 -0
- package/dist/src/planetscale-connector.js +532 -0
- package/dist/src/planetscale-deploy.d.ts +26 -0
- package/dist/src/planetscale-deploy.js +235 -0
- package/dist/src/planetscale-information-schema.d.ts +32 -0
- package/dist/src/planetscale-information-schema.js +299 -0
- package/dist/src/planetscale-mysql.d.ts +33 -0
- package/dist/src/planetscale-mysql.js +547 -0
- package/dist/src/planetscale-roles.d.ts +11 -0
- package/dist/src/planetscale-roles.js +60 -0
- package/dist/src/planetscale-row.d.ts +12 -0
- package/dist/src/planetscale-row.js +39 -0
- package/dist/src/planetscale-server.d.ts +42 -0
- package/dist/src/planetscale-server.js +137 -0
- package/dist/src/planetscale-sql.d.ts +701 -0
- package/dist/src/planetscale-sql.js +7167 -0
- package/dist/src/planetscale-store.d.ts +126 -0
- package/dist/src/planetscale-store.js +827 -0
- package/dist/src/planetscale-twin.d.ts +48 -0
- package/dist/src/planetscale-twin.js +290 -0
- package/dist/src/planetscale-values.d.ts +139 -0
- package/dist/src/planetscale-values.js +719 -0
- package/dist/src/planetscale-wire.d.ts +110 -0
- package/dist/src/planetscale-wire.js +188 -0
- package/dist/src/semantics/psdb.d.ts +18 -0
- package/dist/src/semantics/psdb.js +30 -0
- package/package.json +58 -0
- package/src/cli.ts +58 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/index.ts +267 -0
- package/src/manifest.ts +60 -0
- package/src/planetscale-budget.ts +347 -0
- package/src/planetscale-capabilities.ts +3862 -0
- package/src/planetscale-collation-weights.gen.ts +13 -0
- package/src/planetscale-collation.ts +378 -0
- package/src/planetscale-conformance.ts +237 -0
- package/src/planetscale-connector.ts +571 -0
- package/src/planetscale-deploy.ts +197 -0
- package/src/planetscale-information-schema.ts +322 -0
- package/src/planetscale-mysql.ts +339 -0
- package/src/planetscale-roles.ts +71 -0
- package/src/planetscale-row.ts +43 -0
- package/src/planetscale-server.ts +162 -0
- package/src/planetscale-sql.ts +5957 -0
- package/src/planetscale-store.ts +869 -0
- package/src/planetscale-twin.ts +338 -0
- package/src/planetscale-values.ts +572 -0
- package/src/planetscale-wire.ts +274 -0
- package/src/semantics/psdb.ts +57 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// THE psdb WIRE — protojson encoding of the `psdb.v1alpha1.Database` messages, and the row packing
|
|
2
|
+
// that is the single most-gotten-wrong part of this protocol.
|
|
3
|
+
//
|
|
4
|
+
// GROUNDED in the ACTUALLY-INSTALLED `@planetscale/database@1.20.1` source (npm pack), which is the
|
|
5
|
+
// authoritative decoder: `dist/index.js`'s `decodeRow`/`parse`/`execute` and `dist/cast.js`. Every
|
|
6
|
+
// encoding rule below exists because that client's decoder requires it.
|
|
7
|
+
//
|
|
8
|
+
// ── HOW A ROW IS PACKED (dist/index.js `decodeRow`) ───────────────────────────────────────────
|
|
9
|
+
// { lengths: ['3', '-1', '2'], values: base64('abc' + '' + 'de') }
|
|
10
|
+
// `values` is ONE base64 blob holding every non-null column's bytes CONCATENATED with no
|
|
11
|
+
// separators; `lengths` is the per-column byte width as a DECIMAL STRING (protojson renders int64
|
|
12
|
+
// as a string), and `-1` means SQL NULL. The client walks `lengths`, slicing `atob(values)` by
|
|
13
|
+
// offset. Three consequences a naive encoder gets wrong, all silent:
|
|
14
|
+
// • a length is the BYTE count, not the character count — a multi-byte UTF-8 value packed with
|
|
15
|
+
// its character count shifts every subsequent column's slice, corrupting the whole row;
|
|
16
|
+
// • the client slices with `String.prototype.substring` over the `atob` output, i.e. over
|
|
17
|
+
// LATIN-1 CHARACTER units, then re-decodes with TextDecoder — so the bytes must be laid out as
|
|
18
|
+
// one char per byte before base64, which is what `packBytes` does;
|
|
19
|
+
// • `values` is OMITTED when the row's bytes are empty (an all-NULL row), and `decodeRow` handles
|
|
20
|
+
// that with `row.values ? atob(row.values) : ''` — emitting `""` would work, but omitting is
|
|
21
|
+
// what the vendor does and what the fixture in the client's own suite shows.
|
|
22
|
+
//
|
|
23
|
+
// ── PROTOJSON DEFAULTS ARE OMITTED, AND THE CLIENT DEPENDS ON IT ──────────────────────────────
|
|
24
|
+
// protojson does not emit fields holding their zero value unless `EmitUnpopulated` is set, and
|
|
25
|
+
// PlanetScale does not set it. So `rowsAffected: 0`, `insertId: 0`, an empty `fields`, an empty
|
|
26
|
+
// `rows`, and `type: NULL_TYPE` (enum 0) are all ABSENT from the JSON. The client compensates
|
|
27
|
+
// exactly there — `result?.rowsAffected ? parseInt(...) : 0`, `result?.insertId ?? '0'`,
|
|
28
|
+
// `field.type ||= 'NULL'` — which is the proof that the absence is the vendor's real behaviour and
|
|
29
|
+
// not an accident. A DDL statement's whole result is therefore the empty object `{}`.
|
|
30
|
+
import type { Cell, Field, SessionVars, VitessType } from './planetscale-sql.ts';
|
|
31
|
+
import { BINARY_CHARSET } from './planetscale-sql.ts';
|
|
32
|
+
|
|
33
|
+
/** One packed row, as `ExecuteResponse.result.rows[]` carries it. */
|
|
34
|
+
export type WireRow = { lengths: string[]; values?: string };
|
|
35
|
+
|
|
36
|
+
/** `ExecuteResponse.result` — every key omitted when it holds its protojson zero value. */
|
|
37
|
+
export type WireQueryResult = {
|
|
38
|
+
fields?: WireField[];
|
|
39
|
+
rows?: WireRow[];
|
|
40
|
+
rowsAffected?: string;
|
|
41
|
+
insertId?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** `query.Field`, protojson-shaped. Optional keys are omitted, never null. */
|
|
45
|
+
export type WireField = {
|
|
46
|
+
name: string;
|
|
47
|
+
type?: string;
|
|
48
|
+
table?: string;
|
|
49
|
+
orgTable?: string;
|
|
50
|
+
database?: string;
|
|
51
|
+
orgName?: string;
|
|
52
|
+
columnLength?: number;
|
|
53
|
+
charset?: number;
|
|
54
|
+
flags?: number;
|
|
55
|
+
columnType?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** `vtrpc.RPCError` as it appears on `ExecuteResponse.error` (and in a Connect error body). */
|
|
59
|
+
export type WireError = { code: string; message: string };
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Encode one cell to bytes.
|
|
63
|
+
*
|
|
64
|
+
* The charset decides the encoding, exactly as it decides the client's decoding: charset 63 is
|
|
65
|
+
* MySQL's binary collation, for which `cast` returns a `Uint8Array` built one byte per character
|
|
66
|
+
* (dist/cast.js `isBinary` → dist/text.js `uint8Array`), so a binary cell is laid out LATIN-1 —
|
|
67
|
+
* one char, one byte. Everything else is UTF-8, which is what `decodeUtf8` decodes.
|
|
68
|
+
*/
|
|
69
|
+
/** A binary cell holding a code point above 0xFF, which no write of the engine stores: its UTF-8 bytes, never a throw. */
|
|
70
|
+
function wideBinaryCell(value: string): Uint8Array {
|
|
71
|
+
return new TextEncoder().encode(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function encodeCell(value: string, charset: number | undefined): Uint8Array {
|
|
75
|
+
if (charset === BINARY_CHARSET) {
|
|
76
|
+
// A binary cell is expected to already be BYTE-SHAPED (one char per byte) — the engine's
|
|
77
|
+
// `storeValue` guarantees that on every write path. If a code point above 0xFF reaches here
|
|
78
|
+
// anyway (state written by an older build, or a caller folding resources in by hand), the
|
|
79
|
+
// answer is to UTF-8-encode it, NOT to throw.
|
|
80
|
+
//
|
|
81
|
+
// §9 round one, finding 3: this used to throw a plain `Error`. Because it is not a `SqlError`,
|
|
82
|
+
// it escaped `handlePlanetscaleTwinRequest` entirely, the server's catch-all turned it into an
|
|
83
|
+
// HTTP 500, and the client raised `UnknownError` — so one legal `INSERT INTO b (p) VALUES
|
|
84
|
+
// ('日')` made that table UNREADABLE FOREVER. Real MySQL just stores the UTF-8 bytes. A twin's
|
|
85
|
+
// read path must never be the thing that crashes; bytes are bytes.
|
|
86
|
+
let byteShaped = true;
|
|
87
|
+
for (let i = 0; i < value.length; i++) if (value.charCodeAt(i) > 0xff) { byteShaped = false; break; }
|
|
88
|
+
if (!byteShaped) return wideBinaryCell(value);
|
|
89
|
+
const out = new Uint8Array(value.length);
|
|
90
|
+
for (let i = 0; i < value.length; i++) out[i] = value.charCodeAt(i);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
return new TextEncoder().encode(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Base64 of a byte sequence laid out one char per byte — the layout `atob` reproduces. */
|
|
97
|
+
export function packBytes(bytes: Uint8Array): string {
|
|
98
|
+
let binary = '';
|
|
99
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]!);
|
|
100
|
+
return btoa(binary);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Pack one result row: per-column byte lengths plus the concatenated payload. */
|
|
104
|
+
export function packRow(fields: Field[], cells: Cell[]): WireRow {
|
|
105
|
+
const lengths: string[] = [];
|
|
106
|
+
const chunks: Uint8Array[] = [];
|
|
107
|
+
let total = 0;
|
|
108
|
+
for (let i = 0; i < fields.length; i++) {
|
|
109
|
+
const cell = cells[i] ?? null;
|
|
110
|
+
if (cell === null) { lengths.push('-1'); continue; }
|
|
111
|
+
// A JSON column reports charset 63 as MySQL's does, but its text is utf8mb4 (MySQL 8 manual,
|
|
112
|
+
// "The JSON Data Type": JSON strings use utf8mb4), so it packs as text, never as a byte string.
|
|
113
|
+
const bytes = encodeCell(cell, fields[i]?.type === 'JSON' ? undefined : fields[i]?.charset);
|
|
114
|
+
lengths.push(String(bytes.length));
|
|
115
|
+
chunks.push(bytes);
|
|
116
|
+
total += bytes.length;
|
|
117
|
+
}
|
|
118
|
+
if (total === 0) return { lengths };
|
|
119
|
+
const joined = new Uint8Array(total);
|
|
120
|
+
let offset = 0;
|
|
121
|
+
for (const chunk of chunks) { joined.set(chunk, offset); offset += chunk.length; }
|
|
122
|
+
return { lengths, values: packBytes(joined) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The inverse — used by this pack's own tests and by the connector to read a real response. */
|
|
126
|
+
export function unpackRow(row: WireRow): Array<string | null> {
|
|
127
|
+
const raw = row.values === undefined ? '' : atob(row.values);
|
|
128
|
+
let offset = 0;
|
|
129
|
+
return row.lengths.map((size) => {
|
|
130
|
+
const width = parseInt(size, 10);
|
|
131
|
+
if (width < 0) return null;
|
|
132
|
+
const slice = raw.substring(offset, offset + width);
|
|
133
|
+
offset += width;
|
|
134
|
+
return slice;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Field → protojson, dropping every key at its zero value (see the header). */
|
|
139
|
+
export function encodeField(field: Field): WireField {
|
|
140
|
+
const out: WireField = { name: field.name };
|
|
141
|
+
if (field.type !== 'NULL') out.type = field.type;
|
|
142
|
+
if (field.table !== undefined) out.table = field.table;
|
|
143
|
+
if (field.orgTable !== undefined) out.orgTable = field.orgTable;
|
|
144
|
+
if (field.database !== undefined) out.database = field.database;
|
|
145
|
+
if (field.orgName !== undefined) out.orgName = field.orgName;
|
|
146
|
+
if (field.columnLength !== undefined && field.columnLength !== 0) out.columnLength = field.columnLength;
|
|
147
|
+
if (field.charset !== undefined && field.charset !== 0) out.charset = field.charset;
|
|
148
|
+
if (field.flags !== undefined && field.flags !== 0) out.flags = field.flags;
|
|
149
|
+
if (field.columnType !== undefined) out.columnType = field.columnType;
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A whole result set → `ExecuteResponse.result`. `{}` for a DDL statement, by construction. */
|
|
154
|
+
export function encodeQueryResult(input: {
|
|
155
|
+
fields: Field[];
|
|
156
|
+
rows: Cell[][];
|
|
157
|
+
rowsAffected: number;
|
|
158
|
+
insertId: number;
|
|
159
|
+
}): WireQueryResult {
|
|
160
|
+
const out: WireQueryResult = {};
|
|
161
|
+
if (input.fields.length > 0) out.fields = input.fields.map(encodeField);
|
|
162
|
+
if (input.rows.length > 0) out.rows = input.rows.map((r) => packRow(input.fields, r));
|
|
163
|
+
if (input.rowsAffected !== 0) out.rowsAffected = String(input.rowsAffected);
|
|
164
|
+
if (input.insertId !== 0) out.insertId = String(input.insertId);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
// SESSION
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* `psdb.v1alpha1.Session`, as far as this protocol can be observed.
|
|
174
|
+
*
|
|
175
|
+
* HONESTY NOTE: the client treats the session as COMPLETELY OPAQUE — `Connection.execute` stores
|
|
176
|
+
* whatever came back and echoes it on the next request, and reads no field of it (dist/index.js,
|
|
177
|
+
* `this.session = session`). So the only facts about its shape that this pack can assert are the
|
|
178
|
+
* ones its own protocol needs: an identity that survives a round trip, and the transaction state
|
|
179
|
+
* that makes `Connection.transaction()` work. The real Vitess `Session` proto carries many more
|
|
180
|
+
* fields (shard sessions, options, warnings, DDL strategy); this twin does NOT fabricate them, and
|
|
181
|
+
* `planetscale.session.vitess_session_fields` files that as a todo rather than inventing a shape.
|
|
182
|
+
*/
|
|
183
|
+
export type WireSession = {
|
|
184
|
+
signature: string;
|
|
185
|
+
vitessSession: {
|
|
186
|
+
autocommit: boolean;
|
|
187
|
+
targetString: string;
|
|
188
|
+
sessionUUID: string;
|
|
189
|
+
inTransaction: boolean;
|
|
190
|
+
/** vtgate.proto Session.user_defined_variables: "all the @variables defined for this session", each a BindVariable */
|
|
191
|
+
userDefinedVariables?: Record<string, { type?: string; value?: string }>;
|
|
192
|
+
/** vtgate.proto Session.system_variables: "all session variables set for this connection" (the twin keeps sql_mode) */
|
|
193
|
+
systemVariables?: Record<string, string>;
|
|
194
|
+
/** vtgate.proto Session.warnings: "non-fatal warnings from the previous query", each a QueryWarning (code, message) */
|
|
195
|
+
warnings?: Array<{ code?: number; message?: string }>;
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The session variables a client's session carries, read back from what it echoes: user variables from
|
|
201
|
+
* `user_defined_variables` (a BindVariable each: its type, and its value as bytes, base64 in protojson; a NULL value is
|
|
202
|
+
* the zero type, NULL_TYPE, which protojson omits) and the SQL mode from `system_variables.sql_mode`. The session is the
|
|
203
|
+
* client's to echo, as Vitess keeps them; the twin stores none of it.
|
|
204
|
+
*/
|
|
205
|
+
export function sessionVarsOf(session: unknown): SessionVars {
|
|
206
|
+
const vs = (session as { vitessSession?: WireSession['vitessSession'] } | null | undefined)?.vitessSession;
|
|
207
|
+
const user: SessionVars['user'] = {};
|
|
208
|
+
for (const [name, bv] of Object.entries(vs?.userDefinedVariables ?? {})) {
|
|
209
|
+
if (bv === null || typeof bv !== 'object') continue;
|
|
210
|
+
const type = typeof bv.type === 'string' && bv.type !== 'NULL_TYPE' ? (bv.type as VitessType) : undefined;
|
|
211
|
+
const bytes = atob(bv.value ?? '');
|
|
212
|
+
// a binary string keeps its bytes (one character per byte, as the engine holds binary cells); text is UTF-8
|
|
213
|
+
user[name.toLowerCase()] = type === undefined ? { value: null, type: 'NULL' } : { value: type === 'VARBINARY' ? bytes : new TextDecoder().decode(Uint8Array.from(bytes, (c) => c.charCodeAt(0))), type };
|
|
214
|
+
}
|
|
215
|
+
const sqlMode = vs?.systemVariables?.sql_mode;
|
|
216
|
+
const collationConnection = vs?.systemVariables?.collation_connection;
|
|
217
|
+
const timeZone = vs?.systemVariables?.time_zone;
|
|
218
|
+
const warnings = Array.isArray(vs?.warnings) ? vs!.warnings.map((w) => ({ level: 'Warning', code: Number(w?.code ?? 0), message: String(w?.message ?? '') })) : [];
|
|
219
|
+
return { user, ...(typeof sqlMode === 'string' ? { sqlMode } : {}), ...(typeof collationConnection === 'string' ? { collationConnection } : {}), ...(typeof timeZone === 'string' ? { timeZone } : {}), ...(warnings.length ? { warnings } : {}) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function encodeVars(vars: SessionVars | undefined): Pick<WireSession['vitessSession'], 'userDefinedVariables' | 'systemVariables' | 'warnings'> {
|
|
223
|
+
if (vars === undefined) return {};
|
|
224
|
+
const names = Object.keys(vars.user);
|
|
225
|
+
return {
|
|
226
|
+
...(names.length ? { userDefinedVariables: Object.fromEntries(names.sort().map((n) => {
|
|
227
|
+
const v = vars.user[n]!;
|
|
228
|
+
const bytes = v.value === null ? undefined : v.type === 'VARBINARY' ? Uint8Array.from(v.value, (c) => c.charCodeAt(0)) : new TextEncoder().encode(v.value);
|
|
229
|
+
return [n, bytes === undefined ? {} : { type: v.type, value: packBytes(bytes) }];
|
|
230
|
+
})) } : {}),
|
|
231
|
+
...(vars.sqlMode !== undefined || vars.collationConnection !== undefined || vars.timeZone !== undefined ? { systemVariables: {
|
|
232
|
+
...(vars.sqlMode !== undefined ? { sql_mode: vars.sqlMode } : {}), ...(vars.collationConnection !== undefined ? { collation_connection: vars.collationConnection } : {}),
|
|
233
|
+
...(vars.timeZone !== undefined ? { time_zone: vars.timeZone } : {}),
|
|
234
|
+
} } : {}),
|
|
235
|
+
...(vars.warnings?.length ? { warnings: vars.warnings.map((w) => ({ code: w.code, message: w.message })) } : {}),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export type WireUser = { username: string; psid: string };
|
|
240
|
+
|
|
241
|
+
/** `CreateSessionResponse`. `branch` and `user` come straight from ps-http-sim's own response. */
|
|
242
|
+
export type WireCreateSessionResponse = { branch: string; user: WireUser; session: WireSession };
|
|
243
|
+
|
|
244
|
+
export type WireExecuteResponse = {
|
|
245
|
+
session?: WireSession;
|
|
246
|
+
result?: WireQueryResult;
|
|
247
|
+
error?: WireError;
|
|
248
|
+
/** seconds, as a float — the client multiplies by 1000 for `ExecutedQuery.time`. */
|
|
249
|
+
timing?: number;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export function makeSession(id: string, opts: { branch: string; inTransaction: boolean; vars?: SessionVars }): WireSession {
|
|
253
|
+
return {
|
|
254
|
+
// A stable, derivable signature — NOT entropy: the served response must be a pure function of
|
|
255
|
+
// (request, stored state), so the same session id always renders the same signature.
|
|
256
|
+
signature: packBytes(new TextEncoder().encode(`twin:${opts.branch}:${id}`)),
|
|
257
|
+
vitessSession: {
|
|
258
|
+
autocommit: !opts.inTransaction,
|
|
259
|
+
targetString: `${opts.branch}@primary`,
|
|
260
|
+
sessionUUID: id,
|
|
261
|
+
inTransaction: opts.inTransaction,
|
|
262
|
+
...encodeVars(opts.vars),
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Read the session id a client echoed back. Returns null for a missing/unrecognisable session. */
|
|
268
|
+
export function sessionIdOf(session: unknown): string | null {
|
|
269
|
+
if (session === null || typeof session !== 'object') return null;
|
|
270
|
+
const vs = (session as { vitessSession?: unknown }).vitessSession;
|
|
271
|
+
if (vs === null || typeof vs !== 'object') return null;
|
|
272
|
+
const id = (vs as { sessionUUID?: unknown }).sessionUUID;
|
|
273
|
+
return typeof id === 'string' && id !== '' ? id : null;
|
|
274
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The psdb RPCs' handlers, by operationId (docs/contributing/architecture.md, "The semantics layer"). Each delegates
|
|
2
|
+
// to the pack's request handler (planetscale-twin.ts), which keeps psdb's order and both of its envelopes: Connect's
|
|
3
|
+
// method and verb checks, then Basic auth (401 in `{session, error}`), then the body (400 `{code, message}`), then the
|
|
4
|
+
// RPC, whose query errors are 200 `{session, error}` and whose read-only refusal is per statement (manifest.ts says why
|
|
5
|
+
// the kernel's `crossCutting` is not in front). CloseSession asks the declared session machine (manifest.ts) before it
|
|
6
|
+
// closes one. Prepare answers Connect's `unimplemented`, as ps-http-sim does. StreamExecute has no handler.
|
|
7
|
+
//
|
|
8
|
+
// These are dispatch handlers, not `bindSemantics` ones: a semantics context parses the body before the handler runs,
|
|
9
|
+
// and a malformed body would throw there instead of answering psdb's 400 after its auth check.
|
|
10
|
+
import type { DerivedCall, DerivedHandler } from '@volter/world-core';
|
|
11
|
+
import { handlePlanetscaleTwinRequest, type PlanetscaleRequest, type PlanetscaleResponse } from '../planetscale-twin.ts';
|
|
12
|
+
|
|
13
|
+
/** What a psdb request is answered against: the server's options and the instant it happens at. */
|
|
14
|
+
export type PsdbOptions = {
|
|
15
|
+
root?: string;
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
username?: string;
|
|
18
|
+
password?: string;
|
|
19
|
+
database?: string;
|
|
20
|
+
branch?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** A fetch Request as the psdb request handler reads it. */
|
|
24
|
+
export async function psdbRequest(request: Request, options: PsdbOptions, occurredAt: string): Promise<PlanetscaleRequest> {
|
|
25
|
+
const url = new URL(request.url);
|
|
26
|
+
const body = request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : '';
|
|
27
|
+
const headers: Record<string, string> = {};
|
|
28
|
+
request.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
|
|
29
|
+
return {
|
|
30
|
+
method: request.method,
|
|
31
|
+
path: url.pathname + (url.search || ''),
|
|
32
|
+
body,
|
|
33
|
+
headers,
|
|
34
|
+
readOnly: options.readOnly ?? false,
|
|
35
|
+
occurredAt,
|
|
36
|
+
...(options.root !== undefined ? { root: options.root } : {}),
|
|
37
|
+
...(options.username !== undefined ? { username: options.username } : {}),
|
|
38
|
+
...(options.password !== undefined ? { password: options.password } : {}),
|
|
39
|
+
...(options.database !== undefined ? { database: options.database } : {}),
|
|
40
|
+
...(options.branch !== undefined ? { branch: options.branch } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The handler's answer as a fetch Response: only its own headers, none added. */
|
|
45
|
+
export const psdbResponse = ({ status, body, headers }: PlanetscaleResponse): Response =>
|
|
46
|
+
new Response(JSON.stringify(body), { status, headers: { ...(headers ?? {}) } });
|
|
47
|
+
|
|
48
|
+
/** A fault inside the twin, in Connect's transport shape; never an exception out of the fetch. */
|
|
49
|
+
export const psdbInternal = (error: unknown): Response =>
|
|
50
|
+
new Response(JSON.stringify({ code: 'internal', message: String(error) }), { status: 500, headers: { 'content-type': 'application/json' } });
|
|
51
|
+
|
|
52
|
+
export function psdbHandlers(options: PsdbOptions, now: () => string): Record<string, DerivedHandler> {
|
|
53
|
+
const answer = async (request: Request): Promise<Response> =>
|
|
54
|
+
psdbResponse(await handlePlanetscaleTwinRequest(await psdbRequest(request, options, now())));
|
|
55
|
+
const serve = ({ request }: DerivedCall): Promise<Response> => answer(request).catch(psdbInternal);
|
|
56
|
+
return { CreateSession: serve, Execute: serve, CloseSession: serve, Prepare: serve };
|
|
57
|
+
}
|