alchemy 0.64.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/bin/alchemy.js +7569 -5772
  2. package/bin/commands/init.ts +130 -9
  3. package/bin/constants.ts +4 -0
  4. package/bin/services/cdp-manager/cdp-proxy.ts +69 -0
  5. package/bin/services/cdp-manager/server.ts +173 -0
  6. package/bin/services/execute-alchemy.ts +83 -4
  7. package/bin/types.ts +1 -0
  8. package/lib/cloudflare/bundle/plugin-wasm.d.ts.map +1 -1
  9. package/lib/cloudflare/bundle/plugin-wasm.js +20 -11
  10. package/lib/cloudflare/bundle/plugin-wasm.js.map +1 -1
  11. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  12. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  13. package/lib/cloudflare/hyperdrive.d.ts +7 -2
  14. package/lib/cloudflare/hyperdrive.d.ts.map +1 -1
  15. package/lib/cloudflare/hyperdrive.js +39 -57
  16. package/lib/cloudflare/hyperdrive.js.map +1 -1
  17. package/lib/cloudflare/index.d.ts +1 -0
  18. package/lib/cloudflare/index.d.ts.map +1 -1
  19. package/lib/cloudflare/index.js +1 -0
  20. package/lib/cloudflare/index.js.map +1 -1
  21. package/lib/cloudflare/next.d.ts +13 -0
  22. package/lib/cloudflare/next.d.ts.map +1 -0
  23. package/lib/cloudflare/next.js +91 -0
  24. package/lib/cloudflare/next.js.map +1 -0
  25. package/lib/cloudflare/worker-assets.d.ts +3 -3
  26. package/lib/cloudflare/worker-assets.d.ts.map +1 -1
  27. package/lib/cloudflare/worker-assets.js +22 -47
  28. package/lib/cloudflare/worker-assets.js.map +1 -1
  29. package/lib/cloudflare/wrangler.json.d.ts +2 -2
  30. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  31. package/lib/cloudflare/wrangler.json.js +4 -1
  32. package/lib/cloudflare/wrangler.json.js.map +1 -1
  33. package/lib/planetscale/branch.d.ts.map +1 -1
  34. package/lib/planetscale/branch.js +36 -25
  35. package/lib/planetscale/branch.js.map +1 -1
  36. package/lib/planetscale/database.d.ts +23 -6
  37. package/lib/planetscale/database.d.ts.map +1 -1
  38. package/lib/planetscale/database.js +11 -5
  39. package/lib/planetscale/database.js.map +1 -1
  40. package/lib/planetscale/index.d.ts +1 -0
  41. package/lib/planetscale/index.d.ts.map +1 -1
  42. package/lib/planetscale/index.js +1 -0
  43. package/lib/planetscale/index.js.map +1 -1
  44. package/lib/planetscale/password.d.ts +3 -1
  45. package/lib/planetscale/password.d.ts.map +1 -1
  46. package/lib/planetscale/password.js +3 -1
  47. package/lib/planetscale/password.js.map +1 -1
  48. package/lib/planetscale/role.d.ts +126 -0
  49. package/lib/planetscale/role.d.ts.map +1 -0
  50. package/lib/planetscale/role.js +144 -0
  51. package/lib/planetscale/role.js.map +1 -0
  52. package/lib/planetscale/utils.d.ts +17 -3
  53. package/lib/planetscale/utils.d.ts.map +1 -1
  54. package/lib/planetscale/utils.js +110 -31
  55. package/lib/planetscale/utils.js.map +1 -1
  56. package/lib/secret.d.ts +0 -3
  57. package/lib/secret.d.ts.map +1 -1
  58. package/lib/secret.js +0 -2
  59. package/lib/secret.js.map +1 -1
  60. package/lib/state/cloudflare-state-store.js +3 -3
  61. package/lib/state/cloudflare-state-store.js.map +1 -1
  62. package/lib/test/bun.d.ts +2 -2
  63. package/lib/test/bun.d.ts.map +1 -1
  64. package/lib/test/vitest.d.ts +2 -2
  65. package/lib/test/vitest.d.ts.map +1 -1
  66. package/lib/test/vitest.js +4 -4
  67. package/lib/test/vitest.js.map +1 -1
  68. package/lib/util/content-type.js +2 -2
  69. package/lib/util/content-type.js.map +1 -1
  70. package/lib/util/dedent.d.ts.map +1 -1
  71. package/lib/util/dedent.js +30 -18
  72. package/lib/util/dedent.js.map +1 -1
  73. package/lib/util/memoize.d.ts.map +1 -1
  74. package/lib/util/memoize.js +11 -1
  75. package/lib/util/memoize.js.map +1 -1
  76. package/package.json +5 -1
  77. package/src/cloudflare/bundle/plugin-wasm.ts +34 -14
  78. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  79. package/src/cloudflare/hyperdrive.ts +136 -95
  80. package/src/cloudflare/index.ts +1 -0
  81. package/src/cloudflare/next.ts +123 -0
  82. package/src/cloudflare/worker-assets.ts +43 -105
  83. package/src/cloudflare/wrangler.json.ts +6 -3
  84. package/src/planetscale/branch.ts +52 -27
  85. package/src/planetscale/database.ts +75 -46
  86. package/src/planetscale/index.ts +1 -0
  87. package/src/planetscale/password.ts +3 -1
  88. package/src/planetscale/role.ts +278 -0
  89. package/src/planetscale/utils.ts +172 -30
  90. package/src/secret.ts +0 -4
  91. package/src/state/cloudflare-state-store.ts +4 -4
  92. package/src/test/bun.ts +2 -2
  93. package/src/test/vitest.ts +6 -6
  94. package/src/util/content-type.ts +2 -2
  95. package/src/util/dedent.ts +33 -21
  96. package/src/util/memoize.ts +13 -1
  97. package/templates/nextjs/README.md +85 -0
  98. package/templates/nextjs/_env +1 -0
  99. package/templates/nextjs/_env.example +1 -0
  100. package/templates/nextjs/_gitignore +46 -0
  101. package/templates/nextjs/alchemy.run.ts +16 -0
  102. package/templates/nextjs/eslint.config.mjs +16 -0
  103. package/templates/nextjs/next-env.d.ts +5 -0
  104. package/templates/nextjs/next.config.ts +10 -0
  105. package/templates/nextjs/open-next.config.ts +5 -0
  106. package/templates/nextjs/package.json +31 -0
  107. package/templates/nextjs/src/app/api/hello/route.ts +19 -0
  108. package/templates/nextjs/src/app/favicon.ico +0 -0
  109. package/templates/nextjs/src/app/globals.css +72 -0
  110. package/templates/nextjs/src/app/layout.tsx +19 -0
  111. package/templates/nextjs/src/app/page.tsx +55 -0
  112. package/templates/nextjs/tsconfig.json +33 -0
  113. package/templates/nextjs/types/env.d.ts +14 -0
