@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,53 @@
|
|
|
1
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
2
|
+
import { type ScopeImage } from '../../../src/planetscale-store.js';
|
|
3
|
+
import { type AuditActor } from './audit.js';
|
|
4
|
+
type Row = Record<string, unknown>;
|
|
5
|
+
/** The live branches of a database (by name). */
|
|
6
|
+
export declare const branchesOf: (ctx: SemanticsContext, database: string) => Row[];
|
|
7
|
+
/** The store scope a branch's statements act on (`main` is the World's image: undefined). */
|
|
8
|
+
export declare const branchScope: (database: string, branch: string) => string | undefined;
|
|
9
|
+
/** The context the root store is written with. */
|
|
10
|
+
export declare const storeCtx: (ctx: SemanticsContext, at?: string) => {
|
|
11
|
+
root?: string;
|
|
12
|
+
occurredAt: string;
|
|
13
|
+
};
|
|
14
|
+
/** A branch as the spec's DatabaseBranch answers it, the bookkeeping kept beside it. */
|
|
15
|
+
export declare function makeBranch(ctx: SemanticsContext, db: Row, o: {
|
|
16
|
+
name: string;
|
|
17
|
+
parent: string | null;
|
|
18
|
+
production: boolean;
|
|
19
|
+
parentSchema: ScopeImage['tables'];
|
|
20
|
+
restoredFrom?: Row;
|
|
21
|
+
}): Promise<Row>;
|
|
22
|
+
/**
|
|
23
|
+
* create_branch: from `parent_branch` (default: the database's default branch) with its schema and no rows, or from
|
|
24
|
+
* `backup_id` with the backup's schema and rows ("If provided, restores the backup's schema and data to the new branch.
|
|
25
|
+
* Must have `restore_production_branch_backup(s)` or `restore_backup(s)` access to do this", the spec). A name the
|
|
26
|
+
* database already has, or one outside PlanetScale's branch-name rule, answers 422 (the twin's wording). The branch is
|
|
27
|
+
* `pending` and not ready until the World clock makes it ready (semantics/time.ts).
|
|
28
|
+
*/
|
|
29
|
+
declare function createBranch(ctx: SemanticsContext): Promise<Response>;
|
|
30
|
+
/** The image a backup holds (src/planetscale-store.ts, `_snapshot`), or an empty one for a backup that holds none. */
|
|
31
|
+
export declare function backupImage(ctx: SemanticsContext, backup: Row): ScopeImage;
|
|
32
|
+
declare function getBranch(ctx: SemanticsContext): Promise<Response>;
|
|
33
|
+
/** list_branches, oldest first (the twin's order), filtered by `q` (a name contains it), `production` and
|
|
34
|
+
* `safe_migrations`. */
|
|
35
|
+
declare function listBranches(ctx: SemanticsContext): Promise<Response>;
|
|
36
|
+
/**
|
|
37
|
+
* delete_branch. "You cannot delete a branch that's set as default" and "Only Organization Administrators have
|
|
38
|
+
* permission to delete production branches" (https://planetscale.com/docs/vitess/schema-changes/branching); a token needs
|
|
39
|
+
* `delete_production_branch` for a production branch ("Delete a production database branch",
|
|
40
|
+
* https://planetscale.com/docs/api/reference/service-tokens). The branch's tables, rows and passwords go with it.
|
|
41
|
+
*/
|
|
42
|
+
declare function deleteBranch(ctx: SemanticsContext): Promise<Response>;
|
|
43
|
+
/** A branch gone: its passwords stop authenticating and its tables and rows are dropped. */
|
|
44
|
+
export declare function removeBranch(ctx: SemanticsContext, database: string, b: Row, actor?: AuditActor): Promise<void>;
|
|
45
|
+
export declare const branchSemantics: {
|
|
46
|
+
enable_safe_migrations: (ctx: SemanticsContext) => Promise<Response>;
|
|
47
|
+
disable_safe_migrations: (ctx: SemanticsContext) => Promise<Response>;
|
|
48
|
+
create_branch: typeof createBranch;
|
|
49
|
+
get_branch: typeof getBranch;
|
|
50
|
+
list_branches: typeof listBranches;
|
|
51
|
+
delete_branch: typeof deleteBranch;
|
|
52
|
+
};
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { dropScope, scopeImage, scopeOf, seedScope } from "../../../src/planetscale-store.js";
|
|
2
|
+
import { lacks } from "../token-gate.js";
|
|
3
|
+
import { actorOf, AVATAR, mintId, page, REGION } from "./shared.js";
|
|
4
|
+
import { audit, orgOf } from "./audit.js";
|
|
5
|
+
/** The live branches of a database (by name). */
|
|
6
|
+
export const branchesOf = (ctx, database) => ctx.rowsRaw('DatabaseBranch').filter((b) => b._database === database && b.deleted_at === null);
|
|
7
|
+
/** The store scope a branch's statements act on (`main` is the World's image: undefined). */
|
|
8
|
+
export const branchScope = (database, branch) => scopeOf(database, branch);
|
|
9
|
+
/** The context the root store is written with. */
|
|
10
|
+
export const storeCtx = (ctx, at = ctx.occurredAt) => ({ ...(ctx.root !== undefined ? { root: ctx.root } : {}), occurredAt: at });
|
|
11
|
+
/** A branch as the spec's DatabaseBranch answers it, the bookkeeping kept beside it. */
|
|
12
|
+
export async function makeBranch(ctx, db, o) {
|
|
13
|
+
const database = String(db.name);
|
|
14
|
+
const org = String(ctx.call.params.organization);
|
|
15
|
+
const id = mintId(ctx, 'DatabaseBranch', 'br', `${database}/${o.name}`);
|
|
16
|
+
const at = ctx.occurredAt;
|
|
17
|
+
return ctx.write('DatabaseBranch', id, {
|
|
18
|
+
id, name: o.name, created_at: at, updated_at: at, deleted_at: null, restore_checklist_completed_at: null,
|
|
19
|
+
schema_last_updated_at: at, kind: 'mysql', mysql_address: 'aws.connect.psdb.cloud', mysql_edge_address: 'aws.connect.psdb.cloud',
|
|
20
|
+
state: 'pending', ready: false, schema_ready: false, metal: false, production: o.production, safe_migrations: false,
|
|
21
|
+
deletion_protected: false, sharded: false, shard_count: 1, keyspace_count: 1, stale_schema: false,
|
|
22
|
+
cluster_name: String(db._cluster_size ?? 'PS_10'), cluster_iops: 0, direct_vtgate: false,
|
|
23
|
+
actor: actorOf(ctx),
|
|
24
|
+
restored_from_branch: o.restoredFrom ? { id: String(o.restoredFrom.id), name: String(o.restoredFrom.name), created_at: String(o.restoredFrom.created_at), updated_at: String(o.restoredFrom.updated_at), deleted_at: null } : null,
|
|
25
|
+
private_edge_connectivity: false, has_replicas: o.production, has_read_only_replicas: false,
|
|
26
|
+
html_url: `https://app.planetscale.com/${org}/${database}/${o.name}`,
|
|
27
|
+
url: `https://api.planetscale.com/v1/organizations/${org}/databases/${database}/branches/${o.name}`,
|
|
28
|
+
region: REGION(),
|
|
29
|
+
parent_branch: o.parent,
|
|
30
|
+
_database: database, _parent_schema: o.parentSchema,
|
|
31
|
+
}, 'branch.create');
|
|
32
|
+
}
|
|
33
|
+
/** "PlanetScale branch names must be lowercase, alphanumeric characters and hyphens are allowed"
|
|
34
|
+
* (https://planetscale.com/docs/vitess/integrations/github-actions). */
|
|
35
|
+
const BRANCH_NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
36
|
+
const unprocessable = (ctx, message) => ctx.refuse({ status: 422, code: 'unprocessable_entity', message });
|
|
37
|
+
const body = (ctx) => (ctx.body && typeof ctx.body === 'object' && !Array.isArray(ctx.body) ? ctx.body : {});
|
|
38
|
+
const view = (ctx, b) => ctx.get('DatabaseBranch', String(b.id));
|
|
39
|
+
/** The created database a path names, live, or its 404. Branches exist only on a database this lane created. */
|
|
40
|
+
function createdDatabase(ctx) {
|
|
41
|
+
const name = String(ctx.call.params.database ?? '');
|
|
42
|
+
const db = ctx.rowsRaw('Database').find((d) => d.name === name && !d._deleted_at);
|
|
43
|
+
return db ? { db } : { refused: ctx.notFound('Database', name, 'database') };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* create_branch: from `parent_branch` (default: the database's default branch) with its schema and no rows, or from
|
|
47
|
+
* `backup_id` with the backup's schema and rows ("If provided, restores the backup's schema and data to the new branch.
|
|
48
|
+
* Must have `restore_production_branch_backup(s)` or `restore_backup(s)` access to do this", the spec). A name the
|
|
49
|
+
* database already has, or one outside PlanetScale's branch-name rule, answers 422 (the twin's wording). The branch is
|
|
50
|
+
* `pending` and not ready until the World clock makes it ready (semantics/time.ts).
|
|
51
|
+
*/
|
|
52
|
+
async function createBranch(ctx) {
|
|
53
|
+
const { refused, db } = createdDatabase(ctx);
|
|
54
|
+
if (refused)
|
|
55
|
+
return refused;
|
|
56
|
+
const database = String(db.name);
|
|
57
|
+
const b = body(ctx);
|
|
58
|
+
const name = typeof b.name === 'string' ? b.name : '';
|
|
59
|
+
if (!name)
|
|
60
|
+
return unprocessable(ctx, 'name is required');
|
|
61
|
+
if (!BRANCH_NAME.test(name))
|
|
62
|
+
return unprocessable(ctx, 'Name must be lowercase, alphanumeric characters and hyphens');
|
|
63
|
+
if (branchesOf(ctx, database).some((x) => x.name === name))
|
|
64
|
+
return unprocessable(ctx, 'Name has already been taken');
|
|
65
|
+
if (typeof b.backup_id === 'string' && b.backup_id) {
|
|
66
|
+
const backup = ctx.rowsRaw('Backup').find((k) => k.id === b.backup_id && k._database === database && k.deleted_at === null);
|
|
67
|
+
if (!backup)
|
|
68
|
+
return ctx.notFound('Backup', b.backup_id, 'backup_id');
|
|
69
|
+
if (backup.state !== 'success')
|
|
70
|
+
return unprocessable(ctx, 'The backup has not completed');
|
|
71
|
+
const source = branchesOf(ctx, database).find((x) => x.name === backup.database_branch?.name);
|
|
72
|
+
const denied = lacks(ctx, database, source?.production === true ? ['restore_production_branch_backup'] : ['restore_backup', 'restore_production_branch_backup']);
|
|
73
|
+
if (denied)
|
|
74
|
+
return denied;
|
|
75
|
+
const snapshot = backupImage(ctx, backup);
|
|
76
|
+
const record = await makeBranch(ctx, db, { name, parent: String(backup.database_branch.name), production: false, parentSchema: snapshot.tables, restoredFrom: source });
|
|
77
|
+
const scope = branchScope(database, name);
|
|
78
|
+
await seedScope(storeCtx(ctx), scope, snapshot);
|
|
79
|
+
await ctx.write('Backup', String(backup.id), { ...ctx.own(backup), restored_branches: [...(backup.restored_branches ?? []), String(record.id)] }, 'backup.restore');
|
|
80
|
+
await audit(ctx, orgOf(ctx), 'database_branch', 'created', { type: 'DatabaseBranch', id: String(record.id), name }, { auditable: { type: 'Database', id: String(db.id), name: database } });
|
|
81
|
+
return ctx.reply(view(ctx, record), 201);
|
|
82
|
+
}
|
|
83
|
+
const denied = lacks(ctx, database, ['create_branch']);
|
|
84
|
+
if (denied)
|
|
85
|
+
return denied;
|
|
86
|
+
const parentName = typeof b.parent_branch === 'string' && b.parent_branch ? b.parent_branch : String(db.default_branch);
|
|
87
|
+
const parent = branchesOf(ctx, database).find((x) => x.name === parentName);
|
|
88
|
+
if (!parent)
|
|
89
|
+
return ctx.notFound('DatabaseBranch', parentName, 'parent_branch');
|
|
90
|
+
const schema = scopeImage(ctx.root, branchScope(database, parentName)).tables;
|
|
91
|
+
const record = await makeBranch(ctx, db, { name, parent: parentName, production: false, parentSchema: schema });
|
|
92
|
+
await seedScope(storeCtx(ctx), branchScope(database, name), { tables: schema, rows: [] });
|
|
93
|
+
await audit(ctx, orgOf(ctx), 'database_branch', 'created', { type: 'DatabaseBranch', id: String(record.id), name }, { auditable: { type: 'Database', id: String(db.id), name: database } });
|
|
94
|
+
return ctx.reply(view(ctx, record), 201);
|
|
95
|
+
}
|
|
96
|
+
/** The image a backup holds (src/planetscale-store.ts, `_snapshot`), or an empty one for a backup that holds none. */
|
|
97
|
+
export function backupImage(ctx, backup) {
|
|
98
|
+
const snap = backup._snapshot === undefined ? undefined : ctx.tree().find((r) => r.type === '_snapshot' && r.id === backup._snapshot);
|
|
99
|
+
const image = snap ? ctx.own(snap).image : undefined;
|
|
100
|
+
return image ?? { tables: [], rows: [] };
|
|
101
|
+
}
|
|
102
|
+
async function getBranch(ctx) {
|
|
103
|
+
const { refused, db } = createdDatabase(ctx);
|
|
104
|
+
if (refused)
|
|
105
|
+
return refused;
|
|
106
|
+
const b = branchesOf(ctx, String(db.name)).find((x) => x.name === ctx.call.params.branch);
|
|
107
|
+
return b ? ctx.reply(view(ctx, b)) : ctx.notFound('DatabaseBranch', String(ctx.call.params.branch), 'branch');
|
|
108
|
+
}
|
|
109
|
+
/** list_branches, oldest first (the twin's order), filtered by `q` (a name contains it), `production` and
|
|
110
|
+
* `safe_migrations`. */
|
|
111
|
+
async function listBranches(ctx) {
|
|
112
|
+
const { refused, db } = createdDatabase(ctx);
|
|
113
|
+
if (refused)
|
|
114
|
+
return refused;
|
|
115
|
+
const q = new URL(ctx.call.request.url).searchParams;
|
|
116
|
+
const flag = (k) => (q.has(k) ? q.get(k) === 'true' : undefined);
|
|
117
|
+
const production = flag('production');
|
|
118
|
+
const safe = flag('safe_migrations');
|
|
119
|
+
const rows = branchesOf(ctx, String(db.name)).filter((b) => (!q.get('q') || String(b.name).includes(q.get('q')))
|
|
120
|
+
&& (production === undefined || b.production === production) && (safe === undefined || b.safe_migrations === safe));
|
|
121
|
+
return page(ctx, rows.map((b) => view(ctx, b)));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* delete_branch. "You cannot delete a branch that's set as default" and "Only Organization Administrators have
|
|
125
|
+
* permission to delete production branches" (https://planetscale.com/docs/vitess/schema-changes/branching); a token needs
|
|
126
|
+
* `delete_production_branch` for a production branch ("Delete a production database branch",
|
|
127
|
+
* https://planetscale.com/docs/api/reference/service-tokens). The branch's tables, rows and passwords go with it.
|
|
128
|
+
*/
|
|
129
|
+
async function deleteBranch(ctx) {
|
|
130
|
+
const { refused, db } = createdDatabase(ctx);
|
|
131
|
+
if (refused)
|
|
132
|
+
return refused;
|
|
133
|
+
const database = String(db.name);
|
|
134
|
+
const b = branchesOf(ctx, database).find((x) => x.name === ctx.call.params.branch);
|
|
135
|
+
if (!b)
|
|
136
|
+
return ctx.notFound('DatabaseBranch', String(ctx.call.params.branch), 'branch');
|
|
137
|
+
if (b.name === db.default_branch)
|
|
138
|
+
return unprocessable(ctx, 'You cannot delete the default branch');
|
|
139
|
+
if (b.production === true) {
|
|
140
|
+
const denied = lacks(ctx, database, ['delete_production_branch']);
|
|
141
|
+
if (denied)
|
|
142
|
+
return denied;
|
|
143
|
+
}
|
|
144
|
+
await removeBranch(ctx, database, b);
|
|
145
|
+
return new Response(null, { status: 204 });
|
|
146
|
+
}
|
|
147
|
+
/** A branch gone: its passwords stop authenticating and its tables and rows are dropped. */
|
|
148
|
+
export async function removeBranch(ctx, database, b, actor) {
|
|
149
|
+
const at = ctx.occurredAt;
|
|
150
|
+
for (const p of ctx.rowsRaw('DatabaseBranchPassword')) {
|
|
151
|
+
if (p._database === database && p.database_branch?.name === b.name && p.deleted_at === null) {
|
|
152
|
+
await ctx.write('DatabaseBranchPassword', String(p.id), { ...ctx.own(p), deleted_at: at }, 'password.delete');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// a deploy request outlives its branch, marked `branch_deleted` (the spec's DatabaseDeployRequest)
|
|
156
|
+
for (const dr of ctx.rowsRaw('DatabaseDeployRequest')) {
|
|
157
|
+
if (dr._database === database && dr.branch === b.name && dr.branch_deleted !== true) {
|
|
158
|
+
await ctx.write('DatabaseDeployRequest', String(dr.id), { ...ctx.own(dr), branch_deleted: true, branch_deleted_at: at, branch_deleted_by: actor ? { id: actor.id, display_name: actor.name, avatar_url: AVATAR } : actorOf(ctx), updated_at: at }, 'deploy_request.branch_deleted');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const scope = branchScope(database, String(b.name));
|
|
162
|
+
if (scope)
|
|
163
|
+
await dropScope(storeCtx(ctx), scope);
|
|
164
|
+
await ctx.write('DatabaseBranch', String(b.id), { ...ctx.own(b), deleted_at: at, updated_at: at }, 'branch.delete');
|
|
165
|
+
const db = ctx.rowsRaw('Database').find((d) => d.name === database);
|
|
166
|
+
await audit(ctx, String(db?._organization ?? orgOf(ctx)), 'database_branch', 'deleted', { type: 'DatabaseBranch', id: String(b.id), name: String(b.name) }, { auditable: { type: 'Database', id: String(db?.id ?? database), name: database }, ...(actor ? { actor } : {}) });
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* enable_safe_migrations / disable_safe_migrations. "With safe migrations enabled, Data Definition Language (DDL)
|
|
170
|
+
* statements issued to branches with safe migrations enabled will automatically be rejected by PlanetScale"
|
|
171
|
+
* (https://planetscale.com/docs/vitess/schema-changes/safe-migrations): psdb refuses DDL on the branch while the flag is
|
|
172
|
+
* set (the pack root's planetscale-twin.ts). The answer is the branch. The spec names no access for either operation;
|
|
173
|
+
* the twin's gate asks `write_database` (token-gate.ts).
|
|
174
|
+
*/
|
|
175
|
+
function safeMigrations(on) {
|
|
176
|
+
return async (ctx) => {
|
|
177
|
+
const { refused, db } = createdDatabase(ctx);
|
|
178
|
+
if (refused)
|
|
179
|
+
return refused;
|
|
180
|
+
const b = branchesOf(ctx, String(db.name)).find((x) => x.name === ctx.call.params.branch);
|
|
181
|
+
if (!b)
|
|
182
|
+
return ctx.notFound('DatabaseBranch', String(ctx.call.params.branch), 'branch');
|
|
183
|
+
if (b.safe_migrations !== on) {
|
|
184
|
+
await ctx.write('DatabaseBranch', String(b.id), { ...ctx.own(b), safe_migrations: on, updated_at: ctx.occurredAt }, on ? 'branch.enable_safe_migrations' : 'branch.disable_safe_migrations');
|
|
185
|
+
await audit(ctx, orgOf(ctx), 'database_branch', on ? 'enabled_safe_migrations' : 'disabled_safe_migrations', { type: 'DatabaseBranch', id: String(b.id), name: String(b.name) }, { auditable: { type: 'Database', id: String(db.id), name: String(db.name) } });
|
|
186
|
+
}
|
|
187
|
+
return ctx.reply(view(ctx, branchesOf(ctx, String(db.name)).find((x) => x.id === b.id)));
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export const branchSemantics = {
|
|
191
|
+
enable_safe_migrations: safeMigrations(true),
|
|
192
|
+
disable_safe_migrations: safeMigrations(false),
|
|
193
|
+
create_branch: createBranch,
|
|
194
|
+
get_branch: getBranch,
|
|
195
|
+
list_branches: listBranches,
|
|
196
|
+
delete_branch: deleteBranch,
|
|
197
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// A database's branches. "When your database is first initialized, a single production branch is created called `main`
|
|
2
|
+
// and acts as the default branch. When you create additional branches, the schema of the source branch is copied to the
|
|
3
|
+
// new branch"; "Development branches **will not** copy over data, just the schema"
|
|
4
|
+
// (https://planetscale.com/docs/vitess/schema-changes/branching). A branch's tables and rows live in the pack root's
|
|
5
|
+
// store: `main` is the World's one image, any other branch a scope of its own (src/planetscale-store.ts, "BRANCH
|
|
6
|
+
// SCOPES"). Where the reference stops and the twin decides, the comment beside the rule says so.
|
|
7
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
8
|
+
import { dropScope, scopeImage, scopeOf, seedScope, type ScopeImage } from '../../../src/planetscale-store.ts';
|
|
9
|
+
import { lacks } from '../token-gate.ts';
|
|
10
|
+
import { actorOf, AVATAR, mintId, page, REGION } from './shared.ts';
|
|
11
|
+
import { audit, orgOf, type AuditActor } from './audit.ts';
|
|
12
|
+
|
|
13
|
+
type Row = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
/** The live branches of a database (by name). */
|
|
16
|
+
export const branchesOf = (ctx: SemanticsContext, database: string): Row[] =>
|
|
17
|
+
ctx.rowsRaw('DatabaseBranch').filter((b) => b._database === database && b.deleted_at === null);
|
|
18
|
+
|
|
19
|
+
/** The store scope a branch's statements act on (`main` is the World's image: undefined). */
|
|
20
|
+
export const branchScope = (database: string, branch: string): string | undefined => scopeOf(database, branch);
|
|
21
|
+
|
|
22
|
+
/** The context the root store is written with. */
|
|
23
|
+
export const storeCtx = (ctx: SemanticsContext, at = ctx.occurredAt): { root?: string; occurredAt: string } => ({ ...(ctx.root !== undefined ? { root: ctx.root } : {}), occurredAt: at });
|
|
24
|
+
|
|
25
|
+
/** A branch as the spec's DatabaseBranch answers it, the bookkeeping kept beside it. */
|
|
26
|
+
export async function makeBranch(ctx: SemanticsContext, db: Row, o: { name: string; parent: string | null; production: boolean; parentSchema: ScopeImage['tables']; restoredFrom?: Row }): Promise<Row> {
|
|
27
|
+
const database = String(db.name);
|
|
28
|
+
const org = String(ctx.call.params.organization);
|
|
29
|
+
const id = mintId(ctx, 'DatabaseBranch', 'br', `${database}/${o.name}`);
|
|
30
|
+
const at = ctx.occurredAt;
|
|
31
|
+
return ctx.write('DatabaseBranch', id, {
|
|
32
|
+
id, name: o.name, created_at: at, updated_at: at, deleted_at: null, restore_checklist_completed_at: null,
|
|
33
|
+
schema_last_updated_at: at, kind: 'mysql', mysql_address: 'aws.connect.psdb.cloud', mysql_edge_address: 'aws.connect.psdb.cloud',
|
|
34
|
+
state: 'pending', ready: false, schema_ready: false, metal: false, production: o.production, safe_migrations: false,
|
|
35
|
+
deletion_protected: false, sharded: false, shard_count: 1, keyspace_count: 1, stale_schema: false,
|
|
36
|
+
cluster_name: String(db._cluster_size ?? 'PS_10'), cluster_iops: 0, direct_vtgate: false,
|
|
37
|
+
actor: actorOf(ctx),
|
|
38
|
+
restored_from_branch: o.restoredFrom ? { id: String(o.restoredFrom.id), name: String(o.restoredFrom.name), created_at: String(o.restoredFrom.created_at), updated_at: String(o.restoredFrom.updated_at), deleted_at: null } : null,
|
|
39
|
+
private_edge_connectivity: false, has_replicas: o.production, has_read_only_replicas: false,
|
|
40
|
+
html_url: `https://app.planetscale.com/${org}/${database}/${o.name}`,
|
|
41
|
+
url: `https://api.planetscale.com/v1/organizations/${org}/databases/${database}/branches/${o.name}`,
|
|
42
|
+
region: REGION(),
|
|
43
|
+
parent_branch: o.parent,
|
|
44
|
+
_database: database, _parent_schema: o.parentSchema,
|
|
45
|
+
}, 'branch.create');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** "PlanetScale branch names must be lowercase, alphanumeric characters and hyphens are allowed"
|
|
49
|
+
* (https://planetscale.com/docs/vitess/integrations/github-actions). */
|
|
50
|
+
const BRANCH_NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
51
|
+
|
|
52
|
+
const unprocessable = (ctx: SemanticsContext, message: string): Response => ctx.refuse({ status: 422, code: 'unprocessable_entity', message });
|
|
53
|
+
const body = (ctx: SemanticsContext): Row => (ctx.body && typeof ctx.body === 'object' && !Array.isArray(ctx.body) ? (ctx.body as Row) : {});
|
|
54
|
+
const view = (ctx: SemanticsContext, b: Row): Row => ctx.get('DatabaseBranch', String(b.id))!;
|
|
55
|
+
|
|
56
|
+
/** The created database a path names, live, or its 404. Branches exist only on a database this lane created. */
|
|
57
|
+
function createdDatabase(ctx: SemanticsContext): { refused?: Response; db?: Row } {
|
|
58
|
+
const name = String(ctx.call.params.database ?? '');
|
|
59
|
+
const db = ctx.rowsRaw('Database').find((d) => d.name === name && !d._deleted_at);
|
|
60
|
+
return db ? { db } : { refused: ctx.notFound('Database', name, 'database') };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* create_branch: from `parent_branch` (default: the database's default branch) with its schema and no rows, or from
|
|
65
|
+
* `backup_id` with the backup's schema and rows ("If provided, restores the backup's schema and data to the new branch.
|
|
66
|
+
* Must have `restore_production_branch_backup(s)` or `restore_backup(s)` access to do this", the spec). A name the
|
|
67
|
+
* database already has, or one outside PlanetScale's branch-name rule, answers 422 (the twin's wording). The branch is
|
|
68
|
+
* `pending` and not ready until the World clock makes it ready (semantics/time.ts).
|
|
69
|
+
*/
|
|
70
|
+
async function createBranch(ctx: SemanticsContext): Promise<Response> {
|
|
71
|
+
const { refused, db } = createdDatabase(ctx);
|
|
72
|
+
if (refused) return refused;
|
|
73
|
+
const database = String(db!.name);
|
|
74
|
+
const b = body(ctx);
|
|
75
|
+
const name = typeof b.name === 'string' ? b.name : '';
|
|
76
|
+
if (!name) return unprocessable(ctx, 'name is required');
|
|
77
|
+
if (!BRANCH_NAME.test(name)) return unprocessable(ctx, 'Name must be lowercase, alphanumeric characters and hyphens');
|
|
78
|
+
if (branchesOf(ctx, database).some((x) => x.name === name)) return unprocessable(ctx, 'Name has already been taken');
|
|
79
|
+
if (typeof b.backup_id === 'string' && b.backup_id) {
|
|
80
|
+
const backup = ctx.rowsRaw('Backup').find((k) => k.id === b.backup_id && k._database === database && k.deleted_at === null);
|
|
81
|
+
if (!backup) return ctx.notFound('Backup', b.backup_id, 'backup_id');
|
|
82
|
+
if (backup.state !== 'success') return unprocessable(ctx, 'The backup has not completed');
|
|
83
|
+
const source = branchesOf(ctx, database).find((x) => x.name === (backup.database_branch as Row | undefined)?.name);
|
|
84
|
+
const denied = lacks(ctx, database, source?.production === true ? ['restore_production_branch_backup'] : ['restore_backup', 'restore_production_branch_backup']);
|
|
85
|
+
if (denied) return denied;
|
|
86
|
+
const snapshot = backupImage(ctx, backup);
|
|
87
|
+
const record = await makeBranch(ctx, db!, { name, parent: String((backup.database_branch as Row).name), production: false, parentSchema: snapshot.tables, restoredFrom: source });
|
|
88
|
+
const scope = branchScope(database, name)!;
|
|
89
|
+
await seedScope(storeCtx(ctx), scope, snapshot);
|
|
90
|
+
await ctx.write('Backup', String(backup.id), { ...ctx.own(backup), restored_branches: [...((backup.restored_branches ?? []) as string[]), String(record.id)] }, 'backup.restore');
|
|
91
|
+
await audit(ctx, orgOf(ctx), 'database_branch', 'created', { type: 'DatabaseBranch', id: String(record.id), name }, { auditable: { type: 'Database', id: String(db!.id), name: database } });
|
|
92
|
+
return ctx.reply(view(ctx, record), 201);
|
|
93
|
+
}
|
|
94
|
+
const denied = lacks(ctx, database, ['create_branch']);
|
|
95
|
+
if (denied) return denied;
|
|
96
|
+
const parentName = typeof b.parent_branch === 'string' && b.parent_branch ? b.parent_branch : String(db!.default_branch);
|
|
97
|
+
const parent = branchesOf(ctx, database).find((x) => x.name === parentName);
|
|
98
|
+
if (!parent) return ctx.notFound('DatabaseBranch', parentName, 'parent_branch');
|
|
99
|
+
const schema = scopeImage(ctx.root, branchScope(database, parentName)).tables;
|
|
100
|
+
const record = await makeBranch(ctx, db!, { name, parent: parentName, production: false, parentSchema: schema });
|
|
101
|
+
await seedScope(storeCtx(ctx), branchScope(database, name)!, { tables: schema, rows: [] });
|
|
102
|
+
await audit(ctx, orgOf(ctx), 'database_branch', 'created', { type: 'DatabaseBranch', id: String(record.id), name }, { auditable: { type: 'Database', id: String(db!.id), name: database } });
|
|
103
|
+
return ctx.reply(view(ctx, record), 201);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The image a backup holds (src/planetscale-store.ts, `_snapshot`), or an empty one for a backup that holds none. */
|
|
107
|
+
export function backupImage(ctx: SemanticsContext, backup: Row): ScopeImage {
|
|
108
|
+
const snap = backup._snapshot === undefined ? undefined : ctx.tree().find((r) => r.type === '_snapshot' && r.id === backup._snapshot);
|
|
109
|
+
const image = snap ? (ctx.own(snap).image as ScopeImage | undefined) : undefined;
|
|
110
|
+
return image ?? { tables: [], rows: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function getBranch(ctx: SemanticsContext): Promise<Response> {
|
|
114
|
+
const { refused, db } = createdDatabase(ctx);
|
|
115
|
+
if (refused) return refused;
|
|
116
|
+
const b = branchesOf(ctx, String(db!.name)).find((x) => x.name === ctx.call.params.branch);
|
|
117
|
+
return b ? ctx.reply(view(ctx, b)) : ctx.notFound('DatabaseBranch', String(ctx.call.params.branch), 'branch');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** list_branches, oldest first (the twin's order), filtered by `q` (a name contains it), `production` and
|
|
121
|
+
* `safe_migrations`. */
|
|
122
|
+
async function listBranches(ctx: SemanticsContext): Promise<Response> {
|
|
123
|
+
const { refused, db } = createdDatabase(ctx);
|
|
124
|
+
if (refused) return refused;
|
|
125
|
+
const q = new URL(ctx.call.request.url).searchParams;
|
|
126
|
+
const flag = (k: string): boolean | undefined => (q.has(k) ? q.get(k) === 'true' : undefined);
|
|
127
|
+
const production = flag('production');
|
|
128
|
+
const safe = flag('safe_migrations');
|
|
129
|
+
const rows = branchesOf(ctx, String(db!.name)).filter((b) => (!q.get('q') || String(b.name).includes(q.get('q')!))
|
|
130
|
+
&& (production === undefined || b.production === production) && (safe === undefined || b.safe_migrations === safe));
|
|
131
|
+
return page(ctx, rows.map((b) => view(ctx, b)));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* delete_branch. "You cannot delete a branch that's set as default" and "Only Organization Administrators have
|
|
136
|
+
* permission to delete production branches" (https://planetscale.com/docs/vitess/schema-changes/branching); a token needs
|
|
137
|
+
* `delete_production_branch` for a production branch ("Delete a production database branch",
|
|
138
|
+
* https://planetscale.com/docs/api/reference/service-tokens). The branch's tables, rows and passwords go with it.
|
|
139
|
+
*/
|
|
140
|
+
async function deleteBranch(ctx: SemanticsContext): Promise<Response> {
|
|
141
|
+
const { refused, db } = createdDatabase(ctx);
|
|
142
|
+
if (refused) return refused;
|
|
143
|
+
const database = String(db!.name);
|
|
144
|
+
const b = branchesOf(ctx, database).find((x) => x.name === ctx.call.params.branch);
|
|
145
|
+
if (!b) return ctx.notFound('DatabaseBranch', String(ctx.call.params.branch), 'branch');
|
|
146
|
+
if (b.name === db!.default_branch) return unprocessable(ctx, 'You cannot delete the default branch');
|
|
147
|
+
if (b.production === true) { const denied = lacks(ctx, database, ['delete_production_branch']); if (denied) return denied; }
|
|
148
|
+
await removeBranch(ctx, database, b);
|
|
149
|
+
return new Response(null, { status: 204 });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A branch gone: its passwords stop authenticating and its tables and rows are dropped. */
|
|
153
|
+
export async function removeBranch(ctx: SemanticsContext, database: string, b: Row, actor?: AuditActor): Promise<void> {
|
|
154
|
+
const at = ctx.occurredAt;
|
|
155
|
+
for (const p of ctx.rowsRaw('DatabaseBranchPassword')) {
|
|
156
|
+
if (p._database === database && (p.database_branch as Row | undefined)?.name === b.name && p.deleted_at === null) {
|
|
157
|
+
await ctx.write('DatabaseBranchPassword', String(p.id), { ...ctx.own(p), deleted_at: at }, 'password.delete');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// a deploy request outlives its branch, marked `branch_deleted` (the spec's DatabaseDeployRequest)
|
|
161
|
+
for (const dr of ctx.rowsRaw('DatabaseDeployRequest')) {
|
|
162
|
+
if (dr._database === database && dr.branch === b.name && dr.branch_deleted !== true) {
|
|
163
|
+
await ctx.write('DatabaseDeployRequest', String(dr.id), { ...ctx.own(dr), branch_deleted: true, branch_deleted_at: at, branch_deleted_by: actor ? { id: actor.id, display_name: actor.name, avatar_url: AVATAR } : actorOf(ctx), updated_at: at }, 'deploy_request.branch_deleted');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const scope = branchScope(database, String(b.name));
|
|
167
|
+
if (scope) await dropScope(storeCtx(ctx), scope);
|
|
168
|
+
await ctx.write('DatabaseBranch', String(b.id), { ...ctx.own(b), deleted_at: at, updated_at: at }, 'branch.delete');
|
|
169
|
+
const db = ctx.rowsRaw('Database').find((d) => d.name === database);
|
|
170
|
+
await audit(ctx, String(db?._organization ?? orgOf(ctx)), 'database_branch', 'deleted', { type: 'DatabaseBranch', id: String(b.id), name: String(b.name) }, { auditable: { type: 'Database', id: String(db?.id ?? database), name: database }, ...(actor ? { actor } : {}) });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* enable_safe_migrations / disable_safe_migrations. "With safe migrations enabled, Data Definition Language (DDL)
|
|
175
|
+
* statements issued to branches with safe migrations enabled will automatically be rejected by PlanetScale"
|
|
176
|
+
* (https://planetscale.com/docs/vitess/schema-changes/safe-migrations): psdb refuses DDL on the branch while the flag is
|
|
177
|
+
* set (the pack root's planetscale-twin.ts). The answer is the branch. The spec names no access for either operation;
|
|
178
|
+
* the twin's gate asks `write_database` (token-gate.ts).
|
|
179
|
+
*/
|
|
180
|
+
function safeMigrations(on: boolean) {
|
|
181
|
+
return async (ctx: SemanticsContext): Promise<Response> => {
|
|
182
|
+
const { refused, db } = createdDatabase(ctx);
|
|
183
|
+
if (refused) return refused;
|
|
184
|
+
const b = branchesOf(ctx, String(db!.name)).find((x) => x.name === ctx.call.params.branch);
|
|
185
|
+
if (!b) return ctx.notFound('DatabaseBranch', String(ctx.call.params.branch), 'branch');
|
|
186
|
+
if (b.safe_migrations !== on) {
|
|
187
|
+
await ctx.write('DatabaseBranch', String(b.id), { ...ctx.own(b), safe_migrations: on, updated_at: ctx.occurredAt }, on ? 'branch.enable_safe_migrations' : 'branch.disable_safe_migrations');
|
|
188
|
+
await audit(ctx, orgOf(ctx), 'database_branch', on ? 'enabled_safe_migrations' : 'disabled_safe_migrations', { type: 'DatabaseBranch', id: String(b.id), name: String(b.name) }, { auditable: { type: 'Database', id: String(db!.id), name: String(db!.name) } });
|
|
189
|
+
}
|
|
190
|
+
return ctx.reply(view(ctx, branchesOf(ctx, String(db!.name)).find((x) => x.id === b.id)!));
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const branchSemantics = {
|
|
195
|
+
enable_safe_migrations: safeMigrations(true),
|
|
196
|
+
disable_safe_migrations: safeMigrations(false),
|
|
197
|
+
create_branch: createBranch,
|
|
198
|
+
get_branch: getBranch,
|
|
199
|
+
list_branches: listBranches,
|
|
200
|
+
delete_branch: deleteBranch,
|
|
201
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SemanticsContext } from '@volter/world-core';
|
|
2
|
+
import { type AuditActor } from './audit.js';
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
export declare const CHECK_AFTER = 30000;
|
|
5
|
+
export declare const RUN_FOR = 120000;
|
|
6
|
+
export declare const REVERT_WINDOW: number;
|
|
7
|
+
/** The spec's Deployment for a deploy request as it stands. */
|
|
8
|
+
export declare function deploymentOf(dr: Row): Row;
|
|
9
|
+
/** Who is acting: the calling token's id, or the person a page names (screens pass it). */
|
|
10
|
+
export declare const actingAs: (ctx: SemanticsContext) => string;
|
|
11
|
+
declare function createDeployRequest(ctx: SemanticsContext): Promise<Response>;
|
|
12
|
+
declare function getDeployRequest(ctx: SemanticsContext): Promise<Response>;
|
|
13
|
+
declare function getDeployment(ctx: SemanticsContext): Promise<Response>;
|
|
14
|
+
declare function listDeployOperations(ctx: SemanticsContext): Promise<Response>;
|
|
15
|
+
/** list_deploy_requests, newest first (the twin's order), filtered by `state`, `branch` and `into_branch`. */
|
|
16
|
+
declare function listDeployRequests(ctx: SemanticsContext): Promise<Response>;
|
|
17
|
+
/** The reviews of a deploy request (bookkeeping `_deploy_review` records), oldest first. */
|
|
18
|
+
export declare const reviewsOf: (ctx: SemanticsContext, dr: Row) => Row[];
|
|
19
|
+
/**
|
|
20
|
+
* Record a review. `reviewer` is who approves: a token's id, or `person:<email>` from the deploy request's page;
|
|
21
|
+
* `actor` is the spec's actor object. With approval required, a creator cannot approve its own request, nor a person the
|
|
22
|
+
* token that created it (`_created_by` on the token, set on the service tokens page).
|
|
23
|
+
*/
|
|
24
|
+
export declare function recordReview(ctx: SemanticsContext, db: Row, dr: Row, reviewer: string, actor: Row, state: 'approved' | 'commented', text: string, auditActor: AuditActor): Promise<Row | Response>;
|
|
25
|
+
declare function reviewDeployRequest(ctx: SemanticsContext): Promise<Response>;
|
|
26
|
+
declare function listReviews(ctx: SemanticsContext): Promise<Response>;
|
|
27
|
+
/** queue_deploy_request: a deployable request, approved where the database requires it, joins the serial queue. */
|
|
28
|
+
declare function queueDeployRequest(ctx: SemanticsContext): Promise<Response>;
|
|
29
|
+
declare function closeDeployRequest(ctx: SemanticsContext): Promise<Response>;
|
|
30
|
+
/** Deploy a request's diff to its base at `at`, saving what a revert needs (each dropped column's values by primary key,
|
|
31
|
+
* each dropped table's definition and rows). */
|
|
32
|
+
export declare function applyDeploy(ctx: SemanticsContext, dr: Row, at: string): Promise<Row>;
|
|
33
|
+
declare function completeRevert(ctx: SemanticsContext): Promise<Response>;
|
|
34
|
+
export declare function catchUpDeployRequests(ctx: SemanticsContext): Promise<void>;
|
|
35
|
+
export declare const deployRequestSemantics: {
|
|
36
|
+
create_deploy_request: typeof createDeployRequest;
|
|
37
|
+
get_deploy_request: typeof getDeployRequest;
|
|
38
|
+
list_deploy_requests: typeof listDeployRequests;
|
|
39
|
+
get_deployment: typeof getDeployment;
|
|
40
|
+
list_deploy_operations: typeof listDeployOperations;
|
|
41
|
+
review_deploy_request: typeof reviewDeployRequest;
|
|
42
|
+
list_deploy_request_reviews: typeof listReviews;
|
|
43
|
+
queue_deploy_request: typeof queueDeployRequest;
|
|
44
|
+
close_deploy_request: typeof closeDeployRequest;
|
|
45
|
+
complete_revert: typeof completeRevert;
|
|
46
|
+
};
|
|
47
|
+
export {};
|