@alchemy.run/planetscale 0.0.0-stage → 2.0.0-beta.84
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 +201 -0
- package/README.md +87 -2
- package/lib/AuthProvider.d.ts +64 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +384 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Branch.d.ts +128 -0
- package/lib/Branch.d.ts.map +1 -0
- package/lib/Branch.js +420 -0
- package/lib/Branch.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/Credentials.d.ts +33 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +65 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Database.d.ts +138 -0
- package/lib/Database.d.ts.map +1 -0
- package/lib/Database.js +1 -0
- package/lib/Database.js.map +1 -0
- package/lib/MySQL/MySQLBranch.d.ts +87 -0
- package/lib/MySQL/MySQLBranch.d.ts.map +1 -0
- package/lib/MySQL/MySQLBranch.js +15 -0
- package/lib/MySQL/MySQLBranch.js.map +1 -0
- package/lib/MySQL/MySQLClusterSize.d.ts +32 -0
- package/lib/MySQL/MySQLClusterSize.d.ts.map +1 -0
- package/lib/MySQL/MySQLClusterSize.js +111 -0
- package/lib/MySQL/MySQLClusterSize.js.map +1 -0
- package/lib/MySQL/MySQLDatabase.d.ts +141 -0
- package/lib/MySQL/MySQLDatabase.d.ts.map +1 -0
- package/lib/MySQL/MySQLDatabase.js +294 -0
- package/lib/MySQL/MySQLDatabase.js.map +1 -0
- package/lib/MySQL/MySQLMigrations.d.ts +24 -0
- package/lib/MySQL/MySQLMigrations.d.ts.map +1 -0
- package/lib/MySQL/MySQLMigrations.js +106 -0
- package/lib/MySQL/MySQLMigrations.js.map +1 -0
- package/lib/MySQL/MySQLOrigin.d.ts +15 -0
- package/lib/MySQL/MySQLOrigin.d.ts.map +1 -0
- package/lib/MySQL/MySQLOrigin.js +2 -0
- package/lib/MySQL/MySQLOrigin.js.map +1 -0
- package/lib/MySQL/MySQLPassword.d.ts +116 -0
- package/lib/MySQL/MySQLPassword.d.ts.map +1 -0
- package/lib/MySQL/MySQLPassword.js +212 -0
- package/lib/MySQL/MySQLPassword.js.map +1 -0
- package/lib/MySQL/index.d.ts +6 -0
- package/lib/MySQL/index.d.ts.map +1 -0
- package/lib/MySQL/index.js +6 -0
- package/lib/MySQL/index.js.map +1 -0
- package/lib/OAuthClient.d.ts +32 -0
- package/lib/OAuthClient.d.ts.map +1 -0
- package/lib/OAuthClient.js +52 -0
- package/lib/OAuthClient.js.map +1 -0
- package/lib/Postgres/PostgresBranch.d.ts +75 -0
- package/lib/Postgres/PostgresBranch.d.ts.map +1 -0
- package/lib/Postgres/PostgresBranch.js +15 -0
- package/lib/Postgres/PostgresBranch.js.map +1 -0
- package/lib/Postgres/PostgresClusterSize.d.ts +77 -0
- package/lib/Postgres/PostgresClusterSize.d.ts.map +1 -0
- package/lib/Postgres/PostgresClusterSize.js +89 -0
- package/lib/Postgres/PostgresClusterSize.js.map +1 -0
- package/lib/Postgres/PostgresDatabase.d.ts +95 -0
- package/lib/Postgres/PostgresDatabase.d.ts.map +1 -0
- package/lib/Postgres/PostgresDatabase.js +294 -0
- package/lib/Postgres/PostgresDatabase.js.map +1 -0
- package/lib/Postgres/PostgresDefaultRole.d.ts +94 -0
- package/lib/Postgres/PostgresDefaultRole.d.ts.map +1 -0
- package/lib/Postgres/PostgresDefaultRole.js +187 -0
- package/lib/Postgres/PostgresDefaultRole.js.map +1 -0
- package/lib/Postgres/PostgresMigrations.d.ts +24 -0
- package/lib/Postgres/PostgresMigrations.d.ts.map +1 -0
- package/lib/Postgres/PostgresMigrations.js +116 -0
- package/lib/Postgres/PostgresMigrations.js.map +1 -0
- package/lib/Postgres/PostgresOrigin.d.ts +15 -0
- package/lib/Postgres/PostgresOrigin.d.ts.map +1 -0
- package/lib/Postgres/PostgresOrigin.js +2 -0
- package/lib/Postgres/PostgresOrigin.js.map +1 -0
- package/lib/Postgres/PostgresRole.d.ts +142 -0
- package/lib/Postgres/PostgresRole.d.ts.map +1 -0
- package/lib/Postgres/PostgresRole.js +268 -0
- package/lib/Postgres/PostgresRole.js.map +1 -0
- package/lib/Postgres/index.d.ts +7 -0
- package/lib/Postgres/index.d.ts.map +1 -0
- package/lib/Postgres/index.js +7 -0
- package/lib/Postgres/index.js.map +1 -0
- package/lib/Providers.d.ts +24 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +40 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Util.d.ts +93 -0
- package/lib/Util.d.ts.map +1 -0
- package/lib/Util.js +135 -0
- package/lib/Util.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +9 -0
- package/lib/index.js.map +1 -0
- package/package.json +94 -3
- package/src/AuthProvider.ts +571 -0
- package/src/Branch.ts +657 -0
- package/src/Contributions.ts +9 -0
- package/src/Credentials.ts +116 -0
- package/src/Database.ts +152 -0
- package/src/MySQL/MySQLBranch.ts +109 -0
- package/src/MySQL/MySQLClusterSize.ts +185 -0
- package/src/MySQL/MySQLDatabase.ts +521 -0
- package/src/MySQL/MySQLMigrations.ts +182 -0
- package/src/MySQL/MySQLOrigin.ts +15 -0
- package/src/MySQL/MySQLPassword.ts +428 -0
- package/src/MySQL/index.ts +5 -0
- package/src/OAuthClient.ts +59 -0
- package/src/Postgres/PostgresBranch.ts +97 -0
- package/src/Postgres/PostgresClusterSize.ts +262 -0
- package/src/Postgres/PostgresDatabase.ts +469 -0
- package/src/Postgres/PostgresDefaultRole.ts +340 -0
- package/src/Postgres/PostgresMigrations.ts +193 -0
- package/src/Postgres/PostgresOrigin.ts +15 -0
- package/src/Postgres/PostgresRole.ts +547 -0
- package/src/Postgres/index.ts +6 -0
- package/src/Providers.ts +67 -0
- package/src/Util.ts +205 -0
- package/src/index.ts +14 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import {
|
|
2
|
+
diffMigrations,
|
|
3
|
+
migrationsAttrs,
|
|
4
|
+
migrationsInputOf,
|
|
5
|
+
stampedOf,
|
|
6
|
+
} from "@alchemy.run/sql/Migrations";
|
|
7
|
+
import { hashImports } from "@alchemy.run/sql/SqlFile";
|
|
8
|
+
import * as planetscale from "@distilled.cloud/planetscale";
|
|
9
|
+
import { Credentials } from "@distilled.cloud/planetscale/Credentials";
|
|
10
|
+
import { havePropsChanged, isResolved } from "alchemy/Diff";
|
|
11
|
+
import { createPhysicalName } from "alchemy/PhysicalName";
|
|
12
|
+
import * as Provider from "alchemy/Provider";
|
|
13
|
+
import { Resource } from "alchemy/Resource";
|
|
14
|
+
import { recordsEqual } from "alchemy/Util/equal";
|
|
15
|
+
import * as Effect from "effect/Effect";
|
|
16
|
+
import * as Stream from "effect/Stream";
|
|
17
|
+
import type { BaseDatabaseAttributes, BaseDatabaseProps } from "../Database.ts";
|
|
18
|
+
import type { Providers } from "../Providers.ts";
|
|
19
|
+
import {
|
|
20
|
+
deleteUnprotectedDatabase,
|
|
21
|
+
PlanetscaleConflict,
|
|
22
|
+
replaceDatabase,
|
|
23
|
+
waitForBranchReady,
|
|
24
|
+
waitForDatabaseReady,
|
|
25
|
+
} from "../Util.ts";
|
|
26
|
+
import {
|
|
27
|
+
ensurePostgresProductionBranchClusterSize,
|
|
28
|
+
toPostgresClusterArch,
|
|
29
|
+
toPostgresClusterSku,
|
|
30
|
+
type PostgresClusterSize,
|
|
31
|
+
} from "./PostgresClusterSize.ts";
|
|
32
|
+
import { runPostgresImports, runPostgresMigrations } from "./PostgresMigrations.ts";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Properties for creating or updating a PostgreSQL PlanetScale database.
|
|
36
|
+
*/
|
|
37
|
+
export interface PostgresDatabaseProps extends BaseDatabaseProps {
|
|
38
|
+
/**
|
|
39
|
+
* The PostgreSQL database cluster size. Required.
|
|
40
|
+
*
|
|
41
|
+
* Short NAS sizes (`"PS_10"`) are expanded using the target region and
|
|
42
|
+
* architecture. [PlanetScale Metal](https://planetscale.com/docs/metal)
|
|
43
|
+
* requires the full SKU (e.g. `"M1_10_AWS_ARM_D_METAL_10"`) because
|
|
44
|
+
* Metal also encodes CPU series, architecture, and NVMe drive size.
|
|
45
|
+
*
|
|
46
|
+
* @see https://planetscale.com/docs/postgres/pricing
|
|
47
|
+
* @see {@link PostgresClusterSize}
|
|
48
|
+
*/
|
|
49
|
+
clusterSize: PostgresClusterSize;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* PostgreSQL major version. Defaults to the latest available major
|
|
53
|
+
* version.
|
|
54
|
+
*/
|
|
55
|
+
majorVersion?: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CPU architecture. Cannot be changed after creation.
|
|
59
|
+
* @default "x86"
|
|
60
|
+
*/
|
|
61
|
+
arch?: "x86" | "arm";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Output attributes of a deployed PostgreSQL PlanetScale database.
|
|
66
|
+
*/
|
|
67
|
+
export interface PostgresDatabaseAttributes extends BaseDatabaseAttributes {
|
|
68
|
+
/** PostgreSQL CPU architecture. */
|
|
69
|
+
arch: "x86" | "arm";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A PostgreSQL PlanetScale database. For MySQL, use {@link MySQLDatabase}
|
|
74
|
+
* instead.
|
|
75
|
+
*
|
|
76
|
+
* ### Creating a PostgreSQL Database
|
|
77
|
+
* **Example:** Basic PostgreSQL database
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const db = yield* Planetscale.PostgresDatabase("MyDb", {
|
|
80
|
+
* clusterSize: "PS_10",
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* **Example:** PlanetScale Metal (NVMe)
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const db = yield* Planetscale.PostgresDatabase("MyDb", {
|
|
87
|
+
* clusterSize: "M1_10_AWS_ARM_D_METAL_10",
|
|
88
|
+
* arch: "arm",
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* ### Migrations and seed data
|
|
93
|
+
* **Example:** Apply migrations and seed files
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const db = yield* Planetscale.PostgresDatabase("MyDb", {
|
|
96
|
+
* clusterSize: "PS_10",
|
|
97
|
+
* migrationsDir: "./migrations/postgres",
|
|
98
|
+
* importFiles: ["./seed/postgres.sql"],
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* ### Deletion protection
|
|
103
|
+
* **Example:** Refuse deletes of a production database
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const db = yield* Planetscale.PostgresDatabase("MyDb", {
|
|
106
|
+
* clusterSize: "PS_10",
|
|
107
|
+
* deletionProtection: true,
|
|
108
|
+
* });
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* ### Adoption
|
|
112
|
+
* **Example:** Adopting an existing database
|
|
113
|
+
* ```typescript
|
|
114
|
+
* import { adopt } from "alchemy/AdoptPolicy";
|
|
115
|
+
*
|
|
116
|
+
* const db = yield* Planetscale.PostgresDatabase("Existing", {
|
|
117
|
+
* name: "existing-db",
|
|
118
|
+
* clusterSize: "PS_10",
|
|
119
|
+
* }).pipe(adopt());
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export type PostgresDatabase = Resource<
|
|
123
|
+
"Planetscale.PostgresDatabase",
|
|
124
|
+
PostgresDatabaseProps,
|
|
125
|
+
PostgresDatabaseAttributes,
|
|
126
|
+
never,
|
|
127
|
+
Providers
|
|
128
|
+
>;
|
|
129
|
+
|
|
130
|
+
/** @resource */
|
|
131
|
+
export const PostgresDatabase = Resource<PostgresDatabase>("Planetscale.PostgresDatabase");
|
|
132
|
+
|
|
133
|
+
export const PostgresDatabaseProvider = () =>
|
|
134
|
+
Provider.succeed(PostgresDatabase, {
|
|
135
|
+
stables: ["id", "organization", "region"],
|
|
136
|
+
diff: Effect.fn(function* ({ news, olds, output }) {
|
|
137
|
+
if (!isResolved(news)) return undefined;
|
|
138
|
+
|
|
139
|
+
// Database names are rename-mutable (reconcile folds `new_name`
|
|
140
|
+
// into the settings sync), so `name` cannot live in the
|
|
141
|
+
// provider-level stables. Almost no update is a rename though —
|
|
142
|
+
// for those, advertise `name` as stable on the update so
|
|
143
|
+
// downstream consumers (branches, roles) still resolve
|
|
144
|
+
// `database.name` at plan time instead of seeing `undefined` and
|
|
145
|
+
// falsely planning a replacement. The name only changes when the
|
|
146
|
+
// `name` prop itself changes: an explicit name renames iff it
|
|
147
|
+
// differs from the observed name, and an omitted name is
|
|
148
|
+
// engine-generated deterministically (stable across updates).
|
|
149
|
+
const nameIsStable =
|
|
150
|
+
output?.name !== undefined &&
|
|
151
|
+
(news.name !== undefined ? news.name === output.name : olds?.name === undefined);
|
|
152
|
+
const stables = nameIsStable ? ["id", "organization", "region", "name"] : undefined;
|
|
153
|
+
|
|
154
|
+
if (news.region?.slug && output?.region?.slug && news.region.slug !== output.region.slug) {
|
|
155
|
+
return yield* replaceDatabase(news, output, "region");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (news.replicas !== olds.replicas) {
|
|
159
|
+
return yield* replaceDatabase(news, output, "replicas");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const oldArch = output?.arch ?? olds.arch ?? "x86";
|
|
163
|
+
if (news.arch && news.arch !== oldArch) {
|
|
164
|
+
return yield* replaceDatabase(news, output, "arch");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (yield* diffMigrations({ news, output })) {
|
|
168
|
+
return { action: "update", stables } as const;
|
|
169
|
+
}
|
|
170
|
+
if (news.importFiles?.length) {
|
|
171
|
+
const newHashes = yield* hashImports(news.importFiles, yield* rootDir);
|
|
172
|
+
if (!recordsEqual(newHashes, output?.importHashes ?? {})) {
|
|
173
|
+
return { action: "update", stables } as const;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Remaining prop changes (rename, settings, clusterSize, …) are
|
|
178
|
+
// in-place updates. Decide them here instead of falling back to
|
|
179
|
+
// the engine's default deep-compare so the conditional `name`
|
|
180
|
+
// stable above is attached — the default path uses the
|
|
181
|
+
// provider-level stables, which strip `name` from downstream plan
|
|
182
|
+
// resolution.
|
|
183
|
+
if (havePropsChanged(olds, news)) {
|
|
184
|
+
return { action: "update", stables } as const;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Nothing changed. Still advertise the conditional `name` stable so
|
|
188
|
+
// a `--force` deploy (which upgrades this noop to an update) keeps
|
|
189
|
+
// `name` resolvable downstream instead of falsely replacing
|
|
190
|
+
// consumers such as roles and passwords (#1832).
|
|
191
|
+
return stables ? ({ action: "noop", stables } as const) : undefined;
|
|
192
|
+
}),
|
|
193
|
+
|
|
194
|
+
read: Effect.fn(function* ({ id, output, olds }) {
|
|
195
|
+
const { organization } = yield* yield* Credentials;
|
|
196
|
+
const databaseName = output?.name ?? (yield* createDatabaseName(id, olds?.name));
|
|
197
|
+
|
|
198
|
+
const data = yield* planetscale
|
|
199
|
+
.getDatabase({
|
|
200
|
+
organization,
|
|
201
|
+
database: databaseName,
|
|
202
|
+
})
|
|
203
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
|
|
204
|
+
|
|
205
|
+
if (!data) return undefined;
|
|
206
|
+
|
|
207
|
+
if (data.kind !== "postgresql") {
|
|
208
|
+
return yield* new PlanetscaleConflict({
|
|
209
|
+
message:
|
|
210
|
+
`Planetscale database "${data.name}" has kind "${data.kind}" but this resource ` +
|
|
211
|
+
`is a PostgresDatabase. Use Planetscale.${data.kind === "mysql" ? "MySQLDatabase" : data.kind}() instead, ` +
|
|
212
|
+
`or delete the existing database and retry.`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const defaultBranch = data.default_branch ?? "main";
|
|
217
|
+
|
|
218
|
+
// Observe `arch` and `clusterSize` from the default branch rather
|
|
219
|
+
// than echoing input.
|
|
220
|
+
const branch = yield* planetscale
|
|
221
|
+
.getBranch({
|
|
222
|
+
organization,
|
|
223
|
+
database: data.name,
|
|
224
|
+
branch: defaultBranch,
|
|
225
|
+
})
|
|
226
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
|
|
227
|
+
|
|
228
|
+
const arch = toPostgresClusterArch(branch!.cluster_architecture);
|
|
229
|
+
const clusterSize = branch!.cluster_name;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
id: data.id,
|
|
233
|
+
name: data.name,
|
|
234
|
+
organization,
|
|
235
|
+
state: data.state,
|
|
236
|
+
defaultBranch,
|
|
237
|
+
plan: data.plan ?? "hobby",
|
|
238
|
+
createdAt: data.created_at,
|
|
239
|
+
updatedAt: data.updated_at,
|
|
240
|
+
htmlUrl: data.html_url,
|
|
241
|
+
region: { slug: data.region.slug },
|
|
242
|
+
migrationsDir: output?.migrationsDir ?? (olds && migrationsInputOf(olds))?.dir,
|
|
243
|
+
migrationsTable: output?.migrationsTable ?? (olds && migrationsInputOf(olds))?.table,
|
|
244
|
+
migrationsHashes: output?.migrationsHashes ?? {},
|
|
245
|
+
importHashes: output?.importHashes ?? {},
|
|
246
|
+
clusterSize,
|
|
247
|
+
arch,
|
|
248
|
+
requireApprovalForDeploy: data.require_approval_for_deploy ?? false,
|
|
249
|
+
restrictBranchRegion: data.restrict_branch_region ?? false,
|
|
250
|
+
productionBranchWebConsole: data.production_branch_web_console ?? false,
|
|
251
|
+
deletionProtection: data.deletion_protected ?? false,
|
|
252
|
+
};
|
|
253
|
+
}),
|
|
254
|
+
|
|
255
|
+
reconcile: Effect.fn(function* ({ id, news, output, session }) {
|
|
256
|
+
const { organization } = yield* yield* Credentials;
|
|
257
|
+
const newName = yield* createDatabaseName(id, news.name);
|
|
258
|
+
const clusterSize = toPostgresClusterSku({
|
|
259
|
+
size: news.clusterSize,
|
|
260
|
+
arch: news.arch,
|
|
261
|
+
region: news.region?.slug,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Observe — read live state under either the cached name (rename
|
|
265
|
+
// / refresh) or the freshly-derived name (greenfield). Observing
|
|
266
|
+
// unconditionally absorbs the race between `read` and `reconcile`
|
|
267
|
+
// — if a foreign actor (or a previously-failed-to-persist
|
|
268
|
+
// create) produced a database under the desired name in the
|
|
269
|
+
// meantime, we'll find it here and skip the duplicate create.
|
|
270
|
+
// Adoption routing has already been gated by `read` returning
|
|
271
|
+
// `Unowned`, so by the time we reach reconcile any observed
|
|
272
|
+
// database is fair game.
|
|
273
|
+
const observedName = output?.name ?? newName;
|
|
274
|
+
let observed = yield* planetscale
|
|
275
|
+
.getDatabase({
|
|
276
|
+
organization,
|
|
277
|
+
database: observedName,
|
|
278
|
+
})
|
|
279
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
|
|
280
|
+
|
|
281
|
+
// Ensure — if missing, create.
|
|
282
|
+
if (!observed) {
|
|
283
|
+
yield* session.note("Creating database...");
|
|
284
|
+
observed = yield* planetscale.createDatabase({
|
|
285
|
+
organization,
|
|
286
|
+
name: newName,
|
|
287
|
+
region: news.region?.slug,
|
|
288
|
+
kind: "postgresql",
|
|
289
|
+
cluster_size: clusterSize,
|
|
290
|
+
replicas: news.replicas,
|
|
291
|
+
major_version: news.majorVersion,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Wait for the database to finish provisioning before any
|
|
296
|
+
// downstream sync runs. PlanetScale reports `state: "pending"`
|
|
297
|
+
// for a while after createDatabase returns, during which branch
|
|
298
|
+
// operations on this database (including our own settings PATCH)
|
|
299
|
+
// race against the provisioning fiber.
|
|
300
|
+
yield* waitForDatabaseReady(organization, observed.name, session);
|
|
301
|
+
|
|
302
|
+
if (observed.kind !== "postgresql") {
|
|
303
|
+
return yield* new PlanetscaleConflict({
|
|
304
|
+
message:
|
|
305
|
+
`Planetscale database "${observed.name}" has kind "${observed.kind}" but this resource ` +
|
|
306
|
+
`is a PostgresDatabase. Use Planetscale.${observed.kind === "mysql" ? "MySQLDatabase" : observed.kind}() instead, ` +
|
|
307
|
+
`or delete the existing database and retry.`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Sync — ensure a non-`main` default branch exists before
|
|
312
|
+
// referencing it via `default_branch`. The branch is created
|
|
313
|
+
// empty (no cluster size yet); we'll size it below.
|
|
314
|
+
if (news.defaultBranch && news.defaultBranch !== "main") {
|
|
315
|
+
yield* waitForDatabaseReady(organization, observed.name, session);
|
|
316
|
+
const branchExists = yield* planetscale
|
|
317
|
+
.getBranch({
|
|
318
|
+
organization,
|
|
319
|
+
database: observed.name,
|
|
320
|
+
branch: news.defaultBranch,
|
|
321
|
+
})
|
|
322
|
+
.pipe(
|
|
323
|
+
Effect.map(() => true),
|
|
324
|
+
Effect.catchTag("NotFound", () => Effect.succeed(false)),
|
|
325
|
+
);
|
|
326
|
+
if (!branchExists) {
|
|
327
|
+
yield* planetscale.createBranch({
|
|
328
|
+
organization,
|
|
329
|
+
database: observed.name,
|
|
330
|
+
name: news.defaultBranch,
|
|
331
|
+
parent_branch: "main",
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Sync settings — `updateSettings` is upsert-shaped, so we can
|
|
337
|
+
// call it with the full desired payload on every reconcile.
|
|
338
|
+
// Folds the rename in via `new_name` so we don't pay for a
|
|
339
|
+
// separate PATCH.
|
|
340
|
+
const updated = yield* planetscale.updateDatabaseSettings({
|
|
341
|
+
organization,
|
|
342
|
+
database: observed.name,
|
|
343
|
+
new_name: newName !== observed.name ? newName : undefined,
|
|
344
|
+
require_approval_for_deploy: news.requireApprovalForDeploy,
|
|
345
|
+
restrict_branch_region: news.restrictBranchRegion,
|
|
346
|
+
production_branch_web_console: news.productionBranchWebConsole,
|
|
347
|
+
deletion_protected: news.deletionProtection,
|
|
348
|
+
default_branch: news.defaultBranch,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Sync cluster size on the active default branch.
|
|
352
|
+
const branch = news.defaultBranch ?? updated.default_branch ?? "main";
|
|
353
|
+
yield* ensurePostgresProductionBranchClusterSize(
|
|
354
|
+
organization,
|
|
355
|
+
updated.name,
|
|
356
|
+
branch,
|
|
357
|
+
news.clusterSize,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const migrationTarget = {
|
|
361
|
+
organization,
|
|
362
|
+
database: updated.name,
|
|
363
|
+
branch,
|
|
364
|
+
};
|
|
365
|
+
const migrationsInput = migrationsInputOf(news);
|
|
366
|
+
if (migrationsInput || news.importFiles?.length) {
|
|
367
|
+
yield* waitForBranchReady(organization, updated.name, branch, session);
|
|
368
|
+
}
|
|
369
|
+
const migrations = migrationsInput
|
|
370
|
+
? yield* runPostgresMigrations(migrationTarget, migrationsInput, stampedOf(output))
|
|
371
|
+
: undefined;
|
|
372
|
+
const importHashes = news.importFiles?.length
|
|
373
|
+
? yield* runPostgresImports(
|
|
374
|
+
migrationTarget,
|
|
375
|
+
news.importFiles,
|
|
376
|
+
yield* rootDir,
|
|
377
|
+
output?.importHashes ?? {},
|
|
378
|
+
)
|
|
379
|
+
: {};
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
id: updated.id,
|
|
383
|
+
name: updated.name,
|
|
384
|
+
organization,
|
|
385
|
+
state: updated.state,
|
|
386
|
+
defaultBranch: updated.default_branch ?? branch,
|
|
387
|
+
plan: updated.plan ?? output?.plan ?? "hobby",
|
|
388
|
+
createdAt: updated.created_at,
|
|
389
|
+
updatedAt: updated.updated_at,
|
|
390
|
+
htmlUrl: updated.html_url,
|
|
391
|
+
region: { slug: updated.region.slug },
|
|
392
|
+
clusterSize: clusterSize,
|
|
393
|
+
...migrationsAttrs({ input: migrationsInput, run: migrations, output }),
|
|
394
|
+
importHashes,
|
|
395
|
+
arch: news.arch ?? output?.arch ?? "x86",
|
|
396
|
+
requireApprovalForDeploy: updated.require_approval_for_deploy ?? false,
|
|
397
|
+
restrictBranchRegion: updated.restrict_branch_region ?? false,
|
|
398
|
+
productionBranchWebConsole: updated.production_branch_web_console ?? false,
|
|
399
|
+
deletionProtection: updated.deletion_protected ?? false,
|
|
400
|
+
} satisfies PostgresDatabaseAttributes;
|
|
401
|
+
}),
|
|
402
|
+
|
|
403
|
+
delete: Effect.fn(function* ({ output }) {
|
|
404
|
+
yield* deleteUnprotectedDatabase(output.organization, output.name);
|
|
405
|
+
}),
|
|
406
|
+
|
|
407
|
+
list: Effect.fn(function* () {
|
|
408
|
+
const { organization } = yield* yield* Credentials;
|
|
409
|
+
|
|
410
|
+
const databases = yield* planetscale.listDatabases.pages({ organization }).pipe(
|
|
411
|
+
Stream.runCollect,
|
|
412
|
+
Effect.map((chunk) =>
|
|
413
|
+
Array.from(chunk).flatMap((page) => page.data.filter((db) => db.kind === "postgresql")),
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const rows = yield* Effect.forEach(
|
|
418
|
+
databases,
|
|
419
|
+
(data) =>
|
|
420
|
+
Effect.gen(function* () {
|
|
421
|
+
const defaultBranch = data.default_branch ?? "main";
|
|
422
|
+
const branch = yield* planetscale
|
|
423
|
+
.getBranch({
|
|
424
|
+
organization,
|
|
425
|
+
database: data.name,
|
|
426
|
+
branch: defaultBranch,
|
|
427
|
+
})
|
|
428
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
|
|
429
|
+
|
|
430
|
+
const arch = toPostgresClusterArch(branch?.cluster_architecture);
|
|
431
|
+
const clusterSize = branch?.cluster_name ?? "";
|
|
432
|
+
|
|
433
|
+
const attrs: PostgresDatabase["Attributes"] = {
|
|
434
|
+
id: data.id,
|
|
435
|
+
name: data.name,
|
|
436
|
+
organization,
|
|
437
|
+
state: data.state,
|
|
438
|
+
defaultBranch,
|
|
439
|
+
plan: data.plan ?? "hobby",
|
|
440
|
+
createdAt: data.created_at,
|
|
441
|
+
updatedAt: data.updated_at,
|
|
442
|
+
htmlUrl: data.html_url,
|
|
443
|
+
region: { slug: data.region.slug },
|
|
444
|
+
clusterSize,
|
|
445
|
+
migrationsDir: undefined,
|
|
446
|
+
migrationsTable: undefined,
|
|
447
|
+
migrationsHashes: {},
|
|
448
|
+
importHashes: {},
|
|
449
|
+
arch,
|
|
450
|
+
requireApprovalForDeploy: data.require_approval_for_deploy ?? false,
|
|
451
|
+
restrictBranchRegion: data.restrict_branch_region ?? false,
|
|
452
|
+
productionBranchWebConsole: data.production_branch_web_console ?? false,
|
|
453
|
+
deletionProtection: data.deletion_protected ?? false,
|
|
454
|
+
};
|
|
455
|
+
return attrs;
|
|
456
|
+
}),
|
|
457
|
+
{ concurrency: 10 },
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
return rows;
|
|
461
|
+
}),
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const createDatabaseName = (id: string, name: string | undefined) =>
|
|
465
|
+
Effect.gen(function* () {
|
|
466
|
+
return name ?? (yield* createPhysicalName({ id, lowercase: true, maxLength: 63 }));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const rootDir = Effect.sync(() => process.cwd());
|