@samebase/alchemy-convex 0.4.1 → 0.6.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 +26 -0
- package/lib/index.d.ts +16 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +334 -71
- package/lib/index.js.map +1 -1
- package/package.json +3 -1
- package/src/Deployment.ts +9 -2
- package/src/Project.ts +125 -88
- package/src/Push.ts +74 -46
- package/src/State.ts +435 -0
- package/src/index.ts +1 -0
- package/src/store/convex/_generated/api.d.ts +51 -0
- package/src/store/convex/_generated/api.js +23 -0
- package/src/store/convex/_generated/dataModel.d.ts +60 -0
- package/src/store/convex/_generated/server.d.ts +162 -0
- package/src/store/convex/_generated/server.js +101 -0
- package/src/store/convex/schema.ts +19 -0
- package/src/store/convex/state.ts +219 -0
- package/src/store/convex/version.ts +6 -0
- package/src/store/package.json +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@samebase/alchemy-convex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The Convex plane for Alchemy v2: projects, deployments, environment variables, and the push of the functions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"@types/node": "24.19.2",
|
|
58
58
|
"alchemy": "2.0.0-beta.83",
|
|
59
59
|
"convex": "1.46.0",
|
|
60
|
+
"convex-test": "0.0.60",
|
|
60
61
|
"effect": "4.0.4",
|
|
61
62
|
"publint": "0.3.25",
|
|
62
63
|
"typescript": "6.0.3",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"peerDependencies": {
|
|
67
68
|
"@effect/platform-node": "^4.0.0",
|
|
68
69
|
"alchemy": "2.0.0-beta.83",
|
|
70
|
+
"convex": "^1.46.0",
|
|
69
71
|
"effect": "^4.0.0"
|
|
70
72
|
},
|
|
71
73
|
"engines": {
|
package/src/Deployment.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
type EnvValue,
|
|
70
70
|
keyTargetOf,
|
|
71
71
|
push,
|
|
72
|
+
PUSH_KEY_ACTIONS,
|
|
72
73
|
pushHash,
|
|
73
74
|
type Source,
|
|
74
75
|
withPushKey,
|
|
@@ -681,8 +682,14 @@ export const DeploymentProvider = () =>
|
|
|
681
682
|
? push(spawner, access.key, { ...options, previewName: undefined })
|
|
682
683
|
: access.kind === "previewKey"
|
|
683
684
|
? push(spawner, access.key, { ...options, previewName: target.previewName })
|
|
684
|
-
: withPushKey(
|
|
685
|
-
|
|
685
|
+
: withPushKey(
|
|
686
|
+
access.api,
|
|
687
|
+
target.name,
|
|
688
|
+
// A push command gets every action; `convex deploy` only its own.
|
|
689
|
+
typeof news.source === "object" && news.source.command !== undefined
|
|
690
|
+
? undefined
|
|
691
|
+
: PUSH_KEY_ACTIONS,
|
|
692
|
+
(key) => push(spawner, key, { ...options, previewName: undefined }),
|
|
686
693
|
);
|
|
687
694
|
yield* pushed.pipe(
|
|
688
695
|
// A deployment that this reconcile created and could not push to
|
package/src/Project.ts
CHANGED
|
@@ -25,7 +25,12 @@ import * as Effect from "effect/Effect";
|
|
|
25
25
|
import * as Schema from "effect/Schema";
|
|
26
26
|
import type { components } from "@convex-dev/platform/managementApi";
|
|
27
27
|
import { shouldAdopt } from "./Adoption.ts";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
absentAsUndefined,
|
|
30
|
+
ConvexApiError,
|
|
31
|
+
ManagementApi,
|
|
32
|
+
type ManagementApiService,
|
|
33
|
+
} from "./ManagementApi.ts";
|
|
29
34
|
import type { Providers } from "./Providers.ts";
|
|
30
35
|
|
|
31
36
|
/** Region names the Management API accepts, from the generated schema. */
|
|
@@ -159,6 +164,8 @@ const DeploymentRow = Schema.Struct({
|
|
|
159
164
|
deploymentType: Schema.String,
|
|
160
165
|
isDefault: Schema.Boolean,
|
|
161
166
|
deploymentUrl: Schema.String,
|
|
167
|
+
/** Absent or null until the first push. */
|
|
168
|
+
lastDeployTime: Schema.optionalKey(Schema.NullOr(Schema.Number)),
|
|
162
169
|
});
|
|
163
170
|
const DeploymentRows = Schema.Array(DeploymentRow);
|
|
164
171
|
|
|
@@ -202,29 +209,123 @@ const isTeamOf = (details: ProjectDetails, team: string | number) =>
|
|
|
202
209
|
? details.teamId === Number(team)
|
|
203
210
|
: details.teamSlug === team;
|
|
204
211
|
|
|
212
|
+
/** The row of the project's default production deployment, or undefined. */
|
|
213
|
+
export const prodDeploymentOf = (api: ManagementApiService, projectId: number) =>
|
|
214
|
+
api
|
|
215
|
+
.request("list deployments", (client) =>
|
|
216
|
+
client.GET("/projects/{project_id}/list_deployments", {
|
|
217
|
+
params: { path: { project_id: projectId } },
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
.pipe(
|
|
221
|
+
Effect.map((rows) =>
|
|
222
|
+
Schema.decodeUnknownSync(DeploymentRows)(rows).find(
|
|
223
|
+
(row) => row.deploymentType === "prod" && row.isDefault,
|
|
224
|
+
),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
/** GET /projects/{id}, or undefined after a 404. */
|
|
229
|
+
export const getProjectById = (api: ManagementApiService, projectId: number) =>
|
|
230
|
+
absentAsUndefined(
|
|
231
|
+
api.request("get project", (client) =>
|
|
232
|
+
client.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } }),
|
|
233
|
+
),
|
|
234
|
+
).pipe(
|
|
235
|
+
Effect.map((body) =>
|
|
236
|
+
body === undefined ? undefined : Schema.decodeUnknownSync(ProjectDetails)(body),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Every project in the team with exactly this name, in the order of the
|
|
242
|
+
* listing. Reads every page. `q` narrows the listing on the server, and the
|
|
243
|
+
* exact match here decides. Pattern from Confect
|
|
244
|
+
* `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
|
|
245
|
+
* (https://github.com/rjdellecese/confect, ISC license).
|
|
246
|
+
*/
|
|
247
|
+
export const listProjectsByName = (api: ManagementApiService, teamId: number, name: string) =>
|
|
248
|
+
Effect.gen(function* () {
|
|
249
|
+
const matches: ProjectDetails[] = [];
|
|
250
|
+
const seenCursors = new Set<string>();
|
|
251
|
+
let cursor: string | undefined;
|
|
252
|
+
while (true) {
|
|
253
|
+
const body = yield* api.request("list projects", (client) =>
|
|
254
|
+
client.GET("/teams/{team_id}/projects", {
|
|
255
|
+
params: {
|
|
256
|
+
path: { team_id: teamId },
|
|
257
|
+
query: { q: name, ...(cursor === undefined ? {} : { cursor }) },
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
const page = Schema.decodeUnknownSync(ProjectPage)(body);
|
|
262
|
+
matches.push(...page.items.filter((row) => row.name === name));
|
|
263
|
+
if (!page.pagination.hasMore) break;
|
|
264
|
+
const next = page.pagination.nextCursor;
|
|
265
|
+
if (next === undefined || next === null || seenCursors.has(next)) {
|
|
266
|
+
return yield* new ConvexApiError({
|
|
267
|
+
operation: "list projects",
|
|
268
|
+
status: 0,
|
|
269
|
+
code: "InvalidPagination",
|
|
270
|
+
message: `The project listing of team ${teamId} reports more pages but gives no new cursor. Convex.Project stops so that it does not miss a project named "${name}".`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
seenCursors.add(next);
|
|
274
|
+
cursor = next;
|
|
275
|
+
}
|
|
276
|
+
return matches;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
/** The one project in the team with exactly this name, or undefined. More than one is an error. */
|
|
280
|
+
export const findProjectByName = (api: ManagementApiService, teamId: number, name: string) =>
|
|
281
|
+
listProjectsByName(api, teamId, name).pipe(
|
|
282
|
+
Effect.flatMap((matches) =>
|
|
283
|
+
matches.length > 1
|
|
284
|
+
? new AmbiguousProject({ teamId, name, projectIds: matches.map((row) => row.id) })
|
|
285
|
+
: Effect.succeed(matches[0]),
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* POST /teams/{team_id}/create_project, then GET /projects/{id}. The identity
|
|
291
|
+
* comes from the create response, never from a name lookup.
|
|
292
|
+
*/
|
|
293
|
+
export const createProject = (
|
|
294
|
+
api: ManagementApiService,
|
|
295
|
+
teamId: number,
|
|
296
|
+
props: {
|
|
297
|
+
readonly name: string;
|
|
298
|
+
readonly deploymentType: "prod" | "dev";
|
|
299
|
+
readonly deploymentRegion: DeploymentRegion | undefined;
|
|
300
|
+
},
|
|
301
|
+
) =>
|
|
302
|
+
Effect.gen(function* () {
|
|
303
|
+
const created = yield* api.request("create project", (client) =>
|
|
304
|
+
client.POST("/teams/{team_id}/create_project", {
|
|
305
|
+
params: { path: { team_id: teamId } },
|
|
306
|
+
body: {
|
|
307
|
+
projectName: props.name,
|
|
308
|
+
deploymentType: props.deploymentType,
|
|
309
|
+
...(props.deploymentRegion === undefined
|
|
310
|
+
? {}
|
|
311
|
+
: { deploymentRegion: props.deploymentRegion }),
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
);
|
|
315
|
+
const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
|
|
316
|
+
const details = yield* getProjectById(api, projectId);
|
|
317
|
+
if (details === undefined) return yield* new CreatedProjectNotFound({ projectId });
|
|
318
|
+
return details;
|
|
319
|
+
});
|
|
320
|
+
|
|
205
321
|
export const ProjectProvider = () =>
|
|
206
322
|
Provider.effect(
|
|
207
323
|
Project,
|
|
208
324
|
Effect.gen(function* () {
|
|
209
325
|
const api = yield* ManagementApi;
|
|
210
326
|
|
|
211
|
-
const prodDeployment = (projectId: number) =>
|
|
212
|
-
api
|
|
213
|
-
.request("list deployments", (client) =>
|
|
214
|
-
client.GET("/projects/{project_id}/list_deployments", {
|
|
215
|
-
params: { path: { project_id: projectId } },
|
|
216
|
-
}),
|
|
217
|
-
)
|
|
218
|
-
.pipe(
|
|
219
|
-
Effect.map((rows) =>
|
|
220
|
-
Schema.decodeUnknownSync(DeploymentRows)(rows).find(
|
|
221
|
-
(row) => row.deploymentType === "prod" && row.isDefault,
|
|
222
|
-
),
|
|
223
|
-
),
|
|
224
|
-
);
|
|
225
|
-
|
|
226
327
|
const attributesOf = (details: ProjectDetails) =>
|
|
227
|
-
|
|
328
|
+
prodDeploymentOf(api, details.id).pipe(
|
|
228
329
|
Effect.map((prod): ProjectAttributes => ({
|
|
229
330
|
projectId: details.id,
|
|
230
331
|
slug: details.slug,
|
|
@@ -235,16 +336,7 @@ export const ProjectProvider = () =>
|
|
|
235
336
|
})),
|
|
236
337
|
);
|
|
237
338
|
|
|
238
|
-
const getById = (projectId: number) =>
|
|
239
|
-
absentAsUndefined(
|
|
240
|
-
api.request("get project", (client) =>
|
|
241
|
-
client.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } }),
|
|
242
|
-
),
|
|
243
|
-
).pipe(
|
|
244
|
-
Effect.map((body) =>
|
|
245
|
-
body === undefined ? undefined : Schema.decodeUnknownSync(ProjectDetails)(body),
|
|
246
|
-
),
|
|
247
|
-
);
|
|
339
|
+
const getById = (projectId: number) => getProjectById(api, projectId);
|
|
248
340
|
|
|
249
341
|
/** The project with this slug in the team, or undefined. Official endpoint; the team can be a slug. */
|
|
250
342
|
const getBySlug = (team: string | number, slug: string) =>
|
|
@@ -260,51 +352,7 @@ export const ProjectProvider = () =>
|
|
|
260
352
|
),
|
|
261
353
|
);
|
|
262
354
|
|
|
263
|
-
|
|
264
|
-
* The one project in the team with exactly this name, or undefined.
|
|
265
|
-
* Reads every page. `q` narrows the listing on the server, and the
|
|
266
|
-
* exact match here decides. Pattern from Confect
|
|
267
|
-
* `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
|
|
268
|
-
* (https://github.com/rjdellecese/confect, ISC license).
|
|
269
|
-
*/
|
|
270
|
-
const findByName = (teamId: number, name: string) =>
|
|
271
|
-
Effect.gen(function* () {
|
|
272
|
-
const matches: ProjectDetails[] = [];
|
|
273
|
-
const seenCursors = new Set<string>();
|
|
274
|
-
let cursor: string | undefined;
|
|
275
|
-
while (true) {
|
|
276
|
-
const body = yield* api.request("list projects", (client) =>
|
|
277
|
-
client.GET("/teams/{team_id}/projects", {
|
|
278
|
-
params: {
|
|
279
|
-
path: { team_id: teamId },
|
|
280
|
-
query: { q: name, ...(cursor === undefined ? {} : { cursor }) },
|
|
281
|
-
},
|
|
282
|
-
}),
|
|
283
|
-
);
|
|
284
|
-
const page = Schema.decodeUnknownSync(ProjectPage)(body);
|
|
285
|
-
matches.push(...page.items.filter((row) => row.name === name));
|
|
286
|
-
if (!page.pagination.hasMore) break;
|
|
287
|
-
const next = page.pagination.nextCursor;
|
|
288
|
-
if (next === undefined || next === null || seenCursors.has(next)) {
|
|
289
|
-
return yield* new ConvexApiError({
|
|
290
|
-
operation: "list projects",
|
|
291
|
-
status: 0,
|
|
292
|
-
code: "InvalidPagination",
|
|
293
|
-
message: `The project listing of team ${teamId} reports more pages but gives no new cursor. Convex.Project stops so that it does not miss a project named "${name}".`,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
seenCursors.add(next);
|
|
297
|
-
cursor = next;
|
|
298
|
-
}
|
|
299
|
-
if (matches.length > 1) {
|
|
300
|
-
return yield* new AmbiguousProject({
|
|
301
|
-
teamId,
|
|
302
|
-
name,
|
|
303
|
-
projectIds: matches.map((row) => row.id),
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
return matches[0];
|
|
307
|
-
});
|
|
355
|
+
const findByName = (teamId: number, name: string) => findProjectByName(api, teamId, name);
|
|
308
356
|
|
|
309
357
|
/**
|
|
310
358
|
* Brings an existing project to the desired name. The team cannot
|
|
@@ -458,22 +506,11 @@ export const ProjectProvider = () =>
|
|
|
458
506
|
return yield* converge(existing, teamId, news.name);
|
|
459
507
|
}
|
|
460
508
|
}
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
deploymentType: news.deploymentType ?? "prod",
|
|
467
|
-
...(news.deploymentRegion === undefined
|
|
468
|
-
? {}
|
|
469
|
-
: { deploymentRegion: news.deploymentRegion }),
|
|
470
|
-
},
|
|
471
|
-
}),
|
|
472
|
-
);
|
|
473
|
-
// Identity comes from the create response, never from a name lookup.
|
|
474
|
-
const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
|
|
475
|
-
const details = yield* getById(projectId);
|
|
476
|
-
if (details === undefined) return yield* new CreatedProjectNotFound({ projectId });
|
|
509
|
+
const details = yield* createProject(api, teamId, {
|
|
510
|
+
name: news.name,
|
|
511
|
+
deploymentType: news.deploymentType ?? "prod",
|
|
512
|
+
deploymentRegion: news.deploymentRegion,
|
|
513
|
+
});
|
|
477
514
|
return yield* converge(details, teamId, news.name);
|
|
478
515
|
}),
|
|
479
516
|
|
package/src/Push.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { tmpdir } from "node:os";
|
|
|
27
27
|
import { dirname, join, relative, resolve } from "node:path";
|
|
28
28
|
import { stripVTControlCharacters } from "node:util";
|
|
29
29
|
import { createDeploymentClient } from "@convex-dev/platform";
|
|
30
|
+
import type { paths } from "@convex-dev/platform/managementApi";
|
|
30
31
|
import { hashDirectory } from "alchemy/Command/Memo";
|
|
31
32
|
import * as Clock from "effect/Clock";
|
|
32
33
|
import * as Effect from "effect/Effect";
|
|
@@ -80,12 +81,19 @@ const sourceOf = (source: Source) =>
|
|
|
80
81
|
/** Lifetime of a push key. Long enough for a slow push with index backfills. */
|
|
81
82
|
export const PUSH_KEY_LIFETIME_MS = 2 * 60 * 60 * 1000;
|
|
82
83
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
84
|
+
/** An action of a deploy key, such as `deployment:deploy`, from the generated schema. */
|
|
85
|
+
export type DeployKeyAction = NonNullable<
|
|
86
|
+
NonNullable<
|
|
87
|
+
paths["/deployments/{deployment_name}/create_deploy_key"]["post"]["requestBody"]
|
|
88
|
+
>["content"]["application/json"]["allowedActions"]
|
|
89
|
+
>[number];
|
|
90
|
+
|
|
91
|
+
/** The only actions a key for `convex deploy` gets. */
|
|
92
|
+
export const PUSH_KEY_ACTIONS = [
|
|
85
93
|
"deployment:deploy",
|
|
86
94
|
"deployment:env:view",
|
|
87
95
|
"deployment:env:write",
|
|
88
|
-
] as const;
|
|
96
|
+
] as const satisfies readonly DeployKeyAction[];
|
|
89
97
|
|
|
90
98
|
/** POST /deployments/{deployment_name}/create_deploy_key */
|
|
91
99
|
const CreatedDeployKey = Schema.Struct({ deployKey: Schema.String });
|
|
@@ -271,7 +279,7 @@ const CliManifest = Schema.Struct({
|
|
|
271
279
|
});
|
|
272
280
|
|
|
273
281
|
/** `[node, <convex bin>]` for the `convex` package that `cwd` resolves. Never downloads. */
|
|
274
|
-
const installedCli = (cwd: string) =>
|
|
282
|
+
export const installedCli = (cwd: string) =>
|
|
275
283
|
Effect.tryPromise({
|
|
276
284
|
try: async (): Promise<readonly [string, ...string[]]> => {
|
|
277
285
|
const manifestPath = createRequire(join(cwd, "package.json")).resolve("convex/package.json");
|
|
@@ -360,58 +368,78 @@ const cliEnvironment = (home: string): Record<string, string | undefined> => ({
|
|
|
360
368
|
XDG_CONFIG_HOME: home,
|
|
361
369
|
});
|
|
362
370
|
|
|
371
|
+
/**
|
|
372
|
+
* A deploy key of `deployment` that expires after PUSH_KEY_LIFETIME_MS, with
|
|
373
|
+
* only `allowedActions`, or every action when that is undefined. Delete it
|
|
374
|
+
* with {@link deleteDeployKey} when the work is done.
|
|
375
|
+
*/
|
|
376
|
+
export const createDeployKey = (
|
|
377
|
+
api: ManagementApiService,
|
|
378
|
+
deployment: string,
|
|
379
|
+
name: string,
|
|
380
|
+
allowedActions: readonly DeployKeyAction[] | undefined,
|
|
381
|
+
) =>
|
|
382
|
+
Effect.gen(function* () {
|
|
383
|
+
const now = yield* Clock.currentTimeMillis;
|
|
384
|
+
const created = yield* api.request("create deploy key", (client) =>
|
|
385
|
+
client.POST("/deployments/{deployment_name}/create_deploy_key", {
|
|
386
|
+
params: { path: { deployment_name: deployment } },
|
|
387
|
+
body: {
|
|
388
|
+
name,
|
|
389
|
+
...(allowedActions === undefined ? {} : { allowedActions: [...allowedActions] }),
|
|
390
|
+
expiresAt: now + PUSH_KEY_LIFETIME_MS,
|
|
391
|
+
},
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
return Redacted.make(Schema.decodeUnknownSync(CreatedDeployKey)(created).deployKey);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Deletes a key that {@link createDeployKey} made. The delete is skipped for
|
|
399
|
+
* a key that is the OAuth credential. A failed delete is logged: the key
|
|
400
|
+
* expires anyway.
|
|
401
|
+
*/
|
|
402
|
+
export const deleteDeployKey = (
|
|
403
|
+
api: ManagementApiService,
|
|
404
|
+
deployment: string,
|
|
405
|
+
key: Redacted.Redacted<string>,
|
|
406
|
+
) =>
|
|
407
|
+
Effect.gen(function* () {
|
|
408
|
+
if (yield* api.isCredentialToken(Redacted.value(key))) return;
|
|
409
|
+
yield* absentAsUndefined(
|
|
410
|
+
retryIdempotentWrite(
|
|
411
|
+
api.requestVoid("delete deploy key", (client) =>
|
|
412
|
+
client.POST("/deployments/{deployment_name}/delete_deploy_key", {
|
|
413
|
+
params: { path: { deployment_name: deployment } },
|
|
414
|
+
body: { id: Redacted.value(key) },
|
|
415
|
+
}),
|
|
416
|
+
),
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
}).pipe(
|
|
420
|
+
Effect.catch((error) =>
|
|
421
|
+
Effect.logWarning(
|
|
422
|
+
`Convex.Deployment could not delete its key on ${deployment} (${error.message}). The key expires on its own within ${PUSH_KEY_LIFETIME_MS / 60_000} minutes.`,
|
|
423
|
+
),
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
|
|
363
427
|
/**
|
|
364
428
|
* Creates a deploy key for one push, runs `use` with it, and deletes it
|
|
365
|
-
* afterwards
|
|
366
|
-
*
|
|
367
|
-
*
|
|
429
|
+
* afterwards, also after a failure. `allowedActions` is PUSH_KEY_ACTIONS for
|
|
430
|
+
* `convex deploy`, or undefined for a push command, which can do more than a
|
|
431
|
+
* push, such as the uploads of convex-hosting through `convex run`.
|
|
368
432
|
*/
|
|
369
433
|
export const withPushKey = <A, E, R>(
|
|
370
434
|
api: ManagementApiService,
|
|
371
435
|
deployment: string,
|
|
372
|
-
|
|
436
|
+
allowedActions: readonly DeployKeyAction[] | undefined,
|
|
373
437
|
use: (key: Redacted.Redacted<string>) => Effect.Effect<A, E, R>,
|
|
374
438
|
) =>
|
|
375
439
|
Effect.acquireUseRelease(
|
|
376
|
-
|
|
377
|
-
const now = yield* Clock.currentTimeMillis;
|
|
378
|
-
const created = yield* api.request("create deploy key", (client) =>
|
|
379
|
-
client.POST("/deployments/{deployment_name}/create_deploy_key", {
|
|
380
|
-
params: { path: { deployment_name: deployment } },
|
|
381
|
-
body: {
|
|
382
|
-
name: "alchemy-push",
|
|
383
|
-
// A push command can do more than a push, such as the uploads of
|
|
384
|
-
// convex-hosting through `convex run`, so its key has every action.
|
|
385
|
-
...(source === undefined || sourceOf(source).command === undefined
|
|
386
|
-
? { allowedActions: [...PUSH_KEY_ACTIONS] }
|
|
387
|
-
: {}),
|
|
388
|
-
expiresAt: now + PUSH_KEY_LIFETIME_MS,
|
|
389
|
-
},
|
|
390
|
-
}),
|
|
391
|
-
);
|
|
392
|
-
return Redacted.make(Schema.decodeUnknownSync(CreatedDeployKey)(created).deployKey);
|
|
393
|
-
}),
|
|
440
|
+
createDeployKey(api, deployment, "alchemy-push", allowedActions),
|
|
394
441
|
use,
|
|
395
|
-
(key) =>
|
|
396
|
-
Effect.gen(function* () {
|
|
397
|
-
if (yield* api.isCredentialToken(Redacted.value(key))) return;
|
|
398
|
-
yield* absentAsUndefined(
|
|
399
|
-
retryIdempotentWrite(
|
|
400
|
-
api.requestVoid("delete deploy key", (client) =>
|
|
401
|
-
client.POST("/deployments/{deployment_name}/delete_deploy_key", {
|
|
402
|
-
params: { path: { deployment_name: deployment } },
|
|
403
|
-
body: { id: Redacted.value(key) },
|
|
404
|
-
}),
|
|
405
|
-
),
|
|
406
|
-
),
|
|
407
|
-
);
|
|
408
|
-
}).pipe(
|
|
409
|
-
Effect.catch((error) =>
|
|
410
|
-
Effect.logWarning(
|
|
411
|
-
`Convex.Deployment could not delete its push key on ${deployment} (${error.message}). The key expires on its own within ${PUSH_KEY_LIFETIME_MS / 60_000} minutes.`,
|
|
412
|
-
),
|
|
413
|
-
),
|
|
414
|
-
),
|
|
442
|
+
(key) => deleteDeployKey(api, deployment, key),
|
|
415
443
|
);
|
|
416
444
|
|
|
417
445
|
/** Every variable of the deployment with its value. */
|