@alchemy.run/planetscale 0.0.0 → 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 +83 -4
- 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 +79 -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,116 @@
|
|
|
1
|
+
import { ConfigError } from "@distilled.cloud/core/errors";
|
|
2
|
+
import {
|
|
3
|
+
type Config as PlanetscaleClientConfig,
|
|
4
|
+
Credentials,
|
|
5
|
+
DEFAULT_API_BASE_URL,
|
|
6
|
+
} from "@distilled.cloud/planetscale/Credentials";
|
|
7
|
+
import * as CredentialsCache from "alchemy/Auth/CredentialsCache";
|
|
8
|
+
import {
|
|
9
|
+
deferUntilFirstUse,
|
|
10
|
+
orDieCredentialsUnavailable,
|
|
11
|
+
resolveProviderConfig,
|
|
12
|
+
} from "alchemy/Auth/Resolve";
|
|
13
|
+
import * as Config from "effect/Config";
|
|
14
|
+
import * as Effect from "effect/Effect";
|
|
15
|
+
import * as Layer from "effect/Layer";
|
|
16
|
+
import * as Redacted from "effect/Redacted";
|
|
17
|
+
import {
|
|
18
|
+
PLANETSCALE_AUTH_PROVIDER_NAME,
|
|
19
|
+
type PlanetscaleAuthConfig,
|
|
20
|
+
type PlanetscaleResolvedCredentials,
|
|
21
|
+
} from "./AuthProvider.ts";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
Credentials,
|
|
25
|
+
CredentialsFromEnv,
|
|
26
|
+
DEFAULT_API_BASE_URL,
|
|
27
|
+
} from "@distilled.cloud/planetscale/Credentials";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a PlanetScale `Credentials` Layer from an explicit token. Useful for
|
|
31
|
+
* tests or when the caller already has credentials in hand.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* Effect.provide(
|
|
36
|
+
* Planetscale.fromToken({
|
|
37
|
+
* tokenId: "abcd1234",
|
|
38
|
+
* token: "api-token-secret",
|
|
39
|
+
* organization: "my-org",
|
|
40
|
+
* }),
|
|
41
|
+
* )
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const fromToken = (input: {
|
|
45
|
+
tokenId: string | Redacted.Redacted<string>;
|
|
46
|
+
token: string | Redacted.Redacted<string>;
|
|
47
|
+
organization: string;
|
|
48
|
+
apiBaseUrl?: string;
|
|
49
|
+
}) =>
|
|
50
|
+
Layer.succeed(
|
|
51
|
+
Credentials,
|
|
52
|
+
Effect.succeed({
|
|
53
|
+
tokenId: typeof input.tokenId === "string" ? Redacted.make(input.tokenId) : input.tokenId,
|
|
54
|
+
token: typeof input.token === "string" ? Redacted.make(input.token) : input.token,
|
|
55
|
+
organization: input.organization,
|
|
56
|
+
apiBaseUrl: input.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a PlanetScale `Credentials` Layer that resolves credentials via the
|
|
62
|
+
* Alchemy AuthProvider using the configured profile (defaults to "default",
|
|
63
|
+
* selected by the current Alchemy profile).
|
|
64
|
+
*/
|
|
65
|
+
export const fromAuthProvider = () =>
|
|
66
|
+
Layer.effect(
|
|
67
|
+
Credentials,
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const apiBaseUrl = yield* Config.String("PLANETSCALE_API_BASE_URL").pipe(
|
|
70
|
+
Config.withDefault(DEFAULT_API_BASE_URL),
|
|
71
|
+
);
|
|
72
|
+
// Defer profile lookup and credential resolution until first use, so
|
|
73
|
+
// building the provider layers never requires a configured profile.
|
|
74
|
+
const resolve = yield* resolveProviderConfig<
|
|
75
|
+
PlanetscaleAuthConfig,
|
|
76
|
+
PlanetscaleResolvedCredentials
|
|
77
|
+
>(PLANETSCALE_AUTH_PROVIDER_NAME).pipe(
|
|
78
|
+
Effect.flatMap(({ profileName, resolve }) =>
|
|
79
|
+
Effect.mapError(resolve, (e) =>
|
|
80
|
+
ConfigError.make({
|
|
81
|
+
message: `Failed to resolve Planetscale credentials from ${profileName === undefined ? "the CI environment" : `profile '${profileName}'`}: ${e.message}`,
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
deferUntilFirstUse,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Cache until shortly before the OAuth token expires (service tokens
|
|
89
|
+
// never expire) so a long `alchemy dev` session re-resolves — and
|
|
90
|
+
// thereby refreshes — the token instead of keeping the first, by then
|
|
91
|
+
// dead, resolution forever. The cache wraps `resolve` (not the mapped
|
|
92
|
+
// client config) because the mapping drops the `expires` timestamp.
|
|
93
|
+
const cached = yield* CredentialsCache.cacheUntilExpiry(resolve, (creds) =>
|
|
94
|
+
creds.type === "oauth" ? creds.expires : undefined,
|
|
95
|
+
);
|
|
96
|
+
return cached.pipe(
|
|
97
|
+
Effect.map((creds): PlanetscaleClientConfig =>
|
|
98
|
+
creds.type === "oauth"
|
|
99
|
+
? {
|
|
100
|
+
type: "oauth",
|
|
101
|
+
accessToken: creds.accessToken,
|
|
102
|
+
organization: creds.organization,
|
|
103
|
+
apiBaseUrl,
|
|
104
|
+
}
|
|
105
|
+
: {
|
|
106
|
+
type: "serviceToken",
|
|
107
|
+
tokenId: creds.tokenId,
|
|
108
|
+
token: creds.token,
|
|
109
|
+
organization: creds.organization,
|
|
110
|
+
apiBaseUrl,
|
|
111
|
+
},
|
|
112
|
+
),
|
|
113
|
+
orDieCredentialsUnavailable(PLANETSCALE_AUTH_PROVIDER_NAME),
|
|
114
|
+
);
|
|
115
|
+
}),
|
|
116
|
+
);
|
package/src/Database.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { MigrationsInput } from "@alchemy.run/sql/Migrations";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Region selector for a PlanetScale Database.
|
|
5
|
+
*/
|
|
6
|
+
export interface DatabaseRegion {
|
|
7
|
+
/**
|
|
8
|
+
* Region slug (e.g. `"us-east"`, `"eu-west"`, `"gcp-us-central1"`).
|
|
9
|
+
* @see https://planetscale.com/docs/concepts/regions#available-regions
|
|
10
|
+
*/
|
|
11
|
+
slug: "us-east" | "eu-west" | "gcp-us-central1" | (string & {});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Properties shared between {@link MySQLDatabase} and
|
|
16
|
+
* {@link PostgresDatabase}. Engine-specific props live on the per-engine
|
|
17
|
+
* resource types.
|
|
18
|
+
*/
|
|
19
|
+
export interface BaseDatabaseProps {
|
|
20
|
+
/**
|
|
21
|
+
* Database name. Must be lowercase. If omitted, a unique name will be
|
|
22
|
+
* generated from the stack/stage/logical-id. A change that replaces the
|
|
23
|
+
* database needs a new name, so it fails while an explicit name stays
|
|
24
|
+
* the same.
|
|
25
|
+
*/
|
|
26
|
+
name?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Region where the database will be created. Cannot be changed after
|
|
30
|
+
* creation. If omitted, the organization's default region is used.
|
|
31
|
+
*/
|
|
32
|
+
region?: DatabaseRegion;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Number of replicas for the database. `0` for non-HA, `2+` for HA.
|
|
36
|
+
* Create-only for PostgreSQL databases; MySQL databases reconcile
|
|
37
|
+
* changes in place ({@link MySQLDatabaseProps.replicas}).
|
|
38
|
+
*/
|
|
39
|
+
replicas?: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether deploy requests must be approved by a database administrator
|
|
43
|
+
* other than the request creator.
|
|
44
|
+
*/
|
|
45
|
+
requireApprovalForDeploy?: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether to limit branch creation to the same region as the one
|
|
49
|
+
* selected during database creation.
|
|
50
|
+
*/
|
|
51
|
+
restrictBranchRegion?: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Whether the web console can be used on the production branch.
|
|
55
|
+
*/
|
|
56
|
+
productionBranchWebConsole?: boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether PlanetScale refuses to delete the database. While enabled,
|
|
60
|
+
* destroying or replacing the database fails with a
|
|
61
|
+
* `PlanetscaleDeletionProtected` error; set it to `false` and deploy
|
|
62
|
+
* before deleting. If omitted, Alchemy leaves the database's current
|
|
63
|
+
* setting unchanged.
|
|
64
|
+
*
|
|
65
|
+
* A replacement (e.g. a region change) creates the new database first,
|
|
66
|
+
* so deleting the protected old one fails afterwards. The resource then
|
|
67
|
+
* manages the new database, so turn protection off on the old one in
|
|
68
|
+
* PlanetScale directly and deploy again.
|
|
69
|
+
*/
|
|
70
|
+
deletionProtection?: boolean;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The default branch of the database.
|
|
74
|
+
* @default "main"
|
|
75
|
+
*/
|
|
76
|
+
defaultBranch?: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* SQL migrations to apply against the default branch. Accepts a directory
|
|
80
|
+
* path, a `Drizzle.Schema` resource, or `{ dir, table? }`.
|
|
81
|
+
*
|
|
82
|
+
* Bookkeeping always lives in Alchemy's `__alchemy_migrations` table. A
|
|
83
|
+
* database previously migrated by drizzle-kit or Prisma is adopted by a
|
|
84
|
+
* one-way conversion on first deploy: the old tool's applied history is
|
|
85
|
+
* copied into Alchemy's table and the old table is left frozen. No
|
|
86
|
+
* baselining required.
|
|
87
|
+
*/
|
|
88
|
+
migrations?: MigrationsInput;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Paths to additional `.sql` files to apply after migrations. Each file is
|
|
92
|
+
* hashed; only files whose contents change are re-applied on later deploys.
|
|
93
|
+
*/
|
|
94
|
+
importFiles?: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Output attributes shared by both database engines.
|
|
99
|
+
*/
|
|
100
|
+
export interface BaseDatabaseAttributes {
|
|
101
|
+
/** The unique identifier of the database. */
|
|
102
|
+
id: string;
|
|
103
|
+
/** The name of the database. */
|
|
104
|
+
name: string;
|
|
105
|
+
/** The PlanetScale organization slug that owns the database. */
|
|
106
|
+
organization: string;
|
|
107
|
+
/** The current state of the database. */
|
|
108
|
+
state: string;
|
|
109
|
+
/** The default branch name. */
|
|
110
|
+
defaultBranch: string;
|
|
111
|
+
/** The plan tier (e.g. `"hobby"`, `"scaler"`). */
|
|
112
|
+
plan: string;
|
|
113
|
+
/** Time at which the database was created (ISO 8601). */
|
|
114
|
+
createdAt: string;
|
|
115
|
+
/** Time at which the database was last updated (ISO 8601). */
|
|
116
|
+
updatedAt: string;
|
|
117
|
+
/** HTML URL for accessing the database in the dashboard. */
|
|
118
|
+
htmlUrl: string;
|
|
119
|
+
/** The region of the database as reported by PlanetScale. */
|
|
120
|
+
region: DatabaseRegion;
|
|
121
|
+
/** The cluster size that was actually applied. */
|
|
122
|
+
clusterSize: string;
|
|
123
|
+
/** Directory containing migration files, if configured. */
|
|
124
|
+
migrationsDir: string | undefined;
|
|
125
|
+
/** Table used to track applied migrations, if configured. */
|
|
126
|
+
migrationsTable: string | undefined;
|
|
127
|
+
/** Content hashes for the last applied migration files. */
|
|
128
|
+
migrationsHashes: Record<string, string>;
|
|
129
|
+
/** Content hashes for the last applied import files. */
|
|
130
|
+
importHashes: Record<string, string>;
|
|
131
|
+
/**
|
|
132
|
+
* Whether deploy requests must be approved by a database administrator
|
|
133
|
+
* other than the request creator.
|
|
134
|
+
*/
|
|
135
|
+
requireApprovalForDeploy: boolean;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether to limit branch creation to the same region as the one
|
|
139
|
+
* selected during database creation.
|
|
140
|
+
*/
|
|
141
|
+
restrictBranchRegion: boolean;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Whether the web console can be used on the production branch.
|
|
145
|
+
*/
|
|
146
|
+
productionBranchWebConsole: boolean;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether deletion protection is enabled for the database.
|
|
150
|
+
*/
|
|
151
|
+
deletionProtection: boolean;
|
|
152
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Resource } from "alchemy/Resource";
|
|
2
|
+
import { type BaseBranchAttributes, type BaseBranchProps, makeBranchProvider } from "../Branch.ts";
|
|
3
|
+
import type { Providers } from "../Providers.ts";
|
|
4
|
+
import type { MySQLClusterSize } from "./MySQLClusterSize.ts";
|
|
5
|
+
import type { MySQLDatabase } from "./MySQLDatabase.ts";
|
|
6
|
+
import { runMySQLImports, runMySQLMigrations } from "./MySQLMigrations.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Properties for creating or updating a {@link MySQLBranch}.
|
|
10
|
+
*/
|
|
11
|
+
export interface MySQLBranchProps extends BaseBranchProps {
|
|
12
|
+
/**
|
|
13
|
+
* Whether the branch should be a production branch.
|
|
14
|
+
*/
|
|
15
|
+
isProduction: boolean;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The MySQL database. Either a string database name or a
|
|
19
|
+
* {@link MySQLDatabase} resource.
|
|
20
|
+
*/
|
|
21
|
+
database: string | MySQLDatabase;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* MySQL cluster size for the branch. Required if `backupId` is provided.
|
|
25
|
+
* For non-production branches must be `"PS_DEV"`.
|
|
26
|
+
*/
|
|
27
|
+
clusterSize?: MySQLClusterSize;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parent branch — either a string name or another {@link MySQLBranch}.
|
|
31
|
+
* @default "main"
|
|
32
|
+
*/
|
|
33
|
+
parentBranch?: string | MySQLBranch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Output attributes of a deployed {@link MySQLBranch}.
|
|
38
|
+
*/
|
|
39
|
+
export interface MySQLBranchAttributes extends BaseBranchAttributes {}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A PlanetScale branch of a {@link MySQLDatabase}. For PostgreSQL branches
|
|
43
|
+
* use {@link PostgresBranch} instead.
|
|
44
|
+
*
|
|
45
|
+
* ### Creating a Branch
|
|
46
|
+
* **Example:** Branch from main
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const branch = yield* Planetscale.MySQLBranch("Feature123", {
|
|
49
|
+
* database: "my-db",
|
|
50
|
+
* parentBranch: "main",
|
|
51
|
+
* isProduction: false,
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* **Example:** Branch from a MySQLDatabase resource
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const db = yield* Planetscale.MySQLDatabase("MyDb", { clusterSize: "PS_10" });
|
|
58
|
+
* const branch = yield* Planetscale.MySQLBranch("Feature456", {
|
|
59
|
+
* database: db,
|
|
60
|
+
* parentBranch: "main",
|
|
61
|
+
* isProduction: false,
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* ### Restoring from Backup
|
|
66
|
+
* **Example:** Branch restored from a backup
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const branch = yield* Planetscale.MySQLBranch("Restored", {
|
|
69
|
+
* database: "my-db",
|
|
70
|
+
* parentBranch: "main",
|
|
71
|
+
* isProduction: true,
|
|
72
|
+
* backupId: "backup-123",
|
|
73
|
+
* clusterSize: "PS_10",
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* ### Migrations and seed data
|
|
78
|
+
* **Example:** Apply migrations on a branch
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const branch = yield* Planetscale.MySQLBranch("Feature123", {
|
|
81
|
+
* database: db,
|
|
82
|
+
* parentBranch: "main",
|
|
83
|
+
* isProduction: false,
|
|
84
|
+
* migrationsDir: "./migrations",
|
|
85
|
+
* importFiles: ["./seed.sql"],
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export type MySQLBranch = Resource<
|
|
90
|
+
"Planetscale.MySQLBranch",
|
|
91
|
+
MySQLBranchProps,
|
|
92
|
+
MySQLBranchAttributes,
|
|
93
|
+
never,
|
|
94
|
+
Providers
|
|
95
|
+
>;
|
|
96
|
+
|
|
97
|
+
/** @resource */
|
|
98
|
+
export const MySQLBranch = Resource<MySQLBranch>("Planetscale.MySQLBranch");
|
|
99
|
+
|
|
100
|
+
export const MySQLBranchProvider = () =>
|
|
101
|
+
makeBranchProvider({
|
|
102
|
+
resource: MySQLBranch,
|
|
103
|
+
expectedKind: "mysql",
|
|
104
|
+
engineLabel: "MySQLBranch",
|
|
105
|
+
runners: {
|
|
106
|
+
runMigrations: runMySQLMigrations,
|
|
107
|
+
runImports: runMySQLImports,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as ps from "@distilled.cloud/planetscale";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Schedule from "effect/Schedule";
|
|
4
|
+
import { PlanetscaleConflict, pollUntil } from "../Util.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Available PlanetScale MySQL (Vitess) cluster sizes.
|
|
8
|
+
*
|
|
9
|
+
* `PS_*` sizes are backed by network-attached storage (NAS) and can be
|
|
10
|
+
* specified either as the short size (`"PS_10"`) or the API SKU
|
|
11
|
+
* (`"PS_10_AWS_X86"`)
|
|
12
|
+
*
|
|
13
|
+
* @see https://planetscale.com/docs/concepts/planetscale-skus
|
|
14
|
+
*/
|
|
15
|
+
export type MySQLClusterSize =
|
|
16
|
+
| "PS_DEV"
|
|
17
|
+
| "PS_5"
|
|
18
|
+
| "PS_10"
|
|
19
|
+
| "PS_20"
|
|
20
|
+
| "PS_40"
|
|
21
|
+
| "PS_80"
|
|
22
|
+
| "PS_160"
|
|
23
|
+
| "PS_320"
|
|
24
|
+
| "PS_400"
|
|
25
|
+
| "PS_640"
|
|
26
|
+
| "PS_700"
|
|
27
|
+
| "PS_900"
|
|
28
|
+
| "PS_1280"
|
|
29
|
+
| "PS_1400"
|
|
30
|
+
| "PS_1800"
|
|
31
|
+
| "PS_2100"
|
|
32
|
+
| "PS_2560"
|
|
33
|
+
| "PS_2700"
|
|
34
|
+
| "PS_2800"
|
|
35
|
+
| (string & {});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Polls keyspaces in a branch until the named keyspace reports
|
|
39
|
+
* `resizing === false` (i.e. any in-flight resize has completed).
|
|
40
|
+
*/
|
|
41
|
+
export const waitForKeyspaceReady = Effect.fn(function* (
|
|
42
|
+
organization: string,
|
|
43
|
+
database: string,
|
|
44
|
+
branch: string,
|
|
45
|
+
keyspace: string,
|
|
46
|
+
) {
|
|
47
|
+
yield* pollUntil(
|
|
48
|
+
`keyspace "${keyspace}" not resizing`,
|
|
49
|
+
ps.listKeyspaces({ organization, database, branch }),
|
|
50
|
+
(page) => {
|
|
51
|
+
const ks = page.data.find((x) => x.name === keyspace);
|
|
52
|
+
// If keyspace is missing, treat as ready (caller will re-check)
|
|
53
|
+
return ks ? !ks.resizing : true;
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Observes the default keyspace of a branch. The default keyspace usually
|
|
60
|
+
* shares the database's name, but a renamed database keeps its original
|
|
61
|
+
* keyspace name, so prefer the `default` flag and fall back to name
|
|
62
|
+
* matching.
|
|
63
|
+
*/
|
|
64
|
+
const observeDefaultKeyspace = Effect.fn(function* (
|
|
65
|
+
organization: string,
|
|
66
|
+
database: string,
|
|
67
|
+
branch: string,
|
|
68
|
+
) {
|
|
69
|
+
const keyspaces = yield* ps.listKeyspaces({
|
|
70
|
+
organization,
|
|
71
|
+
database,
|
|
72
|
+
branch,
|
|
73
|
+
page: 1,
|
|
74
|
+
per_page: 100,
|
|
75
|
+
});
|
|
76
|
+
return keyspaces.data.find((x) => x.default) ?? keyspaces.data.find((x) => x.name === database);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Observes the total replica count of a branch's default keyspace, or
|
|
81
|
+
* `undefined` when the branch or keyspace does not exist (yet).
|
|
82
|
+
*/
|
|
83
|
+
export const observeDefaultKeyspaceReplicas = Effect.fn(function* (
|
|
84
|
+
organization: string,
|
|
85
|
+
database: string,
|
|
86
|
+
branch: string,
|
|
87
|
+
) {
|
|
88
|
+
return yield* observeDefaultKeyspace(organization, database, branch).pipe(
|
|
89
|
+
Effect.map((keyspace) => keyspace?.replicas),
|
|
90
|
+
Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Ensures the default keyspace of a MySQL production branch has the
|
|
96
|
+
* expected cluster size and (optionally) total replica count, driving
|
|
97
|
+
* PlanetScale's in-place keyspace resize lifecycle when it doesn't.
|
|
98
|
+
* Cluster sizes can only be configured on production branches. Returns
|
|
99
|
+
* the final observed keyspace.
|
|
100
|
+
*/
|
|
101
|
+
export const ensureMySQLProductionBranchClusterSize = Effect.fn(function* (
|
|
102
|
+
organization: string,
|
|
103
|
+
database: string,
|
|
104
|
+
branch: string,
|
|
105
|
+
expectedClusterSize: MySQLClusterSize,
|
|
106
|
+
expectedReplicas?: number,
|
|
107
|
+
) {
|
|
108
|
+
let keyspace = yield* observeDefaultKeyspace(organization, database, branch);
|
|
109
|
+
if (!keyspace) {
|
|
110
|
+
return yield* Effect.die(`No default keyspace found for branch ${branch}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
yield* waitForKeyspaceReady(organization, database, branch, keyspace.name);
|
|
114
|
+
|
|
115
|
+
if (keyspace.cluster_name !== expectedClusterSize) {
|
|
116
|
+
yield* ps.updateBranchClusterConfig({
|
|
117
|
+
organization,
|
|
118
|
+
database,
|
|
119
|
+
branch,
|
|
120
|
+
cluster_size: expectedClusterSize,
|
|
121
|
+
});
|
|
122
|
+
yield* waitForKeyspaceReady(organization, database, branch, keyspace.name);
|
|
123
|
+
// Re-observe so the replica sync below diffs against the post-resize
|
|
124
|
+
// keyspace state.
|
|
125
|
+
keyspace = (yield* observeDefaultKeyspace(organization, database, branch)) ?? keyspace;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Sync replicas — MySQL databases cannot configure replicas at
|
|
129
|
+
// creation time (the API rejects the `replicas` param for mysql), so
|
|
130
|
+
// the desired total replica count is converged in place via a keyspace
|
|
131
|
+
// resize request.
|
|
132
|
+
if (expectedReplicas !== undefined && keyspace.replicas !== expectedReplicas) {
|
|
133
|
+
// Each cluster size includes a fixed number of replicas (2 for
|
|
134
|
+
// production PS_* sizes); the resize API only accepts the count of
|
|
135
|
+
// additional replicas beyond that.
|
|
136
|
+
const includedReplicas = keyspace.replicas - keyspace.extra_replicas;
|
|
137
|
+
const extraReplicas = expectedReplicas - includedReplicas;
|
|
138
|
+
if (extraReplicas < 0) {
|
|
139
|
+
return yield* new PlanetscaleConflict({
|
|
140
|
+
message:
|
|
141
|
+
`Cannot set replicas to ${expectedReplicas} on keyspace "${keyspace.name}": ` +
|
|
142
|
+
`cluster size ${keyspace.cluster_name} always includes ${includedReplicas} replicas. ` +
|
|
143
|
+
`Set replicas to ${includedReplicas} or more (or omit it).`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const resize = yield* ps
|
|
148
|
+
.updateKeyspaceResizeRequest({
|
|
149
|
+
organization,
|
|
150
|
+
database,
|
|
151
|
+
branch,
|
|
152
|
+
keyspace: keyspace.name,
|
|
153
|
+
extra_replicas: extraReplicas,
|
|
154
|
+
})
|
|
155
|
+
.pipe(
|
|
156
|
+
// A prior resize (e.g. the cluster-size change above) can still
|
|
157
|
+
// be finalizing after the keyspace reports `resizing: false`;
|
|
158
|
+
// PlanetScale rejects new resize requests during that window.
|
|
159
|
+
Effect.retry({
|
|
160
|
+
while: (e): boolean =>
|
|
161
|
+
e._tag === "UnprocessableEntity" && e.message.includes("resize in progress"),
|
|
162
|
+
schedule: Schedule.max([Schedule.spaced("5 seconds"), Schedule.recurs(120)]),
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
yield* pollUntil(
|
|
167
|
+
`keyspace "${keyspace.name}" resize completed`,
|
|
168
|
+
ps.listKeyspaceResizeRequests({
|
|
169
|
+
organization,
|
|
170
|
+
database,
|
|
171
|
+
branch,
|
|
172
|
+
keyspace: keyspace.name,
|
|
173
|
+
}),
|
|
174
|
+
(page) => {
|
|
175
|
+
const request = page.data.find((r) => r.id === resize.id);
|
|
176
|
+
// If the request is missing, treat as settled (caller re-observes).
|
|
177
|
+
return request ? request.state === "completed" : true;
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
keyspace = (yield* observeDefaultKeyspace(organization, database, branch)) ?? keyspace;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return keyspace;
|
|
185
|
+
});
|