@samebase/alchemy-convex 0.2.0 → 0.4.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/NOTICE +24 -0
- package/README.md +135 -164
- package/lib/index.d.ts +175 -185
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +800 -502
- package/lib/index.js.map +1 -1
- package/package.json +4 -3
- package/src/Credentials.ts +85 -27
- package/src/Deployment.ts +748 -0
- package/src/ManagementApi.ts +84 -64
- package/src/Project.ts +133 -16
- package/src/Providers.ts +6 -34
- package/src/Push.ts +646 -0
- package/src/index.ts +22 -35
- package/src/DefaultEnvironmentVariable.ts +0 -263
- package/src/Deploy.ts +0 -193
- package/src/DeployKey.ts +0 -332
- package/src/EnvironmentVariable.ts +0 -262
- package/src/PreviewDeployKey.ts +0 -153
package/lib/index.js
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
5
|
import * as Config from "effect/Config";
|
|
5
6
|
import * as Context from "effect/Context";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
import * as Layer from "effect/Layer";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
8
10
|
import * as Redacted from "effect/Redacted";
|
|
11
|
+
import * as Result from "effect/Result";
|
|
9
12
|
import * as Schema from "effect/Schema";
|
|
10
13
|
import { Resource } from "alchemy";
|
|
11
14
|
import { AdoptPolicy, OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
|
|
12
|
-
import {
|
|
15
|
+
import { isResolved } from "alchemy/Diff";
|
|
13
16
|
import * as Provider from "alchemy/Provider";
|
|
17
|
+
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
|
|
14
18
|
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
15
19
|
import { Stack } from "alchemy/Stack";
|
|
16
|
-
import * as Option from "effect/Option";
|
|
17
20
|
import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
|
|
18
21
|
import * as Schedule from "effect/Schedule";
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
19
23
|
import { stripVTControlCharacters } from "node:util";
|
|
20
|
-
import { CommandExecutor, CommandExecutorLive } from "alchemy/Command";
|
|
21
24
|
import { hashDirectory } from "alchemy/Command/Memo";
|
|
22
|
-
import
|
|
25
|
+
import * as Clock from "effect/Clock";
|
|
26
|
+
import * as ChildProcess from "effect/process/ChildProcess";
|
|
27
|
+
import * as Stream from "effect/Stream";
|
|
23
28
|
//#region src/Credentials.ts
|
|
24
29
|
var Credentials = class extends Context.Service()("@samebase/alchemy-convex/Credentials") {};
|
|
25
|
-
/** Environment variable read by {@link fromConfig}. */
|
|
30
|
+
/** Environment variable for the access token, read by {@link fromConfig}. */
|
|
26
31
|
const ACCESS_TOKEN_ENV = "CONVEX_ACCESS_TOKEN";
|
|
32
|
+
/** Environment variable for the deploy key, read by {@link fromConfig}. The Convex CLI reads the same name. */
|
|
33
|
+
const DEPLOY_KEY_ENV = "CONVEX_DEPLOY_KEY";
|
|
27
34
|
/** The Convex CLI stores its device login here after `npx convex login`. */
|
|
28
35
|
const convexCliConfigPath = () => join(homedir(), ".convex", "config.json");
|
|
29
36
|
const CliConfig = Schema.Struct({ accessToken: Schema.String });
|
|
37
|
+
var CredentialsError = class extends Schema.TaggedError()("CredentialsError", { reason: Schema.String }) {};
|
|
30
38
|
/**
|
|
31
39
|
* Token the Convex CLI saved on this machine. It works on the Management API
|
|
32
40
|
* for the teams the logged-in user belongs to. Fails when nobody is logged in.
|
|
@@ -35,7 +43,7 @@ const readConvexCliLogin = Effect.gen(function* () {
|
|
|
35
43
|
const path = convexCliConfigPath();
|
|
36
44
|
const text = yield* Effect.tryPromise({
|
|
37
45
|
try: () => readFile(path, "utf8"),
|
|
38
|
-
catch: () => new CredentialsError({ reason: `no Convex CLI login at ${path}
|
|
46
|
+
catch: () => new CredentialsError({ reason: `This step needs the Convex Management API. Set ${ACCESS_TOKEN_ENV} to a team access token, or run \`npx convex login\`. There is no Convex CLI login at ${path}.` })
|
|
39
47
|
});
|
|
40
48
|
const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(CliConfig))(text).pipe(Effect.mapError(() => new CredentialsError({ reason: `${path} has no accessToken` })));
|
|
41
49
|
return {
|
|
@@ -43,22 +51,35 @@ const readConvexCliLogin = Effect.gen(function* () {
|
|
|
43
51
|
source: "convex-cli-login"
|
|
44
52
|
};
|
|
45
53
|
});
|
|
46
|
-
var CredentialsError = class extends Schema.TaggedError()("CredentialsError", { reason: Schema.String }) {};
|
|
47
54
|
/**
|
|
48
|
-
* Default resolution
|
|
49
|
-
*
|
|
50
|
-
* machine
|
|
51
|
-
* nothing.
|
|
55
|
+
* Default resolution through the stack ConfigProvider (process env, `.env`,
|
|
56
|
+
* stack secrets): `CONVEX_ACCESS_TOKEN`, then the Convex CLI login on this
|
|
57
|
+
* machine, for the access token; `CONVEX_DEPLOY_KEY` for the deploy key. A
|
|
58
|
+
* developer who ran `npx convex login` needs nothing. CI sets only the
|
|
59
|
+
* deploy key when the stack pushes to deployments that exist, and the access
|
|
60
|
+
* token when the stack also creates projects or deployments.
|
|
52
61
|
*/
|
|
53
|
-
const fromConfig = () => Layer.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
const fromConfig = () => Layer.effect(Credentials, Effect.gen(function* () {
|
|
63
|
+
const resolved = yield* Effect.cached(Config.Redacted(ACCESS_TOKEN_ENV).pipe(Effect.map((token) => ({
|
|
64
|
+
accessToken: token,
|
|
65
|
+
source: "env"
|
|
66
|
+
})), Effect.catch(() => readConvexCliLogin), Effect.result));
|
|
67
|
+
const deployKey = yield* Effect.cached(Config.option(Config.Redacted(DEPLOY_KEY_ENV)).pipe(Effect.map(Option.getOrUndefined), Effect.orDie));
|
|
68
|
+
return {
|
|
69
|
+
accessToken: resolved.pipe(Effect.flatMap(Effect.fromResult), Effect.orDie),
|
|
70
|
+
hasAccessToken: resolved.pipe(Effect.map(Result.isSuccess)),
|
|
71
|
+
deployKey
|
|
72
|
+
};
|
|
61
73
|
}));
|
|
74
|
+
/** Explicit values, for tests or callers that already hold them. */
|
|
75
|
+
const fromValues = (values) => Layer.succeed(Credentials, {
|
|
76
|
+
accessToken: values.accessToken === void 0 ? Effect.die(new CredentialsError({ reason: "This step needs the Convex Management API, and fromValues got no accessToken." })) : Effect.succeed({
|
|
77
|
+
accessToken: values.accessToken,
|
|
78
|
+
source: "explicit"
|
|
79
|
+
}),
|
|
80
|
+
hasAccessToken: Effect.succeed(values.accessToken !== void 0),
|
|
81
|
+
deployKey: Effect.succeed(values.deployKey)
|
|
82
|
+
});
|
|
62
83
|
//#endregion
|
|
63
84
|
//#region src/Adoption.ts
|
|
64
85
|
/** True when the resource `fqn` may take over an object that is not in state. */
|
|
@@ -145,8 +166,8 @@ const TeamRow = Schema.Struct({
|
|
|
145
166
|
slug: Schema.String
|
|
146
167
|
});
|
|
147
168
|
const TeamRows = Schema.Array(TeamRow);
|
|
148
|
-
|
|
149
|
-
|
|
169
|
+
/** The service for one way to get the bearer token. */
|
|
170
|
+
const makeService = (credentials) => {
|
|
150
171
|
const client = Effect.cached(credentials.pipe(Effect.map((resolved) => createManagementClient(Redacted.value(resolved.accessToken))))).pipe(Effect.flatten);
|
|
151
172
|
const request = (operation, run) => client.pipe(Effect.flatMap((c) => settle(operation, () => run(c))));
|
|
152
173
|
const requestVoid = (operation, run) => client.pipe(Effect.flatMap((c) => settleVoid(operation, () => run(c))));
|
|
@@ -175,240 +196,123 @@ const ManagementApiLive = () => Layer.effect(ManagementApi, Effect.gen(function*
|
|
|
175
196
|
return match.id;
|
|
176
197
|
});
|
|
177
198
|
const isCredentialToken = (secret) => credentials.pipe(Effect.map((resolved) => tokenPart(Redacted.value(resolved.accessToken)) === tokenPart(secret)));
|
|
199
|
+
const withToken = (token) => makeService(Effect.succeed({
|
|
200
|
+
accessToken: token,
|
|
201
|
+
source: "explicit"
|
|
202
|
+
}));
|
|
178
203
|
return {
|
|
179
204
|
request,
|
|
180
205
|
requestVoid,
|
|
181
206
|
deploymentRequest,
|
|
182
207
|
resolveTeamId,
|
|
183
|
-
isCredentialToken
|
|
208
|
+
isCredentialToken,
|
|
209
|
+
withToken
|
|
184
210
|
};
|
|
211
|
+
};
|
|
212
|
+
const ManagementApiLive = () => Layer.effect(ManagementApi, Effect.gen(function* () {
|
|
213
|
+
const credentials = yield* Credentials;
|
|
214
|
+
return makeService(credentials.accessToken);
|
|
185
215
|
}));
|
|
186
216
|
//#endregion
|
|
187
|
-
//#region src/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
*/
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
* when another resource writes a variable of the same deployment at the same
|
|
208
|
-
* time. Use it only where the variable is already ours to set.
|
|
209
|
-
*/
|
|
210
|
-
const updateVariable = (deployment, deployKey, name, value) => retryIdempotentWrite(writeVariable(deployment, deployKey, name, value));
|
|
217
|
+
//#region src/Push.ts
|
|
218
|
+
const sourceOf = (source) => typeof source === "string" ? {
|
|
219
|
+
path: source,
|
|
220
|
+
command: void 0,
|
|
221
|
+
include: void 0
|
|
222
|
+
} : {
|
|
223
|
+
path: source.path,
|
|
224
|
+
command: source.command,
|
|
225
|
+
include: source.include
|
|
226
|
+
};
|
|
227
|
+
/** Lifetime of a push key. Long enough for a slow push with index backfills. */
|
|
228
|
+
const PUSH_KEY_LIFETIME_MS = 72e5;
|
|
229
|
+
/** The only actions a push key gets. */
|
|
230
|
+
const PUSH_KEY_ACTIONS = [
|
|
231
|
+
"deployment:deploy",
|
|
232
|
+
"deployment:env:view",
|
|
233
|
+
"deployment:env:write"
|
|
234
|
+
];
|
|
235
|
+
/** POST /deployments/{deployment_name}/create_deploy_key */
|
|
236
|
+
const CreatedDeployKey = Schema.Struct({ deployKey: Schema.String });
|
|
211
237
|
/** GET https://<deployment>.convex.cloud/api/v1/list_environment_variables */
|
|
212
238
|
const EnvironmentVariableList = Schema.Struct({ environmentVariables: Schema.Record(Schema.String, Schema.String) });
|
|
213
|
-
/**
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
*/
|
|
220
|
-
var
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
239
|
+
/** No `convex` package resolves from the app directory. */
|
|
240
|
+
var ConvexCliNotFound = class extends Schema.TaggedError()("ConvexCliNotFound", { cwd: Schema.String }) {
|
|
241
|
+
get message() {
|
|
242
|
+
return `Convex.Deployment found no installed convex package from ${this.cwd}, and it does not download one. Install convex in the app (npm install convex), or set source to the functions directory of the app.`;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
/** No directory at or above the parent of `source` has a package.json. */
|
|
246
|
+
var ConvexAppNotFound = class extends Schema.TaggedError()("ConvexAppNotFound", { source: Schema.String }) {
|
|
247
|
+
get message() {
|
|
248
|
+
return `Convex.Deployment found no package.json at or above the parent of ${this.source}. Set source to the functions directory of a Convex app, such as "./convex".`;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
/** The Convex CLI failed, or exited 0 without the deployment URL. The output tail never contains the key. */
|
|
252
|
+
var CodePushFailed = class extends Schema.TaggedError()("CodePushFailed", {
|
|
253
|
+
/** Exit code of the CLI. Undefined when it did not start or a signal stopped it. */
|
|
254
|
+
exitCode: Schema.UndefinedOr(Schema.Number),
|
|
255
|
+
/** The last lines of the CLI output, with the deploy key removed. */
|
|
256
|
+
outputTail: Schema.String
|
|
230
257
|
}) {
|
|
231
258
|
get message() {
|
|
232
|
-
return
|
|
259
|
+
return this.exitCode === 0 ? `convex deploy exited 0 but printed no "Deployed Convex functions to <url>" line. Last output, with the deploy key removed:\n${this.outputTail}` : `convex deploy failed${this.exitCode === void 0 ? "" : ` with exit code ${this.exitCode}`}. Last output, with the deploy key removed:\n${this.outputTail}`;
|
|
233
260
|
}
|
|
234
261
|
};
|
|
235
|
-
/**
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
resourceType: EnvironmentVariable.Type,
|
|
243
|
-
field: "deployment",
|
|
244
|
-
current: output.deployment,
|
|
245
|
-
requested: news.deployment
|
|
246
|
-
})) : Effect.void;
|
|
247
|
-
/**
|
|
248
|
-
* The deploy key for the variable in state: from the attributes, or, for
|
|
249
|
-
* state from 0.1.x, from props that name the same deployment.
|
|
250
|
-
*/
|
|
251
|
-
const keyOf = (output, olds) => output.deployKey ?? (olds?.deployment === output.deployment ? olds.deployKey : void 0);
|
|
252
|
-
const EnvironmentVariableProvider = () => Provider.effect(EnvironmentVariable, Effect.gen(function* () {
|
|
253
|
-
const api = yield* ManagementApi;
|
|
254
|
-
return {
|
|
255
|
-
diff: Effect.fn(function* ({ news, output }) {
|
|
256
|
-
if (!isResolved(news) || output === void 0) return void 0;
|
|
257
|
-
yield* checkIdentity$1(output, news);
|
|
258
|
-
}),
|
|
259
|
-
read: Effect.fn(function* ({ olds, output }) {
|
|
260
|
-
if (output !== void 0) {
|
|
261
|
-
const key = keyOf(output, olds);
|
|
262
|
-
if (key === void 0) return output;
|
|
263
|
-
return (yield* readVariable(api, output.deployment, key, output.name)) === void 0 ? void 0 : output;
|
|
264
|
-
}
|
|
265
|
-
const found = yield* readVariable(api, olds.deployment, olds.deployKey, olds.name);
|
|
266
|
-
return found === void 0 ? void 0 : Unowned({
|
|
267
|
-
...found,
|
|
268
|
-
deployKey: olds.deployKey
|
|
269
|
-
});
|
|
270
|
-
}),
|
|
271
|
-
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
272
|
-
if (output !== void 0) yield* checkIdentity$1(output, news);
|
|
273
|
-
const write = writeVariable(news.deployment, news.deployKey, news.name, news.value);
|
|
274
|
-
if (output !== void 0 || (yield* shouldAdopt(fqn))) yield* retryIdempotentWrite(write);
|
|
275
|
-
else yield* retryIdempotentWrite(Effect.gen(function* () {
|
|
276
|
-
if ((yield* readVariable(api, news.deployment, news.deployKey, news.name)) !== void 0) return yield* new OwnedBySomeoneElse({
|
|
277
|
-
message: `Environment variable ${news.name} already exists on deployment ${news.deployment}. Re-run with --adopt to take it over and overwrite its value, or use a different name.`,
|
|
278
|
-
resourceType: EnvironmentVariable.Type,
|
|
279
|
-
physicalName: news.name
|
|
280
|
-
});
|
|
281
|
-
yield* write;
|
|
282
|
-
}));
|
|
283
|
-
return {
|
|
284
|
-
deployment: news.deployment,
|
|
285
|
-
name: news.name,
|
|
286
|
-
deployKey: news.deployKey
|
|
287
|
-
};
|
|
288
|
-
}),
|
|
289
|
-
delete: Effect.fn(function* ({ olds, output }) {
|
|
290
|
-
const key = keyOf(output, olds);
|
|
291
|
-
if (key === void 0) {
|
|
292
|
-
yield* Effect.logWarning(`Environment variable ${output.name} on deployment ${output.deployment} stays: Alchemy has no deploy key for that deployment. Remove it in the Convex dashboard.`);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
yield* absentAsUndefined(updateVariable(output.deployment, key, output.name, null));
|
|
296
|
-
})
|
|
297
|
-
};
|
|
298
|
-
}));
|
|
299
|
-
//#endregion
|
|
300
|
-
//#region src/DefaultEnvironmentVariable.ts
|
|
301
|
-
/**
|
|
302
|
-
* Attributes when the listing has `name` for `deploymentType`. An unfiltered
|
|
303
|
-
* listing merges the deployment types that share one value into one row, so
|
|
304
|
-
* the match checks membership in `deploymentTypes`.
|
|
305
|
-
*/
|
|
306
|
-
const findDefault = (list, projectId, name, deploymentType) => list.items.some((item) => item.name === name && item.deploymentTypes.includes(deploymentType)) ? {
|
|
307
|
-
projectId,
|
|
308
|
-
name,
|
|
309
|
-
deploymentType
|
|
310
|
-
} : void 0;
|
|
311
|
-
/**
|
|
312
|
-
* Lists the defaults named `name` for `deploymentType`. Convex filters on
|
|
313
|
-
* both, so the listing has at most one row and needs no pagination.
|
|
314
|
-
*/
|
|
315
|
-
const listDefaults = (api, projectId, name, deploymentType) => api.request("list default environment variables", (client) => client.GET("/projects/{project_id}/list_default_environment_variables", { params: {
|
|
316
|
-
path: { project_id: projectId },
|
|
317
|
-
query: {
|
|
318
|
-
name,
|
|
319
|
-
deploymentType
|
|
262
|
+
/** The CLI pushed to another deployment than the one of the resource. Checked after the push. */
|
|
263
|
+
var PushTargetMismatch = class extends Schema.TaggedError()("PushTargetMismatch", {
|
|
264
|
+
expected: Schema.String,
|
|
265
|
+
actual: Schema.String
|
|
266
|
+
}) {
|
|
267
|
+
get message() {
|
|
268
|
+
return `The Convex CLI pushed the functions to deployment ${this.actual}, but Convex.Deployment expected ${this.expected}. Check that convex.json does not select another deployment.`;
|
|
320
269
|
}
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const updateDefault = (api, projectId, name, deploymentType, value) => retryIdempotentWrite(writeDefault(api, projectId, name, deploymentType, value));
|
|
338
|
-
/** The default's identity, or undefined when the default or the project is absent. */
|
|
339
|
-
const readDefault = (api, projectId, name, deploymentType) => absentAsUndefined(listDefaults(api, projectId, name, deploymentType)).pipe(Effect.map((list) => list === void 0 ? void 0 : findDefault(list, projectId, name, deploymentType)));
|
|
270
|
+
};
|
|
271
|
+
/** Variables in `env` exist on the deployment with other values, and adoption is off. */
|
|
272
|
+
var VariablesOwnedBySomeoneElse = class extends Schema.TaggedError()("VariablesOwnedBySomeoneElse", {
|
|
273
|
+
deployment: Schema.String,
|
|
274
|
+
names: Schema.Array(Schema.String)
|
|
275
|
+
}) {
|
|
276
|
+
get message() {
|
|
277
|
+
return `Environment variables ${this.names.join(", ")} already exist on deployment ${this.deployment} with other values. Re-run with --adopt to take them over and overwrite them, or remove them from env.`;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
/** A preview deploy key cannot set variables: the deployment API takes only a deploy key of the deployment. */
|
|
281
|
+
var EnvironmentNeedsDeployKey = class extends Schema.TaggedError()("EnvironmentNeedsDeployKey", { deployment: Schema.String }) {
|
|
282
|
+
get message() {
|
|
283
|
+
return `Convex.Deployment cannot set or remove env variables on preview deployment ${this.deployment} with a preview deploy key: Convex lets that key create, push to, and delete preview deployments, but not change their variables. Set CONVEX_ACCESS_TOKEN for this stage, or set the values as project default environment variables for preview deployments in the Convex dashboard.`;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
340
286
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
287
|
+
* What a deploy key targets, from its prefix: `<type>:<deployment>|<secret>`
|
|
288
|
+
* (or `<deployment>|<secret>` in old keys) for one deployment, and
|
|
289
|
+
* `preview:<team>:<project>|<secret>` for the preview deployments of a
|
|
290
|
+
* project. Same rules as convex 1.46 `cli/lib/deployment.js`.
|
|
343
291
|
*/
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
current: `${output.projectId}`,
|
|
354
|
-
requested: `${news.projectId}`
|
|
355
|
-
} : output.name !== news.name ? {
|
|
356
|
-
field: "name",
|
|
357
|
-
current: output.name,
|
|
358
|
-
requested: news.name
|
|
359
|
-
} : output.deploymentType !== news.deploymentType ? {
|
|
360
|
-
field: "deploymentType",
|
|
361
|
-
current: output.deploymentType,
|
|
362
|
-
requested: news.deploymentType
|
|
363
|
-
} : void 0;
|
|
364
|
-
return changed === void 0 ? Effect.void : Effect.fail(new VariableIdentityChange({
|
|
365
|
-
resourceType: DefaultEnvironmentVariable.Type,
|
|
366
|
-
...changed
|
|
367
|
-
}));
|
|
368
|
-
};
|
|
369
|
-
const DefaultEnvironmentVariableProvider = () => Provider.effect(DefaultEnvironmentVariable, Effect.gen(function* () {
|
|
370
|
-
const api = yield* ManagementApi;
|
|
371
|
-
return {
|
|
372
|
-
diff: Effect.fn(function* ({ news, output }) {
|
|
373
|
-
if (!isResolved(news) || output === void 0) return void 0;
|
|
374
|
-
yield* checkIdentity(output, news);
|
|
375
|
-
}),
|
|
376
|
-
read: Effect.fn(function* ({ olds, output }) {
|
|
377
|
-
const at = output ?? olds;
|
|
378
|
-
const found = yield* readDefault(api, at.projectId, at.name, at.deploymentType);
|
|
379
|
-
if (found === void 0) return void 0;
|
|
380
|
-
return output ?? Unowned(found);
|
|
381
|
-
}),
|
|
382
|
-
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
383
|
-
if (output !== void 0) yield* checkIdentity(output, news);
|
|
384
|
-
const write = writeDefault(api, news.projectId, news.name, news.deploymentType, news.value);
|
|
385
|
-
if (output !== void 0 || (yield* shouldAdopt(fqn))) yield* retryIdempotentWrite(write);
|
|
386
|
-
else yield* retryIdempotentWrite(Effect.gen(function* () {
|
|
387
|
-
if ((yield* readDefault(api, news.projectId, news.name, news.deploymentType)) !== void 0) return yield* new OwnedBySomeoneElse({
|
|
388
|
-
message: `Project ${news.projectId} already has a ${news.deploymentType} default environment variable ${news.name}. Re-run with --adopt to take it over and overwrite its value, or use a different name.`,
|
|
389
|
-
resourceType: DefaultEnvironmentVariable.Type,
|
|
390
|
-
physicalName: news.name
|
|
391
|
-
});
|
|
392
|
-
yield* write;
|
|
393
|
-
}));
|
|
394
|
-
return {
|
|
395
|
-
projectId: news.projectId,
|
|
396
|
-
name: news.name,
|
|
397
|
-
deploymentType: news.deploymentType
|
|
398
|
-
};
|
|
399
|
-
}),
|
|
400
|
-
delete: Effect.fn(function* ({ output }) {
|
|
401
|
-
yield* absentAsUndefined(updateDefault(api, output.projectId, output.name, output.deploymentType, null));
|
|
402
|
-
})
|
|
292
|
+
const keyTargetOf = (key) => {
|
|
293
|
+
const bar = key.indexOf("|");
|
|
294
|
+
if (bar <= 0 || /[\s#'"`]/.test(key)) return { kind: "other" };
|
|
295
|
+
const [first, second, third, ...rest] = key.slice(0, bar).split(":");
|
|
296
|
+
if (rest.length > 0) return { kind: "other" };
|
|
297
|
+
if (first === "preview" && second && third) return {
|
|
298
|
+
kind: "preview",
|
|
299
|
+
team: second,
|
|
300
|
+
project: third
|
|
403
301
|
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
302
|
+
if (third !== void 0) return { kind: "other" };
|
|
303
|
+
if (second) return {
|
|
304
|
+
kind: "deployment",
|
|
305
|
+
deployment: second
|
|
306
|
+
};
|
|
307
|
+
if (first && second === void 0) return {
|
|
308
|
+
kind: "deployment",
|
|
309
|
+
deployment: first
|
|
310
|
+
};
|
|
311
|
+
return { kind: "other" };
|
|
312
|
+
};
|
|
408
313
|
/**
|
|
409
314
|
* The CLI prints this line with `logFinishedStep` after the push succeeds, to
|
|
410
|
-
* stderr (convex 1.
|
|
411
|
-
* `cli/deploy.js` for preview deployments). The host is
|
|
315
|
+
* stderr (convex 1.46 `cli/lib/deploy2.js`). The host is
|
|
412
316
|
* `<deployment>.convex.cloud` or `<deployment>.<region>.convex.cloud`.
|
|
413
317
|
*/
|
|
414
318
|
const DEPLOYED_LINE = /Deployed Convex functions to (https:\/\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud)\/?(?=\s|$)/m;
|
|
@@ -422,298 +326,632 @@ const parseDeployOutput = (text) => {
|
|
|
422
326
|
deploymentName
|
|
423
327
|
};
|
|
424
328
|
};
|
|
425
|
-
/** The
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
329
|
+
/** The last non-empty lines of `text`, without escape codes and with every secret replaced. */
|
|
330
|
+
const outputTail = (text, secrets) => secrets.reduce((redacted, secret) => redacted.split(secret).join("[REDACTED]"), stripVTControlCharacters(text)).split(/\r?\n|\r/).filter((line) => line.trim() !== "").slice(-20).join("\n");
|
|
331
|
+
const valueOf = (value) => Redacted.isRedacted(value) ? Redacted.value(value) : value;
|
|
332
|
+
/** The app directory: the nearest directory at or above the parent of `source` with a package.json. */
|
|
333
|
+
const appDirectoryOf = (source) => Effect.tryPromise({
|
|
334
|
+
try: async () => {
|
|
335
|
+
let directory = dirname(resolve(source));
|
|
336
|
+
while (true) try {
|
|
337
|
+
await access(join(directory, "package.json"));
|
|
338
|
+
return directory;
|
|
339
|
+
} catch {
|
|
340
|
+
const parent = dirname(directory);
|
|
341
|
+
if (parent === directory) throw new Error("no package.json");
|
|
342
|
+
directory = parent;
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
catch: () => new ConvexAppNotFound({ source })
|
|
346
|
+
});
|
|
347
|
+
/** `source` is not the functions directory that the Convex CLI pushes from this app. */
|
|
348
|
+
var SourceMismatch = class extends Schema.TaggedError()("SourceMismatch", {
|
|
349
|
+
source: Schema.String,
|
|
350
|
+
functions: Schema.String
|
|
351
|
+
}) {
|
|
443
352
|
get message() {
|
|
444
|
-
return `
|
|
353
|
+
return `Convex.Deployment source is ${this.source}, but the Convex CLI pushes ${this.functions} in this app (the "functions" field of convex.json, or convex/ by default). Set source to ${this.functions}.`;
|
|
445
354
|
}
|
|
446
355
|
};
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
356
|
+
const ConvexJson = Schema.Struct({ functions: Schema.optionalKey(Schema.String) });
|
|
357
|
+
/** Fails with {@link SourceMismatch} when `path` is not the functions directory of the app in `cwd`. */
|
|
358
|
+
const checkFunctionsDirectory = (cwd, path) => Effect.gen(function* () {
|
|
359
|
+
const text = yield* Effect.promise(() => readFile(join(cwd, "convex.json"), "utf8").catch(() => void 0));
|
|
360
|
+
const configured = text === void 0 ? void 0 : Schema.decodeUnknownSync(Schema.fromJsonString(ConvexJson))(text).functions;
|
|
361
|
+
const functions = resolve(cwd, configured ?? "convex");
|
|
362
|
+
if (functions !== resolve(path)) return yield* new SourceMismatch({
|
|
363
|
+
source: resolve(path),
|
|
364
|
+
functions
|
|
452
365
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
...news.timeout === void 0 ? {} : { timeout: news.timeout },
|
|
467
|
-
env: {
|
|
468
|
-
...news.env,
|
|
469
|
-
CONVEX_DEPLOY_KEY: news.deployKey
|
|
470
|
-
}
|
|
471
|
-
}, session);
|
|
472
|
-
const output = `${stdout}\n${stderr}`;
|
|
473
|
-
const deployed = parseDeployOutput(output);
|
|
474
|
-
if (deployed === void 0) return yield* new DeployOutputError({ outputTail: stripVTControlCharacters(output).split(/\r?\n|\r/).filter((line) => line.trim() !== "").slice(-20).join("\n") });
|
|
475
|
-
return {
|
|
476
|
-
...deployed,
|
|
477
|
-
hash: yield* hashOf(news)
|
|
478
|
-
};
|
|
479
|
-
}),
|
|
480
|
-
delete: () => Effect.void
|
|
481
|
-
};
|
|
482
|
-
}));
|
|
483
|
-
//#endregion
|
|
484
|
-
//#region src/DeployKey.ts
|
|
366
|
+
});
|
|
367
|
+
const CliManifest = Schema.Struct({ bin: Schema.Union([Schema.String, Schema.Record(Schema.String, Schema.String)]) });
|
|
368
|
+
/** `[node, <convex bin>]` for the `convex` package that `cwd` resolves. Never downloads. */
|
|
369
|
+
const installedCli = (cwd) => Effect.tryPromise({
|
|
370
|
+
try: async () => {
|
|
371
|
+
const manifestPath = createRequire(join(cwd, "package.json")).resolve("convex/package.json");
|
|
372
|
+
const { bin } = Schema.decodeUnknownSync(Schema.fromJsonString(CliManifest))(await readFile(manifestPath, "utf8"));
|
|
373
|
+
const entry = typeof bin === "string" ? bin : bin["convex"];
|
|
374
|
+
if (entry === void 0) throw new Error("the convex package has no convex bin");
|
|
375
|
+
return [process.execPath, resolve(dirname(manifestPath), entry)];
|
|
376
|
+
},
|
|
377
|
+
catch: () => new ConvexCliNotFound({ cwd })
|
|
378
|
+
});
|
|
485
379
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
380
|
+
* The content identity of a push: the files of the app directory that git
|
|
381
|
+
* does not ignore (or `path`, the manifests, and the `include` globs), the
|
|
382
|
+
* nearest lockfile, the target deployment, and the `env` names and values.
|
|
383
|
+
* A change in a workspace package outside the app directory is not seen.
|
|
490
384
|
*/
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
385
|
+
const pushHash = (options) => Effect.gen(function* () {
|
|
386
|
+
const source = options.source === void 0 ? void 0 : sourceOf(options.source);
|
|
387
|
+
const files = source === void 0 ? void 0 : yield* appDirectoryOf(source.path).pipe(Effect.flatMap((cwd) => hashDirectory({
|
|
388
|
+
cwd,
|
|
389
|
+
memo: source.include === void 0 ? {} : {
|
|
390
|
+
include: [
|
|
391
|
+
`${relative(cwd, resolve(source.path)).split("\\").join("/")}/**`,
|
|
392
|
+
"package.json",
|
|
393
|
+
"convex.json",
|
|
394
|
+
...source.include
|
|
395
|
+
],
|
|
396
|
+
lockfile: true
|
|
397
|
+
}
|
|
398
|
+
})));
|
|
399
|
+
const env = Object.entries(options.env).map(([name, value]) => [name, valueOf(value)]).sort(([a], [b]) => a < b ? -1 : 1);
|
|
400
|
+
return createHash("sha256").update(JSON.stringify({
|
|
401
|
+
files,
|
|
402
|
+
deployment: options.deployment,
|
|
403
|
+
env
|
|
404
|
+
})).digest("hex");
|
|
405
|
+
});
|
|
495
406
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
407
|
+
* Runs `use` with an env file that holds the key, in a new temporary
|
|
408
|
+
* directory, and removes the directory afterwards: on success, on failure,
|
|
409
|
+
* and on interruption.
|
|
498
410
|
*/
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
411
|
+
const withKeyFile = (key, use) => Effect.acquireUseRelease(Effect.promise(() => mkdtemp(join(tmpdir(), "alchemy-convex-"))), (home) => Effect.promise(() => writeFile(join(home, "deploy.env"), `CONVEX_DEPLOY_KEY=${key}\n`, { mode: 384 })).pipe(Effect.andThen(use({
|
|
412
|
+
envFile: join(home, "deploy.env"),
|
|
413
|
+
home
|
|
414
|
+
}))), (home) => Effect.promise(() => rm(home, {
|
|
415
|
+
recursive: true,
|
|
416
|
+
force: true
|
|
417
|
+
})));
|
|
418
|
+
/** This process's variables without `CONVEX_*`, with `home` as the home directory. */
|
|
419
|
+
const cliEnvironment = (home) => ({
|
|
420
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("CONVEX_"))),
|
|
421
|
+
HOME: home,
|
|
422
|
+
USERPROFILE: home,
|
|
423
|
+
XDG_CONFIG_HOME: home
|
|
424
|
+
});
|
|
502
425
|
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* (https://github.com/rjdellecese/confect, ISC license).
|
|
426
|
+
* Creates a deploy key for one push, runs `use` with it, and deletes it
|
|
427
|
+
* afterwards. Without a push command, the key can only deploy and read and
|
|
428
|
+
* write variables. The delete is skipped for a key that is the OAuth credential.
|
|
429
|
+
* A failed delete is logged: the key expires anyway.
|
|
508
430
|
*/
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
431
|
+
const withPushKey = (api, deployment, source, use) => Effect.acquireUseRelease(Effect.gen(function* () {
|
|
432
|
+
const now = yield* Clock.currentTimeMillis;
|
|
433
|
+
const created = yield* api.request("create deploy key", (client) => client.POST("/deployments/{deployment_name}/create_deploy_key", {
|
|
434
|
+
params: { path: { deployment_name: deployment } },
|
|
435
|
+
body: {
|
|
436
|
+
name: "alchemy-push",
|
|
437
|
+
...source === void 0 || sourceOf(source).command === void 0 ? { allowedActions: [...PUSH_KEY_ACTIONS] } : {},
|
|
438
|
+
expiresAt: now + PUSH_KEY_LIFETIME_MS
|
|
439
|
+
}
|
|
440
|
+
}));
|
|
441
|
+
return Redacted.make(Schema.decodeUnknownSync(CreatedDeployKey)(created).deployKey);
|
|
442
|
+
}), use, (key) => Effect.gen(function* () {
|
|
443
|
+
if (yield* api.isCredentialToken(Redacted.value(key))) return;
|
|
444
|
+
yield* absentAsUndefined(retryIdempotentWrite(api.requestVoid("delete deploy key", (client) => client.POST("/deployments/{deployment_name}/delete_deploy_key", {
|
|
445
|
+
params: { path: { deployment_name: deployment } },
|
|
446
|
+
body: { id: Redacted.value(key) }
|
|
447
|
+
}))));
|
|
448
|
+
}).pipe(Effect.catch((error) => Effect.logWarning(`Convex.Deployment could not delete its push key on ${deployment} (${error.message}). The key expires on its own within ${PUSH_KEY_LIFETIME_MS / 6e4} minutes.`))));
|
|
449
|
+
/** Every variable of the deployment with its value. */
|
|
450
|
+
const listVariables = (url, key) => settle("list environment variables", () => createDeploymentClient(url, Redacted.value(key)).GET("/list_environment_variables")).pipe(Effect.map((body) => Schema.decodeUnknownSync(EnvironmentVariableList)(body).environmentVariables));
|
|
451
|
+
/** One call that sets variables, or removes those whose value is null. */
|
|
452
|
+
const writeVariables = (url, key, changes) => settleVoid("update environment variables", () => createDeploymentClient(url, Redacted.value(key)).POST("/update_environment_variables", { body: { changes: changes.map(({ name, value }) => ({
|
|
453
|
+
name,
|
|
454
|
+
value
|
|
455
|
+
})) } }));
|
|
456
|
+
/**
|
|
457
|
+
* The app directory and the program of a push. Fails before any change when
|
|
458
|
+
* `source` is not the functions directory of the app, or when the app has no
|
|
459
|
+
* installed Convex CLI and no push command.
|
|
460
|
+
*/
|
|
461
|
+
const checkSource = (input) => Effect.gen(function* () {
|
|
462
|
+
const source = sourceOf(input);
|
|
463
|
+
const cwd = yield* appDirectoryOf(source.path);
|
|
464
|
+
yield* checkFunctionsDirectory(cwd, source.path);
|
|
465
|
+
return {
|
|
466
|
+
cwd,
|
|
467
|
+
program: source.command ?? (yield* installedCli(cwd))
|
|
468
|
+
};
|
|
514
469
|
});
|
|
515
|
-
/** GET /deployments/{deployment_name}/list_deploy_keys */
|
|
516
|
-
const DeployKeyList = Schema.Array(ListedKey);
|
|
517
|
-
/** POST /deployments/{deployment_name}/create_deploy_key */
|
|
518
|
-
const CreatedDeployKey = Schema.Struct({ deployKey: Schema.String });
|
|
519
470
|
/**
|
|
520
|
-
*
|
|
521
|
-
*
|
|
471
|
+
* Sets the `env` variables and pushes the functions of `source` to one
|
|
472
|
+
* deployment with `key`: a deploy key of the deployment, or a preview deploy
|
|
473
|
+
* key of its project with `previewName`. Returns the URL that the CLI
|
|
474
|
+
* printed, or undefined without `source`.
|
|
522
475
|
*/
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
476
|
+
const push = (spawner, key, options) => Effect.gen(function* () {
|
|
477
|
+
const source = options.source === void 0 ? void 0 : sourceOf(options.source);
|
|
478
|
+
const resolved = options.source === void 0 ? void 0 : yield* checkSource(options.source);
|
|
479
|
+
const cwd = resolved?.cwd;
|
|
480
|
+
const program = resolved?.program;
|
|
481
|
+
const env = Object.entries(options.env).sort(([a], [b]) => a < b ? -1 : 1);
|
|
482
|
+
const removed = options.previousEnvNames.filter((name) => !Object.hasOwn(options.env, name));
|
|
483
|
+
const written = env.length === 0 && removed.length === 0 ? {
|
|
484
|
+
current: {},
|
|
485
|
+
changes: []
|
|
486
|
+
} : yield* retryIdempotentWrite(Effect.gen(function* () {
|
|
487
|
+
const current = yield* listVariables(options.url, key);
|
|
488
|
+
if (!options.adopt) {
|
|
489
|
+
const taken = env.filter(([name, value]) => !options.previousEnvNames.includes(name) && Object.hasOwn(current, name) && current[name] !== valueOf(value)).map(([name]) => name);
|
|
490
|
+
if (taken.length > 0) return yield* new VariablesOwnedBySomeoneElse({
|
|
491
|
+
deployment: options.deployment,
|
|
492
|
+
names: taken
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const changes = [...env.filter(([name, value]) => current[name] !== valueOf(value)).map(([name, value]) => ({
|
|
496
|
+
name,
|
|
497
|
+
value: valueOf(value)
|
|
498
|
+
})), ...removed.filter((name) => Object.hasOwn(current, name)).map((name) => ({
|
|
499
|
+
name,
|
|
500
|
+
value: null
|
|
501
|
+
}))];
|
|
502
|
+
if (changes.length > 0) yield* writeVariables(options.url, key, changes);
|
|
503
|
+
return {
|
|
504
|
+
current,
|
|
505
|
+
changes
|
|
506
|
+
};
|
|
507
|
+
}));
|
|
508
|
+
/**
|
|
509
|
+
* After a failed push, the variables go back to their earlier values, so
|
|
510
|
+
* that the deployment matches the hash in state. A failed rollback is
|
|
511
|
+
* logged: the push error is the one to report.
|
|
512
|
+
*/
|
|
513
|
+
const rollback = written.changes.length === 0 ? Effect.void : retryIdempotentWrite(writeVariables(options.url, key, written.changes.map(({ name }) => ({
|
|
514
|
+
name,
|
|
515
|
+
value: Object.hasOwn(written.current, name) ? written.current[name] ?? null : null
|
|
516
|
+
})))).pipe(Effect.catch((error) => Effect.logWarning(`Convex.Deployment could not set the env variables of ${options.deployment} back after the failed push (${error.message}). Run the deploy again.`)));
|
|
517
|
+
if (source === void 0 || cwd === void 0 || program === void 0) return void 0;
|
|
518
|
+
return yield* runPush(spawner, key, {
|
|
519
|
+
...options,
|
|
520
|
+
cwd,
|
|
521
|
+
program,
|
|
522
|
+
command: source.command
|
|
523
|
+
}).pipe(Effect.onError(() => rollback));
|
|
524
|
+
});
|
|
525
|
+
/** Runs the installed CLI or the push command once, and checks the deployment that it names. */
|
|
526
|
+
const runPush = (spawner, key, options) => Effect.gen(function* () {
|
|
527
|
+
const { cwd, program } = options;
|
|
528
|
+
const secret = Redacted.value(key);
|
|
529
|
+
const secrets = [secret, secret.slice(secret.indexOf("|") + 1)].filter((value) => value.length >= 8);
|
|
530
|
+
const result = yield* withKeyFile(secret, ({ envFile, home }) => Effect.gen(function* () {
|
|
531
|
+
const [command, ...programArgs] = program;
|
|
532
|
+
const args = options.command === void 0 ? [
|
|
533
|
+
...programArgs,
|
|
534
|
+
"deploy",
|
|
535
|
+
...options.previewName === void 0 ? [] : ["--preview-name", options.previewName],
|
|
536
|
+
"--env-file",
|
|
537
|
+
envFile
|
|
538
|
+
] : programArgs;
|
|
539
|
+
const child = yield* spawner.spawn(ChildProcess.make(command, args, {
|
|
540
|
+
cwd,
|
|
541
|
+
env: {
|
|
542
|
+
...cliEnvironment(home),
|
|
543
|
+
...options.command === void 0 ? {} : { CONVEX_DEPLOY_KEY: secret }
|
|
544
|
+
},
|
|
545
|
+
extendEnv: false,
|
|
546
|
+
stdin: "ignore",
|
|
547
|
+
stdout: "pipe",
|
|
548
|
+
stderr: "pipe"
|
|
549
|
+
}));
|
|
550
|
+
const [exitCode, stdout, stderr] = yield* Effect.all([
|
|
551
|
+
child.exitCode,
|
|
552
|
+
Stream.mkString(Stream.decodeText(child.stdout)),
|
|
553
|
+
Stream.mkString(Stream.decodeText(child.stderr))
|
|
554
|
+
], { concurrency: "unbounded" });
|
|
555
|
+
return {
|
|
556
|
+
exitCode: Number(exitCode),
|
|
557
|
+
output: `${stdout}\n${stderr}`
|
|
558
|
+
};
|
|
559
|
+
}).pipe(Effect.scoped)).pipe(Effect.mapError((error) => new CodePushFailed({
|
|
560
|
+
exitCode: void 0,
|
|
561
|
+
outputTail: outputTail(error.message, secrets)
|
|
562
|
+
})));
|
|
563
|
+
const deployed = parseDeployOutput(result.output);
|
|
564
|
+
if (result.exitCode !== 0 || deployed === void 0) return yield* new CodePushFailed({
|
|
565
|
+
exitCode: result.exitCode,
|
|
566
|
+
outputTail: outputTail(result.output, secrets)
|
|
567
|
+
});
|
|
568
|
+
if (deployed.deploymentName !== options.deployment) return yield* new PushTargetMismatch({
|
|
569
|
+
expected: options.deployment,
|
|
570
|
+
actual: deployed.deploymentName
|
|
571
|
+
});
|
|
572
|
+
return deployed.url;
|
|
573
|
+
});
|
|
574
|
+
//#endregion
|
|
575
|
+
//#region src/Deployment.ts
|
|
576
|
+
const Deployment = Resource("Convex.Deployment", { defaultRemovalPolicy: "retain" });
|
|
577
|
+
/** The props name another deployment than the one in state. */
|
|
578
|
+
var DeploymentIdentityChange = class extends Schema.TaggedError()("DeploymentIdentityChange", {
|
|
579
|
+
/** Name of the deployment in state, such as `happy-animal-123`. */
|
|
580
|
+
deployment: Schema.String,
|
|
581
|
+
field: Schema.Literals([
|
|
582
|
+
"projectId",
|
|
583
|
+
"type",
|
|
584
|
+
"name"
|
|
585
|
+
]),
|
|
586
|
+
current: Schema.String,
|
|
587
|
+
requested: Schema.String
|
|
532
588
|
}) {
|
|
533
589
|
get message() {
|
|
534
|
-
|
|
535
|
-
return this.afterCreate ? `Convex created deploy key "${this.name}" on ${this.target}, but the listing shows ${listed} with that name, not exactly one, so this resource cannot track the new key. Alchemy does not revoke it. Delete the keys with that name in the Convex dashboard, then run the deploy again.` : `${this.target} already lists ${listed} with the name "${this.name}", but Alchemy state has no such key. An interrupted run probably created it, and its secret is lost. Delete these keys in the Convex dashboard, then run the deploy again.`;
|
|
590
|
+
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.`;
|
|
536
591
|
}
|
|
537
592
|
};
|
|
538
|
-
/**
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
593
|
+
/** The project has no default production deployment for a `prod` resource to take over. */
|
|
594
|
+
var ProductionDeploymentNotFound = class extends Schema.TaggedError()("ProductionDeploymentNotFound", { projectId: Schema.Number }) {
|
|
595
|
+
get message() {
|
|
596
|
+
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.`;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
/** More than one deployment of the project has the name, so the lookup cannot choose one. */
|
|
600
|
+
var AmbiguousDeployment = class extends Schema.TaggedError()("AmbiguousDeployment", {
|
|
601
|
+
projectId: Schema.Number,
|
|
602
|
+
type: Schema.Literals(["dev", "preview"]),
|
|
603
|
+
name: Schema.String,
|
|
604
|
+
deployments: Schema.Array(Schema.String)
|
|
546
605
|
}) {
|
|
547
606
|
get message() {
|
|
548
|
-
return `
|
|
607
|
+
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.`;
|
|
549
608
|
}
|
|
550
609
|
};
|
|
551
610
|
/**
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
* when the secret is the Management API credential itself, or when the
|
|
555
|
-
* listing does not show exactly one key with that name. It never revokes a
|
|
556
|
-
* key: the returned secret can be a shared credential.
|
|
611
|
+
* The URL of a Convex cloud deployment: `https://<name>.convex.cloud`, or
|
|
612
|
+
* `https://<name>.<region>.convex.cloud` outside US East.
|
|
557
613
|
*/
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
614
|
+
const CLOUD_URL = /^https:\/\/[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud$/;
|
|
615
|
+
/**
|
|
616
|
+
* The HTTP actions URL of a deployment: the client URL with `.convex.site`
|
|
617
|
+
* for `.convex.cloud`, as Convex sets CONVEX_SITE_URL. Custom domains are
|
|
618
|
+
* not covered.
|
|
619
|
+
*/
|
|
620
|
+
const siteUrlOf = (url) => url.replace(/\.convex\.cloud$/, ".convex.site");
|
|
621
|
+
/** One row of list_deployments, GET /deployments/{name}, and create_deployment. Only the fields the provider uses. */
|
|
622
|
+
const DeploymentRow$1 = Schema.Struct({
|
|
623
|
+
name: Schema.String,
|
|
624
|
+
projectId: Schema.Number,
|
|
625
|
+
deploymentType: Schema.String,
|
|
626
|
+
isDefault: Schema.Boolean,
|
|
627
|
+
reference: Schema.String,
|
|
628
|
+
/** Set in list_deployments and create_deployment rows only: GET /deployments/{name} answers null. */
|
|
629
|
+
previewIdentifier: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
630
|
+
deploymentUrl: Schema.String.check(Schema.isPattern(CLOUD_URL)),
|
|
631
|
+
/** Absent or null until the first push: list_deployments leaves it out for a new deployment. */
|
|
632
|
+
lastDeployTime: Schema.optionalKey(Schema.NullOr(Schema.Number))
|
|
633
|
+
});
|
|
634
|
+
const DeploymentRows$1 = Schema.Array(DeploymentRow$1);
|
|
635
|
+
/** GET https://<deployment>/api/v1/deployment_info, for a cloud deployment. Only the fields the provider uses. */
|
|
636
|
+
const DeploymentInfo = Schema.Struct({
|
|
637
|
+
kind: Schema.Literal("cloud"),
|
|
638
|
+
projectId: Schema.Number,
|
|
639
|
+
deploymentType: Schema.String,
|
|
640
|
+
reference: Schema.optionalKey(Schema.NullOr(Schema.String))
|
|
641
|
+
});
|
|
642
|
+
/** GET https://<deployment>/api/v1/get_canonical_urls */
|
|
643
|
+
const CanonicalUrls = Schema.Struct({
|
|
644
|
+
convexCloudUrl: Schema.String,
|
|
645
|
+
convexSiteUrl: Schema.String
|
|
585
646
|
});
|
|
647
|
+
const targetOf = (row, type, previewName) => ({
|
|
648
|
+
projectId: row.projectId,
|
|
649
|
+
type,
|
|
650
|
+
name: row.name,
|
|
651
|
+
reference: row.reference,
|
|
652
|
+
previewName: type === "preview" ? previewName : void 0,
|
|
653
|
+
url: row.deploymentUrl,
|
|
654
|
+
siteUrl: siteUrlOf(row.deploymentUrl)
|
|
655
|
+
});
|
|
656
|
+
/** True when the deployment has received a push. */
|
|
657
|
+
const isDeployed = (row) => row.lastDeployTime !== void 0 && row.lastDeployTime !== null;
|
|
658
|
+
/** The `name` prop of a deployment: the dev reference or the preview name. */
|
|
659
|
+
const nameOf = (target) => target.type === "dev" ? target.reference : target.type === "preview" ? target.previewName : void 0;
|
|
586
660
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* this resource owns: createTrackedKey found exactly one new listed key, and
|
|
590
|
-
* the secret was not the credential. State from 0.1.x has no `keyId`, and its
|
|
591
|
-
* secret can be an OAuth token that Convex returned as a key, so a delete
|
|
592
|
-
* could revoke that OAuth token. Such a key stays, and a warning names it.
|
|
661
|
+
* Fails with {@link DeploymentIdentityChange} when the props name another
|
|
662
|
+
* deployment than `current`: the attributes in state, or a live deployment.
|
|
593
663
|
*/
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
664
|
+
const checkIdentity = (current, news) => {
|
|
665
|
+
const change = (field, from, to) => Effect.fail(new DeploymentIdentityChange({
|
|
666
|
+
deployment: current.name,
|
|
667
|
+
field,
|
|
668
|
+
current: from,
|
|
669
|
+
requested: to
|
|
670
|
+
}));
|
|
671
|
+
if (current.projectId !== news.project.projectId) return change("projectId", String(current.projectId), String(news.project.projectId));
|
|
672
|
+
if (current.type !== news.type) return change("type", current.type, news.type);
|
|
673
|
+
const currentName = nameOf(current);
|
|
674
|
+
if (news.type !== "prod" && currentName !== news.name) return change("name", currentName ?? "(none)", news.name);
|
|
675
|
+
return Effect.void;
|
|
676
|
+
};
|
|
677
|
+
const addressOf = (props) => ({
|
|
678
|
+
projectId: props.project.projectId,
|
|
679
|
+
type: props.type,
|
|
680
|
+
name: props.type === "prod" ? void 0 : props.name
|
|
600
681
|
});
|
|
601
|
-
|
|
602
|
-
const
|
|
682
|
+
/** True when an update writes or removes variables: `env` has names, or state has names to remove. */
|
|
683
|
+
const changesEnv = (news, output) => Object.keys(news.env ?? {}).length > 0 || envNamesOf(output).length > 0;
|
|
684
|
+
/** The `env` names in state. State from 0.3 has none. */
|
|
685
|
+
const envNamesOf = (output) => output?.envNames ?? [];
|
|
686
|
+
/** The address of the deployment in state. */
|
|
687
|
+
const addressInState = (output) => ({
|
|
688
|
+
projectId: output.projectId,
|
|
689
|
+
type: output.type,
|
|
690
|
+
name: nameOf(output)
|
|
691
|
+
});
|
|
692
|
+
const ProjectSlugs = Schema.Struct({
|
|
693
|
+
slug: Schema.String,
|
|
694
|
+
teamSlug: Schema.String
|
|
695
|
+
});
|
|
696
|
+
/** True for an answer that says this credential cannot read the object. */
|
|
697
|
+
const isRefused = (error) => error.status === 400 || error.status === 401 || error.status === 403 || error.status === 404;
|
|
698
|
+
const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
|
|
603
699
|
const api = yield* ManagementApi;
|
|
604
|
-
const
|
|
700
|
+
const credentials = yield* Credentials;
|
|
701
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
702
|
+
/** The live deployment, or undefined after a 404. GET /deployments/{name} answers no preview identifier. */
|
|
703
|
+
const getByName = (management, name) => absentAsUndefined(management.request("get deployment", (client) => client.GET("/deployments/{deployment_name}", { params: { path: { deployment_name: name } } }))).pipe(Effect.map((body) => body === void 0 ? void 0 : Schema.decodeUnknownSync(DeploymentRow$1)(body)));
|
|
704
|
+
const listDeployments = (management, projectId, type, isDefault) => management.request("list deployments", (client) => client.GET("/projects/{project_id}/list_deployments", { params: {
|
|
705
|
+
path: { project_id: projectId },
|
|
706
|
+
query: {
|
|
707
|
+
deploymentType: type,
|
|
708
|
+
...isDefault === void 0 ? {} : { isDefault }
|
|
709
|
+
}
|
|
710
|
+
} })).pipe(Effect.map((body) => Schema.decodeUnknownSync(DeploymentRows$1)(body)));
|
|
711
|
+
/** The row of the project's default production deployment, or undefined. */
|
|
712
|
+
const productionRow = (management, projectId) => listDeployments(management, projectId, "prod", true).pipe(Effect.map((rows) => rows.find((candidate) => candidate.deploymentType === "prod" && candidate.isDefault)));
|
|
713
|
+
/** The project's default production deployment, or undefined. */
|
|
714
|
+
const production = (management, projectId) => productionRow(management, projectId).pipe(Effect.map((row) => row === void 0 ? void 0 : targetOf(row, "prod", void 0)));
|
|
605
715
|
/**
|
|
606
|
-
*
|
|
607
|
-
*
|
|
716
|
+
* The one dev deployment with this reference, or the one preview
|
|
717
|
+
* deployment with this preview name, or undefined.
|
|
608
718
|
*/
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
719
|
+
const findByName = (management, projectId, type, name) => Effect.gen(function* () {
|
|
720
|
+
const matches = (yield* listDeployments(management, projectId, type)).filter((row) => row.deploymentType === type && (type === "dev" ? row.reference === name : row.previewIdentifier === name));
|
|
721
|
+
if (matches.length > 1) return yield* new AmbiguousDeployment({
|
|
722
|
+
projectId,
|
|
723
|
+
type,
|
|
724
|
+
name,
|
|
725
|
+
deployments: matches.map((row) => row.name)
|
|
726
|
+
});
|
|
727
|
+
return matches[0];
|
|
728
|
+
});
|
|
729
|
+
/** The deployment that the address names, through the Management API, or undefined. */
|
|
730
|
+
const lookup = (management, address) => address.type === "prod" || address.name === void 0 ? production(management, address.projectId) : findByName(management, address.projectId, address.type, address.name).pipe(Effect.map((row) => row === void 0 ? void 0 : targetOf(row, address.type, address.name)));
|
|
731
|
+
/** The deployment of a deploy key, when it is the deployment that the address names. */
|
|
732
|
+
const deployKeyTarget = (key, deployment, address) => Effect.gen(function* () {
|
|
733
|
+
const info = Schema.decodeUnknownSync(DeploymentInfo)(yield* api.deploymentRequest("get deployment info", deployment, key, (client) => client.GET("/deployment_info")));
|
|
734
|
+
const reference = info.reference ?? "";
|
|
735
|
+
if (info.projectId !== address.projectId || info.deploymentType !== address.type || address.type === "dev" && reference !== address.name) return;
|
|
736
|
+
const urls = Schema.decodeUnknownSync(CanonicalUrls)(yield* api.deploymentRequest("get canonical urls", deployment, key, (client) => client.GET("/get_canonical_urls")));
|
|
737
|
+
return {
|
|
738
|
+
projectId: info.projectId,
|
|
739
|
+
type: address.type,
|
|
740
|
+
name: deployment,
|
|
741
|
+
reference,
|
|
742
|
+
previewName: void 0,
|
|
743
|
+
url: urls.convexCloudUrl,
|
|
744
|
+
siteUrl: urls.convexSiteUrl
|
|
745
|
+
};
|
|
746
|
+
}).pipe(Effect.catchIf(isRefused, () => Effect.succeed(void 0)));
|
|
747
|
+
/** True when the preview deploy key belongs to the project. */
|
|
748
|
+
const previewKeyFits = (management, slugs, projectId) => management.request("get project", (client) => client.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } })).pipe(Effect.map((body) => {
|
|
749
|
+
const project = Schema.decodeUnknownSync(ProjectSlugs)(body);
|
|
750
|
+
return project.slug === slugs.project && project.teamSlug === slugs.team;
|
|
751
|
+
}), Effect.catchIf(isRefused, () => Effect.succeed(false)));
|
|
752
|
+
/**
|
|
753
|
+
* The narrowest credential that reaches the deployment. See {@link Access}.
|
|
754
|
+
* When `changesEnv`, a preview deploy key gives way to the access token,
|
|
755
|
+
* because it cannot change variables.
|
|
756
|
+
*/
|
|
757
|
+
const accessFor = (address, changesEnv) => Effect.gen(function* () {
|
|
758
|
+
const key = yield* credentials.deployKey;
|
|
759
|
+
if (key !== void 0) {
|
|
760
|
+
const target = keyTargetOf(Redacted.value(key));
|
|
761
|
+
if (target.kind === "deployment" && address.type !== "preview") {
|
|
762
|
+
const found = yield* deployKeyTarget(key, target.deployment, address);
|
|
763
|
+
if (found !== void 0) return {
|
|
764
|
+
kind: "deployKey",
|
|
765
|
+
key,
|
|
766
|
+
target: found
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
if (target.kind === "preview" && address.type === "preview") {
|
|
770
|
+
const scoped = api.withToken(key);
|
|
771
|
+
if (!(changesEnv && (yield* credentials.hasAccessToken)) && (yield* previewKeyFits(scoped, target, address.projectId))) return {
|
|
772
|
+
kind: "previewKey",
|
|
773
|
+
key,
|
|
774
|
+
api: scoped
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
kind: "token",
|
|
780
|
+
api
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
/** The live deployment of a resource in state, or undefined when it is gone. */
|
|
784
|
+
const live = (access, output) => Effect.gen(function* () {
|
|
785
|
+
if (access.kind === "deployKey") return access.target;
|
|
786
|
+
if (output.type === "prod") return yield* absentAsUndefined(production(access.api, output.projectId));
|
|
787
|
+
const row = yield* getByName(access.api, output.name);
|
|
788
|
+
return row === void 0 ? void 0 : targetOf(row, output.type, output.previewName);
|
|
789
|
+
});
|
|
790
|
+
/** The content identity of `source` and `env` on a deployment, or undefined when there is nothing to push. */
|
|
791
|
+
const wantedHash = (news, deployment) => news.source === void 0 && Object.keys(news.env ?? {}).length === 0 ? Effect.succeed(void 0) : pushHash({
|
|
792
|
+
source: news.source,
|
|
793
|
+
deployment,
|
|
794
|
+
env: news.env ?? {}
|
|
795
|
+
});
|
|
613
796
|
return {
|
|
614
|
-
stables: [
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (output === void 0) return void 0;
|
|
621
|
-
|
|
622
|
-
|
|
797
|
+
stables: [
|
|
798
|
+
"projectId",
|
|
799
|
+
"type",
|
|
800
|
+
"previewName"
|
|
801
|
+
],
|
|
802
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
803
|
+
if (!isResolved(news) || output === void 0) return void 0;
|
|
804
|
+
yield* checkIdentity(output, news);
|
|
805
|
+
const access = yield* accessFor(addressOf(news), changesEnv(news, output));
|
|
806
|
+
if ((yield* live(access, output))?.name !== output.name) return { action: "update" };
|
|
807
|
+
return (yield* wantedHash(news, output.name)) === output.hash ? { action: "noop" } : { action: "update" };
|
|
623
808
|
}),
|
|
624
|
-
|
|
809
|
+
read: Effect.fn(function* ({ olds, output }) {
|
|
625
810
|
if (output !== void 0) {
|
|
626
|
-
|
|
811
|
+
const access = yield* accessFor(addressInState(output), false);
|
|
812
|
+
const current = yield* live(access, output);
|
|
813
|
+
return current === void 0 ? void 0 : {
|
|
814
|
+
...output,
|
|
815
|
+
...current
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const address = addressOf(olds);
|
|
819
|
+
const access = yield* accessFor(address, false);
|
|
820
|
+
if (access.kind === "deployKey") return Unowned({
|
|
821
|
+
...access.target,
|
|
822
|
+
hash: void 0,
|
|
823
|
+
envNames: []
|
|
824
|
+
});
|
|
825
|
+
if (olds.type === "prod") {
|
|
826
|
+
const row = yield* productionRow(access.api, address.projectId);
|
|
827
|
+
if (row === void 0) return void 0;
|
|
828
|
+
const attributes = {
|
|
829
|
+
...targetOf(row, "prod", void 0),
|
|
830
|
+
hash: void 0,
|
|
831
|
+
envNames: []
|
|
832
|
+
};
|
|
833
|
+
return isDeployed(row) ? Unowned(attributes) : attributes;
|
|
627
834
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
835
|
+
const found = yield* lookup(access.api, address);
|
|
836
|
+
if (found === void 0) return void 0;
|
|
837
|
+
return Unowned({
|
|
838
|
+
...found,
|
|
839
|
+
hash: void 0,
|
|
840
|
+
envNames: []
|
|
841
|
+
});
|
|
842
|
+
}),
|
|
843
|
+
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
844
|
+
if (output !== void 0) yield* checkIdentity(output, news);
|
|
845
|
+
if (news.source !== void 0) yield* checkSource(news.source);
|
|
846
|
+
const address = addressOf(news);
|
|
847
|
+
const access = yield* accessFor(address, changesEnv(news, output));
|
|
848
|
+
const env = news.env ?? {};
|
|
849
|
+
if (access.kind === "previewKey" && changesEnv(news, output)) return yield* new EnvironmentNeedsDeployKey({ deployment: output?.name ?? address.name ?? "(new)" });
|
|
850
|
+
const adopt = yield* shouldAdopt(fqn);
|
|
851
|
+
const refuse = (name) => new OwnedBySomeoneElse({
|
|
852
|
+
message: `Project ${address.projectId} already has ${news.type} deployment ${name}${news.type === "prod" ? " with code" : ` named "${news.name}"`}. Re-run with --adopt to take it over${news.type === "prod" ? "" : ", or use another name"}.`,
|
|
853
|
+
resourceType: Deployment.Type,
|
|
854
|
+
physicalName: name
|
|
855
|
+
});
|
|
856
|
+
const { target, created } = yield* Effect.gen(function* () {
|
|
857
|
+
if (access.kind === "deployKey") {
|
|
858
|
+
if (output === void 0 && !adopt) return yield* refuse(access.target.name);
|
|
859
|
+
return {
|
|
860
|
+
target: access.target,
|
|
861
|
+
created: false
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
if (news.type === "prod") {
|
|
865
|
+
const row = yield* productionRow(access.api, address.projectId);
|
|
866
|
+
if (row === void 0) return yield* new ProductionDeploymentNotFound({ projectId: address.projectId });
|
|
867
|
+
if (output === void 0 && isDeployed(row) && !adopt) return yield* refuse(row.name);
|
|
868
|
+
return {
|
|
869
|
+
target: targetOf(row, "prod", void 0),
|
|
870
|
+
created: false
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
if (output !== void 0) {
|
|
874
|
+
const row = yield* getByName(access.api, output.name);
|
|
875
|
+
if (row !== void 0) {
|
|
876
|
+
if (row.deploymentType !== output.type) return yield* new DeploymentIdentityChange({
|
|
877
|
+
deployment: row.name,
|
|
878
|
+
field: "type",
|
|
879
|
+
current: row.deploymentType,
|
|
880
|
+
requested: news.type
|
|
881
|
+
});
|
|
882
|
+
const current = targetOf(row, output.type, output.previewName);
|
|
883
|
+
yield* checkIdentity(current, news);
|
|
884
|
+
return {
|
|
885
|
+
target: current,
|
|
886
|
+
created: false
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const existing = yield* lookup(access.api, address);
|
|
891
|
+
if (existing !== void 0) {
|
|
892
|
+
if (!adopt) return yield* refuse(existing.name);
|
|
893
|
+
return {
|
|
894
|
+
target: existing,
|
|
895
|
+
created: false
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const row = yield* access.api.request("create deployment", (client) => client.POST("/projects/{project_id}/create_deployment", {
|
|
899
|
+
params: { path: { project_id: address.projectId } },
|
|
635
900
|
body: {
|
|
636
|
-
|
|
637
|
-
|
|
901
|
+
type: news.type,
|
|
902
|
+
reference: news.name
|
|
638
903
|
}
|
|
639
|
-
}))
|
|
640
|
-
|
|
904
|
+
}));
|
|
905
|
+
return {
|
|
906
|
+
target: targetOf(Schema.decodeUnknownSync(DeploymentRow$1)(row), news.type, news.name),
|
|
907
|
+
created: true
|
|
908
|
+
};
|
|
641
909
|
});
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
910
|
+
const previousEnvNames = output?.name === target.name ? envNamesOf(output) : [];
|
|
911
|
+
const before = yield* wantedHash(news, target.name);
|
|
912
|
+
if (before === void 0 && previousEnvNames.length === 0) return {
|
|
913
|
+
...target,
|
|
914
|
+
hash: void 0,
|
|
915
|
+
envNames: []
|
|
647
916
|
};
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
* that this resource created. A second delete answers 404.
|
|
672
|
-
*/
|
|
673
|
-
const deleteKey = (projectId, secret) => retryIdempotentWrite(api.requestVoid("delete preview deploy key", (client) => client.POST("/projects/{project_id}/delete_preview_deploy_key", {
|
|
674
|
-
params: { path: { project_id: projectId } },
|
|
675
|
-
body: { id: secret }
|
|
676
|
-
})));
|
|
677
|
-
return {
|
|
678
|
-
stables: ["uniqueName", "projectId"],
|
|
679
|
-
diff: ({ olds, news, output }) => {
|
|
680
|
-
if (!isResolved(news)) return Effect.succeed(void 0);
|
|
681
|
-
return Effect.succeed(output !== void 0 && output.previewDeployKey === void 0 || havePropsChanged(olds, news) ? { action: "replace" } : { action: "noop" });
|
|
682
|
-
},
|
|
683
|
-
read: Effect.fn(function* ({ output }) {
|
|
684
|
-
if (output === void 0) return void 0;
|
|
685
|
-
const entries = yield* absentAsUndefined(listKeys(output.projectId));
|
|
686
|
-
return entries !== void 0 && isKeyListed(entries, output) ? output : void 0;
|
|
687
|
-
}),
|
|
688
|
-
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
689
|
-
if (output !== void 0) {
|
|
690
|
-
if (isKeyListed(yield* listKeys(news.projectId), output)) return output;
|
|
691
|
-
}
|
|
692
|
-
const name = requestedKeyName(news.name, fqn, instanceId);
|
|
693
|
-
const { keyId, uniqueName, secret } = yield* createTrackedKey({
|
|
694
|
-
target: `project ${news.projectId}`,
|
|
695
|
-
name,
|
|
696
|
-
list: listKeys(news.projectId),
|
|
697
|
-
create: api.request("create preview deploy key", (client) => client.POST("/projects/{project_id}/create_preview_deploy_key", {
|
|
698
|
-
params: { path: { project_id: news.projectId } },
|
|
699
|
-
body: { name }
|
|
700
|
-
})).pipe(Effect.map((body) => Schema.decodeUnknownSync(CreatedPreviewDeployKey)(body).previewDeployKey)),
|
|
701
|
-
isCredentialToken: api.isCredentialToken
|
|
702
|
-
});
|
|
917
|
+
if (output?.name === target.name && before === output.hash) return {
|
|
918
|
+
...target,
|
|
919
|
+
hash: output.hash,
|
|
920
|
+
envNames: previousEnvNames
|
|
921
|
+
};
|
|
922
|
+
const options = {
|
|
923
|
+
deployment: target.name,
|
|
924
|
+
url: target.url,
|
|
925
|
+
source: news.source,
|
|
926
|
+
env,
|
|
927
|
+
previousEnvNames,
|
|
928
|
+
adopt
|
|
929
|
+
};
|
|
930
|
+
yield* (access.kind === "deployKey" ? push(spawner, access.key, {
|
|
931
|
+
...options,
|
|
932
|
+
previewName: void 0
|
|
933
|
+
}) : access.kind === "previewKey" ? push(spawner, access.key, {
|
|
934
|
+
...options,
|
|
935
|
+
previewName: target.previewName
|
|
936
|
+
}) : withPushKey(access.api, target.name, news.source, (key) => push(spawner, key, {
|
|
937
|
+
...options,
|
|
938
|
+
previewName: void 0
|
|
939
|
+
}))).pipe(Effect.onError(() => created && access.kind !== "deployKey" ? absentAsUndefined(retryIdempotentWrite(access.api.requestVoid("delete deployment", (client) => client.POST("/deployments/{deployment_name}/delete", { params: { path: { deployment_name: target.name } } })))).pipe(Effect.catch((error) => Effect.logWarning(`Convex.Deployment could not delete ${target.name} after the failed push (${error.message}). The next run takes it over only with --adopt.`))) : Effect.void));
|
|
703
940
|
return {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
previewDeployKey: secret
|
|
941
|
+
...target,
|
|
942
|
+
hash: yield* wantedHash(news, target.name),
|
|
943
|
+
envNames: Object.keys(env).sort()
|
|
708
944
|
};
|
|
709
945
|
}),
|
|
710
946
|
delete: Effect.fn(function* ({ output }) {
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
947
|
+
const keep = Effect.logWarning(`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.`);
|
|
948
|
+
if (output.type === "prod") return yield* keep;
|
|
949
|
+
const access = yield* accessFor(addressInState(output), false);
|
|
950
|
+
const management = access.kind === "deployKey" ? api : access.api;
|
|
951
|
+
const row = yield* getByName(management, output.name);
|
|
952
|
+
if (row === void 0) return;
|
|
953
|
+
if (row.deploymentType === "prod") return yield* keep;
|
|
954
|
+
yield* absentAsUndefined(retryIdempotentWrite(management.requestVoid("delete deployment", (client) => client.POST("/deployments/{deployment_name}/delete", { params: { path: { deployment_name: output.name } } }))));
|
|
717
955
|
})
|
|
718
956
|
};
|
|
719
957
|
}));
|
|
@@ -761,12 +999,32 @@ var CreatedProjectNotFound = class extends Schema.TaggedError()("CreatedProjectN
|
|
|
761
999
|
return `Convex created project ${this.projectId} but GET /projects/${this.projectId} did not return it. Run the deploy again with --adopt to take over project ${this.projectId}.`;
|
|
762
1000
|
}
|
|
763
1001
|
};
|
|
1002
|
+
/** No project in the team has the slug. A `slug` resource never creates one. */
|
|
1003
|
+
var ProjectNotFound = class extends Schema.TaggedError()("ProjectNotFound", {
|
|
1004
|
+
team: Schema.String,
|
|
1005
|
+
slug: Schema.String
|
|
1006
|
+
}) {
|
|
1007
|
+
get message() {
|
|
1008
|
+
return `Team ${this.team} has no project with the slug "${this.slug}", and a Convex.Project with a slug never creates one. Check the slug in the project URL of the Convex dashboard, or use name to create a project.`;
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
/** The props change the slug of a `slug` resource: that is another project. */
|
|
1012
|
+
var ProjectSlugChange = class extends Schema.TaggedError()("ProjectSlugChange", {
|
|
1013
|
+
projectId: Schema.Number,
|
|
1014
|
+
slug: Schema.String,
|
|
1015
|
+
requestedSlug: Schema.String
|
|
1016
|
+
}) {
|
|
1017
|
+
get message() {
|
|
1018
|
+
return `Convex.Project ${this.projectId} has the slug "${this.slug}", and the props ask for "${this.requestedSlug}", which is another project. Set slug back to "${this.slug}". For the other project, create a new resource with a new logical id.`;
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
764
1021
|
/** Fields of GET /projects/{id}, PATCH /projects/{id}, and GET /teams/{team}/projects rows that the provider uses. */
|
|
765
1022
|
const ProjectDetails = Schema.Struct({
|
|
766
1023
|
id: Schema.Number,
|
|
767
1024
|
name: Schema.String,
|
|
768
1025
|
slug: Schema.String,
|
|
769
1026
|
teamId: Schema.Number,
|
|
1027
|
+
teamSlug: Schema.String,
|
|
770
1028
|
prodDeploymentName: Schema.NullOr(Schema.String)
|
|
771
1029
|
});
|
|
772
1030
|
/** GET /teams/{team_id}/projects: one page of projects. */
|
|
@@ -807,6 +1065,8 @@ const checkCreateTimeProps = (projectId, olds, news) => {
|
|
|
807
1065
|
}));
|
|
808
1066
|
return Effect.void;
|
|
809
1067
|
};
|
|
1068
|
+
/** True when `team`, a slug or a numeric id, names the team of the project. */
|
|
1069
|
+
const isTeamOf = (details, team) => typeof team === "number" || /^\d+$/.test(team) ? details.teamId === Number(team) : details.teamSlug === team;
|
|
810
1070
|
const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
811
1071
|
const api = yield* ManagementApi;
|
|
812
1072
|
const prodDeployment = (projectId) => api.request("list deployments", (client) => client.GET("/projects/{project_id}/list_deployments", { params: { path: { project_id: projectId } } })).pipe(Effect.map((rows) => Schema.decodeUnknownSync(DeploymentRows)(rows).find((row) => row.deploymentType === "prod" && row.isDefault)));
|
|
@@ -819,6 +1079,11 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
819
1079
|
prodDeploymentUrl: prod?.deploymentUrl
|
|
820
1080
|
})));
|
|
821
1081
|
const getById = (projectId) => absentAsUndefined(api.request("get project", (client) => client.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } }))).pipe(Effect.map((body) => body === void 0 ? void 0 : Schema.decodeUnknownSync(ProjectDetails)(body)));
|
|
1082
|
+
/** The project with this slug in the team, or undefined. Official endpoint; the team can be a slug. */
|
|
1083
|
+
const getBySlug = (team, slug) => absentAsUndefined(api.request("get project by slug", (client) => client.GET("/teams/{team_id_or_slug}/projects/{project_slug}", { params: { path: {
|
|
1084
|
+
team_id_or_slug: String(team),
|
|
1085
|
+
project_slug: slug
|
|
1086
|
+
} } }))).pipe(Effect.map((body) => body === void 0 ? void 0 : Schema.decodeUnknownSync(ProjectDetails)(body)));
|
|
822
1087
|
/**
|
|
823
1088
|
* The one project in the team with exactly this name, or undefined.
|
|
824
1089
|
* Reads every page. `q` narrows the listing on the server, and the
|
|
@@ -884,6 +1149,22 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
884
1149
|
diff: Effect.fn(function* ({ olds, news, output }) {
|
|
885
1150
|
if (!isResolved(news)) return void 0;
|
|
886
1151
|
if (output === void 0) return void 0;
|
|
1152
|
+
if ("slug" in news) {
|
|
1153
|
+
if (news.slug !== output.slug) return yield* new ProjectSlugChange({
|
|
1154
|
+
projectId: output.projectId,
|
|
1155
|
+
slug: output.slug,
|
|
1156
|
+
requestedSlug: news.slug
|
|
1157
|
+
});
|
|
1158
|
+
if (olds.team !== news.team) {
|
|
1159
|
+
const details = yield* getById(output.projectId);
|
|
1160
|
+
if (details !== void 0 && !isTeamOf(details, news.team)) return yield* new ProjectTeamChange({
|
|
1161
|
+
projectId: output.projectId,
|
|
1162
|
+
teamId: output.teamId,
|
|
1163
|
+
requestedTeamId: yield* api.resolveTeamId(news.team)
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
return { action: "noop" };
|
|
1167
|
+
}
|
|
887
1168
|
if (olds.team !== news.team) {
|
|
888
1169
|
const requestedTeamId = yield* api.resolveTeamId(news.team);
|
|
889
1170
|
if (requestedTeamId !== output.teamId) return yield* new ProjectTeamChange({
|
|
@@ -892,20 +1173,43 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
892
1173
|
requestedTeamId
|
|
893
1174
|
});
|
|
894
1175
|
}
|
|
895
|
-
return olds.name === news.name ? void 0 : { action: "update" };
|
|
1176
|
+
return "name" in olds && olds.name === news.name ? void 0 : { action: "update" };
|
|
896
1177
|
}),
|
|
897
1178
|
read: Effect.fn(function* ({ olds, output }) {
|
|
898
1179
|
if (output !== void 0) {
|
|
899
1180
|
const details = yield* getById(output.projectId);
|
|
900
1181
|
return details === void 0 ? void 0 : yield* attributesOf(details);
|
|
901
1182
|
}
|
|
902
|
-
const existing = yield* findByName(yield* api.resolveTeamId(olds.team), olds.name);
|
|
1183
|
+
const existing = "slug" in olds ? yield* getBySlug(olds.team, olds.slug) : yield* findByName(yield* api.resolveTeamId(olds.team), olds.name);
|
|
903
1184
|
return existing === void 0 ? void 0 : Unowned(yield* attributesOf(existing));
|
|
904
1185
|
}),
|
|
905
1186
|
reconcile: Effect.fn(function* ({ fqn, olds, news, output }) {
|
|
1187
|
+
if ("slug" in news) {
|
|
1188
|
+
const details = output === void 0 ? yield* getBySlug(news.team, news.slug) : yield* getById(output.projectId);
|
|
1189
|
+
if (details === void 0) return yield* new ProjectNotFound({
|
|
1190
|
+
team: String(news.team),
|
|
1191
|
+
slug: news.slug
|
|
1192
|
+
});
|
|
1193
|
+
if (details.slug !== news.slug) return yield* new ProjectSlugChange({
|
|
1194
|
+
projectId: details.id,
|
|
1195
|
+
slug: details.slug,
|
|
1196
|
+
requestedSlug: news.slug
|
|
1197
|
+
});
|
|
1198
|
+
if (!isTeamOf(details, news.team)) return yield* new ProjectTeamChange({
|
|
1199
|
+
projectId: details.id,
|
|
1200
|
+
teamId: details.teamId,
|
|
1201
|
+
requestedTeamId: yield* api.resolveTeamId(news.team)
|
|
1202
|
+
});
|
|
1203
|
+
if (output === void 0 && !(yield* shouldAdopt(fqn))) return yield* new OwnedBySomeoneElse({
|
|
1204
|
+
message: `Convex project ${details.id} ("${news.slug}") exists and is not in this stack's state. Re-run with --adopt to take it over.`,
|
|
1205
|
+
resourceType: Project.Type,
|
|
1206
|
+
physicalName: news.slug
|
|
1207
|
+
});
|
|
1208
|
+
return yield* attributesOf(details);
|
|
1209
|
+
}
|
|
906
1210
|
const teamId = yield* api.resolveTeamId(news.team);
|
|
907
1211
|
if (output !== void 0) {
|
|
908
|
-
if (olds !== void 0) yield* checkCreateTimeProps(output.projectId, olds, news);
|
|
1212
|
+
if (olds !== void 0 && "name" in olds) yield* checkCreateTimeProps(output.projectId, olds, news);
|
|
909
1213
|
if (output.teamId !== teamId) return yield* new ProjectTeamChange({
|
|
910
1214
|
projectId: output.projectId,
|
|
911
1215
|
teamId: output.teamId,
|
|
@@ -946,18 +1250,12 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
946
1250
|
//#region src/Providers.ts
|
|
947
1251
|
var Providers = class extends Provider.ProviderCollection()("Convex") {};
|
|
948
1252
|
/**
|
|
949
|
-
*
|
|
950
|
-
* default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login
|
|
1253
|
+
* The Convex resource providers. Pass a credentials layer to override the
|
|
1254
|
+
* default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login, and
|
|
1255
|
+
* `CONVEX_DEPLOY_KEY`).
|
|
951
1256
|
*/
|
|
952
|
-
const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([
|
|
953
|
-
Project,
|
|
954
|
-
DeployKey,
|
|
955
|
-
PreviewDeployKey,
|
|
956
|
-
EnvironmentVariable,
|
|
957
|
-
DefaultEnvironmentVariable,
|
|
958
|
-
Deploy
|
|
959
|
-
])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeployKeyProvider(), PreviewDeployKeyProvider(), EnvironmentVariableProvider(), DefaultEnvironmentVariableProvider(), DeployProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.provide(CommandExecutorLive()), Layer.orDie);
|
|
1257
|
+
const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([Project, Deployment])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.orDie);
|
|
960
1258
|
//#endregion
|
|
961
|
-
export { ACCESS_TOKEN_ENV,
|
|
1259
|
+
export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, VariablesOwnedBySomeoneElse, fromConfig, fromValues, parseDeployOutput, providers };
|
|
962
1260
|
|
|
963
1261
|
//# sourceMappingURL=index.js.map
|