@sponson/sim 0.0.0-stage → 0.5.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 +9 -2
- package/dist/auth.d.ts +60 -0
- package/dist/auth.js +195 -0
- package/dist/auth.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +16 -0
- package/dist/bin.js.map +1 -0
- package/dist/chaos.d.ts +74 -0
- package/dist/chaos.js +80 -0
- package/dist/chaos.js.map +1 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -0
- package/dist/demo.d.ts +39 -0
- package/dist/demo.js +168 -0
- package/dist/demo.js.map +1 -0
- package/dist/graphql.d.ts +22 -0
- package/dist/graphql.js +36 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +138 -0
- package/dist/provider.js +68 -0
- package/dist/provider.js.map +1 -0
- package/dist/routes/algolia.d.ts +37 -0
- package/dist/routes/algolia.js +83 -0
- package/dist/routes/algolia.js.map +1 -0
- package/dist/routes/aws.d.ts +43 -0
- package/dist/routes/aws.js +211 -0
- package/dist/routes/aws.js.map +1 -0
- package/dist/routes/clerk.d.ts +33 -0
- package/dist/routes/clerk.js +85 -0
- package/dist/routes/clerk.js.map +1 -0
- package/dist/routes/cloudflare.d.ts +80 -0
- package/dist/routes/cloudflare.js +176 -0
- package/dist/routes/cloudflare.js.map +1 -0
- package/dist/routes/digitalocean.d.ts +58 -0
- package/dist/routes/digitalocean.js +158 -0
- package/dist/routes/digitalocean.js.map +1 -0
- package/dist/routes/fly.d.ts +22 -0
- package/dist/routes/fly.js +113 -0
- package/dist/routes/fly.js.map +1 -0
- package/dist/routes/launchdarkly.d.ts +98 -0
- package/dist/routes/launchdarkly.js +213 -0
- package/dist/routes/launchdarkly.js.map +1 -0
- package/dist/routes/neon.d.ts +73 -0
- package/dist/routes/neon.js +184 -0
- package/dist/routes/neon.js.map +1 -0
- package/dist/routes/netlify.d.ts +126 -0
- package/dist/routes/netlify.js +347 -0
- package/dist/routes/netlify.js.map +1 -0
- package/dist/routes/planetscale.d.ts +95 -0
- package/dist/routes/planetscale.js +234 -0
- package/dist/routes/planetscale.js.map +1 -0
- package/dist/routes/railway.d.ts +24 -0
- package/dist/routes/railway.js +112 -0
- package/dist/routes/railway.js.map +1 -0
- package/dist/routes/rest.d.ts +138 -0
- package/dist/routes/rest.js +462 -0
- package/dist/routes/rest.js.map +1 -0
- package/dist/routes/supabase.d.ts +103 -0
- package/dist/routes/supabase.js +234 -0
- package/dist/routes/supabase.js.map +1 -0
- package/dist/routes/vercel.d.ts +114 -0
- package/dist/routes/vercel.js +370 -0
- package/dist/routes/vercel.js.map +1 -0
- package/dist/routes/xata.d.ts +57 -0
- package/dist/routes/xata.js +115 -0
- package/dist/routes/xata.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +189 -0
- package/dist/server.js.map +1 -0
- package/dist/state.d.ts +102 -0
- package/dist/state.js +121 -0
- package/dist/state.js.map +1 -0
- package/package.json +51 -3
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated PlanetScale: organizations, databases, branches and branch passwords (Vitess / MySQL databases).
|
|
3
|
+
*
|
|
4
|
+
* Assumptions about the real API that this fake encodes, each marked with how far it is checked. "Verified" means
|
|
5
|
+
* against PlanetScale's published OpenAPI document (https://planetscale.com/docs/openapi.yaml, fetched 2026-10-11)
|
|
6
|
+
* or the documentation page cited; see docs/api-verification.md, section "PlanetScale". The ones a wrong guess
|
|
7
|
+
* would break are pinned in scenarios/contract.test.ts.
|
|
8
|
+
* PS1. Authentication is `Authorization: <SERVICE_TOKEN_ID>:<SERVICE_TOKEN>`, no scheme word (verified:
|
|
9
|
+
* https://planetscale.com/docs/api/reference/service-tokens). The sim answers 401 to any other form,
|
|
10
|
+
* including `Bearer …`. The CLI's variable names, PLANETSCALE_SERVICE_TOKEN_ID and PLANETSCALE_SERVICE_TOKEN,
|
|
11
|
+
* are the ones the adapter reads (verified: https://planetscale.com/docs/cli/service-tokens.md).
|
|
12
|
+
* PS2. Every path is under `/organizations/{organization}/databases/{database}`, and branches are addressed by
|
|
13
|
+
* name, not id: GET and DELETE `/branches/{branch}` (verified: path parameter "The name of the branch").
|
|
14
|
+
* PS3. Lists (`list_branches`, `list_passwords`) are `{ data, next_page, … }`; the next page is `?page=<n>`
|
|
15
|
+
* and `next_page` is null on the last page; `per_page` defaults to 25 (verified). The sim pages at 25, or
|
|
16
|
+
* at chaos `page_size` when set.
|
|
17
|
+
* PS4. POST `/branches` with `{ name, parent_branch }` answers 201 with the branch (verified), which is not
|
|
18
|
+
* ready yet: `ready: false`, `state: "pending"` (fields and enum verified; that a new branch starts not
|
|
19
|
+
* ready is verified only through the CLI's `--wait`, "Wait until the branch is ready",
|
|
20
|
+
* https://planetscale.com/docs/cli/branch.md). How long it takes is unverified; the sim reports ready
|
|
21
|
+
* from the first GET, or after chaos `planetscale_ready_ms`.
|
|
22
|
+
* PS5. Creating a branch whose name exists answers 422 (unverified: the spec lists 401/403/404/422/429/500 and
|
|
23
|
+
* no 409; the body's wording is unknown). The adapter re-reads the branch by name after any 409 or 422.
|
|
24
|
+
* PS6. A parent branch that does not exist answers 404 here (unverified); the adapter checks the parent with
|
|
25
|
+
* GET first, so the exact answer does not matter to it.
|
|
26
|
+
* PS7. DELETE `/branches/{branch}` answers 204 (verified) and the branch is gone at once (unverified: needs a
|
|
27
|
+
* live account). Deleting a branch deletes its passwords (unverified).
|
|
28
|
+
* PS8. POST `/branches/{branch}/passwords` with `{ name, role }` answers 201 with `id`, `name`, `role`,
|
|
29
|
+
* `username`, `access_host_url` and `plain_text` (verified). `plain_text` is null in every other response
|
|
30
|
+
* (verified: "Null except in the response from the create endpoint"); the CLI docs say the password "is
|
|
31
|
+
* shown once … and cannot be retrieved afterwards" (https://planetscale.com/docs/cli/password.md).
|
|
32
|
+
* PS9. Password names are not unique (unverified: the spec calls `name` "Optional name of the password" and
|
|
33
|
+
* documents no uniqueness). The sim accepts duplicates; the adapter refuses to guess between two.
|
|
34
|
+
* PS10. A password's role cannot be changed in place: PATCH takes only `name` and `cidrs` (verified).
|
|
35
|
+
* PS11. DELETE `/branches/{branch}/passwords/{id}` answers 204 (verified); a password that does not exist, 404.
|
|
36
|
+
* PS12. Creating a password on a branch that is not ready yet is refused (unverified: the real behaviour is
|
|
37
|
+
* unknown, so the sim takes the strict reading and answers 422); the adapter waits for `ready` first.
|
|
38
|
+
* PS13. Error bodies are `{ code, message }` (unverified; not in the spec). The adapter never reads them.
|
|
39
|
+
* PS14. Branch names: the sim accepts any non-empty name. Which characters the real API accepts is
|
|
40
|
+
* unverified; the adapter's default name uses only lowercase letters, digits and dashes.
|
|
41
|
+
*/
|
|
42
|
+
import { Reply, route, router } from "../provider.js";
|
|
43
|
+
/** The access host every simulated password hands out (the real API's `access_host_url`). */
|
|
44
|
+
export const PLANETSCALE_SIM_HOST = "aws.connect.psdb.sim";
|
|
45
|
+
/** The simulated PlanetScale API, registered in `PROVIDERS` (packages/sim/src/state.ts) and served under `/planetscale`. */
|
|
46
|
+
export const planetscaleSim = {
|
|
47
|
+
env: { token: "PLANETSCALE_SERVICE_TOKEN", url: "PLANETSCALE_API_URL", testToken: "tok_planetscale", more: { PLANETSCALE_SERVICE_TOKEN_ID: "tokid_planetscale" } },
|
|
48
|
+
defaultSeed: { organizations: { acme: { app: [{ name: "main", production: true }] } } },
|
|
49
|
+
/** PS1: `Authorization: <id>:<token>`, no `Bearer`. */
|
|
50
|
+
authorized(headers) {
|
|
51
|
+
return /^[^\s:]+:\S+$/.test(headers.authorization ?? "");
|
|
52
|
+
},
|
|
53
|
+
reset(core, seed) {
|
|
54
|
+
const organizations = {};
|
|
55
|
+
for (const [org, dbs] of Object.entries(seed?.organizations ?? {})) {
|
|
56
|
+
const databases = {};
|
|
57
|
+
for (const [db, branches] of Object.entries(dbs)) {
|
|
58
|
+
const list = [];
|
|
59
|
+
for (const b of branches) {
|
|
60
|
+
const branch = createPsBranch(core, list, b.name, b.parent ?? null, "sim", b.production === true);
|
|
61
|
+
for (const pw of b.passwords ?? [])
|
|
62
|
+
createPsPassword(core, branch, pw.name, pw.role ?? "admin", "sim");
|
|
63
|
+
}
|
|
64
|
+
databases[db] = { branches: list };
|
|
65
|
+
}
|
|
66
|
+
organizations[org] = { databases };
|
|
67
|
+
}
|
|
68
|
+
return { organizations };
|
|
69
|
+
},
|
|
70
|
+
/**
|
|
71
|
+
* `branch.<name>` or `password.<branch>.<name>` (or `planetscale:<organization>.…`): "delete", or "recreate"
|
|
72
|
+
* (same name, new id; a re-created password has a new plaintext) — what a human in the console would do.
|
|
73
|
+
*/
|
|
74
|
+
drift(core, state, { key, rest, value, only }) {
|
|
75
|
+
const [kind, ...path] = rest.split(".");
|
|
76
|
+
if (kind !== "branch" && kind !== "password")
|
|
77
|
+
return false;
|
|
78
|
+
if (value !== "delete" && value !== "recreate")
|
|
79
|
+
throw new Error(`drift ${key}: only "delete" and "recreate" are supported`);
|
|
80
|
+
let hit = false;
|
|
81
|
+
for (const [org, o] of Object.entries(state.organizations)) {
|
|
82
|
+
if (only !== undefined && org !== only)
|
|
83
|
+
continue;
|
|
84
|
+
for (const d of Object.values(o.databases))
|
|
85
|
+
hit = (kind === "branch" ? driftBranch(core, d.branches, path.join("."), value) : driftPassword(core, d.branches, path, value)) || hit;
|
|
86
|
+
}
|
|
87
|
+
if (!hit)
|
|
88
|
+
throw new Error(`drift ${key}: nothing named ${path.join(".")}`);
|
|
89
|
+
return true;
|
|
90
|
+
},
|
|
91
|
+
routes(core, state, req) {
|
|
92
|
+
return routes(core, state, req);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
function driftBranch(core, branches, name, value) {
|
|
96
|
+
const i = branches.findIndex((b) => b.name === name);
|
|
97
|
+
if (i < 0)
|
|
98
|
+
return false;
|
|
99
|
+
const [old] = branches.splice(i, 1);
|
|
100
|
+
if (value === "recreate")
|
|
101
|
+
createPsBranch(core, branches, old.name, old.parent_branch, "sim", old.production);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
function driftPassword(core, branches, path, value) {
|
|
105
|
+
const [branchName, ...rest] = path;
|
|
106
|
+
const b = branches.find((x) => x.name === branchName);
|
|
107
|
+
const pw = b?.passwords.find((p) => p.name === rest.join("."));
|
|
108
|
+
if (!b || !pw)
|
|
109
|
+
return false;
|
|
110
|
+
b.passwords = b.passwords.filter((p) => p !== pw);
|
|
111
|
+
if (value === "recreate")
|
|
112
|
+
createPsPassword(core, b, pw.name, pw.role, "sim");
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
function dbOf(state, org, db) {
|
|
116
|
+
return state.organizations[org]?.databases[db];
|
|
117
|
+
}
|
|
118
|
+
const ROLES = new Set(["reader", "writer", "admin", "readwriter"]);
|
|
119
|
+
/** One route per method and path; `:name` segments arrive URI-decoded in `params`. */
|
|
120
|
+
const routes = router([
|
|
121
|
+
route("GET", "/organizations/:org/databases/:db/branches", ({ core, state, params, url }) => {
|
|
122
|
+
const d = dbOf(state, params.org, params.db);
|
|
123
|
+
return d ? listReply(core, d.branches.map(publicBranch), url) : notFound();
|
|
124
|
+
}),
|
|
125
|
+
route("POST", "/organizations/:org/databases/:db/branches", ({ core, state, params, body }) => {
|
|
126
|
+
const d = dbOf(state, params.org, params.db);
|
|
127
|
+
if (!d)
|
|
128
|
+
return notFound();
|
|
129
|
+
const b = (body ?? {});
|
|
130
|
+
if (typeof b.name !== "string" || b.name === "")
|
|
131
|
+
return unprocessable("Name can't be blank");
|
|
132
|
+
if (d.branches.some((x) => x.name === b.name))
|
|
133
|
+
return unprocessable("Name has already been taken"); // PS5
|
|
134
|
+
const parent = typeof b.parent_branch === "string" ? b.parent_branch : (d.branches.find((x) => x.production)?.name ?? null);
|
|
135
|
+
if (parent !== null && !d.branches.some((x) => x.name === parent))
|
|
136
|
+
return notFound(); // PS6
|
|
137
|
+
const branch = createPsBranch(core, d.branches, b.name, parent, "api", false);
|
|
138
|
+
branch.readyAt = Date.now() + core.chaos.planetscale_ready_ms;
|
|
139
|
+
// PS4: the create answer always reports a branch that is still being provisioned.
|
|
140
|
+
return new Reply(201, { ...publicBranch(branch), ready: false, state: "pending" });
|
|
141
|
+
}),
|
|
142
|
+
route("GET", "/organizations/:org/databases/:db/branches/:branch", ({ state, params }) => {
|
|
143
|
+
const b = dbOf(state, params.org, params.db)?.branches.find((x) => x.name === params.branch);
|
|
144
|
+
return b ? new Reply(200, publicBranch(b)) : notFound();
|
|
145
|
+
}),
|
|
146
|
+
route("DELETE", "/organizations/:org/databases/:db/branches/:branch", ({ state, params }) => {
|
|
147
|
+
const d = dbOf(state, params.org, params.db);
|
|
148
|
+
const b = d?.branches.find((x) => x.name === params.branch);
|
|
149
|
+
if (!d || !b)
|
|
150
|
+
return notFound();
|
|
151
|
+
if (b.production)
|
|
152
|
+
return unprocessable("Cannot delete a production branch");
|
|
153
|
+
d.branches.splice(d.branches.indexOf(b), 1); // PS7
|
|
154
|
+
return new Reply(204, undefined);
|
|
155
|
+
}),
|
|
156
|
+
route("GET", "/organizations/:org/databases/:db/branches/:branch/passwords", ({ core, state, params, url }) => {
|
|
157
|
+
const b = dbOf(state, params.org, params.db)?.branches.find((x) => x.name === params.branch);
|
|
158
|
+
return b ? listReply(core, b.passwords.map((p) => publicPassword(b, p)), url) : notFound();
|
|
159
|
+
}),
|
|
160
|
+
route("POST", "/organizations/:org/databases/:db/branches/:branch/passwords", ({ core, state, params, body }) => {
|
|
161
|
+
const b = dbOf(state, params.org, params.db)?.branches.find((x) => x.name === params.branch);
|
|
162
|
+
if (!b)
|
|
163
|
+
return notFound();
|
|
164
|
+
if (Date.now() < b.readyAt)
|
|
165
|
+
return unprocessable("Branch is not ready yet"); // PS12
|
|
166
|
+
const p = (body ?? {});
|
|
167
|
+
const role = p.role === undefined ? "admin" : p.role;
|
|
168
|
+
if (typeof role !== "string" || !ROLES.has(role))
|
|
169
|
+
return unprocessable("Role is not included in the list");
|
|
170
|
+
const pw = createPsPassword(core, b, typeof p.name === "string" ? p.name : "", role, "api");
|
|
171
|
+
return new Reply(201, { ...publicPassword(b, pw), plain_text: pw.plainText }); // PS8
|
|
172
|
+
}),
|
|
173
|
+
route("GET", "/organizations/:org/databases/:db/branches/:branch/passwords/:id", ({ state, params }) => {
|
|
174
|
+
const b = dbOf(state, params.org, params.db)?.branches.find((x) => x.name === params.branch);
|
|
175
|
+
const pw = b?.passwords.find((p) => p.id === params.id);
|
|
176
|
+
return b && pw ? new Reply(200, publicPassword(b, pw)) : notFound();
|
|
177
|
+
}),
|
|
178
|
+
route("DELETE", "/organizations/:org/databases/:db/branches/:branch/passwords/:id", ({ state, params }) => {
|
|
179
|
+
const b = dbOf(state, params.org, params.db)?.branches.find((x) => x.name === params.branch);
|
|
180
|
+
const pw = b?.passwords.find((p) => p.id === params.id);
|
|
181
|
+
if (!b || !pw)
|
|
182
|
+
return notFound(); // PS11
|
|
183
|
+
b.passwords = b.passwords.filter((p) => p !== pw);
|
|
184
|
+
return new Reply(204, undefined);
|
|
185
|
+
}),
|
|
186
|
+
], () => notFound());
|
|
187
|
+
/** PS3: page-numbered lists, 25 per page unless chaos `page_size` or `per_page` says otherwise. */
|
|
188
|
+
function listReply(core, items, url) {
|
|
189
|
+
const perPage = core.chaos.page_size > 0 ? core.chaos.page_size : Number(url.searchParams.get("per_page") ?? 25) || 25;
|
|
190
|
+
const pageNo = Math.max(1, Number(url.searchParams.get("page") ?? 1) || 1);
|
|
191
|
+
const data = items.slice((pageNo - 1) * perPage, pageNo * perPage);
|
|
192
|
+
const last = pageNo * perPage >= items.length;
|
|
193
|
+
return new Reply(200, { type: "list", current_page: pageNo, per_page: perPage, next_page: last ? null : pageNo + 1, prev_page: pageNo > 1 ? pageNo - 1 : null, data });
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Add a branch as PlanetScale would. Tests use it to seed "someone else's" branch. A seeded branch is ready at once.
|
|
197
|
+
*/
|
|
198
|
+
export function createPsBranch(core, branches, name, parent, createdBy, production) {
|
|
199
|
+
const branch = { id: core.nextId("psbr_"), name, parent_branch: parent, production, readyAt: 0, created_at: new Date().toISOString(), passwords: [], createdBy };
|
|
200
|
+
branches.push(branch);
|
|
201
|
+
return branch;
|
|
202
|
+
}
|
|
203
|
+
/** Add a password to a branch. Its plaintext starts `pscale_pw_`, like the real ones (PS8). */
|
|
204
|
+
export function createPsPassword(core, branch, name, role, createdBy) {
|
|
205
|
+
const id = core.nextId("pspw_");
|
|
206
|
+
const pw = { id, name, role, username: `user_${id}`, plainText: `pscale_pw_sim_${id}`, createdAt: Date.now(), createdBy };
|
|
207
|
+
branch.passwords.push(pw);
|
|
208
|
+
return pw;
|
|
209
|
+
}
|
|
210
|
+
function publicBranch(b) {
|
|
211
|
+
const ready = Date.now() >= b.readyAt;
|
|
212
|
+
return { id: b.id, name: b.name, kind: "mysql", parent_branch: b.parent_branch, production: b.production, ready, state: ready ? "ready" : "pending", created_at: b.created_at, updated_at: b.created_at };
|
|
213
|
+
}
|
|
214
|
+
function publicPassword(b, p) {
|
|
215
|
+
return {
|
|
216
|
+
id: p.id,
|
|
217
|
+
name: p.name,
|
|
218
|
+
role: p.role,
|
|
219
|
+
username: p.username,
|
|
220
|
+
access_host_url: PLANETSCALE_SIM_HOST,
|
|
221
|
+
plain_text: null,
|
|
222
|
+
expired: false,
|
|
223
|
+
database_branch: { id: b.id, name: b.name, production: b.production },
|
|
224
|
+
created_at: new Date(p.createdAt).toISOString(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** PS13: `{ code, message }`. */
|
|
228
|
+
function notFound() {
|
|
229
|
+
return new Reply(404, { code: "not_found", message: "Not Found" });
|
|
230
|
+
}
|
|
231
|
+
function unprocessable(message) {
|
|
232
|
+
return new Reply(422, { code: "unprocessable", message });
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=planetscale.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planetscale.js","sourceRoot":"","sources":["../../src/routes/planetscale.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ProviderSim } from "../provider.js";
|
|
2
|
+
/** One environment: its project and name (a PR environment is named after the pull request). */
|
|
3
|
+
export interface RailwayEnvironment {
|
|
4
|
+
projectId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
/** Simulated Railway state: environments, and variables by `<environmentId>/<serviceId or "">`. */
|
|
8
|
+
export interface RailwayState {
|
|
9
|
+
environments: Record<string, RailwayEnvironment>;
|
|
10
|
+
variables: Record<string, Record<string, string>>;
|
|
11
|
+
}
|
|
12
|
+
/** Initial Railway state. */
|
|
13
|
+
export interface RailwaySeed {
|
|
14
|
+
environments?: Record<string, RailwayEnvironment>;
|
|
15
|
+
variables?: Record<string, Record<string, string>>;
|
|
16
|
+
}
|
|
17
|
+
/** The project and PR environment of the default seed. */
|
|
18
|
+
export declare const RAILWAY_DEMO: {
|
|
19
|
+
readonly projectId: "prj-railway-demo";
|
|
20
|
+
readonly environmentId: "env-pr-42";
|
|
21
|
+
readonly serviceId: "svc-web";
|
|
22
|
+
};
|
|
23
|
+
/** Simulated Railway GraphQL API; see the assumptions at the top of this file. */
|
|
24
|
+
export declare const railwaySim: ProviderSim<RailwayState, RailwaySeed>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simulated Railway public GraphQL API (`POST https://backboard.railway.com/graphql/v2`), the subset that sets the
|
|
3
|
+
* variables of a service in an environment (a pull-request environment Railway created): enough to test the generic
|
|
4
|
+
* `http` adapter's GraphQL requests against Railway's real operation shapes.
|
|
5
|
+
*
|
|
6
|
+
* Assumptions (docs/api-verification.md, "Railway and Fly.io through the generic adapter"):
|
|
7
|
+
* RW1. One endpoint; `Authorization: Bearer <account or workspace token>` (a project token goes in
|
|
8
|
+
* `Project-Access-Token`). Verified: docs.railway.com/reference/public-api.
|
|
9
|
+
* RW2. Failures are HTTP 200 with `errors[]` (`message`, `extensions.code`); a refused token is
|
|
10
|
+
* `INTERNAL_SERVER_ERROR` "Not Authorized". Verified: same page ("Always inspect the errors array").
|
|
11
|
+
* RW3. `variables(projectId: String!, environmentId: String!, serviceId: String, unrendered: Boolean)` answers the
|
|
12
|
+
* `EnvironmentVariables` scalar, a flat map of name → string; without `serviceId`, the shared variables.
|
|
13
|
+
* Verified: railwayapp/cli src/gql/schema.json.
|
|
14
|
+
* RW4. `variableUpsert(input: VariableUpsertInput!): Boolean!` (projectId, environmentId, serviceId?, name, value,
|
|
15
|
+
* skipDeploys?) creates or replaces one variable; `variableDelete(input: VariableDeleteInput!): Boolean!`
|
|
16
|
+
* (projectId, environmentId, name, serviceId?) removes it. Verified: same schema. Deleting a variable that
|
|
17
|
+
* does not exist answers `true` here: unverified (the adapter treats "not found" as gone either way).
|
|
18
|
+
* RW5. An unknown project or environment is an error "Environment not found". Unverified wording.
|
|
19
|
+
*/
|
|
20
|
+
import { graphqlData, graphqlError, graphqlOperation, isMutation } from "../graphql.js";
|
|
21
|
+
import { Reply, route, router } from "../provider.js";
|
|
22
|
+
/** The project and PR environment of the default seed. */
|
|
23
|
+
export const RAILWAY_DEMO = { projectId: "prj-railway-demo", environmentId: "env-pr-42", serviceId: "svc-web" };
|
|
24
|
+
/** Simulated Railway GraphQL API; see the assumptions at the top of this file. */
|
|
25
|
+
export const railwaySim = {
|
|
26
|
+
env: { token: "RAILWAY_TOKEN", url: "RAILWAY_API_URL", testToken: "tok_railway" },
|
|
27
|
+
defaultSeed: { environments: { [RAILWAY_DEMO.environmentId]: { projectId: RAILWAY_DEMO.projectId, name: "pr-42" } } },
|
|
28
|
+
reset(_core, seed) {
|
|
29
|
+
return { environments: structuredClone(seed?.environments ?? {}), variables: structuredClone(seed?.variables ?? {}) };
|
|
30
|
+
},
|
|
31
|
+
/** `vars.<environmentId>.<serviceId or "shared">.<NAME>`: a new value, or "delete". */
|
|
32
|
+
drift(_core, state, { key, rest, value, only }) {
|
|
33
|
+
if (only !== undefined)
|
|
34
|
+
return false;
|
|
35
|
+
const [kind, env, service, name] = rest.split(".");
|
|
36
|
+
if (kind !== "vars" || !env || !service || !name)
|
|
37
|
+
return false;
|
|
38
|
+
const k = `${env}/${service === "shared" ? "" : service}`;
|
|
39
|
+
const vars = (state.variables[k] ??= {});
|
|
40
|
+
if (value === "delete") {
|
|
41
|
+
if (!(name in vars))
|
|
42
|
+
throw new Error(`drift ${key}: no variable ${name}`);
|
|
43
|
+
delete vars[name];
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
vars[name] = value;
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
isWrite(req) {
|
|
50
|
+
return isMutation(req.body);
|
|
51
|
+
},
|
|
52
|
+
routes(core, state, req) {
|
|
53
|
+
return routes(core, state, req);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
function str(v) {
|
|
57
|
+
return typeof v === "string" && v !== "" ? v : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The variable map a request addresses (RW3, RW5). */
|
|
60
|
+
function scopeOf(state, a) {
|
|
61
|
+
const projectId = str(a.projectId);
|
|
62
|
+
const environmentId = str(a.environmentId);
|
|
63
|
+
if (!projectId || !environmentId)
|
|
64
|
+
return graphqlError("projectId and environmentId are required", "BAD_USER_INPUT", 400);
|
|
65
|
+
const env = state.environments[environmentId];
|
|
66
|
+
if (!env || env.projectId !== projectId)
|
|
67
|
+
return graphqlError("Environment not found", "NOT_FOUND");
|
|
68
|
+
return { key: `${environmentId}/${str(a.serviceId) ?? ""}` };
|
|
69
|
+
}
|
|
70
|
+
function input(v) {
|
|
71
|
+
return typeof v.input === "object" && v.input !== null ? v.input : {};
|
|
72
|
+
}
|
|
73
|
+
const RESOLVERS = {
|
|
74
|
+
variables(state, v) {
|
|
75
|
+
const scope = scopeOf(state, v);
|
|
76
|
+
if (scope instanceof Reply)
|
|
77
|
+
return scope;
|
|
78
|
+
return graphqlData({ variables: { ...(state.variables[scope.key] ?? {}) } });
|
|
79
|
+
},
|
|
80
|
+
variableUpsert(state, v) {
|
|
81
|
+
const i = input(v);
|
|
82
|
+
const scope = scopeOf(state, i);
|
|
83
|
+
if (scope instanceof Reply)
|
|
84
|
+
return scope;
|
|
85
|
+
const name = str(i.name);
|
|
86
|
+
if (!name || typeof i.value !== "string")
|
|
87
|
+
return graphqlError("name and value are required", "BAD_USER_INPUT", 400);
|
|
88
|
+
(state.variables[scope.key] ??= {})[name] = i.value;
|
|
89
|
+
return graphqlData({ variableUpsert: true });
|
|
90
|
+
},
|
|
91
|
+
variableDelete(state, v) {
|
|
92
|
+
const i = input(v);
|
|
93
|
+
const scope = scopeOf(state, i);
|
|
94
|
+
if (scope instanceof Reply)
|
|
95
|
+
return scope;
|
|
96
|
+
const name = str(i.name);
|
|
97
|
+
if (!name)
|
|
98
|
+
return graphqlError("name is required", "BAD_USER_INPUT", 400);
|
|
99
|
+
delete state.variables[scope.key]?.[name];
|
|
100
|
+
return graphqlData({ variableDelete: true });
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const routes = router([
|
|
104
|
+
route("POST", "/", ({ state, body }) => {
|
|
105
|
+
const op = graphqlOperation(body);
|
|
106
|
+
if (op instanceof Reply)
|
|
107
|
+
return op;
|
|
108
|
+
const resolve = RESOLVERS[op.root];
|
|
109
|
+
return resolve ? resolve(state, op.variables) : graphqlError(`Cannot query field "${op.root}" (not simulated)`, "GRAPHQL_VALIDATION_FAILED", 400);
|
|
110
|
+
}),
|
|
111
|
+
], () => new Reply(404, { error: "not found" }));
|
|
112
|
+
//# sourceMappingURL=railway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"railway.js","sourceRoot":"","sources":["../../src/routes/railway.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { type DigestConfig, type IssuedToken, type SigV4Config } from "../auth.js";
|
|
2
|
+
import { type ProviderSim } from "../provider.js";
|
|
3
|
+
/** One object of a collection, as stored and answered: its id (under the style's id field) and every field a client wrote. */
|
|
4
|
+
export type RestItem = Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* How one collection answers (assumption R2). Every member is optional; the defaults are the conventional shape:
|
|
7
|
+
* `{ data: <object> }`, `{ data: [...], next_cursor }`, string ids under `id`.
|
|
8
|
+
*/
|
|
9
|
+
export interface RestStyle {
|
|
10
|
+
/** JSON pointer where a list answer holds the array. Default `/data`; `""` answers the bare array. */
|
|
11
|
+
list_path?: string;
|
|
12
|
+
/** JSON pointer where a single-object answer holds the object. Default `/data`; `""` answers it bare. */
|
|
13
|
+
item_path?: string;
|
|
14
|
+
/** JSON pointer of the next page's cursor in a list answer. Default `/next_cursor`; null: no cursor and no paging. */
|
|
15
|
+
next_path?: string | null;
|
|
16
|
+
/** The field holding the id. Default `id`. */
|
|
17
|
+
id_field?: string;
|
|
18
|
+
/** The body field a create may choose the id with. Default: the id field. */
|
|
19
|
+
id_from?: string;
|
|
20
|
+
/** Assign integer ids instead of `it_<n>`. */
|
|
21
|
+
numeric_ids?: boolean;
|
|
22
|
+
/** Read form-encoded `true`/`false` and numbers as booleans and numbers (assumption R4). */
|
|
23
|
+
form_types?: boolean;
|
|
24
|
+
/** Fields generated on create and shown only in its answer (assumption R6). */
|
|
25
|
+
once_fields?: string[];
|
|
26
|
+
/** A query parameter that names an object of the collection by id (`url`), assumption R3. */
|
|
27
|
+
item_query?: string;
|
|
28
|
+
/** For an object: top-level fields a write names otherwise than a read, written name → stored name (R2). */
|
|
29
|
+
write_fields?: Record<string, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Request headers (lower-case names) whose values a create or update stores as fields, for an API that takes an
|
|
32
|
+
* object's settings in headers (QStash: `upstash-cron` → `cron`, `upstash-schedule-id` → `scheduleId`).
|
|
33
|
+
*/
|
|
34
|
+
header_fields?: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* A POST to `<collection>/<suffix>` creates the object with the suffix (a URL, kept as written) in this field, or
|
|
37
|
+
* updates the one whose id the request chooses (an upsert): QStash's `POST /v2/schedules/<destination>`.
|
|
38
|
+
*/
|
|
39
|
+
suffix_field?: string;
|
|
40
|
+
/** Fields (JSON pointers, `/ApiTokens/0`) generated on create and stored (assumption R10). */
|
|
41
|
+
generated?: string[];
|
|
42
|
+
}
|
|
43
|
+
/** A path whose POST deletes objects its body names (assumption R11): the collection, and where the ids are. */
|
|
44
|
+
export interface RestBulkDelete {
|
|
45
|
+
collection: string;
|
|
46
|
+
/** JSON pointer to the id, or to a list of ids, in the body. */
|
|
47
|
+
ids: string;
|
|
48
|
+
}
|
|
49
|
+
/** A one-off action's endpoint (assumption R9): a fixed answer to every method, and whether it needs a credential. */
|
|
50
|
+
export interface RestEndpoint {
|
|
51
|
+
/** Default 200. */
|
|
52
|
+
status?: number;
|
|
53
|
+
/** Default `{}`. */
|
|
54
|
+
reply?: unknown;
|
|
55
|
+
/** Answers without a credential (an app's own endpoint). */
|
|
56
|
+
public?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** One call to a seeded endpoint (R9), as received. */
|
|
59
|
+
export interface RestCall {
|
|
60
|
+
method: string;
|
|
61
|
+
path: string;
|
|
62
|
+
/** Lower-case names. */
|
|
63
|
+
headers: Record<string, string>;
|
|
64
|
+
body: unknown;
|
|
65
|
+
}
|
|
66
|
+
/** Simulated REST API state, by path: collections of objects, objects that are not in a collection, and styles. */
|
|
67
|
+
export interface RestState {
|
|
68
|
+
collections: Record<string, RestItem[]>;
|
|
69
|
+
objects: Record<string, Record<string, unknown>>;
|
|
70
|
+
styles: Record<string, RestStyle>;
|
|
71
|
+
/** Paths whose POST deletes the objects its body names (R11). */
|
|
72
|
+
deletes: Record<string, RestBulkDelete>;
|
|
73
|
+
/** Every value generated for a style's `generated` fields (R10). */
|
|
74
|
+
generated: string[];
|
|
75
|
+
/** Paths that name a collection kept under another path, by path: a create posted elsewhere
|
|
76
|
+
* (`/gates/g/rule` adds to `/gates/g/rules`), or a list at another URL (`/settings/all` lists `/settings`). */
|
|
77
|
+
aliases: Record<string, string>;
|
|
78
|
+
/** Further header names (lower-case) that carry a credential (assumption R1). */
|
|
79
|
+
auth_headers: string[];
|
|
80
|
+
/** Paths read with a POST, by path: the object each answers (assumption R8). */
|
|
81
|
+
reads: Record<string, string>;
|
|
82
|
+
/** Present when the seed configures real credentials (R7). */
|
|
83
|
+
auth?: RestAuthState;
|
|
84
|
+
/** One-off action endpoints by path (R9), and every call they received. */
|
|
85
|
+
endpoints: Record<string, RestEndpoint>;
|
|
86
|
+
calls: RestCall[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Real credentials for the REST sim (R7). Each key enables one scheme; with none of them, R1 applies (any
|
|
90
|
+
* non-empty credential).
|
|
91
|
+
*/
|
|
92
|
+
export interface RestAuthSeed {
|
|
93
|
+
/** OAuth2 clients the token endpoint accepts: client id → client secret. */
|
|
94
|
+
clients?: Record<string, string>;
|
|
95
|
+
/** Lifetime of the tokens it issues, in seconds (default 3600). */
|
|
96
|
+
token_ttl?: number;
|
|
97
|
+
/** Bearer tokens accepted as they are (the token a CLI such as `gcloud auth print-access-token` prints). */
|
|
98
|
+
tokens?: string[];
|
|
99
|
+
/**
|
|
100
|
+
* Accepted sets of header values (lower-case name → exact value), each complete on its own: a key behind a scheme
|
|
101
|
+
* prefix (`Api-Key <key>`), or an API key together with an application key.
|
|
102
|
+
*/
|
|
103
|
+
headers?: Array<Record<string, string>>;
|
|
104
|
+
/** Digest users (name → password), the algorithm (`MD5` by default) and the realm. */
|
|
105
|
+
digest?: DigestConfig;
|
|
106
|
+
/** SigV4 access keys (id → secret) and the region and service requests must be signed for. */
|
|
107
|
+
sigv4?: Omit<SigV4Config, "now">;
|
|
108
|
+
/** A header (lower-case) accepted when it carries a value the sim generated (R10): another object's token. */
|
|
109
|
+
generated_header?: string;
|
|
110
|
+
}
|
|
111
|
+
/** The REST sim's credential state: what was seeded, every token issued and every Digest nonce handed out. */
|
|
112
|
+
export interface RestAuthState extends RestAuthSeed {
|
|
113
|
+
issued: IssuedToken[];
|
|
114
|
+
nonces: string[];
|
|
115
|
+
/** Requests answered 401, for assertions ("the expired token was refused once"). */
|
|
116
|
+
refused: number;
|
|
117
|
+
}
|
|
118
|
+
/** Initial REST state, by path: objects per collection (ids assigned when absent), stand-alone objects, styles. */
|
|
119
|
+
export interface RestSeed {
|
|
120
|
+
collections?: Record<string, Array<Record<string, unknown>>>;
|
|
121
|
+
objects?: Record<string, Record<string, unknown>>;
|
|
122
|
+
/** How each collection answers, by collection path (assumption R2). */
|
|
123
|
+
styles?: Record<string, RestStyle>;
|
|
124
|
+
/** Paths that name a collection kept under another path, by path: `{ "/gates/g/rule": "/gates/g/rules" }`. */
|
|
125
|
+
aliases?: Record<string, string>;
|
|
126
|
+
/** Further header names that carry a credential, for an API whose header is not named `*-Key` or `*-Token`. */
|
|
127
|
+
auth_headers?: string[];
|
|
128
|
+
/** Paths read with a POST (an ARM list action), by path: the object each answers (assumption R8). */
|
|
129
|
+
reads?: Record<string, string>;
|
|
130
|
+
/** Real credentials (R7). */
|
|
131
|
+
auth?: RestAuthSeed;
|
|
132
|
+
/** One-off action endpoints by path (R9). */
|
|
133
|
+
endpoints?: Record<string, RestEndpoint>;
|
|
134
|
+
/** Paths whose POST deletes the objects its body names (R11), by path. */
|
|
135
|
+
deletes?: Record<string, RestBulkDelete>;
|
|
136
|
+
}
|
|
137
|
+
/** Simulated generic REST API; see the assumptions at the top of this file. */
|
|
138
|
+
export declare const restSim: ProviderSim<RestState, RestSeed>;
|