alchemy 0.86.0 → 0.89.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/bin/alchemy.js +483 -541
- package/lib/cloudflare/compatibility-date.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.d.ts.map +1 -1
- package/lib/cloudflare/dns-records.d.ts +5 -1
- package/lib/cloudflare/dns-records.d.ts.map +1 -1
- package/lib/cloudflare/dns-records.js +54 -31
- package/lib/cloudflare/dns-records.js.map +1 -1
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/r2-bucket-notification.d.ts +312 -0
- package/lib/cloudflare/r2-bucket-notification.d.ts.map +1 -0
- package/lib/cloudflare/r2-bucket-notification.js +387 -0
- package/lib/cloudflare/r2-bucket-notification.js.map +1 -0
- package/lib/cloudflare/workflow.d.ts +17 -0
- package/lib/cloudflare/workflow.d.ts.map +1 -1
- package/lib/cloudflare/workflow.js +1 -0
- package/lib/cloudflare/workflow.js.map +1 -1
- package/lib/cloudflare/wrangler.json.js +1 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/docker/api.d.ts +51 -0
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +30 -0
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/container.d.ts +7 -1
- package/lib/docker/container.d.ts.map +1 -1
- package/lib/docker/container.js +51 -3
- package/lib/docker/container.js.map +1 -1
- package/lib/docker/network.d.ts.map +1 -1
- package/lib/docker/network.js +24 -7
- package/lib/docker/network.js.map +1 -1
- package/lib/planetscale/api.d.ts +1 -0
- package/lib/planetscale/api.d.ts.map +1 -1
- package/lib/planetscale/api.js +1 -1
- package/lib/planetscale/api.js.map +1 -1
- package/lib/planetscale/branch.d.ts +28 -0
- package/lib/planetscale/branch.d.ts.map +1 -1
- package/lib/planetscale/branch.js +12 -0
- package/lib/planetscale/branch.js.map +1 -1
- package/lib/planetscale/database-extensions.d.ts +515 -0
- package/lib/planetscale/database-extensions.d.ts.map +1 -0
- package/lib/planetscale/database-extensions.js +389 -0
- package/lib/planetscale/database-extensions.js.map +1 -0
- package/lib/planetscale/database.d.ts +40 -2
- package/lib/planetscale/database.d.ts.map +1 -1
- package/lib/planetscale/database.js +76 -1
- package/lib/planetscale/database.js.map +1 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +1 -2
- package/lib/scope.js.map +1 -1
- package/lib/util/telemetry.d.ts +1 -6
- package/lib/util/telemetry.d.ts.map +1 -1
- package/lib/util/telemetry.js.map +1 -1
- package/package.json +4 -4
- package/src/cloudflare/dns-records.ts +71 -51
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/r2-bucket-notification.ts +809 -0
- package/src/cloudflare/workflow.ts +18 -0
- package/src/cloudflare/wrangler.json.ts +1 -0
- package/src/docker/api.ts +73 -0
- package/src/docker/container.ts +78 -4
- package/src/docker/network.ts +27 -7
- package/src/planetscale/api.ts +1 -1
- package/src/planetscale/branch.ts +45 -0
- package/src/planetscale/database-extensions.ts +947 -0
- package/src/planetscale/database.ts +141 -2
- package/src/scope.ts +1 -2
- package/src/util/telemetry.ts +1 -20
- package/workers/tunnel-proxy.js +1 -1
- package/lib/state/instrumented-state-store.d.ts +0 -19
- package/lib/state/instrumented-state-store.d.ts.map +0 -1
- package/lib/state/instrumented-state-store.js +0 -68
- package/lib/state/instrumented-state-store.js.map +0 -1
- package/src/state/instrumented-state-store.ts +0 -108
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { poll } from "../util/poll.ts";
|
|
2
|
+
import { extractToken, type PlanetScaleProps } from "./api.ts";
|
|
3
|
+
import type { Branch } from "./branch.ts";
|
|
4
|
+
import type { Database } from "./database.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base libraries that PlanetScale always includes in shared_preload_libraries.
|
|
8
|
+
* These are managed internally and should not be toggled by users.
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
const BASE_LIBRARIES = ["pg_pscale_utils", "pg_readonly", "pgextwlist"];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for the pg_cron extension.
|
|
15
|
+
* Schedules and runs PostgreSQL commands inside the database.
|
|
16
|
+
*/
|
|
17
|
+
export interface PgCronConfig {
|
|
18
|
+
/**
|
|
19
|
+
* The database in which pg_cron metadata is kept.
|
|
20
|
+
* @default "postgres"
|
|
21
|
+
*/
|
|
22
|
+
databaseName?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Whether to launch active jobs when pg_cron starts.
|
|
25
|
+
* @default "on"
|
|
26
|
+
*/
|
|
27
|
+
launchActiveJobs?: "on" | "off";
|
|
28
|
+
/**
|
|
29
|
+
* Minimum message level for pg_cron log messages.
|
|
30
|
+
* @default "warning"
|
|
31
|
+
*/
|
|
32
|
+
logMinMessages?: "error" | "warning" | "notice" | "info" | "log" | "debug";
|
|
33
|
+
/**
|
|
34
|
+
* Whether to log each job run into the job_run_details table.
|
|
35
|
+
* @default "on"
|
|
36
|
+
*/
|
|
37
|
+
logRun?: "on" | "off";
|
|
38
|
+
/**
|
|
39
|
+
* Whether to log the SQL statement of each job.
|
|
40
|
+
* @default "on"
|
|
41
|
+
*/
|
|
42
|
+
logStatement?: "on" | "off";
|
|
43
|
+
/**
|
|
44
|
+
* Maximum number of concurrently running jobs.
|
|
45
|
+
* @default 1
|
|
46
|
+
*/
|
|
47
|
+
maxRunningJobs?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Configuration for the pg_duckdb extension.
|
|
52
|
+
* Embeds DuckDB inside PostgreSQL for analytical queries.
|
|
53
|
+
*/
|
|
54
|
+
export interface PgDuckdbConfig {
|
|
55
|
+
/**
|
|
56
|
+
* The PostgreSQL role used by DuckDB.
|
|
57
|
+
* @default "pscale_superuser"
|
|
58
|
+
*/
|
|
59
|
+
postgresRole?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Memory limit for DuckDB. 0 means unlimited.
|
|
62
|
+
* @default "0"
|
|
63
|
+
*/
|
|
64
|
+
memoryLimit?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Configuration for the pg_hint_plan extension.
|
|
69
|
+
* Controls query execution plans using hints in SQL comments.
|
|
70
|
+
*/
|
|
71
|
+
export interface PgHintPlanConfig {
|
|
72
|
+
/**
|
|
73
|
+
* Whether to enable hint processing.
|
|
74
|
+
* @default "on"
|
|
75
|
+
*/
|
|
76
|
+
enableHint?: "on" | "off";
|
|
77
|
+
/**
|
|
78
|
+
* Whether to enable the hint table feature.
|
|
79
|
+
* @default "off"
|
|
80
|
+
*/
|
|
81
|
+
enableHintTable?: "on" | "off";
|
|
82
|
+
/**
|
|
83
|
+
* Message level for hint parsing messages.
|
|
84
|
+
* @default "info"
|
|
85
|
+
*/
|
|
86
|
+
parseMessages?: "error" | "warning" | "notice" | "info" | "log" | "debug";
|
|
87
|
+
/**
|
|
88
|
+
* Controls debug output of query plans.
|
|
89
|
+
* @default "off"
|
|
90
|
+
*/
|
|
91
|
+
debugPrint?: "off" | "on" | "detailed" | "verbose";
|
|
92
|
+
/**
|
|
93
|
+
* Message level for debug output.
|
|
94
|
+
* @default "info"
|
|
95
|
+
*/
|
|
96
|
+
messageLevel?: "error" | "warning" | "notice" | "info" | "log" | "debug";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Configuration for the pg_partman background worker extension.
|
|
101
|
+
* Automates partition management for time-based and serial-based partitioning.
|
|
102
|
+
*/
|
|
103
|
+
export interface PgPartmanBgwConfig {
|
|
104
|
+
/**
|
|
105
|
+
* How often (in seconds) the background worker runs maintenance.
|
|
106
|
+
* @default 3600
|
|
107
|
+
*/
|
|
108
|
+
interval?: number;
|
|
109
|
+
/**
|
|
110
|
+
* The database to connect to for maintenance.
|
|
111
|
+
* @default "postgres"
|
|
112
|
+
*/
|
|
113
|
+
dbname?: string;
|
|
114
|
+
/**
|
|
115
|
+
* The role to use when connecting.
|
|
116
|
+
* @default "postgres"
|
|
117
|
+
*/
|
|
118
|
+
role?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Whether to run ANALYZE on partitions after maintenance.
|
|
121
|
+
* @default "off"
|
|
122
|
+
*/
|
|
123
|
+
analyze?: "on" | "off";
|
|
124
|
+
/**
|
|
125
|
+
* Whether to use pg_jobmon for logging.
|
|
126
|
+
* @default "on"
|
|
127
|
+
*/
|
|
128
|
+
jobmon?: "on" | "off";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Configuration for the pg_squeeze extension.
|
|
133
|
+
* Removes dead tuples from tables with minimal locking.
|
|
134
|
+
*/
|
|
135
|
+
export interface PgSqueezeConfig {
|
|
136
|
+
/**
|
|
137
|
+
* Maximum time (in milliseconds) to hold an exclusive lock during processing. 0 means no limit.
|
|
138
|
+
* @default 0
|
|
139
|
+
*/
|
|
140
|
+
maxXlockTime?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Databases in which the squeeze worker auto-starts, comma-separated.
|
|
143
|
+
* @default "postgres"
|
|
144
|
+
*/
|
|
145
|
+
workerAutostart?: string;
|
|
146
|
+
/**
|
|
147
|
+
* Number of squeeze workers per database.
|
|
148
|
+
* @default 1
|
|
149
|
+
*/
|
|
150
|
+
workersPerDatabase?: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Configuration for the pg_stat_statements extension.
|
|
155
|
+
* Tracks planning and execution statistics of all SQL statements.
|
|
156
|
+
*/
|
|
157
|
+
export interface PgStatStatementsConfig {
|
|
158
|
+
/**
|
|
159
|
+
* Maximum number of statements tracked.
|
|
160
|
+
* @default 5000
|
|
161
|
+
*/
|
|
162
|
+
max?: number;
|
|
163
|
+
/**
|
|
164
|
+
* Which statements to track.
|
|
165
|
+
* @default "top"
|
|
166
|
+
*/
|
|
167
|
+
track?: "top" | "all";
|
|
168
|
+
/**
|
|
169
|
+
* Whether to track utility commands.
|
|
170
|
+
* @default "on"
|
|
171
|
+
*/
|
|
172
|
+
trackUtility?: "on" | "off";
|
|
173
|
+
/**
|
|
174
|
+
* Whether to track planning statistics.
|
|
175
|
+
* @default "off"
|
|
176
|
+
*/
|
|
177
|
+
trackPlanning?: "on" | "off";
|
|
178
|
+
/**
|
|
179
|
+
* Whether to save statistics across server restarts.
|
|
180
|
+
* @default "on"
|
|
181
|
+
*/
|
|
182
|
+
save?: "on" | "off";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Configuration for the timescaledb extension.
|
|
187
|
+
* Provides time-series data support for PostgreSQL.
|
|
188
|
+
*/
|
|
189
|
+
export interface TimescaledbConfig {
|
|
190
|
+
/**
|
|
191
|
+
* Log level for the background worker.
|
|
192
|
+
* @default "warning"
|
|
193
|
+
*/
|
|
194
|
+
bgwLogLevel?: "error" | "warning" | "notice" | "info" | "log" | "debug";
|
|
195
|
+
/**
|
|
196
|
+
* Enable chunk append optimization.
|
|
197
|
+
* @default "on"
|
|
198
|
+
*/
|
|
199
|
+
enableChunkAppend?: "on" | "off";
|
|
200
|
+
/**
|
|
201
|
+
* Enable chunk skipping optimization.
|
|
202
|
+
* @default "off"
|
|
203
|
+
*/
|
|
204
|
+
enableChunkSkipping?: "on" | "off";
|
|
205
|
+
/**
|
|
206
|
+
* Enable constraint-aware append optimization.
|
|
207
|
+
* @default "on"
|
|
208
|
+
*/
|
|
209
|
+
enableConstraintAwareAppend?: "on" | "off";
|
|
210
|
+
/**
|
|
211
|
+
* Enable constraint exclusion optimization.
|
|
212
|
+
* @default "on"
|
|
213
|
+
*/
|
|
214
|
+
enableConstraintExclusion?: "on" | "off";
|
|
215
|
+
/**
|
|
216
|
+
* Enable custom hash aggregate optimization.
|
|
217
|
+
* @default "off"
|
|
218
|
+
*/
|
|
219
|
+
enableCustomHashagg?: "on" | "off";
|
|
220
|
+
/**
|
|
221
|
+
* Enable deprecation warnings.
|
|
222
|
+
* @default "on"
|
|
223
|
+
*/
|
|
224
|
+
enableDeprecationWarnings?: "on" | "off";
|
|
225
|
+
/**
|
|
226
|
+
* Enable event triggers.
|
|
227
|
+
* @default "off"
|
|
228
|
+
*/
|
|
229
|
+
enableEventTriggers?: "on" | "off";
|
|
230
|
+
/**
|
|
231
|
+
* Enable foreign key propagation.
|
|
232
|
+
* @default "on"
|
|
233
|
+
*/
|
|
234
|
+
enableForeignKeyPropagation?: "on" | "off";
|
|
235
|
+
/**
|
|
236
|
+
* Enable logging of job execution.
|
|
237
|
+
* @default "off"
|
|
238
|
+
*/
|
|
239
|
+
enableJobExecutionLogging?: "on" | "off";
|
|
240
|
+
/**
|
|
241
|
+
* Enable now() constification.
|
|
242
|
+
* @default "on"
|
|
243
|
+
*/
|
|
244
|
+
enableNowConstify?: "on" | "off";
|
|
245
|
+
/**
|
|
246
|
+
* Enable query optimizations.
|
|
247
|
+
* @default "on"
|
|
248
|
+
*/
|
|
249
|
+
enableOptimizations?: "on" | "off";
|
|
250
|
+
/**
|
|
251
|
+
* Enable ordered append optimization.
|
|
252
|
+
* @default "on"
|
|
253
|
+
*/
|
|
254
|
+
enableOrderedAppend?: "on" | "off";
|
|
255
|
+
/**
|
|
256
|
+
* Enable parallel chunk append optimization.
|
|
257
|
+
* @default "on"
|
|
258
|
+
*/
|
|
259
|
+
enableParallelChunkAppend?: "on" | "off";
|
|
260
|
+
/**
|
|
261
|
+
* Enable qual propagation optimization.
|
|
262
|
+
* @default "on"
|
|
263
|
+
*/
|
|
264
|
+
enableQualPropagation?: "on" | "off";
|
|
265
|
+
/**
|
|
266
|
+
* Enable runtime exclusion optimization.
|
|
267
|
+
* @default "on"
|
|
268
|
+
*/
|
|
269
|
+
enableRuntimeExclusion?: "on" | "off";
|
|
270
|
+
/**
|
|
271
|
+
* Enable tiered reads.
|
|
272
|
+
* @default "on"
|
|
273
|
+
*/
|
|
274
|
+
enableTieredReads?: "on" | "off";
|
|
275
|
+
/**
|
|
276
|
+
* Enable TSS callbacks.
|
|
277
|
+
* @default "on"
|
|
278
|
+
*/
|
|
279
|
+
enableTssCallbacks?: "on" | "off";
|
|
280
|
+
/**
|
|
281
|
+
* Maximum number of cached chunks per hypertable.
|
|
282
|
+
* @default 1024
|
|
283
|
+
*/
|
|
284
|
+
maxCachedChunksPerHypertable?: number;
|
|
285
|
+
/**
|
|
286
|
+
* Maximum number of open chunks per insert.
|
|
287
|
+
* @default 1024
|
|
288
|
+
*/
|
|
289
|
+
maxOpenChunksPerInsert?: number;
|
|
290
|
+
/**
|
|
291
|
+
* Whether the database is being restored from a backup.
|
|
292
|
+
* @default "off"
|
|
293
|
+
*/
|
|
294
|
+
restoring?: "on" | "off";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Configuration for the pgvector extension.
|
|
299
|
+
* Provides vector similarity search for PostgreSQL.
|
|
300
|
+
*/
|
|
301
|
+
export interface PgvectorConfig {
|
|
302
|
+
/**
|
|
303
|
+
* Size of the dynamic candidate list for HNSW search.
|
|
304
|
+
* @default 40
|
|
305
|
+
*/
|
|
306
|
+
hnswEfSearch?: number;
|
|
307
|
+
/**
|
|
308
|
+
* Whether to enable iterative scan for HNSW indexes.
|
|
309
|
+
* @default "off"
|
|
310
|
+
*/
|
|
311
|
+
hnswIterativeScan?: "off" | "relaxed_order" | "strict_order";
|
|
312
|
+
/**
|
|
313
|
+
* Maximum number of tuples to scan with HNSW iterative scan.
|
|
314
|
+
* @default 20000
|
|
315
|
+
*/
|
|
316
|
+
hnswMaxScanTuples?: number;
|
|
317
|
+
/**
|
|
318
|
+
* Memory multiplier for HNSW scans.
|
|
319
|
+
* @default 1
|
|
320
|
+
*/
|
|
321
|
+
hnswScanMemMultiplier?: number;
|
|
322
|
+
/**
|
|
323
|
+
* Number of probes for IVFFlat index search.
|
|
324
|
+
* @default 1
|
|
325
|
+
*/
|
|
326
|
+
ivfflatProbes?: number;
|
|
327
|
+
/**
|
|
328
|
+
* Whether to enable iterative scan for IVFFlat indexes.
|
|
329
|
+
* @default "off"
|
|
330
|
+
*/
|
|
331
|
+
ivfflatIterativeScan?: "off" | "relaxed_order" | "strict_order";
|
|
332
|
+
/**
|
|
333
|
+
* Maximum number of probes for IVFFlat iterative scan.
|
|
334
|
+
* @default 32768
|
|
335
|
+
*/
|
|
336
|
+
ivfflatMaxProbes?: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Configuration for the pginsights extension.
|
|
341
|
+
* Provides query performance insights.
|
|
342
|
+
*/
|
|
343
|
+
export interface PgInsightsConfig {
|
|
344
|
+
/**
|
|
345
|
+
* Whether to collect raw (un-normalized) queries.
|
|
346
|
+
* @default "off"
|
|
347
|
+
*/
|
|
348
|
+
rawQueries?: "on" | "off";
|
|
349
|
+
/**
|
|
350
|
+
* Whether to normalize schema names in queries.
|
|
351
|
+
* @default "off"
|
|
352
|
+
*/
|
|
353
|
+
normalizeSchemaNames?: "on" | "off";
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* PostgreSQL database extensions configuration.
|
|
358
|
+
*
|
|
359
|
+
* Each property represents an extension. If present (even as `{}`), the extension
|
|
360
|
+
* is enabled with the given config (or defaults). If absent/undefined, the extension
|
|
361
|
+
* is disabled.
|
|
362
|
+
*/
|
|
363
|
+
export interface DatabaseExtensions {
|
|
364
|
+
/**
|
|
365
|
+
* pg_cron - Job scheduler for PostgreSQL.
|
|
366
|
+
* Schedules and runs PostgreSQL commands on a recurring basis.
|
|
367
|
+
*/
|
|
368
|
+
pgCron?: PgCronConfig;
|
|
369
|
+
/**
|
|
370
|
+
* pg_duckdb - Embedded DuckDB for analytical queries.
|
|
371
|
+
*/
|
|
372
|
+
pgDuckdb?: PgDuckdbConfig;
|
|
373
|
+
/**
|
|
374
|
+
* pg_hint_plan - Query plan hints.
|
|
375
|
+
* Controls execution plans using hints in SQL comments.
|
|
376
|
+
*/
|
|
377
|
+
pgHintPlan?: PgHintPlanConfig;
|
|
378
|
+
/**
|
|
379
|
+
* pg_partman (background worker) - Partition management automation.
|
|
380
|
+
*/
|
|
381
|
+
pgPartmanBgw?: PgPartmanBgwConfig;
|
|
382
|
+
/**
|
|
383
|
+
* pg_squeeze - Dead tuple removal with minimal locking.
|
|
384
|
+
*/
|
|
385
|
+
pgSqueeze?: PgSqueezeConfig;
|
|
386
|
+
/**
|
|
387
|
+
* pg_stat_statements - Query execution statistics tracking.
|
|
388
|
+
*/
|
|
389
|
+
pgStatStatements?: PgStatStatementsConfig;
|
|
390
|
+
/**
|
|
391
|
+
* pg_strict - Stricter SQL behavior for PostgreSQL.
|
|
392
|
+
* This extension has no configurable parameters.
|
|
393
|
+
*/
|
|
394
|
+
pgStrict?: Record<string, never>;
|
|
395
|
+
/**
|
|
396
|
+
* timescaledb - Time-series data support.
|
|
397
|
+
*/
|
|
398
|
+
timescaledb?: TimescaledbConfig;
|
|
399
|
+
/**
|
|
400
|
+
* pgvector - Vector similarity search.
|
|
401
|
+
*/
|
|
402
|
+
vector?: PgvectorConfig;
|
|
403
|
+
/**
|
|
404
|
+
* pginsights - Query performance insights.
|
|
405
|
+
*/
|
|
406
|
+
pgInsights?: PgInsightsConfig;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* A single change within an extensions diff: add, remove, or update.
|
|
411
|
+
* @internal
|
|
412
|
+
*/
|
|
413
|
+
export interface ExtensionDiffEntry {
|
|
414
|
+
/**
|
|
415
|
+
* The shared_preload_libraries name (e.g. "pg_cron", "vector").
|
|
416
|
+
*/
|
|
417
|
+
library: string;
|
|
418
|
+
/**
|
|
419
|
+
* "add" = extension is being enabled,
|
|
420
|
+
* "remove" = extension is being disabled,
|
|
421
|
+
* "update" = extension params changed (stays enabled).
|
|
422
|
+
*/
|
|
423
|
+
action: "add" | "remove" | "update";
|
|
424
|
+
/**
|
|
425
|
+
* The pgconf parameter key-value pairs to send for this extension.
|
|
426
|
+
* Empty for "remove" actions.
|
|
427
|
+
*/
|
|
428
|
+
params: Record<string, string>;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Result of diffing two DatabaseExtensions objects.
|
|
433
|
+
* @internal
|
|
434
|
+
*/
|
|
435
|
+
export interface ExtensionsDiff {
|
|
436
|
+
/**
|
|
437
|
+
* Individual changes per extension.
|
|
438
|
+
*/
|
|
439
|
+
changes: ExtensionDiffEntry[];
|
|
440
|
+
/**
|
|
441
|
+
* True if there are any changes at all.
|
|
442
|
+
*/
|
|
443
|
+
hasChanges: boolean;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
// Mapping from our camelCase config keys to the pgconf parameter names
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
/** @internal */
|
|
451
|
+
interface ParamMapping {
|
|
452
|
+
library: string;
|
|
453
|
+
params: Record<string, string>;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function mapPgCron(config: PgCronConfig): ParamMapping {
|
|
457
|
+
return {
|
|
458
|
+
library: "pg_cron",
|
|
459
|
+
params: {
|
|
460
|
+
"cron.database_name": config.databaseName ?? "postgres",
|
|
461
|
+
"cron.launch_active_jobs": config.launchActiveJobs ?? "on",
|
|
462
|
+
"cron.log_min_messages": config.logMinMessages ?? "warning",
|
|
463
|
+
"cron.log_run": config.logRun ?? "on",
|
|
464
|
+
"cron.log_statement": config.logStatement ?? "on",
|
|
465
|
+
"cron.max_running_jobs": String(config.maxRunningJobs ?? 1),
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function mapPgDuckdb(config: PgDuckdbConfig): ParamMapping {
|
|
471
|
+
return {
|
|
472
|
+
library: "pg_duckdb",
|
|
473
|
+
params: {
|
|
474
|
+
"duckdb.postgres_role": config.postgresRole ?? "pscale_superuser",
|
|
475
|
+
"duckdb.memory_limit": config.memoryLimit ?? "0",
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function mapPgHintPlan(config: PgHintPlanConfig): ParamMapping {
|
|
481
|
+
return {
|
|
482
|
+
library: "pg_hint_plan",
|
|
483
|
+
params: {
|
|
484
|
+
"pg_hint_plan.enable_hint": config.enableHint ?? "on",
|
|
485
|
+
"pg_hint_plan.enable_hint_table": config.enableHintTable ?? "off",
|
|
486
|
+
"pg_hint_plan.parse_messages": config.parseMessages ?? "info",
|
|
487
|
+
"pg_hint_plan.debug_print": config.debugPrint ?? "off",
|
|
488
|
+
"pg_hint_plan.message_level": config.messageLevel ?? "info",
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function mapPgPartmanBgw(config: PgPartmanBgwConfig): ParamMapping {
|
|
494
|
+
return {
|
|
495
|
+
library: "pg_partman_bgw",
|
|
496
|
+
params: {
|
|
497
|
+
"pg_partman_bgw.interval": String(config.interval ?? 3600),
|
|
498
|
+
"pg_partman_bgw.dbname": config.dbname ?? "postgres",
|
|
499
|
+
"pg_partman_bgw.role": config.role ?? "postgres",
|
|
500
|
+
"pg_partman_bgw.analyze": config.analyze ?? "off",
|
|
501
|
+
"pg_partman_bgw.jobmon": config.jobmon ?? "on",
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function mapPgSqueeze(config: PgSqueezeConfig): ParamMapping {
|
|
507
|
+
return {
|
|
508
|
+
library: "pg_squeeze",
|
|
509
|
+
params: {
|
|
510
|
+
"squeeze.max_xlock_time": String(config.maxXlockTime ?? 0),
|
|
511
|
+
"squeeze.worker_autostart": config.workerAutostart ?? "postgres",
|
|
512
|
+
"squeeze.workers_per_database": String(config.workersPerDatabase ?? 1),
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function mapPgStatStatements(config: PgStatStatementsConfig): ParamMapping {
|
|
518
|
+
return {
|
|
519
|
+
library: "pg_stat_statements",
|
|
520
|
+
params: {
|
|
521
|
+
"pg_stat_statements.max": String(config.max ?? 5000),
|
|
522
|
+
"pg_stat_statements.track": config.track ?? "top",
|
|
523
|
+
"pg_stat_statements.track_utility": config.trackUtility ?? "on",
|
|
524
|
+
"pg_stat_statements.track_planning": config.trackPlanning ?? "off",
|
|
525
|
+
"pg_stat_statements.save": config.save ?? "on",
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function mapTimescaledb(config: TimescaledbConfig): ParamMapping {
|
|
531
|
+
return {
|
|
532
|
+
library: "timescaledb",
|
|
533
|
+
params: {
|
|
534
|
+
"timescaledb.bgw_log_level": config.bgwLogLevel ?? "warning",
|
|
535
|
+
"timescaledb.enable_chunk_append": config.enableChunkAppend ?? "on",
|
|
536
|
+
"timescaledb.enable_chunk_skipping": config.enableChunkSkipping ?? "off",
|
|
537
|
+
"timescaledb.enable_constraint_aware_append":
|
|
538
|
+
config.enableConstraintAwareAppend ?? "on",
|
|
539
|
+
"timescaledb.enable_constraint_exclusion":
|
|
540
|
+
config.enableConstraintExclusion ?? "on",
|
|
541
|
+
"timescaledb.enable_custom_hashagg": config.enableCustomHashagg ?? "off",
|
|
542
|
+
"timescaledb.enable_deprecation_warnings":
|
|
543
|
+
config.enableDeprecationWarnings ?? "on",
|
|
544
|
+
"timescaledb.enable_event_triggers": config.enableEventTriggers ?? "off",
|
|
545
|
+
"timescaledb.enable_foreign_key_propagation":
|
|
546
|
+
config.enableForeignKeyPropagation ?? "on",
|
|
547
|
+
"timescaledb.enable_job_execution_logging":
|
|
548
|
+
config.enableJobExecutionLogging ?? "off",
|
|
549
|
+
"timescaledb.enable_now_constify": config.enableNowConstify ?? "on",
|
|
550
|
+
"timescaledb.enable_optimizations": config.enableOptimizations ?? "on",
|
|
551
|
+
"timescaledb.enable_ordered_append": config.enableOrderedAppend ?? "on",
|
|
552
|
+
"timescaledb.enable_parallel_chunk_append":
|
|
553
|
+
config.enableParallelChunkAppend ?? "on",
|
|
554
|
+
"timescaledb.enable_qual_propagation":
|
|
555
|
+
config.enableQualPropagation ?? "on",
|
|
556
|
+
"timescaledb.enable_runtime_exclusion":
|
|
557
|
+
config.enableRuntimeExclusion ?? "on",
|
|
558
|
+
"timescaledb.enable_tiered_reads": config.enableTieredReads ?? "on",
|
|
559
|
+
"timescaledb.enable_tss_callbacks": config.enableTssCallbacks ?? "on",
|
|
560
|
+
"timescaledb.max_cached_chunks_per_hypertable": String(
|
|
561
|
+
config.maxCachedChunksPerHypertable ?? 1024,
|
|
562
|
+
),
|
|
563
|
+
"timescaledb.max_open_chunks_per_insert": String(
|
|
564
|
+
config.maxOpenChunksPerInsert ?? 1024,
|
|
565
|
+
),
|
|
566
|
+
"timescaledb.restoring": config.restoring ?? "off",
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function mapVector(config: PgvectorConfig): ParamMapping {
|
|
572
|
+
return {
|
|
573
|
+
library: "vector",
|
|
574
|
+
params: {
|
|
575
|
+
"hnsw.ef_search": String(config.hnswEfSearch ?? 40),
|
|
576
|
+
"hnsw.iterative_scan": config.hnswIterativeScan ?? "off",
|
|
577
|
+
"hnsw.max_scan_tuples": String(config.hnswMaxScanTuples ?? 20000),
|
|
578
|
+
"hnsw.scan_mem_multiplier": String(config.hnswScanMemMultiplier ?? 1),
|
|
579
|
+
"ivfflat.probes": String(config.ivfflatProbes ?? 1),
|
|
580
|
+
"ivfflat.iterative_scan": config.ivfflatIterativeScan ?? "off",
|
|
581
|
+
"ivfflat.max_probes": String(config.ivfflatMaxProbes ?? 32768),
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function mapPgInsights(config: PgInsightsConfig): ParamMapping {
|
|
587
|
+
return {
|
|
588
|
+
library: "pginsights",
|
|
589
|
+
params: {
|
|
590
|
+
"pginsights.raw_queries": config.rawQueries ?? "off",
|
|
591
|
+
"pginsights.normalize_schema_names": config.normalizeSchemaNames ?? "off",
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Converts a DatabaseExtensions object into a map of library name -> pgconf params.
|
|
598
|
+
* Only includes extensions that are present (enabled).
|
|
599
|
+
* @internal
|
|
600
|
+
*/
|
|
601
|
+
export function extensionsToParamMappings(
|
|
602
|
+
extensions: DatabaseExtensions,
|
|
603
|
+
): Map<string, Record<string, string>> {
|
|
604
|
+
const mappings = new Map<string, Record<string, string>>();
|
|
605
|
+
|
|
606
|
+
if (extensions.pgCron !== undefined) {
|
|
607
|
+
const m = mapPgCron(extensions.pgCron);
|
|
608
|
+
mappings.set(m.library, m.params);
|
|
609
|
+
}
|
|
610
|
+
if (extensions.pgDuckdb !== undefined) {
|
|
611
|
+
const m = mapPgDuckdb(extensions.pgDuckdb);
|
|
612
|
+
mappings.set(m.library, m.params);
|
|
613
|
+
}
|
|
614
|
+
if (extensions.pgHintPlan !== undefined) {
|
|
615
|
+
const m = mapPgHintPlan(extensions.pgHintPlan);
|
|
616
|
+
mappings.set(m.library, m.params);
|
|
617
|
+
}
|
|
618
|
+
if (extensions.pgPartmanBgw !== undefined) {
|
|
619
|
+
const m = mapPgPartmanBgw(extensions.pgPartmanBgw);
|
|
620
|
+
mappings.set(m.library, m.params);
|
|
621
|
+
}
|
|
622
|
+
if (extensions.pgSqueeze !== undefined) {
|
|
623
|
+
const m = mapPgSqueeze(extensions.pgSqueeze);
|
|
624
|
+
mappings.set(m.library, m.params);
|
|
625
|
+
}
|
|
626
|
+
if (extensions.pgStatStatements !== undefined) {
|
|
627
|
+
const m = mapPgStatStatements(extensions.pgStatStatements);
|
|
628
|
+
mappings.set(m.library, m.params);
|
|
629
|
+
}
|
|
630
|
+
if (extensions.pgStrict !== undefined) {
|
|
631
|
+
mappings.set("pg_strict", {});
|
|
632
|
+
}
|
|
633
|
+
if (extensions.timescaledb !== undefined) {
|
|
634
|
+
const m = mapTimescaledb(extensions.timescaledb);
|
|
635
|
+
mappings.set(m.library, m.params);
|
|
636
|
+
}
|
|
637
|
+
if (extensions.vector !== undefined) {
|
|
638
|
+
const m = mapVector(extensions.vector);
|
|
639
|
+
mappings.set(m.library, m.params);
|
|
640
|
+
}
|
|
641
|
+
if (extensions.pgInsights !== undefined) {
|
|
642
|
+
const m = mapPgInsights(extensions.pgInsights);
|
|
643
|
+
mappings.set(m.library, m.params);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return mappings;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Computes the diff between two extension configurations.
|
|
651
|
+
*
|
|
652
|
+
* @param current - The currently active extensions (or `undefined`/`{}` if none)
|
|
653
|
+
* @param desired - The desired extensions configuration
|
|
654
|
+
* @returns An ExtensionsDiff describing what needs to change
|
|
655
|
+
*
|
|
656
|
+
* @example
|
|
657
|
+
* ```ts
|
|
658
|
+
* const diff = diffExtensions(
|
|
659
|
+
* { pgCron: { maxRunningJobs: 1 } },
|
|
660
|
+
* { pgCron: { maxRunningJobs: 5 }, vector: {} },
|
|
661
|
+
* );
|
|
662
|
+
* // diff.changes = [
|
|
663
|
+
* // { library: "pg_cron", action: "update", params: { "cron.max_running_jobs": "5" } },
|
|
664
|
+
* // { library: "vector", action: "add", params: {} },
|
|
665
|
+
* // ]
|
|
666
|
+
* ```
|
|
667
|
+
*/
|
|
668
|
+
export function diffExtensions(
|
|
669
|
+
current: DatabaseExtensions | undefined,
|
|
670
|
+
desired: DatabaseExtensions | undefined,
|
|
671
|
+
): ExtensionsDiff {
|
|
672
|
+
const currentMappings = extensionsToParamMappings(current ?? {});
|
|
673
|
+
const desiredMappings = extensionsToParamMappings(desired ?? {});
|
|
674
|
+
|
|
675
|
+
const changes: ExtensionDiffEntry[] = [];
|
|
676
|
+
|
|
677
|
+
// Find additions and updates
|
|
678
|
+
for (const [library, desiredParams] of desiredMappings) {
|
|
679
|
+
const currentParams = currentMappings.get(library);
|
|
680
|
+
if (!currentParams) {
|
|
681
|
+
// Extension is being added
|
|
682
|
+
changes.push({ library, action: "add", params: desiredParams });
|
|
683
|
+
} else {
|
|
684
|
+
// Extension exists — check if params changed
|
|
685
|
+
const paramsChanged =
|
|
686
|
+
JSON.stringify(sortedEntries(currentParams)) !==
|
|
687
|
+
JSON.stringify(sortedEntries(desiredParams));
|
|
688
|
+
if (paramsChanged) {
|
|
689
|
+
changes.push({ library, action: "update", params: desiredParams });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Find removals
|
|
695
|
+
for (const [library] of currentMappings) {
|
|
696
|
+
if (!desiredMappings.has(library)) {
|
|
697
|
+
changes.push({ library, action: "remove", params: {} });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
changes,
|
|
703
|
+
hasChanges: changes.length > 0,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function sortedEntries(obj: Record<string, string>): [string, string][] {
|
|
708
|
+
return Object.entries(obj).sort(([a], [b]) => a.localeCompare(b));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ---------------------------------------------------------------------------
|
|
712
|
+
// API interaction — reverse-engineered from PlanetScale dashboard HAR captures
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Options for updating database extensions.
|
|
717
|
+
*/
|
|
718
|
+
export interface UpdateExtensionsOptions extends PlanetScaleProps {
|
|
719
|
+
/**
|
|
720
|
+
* The organization name.
|
|
721
|
+
* @default process.env.PLANETSCALE_ORGANIZATION
|
|
722
|
+
*/
|
|
723
|
+
organization?: string;
|
|
724
|
+
/**
|
|
725
|
+
* The database name or Database resource.
|
|
726
|
+
*/
|
|
727
|
+
database: string | Database;
|
|
728
|
+
/**
|
|
729
|
+
* The branch name or Branch resource.
|
|
730
|
+
* @default "main"
|
|
731
|
+
*/
|
|
732
|
+
branch?: string | Branch;
|
|
733
|
+
/**
|
|
734
|
+
* The base URL of the PlanetScale API.
|
|
735
|
+
* @default "https://api.planetscale.com/v1"
|
|
736
|
+
*/
|
|
737
|
+
baseUrl?: string;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function resolveOptions(opts: UpdateExtensionsOptions): {
|
|
741
|
+
organization: string;
|
|
742
|
+
database: string;
|
|
743
|
+
branch: string;
|
|
744
|
+
baseUrl: string;
|
|
745
|
+
token: string;
|
|
746
|
+
} {
|
|
747
|
+
const organization =
|
|
748
|
+
opts.organization ??
|
|
749
|
+
(typeof opts.database !== "string"
|
|
750
|
+
? opts.database.organization
|
|
751
|
+
: undefined) ??
|
|
752
|
+
process.env.PLANETSCALE_ORGANIZATION ??
|
|
753
|
+
process.env.PLANETSCALE_ORG_ID;
|
|
754
|
+
|
|
755
|
+
if (!organization) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
"PlanetScale organization is required. Please set the `organization` property or the `PLANETSCALE_ORGANIZATION` environment variable.",
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const database =
|
|
762
|
+
typeof opts.database === "string" ? opts.database : opts.database.name;
|
|
763
|
+
const branch = !opts.branch
|
|
764
|
+
? "main"
|
|
765
|
+
: typeof opts.branch === "string"
|
|
766
|
+
? opts.branch
|
|
767
|
+
: opts.branch.name;
|
|
768
|
+
const baseUrl = opts.baseUrl ?? "https://api.planetscale.com/v1";
|
|
769
|
+
const token = extractToken(opts);
|
|
770
|
+
|
|
771
|
+
return { organization, database, branch, baseUrl, token };
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Builds the multipart/form-data body for a change request that sets extensions.
|
|
776
|
+
*
|
|
777
|
+
* The API expects:
|
|
778
|
+
* - `_method=PATCH`
|
|
779
|
+
* - `queued=1` (for queue phase) or `update=1` (for apply phase)
|
|
780
|
+
* - `parameters[pgconf][shared_preload_libraries][]` repeated for each library
|
|
781
|
+
* - `parameters[pgconf][<param.name>]=<param.value>` for each config param
|
|
782
|
+
*
|
|
783
|
+
* @internal
|
|
784
|
+
*/
|
|
785
|
+
function buildExtensionFormData(
|
|
786
|
+
desired: DatabaseExtensions,
|
|
787
|
+
phase: "queue" | "apply",
|
|
788
|
+
): FormData {
|
|
789
|
+
const form = new FormData();
|
|
790
|
+
form.append("_method", "PATCH");
|
|
791
|
+
|
|
792
|
+
if (phase === "apply") {
|
|
793
|
+
form.append("update", "1");
|
|
794
|
+
return form;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Queue phase — include all libraries and params
|
|
798
|
+
form.append("queued", "1");
|
|
799
|
+
|
|
800
|
+
const mappings = extensionsToParamMappings(desired);
|
|
801
|
+
|
|
802
|
+
// Always include base libraries
|
|
803
|
+
for (const lib of BASE_LIBRARIES) {
|
|
804
|
+
form.append("parameters[pgconf][shared_preload_libraries][]", lib);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Include pginsights in base libraries if it has config, otherwise include as base
|
|
808
|
+
const hasPgInsights = mappings.has("pginsights");
|
|
809
|
+
if (!hasPgInsights) {
|
|
810
|
+
// pginsights is always loaded; if user didn't explicitly configure it, include with defaults off
|
|
811
|
+
form.append("parameters[pgconf][shared_preload_libraries][]", "pginsights");
|
|
812
|
+
form.append("parameters[pgconf][pginsights.raw_queries]", "off");
|
|
813
|
+
form.append("parameters[pgconf][pginsights.normalize_schema_names]", "off");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Add user-requested extensions
|
|
817
|
+
for (const [library, params] of mappings) {
|
|
818
|
+
form.append("parameters[pgconf][shared_preload_libraries][]", library);
|
|
819
|
+
|
|
820
|
+
for (const [key, value] of Object.entries(params)) {
|
|
821
|
+
form.append(`parameters[pgconf][${key}]`, value);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return form;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Updates the PostgreSQL extensions on a PlanetScale database branch.
|
|
830
|
+
*
|
|
831
|
+
* Uses the reverse-engineered two-phase change request API:
|
|
832
|
+
* 1. **Queue** — submits a change request with the desired extension config
|
|
833
|
+
* 2. **Apply** — triggers execution of the queued change
|
|
834
|
+
* 3. **Poll** — waits for the change to complete
|
|
835
|
+
*
|
|
836
|
+
* @param desired - The desired extension configuration. Extensions present are enabled;
|
|
837
|
+
* extensions absent are disabled.
|
|
838
|
+
* @param options - Connection and targeting options.
|
|
839
|
+
*
|
|
840
|
+
* @example
|
|
841
|
+
* ## Enable pgvector and pg_cron
|
|
842
|
+
*
|
|
843
|
+
* ```ts
|
|
844
|
+
* await updateExtensions(
|
|
845
|
+
* {
|
|
846
|
+
* vector: { hnswEfSearch: 100 },
|
|
847
|
+
* pgCron: {},
|
|
848
|
+
* },
|
|
849
|
+
* {
|
|
850
|
+
* organization: "my-org",
|
|
851
|
+
* database: "my-db",
|
|
852
|
+
* branch: "main",
|
|
853
|
+
* },
|
|
854
|
+
* );
|
|
855
|
+
* ```
|
|
856
|
+
*
|
|
857
|
+
* @example
|
|
858
|
+
* ## Disable all user extensions
|
|
859
|
+
*
|
|
860
|
+
* ```ts
|
|
861
|
+
* await updateExtensions({}, {
|
|
862
|
+
* organization: "my-org",
|
|
863
|
+
* database: "my-db",
|
|
864
|
+
* });
|
|
865
|
+
* ```
|
|
866
|
+
*/
|
|
867
|
+
export async function updateExtensions(
|
|
868
|
+
desired: DatabaseExtensions,
|
|
869
|
+
options: UpdateExtensionsOptions,
|
|
870
|
+
): Promise<void> {
|
|
871
|
+
const { organization, database, branch, baseUrl, token } =
|
|
872
|
+
resolveOptions(options);
|
|
873
|
+
|
|
874
|
+
const changesUrl = `${baseUrl}/organizations/${organization}/databases/${database}/branches/${branch}/changes`;
|
|
875
|
+
|
|
876
|
+
// Wait for any in-flight changes to complete before submitting ours
|
|
877
|
+
await waitForPendingChanges(changesUrl, token);
|
|
878
|
+
|
|
879
|
+
// Phase 1: Queue the change
|
|
880
|
+
const queueForm = buildExtensionFormData(desired, "queue");
|
|
881
|
+
const queueResponse = await fetch(changesUrl, {
|
|
882
|
+
method: "POST",
|
|
883
|
+
headers: {
|
|
884
|
+
Authorization: token,
|
|
885
|
+
},
|
|
886
|
+
body: queueForm,
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
if (!queueResponse.ok) {
|
|
890
|
+
const errorBody = await queueResponse.text();
|
|
891
|
+
throw new Error(
|
|
892
|
+
`Failed to queue extension change (${queueResponse.status}): ${errorBody}`,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Phase 2: Apply the change
|
|
897
|
+
const applyForm = buildExtensionFormData(desired, "apply");
|
|
898
|
+
const applyResponse = await fetch(changesUrl, {
|
|
899
|
+
method: "POST",
|
|
900
|
+
headers: {
|
|
901
|
+
Authorization: token,
|
|
902
|
+
},
|
|
903
|
+
body: applyForm,
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
if (!applyResponse.ok) {
|
|
907
|
+
const errorBody = await applyResponse.text();
|
|
908
|
+
throw new Error(
|
|
909
|
+
`Failed to apply extension change (${applyResponse.status}): ${errorBody}`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Phase 3: Poll until the change is completed
|
|
914
|
+
await waitForPendingChanges(changesUrl, token);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Polls the changes endpoint until all changes are completed or canceled.
|
|
919
|
+
* @internal
|
|
920
|
+
*/
|
|
921
|
+
async function waitForPendingChanges(
|
|
922
|
+
changesUrl: string,
|
|
923
|
+
token: string,
|
|
924
|
+
): Promise<void> {
|
|
925
|
+
await poll({
|
|
926
|
+
description: "extension changes to complete",
|
|
927
|
+
fn: async () => {
|
|
928
|
+
const response = await fetch(`${changesUrl}?per_page=1`, {
|
|
929
|
+
headers: { Authorization: token },
|
|
930
|
+
});
|
|
931
|
+
if (!response.ok) {
|
|
932
|
+
throw new Error(
|
|
933
|
+
`Failed to poll change status (${response.status}): ${await response.text()}`,
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
const data = (await response.json()) as {
|
|
937
|
+
data: Array<{ state: string; id: string }>;
|
|
938
|
+
};
|
|
939
|
+
return data;
|
|
940
|
+
},
|
|
941
|
+
predicate: (result) => {
|
|
942
|
+
if (result.data.length === 0) return true;
|
|
943
|
+
const latest = result.data[0];
|
|
944
|
+
return latest.state === "completed" || latest.state === "canceled";
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
}
|