@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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +473 -0
  3. package/api/src/fetch.ts +50 -0
  4. package/api/src/generated/surface.gen.json +1 -0
  5. package/api/src/generated/ui.gen.json +1 -0
  6. package/api/src/index.ts +19 -0
  7. package/api/src/manifest.ts +136 -0
  8. package/api/src/screens/deploy-request.tsx +111 -0
  9. package/api/src/screens/service-tokens.tsx +141 -0
  10. package/api/src/screens/session.tsx +117 -0
  11. package/api/src/semantics/audit.ts +82 -0
  12. package/api/src/semantics/backups.ts +258 -0
  13. package/api/src/semantics/branches.ts +201 -0
  14. package/api/src/semantics/deploy-requests.ts +493 -0
  15. package/api/src/semantics/index.ts +371 -0
  16. package/api/src/semantics/shared.ts +141 -0
  17. package/api/src/semantics/time.ts +77 -0
  18. package/api/src/token-gate.ts +96 -0
  19. package/dist/api/src/fetch.d.ts +8 -0
  20. package/dist/api/src/fetch.js +51 -0
  21. package/dist/api/src/fetch.ts +50 -0
  22. package/dist/api/src/generated/surface.gen.json +1 -0
  23. package/dist/api/src/generated/ui.gen.json +1 -0
  24. package/dist/api/src/index.ts +19 -0
  25. package/dist/api/src/manifest.d.ts +2 -0
  26. package/dist/api/src/manifest.js +113 -0
  27. package/dist/api/src/manifest.ts +136 -0
  28. package/dist/api/src/screens/deploy-request.d.ts +7 -0
  29. package/dist/api/src/screens/deploy-request.js +106 -0
  30. package/dist/api/src/screens/deploy-request.tsx +111 -0
  31. package/dist/api/src/screens/service-tokens.d.ts +3 -0
  32. package/dist/api/src/screens/service-tokens.js +134 -0
  33. package/dist/api/src/screens/service-tokens.tsx +141 -0
  34. package/dist/api/src/screens/session.d.ts +11 -0
  35. package/dist/api/src/screens/session.js +108 -0
  36. package/dist/api/src/screens/session.tsx +117 -0
  37. package/dist/api/src/semantics/audit.d.ts +31 -0
  38. package/dist/api/src/semantics/audit.js +80 -0
  39. package/dist/api/src/semantics/audit.ts +82 -0
  40. package/dist/api/src/semantics/backups.d.ts +37 -0
  41. package/dist/api/src/semantics/backups.js +264 -0
  42. package/dist/api/src/semantics/backups.ts +258 -0
  43. package/dist/api/src/semantics/branches.d.ts +53 -0
  44. package/dist/api/src/semantics/branches.js +197 -0
  45. package/dist/api/src/semantics/branches.ts +201 -0
  46. package/dist/api/src/semantics/deploy-requests.d.ts +47 -0
  47. package/dist/api/src/semantics/deploy-requests.js +491 -0
  48. package/dist/api/src/semantics/deploy-requests.ts +493 -0
  49. package/dist/api/src/semantics/index.d.ts +20 -0
  50. package/dist/api/src/semantics/index.js +381 -0
  51. package/dist/api/src/semantics/index.ts +371 -0
  52. package/dist/api/src/semantics/shared.d.ts +36 -0
  53. package/dist/api/src/semantics/shared.js +132 -0
  54. package/dist/api/src/semantics/shared.ts +141 -0
  55. package/dist/api/src/semantics/time.d.ts +2 -0
  56. package/dist/api/src/semantics/time.js +81 -0
  57. package/dist/api/src/semantics/time.ts +77 -0
  58. package/dist/api/src/token-gate.d.ts +9 -0
  59. package/dist/api/src/token-gate.js +97 -0
  60. package/dist/api/src/token-gate.ts +96 -0
  61. package/dist/src/cli.d.ts +2 -0
  62. package/dist/src/cli.js +61 -0
  63. package/dist/src/generated/surface.gen.json +1 -0
  64. package/dist/src/generated/ui.gen.json +1 -0
  65. package/dist/src/index.d.ts +26 -0
  66. package/dist/src/index.js +156 -0
  67. package/dist/src/manifest.d.ts +2 -0
  68. package/dist/src/manifest.js +41 -0
  69. package/dist/src/planetscale-budget.d.ts +78 -0
  70. package/dist/src/planetscale-budget.js +305 -0
  71. package/dist/src/planetscale-capabilities.d.ts +10 -0
  72. package/dist/src/planetscale-capabilities.js +3977 -0
  73. package/dist/src/planetscale-collation-weights.gen.d.ts +4 -0
  74. package/dist/src/planetscale-collation-weights.gen.js +12 -0
  75. package/dist/src/planetscale-collation.d.ts +70 -0
  76. package/dist/src/planetscale-collation.js +391 -0
  77. package/dist/src/planetscale-conformance.d.ts +8 -0
  78. package/dist/src/planetscale-conformance.js +213 -0
  79. package/dist/src/planetscale-connector.d.ts +150 -0
  80. package/dist/src/planetscale-connector.js +532 -0
  81. package/dist/src/planetscale-deploy.d.ts +26 -0
  82. package/dist/src/planetscale-deploy.js +235 -0
  83. package/dist/src/planetscale-information-schema.d.ts +32 -0
  84. package/dist/src/planetscale-information-schema.js +299 -0
  85. package/dist/src/planetscale-mysql.d.ts +33 -0
  86. package/dist/src/planetscale-mysql.js +547 -0
  87. package/dist/src/planetscale-roles.d.ts +11 -0
  88. package/dist/src/planetscale-roles.js +60 -0
  89. package/dist/src/planetscale-row.d.ts +12 -0
  90. package/dist/src/planetscale-row.js +39 -0
  91. package/dist/src/planetscale-server.d.ts +42 -0
  92. package/dist/src/planetscale-server.js +137 -0
  93. package/dist/src/planetscale-sql.d.ts +701 -0
  94. package/dist/src/planetscale-sql.js +7167 -0
  95. package/dist/src/planetscale-store.d.ts +126 -0
  96. package/dist/src/planetscale-store.js +827 -0
  97. package/dist/src/planetscale-twin.d.ts +48 -0
  98. package/dist/src/planetscale-twin.js +290 -0
  99. package/dist/src/planetscale-values.d.ts +139 -0
  100. package/dist/src/planetscale-values.js +719 -0
  101. package/dist/src/planetscale-wire.d.ts +110 -0
  102. package/dist/src/planetscale-wire.js +188 -0
  103. package/dist/src/semantics/psdb.d.ts +18 -0
  104. package/dist/src/semantics/psdb.js +30 -0
  105. package/package.json +58 -0
  106. package/src/cli.ts +58 -0
  107. package/src/generated/surface.gen.json +1 -0
  108. package/src/generated/ui.gen.json +1 -0
  109. package/src/index.ts +267 -0
  110. package/src/manifest.ts +60 -0
  111. package/src/planetscale-budget.ts +347 -0
  112. package/src/planetscale-capabilities.ts +3862 -0
  113. package/src/planetscale-collation-weights.gen.ts +13 -0
  114. package/src/planetscale-collation.ts +378 -0
  115. package/src/planetscale-conformance.ts +237 -0
  116. package/src/planetscale-connector.ts +571 -0
  117. package/src/planetscale-deploy.ts +197 -0
  118. package/src/planetscale-information-schema.ts +322 -0
  119. package/src/planetscale-mysql.ts +339 -0
  120. package/src/planetscale-roles.ts +71 -0
  121. package/src/planetscale-row.ts +43 -0
  122. package/src/planetscale-server.ts +162 -0
  123. package/src/planetscale-sql.ts +5957 -0
  124. package/src/planetscale-store.ts +869 -0
  125. package/src/planetscale-twin.ts +338 -0
  126. package/src/planetscale-values.ts +572 -0
  127. package/src/planetscale-wire.ts +274 -0
  128. package/src/semantics/psdb.ts +57 -0
