@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
@@ -0,0 +1,39 @@
1
+ // Shared row identity and SQL quoting for local execution, observation and deployment.
2
+ import { PULLED_ROWID_BASE, tableKey } from "./planetscale-sql.js";
3
+ export function pulledRowId(table, identity, ordinal = 0) {
4
+ // JSON tuple encoding distinguishes embedded separators and duplicate keyless rows.
5
+ let h = 0x811c9dc5;
6
+ const text = JSON.stringify([tableKey(table), identity, ordinal]);
7
+ for (let i = 0; i < text.length; i++) {
8
+ h ^= text.charCodeAt(i);
9
+ h = Math.imul(h, 0x01000193) >>> 0;
10
+ }
11
+ return PULLED_ROWID_BASE + h;
12
+ }
13
+ export function keyedRowIdentity(table, cells) {
14
+ const keys = Object.keys(cells);
15
+ const identity = table.primaryKey.map(k => k.toLowerCase()).sort().map(k => JSON.stringify([k, cells[keys.find(n => n.toLowerCase() === k) ?? k] ?? null]));
16
+ return identity;
17
+ }
18
+ export function quoteSqlLiteral(value) {
19
+ if (value === null)
20
+ return 'NULL';
21
+ const escaped = value.replace(/[\0\b\n\r\t\x1a\\"']/g, (c) => {
22
+ switch (c) {
23
+ case '"': return '\\"';
24
+ case "'": return "\\'";
25
+ case '\n': return '\\n';
26
+ case '\r': return '\\r';
27
+ case '\t': return '\\t';
28
+ case '\\': return '\\\\';
29
+ case '\0': return '\\0';
30
+ case '\b': return '\\b';
31
+ default: return '\\Z';
32
+ }
33
+ });
34
+ return `'${escaped}'`;
35
+ }
36
+ export const quoteIdent = (name) => `\`${name.replace(/`/g, '``')}\``;
37
+ export function keyedRowId(table, cells) {
38
+ return pulledRowId(table.name, keyedRowIdentity(table, cells));
39
+ }
@@ -0,0 +1,42 @@
1
+ import { DEFAULT_BRANCH, DEFAULT_DATABASE } from './planetscale-store.js';
2
+ import { type DerivedFetch } from '@volter/world-core';
3
+ export type PlanetscaleServerOptions = {
4
+ root?: string;
5
+ port?: number;
6
+ readOnly?: boolean;
7
+ /** The username the twin demands. Omit to accept any non-empty username. */
8
+ username?: string;
9
+ /** The password the twin demands. Omit to accept any non-empty password (still 401s a missing one). */
10
+ password?: string;
11
+ /** The schema name the twin presents. */
12
+ database?: string;
13
+ /** The branch name `CreateSession` reports. */
14
+ branch?: string;
15
+ /** The injected clock. Returns the ISO instant this request "happens at". */
16
+ now?: () => string;
17
+ };
18
+ /** Options every planetscale-twin HTTP surface needs, independent of who owns the socket. */
19
+ export interface PlanetscaleTwinFetchOptions {
20
+ root?: string;
21
+ readOnly?: boolean;
22
+ /** The username the twin demands. Omit to accept any non-empty username. */
23
+ username?: string;
24
+ /** The password the twin demands. Omit to accept any non-empty password (still 401s a missing one). */
25
+ password?: string;
26
+ /** The schema name the twin presents. */
27
+ database?: string;
28
+ /** The branch name `CreateSession` reports. */
29
+ branch?: string;
30
+ /** The injected clock. Returns the ISO instant this request "happens at". */
31
+ now?: () => string;
32
+ }
33
+ /** The pack's wire (docs/contributing/architecture.md, "Protocol 3"): the twin's doors in front, the management API and
34
+ * app.planetscale.com's pages to the `api` lane, and the derived dispatch over psdb's proto: CreateSession, Execute,
35
+ * CloseSession and Prepare reach their handlers (semantics/psdb.ts), and everything else is the gap (psdbGap). No
36
+ * `around`: manifest.ts says why. */
37
+ export declare function createPlanetscaleTwinFetch(options?: PlanetscaleTwinFetchOptions): DerivedFetch;
38
+ export declare function createPlanetscaleTwinServer(options?: PlanetscaleServerOptions): Promise<{
39
+ port: number;
40
+ stop: () => void;
41
+ }>;
42
+ export { DEFAULT_BRANCH, DEFAULT_DATABASE };
@@ -0,0 +1,137 @@
1
+ // planetscale twin HTTP server — serve the psdb wire twin over HTTP so the real
2
+ // `@planetscale/database` client, pointed at it with nothing but its own public `url`/`host`
3
+ // option, works UNMODIFIED.
4
+ //
5
+ // ── THE ONE THING AN INTEGRATOR MUST KNOW ─────────────────────────────────────────────────────
6
+ // The client forces `https:` for every scheme except a literal `http:` (dist/index.js `protocol`),
7
+ // so a local twin is reachable by passing `url: 'http://user:pass@127.0.0.1:PORT'` — that is
8
+ // CONFIGURATION through the client's own public option, not a modification. `PLANETSCALE_DATABASE_URL`
9
+ // (what dub reads) takes exactly that shape.
10
+ //
11
+ // ── THE INJECTED CLOCK ────────────────────────────────────────────────────────────────────────
12
+ // `now` produces the `occurredAt` the kernel stamps each action with. It defaults to the wall clock
13
+ // (a server has to), but a test passes its own so a run is reproducible. Nothing in a SERVED
14
+ // response is derived from it except the kernel's own action ordinals — the response body is a pure
15
+ // function of (request, stored state), which is why `timing` is never emitted.
16
+ //
17
+ // FETCH-FIRST (runtime contract R12b): the surface is the plain `createPlanetscaleTwinFetch` and
18
+ // the SERVER is one line of `serveHttp` around it. The fetch is the derived dispatch over psdb's
19
+ // proto (`createDerivedFetch`), with the pack's earlier fetch as its `legacy`: psdb replies carry
20
+ // ONLY the handler's own headers (no default `content-type` is added), and the clock is injectable
21
+ // per instance.
22
+ import { serveHttp } from '@volter/world-core';
23
+ import { createPlanetscaleApiLaneFetch } from "../api/src/fetch.js";
24
+ import { PSDB_METHODS, PSDB_SERVICE } from "./planetscale-twin.js";
25
+ import { psdbHandlers, psdbInternal } from "./semantics/psdb.js";
26
+ import surface from './generated/surface.gen.json' with { type: 'json' };
27
+ import { DEFAULT_BRANCH, DEFAULT_DATABASE, STORE_NAMES, planetscaleStore } from "./planetscale-store.js";
28
+ import { createDerivedFetch, worldNow, statefulTwinManifest } from '@volter/world-core';
29
+ /** The twin's own doors, in front of the vendor's dispatch (docs/contributing/architecture.md, "Doors, screens and the
30
+ * gap"): `GET /twin`, the discovery manifest, and `GET /twin/store/<tables|rows>`, the store door. Undefined for any
31
+ * other request. */
32
+ function twinDoors(request, options) {
33
+ if (request.method !== 'GET')
34
+ return undefined;
35
+ const cleanPath = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
36
+ if (cleanPath === '/twin')
37
+ return discovery();
38
+ // THE STORE DOOR: `GET /twin/store/<tables|rows>` — this pack's own named, read-only,
39
+ // deterministic projection over stored state. The psdb wire is Connect-JSON and unary
40
+ // Connect is POST-ONLY, so even `Execute` (the vendor's read) is a POST. Nothing in the vendor
41
+ // surface can observe stored state with a GET, which is what this door is for — and it is what
42
+ // makes the pack's determinism checkable at the resource level (R9). Twin surface, not vendor
43
+ // surface: keyless.
44
+ if (cleanPath.startsWith('/twin/store/'))
45
+ return storeDoor(cleanPath.slice('/twin/store/'.length), options);
46
+ return undefined;
47
+ }
48
+ /** `GET /twin`. The manifest EDUCATES: it names the store door beside the others, so a caller who only ever sees
49
+ * `GET /twin` can find the one GET read this POST-only protocol has. */
50
+ function discovery() {
51
+ const manifest = statefulTwinManifest({
52
+ vendor: 'planetscale',
53
+ twinOf: 'PlanetScale (psdb.v1alpha1.Database HTTP wire protocol)',
54
+ stores: 'a real MySQL-subset schema and its rows, served over the psdb Connect-JSON envelope',
55
+ });
56
+ return Response.json({
57
+ ...manifest,
58
+ stores: STORE_NAMES,
59
+ doors: { ...manifest.doors, store: 'GET /twin/store/<name>' },
60
+ });
61
+ }
62
+ /** `GET /twin/store/<tables|rows>`, the store door. */
63
+ function storeDoor(name, options) {
64
+ if (!STORE_NAMES.includes(name)) {
65
+ return Response.json({ error: 'unknown store', store: name, stores: STORE_NAMES }, { status: 404 });
66
+ }
67
+ return Response.json(planetscaleStore(name, options.root, options.database ?? DEFAULT_DATABASE));
68
+ }
69
+ /**
70
+ * The gap: Connect's own answer for a request psdb does not serve. A path outside the service is `not_found` naming the
71
+ * service it asked for; a method the twin does not model is `unimplemented`, as Connect answers a method a service lacks;
72
+ * a psdb method asked with a verb other than POST is refused, since unary Connect is POST-only. StreamExecute is the gap:
73
+ * @planetscale/database never calls it (its dist/index.js sends only CreateSession and Execute) though ps-http-sim, the
74
+ * simulator PlanetScale's driver authors publish, serves it as a server stream of ExecuteResponse.
75
+ */
76
+ function psdbGap(request) {
77
+ const path = new URL(request.url).pathname.replace(/\/+$/, '') || '/';
78
+ const prefix = `/${PSDB_SERVICE}/`;
79
+ const json = (status, code, message) => Response.json({ code, message }, { status });
80
+ if (!path.startsWith(prefix))
81
+ return json(404, 'not_found', `unknown service: ${path.replace(/^\//, '').split('/')[0] ?? ''}`);
82
+ const method = path.slice(prefix.length);
83
+ if (request.method.toUpperCase() !== 'POST' && PSDB_METHODS.includes(method)) {
84
+ return json(405, 'unimplemented', `HTTP ${request.method.toUpperCase()} is not supported by this protocol`);
85
+ }
86
+ return json(404, 'unimplemented', `${PSDB_SERVICE}/${method} is not implemented`);
87
+ }
88
+ /** app.planetscale.com's pages the `api` lane serves (its sign-in, the service tokens settings and a deploy request's
89
+ * page), and the World's door
90
+ * that gives a person their PlanetScale password. */
91
+ const SETTINGS_PAGE = /^\/(?:sign-in|_twin\/users\/[^/]+\/password|[^/]+\/settings\/service-tokens(?:\/.*)?|[^/]+\/[^/]+\/deploy-requests\/\d+(?:\/review)?)$/;
92
+ /** The pack's wire (docs/contributing/architecture.md, "Protocol 3"): the twin's doors in front, the management API and
93
+ * app.planetscale.com's pages to the `api` lane, and the derived dispatch over psdb's proto: CreateSession, Execute,
94
+ * CloseSession and Prepare reach their handlers (semantics/psdb.ts), and everything else is the gap (psdbGap). No
95
+ * `around`: manifest.ts says why. */
96
+ export function createPlanetscaleTwinFetch(options = {}) {
97
+ const now = options.now ?? (() => worldNow());
98
+ const psdb = createDerivedFetch({
99
+ surface: surface,
100
+ handlers: psdbHandlers(options, now),
101
+ gap: (request) => psdbGap(request),
102
+ });
103
+ // api.planetscale.com's management API is the pack's `api` lane (../api/src/fetch.ts)
104
+ const api = createPlanetscaleApiLaneFetch({ ...(options.root !== undefined ? { root: options.root } : {}), readOnly: options.readOnly ?? false, clock: now });
105
+ return Object.assign(async (request) => {
106
+ const door = twinDoors(request, options);
107
+ if (door)
108
+ return door;
109
+ const path = new URL(request.url).pathname;
110
+ if (path === '/v1/organizations' || path.startsWith('/v1/organizations/') || SETTINGS_PAGE.test(path)) {
111
+ try {
112
+ return await api(request);
113
+ }
114
+ catch (error) {
115
+ return psdbInternal(error);
116
+ }
117
+ }
118
+ // a move the World clock makes happens when any request next arrives (a deploy finishing changes the branch psdb
119
+ // reads), so the management lane catches up before psdb answers
120
+ try {
121
+ await api.catchUp(request);
122
+ }
123
+ catch (error) {
124
+ return psdbInternal(error);
125
+ }
126
+ return psdb(request);
127
+ }, { owners: () => psdb.owners() });
128
+ }
129
+ export async function createPlanetscaleTwinServer(options = {}) {
130
+ const server = await serveHttp({
131
+ port: options.port ?? 0,
132
+ idleTimeout: 60,
133
+ fetch: createPlanetscaleTwinFetch(options),
134
+ });
135
+ return { port: server.port ?? options.port ?? 0, stop: () => server.stop(true) };
136
+ }
137
+ export { DEFAULT_BRANCH, DEFAULT_DATABASE };