@@ -2,10 +2,10 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { AsyncQueue } from "../util/async-queue.ts";
5
- import { getContentType } from "../util/content-type.ts";
6
- import { CloudflareApiError } from "./api-error.ts";
5
+ import { withExponentialBackoff } from "../util/retry.ts";
6
+ import { extractCloudflareResult } from "./api-response.ts";
7
7
  import type { CloudflareApi } from "./api.ts";
8
- import type { Assets } from "./assets.ts";
8
+ import type { AssetFile, Assets } from "./assets.ts";
9
9
  import type { AssetsConfig, WorkerProps } from "./worker.ts";
10
10
 
11
11
  export interface AssetUploadResult {
@@ -21,32 +21,6 @@ interface FileMetadata {
21
21
  size: number;
22
22
  }
23
23
 
24
- /**
25
- * Response from the assets upload session API
26
- */
27
- interface UploadSessionResponse {
28
- result: {
29
- jwt: string;
30
- buckets: string[][];
31
- };
32
- success: boolean;
33
- errors: any[];
34
- messages: any[];
35
- }
36
-
37
- /**
38
- * Response from the file upload API
39
- */
40
- interface UploadResponse {
41
- result: {
42
- jwt: string;
43
- buckets?: string[][];
44
- };
45
- success: boolean;
46
- errors: any[];
47
- messages: any[];
48
- }
49
-
50
24
  /**
51
25
  * Uploads assets to Cloudflare and returns a completion token
52
26
  *
@@ -73,64 +47,41 @@ export async function uploadAssets(
73
47
  // Process the assets configuration once at the beginning
74
48
  const processedConfig = createAssetConfig(assetConfig);
75
49
 
76
- const { manifest, filePathsByHash } = await prepareAssetManifest(assets);
50
+ const { manifest, filesByHash } = await prepareAssetManifest(assets);
77
51
 
78
52
  // Start the upload session
79
- const uploadSessionResponse = await api.post(
80
- namespace
81
- ? `/accounts/${api.accountId}/workers/dispatch/namespaces/${namespace}/scripts/${workerName}/assets-upload-session`
82
- : `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`,
83
- JSON.stringify({ manifest }),
84
- {
85
- headers: { "Content-Type": "application/json" },
86
- },
53
+ const uploadSession = await extractCloudflareResult<{
54
+ jwt: string;
55
+ buckets: string[][];
56
+ }>(
57
+ `create assets upload session for worker "${workerName}"`,
58
+ api.post(
59
+ namespace
60
+ ? `/accounts/${api.accountId}/workers/dispatch/namespaces/${namespace}/scripts/${workerName}/assets-upload-session`
61
+ : `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`,
62
+ { manifest },
63
+ ),
87
64
  );
88
-
89
- if (!uploadSessionResponse.ok) {
90
- throw new Error(
91
- `Failed to start assets upload session: ${uploadSessionResponse.status} ${uploadSessionResponse.statusText} - ${await uploadSessionResponse.text()}`,
92
- );
93
- }
94
-
95
- const sessionData =
96
- (await uploadSessionResponse.json()) as UploadSessionResponse;
97
-
98
- if (!sessionData?.success) {
99
- if (sessionData?.errors) {
100
- throw new CloudflareApiError(
101
- `Failed to start assets upload session:\n${sessionData.errors
102
- .map((error) => `- ${error.code}: ${error.message}`)
103
- .join("\n")}`,
104
- uploadSessionResponse,
105
- );
106
- }
107
- throw new CloudflareApiError(
108
- `Failed to start assets upload session: ${uploadSessionResponse.statusText}`,
109
- uploadSessionResponse,
110
- );
111
- }
112
-
113
65
  // If there are no buckets, assets are already uploaded or empty
114
- if (!sessionData.result.buckets || sessionData.result.buckets.length === 0) {
66
+ if (!uploadSession.buckets || uploadSession.buckets.length === 0) {
115
67
  return {
116
- completionToken: sessionData.result.jwt,
68
+ completionToken: uploadSession.jwt,
117
69
  assetConfig: processedConfig,
118
70
  };
119
71
  }
120
72
 
121
73
  // Upload the files in batches as specified by the API
122
- let completionToken = sessionData.result.jwt;
123
- const buckets = sessionData.result.buckets;
74
+ let completionToken = uploadSession.jwt;
75
+ const buckets = uploadSession.buckets;
124
76
 
125
77
  const queue = new AsyncQueue(3);
126
78
  await Promise.all(
127
79
  buckets.map((bucket) =>
128
80
  queue.add(async () => {
129
- const jwt = await uploadBucket(
130
- api,
131
- sessionData.result.jwt,
132
- bucket,
133
- filePathsByHash,
81
+ const jwt = await withExponentialBackoff(
82
+ async () =>
83
+ await uploadBucket(api, uploadSession.jwt, bucket, filesByHash),
84
+ () => true,
134
85
  );
135
86
  if (jwt) {
136
87
  completionToken = jwt;
@@ -183,68 +134,55 @@ export function createAssetConfig(config?: AssetsConfig): AssetsConfig {
183
134
  * Prepares the asset manifest for the assets upload session
184
135
  *
185
136
  * @param assets Assets resource containing files to upload
186
- * @returns Asset manifest and file paths by hash
137
+ * @returns Asset manifest and files by hash
187
138
  */
188
139
  export async function prepareAssetManifest(assets: Assets) {
189
140
  const manifest: Record<string, FileMetadata> = {};
190
- const filePathsByHash = new Map<string, string>();
141
+ const filesByHash = new Map<string, AssetFile>();
191
142
  await Promise.all(
192
143
  assets.files.map(async (file) => {
193
144
  const { hash, size } = await calculateFileMetadata(file.filePath);
194
145
  const key = file.path.startsWith("/") ? file.path : `/${file.path}`;
195
146
  manifest[key] = { hash, size };
196
- filePathsByHash.set(hash, file.filePath);
147
+ filesByHash.set(hash, file);
197
148
  }),
198
149
  );
199
- return { manifest, filePathsByHash };
150
+ return { manifest, filesByHash };
200
151
  }
201
152
 
202
153
  async function uploadBucket(
203
154
  api: CloudflareApi,
204
155
  jwt: string,
205
156
  bucket: string[],
206
- files: Map<string, string>,
157
+ filesByHash: Map<string, AssetFile>,
207
158
  ) {
208
159
  const formData = new FormData();
209
160
  await Promise.all(
210
161
  bucket.map(async (fileHash) => {
211
- const filePath = files.get(fileHash);
212
- if (!filePath) {
162
+ const file = filesByHash.get(fileHash);
163
+ if (!file) {
213
164
  throw new Error(`Could not find file with hash ${fileHash}`);
214
165
  }
215
- const fileContent = await fs.readFile(filePath);
166
+ const fileContent = await fs.readFile(file.filePath);
216
167
  const blob = new Blob([fileContent.toString("base64")], {
217
- type: getContentType(filePath) ?? "application/null",
168
+ type: file.contentType,
218
169
  });
219
170
  formData.append(fileHash, blob, fileHash);
220
171
  }),
221
172
  );
222
- const uploadResponse = await api.post(
223
- `/accounts/${api.accountId}/workers/assets/upload?base64=true`,
224
- formData,
225
- {
226
- headers: {
227
- Authorization: `Bearer ${jwt}`,
228
- "Content-Type": "multipart/form-data",
173
+ const uploadResult = await extractCloudflareResult<{ jwt?: string }>(
174
+ "upload asset files",
175
+ api.post(
176
+ `/accounts/${api.accountId}/workers/assets/upload?base64=true`,
177
+ formData,
178
+ {
179
+ headers: {
180
+ Authorization: `Bearer ${jwt}`,
181
+ },
229
182
  },
230
- },
183
+ ),
231
184
  );
232
-
233
- if (!uploadResponse.ok) {
234
- throw new Error(
235
- `Failed to upload asset files: ${uploadResponse.status} ${uploadResponse.statusText}`,
236
- );
237
- }
238
-
239
- const uploadData = (await uploadResponse.json()) as UploadResponse;
240
- if (!uploadData.success) {
241
- const error = uploadData.errors[0];
242
- const message = error.message;
243
- const code = error.code;
244
- throw new Error(`Failed to upload asset files: ${message} (Code: ${code})`);
245
- }
246
-
247
- return uploadData.result.jwt;
185
+ return uploadResult.jwt;
248
186
  }
249
187
 
250
188
  /**
@@ -353,8 +353,8 @@ export interface WranglerJsonSpec {
353
353
  * Queue bindings
354
354
  */
355
355
  queues?: {
356
- producers: { queue: string; binding: string }[];
357
- consumers: {
356
+ producers?: { queue: string; binding: string }[];
357
+ consumers?: {
358
358
  queue: string;
359
359
  max_batch_size?: number;
360
360
  max_concurrency?: number;
@@ -845,7 +845,10 @@ function processBindings(
845
845
  }
846
846
 
847
847
  if (queues.consumers.length > 0) {
848
- spec.queues = queues;
848
+ (spec.queues ??= {}).consumers = queues.consumers;
849
+ }
850
+ if (queues.producers.length > 0) {
851
+ (spec.queues ??= {}).producers = queues.producers;
849
852
  }
850
853
 
851
854
  if (vectorizeIndexes.length > 0) {
@@ -4,7 +4,8 @@ import type { PlanetScaleProps } from "./api/client.gen.ts";
4
4
  import { PlanetScaleClient } from "./api/client.gen.ts";
5
5
  import {
6
6
  ensureProductionBranchClusterSize,
7
- waitForDatabaseReady,
7
+ sanitizeClusterSize,
8
+ waitForBranchReady,
8
9
  type PlanetScaleClusterSize,
9
10
  } from "./utils.ts";
10
11
 
@@ -159,6 +160,11 @@ export const Branch = Resource(
159
160
 
160
161
  const branchName =
161
162
  props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
163
+ const parentBranchName = !props.parentBranch
164
+ ? "main"
165
+ : typeof props.parentBranch === "string"
166
+ ? props.parentBranch
167
+ : props.parentBranch.name;
162
168
 
163
169
  if (this.phase === "update" && this.output.name !== branchName) {
164
170
  // TODO(sam): maybe we don't need to replace? just branch again? or rename?
@@ -188,14 +194,8 @@ export const Branch = Resource(
188
194
  return this.destroy();
189
195
  }
190
196
 
191
- const parentBranchName = !props.parentBranch
192
- ? "main"
193
- : typeof props.parentBranch === "string"
194
- ? props.parentBranch
195
- : props.parentBranch.name;
196
-
197
197
  if (typeof props.parentBranch !== "string" && props.parentBranch) {
198
- await waitForDatabaseReady(
198
+ await waitForBranchReady(
199
199
  api,
200
200
  props.organizationId,
201
201
  props.databaseName,
@@ -262,17 +262,28 @@ export const Branch = Resource(
262
262
  });
263
263
  }
264
264
 
265
- if (props.clusterSize && data.cluster_name !== props.clusterSize) {
266
- await api.organizations.databases.branches.cluster.patch({
267
- path: {
268
- organization: props.organizationId,
269
- database: props.databaseName,
270
- name: branchName,
271
- },
272
- body: {
273
- cluster_size: props.clusterSize,
274
- },
275
- });
265
+ const clusterSize = props.clusterSize
266
+ ? sanitizeClusterSize({
267
+ size: props.clusterSize,
268
+ kind: data.kind,
269
+ arch: data.cluster_architecture === "aarch64" ? "arm" : "x86",
270
+ region: data.region.slug,
271
+ })
272
+ : undefined;
273
+ if (clusterSize && data.cluster_name !== clusterSize) {
274
+ if (!props.isProduction) {
275
+ throw new Error(
276
+ `Cannot change cluster size of non-production branch ${branchName}`,
277
+ );
278
+ }
279
+ await ensureProductionBranchClusterSize(
280
+ api,
281
+ props.organizationId,
282
+ props.databaseName,
283
+ branchName,
284
+ data.kind,
285
+ clusterSize,
286
+ );
276
287
  }
277
288
 
278
289
  return this({
@@ -284,7 +295,21 @@ export const Branch = Resource(
284
295
  htmlUrl: data.html_url,
285
296
  });
286
297
  }
287
- await waitForDatabaseReady(api, props.organizationId, props.databaseName);
298
+ let clusterSize: string | undefined;
299
+ const parent = await waitForBranchReady(
300
+ api,
301
+ props.organizationId,
302
+ props.databaseName,
303
+ parentBranchName,
304
+ );
305
+ if (props.clusterSize) {
306
+ clusterSize = sanitizeClusterSize({
307
+ size: props.clusterSize,
308
+ kind: parent.kind,
309
+ arch: parent.cluster_architecture === "aarch64" ? "arm" : "x86",
310
+ region: parent.region.slug,
311
+ });
312
+ }
288
313
 
289
314
  // Branch doesn't exist, create it
290
315
  const data = await api.organizations.databases.branches.post({
@@ -297,14 +322,15 @@ export const Branch = Resource(
297
322
  parent_branch: parentBranchName,
298
323
  backup_id: props.backupId,
299
324
  seed_data: props.seedData,
300
- cluster_size: props.clusterSize,
325
+ // This is ignored unless props.backupId is provided
326
+ cluster_size: clusterSize,
301
327
  },
302
328
  });
303
329
 
304
330
  // Handle safe migrations if specified
305
331
  if (props.safeMigrations !== undefined) {
306
- // We can't change the migrations mode if the database is not ready
307
- await waitForDatabaseReady(
332
+ // We can't change the migrations mode if the branch is not ready
333
+ await waitForBranchReady(
308
334
  api,
309
335
  props.organizationId,
310
336
  props.databaseName,
@@ -322,15 +348,14 @@ export const Branch = Resource(
322
348
  }
323
349
 
324
350
  // Handle cluster size update if specified
325
- if (props.clusterSize) {
326
- // Wait for database to be ready before modifying cluster size
351
+ if (clusterSize) {
327
352
  await ensureProductionBranchClusterSize(
328
353
  api,
329
354
  props.organizationId,
330
355
  props.databaseName,
331
356
  branchName,
332
- props.clusterSize,
333
- true,
357
+ data.kind,
358
+ clusterSize,
334
359
  );
335
360
  }
336
361
 
@@ -5,13 +5,11 @@ import { PlanetScaleClient } from "./api/client.gen.ts";
5
5
  import {
6
6
  ensureProductionBranchClusterSize,
7
7
  type PlanetScaleClusterSize,
8
+ sanitizeClusterSize,
8
9
  waitForDatabaseReady,
9
10
  } from "./utils.ts";
10
11
 
11
- /**
12
- * Properties for creating or updating a PlanetScale Database
13
- */
14
- export interface DatabaseProps extends PlanetScaleProps {
12
+ interface BaseDatabaseProps extends PlanetScaleProps {
15
13
  /**
16
14
  * The name of the database
17
15
  *
@@ -88,54 +86,79 @@ export interface DatabaseProps extends PlanetScaleProps {
88
86
  * The database cluster size (required)
89
87
  */
90
88
  clusterSize: PlanetScaleClusterSize;
91
- }
92
89
 
93
- /**
94
- * Represents a PlanetScale Database
95
- */
96
- export interface Database
97
- extends Resource<"planetscale::Database">,
98
- DatabaseProps {
99
90
  /**
100
- * The unique identifier of the database
91
+ * The engine kind for the database
92
+ * @default "mysql"
101
93
  */
102
- id: string;
94
+ kind?: "mysql" | "postgresql";
103
95
 
104
96
  /**
105
- * The name of the database
97
+ * The CPU architecture for the database. Only available for PostgreSQL databases.
106
98
  */
107
- name: string;
99
+ arch?: "x86" | "arm";
100
+ }
108
101
 
109
- /**
110
- * The current state of the database
111
- */
112
- state: string;
102
+ /**
103
+ * Properties for creating or updating a PlanetScale Database
104
+ */
105
+ export type DatabaseProps = BaseDatabaseProps &
106
+ (
107
+ | {
108
+ kind?: "mysql";
109
+ arch?: undefined;
110
+ }
111
+ | {
112
+ kind: "postgresql";
113
+ arch?: "x86" | "arm";
114
+ }
115
+ );
113
116
 
114
- /**
115
- * The default branch name
116
- */
117
- defaultBranch: string;
117
+ /**
118
+ * Represents a PlanetScale Database
119
+ */
120
+ export type Database = Resource<"planetscale::Database"> &
121
+ DatabaseProps & {
122
+ /**
123
+ * The unique identifier of the database
124
+ */
125
+ id: string;
118
126
 
119
- /**
120
- * The plan type
121
- */
122
- plan: string;
127
+ /**
128
+ * The name of the database
129
+ */
130
+ name: string;
123
131
 
124
- /**
125
- * Time at which the database was created
126
- */
127
- createdAt: string;
132
+ /**
133
+ * The current state of the database
134
+ */
135
+ state: string;
128
136
 
129
- /**
130
- * Time at which the database was last updated
131
- */
132
- updatedAt: string;
137
+ /**
138
+ * The default branch name
139
+ */
140
+ defaultBranch: string;
133
141
 
134
- /**
135
- * HTML URL to access the database
136
- */
137
- htmlUrl: string;
138
- }
142
+ /**
143
+ * The plan type
144
+ */
145
+ plan: string;
146
+
147
+ /**
148
+ * Time at which the database was created
149
+ */
150
+ createdAt: string;
151
+
152
+ /**
153
+ * Time at which the database was last updated
154
+ */
155
+ updatedAt: string;
156
+
157
+ /**
158
+ * HTML URL to access the database
159
+ */
160
+ htmlUrl: string;
161
+ };
139
162
 
140
163
  /**
141
164
  * Create, manage and delete PlanetScale databases
@@ -182,6 +205,12 @@ export const Database = Resource(
182
205
 
183
206
  const databaseName =
184
207
  props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
208
+ const clusterSize = sanitizeClusterSize({
209
+ size: props.clusterSize,
210
+ kind: props.kind,
211
+ arch: props.arch,
212
+ region: props.region?.slug,
213
+ });
185
214
 
186
215
  if (this.phase === "update" && this.output.name !== databaseName) {
187
216
  await api.organizations.databases.patch({
@@ -278,8 +307,8 @@ export const Database = Resource(
278
307
  props.organizationId,
279
308
  databaseName,
280
309
  props.defaultBranch || "main",
281
- props.clusterSize,
282
- updateResponse.ready,
310
+ updateResponse.kind,
311
+ clusterSize,
283
312
  );
284
313
 
285
314
  return this({
@@ -307,8 +336,8 @@ export const Database = Resource(
307
336
  body: {
308
337
  name: databaseName,
309
338
  region: props.region?.slug,
310
- kind: "mysql",
311
- cluster_size: props.clusterSize,
339
+ kind: props.kind,
340
+ cluster_size: clusterSize,
312
341
  },
313
342
  });
314
343
 
@@ -362,8 +391,8 @@ export const Database = Resource(
362
391
  props.organizationId,
363
392
  databaseName,
364
393
  props.defaultBranch || "main",
365
- props.clusterSize,
366
- false,
394
+ data.kind,
395
+ clusterSize,
367
396
  );
368
397
 
369
398
  // Update database to use new branch as default
@@ -2,4 +2,5 @@ export * from "./api/client.gen.ts";
2
2
  export * from "./branch.ts";
3
3
  export * from "./database.ts";
4
4
  export * from "./password.ts";
5
+ export * from "./role.ts";
5
6
  export * from "./utils.ts";
@@ -102,7 +102,9 @@ export interface Password
102
102
  }
103
103
 
104
104
  /**
105
- * Create and manage database passwords for PlanetScale branches. Database passwords provide secure access to your database with specific roles and permissions.
105
+ * Create and manage database passwords for PlanetScale MySQL branches. Database passwords provide secure access to your database with specific roles and permissions.
106
+ *
107
+ * For Postgres, use [Roles](./role.ts) instead.
106
108
  *
107
109
  * @example
108
110
  * ## Basic Reader Password