@samebase/alchemy-convex 0.2.0 → 0.3.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/README.md +169 -46
- package/lib/index.d.ts +168 -10
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +494 -45
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Code.ts +463 -0
- package/src/Deploy.ts +10 -20
- package/src/Deployment.ts +409 -0
- package/src/EnvironmentVariable.ts +33 -6
- package/src/Providers.ts +9 -1
- package/src/index.ts +20 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// Convex.Deployment: one deployment of a Convex project, with the URLs that a
|
|
2
|
+
// frontend or a Worker needs.
|
|
3
|
+
//
|
|
4
|
+
// - `prod` is the project's default production deployment. Convex creates it
|
|
5
|
+
// with the project, so this resource never creates one. It takes the
|
|
6
|
+
// deployment over without --adopt: it never changes or deletes a production
|
|
7
|
+
// deployment, so the takeover cannot lose data. It follows the default:
|
|
8
|
+
// when another production deployment becomes the default, the next deploy
|
|
9
|
+
// outputs that one. To delete a production deployment, delete its project
|
|
10
|
+
// (Convex.Project with RemovalPolicy.destroy()). Delete reads the live type
|
|
11
|
+
// first, so a dev or preview deployment that became a production
|
|
12
|
+
// deployment is not deleted either.
|
|
13
|
+
// - `dev` and `preview` are created with POST
|
|
14
|
+
// /projects/{project_id}/create_deployment and the `name` prop as the
|
|
15
|
+
// reference. Convex keeps a dev reference as given. For a preview, Convex
|
|
16
|
+
// records `name` as the preview identifier, the name that
|
|
17
|
+
// `npx convex deploy --preview-name <name>` finds, and derives the reference
|
|
18
|
+
// from it (`preview/<slug>`). A create for a preview name that already has
|
|
19
|
+
// a preview deployment deletes that deployment and creates a new one, so
|
|
20
|
+
// reconcile always looks the name up first and never creates over an
|
|
21
|
+
// existing deployment. A dev or preview deployment that exists but is not
|
|
22
|
+
// in state belongs to someone else: the resource takes it over only with
|
|
23
|
+
// --adopt.
|
|
24
|
+
// - `projectId`, `type`, and `name` are the identity. A change fails with
|
|
25
|
+
// DeploymentIdentityChange; the resource is never replaced.
|
|
26
|
+
// - The removal policy is `retain` for every type. A preview deployment
|
|
27
|
+
// expires on Convex's schedule (14 days by default). To delete a dev or
|
|
28
|
+
// preview deployment with the resource, add RemovalPolicy.destroy().
|
|
29
|
+
//
|
|
30
|
+
// Patterns from Confect `packages/alchemy/src/Deployment.ts`
|
|
31
|
+
// (https://github.com/rjdellecese/confect, ISC license).
|
|
32
|
+
import { Resource } from "alchemy";
|
|
33
|
+
import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
|
|
34
|
+
import { isResolved } from "alchemy/Diff";
|
|
35
|
+
import * as Provider from "alchemy/Provider";
|
|
36
|
+
import * as Effect from "effect/Effect";
|
|
37
|
+
import * as Schema from "effect/Schema";
|
|
38
|
+
import { shouldAdopt } from "./Adoption.ts";
|
|
39
|
+
import { absentAsUndefined, ManagementApi, retryIdempotentWrite } from "./ManagementApi.ts";
|
|
40
|
+
import type { Providers } from "./Providers.ts";
|
|
41
|
+
|
|
42
|
+
export type DeploymentProps =
|
|
43
|
+
| {
|
|
44
|
+
/** Numeric project id, such as `project.projectId`. Cannot change. */
|
|
45
|
+
readonly projectId: number;
|
|
46
|
+
/** The project's default production deployment. Cannot change. */
|
|
47
|
+
readonly type: "prod";
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
/** Numeric project id, such as `project.projectId`. Cannot change. */
|
|
51
|
+
readonly projectId: number;
|
|
52
|
+
/** A dev deployment or a preview deployment that this resource creates. Cannot change. */
|
|
53
|
+
readonly type: "dev" | "preview";
|
|
54
|
+
/**
|
|
55
|
+
* The dev reference, such as `staging`, or the preview name, such as a
|
|
56
|
+
* pull request or stage name. Unique in the project. Cannot change.
|
|
57
|
+
*/
|
|
58
|
+
readonly name: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface DeploymentAttributes {
|
|
62
|
+
readonly projectId: number;
|
|
63
|
+
readonly type: DeploymentProps["type"];
|
|
64
|
+
/** Deployment name, such as `happy-animal-123`. */
|
|
65
|
+
readonly name: string;
|
|
66
|
+
/** The reference that Convex lists, such as `production`, `staging`, or `preview/pr-42`. */
|
|
67
|
+
readonly reference: string;
|
|
68
|
+
/** The preview name of a preview deployment. Undefined for prod and dev. */
|
|
69
|
+
readonly previewName: string | undefined;
|
|
70
|
+
/** Client URL, such as `https://happy-animal-123.convex.cloud`. */
|
|
71
|
+
readonly url: string;
|
|
72
|
+
/** HTTP actions URL, such as `https://happy-animal-123.convex.site`. */
|
|
73
|
+
readonly siteUrl: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type Deployment = Resource<
|
|
77
|
+
"Convex.Deployment",
|
|
78
|
+
DeploymentProps,
|
|
79
|
+
DeploymentAttributes,
|
|
80
|
+
never,
|
|
81
|
+
Providers
|
|
82
|
+
>;
|
|
83
|
+
export const Deployment = Resource<Deployment>("Convex.Deployment", {
|
|
84
|
+
defaultRemovalPolicy: "retain",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
/** The props name another deployment than the one in state. */
|
|
88
|
+
export class DeploymentIdentityChange extends Schema.TaggedError<DeploymentIdentityChange>()(
|
|
89
|
+
"DeploymentIdentityChange",
|
|
90
|
+
{
|
|
91
|
+
/** Name of the deployment in state, such as `happy-animal-123`. */
|
|
92
|
+
deployment: Schema.String,
|
|
93
|
+
field: Schema.Literals(["projectId", "type", "name"]),
|
|
94
|
+
current: Schema.String,
|
|
95
|
+
requested: Schema.String,
|
|
96
|
+
},
|
|
97
|
+
) {
|
|
98
|
+
override get message() {
|
|
99
|
+
return `Convex.Deployment cannot change the ${this.field} of deployment ${this.deployment} from ${this.current} to ${this.requested}. Set ${this.field} back to ${this.current}. For another deployment, create a new resource with a new logical id.`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The project has no default production deployment for a `prod` resource to take over. */
|
|
104
|
+
export class ProductionDeploymentNotFound extends Schema.TaggedError<ProductionDeploymentNotFound>()(
|
|
105
|
+
"ProductionDeploymentNotFound",
|
|
106
|
+
{ projectId: Schema.Number },
|
|
107
|
+
) {
|
|
108
|
+
override get message() {
|
|
109
|
+
return `Project ${this.projectId} has no default production deployment, and Convex.Deployment does not create one. Create it in the Convex dashboard, or with \`npx convex deployment create --type prod --default\` in the project, then run the deploy again.`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** More than one deployment of the project has the name, so the lookup cannot choose one. */
|
|
114
|
+
export class AmbiguousDeployment extends Schema.TaggedError<AmbiguousDeployment>()(
|
|
115
|
+
"AmbiguousDeployment",
|
|
116
|
+
{
|
|
117
|
+
projectId: Schema.Number,
|
|
118
|
+
type: Schema.Literals(["dev", "preview"]),
|
|
119
|
+
name: Schema.String,
|
|
120
|
+
deployments: Schema.Array(Schema.String),
|
|
121
|
+
},
|
|
122
|
+
) {
|
|
123
|
+
override get message() {
|
|
124
|
+
return `Project ${this.projectId} has ${this.deployments.length} ${this.type} deployments named "${this.name}" (${this.deployments.join(", ")}). Convex.Deployment does not choose one. Delete the extra deployments in the Convex dashboard, or use another name.`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The URL of a Convex cloud deployment: `https://<name>.convex.cloud`, or
|
|
130
|
+
* `https://<name>.<region>.convex.cloud` outside US East.
|
|
131
|
+
*/
|
|
132
|
+
const CLOUD_URL =
|
|
133
|
+
/^https:\/\/[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud$/;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The HTTP actions URL of a deployment: the client URL with `.convex.site`
|
|
137
|
+
* for `.convex.cloud`, as Convex sets CONVEX_SITE_URL. Custom domains are
|
|
138
|
+
* not covered.
|
|
139
|
+
*/
|
|
140
|
+
export const siteUrlOf = (url: string): string => url.replace(/\.convex\.cloud$/, ".convex.site");
|
|
141
|
+
|
|
142
|
+
/** One row of list_deployments, GET /deployments/{name}, and create_deployment. Only the fields the provider uses. */
|
|
143
|
+
export const DeploymentRow = Schema.Struct({
|
|
144
|
+
name: Schema.String,
|
|
145
|
+
projectId: Schema.Number,
|
|
146
|
+
deploymentType: Schema.String,
|
|
147
|
+
isDefault: Schema.Boolean,
|
|
148
|
+
reference: Schema.String,
|
|
149
|
+
/** Set in list_deployments and create_deployment rows only: GET /deployments/{name} answers null. */
|
|
150
|
+
previewIdentifier: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
151
|
+
deploymentUrl: Schema.String.check(Schema.isPattern(CLOUD_URL)),
|
|
152
|
+
});
|
|
153
|
+
type DeploymentRow = typeof DeploymentRow.Type;
|
|
154
|
+
const DeploymentRows = Schema.Array(DeploymentRow);
|
|
155
|
+
|
|
156
|
+
const attributesOf = (
|
|
157
|
+
row: DeploymentRow,
|
|
158
|
+
type: DeploymentProps["type"],
|
|
159
|
+
previewName: string | undefined,
|
|
160
|
+
): DeploymentAttributes => ({
|
|
161
|
+
projectId: row.projectId,
|
|
162
|
+
type,
|
|
163
|
+
name: row.name,
|
|
164
|
+
reference: row.reference,
|
|
165
|
+
previewName: type === "preview" ? previewName : undefined,
|
|
166
|
+
url: row.deploymentUrl,
|
|
167
|
+
siteUrl: siteUrlOf(row.deploymentUrl),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
/** The `name` prop of the deployment in state: the dev reference or the preview name. */
|
|
171
|
+
const nameOf = (attributes: DeploymentAttributes) =>
|
|
172
|
+
attributes.type === "dev"
|
|
173
|
+
? attributes.reference
|
|
174
|
+
: attributes.type === "preview"
|
|
175
|
+
? attributes.previewName
|
|
176
|
+
: undefined;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fails with {@link DeploymentIdentityChange} when the props name another
|
|
180
|
+
* deployment than `current`: the attributes in state, or a live row.
|
|
181
|
+
*/
|
|
182
|
+
const checkIdentity = (current: DeploymentAttributes, news: DeploymentProps) => {
|
|
183
|
+
const change = (field: "projectId" | "type" | "name", from: string, to: string) =>
|
|
184
|
+
Effect.fail(
|
|
185
|
+
new DeploymentIdentityChange({
|
|
186
|
+
deployment: current.name,
|
|
187
|
+
field,
|
|
188
|
+
current: from,
|
|
189
|
+
requested: to,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
if (current.projectId !== news.projectId) {
|
|
193
|
+
return change("projectId", String(current.projectId), String(news.projectId));
|
|
194
|
+
}
|
|
195
|
+
if (current.type !== news.type) return change("type", current.type, news.type);
|
|
196
|
+
const currentName = nameOf(current);
|
|
197
|
+
if (news.type !== "prod" && currentName !== news.name) {
|
|
198
|
+
return change("name", currentName ?? "(none)", news.name);
|
|
199
|
+
}
|
|
200
|
+
return Effect.void;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export const DeploymentProvider = () =>
|
|
204
|
+
Provider.effect(
|
|
205
|
+
Deployment,
|
|
206
|
+
Effect.gen(function* () {
|
|
207
|
+
const api = yield* ManagementApi;
|
|
208
|
+
|
|
209
|
+
/** The live deployment, or undefined after a 404. GET /deployments/{name} answers no preview identifier. */
|
|
210
|
+
const getByName = (name: string) =>
|
|
211
|
+
absentAsUndefined(
|
|
212
|
+
api.request("get deployment", (client) =>
|
|
213
|
+
client.GET("/deployments/{deployment_name}", {
|
|
214
|
+
params: { path: { deployment_name: name } },
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
).pipe(
|
|
218
|
+
Effect.map((body) =>
|
|
219
|
+
body === undefined ? undefined : Schema.decodeUnknownSync(DeploymentRow)(body),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
const listDeployments = (
|
|
224
|
+
projectId: number,
|
|
225
|
+
type: DeploymentProps["type"],
|
|
226
|
+
isDefault?: boolean,
|
|
227
|
+
) =>
|
|
228
|
+
api
|
|
229
|
+
.request("list deployments", (client) =>
|
|
230
|
+
client.GET("/projects/{project_id}/list_deployments", {
|
|
231
|
+
params: {
|
|
232
|
+
path: { project_id: projectId },
|
|
233
|
+
query: { deploymentType: type, ...(isDefault === undefined ? {} : { isDefault }) },
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
)
|
|
237
|
+
.pipe(Effect.map((body) => Schema.decodeUnknownSync(DeploymentRows)(body)));
|
|
238
|
+
|
|
239
|
+
/** The project's default production deployment, or undefined. */
|
|
240
|
+
const production = (projectId: number) =>
|
|
241
|
+
listDeployments(projectId, "prod", true).pipe(
|
|
242
|
+
Effect.map((rows) => {
|
|
243
|
+
const row = rows.find(
|
|
244
|
+
(candidate) => candidate.deploymentType === "prod" && candidate.isDefault,
|
|
245
|
+
);
|
|
246
|
+
return row === undefined ? undefined : attributesOf(row, "prod", undefined);
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The one dev deployment with this reference, or the one preview
|
|
252
|
+
* deployment with this preview name, or undefined.
|
|
253
|
+
*/
|
|
254
|
+
const findByName = (projectId: number, type: "dev" | "preview", name: string) =>
|
|
255
|
+
Effect.gen(function* () {
|
|
256
|
+
const matches = (yield* listDeployments(projectId, type)).filter(
|
|
257
|
+
(row) =>
|
|
258
|
+
row.deploymentType === type &&
|
|
259
|
+
(type === "dev" ? row.reference === name : row.previewIdentifier === name),
|
|
260
|
+
);
|
|
261
|
+
if (matches.length > 1) {
|
|
262
|
+
return yield* new AmbiguousDeployment({
|
|
263
|
+
projectId,
|
|
264
|
+
type,
|
|
265
|
+
name,
|
|
266
|
+
deployments: matches.map((row) => row.name),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return matches[0];
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
/** The deployment that the props name, for a resource without state. */
|
|
273
|
+
const lookup = (news: DeploymentProps) =>
|
|
274
|
+
news.type === "prod"
|
|
275
|
+
? production(news.projectId)
|
|
276
|
+
: findByName(news.projectId, news.type, news.name).pipe(
|
|
277
|
+
Effect.map((row) =>
|
|
278
|
+
row === undefined ? undefined : attributesOf(row, news.type, news.name),
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
// `name`, `reference`, and the URLs change when an expired preview is
|
|
284
|
+
// created again, or when another production deployment becomes the
|
|
285
|
+
// default.
|
|
286
|
+
stables: ["projectId", "type", "previewName"],
|
|
287
|
+
|
|
288
|
+
// Compares with the attributes: after a refused change, `olds` are
|
|
289
|
+
// the refused props.
|
|
290
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
291
|
+
if (!isResolved(news) || output === undefined) return undefined;
|
|
292
|
+
yield* checkIdentity(output, news);
|
|
293
|
+
// `prod` follows the project's default production deployment,
|
|
294
|
+
// which can change in Convex. Reconcile then returns the new one.
|
|
295
|
+
if (news.type === "prod") {
|
|
296
|
+
// A 404 for a deleted project plans an update; reconcile then reports it.
|
|
297
|
+
const current = yield* absentAsUndefined(production(news.projectId));
|
|
298
|
+
return current?.name === output.name ? undefined : ({ action: "update" } as const);
|
|
299
|
+
}
|
|
300
|
+
// A preview expires, and anyone can delete a deployment. Reconcile
|
|
301
|
+
// then finds or creates it again, so the stack never hands out the
|
|
302
|
+
// URL of a deployment that is gone.
|
|
303
|
+
const live = yield* getByName(output.name);
|
|
304
|
+
return live === undefined ? ({ action: "update" } as const) : undefined;
|
|
305
|
+
}),
|
|
306
|
+
|
|
307
|
+
read: Effect.fn(function* ({ olds, output }) {
|
|
308
|
+
if (output?.type === "prod") {
|
|
309
|
+
// 404: the project is gone, and so is its production deployment.
|
|
310
|
+
return yield* absentAsUndefined(production(output.projectId));
|
|
311
|
+
}
|
|
312
|
+
if (output !== undefined) {
|
|
313
|
+
const live = yield* getByName(output.name);
|
|
314
|
+
return live === undefined
|
|
315
|
+
? undefined
|
|
316
|
+
: attributesOf(live, output.type, output.previewName);
|
|
317
|
+
}
|
|
318
|
+
const found = yield* lookup(olds);
|
|
319
|
+
if (found === undefined) return undefined;
|
|
320
|
+
// The production deployment comes with the project, and this
|
|
321
|
+
// resource never changes or deletes it, so it is ours to read.
|
|
322
|
+
// A dev or preview deployment that is not in state is someone
|
|
323
|
+
// else's until the user passes --adopt.
|
|
324
|
+
return olds.type === "prod" ? found : Unowned(found);
|
|
325
|
+
}),
|
|
326
|
+
|
|
327
|
+
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
328
|
+
// Also here: with an unresolved prop in plan, diff could not check.
|
|
329
|
+
if (output !== undefined) yield* checkIdentity(output, news);
|
|
330
|
+
if (news.type === "prod") {
|
|
331
|
+
// Always the current default, also when another production
|
|
332
|
+
// deployment became the default after the last deploy.
|
|
333
|
+
const current = yield* production(news.projectId);
|
|
334
|
+
if (current === undefined) {
|
|
335
|
+
return yield* new ProductionDeploymentNotFound({ projectId: news.projectId });
|
|
336
|
+
}
|
|
337
|
+
return current;
|
|
338
|
+
}
|
|
339
|
+
if (output !== undefined) {
|
|
340
|
+
const live = yield* getByName(output.name);
|
|
341
|
+
if (live !== undefined) {
|
|
342
|
+
if (live.deploymentType !== output.type) {
|
|
343
|
+
return yield* new DeploymentIdentityChange({
|
|
344
|
+
deployment: live.name,
|
|
345
|
+
field: "type",
|
|
346
|
+
current: live.deploymentType,
|
|
347
|
+
requested: news.type,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const current = attributesOf(live, output.type, output.previewName);
|
|
351
|
+
// The live deployment can have moved to another project.
|
|
352
|
+
yield* checkIdentity(current, news);
|
|
353
|
+
return current;
|
|
354
|
+
}
|
|
355
|
+
// The deployment in state is gone, such as an expired preview.
|
|
356
|
+
// Find or create it below like a resource without state.
|
|
357
|
+
}
|
|
358
|
+
// Never create over an existing deployment: a preview create
|
|
359
|
+
// replaces the preview deployment with the same name. Another
|
|
360
|
+
// writer can still create one between this lookup and the create.
|
|
361
|
+
const existing = yield* lookup(news);
|
|
362
|
+
if (existing !== undefined) {
|
|
363
|
+
if (!(yield* shouldAdopt(fqn))) {
|
|
364
|
+
return yield* new OwnedBySomeoneElse({
|
|
365
|
+
message: `Project ${news.projectId} already has ${news.type} deployment ${existing.name} named "${news.name}". Re-run with --adopt to take it over, or use another name.`,
|
|
366
|
+
resourceType: Deployment.Type,
|
|
367
|
+
physicalName: existing.name,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return existing;
|
|
371
|
+
}
|
|
372
|
+
const created = yield* api.request("create deployment", (client) =>
|
|
373
|
+
client.POST("/projects/{project_id}/create_deployment", {
|
|
374
|
+
params: { path: { project_id: news.projectId } },
|
|
375
|
+
body: { type: news.type, reference: news.name },
|
|
376
|
+
}),
|
|
377
|
+
);
|
|
378
|
+
return attributesOf(
|
|
379
|
+
Schema.decodeUnknownSync(DeploymentRow)(created),
|
|
380
|
+
news.type,
|
|
381
|
+
news.name,
|
|
382
|
+
);
|
|
383
|
+
}),
|
|
384
|
+
|
|
385
|
+
// Runs only with RemovalPolicy.destroy(): the default policy is `retain`.
|
|
386
|
+
delete: Effect.fn(function* ({ output }) {
|
|
387
|
+
const keep = Effect.logWarning(
|
|
388
|
+
`Convex.Deployment does not delete production deployment ${output.name}: it belongs to project ${output.projectId}. To delete it, delete the project with RemovalPolicy.destroy() on Convex.Project.`,
|
|
389
|
+
);
|
|
390
|
+
if (output.type === "prod") return yield* keep;
|
|
391
|
+
// The type in state can be old: a dev or preview deployment can
|
|
392
|
+
// become a production deployment in Convex. The live type decides.
|
|
393
|
+
const live = yield* getByName(output.name);
|
|
394
|
+
// 404: an earlier attempt, the expiry, or someone else deleted it.
|
|
395
|
+
if (live === undefined) return;
|
|
396
|
+
if (live.deploymentType === "prod") return yield* keep;
|
|
397
|
+
yield* absentAsUndefined(
|
|
398
|
+
retryIdempotentWrite(
|
|
399
|
+
api.requestVoid("delete deployment", (client) =>
|
|
400
|
+
client.POST("/deployments/{deployment_name}/delete", {
|
|
401
|
+
params: { path: { deployment_name: output.name } },
|
|
402
|
+
}),
|
|
403
|
+
),
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
}),
|
|
407
|
+
};
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
ManagementApi,
|
|
34
34
|
type ManagementApiService,
|
|
35
35
|
retryIdempotentWrite,
|
|
36
|
+
settle,
|
|
36
37
|
settleVoid,
|
|
37
38
|
} from "./ManagementApi.ts";
|
|
38
39
|
import type { Providers } from "./Providers.ts";
|
|
@@ -49,26 +50,40 @@ export const findVariable = (
|
|
|
49
50
|
Object.hasOwn(list.environmentVariables, name) ? { deployment, name } : undefined;
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* One attempt to set
|
|
53
|
-
* deployment answers 200 with an empty body, so this reads no
|
|
53
|
+
* One attempt to set variables, or to remove those whose `value` is null, in
|
|
54
|
+
* one call. The deployment answers 200 with an empty body, so this reads no
|
|
55
|
+
* response data.
|
|
54
56
|
*/
|
|
55
|
-
const
|
|
57
|
+
export const writeVariables = (
|
|
56
58
|
deployment: string,
|
|
57
59
|
deployKey: Redacted.Redacted<string>,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
changes: ReadonlyArray<{
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly value: string | Redacted.Redacted<string> | null;
|
|
63
|
+
}>,
|
|
60
64
|
) =>
|
|
61
65
|
settleVoid("update environment variables", () =>
|
|
62
66
|
createDeploymentClient(deployment, Redacted.value(deployKey)).POST(
|
|
63
67
|
"/update_environment_variables",
|
|
64
68
|
{
|
|
65
69
|
body: {
|
|
66
|
-
changes:
|
|
70
|
+
changes: changes.map(({ name, value }) => ({
|
|
71
|
+
name,
|
|
72
|
+
value: Redacted.isRedacted(value) ? Redacted.value(value) : value,
|
|
73
|
+
})),
|
|
67
74
|
},
|
|
68
75
|
},
|
|
69
76
|
),
|
|
70
77
|
);
|
|
71
78
|
|
|
79
|
+
/** One attempt to set one variable, or to remove it when `value` is null. */
|
|
80
|
+
const writeVariable = (
|
|
81
|
+
deployment: string,
|
|
82
|
+
deployKey: Redacted.Redacted<string>,
|
|
83
|
+
name: string,
|
|
84
|
+
value: string | Redacted.Redacted<string> | null,
|
|
85
|
+
) => writeVariables(deployment, deployKey, [{ name, value }]);
|
|
86
|
+
|
|
72
87
|
/**
|
|
73
88
|
* Sets one variable, or removes it when `value` is null. The call sets a
|
|
74
89
|
* value, so it runs again after a write conflict or a 5xx answer, for example
|
|
@@ -87,6 +102,18 @@ export const EnvironmentVariableList = Schema.Struct({
|
|
|
87
102
|
environmentVariables: Schema.Record(Schema.String, Schema.String),
|
|
88
103
|
});
|
|
89
104
|
|
|
105
|
+
/** Every variable of the deployment with its value. Fails with a 404 when the deployment is gone. */
|
|
106
|
+
export const listVariables = (deployment: string, deployKey: Redacted.Redacted<string>) =>
|
|
107
|
+
settle("list environment variables", () =>
|
|
108
|
+
createDeploymentClient(deployment, Redacted.value(deployKey)).GET(
|
|
109
|
+
"/list_environment_variables",
|
|
110
|
+
),
|
|
111
|
+
).pipe(
|
|
112
|
+
Effect.map(
|
|
113
|
+
(body) => Schema.decodeUnknownSync(EnvironmentVariableList)(body).environmentVariables,
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
|
|
90
117
|
/** The variable's identity, or undefined when the variable or the deployment is absent. */
|
|
91
118
|
const readVariable = (
|
|
92
119
|
api: ManagementApiService,
|
package/src/Providers.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { CommandExecutorLive } from "alchemy/Command";
|
|
4
4
|
import * as Provider from "alchemy/Provider";
|
|
5
5
|
import * as Layer from "effect/Layer";
|
|
6
|
+
import { Code, CodeProvider } from "./Code.ts";
|
|
6
7
|
import { type Credentials, fromConfig } from "./Credentials.ts";
|
|
7
8
|
import {
|
|
8
9
|
DefaultEnvironmentVariable,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
} from "./DefaultEnvironmentVariable.ts";
|
|
11
12
|
import { Deploy, DeployProvider } from "./Deploy.ts";
|
|
12
13
|
import { DeployKey, DeployKeyProvider } from "./DeployKey.ts";
|
|
14
|
+
import { Deployment, DeploymentProvider } from "./Deployment.ts";
|
|
13
15
|
import { EnvironmentVariable, EnvironmentVariableProvider } from "./EnvironmentVariable.ts";
|
|
14
16
|
import { ManagementApiLive } from "./ManagementApi.ts";
|
|
15
17
|
import { PreviewDeployKey, PreviewDeployKeyProvider } from "./PreviewDeployKey.ts";
|
|
@@ -26,26 +28,32 @@ export const providers = (credentials: Layer.Layer<Credentials> = fromConfig())
|
|
|
26
28
|
Providers,
|
|
27
29
|
Provider.collection([
|
|
28
30
|
Project,
|
|
31
|
+
Deployment,
|
|
29
32
|
DeployKey,
|
|
30
33
|
PreviewDeployKey,
|
|
31
34
|
EnvironmentVariable,
|
|
32
35
|
DefaultEnvironmentVariable,
|
|
36
|
+
Code,
|
|
33
37
|
Deploy,
|
|
34
38
|
]),
|
|
35
39
|
).pipe(
|
|
36
40
|
Layer.provide(
|
|
37
41
|
Layer.mergeAll(
|
|
38
42
|
ProjectProvider(),
|
|
43
|
+
DeploymentProvider(),
|
|
39
44
|
DeployKeyProvider(),
|
|
40
45
|
PreviewDeployKeyProvider(),
|
|
41
46
|
EnvironmentVariableProvider(),
|
|
42
47
|
DefaultEnvironmentVariableProvider(),
|
|
48
|
+
CodeProvider(),
|
|
43
49
|
DeployProvider(),
|
|
44
50
|
),
|
|
45
51
|
),
|
|
46
52
|
Layer.provide(ManagementApiLive()),
|
|
47
53
|
Layer.provide(credentials),
|
|
48
|
-
// Convex.Deploy runs the Convex CLI through Alchemy's
|
|
54
|
+
// The deprecated Convex.Deploy runs the Convex CLI through Alchemy's
|
|
55
|
+
// command executor. Convex.Code spawns it itself, so that its output
|
|
56
|
+
// stays out of the logs.
|
|
49
57
|
Layer.provide(CommandExecutorLive()),
|
|
50
58
|
Layer.orDie,
|
|
51
59
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
// @samebase/alchemy-convex: Convex resources for Alchemy v2.
|
|
2
|
+
export {
|
|
3
|
+
Code,
|
|
4
|
+
type CodeAttributes,
|
|
5
|
+
type CodeDeployment,
|
|
6
|
+
type CodeProps,
|
|
7
|
+
CodePushFailed,
|
|
8
|
+
ConvexCliNotFound,
|
|
9
|
+
DeployKeyMismatch,
|
|
10
|
+
EnvironmentNeedsDeployKey,
|
|
11
|
+
parseDeployOutput,
|
|
12
|
+
PushTargetMismatch,
|
|
13
|
+
} from "./Code.ts";
|
|
2
14
|
export {
|
|
3
15
|
ACCESS_TOKEN_ENV,
|
|
4
16
|
Credentials,
|
|
@@ -19,7 +31,6 @@ export {
|
|
|
19
31
|
deployArgs,
|
|
20
32
|
DeployOutputError,
|
|
21
33
|
type DeployProps,
|
|
22
|
-
parseDeployOutput,
|
|
23
34
|
} from "./Deploy.ts";
|
|
24
35
|
export {
|
|
25
36
|
DeployKey,
|
|
@@ -29,6 +40,14 @@ export {
|
|
|
29
40
|
type DeployKeyProps,
|
|
30
41
|
DeployKeyRecoveryRequired,
|
|
31
42
|
} from "./DeployKey.ts";
|
|
43
|
+
export {
|
|
44
|
+
AmbiguousDeployment,
|
|
45
|
+
Deployment,
|
|
46
|
+
type DeploymentAttributes,
|
|
47
|
+
DeploymentIdentityChange,
|
|
48
|
+
type DeploymentProps,
|
|
49
|
+
ProductionDeploymentNotFound,
|
|
50
|
+
} from "./Deployment.ts";
|
|
32
51
|
export {
|
|
33
52
|
EnvironmentVariable,
|
|
34
53
|
type EnvironmentVariableAttributes,
|