@samebase/alchemy-convex 0.3.0 → 0.4.1
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 +131 -280
- package/lib/index.d.ts +178 -346
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +754 -905
- package/lib/index.js.map +1 -1
- package/package.json +17 -16
- package/src/Credentials.ts +85 -27
- package/src/Deployment.ts +461 -122
- package/src/ManagementApi.ts +84 -64
- package/src/Project.ts +133 -16
- package/src/Providers.ts +5 -41
- package/src/Push.ts +646 -0
- package/src/index.ts +15 -47
- package/src/Code.ts +0 -463
- package/src/DefaultEnvironmentVariable.ts +0 -263
- package/src/Deploy.ts +0 -183
- package/src/DeployKey.ts +0 -332
- package/src/EnvironmentVariable.ts +0 -289
- package/src/PreviewDeployKey.ts +0 -153
package/lib/index.js
CHANGED
|
@@ -1,49 +1,40 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
5
|
-
import
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import * as Config from "effect/Config";
|
|
6
|
+
import * as Context from "effect/Context";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Layer from "effect/Layer";
|
|
9
|
+
import * as Option from "effect/Option";
|
|
10
|
+
import * as Redacted from "effect/Redacted";
|
|
11
|
+
import * as Result from "effect/Result";
|
|
12
|
+
import * as Schema from "effect/Schema";
|
|
6
13
|
import { Resource } from "alchemy";
|
|
7
14
|
import { AdoptPolicy, OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
|
|
15
|
+
import { isResolved } from "alchemy/Diff";
|
|
8
16
|
import * as Provider from "alchemy/Provider";
|
|
9
|
-
import * as Clock from "effect/Clock";
|
|
10
|
-
import * as Effect from "effect/Effect";
|
|
11
|
-
import * as ChildProcess from "effect/process/ChildProcess";
|
|
12
17
|
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
|
|
13
|
-
import * as Redacted from "effect/Redacted";
|
|
14
|
-
import * as Schema from "effect/Schema";
|
|
15
|
-
import * as Stream from "effect/Stream";
|
|
16
18
|
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
17
19
|
import { Stack } from "alchemy/Stack";
|
|
18
|
-
import * as Option from "effect/Option";
|
|
19
|
-
import { havePropsChanged, isResolved } from "alchemy/Diff";
|
|
20
20
|
import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
|
|
21
|
-
import * as Context from "effect/Context";
|
|
22
|
-
import * as Layer from "effect/Layer";
|
|
23
21
|
import * as Schedule from "effect/Schedule";
|
|
24
|
-
import * as Config from "effect/Config";
|
|
25
|
-
import { CommandExecutor, CommandExecutorLive } from "alchemy/Command";
|
|
26
|
-
import { hashDirectory } from "alchemy/Command/Memo";
|
|
27
22
|
import { createHash } from "node:crypto";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (own !== void 0) return own;
|
|
34
|
-
const policy = yield* Effect.serviceOption(AdoptPolicy);
|
|
35
|
-
if (Option.isSome(policy)) return policy.value;
|
|
36
|
-
const context = yield* Effect.serviceOption(AlchemyContext);
|
|
37
|
-
return Option.isSome(context) && context.value.adopt;
|
|
38
|
-
});
|
|
39
|
-
//#endregion
|
|
23
|
+
import { stripVTControlCharacters } from "node:util";
|
|
24
|
+
import { hashDirectory } from "alchemy/Command/Memo";
|
|
25
|
+
import * as Clock from "effect/Clock";
|
|
26
|
+
import * as ChildProcess from "effect/process/ChildProcess";
|
|
27
|
+
import * as Stream from "effect/Stream";
|
|
40
28
|
//#region src/Credentials.ts
|
|
41
29
|
var Credentials = class extends Context.Service()("@samebase/alchemy-convex/Credentials") {};
|
|
42
|
-
/** Environment variable read by {@link fromConfig}. */
|
|
30
|
+
/** Environment variable for the access token, read by {@link fromConfig}. */
|
|
43
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";
|
|
44
34
|
/** The Convex CLI stores its device login here after `npx convex login`. */
|
|
45
35
|
const convexCliConfigPath = () => join(homedir(), ".convex", "config.json");
|
|
46
36
|
const CliConfig = Schema.Struct({ accessToken: Schema.String });
|
|
37
|
+
var CredentialsError = class extends Schema.TaggedError()("CredentialsError", { reason: Schema.String }) {};
|
|
47
38
|
/**
|
|
48
39
|
* Token the Convex CLI saved on this machine. It works on the Management API
|
|
49
40
|
* for the teams the logged-in user belongs to. Fails when nobody is logged in.
|
|
@@ -52,7 +43,7 @@ const readConvexCliLogin = Effect.gen(function* () {
|
|
|
52
43
|
const path = convexCliConfigPath();
|
|
53
44
|
const text = yield* Effect.tryPromise({
|
|
54
45
|
try: () => readFile(path, "utf8"),
|
|
55
|
-
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}.` })
|
|
56
47
|
});
|
|
57
48
|
const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(CliConfig))(text).pipe(Effect.mapError(() => new CredentialsError({ reason: `${path} has no accessToken` })));
|
|
58
49
|
return {
|
|
@@ -60,22 +51,47 @@ const readConvexCliLogin = Effect.gen(function* () {
|
|
|
60
51
|
source: "convex-cli-login"
|
|
61
52
|
};
|
|
62
53
|
});
|
|
63
|
-
var CredentialsError = class extends Schema.TaggedError()("CredentialsError", { reason: Schema.String }) {};
|
|
64
54
|
/**
|
|
65
|
-
* Default resolution
|
|
66
|
-
*
|
|
67
|
-
* machine
|
|
68
|
-
* 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.
|
|
69
61
|
*/
|
|
70
|
-
const fromConfig = () => Layer.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
};
|
|
78
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
|
+
});
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/Adoption.ts
|
|
85
|
+
/** True when the resource `fqn` may take over an object that is not in state. */
|
|
86
|
+
const shouldAdopt = (fqn) => Effect.gen(function* () {
|
|
87
|
+
const stack = yield* Effect.serviceOption(Stack);
|
|
88
|
+
const own = Option.isSome(stack) ? stack.value.resources[fqn]?.Adopt : void 0;
|
|
89
|
+
if (own !== void 0) return own;
|
|
90
|
+
const policy = yield* Effect.serviceOption(AdoptPolicy);
|
|
91
|
+
if (Option.isSome(policy)) return policy.value;
|
|
92
|
+
const context = yield* Effect.serviceOption(AlchemyContext);
|
|
93
|
+
return Option.isSome(context) && context.value.adopt;
|
|
94
|
+
});
|
|
79
95
|
//#endregion
|
|
80
96
|
//#region src/ManagementApi.ts
|
|
81
97
|
/**
|
|
@@ -150,8 +166,8 @@ const TeamRow = Schema.Struct({
|
|
|
150
166
|
slug: Schema.String
|
|
151
167
|
});
|
|
152
168
|
const TeamRows = Schema.Array(TeamRow);
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
/** The service for one way to get the bearer token. */
|
|
170
|
+
const makeService = (credentials) => {
|
|
155
171
|
const client = Effect.cached(credentials.pipe(Effect.map((resolved) => createManagementClient(Redacted.value(resolved.accessToken))))).pipe(Effect.flatten);
|
|
156
172
|
const request = (operation, run) => client.pipe(Effect.flatMap((c) => settle(operation, () => run(c))));
|
|
157
173
|
const requestVoid = (operation, run) => client.pipe(Effect.flatMap((c) => settleVoid(operation, () => run(c))));
|
|
@@ -180,15 +196,382 @@ const ManagementApiLive = () => Layer.effect(ManagementApi, Effect.gen(function*
|
|
|
180
196
|
return match.id;
|
|
181
197
|
});
|
|
182
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
|
+
}));
|
|
183
203
|
return {
|
|
184
204
|
request,
|
|
185
205
|
requestVoid,
|
|
186
206
|
deploymentRequest,
|
|
187
207
|
resolveTeamId,
|
|
188
|
-
isCredentialToken
|
|
208
|
+
isCredentialToken,
|
|
209
|
+
withToken
|
|
189
210
|
};
|
|
211
|
+
};
|
|
212
|
+
const ManagementApiLive = () => Layer.effect(ManagementApi, Effect.gen(function* () {
|
|
213
|
+
const credentials = yield* Credentials;
|
|
214
|
+
return makeService(credentials.accessToken);
|
|
190
215
|
}));
|
|
191
216
|
//#endregion
|
|
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 });
|
|
237
|
+
/** GET https://<deployment>.convex.cloud/api/v1/list_environment_variables */
|
|
238
|
+
const EnvironmentVariableList = Schema.Struct({ environmentVariables: Schema.Record(Schema.String, Schema.String) });
|
|
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
|
|
257
|
+
}) {
|
|
258
|
+
get message() {
|
|
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}`;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
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.`;
|
|
269
|
+
}
|
|
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
|
+
};
|
|
286
|
+
/**
|
|
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`.
|
|
291
|
+
*/
|
|
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
|
|
301
|
+
};
|
|
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
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* The CLI prints this line with `logFinishedStep` after the push succeeds, to
|
|
315
|
+
* stderr (convex 1.46 `cli/lib/deploy2.js`). The host is
|
|
316
|
+
* `<deployment>.convex.cloud` or `<deployment>.<region>.convex.cloud`.
|
|
317
|
+
*/
|
|
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;
|
|
319
|
+
/** Reads the deployment URL from `convex deploy` output. ANSI escape codes are removed first. */
|
|
320
|
+
const parseDeployOutput = (text) => {
|
|
321
|
+
const match = DEPLOYED_LINE.exec(stripVTControlCharacters(text));
|
|
322
|
+
const url = match?.[1];
|
|
323
|
+
const deploymentName = match?.[2];
|
|
324
|
+
return url === void 0 || deploymentName === void 0 ? void 0 : {
|
|
325
|
+
url,
|
|
326
|
+
deploymentName
|
|
327
|
+
};
|
|
328
|
+
};
|
|
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
|
+
}) {
|
|
352
|
+
get message() {
|
|
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}.`;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
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
|
|
365
|
+
});
|
|
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
|
+
});
|
|
379
|
+
/**
|
|
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.
|
|
384
|
+
*/
|
|
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
|
+
});
|
|
406
|
+
/**
|
|
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.
|
|
410
|
+
*/
|
|
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
|
+
});
|
|
425
|
+
/**
|
|
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.
|
|
430
|
+
*/
|
|
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
|
+
};
|
|
469
|
+
});
|
|
470
|
+
/**
|
|
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`.
|
|
475
|
+
*/
|
|
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
|
|
192
575
|
//#region src/Deployment.ts
|
|
193
576
|
const Deployment = Resource("Convex.Deployment", { defaultRemovalPolicy: "retain" });
|
|
194
577
|
/** The props name another deployment than the one in state. */
|
|
@@ -244,10 +627,24 @@ const DeploymentRow$1 = Schema.Struct({
|
|
|
244
627
|
reference: Schema.String,
|
|
245
628
|
/** Set in list_deployments and create_deployment rows only: GET /deployments/{name} answers null. */
|
|
246
629
|
previewIdentifier: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
247
|
-
deploymentUrl: Schema.String.check(Schema.isPattern(CLOUD_URL))
|
|
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))
|
|
248
633
|
});
|
|
249
634
|
const DeploymentRows$1 = Schema.Array(DeploymentRow$1);
|
|
250
|
-
|
|
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
|
|
646
|
+
});
|
|
647
|
+
const targetOf = (row, type, previewName) => ({
|
|
251
648
|
projectId: row.projectId,
|
|
252
649
|
type,
|
|
253
650
|
name: row.name,
|
|
@@ -256,47 +653,71 @@ const attributesOf = (row, type, previewName) => ({
|
|
|
256
653
|
url: row.deploymentUrl,
|
|
257
654
|
siteUrl: siteUrlOf(row.deploymentUrl)
|
|
258
655
|
});
|
|
259
|
-
/**
|
|
260
|
-
const
|
|
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;
|
|
261
660
|
/**
|
|
262
661
|
* Fails with {@link DeploymentIdentityChange} when the props name another
|
|
263
|
-
* deployment than `current`: the attributes in state, or a live
|
|
662
|
+
* deployment than `current`: the attributes in state, or a live deployment.
|
|
264
663
|
*/
|
|
265
|
-
const checkIdentity
|
|
664
|
+
const checkIdentity = (current, news) => {
|
|
266
665
|
const change = (field, from, to) => Effect.fail(new DeploymentIdentityChange({
|
|
267
666
|
deployment: current.name,
|
|
268
667
|
field,
|
|
269
668
|
current: from,
|
|
270
669
|
requested: to
|
|
271
670
|
}));
|
|
272
|
-
if (current.projectId !== news.projectId) return change("projectId", String(current.projectId), String(news.projectId));
|
|
671
|
+
if (current.projectId !== news.project.projectId) return change("projectId", String(current.projectId), String(news.project.projectId));
|
|
273
672
|
if (current.type !== news.type) return change("type", current.type, news.type);
|
|
274
673
|
const currentName = nameOf(current);
|
|
275
674
|
if (news.type !== "prod" && currentName !== news.name) return change("name", currentName ?? "(none)", news.name);
|
|
276
675
|
return Effect.void;
|
|
277
676
|
};
|
|
677
|
+
const addressOf = (props) => ({
|
|
678
|
+
projectId: props.project.projectId,
|
|
679
|
+
type: props.type,
|
|
680
|
+
name: props.type === "prod" ? void 0 : props.name
|
|
681
|
+
});
|
|
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;
|
|
278
698
|
const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
|
|
279
699
|
const api = yield* ManagementApi;
|
|
700
|
+
const credentials = yield* Credentials;
|
|
701
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
280
702
|
/** The live deployment, or undefined after a 404. GET /deployments/{name} answers no preview identifier. */
|
|
281
|
-
const getByName = (name) => absentAsUndefined(
|
|
282
|
-
const listDeployments = (projectId, type, isDefault) =>
|
|
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: {
|
|
283
705
|
path: { project_id: projectId },
|
|
284
706
|
query: {
|
|
285
707
|
deploymentType: type,
|
|
286
708
|
...isDefault === void 0 ? {} : { isDefault }
|
|
287
709
|
}
|
|
288
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)));
|
|
289
713
|
/** The project's default production deployment, or undefined. */
|
|
290
|
-
const production = (projectId) =>
|
|
291
|
-
const row = rows.find((candidate) => candidate.deploymentType === "prod" && candidate.isDefault);
|
|
292
|
-
return row === void 0 ? void 0 : attributesOf(row, "prod", void 0);
|
|
293
|
-
}));
|
|
714
|
+
const production = (management, projectId) => productionRow(management, projectId).pipe(Effect.map((row) => row === void 0 ? void 0 : targetOf(row, "prod", void 0)));
|
|
294
715
|
/**
|
|
295
716
|
* The one dev deployment with this reference, or the one preview
|
|
296
717
|
* deployment with this preview name, or undefined.
|
|
297
718
|
*/
|
|
298
|
-
const findByName = (projectId, type, name) => Effect.gen(function* () {
|
|
299
|
-
const matches = (yield* listDeployments(projectId, type)).filter((row) => row.deploymentType === type && (type === "dev" ? row.reference === name : row.previewIdentifier === name));
|
|
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));
|
|
300
721
|
if (matches.length > 1) return yield* new AmbiguousDeployment({
|
|
301
722
|
projectId,
|
|
302
723
|
type,
|
|
@@ -305,8 +726,73 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
305
726
|
});
|
|
306
727
|
return matches[0];
|
|
307
728
|
});
|
|
308
|
-
/** The deployment that the
|
|
309
|
-
const lookup = (
|
|
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
|
+
});
|
|
310
796
|
return {
|
|
311
797
|
stables: [
|
|
312
798
|
"projectId",
|
|
@@ -315,852 +801,157 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
315
801
|
],
|
|
316
802
|
diff: Effect.fn(function* ({ news, output }) {
|
|
317
803
|
if (!isResolved(news) || output === void 0) return void 0;
|
|
318
|
-
yield* checkIdentity
|
|
319
|
-
|
|
320
|
-
|
|
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" };
|
|
321
808
|
}),
|
|
322
809
|
read: Effect.fn(function* ({ olds, output }) {
|
|
323
|
-
if (output?.type === "prod") return yield* absentAsUndefined(production(output.projectId));
|
|
324
810
|
if (output !== void 0) {
|
|
325
|
-
const
|
|
326
|
-
|
|
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;
|
|
327
834
|
}
|
|
328
|
-
const found = yield* lookup(
|
|
835
|
+
const found = yield* lookup(access.api, address);
|
|
329
836
|
if (found === void 0) return void 0;
|
|
330
|
-
return
|
|
837
|
+
return Unowned({
|
|
838
|
+
...found,
|
|
839
|
+
hash: void 0,
|
|
840
|
+
envNames: []
|
|
841
|
+
});
|
|
331
842
|
}),
|
|
332
843
|
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
333
|
-
if (output !== void 0) yield* checkIdentity
|
|
334
|
-
if (news.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
};
|
|
351
863
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return existing;
|
|
361
|
-
}
|
|
362
|
-
const created = yield* api.request("create deployment", (client) => client.POST("/projects/{project_id}/create_deployment", {
|
|
363
|
-
params: { path: { project_id: news.projectId } },
|
|
364
|
-
body: {
|
|
365
|
-
type: news.type,
|
|
366
|
-
reference: news.name
|
|
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
|
+
};
|
|
367
872
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (live.deploymentType === "prod") return yield* keep;
|
|
377
|
-
yield* absentAsUndefined(retryIdempotentWrite(api.requestVoid("delete deployment", (client) => client.POST("/deployments/{deployment_name}/delete", { params: { path: { deployment_name: output.name } } }))));
|
|
378
|
-
})
|
|
379
|
-
};
|
|
380
|
-
}));
|
|
381
|
-
//#endregion
|
|
382
|
-
//#region src/EnvironmentVariable.ts
|
|
383
|
-
/**
|
|
384
|
-
* The variable's identity when the listing has `name`. Own keys only: a
|
|
385
|
-
* valid variable name such as `toString` must not match the object prototype.
|
|
386
|
-
*/
|
|
387
|
-
const findVariable = (list, deployment, name) => Object.hasOwn(list.environmentVariables, name) ? {
|
|
388
|
-
deployment,
|
|
389
|
-
name
|
|
390
|
-
} : void 0;
|
|
391
|
-
/**
|
|
392
|
-
* One attempt to set variables, or to remove those whose `value` is null, in
|
|
393
|
-
* one call. The deployment answers 200 with an empty body, so this reads no
|
|
394
|
-
* response data.
|
|
395
|
-
*/
|
|
396
|
-
const writeVariables = (deployment, deployKey, changes) => settleVoid("update environment variables", () => createDeploymentClient(deployment, Redacted.value(deployKey)).POST("/update_environment_variables", { body: { changes: changes.map(({ name, value }) => ({
|
|
397
|
-
name,
|
|
398
|
-
value: Redacted.isRedacted(value) ? Redacted.value(value) : value
|
|
399
|
-
})) } }));
|
|
400
|
-
/** One attempt to set one variable, or to remove it when `value` is null. */
|
|
401
|
-
const writeVariable = (deployment, deployKey, name, value) => writeVariables(deployment, deployKey, [{
|
|
402
|
-
name,
|
|
403
|
-
value
|
|
404
|
-
}]);
|
|
405
|
-
/**
|
|
406
|
-
* Sets one variable, or removes it when `value` is null. The call sets a
|
|
407
|
-
* value, so it runs again after a write conflict or a 5xx answer, for example
|
|
408
|
-
* when another resource writes a variable of the same deployment at the same
|
|
409
|
-
* time. Use it only where the variable is already ours to set.
|
|
410
|
-
*/
|
|
411
|
-
const updateVariable = (deployment, deployKey, name, value) => retryIdempotentWrite(writeVariable(deployment, deployKey, name, value));
|
|
412
|
-
/** GET https://<deployment>.convex.cloud/api/v1/list_environment_variables */
|
|
413
|
-
const EnvironmentVariableList = Schema.Struct({ environmentVariables: Schema.Record(Schema.String, Schema.String) });
|
|
414
|
-
/** Every variable of the deployment with its value. Fails with a 404 when the deployment is gone. */
|
|
415
|
-
const listVariables = (deployment, deployKey) => settle("list environment variables", () => createDeploymentClient(deployment, Redacted.value(deployKey)).GET("/list_environment_variables")).pipe(Effect.map((body) => Schema.decodeUnknownSync(EnvironmentVariableList)(body).environmentVariables));
|
|
416
|
-
/** The variable's identity, or undefined when the variable or the deployment is absent. */
|
|
417
|
-
const readVariable = (api, deployment, deployKey, name) => absentAsUndefined(api.deploymentRequest("list environment variables", deployment, deployKey, (client) => client.GET("/list_environment_variables"))).pipe(Effect.map((body) => body === void 0 ? void 0 : findVariable(Schema.decodeUnknownSync(EnvironmentVariableList)(body), deployment, name)));
|
|
418
|
-
const EnvironmentVariable = Resource("Convex.EnvironmentVariable");
|
|
419
|
-
/**
|
|
420
|
-
* The props name another variable than the one in state. Used by
|
|
421
|
-
* Convex.EnvironmentVariable and Convex.DefaultEnvironmentVariable.
|
|
422
|
-
*/
|
|
423
|
-
var VariableIdentityChange = class extends Schema.TaggedError()("VariableIdentityChange", {
|
|
424
|
-
resourceType: Schema.String,
|
|
425
|
-
field: Schema.Literals([
|
|
426
|
-
"name",
|
|
427
|
-
"deployment",
|
|
428
|
-
"projectId",
|
|
429
|
-
"deploymentType"
|
|
430
|
-
]),
|
|
431
|
-
current: Schema.String,
|
|
432
|
-
requested: Schema.String
|
|
433
|
-
}) {
|
|
434
|
-
get message() {
|
|
435
|
-
return `${this.resourceType} cannot change its ${this.field} from ${this.current} to ${this.requested}. Create a new resource with a new logical id for the new name or deployment, and remove this one.`;
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
/** Fails with {@link VariableIdentityChange} when the props name another variable than the one in state. */
|
|
439
|
-
const checkIdentity$1 = (output, news) => output.name !== news.name ? Effect.fail(new VariableIdentityChange({
|
|
440
|
-
resourceType: EnvironmentVariable.Type,
|
|
441
|
-
field: "name",
|
|
442
|
-
current: output.name,
|
|
443
|
-
requested: news.name
|
|
444
|
-
})) : output.deployment !== news.deployment ? Effect.fail(new VariableIdentityChange({
|
|
445
|
-
resourceType: EnvironmentVariable.Type,
|
|
446
|
-
field: "deployment",
|
|
447
|
-
current: output.deployment,
|
|
448
|
-
requested: news.deployment
|
|
449
|
-
})) : Effect.void;
|
|
450
|
-
/**
|
|
451
|
-
* The deploy key for the variable in state: from the attributes, or, for
|
|
452
|
-
* state from 0.1.x, from props that name the same deployment.
|
|
453
|
-
*/
|
|
454
|
-
const keyOf = (output, olds) => output.deployKey ?? (olds?.deployment === output.deployment ? olds.deployKey : void 0);
|
|
455
|
-
const EnvironmentVariableProvider = () => Provider.effect(EnvironmentVariable, Effect.gen(function* () {
|
|
456
|
-
const api = yield* ManagementApi;
|
|
457
|
-
return {
|
|
458
|
-
diff: Effect.fn(function* ({ news, output }) {
|
|
459
|
-
if (!isResolved(news) || output === void 0) return void 0;
|
|
460
|
-
yield* checkIdentity$1(output, news);
|
|
461
|
-
}),
|
|
462
|
-
read: Effect.fn(function* ({ olds, output }) {
|
|
463
|
-
if (output !== void 0) {
|
|
464
|
-
const key = keyOf(output, olds);
|
|
465
|
-
if (key === void 0) return output;
|
|
466
|
-
return (yield* readVariable(api, output.deployment, key, output.name)) === void 0 ? void 0 : output;
|
|
467
|
-
}
|
|
468
|
-
const found = yield* readVariable(api, olds.deployment, olds.deployKey, olds.name);
|
|
469
|
-
return found === void 0 ? void 0 : Unowned({
|
|
470
|
-
...found,
|
|
471
|
-
deployKey: olds.deployKey
|
|
472
|
-
});
|
|
473
|
-
}),
|
|
474
|
-
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
475
|
-
if (output !== void 0) yield* checkIdentity$1(output, news);
|
|
476
|
-
const write = writeVariable(news.deployment, news.deployKey, news.name, news.value);
|
|
477
|
-
if (output !== void 0 || (yield* shouldAdopt(fqn))) yield* retryIdempotentWrite(write);
|
|
478
|
-
else yield* retryIdempotentWrite(Effect.gen(function* () {
|
|
479
|
-
if ((yield* readVariable(api, news.deployment, news.deployKey, news.name)) !== void 0) return yield* new OwnedBySomeoneElse({
|
|
480
|
-
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.`,
|
|
481
|
-
resourceType: EnvironmentVariable.Type,
|
|
482
|
-
physicalName: news.name
|
|
483
|
-
});
|
|
484
|
-
yield* write;
|
|
485
|
-
}));
|
|
486
|
-
return {
|
|
487
|
-
deployment: news.deployment,
|
|
488
|
-
name: news.name,
|
|
489
|
-
deployKey: news.deployKey
|
|
490
|
-
};
|
|
491
|
-
}),
|
|
492
|
-
delete: Effect.fn(function* ({ olds, output }) {
|
|
493
|
-
const key = keyOf(output, olds);
|
|
494
|
-
if (key === void 0) {
|
|
495
|
-
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.`);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
yield* absentAsUndefined(updateVariable(output.deployment, key, output.name, null));
|
|
499
|
-
})
|
|
500
|
-
};
|
|
501
|
-
}));
|
|
502
|
-
//#endregion
|
|
503
|
-
//#region src/Code.ts
|
|
504
|
-
const Code = Resource("Convex.Code");
|
|
505
|
-
/**
|
|
506
|
-
* The CLI prints this line with `logFinishedStep` after the push succeeds, to
|
|
507
|
-
* stderr (convex 1.46 `cli/lib/deploy2.js` for existing deployments,
|
|
508
|
-
* `cli/deploy.js` for preview deployments). The host is
|
|
509
|
-
* `<deployment>.convex.cloud` or `<deployment>.<region>.convex.cloud`.
|
|
510
|
-
*/
|
|
511
|
-
const DEPLOYED_LINE = /Deployed Convex functions to (https:\/\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud)\/?(?=\s|$)/m;
|
|
512
|
-
/** Reads the deployment URL from `convex deploy` output. ANSI escape codes are removed first. */
|
|
513
|
-
const parseDeployOutput = (text) => {
|
|
514
|
-
const match = DEPLOYED_LINE.exec(stripVTControlCharacters(text));
|
|
515
|
-
const url = match?.[1];
|
|
516
|
-
const deploymentName = match?.[2];
|
|
517
|
-
return url === void 0 || deploymentName === void 0 ? void 0 : {
|
|
518
|
-
url,
|
|
519
|
-
deploymentName
|
|
520
|
-
};
|
|
521
|
-
};
|
|
522
|
-
/**
|
|
523
|
-
* What a deploy key targets, from its prefix: `<type>:<deployment>|<secret>`
|
|
524
|
-
* (or `<deployment>|<secret>` in old keys) for one deployment,
|
|
525
|
-
* `preview:<team>:<project>|<secret>` for preview deployments of a project,
|
|
526
|
-
* `project:<team>:<project>|<secret>` for the project's production
|
|
527
|
-
* deployment. Same rules as convex 1.46 `cli/lib/deployment.js`.
|
|
528
|
-
*/
|
|
529
|
-
const keyTargetOf = (key) => {
|
|
530
|
-
const bar = key.indexOf("|");
|
|
531
|
-
if (bar <= 0 || /[\s#'"`]/.test(key)) return { kind: "unknown" };
|
|
532
|
-
const parts = key.slice(0, bar).split(":");
|
|
533
|
-
const [first, second] = parts;
|
|
534
|
-
if (parts.length === 3 && first === "preview") return { kind: "preview" };
|
|
535
|
-
if (parts.length === 3 && first === "project") return { kind: "project" };
|
|
536
|
-
if (parts.length === 2 && second) return {
|
|
537
|
-
kind: "deployment",
|
|
538
|
-
deployment: second
|
|
539
|
-
};
|
|
540
|
-
if (parts.length === 1 && first) return {
|
|
541
|
-
kind: "deployment",
|
|
542
|
-
deployment: first
|
|
543
|
-
};
|
|
544
|
-
return { kind: "unknown" };
|
|
545
|
-
};
|
|
546
|
-
/** The deploy key does not fit the deployment. Checked before the push. */
|
|
547
|
-
var DeployKeyMismatch = class extends Schema.TaggedError()("DeployKeyMismatch", {
|
|
548
|
-
/** The deployment that should receive the push. */
|
|
549
|
-
deployment: Schema.String,
|
|
550
|
-
/** What the key targets. */
|
|
551
|
-
key: Schema.Literals([
|
|
552
|
-
"deployment",
|
|
553
|
-
"preview",
|
|
554
|
-
"project",
|
|
555
|
-
"unknown"
|
|
556
|
-
]),
|
|
557
|
-
/** For a deployment key: the deployment in the key prefix. Never the secret. */
|
|
558
|
-
keyDeployment: Schema.UndefinedOr(Schema.String)
|
|
559
|
-
}) {
|
|
560
|
-
get message() {
|
|
561
|
-
switch (this.key) {
|
|
562
|
-
case "deployment": return `The deploy key is for deployment ${this.keyDeployment}, not for ${this.deployment}. Pass a deploy key of ${this.deployment}, such as Convex.DeployKey with deployment ${this.deployment}.`;
|
|
563
|
-
case "preview": return `A preview deploy key pushes to a preview deployment by its preview name, and ${this.deployment} is not a Convex.Deployment of type "preview". Pass the Convex.Deployment preview resource as deployment, or a deploy key of ${this.deployment}.`;
|
|
564
|
-
case "project": return `Convex.Code does not take a project deploy key: it cannot check before the push that the key targets ${this.deployment}. Pass a deploy key of ${this.deployment} (Convex.DeployKey).`;
|
|
565
|
-
case "unknown": return `deployKey is not a Convex deploy key ("<type>:<deployment>|<secret>" or "preview:<team>:<project>|<secret>"). Pass Convex.DeployKey(...).deployKey or Convex.PreviewDeployKey(...).previewDeployKey.`;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
/** `env` needs the deployment API, which takes only a deploy key of the deployment. */
|
|
570
|
-
var EnvironmentNeedsDeployKey = class extends Schema.TaggedError()("EnvironmentNeedsDeployKey", { deployment: Schema.String }) {
|
|
571
|
-
get message() {
|
|
572
|
-
return `Convex.Code cannot set or remove env variables on ${this.deployment} with a preview deploy key: the deployment API takes only a deploy key of the deployment. Pass a Convex.DeployKey of ${this.deployment}, or use Convex.DefaultEnvironmentVariable with deploymentType "preview" for values that every new preview deployment gets.`;
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
/** No `convex` package resolves from `cwd`. */
|
|
576
|
-
var ConvexCliNotFound = class extends Schema.TaggedError()("ConvexCliNotFound", { cwd: Schema.String }) {
|
|
577
|
-
get message() {
|
|
578
|
-
return `Convex.Code found no installed convex package from ${this.cwd}, and it does not download one. Install convex in the app (npm install convex), set cwd to the app directory, or set the command prop.`;
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
/** The Convex CLI failed, or exited 0 without the deployment URL. The output tail never contains the key. */
|
|
582
|
-
var CodePushFailed = class extends Schema.TaggedError()("CodePushFailed", {
|
|
583
|
-
/** Exit code of the CLI. Undefined when it did not start or a signal stopped it. */
|
|
584
|
-
exitCode: Schema.UndefinedOr(Schema.Number),
|
|
585
|
-
/** The last lines of the CLI output, with the deploy key removed. */
|
|
586
|
-
outputTail: Schema.String
|
|
587
|
-
}) {
|
|
588
|
-
get message() {
|
|
589
|
-
return this.exitCode === 0 ? `convex deploy exited 0 but printed no "Deployed Convex functions to <url>" line, so Convex.Code has no deployment URL. 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}`;
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
/** The CLI pushed to another deployment than the one in the props. Checked after the push. */
|
|
593
|
-
var PushTargetMismatch = class extends Schema.TaggedError()("PushTargetMismatch", {
|
|
594
|
-
expected: Schema.String,
|
|
595
|
-
actual: Schema.String
|
|
596
|
-
}) {
|
|
597
|
-
get message() {
|
|
598
|
-
return `The Convex CLI pushed the functions to deployment ${this.actual}, but Convex.Code expected ${this.expected}. With a preview deploy key, this happens when the key belongs to another project, or when the preview deployment ${this.expected} expired during the deploy and the CLI created ${this.actual} for the same preview name. Check the key. After an expiry, run the deploy again with --adopt, so that Convex.Deployment takes over ${this.actual}.`;
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
/**
|
|
602
|
-
* Checks that the key fits the deployment, and returns the preview name for
|
|
603
|
-
* `--preview-name` when the key is a preview deploy key.
|
|
604
|
-
*/
|
|
605
|
-
const previewNameFor = (target, keyTarget, changesEnvironment) => {
|
|
606
|
-
const mismatch = new DeployKeyMismatch({
|
|
607
|
-
deployment: target.name,
|
|
608
|
-
key: keyTarget.kind,
|
|
609
|
-
keyDeployment: keyTarget.kind === "deployment" ? keyTarget.deployment : void 0
|
|
610
|
-
});
|
|
611
|
-
switch (keyTarget.kind) {
|
|
612
|
-
case "deployment": return keyTarget.deployment === target.name ? Effect.succeed(void 0) : Effect.fail(mismatch);
|
|
613
|
-
case "preview":
|
|
614
|
-
if (target.type !== "preview" || target.previewName === void 0) return Effect.fail(mismatch);
|
|
615
|
-
return changesEnvironment ? Effect.fail(new EnvironmentNeedsDeployKey({ deployment: target.name })) : Effect.succeed(target.previewName);
|
|
616
|
-
case "project":
|
|
617
|
-
case "unknown": return Effect.fail(mismatch);
|
|
618
|
-
default: return keyTarget;
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
/** The last non-empty lines of `text`, without escape codes and with every secret replaced. */
|
|
622
|
-
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");
|
|
623
|
-
const CliManifest = Schema.Struct({ bin: Schema.Union([Schema.String, Schema.Record(Schema.String, Schema.String)]) });
|
|
624
|
-
/** `[node, <convex bin>]` for the `convex` package that `cwd` resolves. Never downloads. */
|
|
625
|
-
const installedCli = (cwd) => Effect.tryPromise({
|
|
626
|
-
try: async () => {
|
|
627
|
-
const manifestPath = createRequire(join(cwd, "package.json")).resolve("convex/package.json");
|
|
628
|
-
const { bin } = Schema.decodeUnknownSync(Schema.fromJsonString(CliManifest))(await readFile(manifestPath, "utf8"));
|
|
629
|
-
const entry = typeof bin === "string" ? bin : bin["convex"];
|
|
630
|
-
if (entry === void 0) throw new Error("the convex package has no convex bin");
|
|
631
|
-
return [process.execPath, resolve(dirname(manifestPath), entry)];
|
|
632
|
-
},
|
|
633
|
-
catch: () => new ConvexCliNotFound({ cwd })
|
|
634
|
-
});
|
|
635
|
-
/**
|
|
636
|
-
* Runs `use` with an env file that holds the key, in a new temporary
|
|
637
|
-
* directory, and removes the directory afterwards: on success, on failure,
|
|
638
|
-
* and on interruption.
|
|
639
|
-
*/
|
|
640
|
-
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({
|
|
641
|
-
envFile: join(home, "deploy.env"),
|
|
642
|
-
home
|
|
643
|
-
}))), (home) => Effect.promise(() => rm(home, {
|
|
644
|
-
recursive: true,
|
|
645
|
-
force: true
|
|
646
|
-
})));
|
|
647
|
-
/**
|
|
648
|
-
* The environment of the CLI: this process's variables without `CONVEX_*`,
|
|
649
|
-
* with `home` as the home directory.
|
|
650
|
-
*/
|
|
651
|
-
const cliEnvironment = (home) => ({
|
|
652
|
-
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("CONVEX_"))),
|
|
653
|
-
HOME: home,
|
|
654
|
-
USERPROFILE: home,
|
|
655
|
-
XDG_CONFIG_HOME: home
|
|
656
|
-
});
|
|
657
|
-
const CodeProvider = () => Provider.effect(Code, Effect.gen(function* () {
|
|
658
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
659
|
-
/** Runs the CLI with piped output. Interruption stops its process group. */
|
|
660
|
-
const run = (argv, cwd, home) => Effect.gen(function* () {
|
|
661
|
-
const [command, ...args] = argv;
|
|
662
|
-
const child = yield* spawner.spawn(ChildProcess.make(command, args, {
|
|
663
|
-
cwd,
|
|
664
|
-
env: cliEnvironment(home),
|
|
665
|
-
extendEnv: false,
|
|
666
|
-
stdin: "ignore",
|
|
667
|
-
stdout: "pipe",
|
|
668
|
-
stderr: "pipe"
|
|
669
|
-
}));
|
|
670
|
-
const [exitCode, stdout, stderr] = yield* Effect.all([
|
|
671
|
-
child.exitCode,
|
|
672
|
-
Stream.mkString(Stream.decodeText(child.stdout)),
|
|
673
|
-
Stream.mkString(Stream.decodeText(child.stderr))
|
|
674
|
-
], { concurrency: "unbounded" });
|
|
675
|
-
return {
|
|
676
|
-
exitCode: Number(exitCode),
|
|
677
|
-
output: `${stdout}\n${stderr}`
|
|
678
|
-
};
|
|
679
|
-
}).pipe(Effect.scoped);
|
|
680
|
-
return {
|
|
681
|
-
diff: () => Effect.succeed({ action: "update" }),
|
|
682
|
-
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
683
|
-
const target = typeof news.deployment === "string" ? {
|
|
684
|
-
name: news.deployment,
|
|
685
|
-
address: news.deployment,
|
|
686
|
-
type: void 0,
|
|
687
|
-
previewName: void 0
|
|
688
|
-
} : {
|
|
689
|
-
...news.deployment,
|
|
690
|
-
address: news.deployment.url
|
|
691
|
-
};
|
|
692
|
-
const key = Redacted.value(news.deployKey);
|
|
693
|
-
const secrets = [key, key.slice(key.indexOf("|") + 1)].filter((secret) => secret.length >= 8);
|
|
694
|
-
const env = Object.entries(news.env ?? {}).sort(([a], [b]) => a < b ? -1 : 1);
|
|
695
|
-
const previous = output !== void 0 && output.deploymentName === target.name ? output.envNames : [];
|
|
696
|
-
const removed = previous.filter((name) => !env.some(([envName]) => envName === name));
|
|
697
|
-
const previewName = yield* previewNameFor(target, keyTargetOf(key), env.length > 0 || removed.length > 0);
|
|
698
|
-
const cwd = resolve(news.cwd);
|
|
699
|
-
const program = news.command ?? (yield* installedCli(cwd));
|
|
700
|
-
if (env.length > 0 || removed.length > 0) {
|
|
701
|
-
const adopt = yield* shouldAdopt(fqn);
|
|
702
|
-
const fresh = env.filter(([name]) => !previous.includes(name));
|
|
703
|
-
yield* retryIdempotentWrite(Effect.gen(function* () {
|
|
704
|
-
if (fresh.length > 0 && !adopt) {
|
|
705
|
-
const current = yield* listVariables(target.address, news.deployKey);
|
|
706
|
-
const taken = fresh.filter(([name, value]) => Object.hasOwn(current, name) && current[name] !== (Redacted.isRedacted(value) ? Redacted.value(value) : value)).map(([name]) => name);
|
|
707
|
-
if (taken.length > 0) return yield* new OwnedBySomeoneElse({
|
|
708
|
-
message: `Environment variables ${taken.join(", ")} already exist on deployment ${target.name} with other values. Re-run with --adopt to take them over and overwrite them, or remove them from env.`,
|
|
709
|
-
resourceType: Code.Type,
|
|
710
|
-
physicalName: target.name
|
|
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
|
|
711
881
|
});
|
|
882
|
+
const current = targetOf(row, output.type, output.previewName);
|
|
883
|
+
yield* checkIdentity(current, news);
|
|
884
|
+
return {
|
|
885
|
+
target: current,
|
|
886
|
+
created: false
|
|
887
|
+
};
|
|
712
888
|
}
|
|
713
|
-
yield* writeVariables(target.address, news.deployKey, [...env.map(([name, value]) => ({
|
|
714
|
-
name,
|
|
715
|
-
value
|
|
716
|
-
})), ...removed.map((name) => ({
|
|
717
|
-
name,
|
|
718
|
-
value: null
|
|
719
|
-
}))]);
|
|
720
|
-
}));
|
|
721
|
-
}
|
|
722
|
-
const args = [
|
|
723
|
-
"deploy",
|
|
724
|
-
...previewName === void 0 ? [] : ["--preview-name", previewName],
|
|
725
|
-
...news.typecheck === void 0 ? [] : [`--typecheck=${news.typecheck}`],
|
|
726
|
-
...news.codegen === void 0 ? [] : [`--codegen=${news.codegen}`]
|
|
727
|
-
];
|
|
728
|
-
const result = yield* withKeyFile(key, ({ envFile, home }) => run([
|
|
729
|
-
...program,
|
|
730
|
-
...args,
|
|
731
|
-
"--env-file",
|
|
732
|
-
envFile
|
|
733
|
-
], cwd, home)).pipe(Effect.mapError((error) => new CodePushFailed({
|
|
734
|
-
exitCode: void 0,
|
|
735
|
-
outputTail: outputTail(error.message, secrets)
|
|
736
|
-
})));
|
|
737
|
-
const deployed = parseDeployOutput(result.output);
|
|
738
|
-
if (result.exitCode !== 0 || deployed === void 0) return yield* new CodePushFailed({
|
|
739
|
-
exitCode: result.exitCode,
|
|
740
|
-
outputTail: outputTail(result.output, secrets)
|
|
741
|
-
});
|
|
742
|
-
if (deployed.deploymentName !== target.name) return yield* new PushTargetMismatch({
|
|
743
|
-
expected: target.name,
|
|
744
|
-
actual: deployed.deploymentName
|
|
745
|
-
});
|
|
746
|
-
return {
|
|
747
|
-
deploymentName: deployed.deploymentName,
|
|
748
|
-
url: deployed.url,
|
|
749
|
-
siteUrl: siteUrlOf(deployed.url),
|
|
750
|
-
deployedAt: yield* Clock.currentTimeMillis,
|
|
751
|
-
envNames: env.map(([name]) => name)
|
|
752
|
-
};
|
|
753
|
-
}),
|
|
754
|
-
delete: () => Effect.void
|
|
755
|
-
};
|
|
756
|
-
}));
|
|
757
|
-
//#endregion
|
|
758
|
-
//#region src/DefaultEnvironmentVariable.ts
|
|
759
|
-
/**
|
|
760
|
-
* Attributes when the listing has `name` for `deploymentType`. An unfiltered
|
|
761
|
-
* listing merges the deployment types that share one value into one row, so
|
|
762
|
-
* the match checks membership in `deploymentTypes`.
|
|
763
|
-
*/
|
|
764
|
-
const findDefault = (list, projectId, name, deploymentType) => list.items.some((item) => item.name === name && item.deploymentTypes.includes(deploymentType)) ? {
|
|
765
|
-
projectId,
|
|
766
|
-
name,
|
|
767
|
-
deploymentType
|
|
768
|
-
} : void 0;
|
|
769
|
-
/**
|
|
770
|
-
* Lists the defaults named `name` for `deploymentType`. Convex filters on
|
|
771
|
-
* both, so the listing has at most one row and needs no pagination.
|
|
772
|
-
*/
|
|
773
|
-
const listDefaults = (api, projectId, name, deploymentType) => api.request("list default environment variables", (client) => client.GET("/projects/{project_id}/list_default_environment_variables", { params: {
|
|
774
|
-
path: { project_id: projectId },
|
|
775
|
-
query: {
|
|
776
|
-
name,
|
|
777
|
-
deploymentType
|
|
778
|
-
}
|
|
779
|
-
} })).pipe(Effect.map((body) => Schema.decodeUnknownSync(DefaultEnvironmentVariableList)(body)));
|
|
780
|
-
/** One attempt to set one default, or to remove it when `value` is null. */
|
|
781
|
-
const writeDefault = (api, projectId, name, deploymentType, value) => api.requestVoid("update default environment variables", (client) => client.POST("/projects/{project_id}/update_default_environment_variables", {
|
|
782
|
-
params: { path: { project_id: projectId } },
|
|
783
|
-
body: { changes: [{
|
|
784
|
-
name,
|
|
785
|
-
deploymentType,
|
|
786
|
-
value: Redacted.isRedacted(value) ? Redacted.value(value) : value
|
|
787
|
-
}] }
|
|
788
|
-
}));
|
|
789
|
-
/**
|
|
790
|
-
* Sets one default, or removes it when `value` is null. Convex answers 200
|
|
791
|
-
* with an empty body, so this reads no response data. The call sets a value,
|
|
792
|
-
* so it runs again after a write conflict or a 5xx answer, for example when
|
|
793
|
-
* another resource writes a default of the same project at the same time.
|
|
794
|
-
*/
|
|
795
|
-
const updateDefault = (api, projectId, name, deploymentType, value) => retryIdempotentWrite(writeDefault(api, projectId, name, deploymentType, value));
|
|
796
|
-
/** The default's identity, or undefined when the default or the project is absent. */
|
|
797
|
-
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)));
|
|
798
|
-
/**
|
|
799
|
-
* GET /projects/{project_id}/list_default_environment_variables. Only the
|
|
800
|
-
* fields the provider uses: the row's `value` is the secret in plain text.
|
|
801
|
-
*/
|
|
802
|
-
const DefaultEnvironmentVariableList = Schema.Struct({ items: Schema.Array(Schema.Struct({
|
|
803
|
-
name: Schema.String,
|
|
804
|
-
deploymentTypes: Schema.Array(Schema.String)
|
|
805
|
-
})) });
|
|
806
|
-
const DefaultEnvironmentVariable = Resource("Convex.DefaultEnvironmentVariable");
|
|
807
|
-
/** Fails with VariableIdentityChange when the props name another default than the one in state. */
|
|
808
|
-
const checkIdentity = (output, news) => {
|
|
809
|
-
const changed = output.projectId !== news.projectId ? {
|
|
810
|
-
field: "projectId",
|
|
811
|
-
current: `${output.projectId}`,
|
|
812
|
-
requested: `${news.projectId}`
|
|
813
|
-
} : output.name !== news.name ? {
|
|
814
|
-
field: "name",
|
|
815
|
-
current: output.name,
|
|
816
|
-
requested: news.name
|
|
817
|
-
} : output.deploymentType !== news.deploymentType ? {
|
|
818
|
-
field: "deploymentType",
|
|
819
|
-
current: output.deploymentType,
|
|
820
|
-
requested: news.deploymentType
|
|
821
|
-
} : void 0;
|
|
822
|
-
return changed === void 0 ? Effect.void : Effect.fail(new VariableIdentityChange({
|
|
823
|
-
resourceType: DefaultEnvironmentVariable.Type,
|
|
824
|
-
...changed
|
|
825
|
-
}));
|
|
826
|
-
};
|
|
827
|
-
const DefaultEnvironmentVariableProvider = () => Provider.effect(DefaultEnvironmentVariable, Effect.gen(function* () {
|
|
828
|
-
const api = yield* ManagementApi;
|
|
829
|
-
return {
|
|
830
|
-
diff: Effect.fn(function* ({ news, output }) {
|
|
831
|
-
if (!isResolved(news) || output === void 0) return void 0;
|
|
832
|
-
yield* checkIdentity(output, news);
|
|
833
|
-
}),
|
|
834
|
-
read: Effect.fn(function* ({ olds, output }) {
|
|
835
|
-
const at = output ?? olds;
|
|
836
|
-
const found = yield* readDefault(api, at.projectId, at.name, at.deploymentType);
|
|
837
|
-
if (found === void 0) return void 0;
|
|
838
|
-
return output ?? Unowned(found);
|
|
839
|
-
}),
|
|
840
|
-
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
841
|
-
if (output !== void 0) yield* checkIdentity(output, news);
|
|
842
|
-
const write = writeDefault(api, news.projectId, news.name, news.deploymentType, news.value);
|
|
843
|
-
if (output !== void 0 || (yield* shouldAdopt(fqn))) yield* retryIdempotentWrite(write);
|
|
844
|
-
else yield* retryIdempotentWrite(Effect.gen(function* () {
|
|
845
|
-
if ((yield* readDefault(api, news.projectId, news.name, news.deploymentType)) !== void 0) return yield* new OwnedBySomeoneElse({
|
|
846
|
-
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.`,
|
|
847
|
-
resourceType: DefaultEnvironmentVariable.Type,
|
|
848
|
-
physicalName: news.name
|
|
849
|
-
});
|
|
850
|
-
yield* write;
|
|
851
|
-
}));
|
|
852
|
-
return {
|
|
853
|
-
projectId: news.projectId,
|
|
854
|
-
name: news.name,
|
|
855
|
-
deploymentType: news.deploymentType
|
|
856
|
-
};
|
|
857
|
-
}),
|
|
858
|
-
delete: Effect.fn(function* ({ output }) {
|
|
859
|
-
yield* absentAsUndefined(updateDefault(api, output.projectId, output.name, output.deploymentType, null));
|
|
860
|
-
})
|
|
861
|
-
};
|
|
862
|
-
}));
|
|
863
|
-
//#endregion
|
|
864
|
-
//#region src/Deploy.ts
|
|
865
|
-
/**
|
|
866
|
-
* @deprecated Since 0.3.0. Use Convex.Code with a Convex.Deployment: it
|
|
867
|
-
* runs the installed Convex CLI, keeps the deploy key out of the process
|
|
868
|
-
* environment, checks that the key fits the deployment, and outputs the site
|
|
869
|
-
* URL. Removing a Deploy resource changes nothing in Convex.
|
|
870
|
-
*/
|
|
871
|
-
const Deploy = Resource("Convex.Deploy");
|
|
872
|
-
/** The `convex deploy` argv, starting with `npx`. */
|
|
873
|
-
const deployArgs = (props) => [
|
|
874
|
-
"npx",
|
|
875
|
-
"convex",
|
|
876
|
-
"deploy",
|
|
877
|
-
...props.previewName === void 0 ? [] : ["--preview-name", props.previewName],
|
|
878
|
-
...props.previewCreate === void 0 ? [] : ["--preview-create", props.previewCreate],
|
|
879
|
-
...props.previewRun === void 0 ? [] : ["--preview-run", props.previewRun],
|
|
880
|
-
...props.extraArgs ?? []
|
|
881
|
-
];
|
|
882
|
-
/** A `convex deploy` argument contains whitespace, which Alchemy's command runner splits on. */
|
|
883
|
-
var DeployArgumentError = class extends Schema.TaggedError()("DeployArgumentError", { argument: Schema.String }) {
|
|
884
|
-
get message() {
|
|
885
|
-
return `Convex.Deploy argument "${this.argument}" contains whitespace, and Alchemy splits the command on whitespace. Use the --flag=value form, such as --typecheck=disable, and names without spaces.`;
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
/** `npx convex deploy` exited 0 but did not print the deployment URL. */
|
|
889
|
-
var DeployOutputError = class extends Schema.TaggedError()("DeployOutputError", { outputTail: Schema.String }) {
|
|
890
|
-
get message() {
|
|
891
|
-
return `npx convex deploy exited 0 but printed no "Deployed Convex functions to <url>" line, so Convex.Deploy has no deployment URL. Remove --dry-run from extraArgs if it is there, and read the CLI output. Last output:\n${this.outputTail}`;
|
|
892
|
-
}
|
|
893
|
-
};
|
|
894
|
-
const DeployProvider = () => Provider.effect(Deploy, Effect.gen(function* () {
|
|
895
|
-
const { run } = yield* CommandExecutor;
|
|
896
|
-
const hashOf = (props) => props.memo === false ? Effect.succeed(void 0) : hashDirectory({
|
|
897
|
-
...props.cwd === void 0 ? {} : { cwd: props.cwd },
|
|
898
|
-
memo: props.memo === true || props.memo === void 0 ? {} : props.memo
|
|
899
|
-
});
|
|
900
|
-
return {
|
|
901
|
-
diff: Effect.fn(function* ({ olds, news, output }) {
|
|
902
|
-
if (!isResolved(news)) return void 0;
|
|
903
|
-
if (news.previewName !== void 0 || news.previewCreate !== void 0 || output === void 0 || output.hash === void 0 || havePropsChanged(olds, news)) return { action: "update" };
|
|
904
|
-
return { action: (yield* hashOf(news)) === output.hash ? "noop" : "update" };
|
|
905
|
-
}),
|
|
906
|
-
reconcile: Effect.fn(function* ({ news, session }) {
|
|
907
|
-
const args = deployArgs(news);
|
|
908
|
-
const spaced = args.find((arg) => /\s/.test(arg));
|
|
909
|
-
if (spaced !== void 0) return yield* new DeployArgumentError({ argument: spaced });
|
|
910
|
-
const { stdout, stderr } = yield* run({
|
|
911
|
-
command: args.join(" "),
|
|
912
|
-
...news.cwd === void 0 ? {} : { cwd: news.cwd },
|
|
913
|
-
...news.timeout === void 0 ? {} : { timeout: news.timeout },
|
|
914
|
-
env: {
|
|
915
|
-
...news.env,
|
|
916
|
-
CONVEX_DEPLOY_KEY: news.deployKey
|
|
917
889
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
};
|
|
929
|
-
}));
|
|
930
|
-
//#endregion
|
|
931
|
-
//#region src/DeployKey.ts
|
|
932
|
-
/**
|
|
933
|
-
* True when `listedName` is how Convex lists a key created as `name`: the
|
|
934
|
-
* requested name itself, or the requested name, a space, and one
|
|
935
|
-
* parenthesized suffix without spaces. The suffix check keeps `my-key` from
|
|
936
|
-
* matching a key created as `my-key (old)`.
|
|
937
|
-
*/
|
|
938
|
-
const isListedAs = (listedName, name) => {
|
|
939
|
-
const prefix = `${name} (`;
|
|
940
|
-
return listedName === name || listedName.startsWith(prefix) && /^[^\s()]+\)$/.test(listedName.slice(prefix.length));
|
|
941
|
-
};
|
|
942
|
-
/**
|
|
943
|
-
* True when the listing still has the key in state: by numeric id, or by
|
|
944
|
-
* listed name for state written by 0.1.x, which has no id.
|
|
945
|
-
*/
|
|
946
|
-
const isKeyListed = (entries, key) => entries.some((entry) => key.keyId === void 0 ? entry.name === key.uniqueName : entry.id === key.keyId);
|
|
947
|
-
/** The listed keys that Convex shows for a key created as `name`. */
|
|
948
|
-
const keysListedAs = (entries, name) => entries.filter((entry) => isListedAs(entry.name, name));
|
|
949
|
-
/**
|
|
950
|
-
* The name that a resource instance requests: `name`, a dash, and the first
|
|
951
|
-
* 12 hex characters of sha256("<fqn>:<instanceId>"). Two resources, or the
|
|
952
|
-
* old and the new generation of one replaced resource, never request the
|
|
953
|
-
* same name. Pattern from Confect `packages/alchemy/src/internal/ResourceIdentity.ts`
|
|
954
|
-
* (https://github.com/rjdellecese/confect, ISC license).
|
|
955
|
-
*/
|
|
956
|
-
const requestedKeyName = (name, fqn, instanceId) => `${name}-${createHash("sha256").update(`${fqn}:${instanceId}`).digest("hex").slice(0, 12)}`;
|
|
957
|
-
/** One row of list_deploy_keys and list_preview_deploy_keys. Only the fields the providers use. */
|
|
958
|
-
const ListedKey = Schema.Struct({
|
|
959
|
-
id: Schema.Number,
|
|
960
|
-
name: Schema.String
|
|
961
|
-
});
|
|
962
|
-
/** GET /deployments/{deployment_name}/list_deploy_keys */
|
|
963
|
-
const DeployKeyList = Schema.Array(ListedKey);
|
|
964
|
-
/** POST /deployments/{deployment_name}/create_deploy_key */
|
|
965
|
-
const CreatedDeployKey = Schema.Struct({ deployKey: Schema.String });
|
|
966
|
-
/**
|
|
967
|
-
* A key cannot be tied to one resource: before the create, listed keys
|
|
968
|
-
* already have the requested name, or after the create, not exactly one has it.
|
|
969
|
-
*/
|
|
970
|
-
var DeployKeyRecoveryRequired = class extends Schema.TaggedError()("DeployKeyRecoveryRequired", {
|
|
971
|
-
/** Where the keys live, such as `deployment happy-otter-123` or `project 3145389`. */
|
|
972
|
-
target: Schema.String,
|
|
973
|
-
/** The requested name. */
|
|
974
|
-
name: Schema.String,
|
|
975
|
-
/** The listed keys with the requested name. */
|
|
976
|
-
keys: Schema.Array(ListedKey),
|
|
977
|
-
/** True when this run created a key before the listing check failed. */
|
|
978
|
-
afterCreate: Schema.Boolean
|
|
979
|
-
}) {
|
|
980
|
-
get message() {
|
|
981
|
-
const listed = this.keys.length === 0 ? "no key" : this.keys.map((key) => `"${key.name}" (id ${key.id})`).join(", ");
|
|
982
|
-
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.`;
|
|
983
|
-
}
|
|
984
|
-
};
|
|
985
|
-
/**
|
|
986
|
-
* Convex returned the Management API credential itself as the new key. This
|
|
987
|
-
* happens with an OAuth token: Convex creates no key. A delete of the
|
|
988
|
-
* returned secret would revoke the credential, so the resource does not keep it.
|
|
989
|
-
*/
|
|
990
|
-
var DeployKeyIsCredential = class extends Schema.TaggedError()("DeployKeyIsCredential", {
|
|
991
|
-
target: Schema.String,
|
|
992
|
-
name: Schema.String
|
|
993
|
-
}) {
|
|
994
|
-
get message() {
|
|
995
|
-
return `Convex returned the Management API credential itself as deploy key "${this.name}" on ${this.target}. With an OAuth token, Convex creates no new key, and deleting the returned key would revoke the OAuth token. Alchemy does not keep it. Use a team access token in CONVEX_ACCESS_TOKEN, or the Convex CLI login.`;
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
|
-
/**
|
|
999
|
-
* Creates a key named `name` and ties it to the caller. Fails before the
|
|
1000
|
-
* create when keys with that name already exist. After the create, fails
|
|
1001
|
-
* when the secret is the Management API credential itself, or when the
|
|
1002
|
-
* listing does not show exactly one key with that name. It never revokes a
|
|
1003
|
-
* key: the returned secret can be a shared credential.
|
|
1004
|
-
*/
|
|
1005
|
-
const createTrackedKey = (options) => Effect.gen(function* () {
|
|
1006
|
-
const { target, name } = options;
|
|
1007
|
-
const existing = keysListedAs(yield* options.list, name);
|
|
1008
|
-
if (existing.length > 0) return yield* new DeployKeyRecoveryRequired({
|
|
1009
|
-
target,
|
|
1010
|
-
name,
|
|
1011
|
-
keys: existing,
|
|
1012
|
-
afterCreate: false
|
|
1013
|
-
});
|
|
1014
|
-
const secret = yield* options.create;
|
|
1015
|
-
if (yield* options.isCredentialToken(secret)) return yield* new DeployKeyIsCredential({
|
|
1016
|
-
target,
|
|
1017
|
-
name
|
|
1018
|
-
});
|
|
1019
|
-
const listed = keysListedAs(yield* options.list, name);
|
|
1020
|
-
const [only] = listed;
|
|
1021
|
-
if (only === void 0 || listed.length !== 1) return yield* new DeployKeyRecoveryRequired({
|
|
1022
|
-
target,
|
|
1023
|
-
name,
|
|
1024
|
-
keys: listed,
|
|
1025
|
-
afterCreate: true
|
|
1026
|
-
});
|
|
1027
|
-
return {
|
|
1028
|
-
keyId: only.id,
|
|
1029
|
-
uniqueName: only.name,
|
|
1030
|
-
secret: Redacted.make(secret)
|
|
1031
|
-
};
|
|
1032
|
-
});
|
|
1033
|
-
/**
|
|
1034
|
-
* The secret to send to a delete call, or undefined when a delete is not
|
|
1035
|
-
* safe. Only a key with a `keyId` in state is known to be a new key that
|
|
1036
|
-
* this resource owns: createTrackedKey found exactly one new listed key, and
|
|
1037
|
-
* the secret was not the credential. State from 0.1.x has no `keyId`, and its
|
|
1038
|
-
* secret can be an OAuth token that Convex returned as a key, so a delete
|
|
1039
|
-
* could revoke that OAuth token. Such a key stays, and a warning names it.
|
|
1040
|
-
*/
|
|
1041
|
-
const secretToDelete = (key, label) => Effect.gen(function* () {
|
|
1042
|
-
if (key.keyId === void 0 || !Redacted.isRedacted(key.secret)) {
|
|
1043
|
-
yield* Effect.logWarning(`${label} has no verified key id and secret in state (state from 0.1.x, for example), so Alchemy does not delete it. Delete it in the Convex dashboard.`);
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
return Redacted.value(key.secret);
|
|
1047
|
-
});
|
|
1048
|
-
const DeployKey = Resource("Convex.DeployKey");
|
|
1049
|
-
const DeployKeyProvider = () => Provider.effect(DeployKey, Effect.gen(function* () {
|
|
1050
|
-
const api = yield* ManagementApi;
|
|
1051
|
-
const listKeys = (deployment) => api.request("list deploy keys", (client) => client.GET("/deployments/{deployment_name}/list_deploy_keys", { params: { path: { deployment_name: deployment } } })).pipe(Effect.map((body) => Schema.decodeUnknownSync(DeployKeyList)(body)));
|
|
1052
|
-
/**
|
|
1053
|
-
* `id` is the secret: Convex accepts it, and it matches only the key
|
|
1054
|
-
* that this resource created. A second delete answers 404.
|
|
1055
|
-
*/
|
|
1056
|
-
const deleteKey = (deployment, secret) => retryIdempotentWrite(api.requestVoid("delete deploy key", (client) => client.POST("/deployments/{deployment_name}/delete_deploy_key", {
|
|
1057
|
-
params: { path: { deployment_name: deployment } },
|
|
1058
|
-
body: { id: secret }
|
|
1059
|
-
})));
|
|
1060
|
-
return {
|
|
1061
|
-
stables: ["uniqueName", "deployment"],
|
|
1062
|
-
diff: ({ olds, news, output }) => {
|
|
1063
|
-
if (!isResolved(news)) return Effect.succeed(void 0);
|
|
1064
|
-
return Effect.succeed(output !== void 0 && output.deployKey === void 0 || havePropsChanged(olds, news) ? { action: "replace" } : { action: "noop" });
|
|
1065
|
-
},
|
|
1066
|
-
read: Effect.fn(function* ({ output }) {
|
|
1067
|
-
if (output === void 0) return void 0;
|
|
1068
|
-
const entries = yield* absentAsUndefined(listKeys(output.deployment));
|
|
1069
|
-
return entries !== void 0 && isKeyListed(entries, output) ? output : void 0;
|
|
1070
|
-
}),
|
|
1071
|
-
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
1072
|
-
if (output !== void 0) {
|
|
1073
|
-
if (isKeyListed(yield* listKeys(news.deployment), output)) return output;
|
|
1074
|
-
}
|
|
1075
|
-
const name = requestedKeyName(news.name, fqn, instanceId);
|
|
1076
|
-
const { keyId, uniqueName, secret } = yield* createTrackedKey({
|
|
1077
|
-
target: `deployment ${news.deployment}`,
|
|
1078
|
-
name,
|
|
1079
|
-
list: listKeys(news.deployment),
|
|
1080
|
-
create: api.request("create deploy key", (client) => client.POST("/deployments/{deployment_name}/create_deploy_key", {
|
|
1081
|
-
params: { path: { deployment_name: news.deployment } },
|
|
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 } },
|
|
1082
900
|
body: {
|
|
1083
|
-
|
|
1084
|
-
|
|
901
|
+
type: news.type,
|
|
902
|
+
reference: news.name
|
|
1085
903
|
}
|
|
1086
|
-
}))
|
|
1087
|
-
|
|
904
|
+
}));
|
|
905
|
+
return {
|
|
906
|
+
target: targetOf(Schema.decodeUnknownSync(DeploymentRow$1)(row), news.type, news.name),
|
|
907
|
+
created: true
|
|
908
|
+
};
|
|
1088
909
|
});
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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: []
|
|
1094
916
|
};
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
* that this resource created. A second delete answers 404.
|
|
1119
|
-
*/
|
|
1120
|
-
const deleteKey = (projectId, secret) => retryIdempotentWrite(api.requestVoid("delete preview deploy key", (client) => client.POST("/projects/{project_id}/delete_preview_deploy_key", {
|
|
1121
|
-
params: { path: { project_id: projectId } },
|
|
1122
|
-
body: { id: secret }
|
|
1123
|
-
})));
|
|
1124
|
-
return {
|
|
1125
|
-
stables: ["uniqueName", "projectId"],
|
|
1126
|
-
diff: ({ olds, news, output }) => {
|
|
1127
|
-
if (!isResolved(news)) return Effect.succeed(void 0);
|
|
1128
|
-
return Effect.succeed(output !== void 0 && output.previewDeployKey === void 0 || havePropsChanged(olds, news) ? { action: "replace" } : { action: "noop" });
|
|
1129
|
-
},
|
|
1130
|
-
read: Effect.fn(function* ({ output }) {
|
|
1131
|
-
if (output === void 0) return void 0;
|
|
1132
|
-
const entries = yield* absentAsUndefined(listKeys(output.projectId));
|
|
1133
|
-
return entries !== void 0 && isKeyListed(entries, output) ? output : void 0;
|
|
1134
|
-
}),
|
|
1135
|
-
reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
|
|
1136
|
-
if (output !== void 0) {
|
|
1137
|
-
if (isKeyListed(yield* listKeys(news.projectId), output)) return output;
|
|
1138
|
-
}
|
|
1139
|
-
const name = requestedKeyName(news.name, fqn, instanceId);
|
|
1140
|
-
const { keyId, uniqueName, secret } = yield* createTrackedKey({
|
|
1141
|
-
target: `project ${news.projectId}`,
|
|
1142
|
-
name,
|
|
1143
|
-
list: listKeys(news.projectId),
|
|
1144
|
-
create: api.request("create preview deploy key", (client) => client.POST("/projects/{project_id}/create_preview_deploy_key", {
|
|
1145
|
-
params: { path: { project_id: news.projectId } },
|
|
1146
|
-
body: { name }
|
|
1147
|
-
})).pipe(Effect.map((body) => Schema.decodeUnknownSync(CreatedPreviewDeployKey)(body).previewDeployKey)),
|
|
1148
|
-
isCredentialToken: api.isCredentialToken
|
|
1149
|
-
});
|
|
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));
|
|
1150
940
|
return {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
previewDeployKey: secret
|
|
941
|
+
...target,
|
|
942
|
+
hash: yield* wantedHash(news, target.name),
|
|
943
|
+
envNames: Object.keys(env).sort()
|
|
1155
944
|
};
|
|
1156
945
|
}),
|
|
1157
946
|
delete: Effect.fn(function* ({ output }) {
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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 } } }))));
|
|
1164
955
|
})
|
|
1165
956
|
};
|
|
1166
957
|
}));
|
|
@@ -1208,12 +999,32 @@ var CreatedProjectNotFound = class extends Schema.TaggedError()("CreatedProjectN
|
|
|
1208
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}.`;
|
|
1209
1000
|
}
|
|
1210
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
|
+
};
|
|
1211
1021
|
/** Fields of GET /projects/{id}, PATCH /projects/{id}, and GET /teams/{team}/projects rows that the provider uses. */
|
|
1212
1022
|
const ProjectDetails = Schema.Struct({
|
|
1213
1023
|
id: Schema.Number,
|
|
1214
1024
|
name: Schema.String,
|
|
1215
1025
|
slug: Schema.String,
|
|
1216
1026
|
teamId: Schema.Number,
|
|
1027
|
+
teamSlug: Schema.String,
|
|
1217
1028
|
prodDeploymentName: Schema.NullOr(Schema.String)
|
|
1218
1029
|
});
|
|
1219
1030
|
/** GET /teams/{team_id}/projects: one page of projects. */
|
|
@@ -1254,6 +1065,8 @@ const checkCreateTimeProps = (projectId, olds, news) => {
|
|
|
1254
1065
|
}));
|
|
1255
1066
|
return Effect.void;
|
|
1256
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;
|
|
1257
1070
|
const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
1258
1071
|
const api = yield* ManagementApi;
|
|
1259
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)));
|
|
@@ -1266,6 +1079,11 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1266
1079
|
prodDeploymentUrl: prod?.deploymentUrl
|
|
1267
1080
|
})));
|
|
1268
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)));
|
|
1269
1087
|
/**
|
|
1270
1088
|
* The one project in the team with exactly this name, or undefined.
|
|
1271
1089
|
* Reads every page. `q` narrows the listing on the server, and the
|
|
@@ -1331,6 +1149,22 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1331
1149
|
diff: Effect.fn(function* ({ olds, news, output }) {
|
|
1332
1150
|
if (!isResolved(news)) return void 0;
|
|
1333
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
|
+
}
|
|
1334
1168
|
if (olds.team !== news.team) {
|
|
1335
1169
|
const requestedTeamId = yield* api.resolveTeamId(news.team);
|
|
1336
1170
|
if (requestedTeamId !== output.teamId) return yield* new ProjectTeamChange({
|
|
@@ -1339,20 +1173,43 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1339
1173
|
requestedTeamId
|
|
1340
1174
|
});
|
|
1341
1175
|
}
|
|
1342
|
-
return olds.name === news.name ? void 0 : { action: "update" };
|
|
1176
|
+
return "name" in olds && olds.name === news.name ? void 0 : { action: "update" };
|
|
1343
1177
|
}),
|
|
1344
1178
|
read: Effect.fn(function* ({ olds, output }) {
|
|
1345
1179
|
if (output !== void 0) {
|
|
1346
1180
|
const details = yield* getById(output.projectId);
|
|
1347
1181
|
return details === void 0 ? void 0 : yield* attributesOf(details);
|
|
1348
1182
|
}
|
|
1349
|
-
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);
|
|
1350
1184
|
return existing === void 0 ? void 0 : Unowned(yield* attributesOf(existing));
|
|
1351
1185
|
}),
|
|
1352
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
|
+
}
|
|
1353
1210
|
const teamId = yield* api.resolveTeamId(news.team);
|
|
1354
1211
|
if (output !== void 0) {
|
|
1355
|
-
if (olds !== void 0) yield* checkCreateTimeProps(output.projectId, olds, news);
|
|
1212
|
+
if (olds !== void 0 && "name" in olds) yield* checkCreateTimeProps(output.projectId, olds, news);
|
|
1356
1213
|
if (output.teamId !== teamId) return yield* new ProjectTeamChange({
|
|
1357
1214
|
projectId: output.projectId,
|
|
1358
1215
|
teamId: output.teamId,
|
|
@@ -1393,20 +1250,12 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1393
1250
|
//#region src/Providers.ts
|
|
1394
1251
|
var Providers = class extends Provider.ProviderCollection()("Convex") {};
|
|
1395
1252
|
/**
|
|
1396
|
-
*
|
|
1397
|
-
* 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`).
|
|
1398
1256
|
*/
|
|
1399
|
-
const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([
|
|
1400
|
-
Project,
|
|
1401
|
-
Deployment,
|
|
1402
|
-
DeployKey,
|
|
1403
|
-
PreviewDeployKey,
|
|
1404
|
-
EnvironmentVariable,
|
|
1405
|
-
DefaultEnvironmentVariable,
|
|
1406
|
-
Code,
|
|
1407
|
-
Deploy
|
|
1408
|
-
])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider(), DeployKeyProvider(), PreviewDeployKeyProvider(), EnvironmentVariableProvider(), DefaultEnvironmentVariableProvider(), CodeProvider(), 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);
|
|
1409
1258
|
//#endregion
|
|
1410
|
-
export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject,
|
|
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 };
|
|
1411
1260
|
|
|
1412
1261
|
//# sourceMappingURL=index.js.map
|