package/src/index.ts ADDED
@@ -0,0 +1,267 @@
1
+ // @volter/twin-planetscale — the PlanetScale psdb HTTP-API twin, built on the shared @volter/world-core
2
+ // kernel. PlanetScale's serverless driver does not speak MySQL's binary protocol: it speaks
3
+ // `psdb.v1alpha1.Database`, a Connect RPC service addressed with unary JSON POSTs
4
+ // (`/psdb.v1alpha1.Database/CreateSession`, `/Execute`, `/CloseSession`) under HTTP Basic auth,
5
+ // with results packed as protojson `QueryResult` — base64 row bytes plus per-column byte lengths,
6
+ // `-1` for NULL, and every zero-valued field OMITTED.
7
+ //
8
+ // Behind that protocol is a REAL DATABASE: a stateful MySQL-subset engine with schemas
9
+ // (CREATE/DROP/TRUNCATE TABLE, SHOW TABLES, DESCRIBE), DML (INSERT/SELECT/UPDATE/DELETE with WHERE,
10
+ // ORDER BY, LIMIT/OFFSET, COUNT(*), ON DUPLICATE KEY UPDATE), MySQL's own errno/sqlstate errors
11
+ // (1146 no-such-table, 1062 duplicate entry, 1048 not-null, 1064 parse error, 1054 unknown column),
12
+ // AUTO_INCREMENT that survives deletes, NULL three-valued logic, and REAL TRANSACTIONS — so the
13
+ // unmodified client's `conn.transaction(async (tx) => …)`, which is nothing but BEGIN/…/COMMIT over
14
+ // the same session, genuinely commits or rolls back.
15
+ //
16
+ // THE HONEST CARVE-OUTS: a twin cannot make a wall clock pass or run a storage engine. `NOW()`
17
+ // and the other clock functions read the World clock's instant for the statement;
18
+ // `RAND()` and `UUID()` are REFUSED rather than frozen to a constant
19
+ // (the serve path must be a pure function of (request, stored state)); real elapsed query `timing`
20
+ // is never emitted; real Vitess sharding/resharding and MySQL's storage-engine physics are todos.
21
+ // PlanetScale's branches are modelled: a password acts on its own branch, `main` is the World's one
22
+ // image and every other branch a scope of its own (planetscale-store.ts, "BRANCH SCOPES"), and the
23
+ // `api` lane deploys a branch's schema changes into its base (api/src/semantics/deploy-requests.ts).
24
+ // The twin's limit: every database's `main` shares the one image. See README ## Coverage.
25
+ //
26
+ // State lives ENTIRELY in the kernel action log (no side-store): every table and every row is one
27
+ // kernel subject, so a restart against the same root answers the same `SELECT`.
28
+ export {
29
+ handlePlanetscaleTwinRequest,
30
+ planetscaleTwinSnapshot,
31
+ extractBasicCredential,
32
+ PSDB_SERVICE,
33
+ PSDB_METHODS,
34
+ PLANETSCALE_UNAUTHENTICATED,
35
+ PLANETSCALE_RESOURCE_TYPES,
36
+ } from './planetscale-twin.ts';
37
+ export type { PlanetscaleRequest, PlanetscaleResponse, PlanetscaleTwinSnapshot } from './planetscale-twin.ts';
38
+
39
+ // The SQL core — exported so a caller (or a reviewer) can drive MySQL semantics in-process,
40
+ // without HTTP. `runSql` over an image is the whole engine; nothing about the protocol is required
41
+ // to exercise it.
42
+ export {
43
+ runSql,
44
+ execStatement,
45
+ parseStatement,
46
+ tokenize,
47
+ evalExpr,
48
+ likeMatch,
49
+ emptyDatabase,
50
+ findTable,
51
+ tableRows,
52
+ tableKey,
53
+ nextRowId,
54
+ vitessTypeFor,
55
+ fieldForColumn,
56
+ isNumericVitessType,
57
+ unsupported,
58
+ parseError,
59
+ SqlError,
60
+ PLANETSCALE_LIMITS,
61
+ VITESS_TYPES,
62
+ BINARY_CHARSET,
63
+ UTF8MB4_CHARSET,
64
+ FLAG_NOT_NULL,
65
+ FLAG_PRI_KEY,
66
+ FLAG_UNIQUE_KEY,
67
+ FLAG_UNSIGNED,
68
+ FLAG_BINARY,
69
+ FLAG_AUTO_INCREMENT,
70
+ } from './planetscale-sql.ts';
71
+ export type { Cell, ColumnDef, Database, Expr, Field, QueryOutcome, RowRec, Statement, TableDef, VitessType, Write } from './planetscale-sql.ts';
72
+
73
+ // The wire codec. Exported because faithful row packing is a headline claim of this pack and a
74
+ // consumer (or a reviewer) must be able to decode a served response without the SDK.
75
+ export {
76
+ packRow,
77
+ unpackRow,
78
+ packBytes,
79
+ encodeCell,
80
+ encodeField,
81
+ encodeQueryResult,
82
+ makeSession,
83
+ sessionIdOf,
84
+ } from './planetscale-wire.ts';
85
+ export type { WireField, WireRow, WireQueryResult, WireError, WireSession, WireExecuteResponse, WireCreateSessionResponse } from './planetscale-wire.ts';
86
+
87
+ // The kernel binding — the projection and the session/transaction book.
88
+ export {
89
+ SERVICE,
90
+ DEFAULT_BRANCH,
91
+ DEFAULT_DATABASE,
92
+ loadState,
93
+ readDatabase,
94
+ executeSql,
95
+ createSession,
96
+ closeSession,
97
+ mintSessionId,
98
+ applyWritesToImage,
99
+ } from './planetscale-store.ts';
100
+ export type { ExecuteContext, ExecuteResult, SessionState, PlanetscaleResourceType } from './planetscale-store.ts';
101
+
102
+ export { createPlanetscaleTwinFetch, createPlanetscaleTwinServer } from './planetscale-server.ts';
103
+ export { createPlanetscaleMysqlStream, createPlanetscaleTwinStream } from './planetscale-mysql.ts';
104
+ export type { PlanetscaleServerOptions, PlanetscaleTwinFetchOptions } from './planetscale-server.ts';
105
+
106
+ export {
107
+ mapTable,
108
+ mapRow,
109
+ mapDescribeRow,
110
+ encodeCellValue,
111
+ quoteSqlLiteral,
112
+ quoteIdent,
113
+ pollTimestamp,
114
+ pullPlanetscaleTables,
115
+ pullPlanetscaleDatabase,
116
+ pullPlanetscaleSnapshot,
117
+ type PlanetscalePullReport,
118
+ type PlanetscaleRefreshReport,
119
+ syncPlanetscaleFromReal,
120
+ syncPlanetscaleFromRemote,
121
+ performPlanetscaleAction,
122
+ pushPlanetscaleAction,
123
+ pushPlanetscaleActions,
124
+ } from './planetscale-connector.ts';
125
+ export type { PlanetscaleLikeClient, PlanetscaleRealTable, PlanetscaleExecutedQuery, PlanetscalePullOptions } from './planetscale-connector.ts';
126
+
127
+ // The client-side rate budget — the fail-closed backstop every live call goes through. The
128
+ // MECHANISM is the kernel's shared, vendor-agnostic `RateBudget`; what lives here is this vendor's
129
+ // DECLARATION plus `guardPlanetscaleClient`, the choke point the connector entrypoints apply
130
+ // unconditionally. There is deliberately no export that disables the guard.
131
+ export {
132
+ PLANETSCALE_BUDGETED_METHODS,
133
+ PLANETSCALE_BUDGET_CEILING,
134
+ PLANETSCALE_BUDGET_MAX_RETRY_AFTER_S,
135
+ PLANETSCALE_BUDGET_WINDOW_MS,
136
+ PLANETSCALE_CALL_WEIGHTS,
137
+ PLANETSCALE_RATE_BUDGET,
138
+ PlanetscaleBudget,
139
+ PlanetscaleBudgetError,
140
+ planetscaleBudgetPath,
141
+ planetscaleCallWeight,
142
+ planetscaleClientBudget,
143
+ guardPlanetscaleClient,
144
+ } from './planetscale-budget.ts';
145
+ export type {
146
+ PlanetscaleBudgetErrorKind,
147
+ PlanetscaleBudgetOptions,
148
+ PlanetscaleBudgetReservation,
149
+ PlanetscaleBudgetSnapshot,
150
+ PlanetscaleBudgetedOptions,
151
+ } from './planetscale-budget.ts';
152
+
153
+ // Registry descriptor: the pack self-describes so tooling can discover it.
154
+ import { registerPack, type TwinPack } from '@volter/world-core';
155
+ import { performPlanetscaleAction, syncPlanetscaleFromRemote } from './planetscale-connector.ts';
156
+ import { PLANETSCALE_RATE_BUDGET as RATE_BUDGET } from './planetscale-budget.ts';
157
+ export const pack: TwinPack = {
158
+ protocol: '2',
159
+ stateSystem: { perform: performPlanetscaleAction, refresh: syncPlanetscaleFromRemote },
160
+ refresh: { onDemand: { atMost: '60s' } },
161
+ roundTrip: [
162
+ { method: 'POST', path: '/psdb.v1alpha1.Database/Execute', headers: { authorization: 'Basic dHdpbjp0d2lu' }, body: { query: 'CREATE TABLE IF NOT EXISTS round_trip (id BIGINT PRIMARY KEY AUTO_INCREMENT, body VARCHAR(255))' } },
163
+ { method: 'POST', path: '/psdb.v1alpha1.Database/Execute', headers: { authorization: 'Basic dHdpbjp0d2lu' }, body: { query: "INSERT INTO round_trip (body) VALUES ('round trip')" } },
164
+ ],
165
+ parityOrigin: 'http://twin',
166
+
167
+ // The SAME object planetscale-budget.ts declares at module load — one source of truth, so
168
+ // registering the pack and importing the connector can never arm two different ceilings.
169
+ rateBudget: RATE_BUDGET,
170
+ vendor: 'planetscale',
171
+ transport: 'rest',
172
+ nativeTransport: { protocol: 'mysql', flag: '--mysql', upstreamEnv: 'PLANETSCALE_TWIN_URL' },
173
+ endpointEnv: {
174
+ name: 'PLANETSCALE_TWIN_URL',
175
+ templates: { PLANETSCALE_DATABASE_URL: 'http://twin:twin@${host}:${port}' },
176
+ note: 'the @planetscale/database client reads PLANETSCALE_DATABASE_URL and honours http:; the injector also redirects *.psdb.cloud.',
177
+ },
178
+ // PRISMA — dub's `new PrismaClient({ omit })` names no adapter; on Prisma's client-engine
179
+ // build (the tab's, and the edge client's) that refuses to construct. The injector supplies
180
+ // this adapter, from the application's own `@prisma/adapter-planetscale`, over the endpoint
181
+ // template's PLANETSCALE_DATABASE_URL, which `@planetscale/database` honours as http:.
182
+ prismaAdapter: { adapter: '@prisma/adapter-planetscale', export: 'PrismaPlanetScale', urlEnv: 'PLANETSCALE_DATABASE_URL' },
183
+ archetype: 'crud',
184
+ bin: 'world-planetscale',
185
+ resources: ['table', 'row', '_session', '_backup'],
186
+ // ADOPTION — declared HERE, not in world-runtime's central SDK_TWINS / ENV_STEM_VENDORS tables
187
+ // (adding-a-twin.md §3; declaring a fact in both homes THROWS). `@planetscale/database` is the
188
+ // only official client of this API surface. `PLANETSCALE` is the credential-env stem: dub reads
189
+ // `PLANETSCALE_DATABASE_URL` (apps/web/lib/planetscale/connection.ts) — the exact var whose
190
+ // DATABASE_URL shape made `volter-world init` bin this vendor as generic infra and report the
191
+ // repo fully covered while a real call escaped (volter-ai/twin#255).
192
+ adoption: {
193
+ // No Python client for the psdb HTTP data API - `@planetscale/database` is JS-only, and Python
194
+ // callers reach PlanetScale over the MySQL wire protocol (a generic driver, not a vendor client).
195
+ pypi: [],
196
+ sdks: ['@planetscale/database'],
197
+ envStems: ['PLANETSCALE'],
198
+ },
199
+ // INTERCEPTION — the SDK addresses ONE host: whatever `config.host` resolves to from the
200
+ // connection URL (dist/index.js's constructor takes `url.hostname`, then `buildURL` rebuilds the
201
+ // request against it). PlanetScale's psdb data plane lives on per-region subdomains of
202
+ // `psdb.cloud` — `aws.connect.psdb.cloud`, `gcp.connect.psdb.cloud`, `<region>.connect.psdb.cloud`
203
+ // — so a SUFFIX matcher is required; an exact host would miss every region but one.
204
+ //
205
+ // `api.planetscale.com` is the MANAGEMENT API, the pack's `api` lane (api/src/, derived from PlanetScale's own
206
+ // Swagger document): the pack claims its /v1/organizations tree, where the lane serves databases, a branch's
207
+ // passwords and backups, and service tokens, and answers PlanetScale's 404 for every other operation; the host's
208
+ // other paths are unclaimed.
209
+ // app.planetscale.com's sign-in and service tokens page are screens the lane serves (api/src/screens/).
210
+ hosts: [{ suffix: '.psdb.cloud' }, { host: 'api.planetscale.com', pathPattern: '^/v1/organizations(/|$)' }, { host: 'app.planetscale.com', pathPattern: '^/(sign-in|[^/]+/settings/service-tokens)(/|$)' }],
211
+ // No endpoint env, and none INVENTED. `@planetscale/database` reads no environment variable of
212
+ // its own — `connect({url})` takes the URL from the application, which chooses the var name
213
+ // (dub's is PLANETSCALE_DATABASE_URL, Vercel's template uses DATABASE_URL). Interception here is
214
+ // the injector's, via the `hosts` suffix above, so an app-read endpoint var is not needed;
215
+ // emitting one anyway would let `covers` report coverage for an app that reads a different name.
216
+ pullPosture: 'on-demand',
217
+ pullPostureReason:
218
+ 'PlanetScale meters ROWS READ rather than requests, so a scheduled full-table pull is unbounded in cost by '
219
+ + 'construction: one SELECT can read millions of billable rows. Pull it explicitly when someone asks, '
220
+ + 'through the guarded connector and its mandatory row limit.',
221
+ specSource:
222
+ 'the installed @planetscale/database@1.20.1 package source (npm pack: dist/index.js, dist/cast.js, '
223
+ + 'dist/sanitization.js, dist/text.js, dist/index.d.ts) as the authoritative wire contract + its own test '
224
+ + 'fixtures for the error envelopes + github.com/mattrobenolt/ps-http-sim (the simulator upstream dub devs '
225
+ + 'run) and the psdb.v1alpha1 Connect service it implements + '
226
+ + 'planetscale.com/docs/reference/planetscale-system-limits for the published scalar limits; see '
227
+ + 'spec-sources.json.',
228
+ description:
229
+ "PlanetScale psdb HTTP-API twin — a real stateful MySQL-subset database (DDL, INSERT/SELECT/UPDATE/DELETE, "
230
+ + 'WHERE/ORDER BY/LIMIT, COUNT(*), ON DUPLICATE KEY UPDATE, AUTO_INCREMENT, NULL three-valued logic, real '
231
+ + "BEGIN/COMMIT/ROLLBACK transactions, MySQL errno/sqlstate errors) served over PlanetScale's Connect-JSON "
232
+ + 'wire protocol: POST /psdb.v1alpha1.Database/{CreateSession,Execute,CloseSession}, HTTP Basic auth, '
233
+ + 'protojson QueryResult with base64 row packing and -1 NULL lengths, query errors at HTTP 200 in the '
234
+ + '{error:{code,message}} envelope and auth failures at 401 in the same one. Non-deterministic SQL (NOW, '
235
+ + 'RAND, UUID) is refused, never frozen. Kernel-backed.',
236
+ browserRouting: { apiPathPrefix: '/psdb.v1alpha1.Database/', loaderHost: 'https://aws.connect.psdb.cloud' },
237
+ };
238
+
239
+ registerPack(pack);
240
+
241
+ /**
242
+ * How a journey reads an answer of this pack (a step's `view`, scripts/behavior-journey.ts). `rows` decodes an Execute
243
+ * answer into what MySQL's client prints for it: `fields`, the result's column names in order; `rows`, each row as an
244
+ * array of its cells as text (MySQL's text protocol spelling: numbers as their digits, NULL as null), in the order the
245
+ * answer holds them; `rowsAffected` and `insertId` as numbers where psdb answers them (0 when it omits them, as its
246
+ * protojson does for a zero); and `error`, `{code, message}`, when the statement was refused. Cells are the packed bytes
247
+ * decoded as UTF-8.
248
+ */
249
+ export const answerViews: Record<string, (body: unknown) => unknown> = {
250
+ rows: (body) => {
251
+ const b = (body ?? {}) as { result?: { fields?: Array<{ name?: string }>; rows?: Array<{ lengths?: string[]; values?: string }>; rowsAffected?: string; insertId?: string }; error?: { code?: string; message?: string } };
252
+ if (b.error) return { error: b.error };
253
+ const r = b.result ?? {};
254
+ const rows = (r.rows ?? []).map((row) => {
255
+ const bytes = Uint8Array.from(atob(row.values ?? ''), (c) => c.charCodeAt(0));
256
+ let at = 0;
257
+ return (row.lengths ?? []).map((size) => {
258
+ const width = Number(size);
259
+ if (width < 0) return null;
260
+ const cell = new TextDecoder().decode(bytes.subarray(at, at + width));
261
+ at += width;
262
+ return cell;
263
+ });
264
+ });
265
+ return { fields: (r.fields ?? []).map((f) => f.name ?? ''), rows, rowsAffected: Number(r.rowsAffected ?? 0), insertId: Number(r.insertId ?? 0) };
266
+ },
267
+ };
@@ -0,0 +1,60 @@
1
+ // PlanetScale's psdb manifest: the vendor facts its published proto does not carry (docs/contributing/architecture.md,
2
+ // "Protocol 3"). The surface is generated (./generated/surface.gen.json, from ../spec/proto by scripts/derive-pack.ts):
3
+ // the Connect service psdb.v1alpha1.Database, five RPCs, all `action`. The psdb wire has no REST resources: tables and
4
+ // rows are world-store subjects owned by planetscale-store.ts, and every MySQL semantic lives in planetscale-sql.ts.
5
+ //
6
+ // WHY NO `auth` AND NO `crossCutting`. psdb answers in two envelopes. A transport error (an unknown method, a malformed
7
+ // body) is Connect's flat `{code, message}` with a lowercase code, declared as `error` below. A query error is HTTP 200
8
+ // `{session, error:{code, message}}`, and an auth failure is HTTP 401 in that same `{session, error}` envelope (the
9
+ // literal fixture of @planetscale/database's own suite; planetscale-twin.ts's header). Read-only is decided per
10
+ // statement: a write on a read-only twin is MySQL's errno 1290 inside a 200 `{session, error}`, while a SELECT is
11
+ // answered. The kernel's `crossCutting` refuses auth through the manifest's one error template and read-only per
12
+ // operation, so it cannot state either rule; the psdb handlers (semantics/psdb.ts) keep auth, the per-statement
13
+ // read-only rule and the malformed-body check, in today's order and with today's bodies, and the pack's fetch is served
14
+ // without `around`. `auth` is not declared, since nothing would enforce it: psdb reads `Authorization: Basic
15
+ // base64(username:password)`, any branch password (planetscale-twin.ts, `extractBasicCredential`).
16
+ import type { DerivedManifest, StateField } from '@volter/world-core';
17
+
18
+ // ps-http-sim, the simulator PlanetScale's driver authors publish for local development: its CloseSession handler
19
+ // closes the session's connection when the request names one (`closeConn(mysqlConnKey{…})`) and answers the session
20
+ // reset, whatever the connection's state.
21
+ const PS_HTTP_SIM = 'https://github.com/mattrobenolt/ps-http-sim/blob/main/main.go';
22
+
23
+ /** A session's `closed`. CreateSession opens one (the initial state); CloseSession closes an open one, discarding any
24
+ * uncommitted transaction as a dropped connection does. Closing one already closed moves nothing and answers 200, as
25
+ * ps-http-sim answers every close. Not moves of a session, and the twin's decisions where nothing shows the vendor's:
26
+ * a CloseSession naming a session the twin never issued answers 200 and writes nothing; an Execute naming a closed
27
+ * session runs in a new session the twin opens for it (planetscale-store.ts, `executeSqlUnlocked`), as an Execute
28
+ * naming none does (ps-http-sim: "if !clientSession { sess = session.New(…) }"). */
29
+ const sessionClosed: StateField = {
30
+ initial: false,
31
+ transitions: [
32
+ { operation: 'CloseSession', from: ['false'], to: 'true', source: PS_HTTP_SIM },
33
+ { operation: 'CloseSession', from: ['true'], source: PS_HTTP_SIM },
34
+ ],
35
+ };
36
+
37
+ export const manifest: DerivedManifest = {
38
+ vendor: 'planetscale',
39
+ service: 'planetscale',
40
+ // the driver always sends JSON (`Content-Type: application/json`, @planetscale/database dist/index.js `postJSON`)
41
+ body: { json: 'always' },
42
+ // the store mints a session `tws-<n>-<the World instant in ms>` (planetscale-store.ts, `mintSessionId`), stored as
43
+ // subject `session:<id>`; the core mints none (it owns no operation), so the template names the counted part only
44
+ ids: { template: '{prefix}-{n}' },
45
+ time: 'iso',
46
+ // Connect's transport-level error body: a lowercase code and a message, no wrapper
47
+ error: { code: '{code}', message: '{message}' },
48
+ // a whole-operation refusal of a write to a read-only twin; the psdb handlers refuse per statement (errno 1290 in a
49
+ // 200 `{session, error}`) and never reach this
50
+ readOnly: { status: 403, code: 'permission_denied', message: 'twin is read-only; omit readOnly to accept writes' },
51
+ malformedBody: { status: 400, code: 'invalid_argument', message: 'malformed JSON request body' },
52
+ // Connect's not_found; psdb addresses nothing by id in a path, so no operation answers it
53
+ notFound: { status: 404, code: 'not_found', message: 'not found' },
54
+ // psdb has no list operation: an Execute's rows are its QueryResult. Required by the type; nothing reads it.
55
+ list: { style: 'envelope', envelope: { data: '{data}' }, limit: { param: 'limit', default: 100, max: 100 } },
56
+ deleted: {},
57
+ resources: {
58
+ Session: { storedAs: '_session', idPrefix: 'tws', state: { closed: sessionClosed } },
59
+ },
60
+ };