@samebase/alchemy-convex 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -55
- package/lib/index.d.ts +510 -9
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1411 -10
- package/lib/index.js.map +1 -1
- package/package.json +8 -5
- package/src/Adoption.ts +30 -0
- package/src/Code.ts +463 -0
- package/src/DefaultEnvironmentVariable.ts +127 -29
- package/src/Deploy.ts +47 -35
- package/src/DeployKey.ts +221 -60
- package/src/Deployment.ts +409 -0
- package/src/EnvironmentVariable.ts +204 -44
- package/src/ManagementApi.ts +57 -1
- package/src/PreviewDeployKey.ts +65 -38
- package/src/Project.ts +236 -52
- package/src/Providers.ts +9 -1
- package/src/index.ts +29 -1
- package/lib/Credentials.d.ts +0 -39
- package/lib/Credentials.d.ts.map +0 -1
- package/lib/Credentials.js +0 -49
- package/lib/Credentials.js.map +0 -1
- package/lib/DefaultEnvironmentVariable.d.ts +0 -58
- package/lib/DefaultEnvironmentVariable.d.ts.map +0 -1
- package/lib/DefaultEnvironmentVariable.js +0 -93
- package/lib/DefaultEnvironmentVariable.js.map +0 -1
- package/lib/Deploy.d.ts +0 -62
- package/lib/Deploy.d.ts.map +0 -1
- package/lib/Deploy.js +0 -92
- package/lib/Deploy.js.map +0 -1
- package/lib/DeployKey.d.ts +0 -48
- package/lib/DeployKey.d.ts.map +0 -1
- package/lib/DeployKey.js +0 -104
- package/lib/DeployKey.js.map +0 -1
- package/lib/EnvironmentVariable.d.ts +0 -37
- package/lib/EnvironmentVariable.d.ts.map +0 -1
- package/lib/EnvironmentVariable.js +0 -65
- package/lib/EnvironmentVariable.js.map +0 -1
- package/lib/ManagementApi.d.ts +0 -50
- package/lib/ManagementApi.d.ts.map +0 -1
- package/lib/ManagementApi.js +0 -103
- package/lib/ManagementApi.js.map +0 -1
- package/lib/PreviewDeployKey.d.ts +0 -34
- package/lib/PreviewDeployKey.d.ts.map +0 -1
- package/lib/PreviewDeployKey.js +0 -78
- package/lib/PreviewDeployKey.js.map +0 -1
- package/lib/Project.d.ts +0 -29
- package/lib/Project.d.ts.map +0 -1
- package/lib/Project.js +0 -114
- package/lib/Project.js.map +0 -1
- package/lib/Providers.d.ts +0 -13
- package/lib/Providers.d.ts.map +0 -1
- package/lib/Providers.js +0 -30
- package/lib/Providers.js.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,11 +1,1412 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { stripVTControlCharacters } from "node:util";
|
|
6
|
+
import { Resource } from "alchemy";
|
|
7
|
+
import { AdoptPolicy, OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
|
|
8
|
+
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
|
+
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
|
+
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
17
|
+
import { Stack } from "alchemy/Stack";
|
|
18
|
+
import * as Option from "effect/Option";
|
|
19
|
+
import { havePropsChanged, isResolved } from "alchemy/Diff";
|
|
20
|
+
import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
|
|
21
|
+
import * as Context from "effect/Context";
|
|
22
|
+
import * as Layer from "effect/Layer";
|
|
23
|
+
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
|
+
import { createHash } from "node:crypto";
|
|
28
|
+
//#region src/Adoption.ts
|
|
29
|
+
/** True when the resource `fqn` may take over an object that is not in state. */
|
|
30
|
+
const shouldAdopt = (fqn) => Effect.gen(function* () {
|
|
31
|
+
const stack = yield* Effect.serviceOption(Stack);
|
|
32
|
+
const own = Option.isSome(stack) ? stack.value.resources[fqn]?.Adopt : void 0;
|
|
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
|
|
40
|
+
//#region src/Credentials.ts
|
|
41
|
+
var Credentials = class extends Context.Service()("@samebase/alchemy-convex/Credentials") {};
|
|
42
|
+
/** Environment variable read by {@link fromConfig}. */
|
|
43
|
+
const ACCESS_TOKEN_ENV = "CONVEX_ACCESS_TOKEN";
|
|
44
|
+
/** The Convex CLI stores its device login here after `npx convex login`. */
|
|
45
|
+
const convexCliConfigPath = () => join(homedir(), ".convex", "config.json");
|
|
46
|
+
const CliConfig = Schema.Struct({ accessToken: Schema.String });
|
|
47
|
+
/**
|
|
48
|
+
* Token the Convex CLI saved on this machine. It works on the Management API
|
|
49
|
+
* for the teams the logged-in user belongs to. Fails when nobody is logged in.
|
|
50
|
+
*/
|
|
51
|
+
const readConvexCliLogin = Effect.gen(function* () {
|
|
52
|
+
const path = convexCliConfigPath();
|
|
53
|
+
const text = yield* Effect.tryPromise({
|
|
54
|
+
try: () => readFile(path, "utf8"),
|
|
55
|
+
catch: () => new CredentialsError({ reason: `no Convex CLI login at ${path}` })
|
|
56
|
+
});
|
|
57
|
+
const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(CliConfig))(text).pipe(Effect.mapError(() => new CredentialsError({ reason: `${path} has no accessToken` })));
|
|
58
|
+
return {
|
|
59
|
+
accessToken: Redacted.make(parsed.accessToken),
|
|
60
|
+
source: "convex-cli-login"
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
var CredentialsError = class extends Schema.TaggedError()("CredentialsError", { reason: Schema.String }) {};
|
|
64
|
+
/**
|
|
65
|
+
* Default resolution: `CONVEX_ACCESS_TOKEN` from the stack ConfigProvider
|
|
66
|
+
* (process env, `.env`, stack secrets), then the Convex CLI login on this
|
|
67
|
+
* machine. CI sets the variable; a developer who ran `npx convex login` needs
|
|
68
|
+
* nothing.
|
|
69
|
+
*/
|
|
70
|
+
const fromConfig = () => Layer.succeed(Credentials, Effect.cached(Config.Redacted(ACCESS_TOKEN_ENV).pipe(Effect.map((accessToken) => ({
|
|
71
|
+
accessToken,
|
|
72
|
+
source: "env"
|
|
73
|
+
})), Effect.catch(() => readConvexCliLogin), Effect.orDie)).pipe(Effect.flatten));
|
|
74
|
+
/** Explicit token, for tests or callers that already hold a value. */
|
|
75
|
+
const fromToken = (accessToken) => Layer.succeed(Credentials, Effect.succeed({
|
|
76
|
+
accessToken,
|
|
77
|
+
source: "explicit"
|
|
78
|
+
}));
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/ManagementApi.ts
|
|
81
|
+
/**
|
|
82
|
+
* One error for every control-plane and deployment call. Resources branch on
|
|
83
|
+
* `status` (404 means absent for read and delete), so the class is load
|
|
84
|
+
* bearing, not decorative.
|
|
85
|
+
*/
|
|
86
|
+
var ConvexApiError = class extends Schema.TaggedError()("ConvexApiError", {
|
|
87
|
+
operation: Schema.String,
|
|
88
|
+
status: Schema.Number,
|
|
89
|
+
code: Schema.optionalKey(Schema.String),
|
|
90
|
+
message: Schema.String
|
|
91
|
+
}) {};
|
|
92
|
+
const ErrorBody = Schema.Struct({
|
|
93
|
+
code: Schema.optionalKey(Schema.String),
|
|
94
|
+
message: Schema.optionalKey(Schema.String)
|
|
95
|
+
});
|
|
96
|
+
const toApiError = (operation, result) => {
|
|
97
|
+
const body = Schema.decodeUnknownSync(Schema.NullishOr(ErrorBody))(result.error ?? null);
|
|
98
|
+
const code = body?.code;
|
|
99
|
+
return new ConvexApiError({
|
|
100
|
+
operation,
|
|
101
|
+
status: result.response.status,
|
|
102
|
+
...code === void 0 ? {} : { code },
|
|
103
|
+
message: body?.message ?? `${operation} failed with HTTP ${result.response.status}`
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
/** The part of a Convex token after the first "|", or the whole value when it has none. */
|
|
107
|
+
const tokenPart = (value) => value.slice(value.indexOf("|") + 1);
|
|
108
|
+
var ManagementApi = class extends Context.Service()("@samebase/alchemy-convex/ManagementApi") {};
|
|
109
|
+
const attempt = (operation, run) => Effect.tryPromise({
|
|
110
|
+
try: run,
|
|
111
|
+
catch: (cause) => new ConvexApiError({
|
|
112
|
+
operation,
|
|
113
|
+
status: 0,
|
|
114
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
/** Reads `data` or fails with the HTTP status and the API's own code and message. */
|
|
118
|
+
const settle = (operation, run) => attempt(operation, run).pipe(Effect.flatMap((result) => result.response.ok && result.data !== void 0 ? Effect.succeed(result.data) : Effect.fail(toApiError(operation, result))));
|
|
119
|
+
/** For endpoints that answer with an empty body, such as delete calls. */
|
|
120
|
+
const settleVoid = (operation, run) => attempt(operation, run).pipe(Effect.flatMap((result) => result.response.ok ? Effect.void : Effect.fail(toApiError(operation, result))));
|
|
121
|
+
/**
|
|
122
|
+
* True when an idempotent write can succeed on a new attempt:
|
|
123
|
+
*
|
|
124
|
+
* - A Convex write conflict. Two parallel update_environment_variables calls
|
|
125
|
+
* on one deployment gave HTTP 503 `OptimisticConcurrencyControlFailure`,
|
|
126
|
+
* "Data read or written in this mutation changed while it was being run",
|
|
127
|
+
* in a live run. The platform codes `OCC` and `WriteConflict` and the
|
|
128
|
+
* message "... changed while this mutation was being run ..." mean the same.
|
|
129
|
+
* - Any other 5xx answer. Two parallel delete_preview_deploy_key calls on one
|
|
130
|
+
* project gave HTTP 500 `InternalServerError`, "Your request couldn't be
|
|
131
|
+
* completed. Try again later.", in a live run. The next attempt succeeded.
|
|
132
|
+
*/
|
|
133
|
+
const isRetryableWriteError = (error) => error.code === "OptimisticConcurrencyControlFailure" || error.code === "OCC" || error.code === "WriteConflict" || /changed while (it|this mutation) was being run/.test(error.message) || error.status >= 500;
|
|
134
|
+
/**
|
|
135
|
+
* Runs an idempotent write again after a retryable Convex error: at most
|
|
136
|
+
* four more attempts, with exponential backoff from 100 ms and jitter. Other
|
|
137
|
+
* errors fail at once, and so does the last retryable error. Use it only for
|
|
138
|
+
* a write that has the same result when it runs twice, such as setting a
|
|
139
|
+
* value or deleting by id. Never for a create that does not check first.
|
|
140
|
+
*/
|
|
141
|
+
const retryIdempotentWrite = (effect) => effect.pipe(Effect.retry({
|
|
142
|
+
while: (error) => error instanceof ConvexApiError && isRetryableWriteError(error),
|
|
143
|
+
times: 4,
|
|
144
|
+
schedule: Schedule.exponential("100 millis").pipe(Schedule.jittered)
|
|
145
|
+
}));
|
|
146
|
+
/** Turns a 404 into `undefined` so read and delete stay idempotent. */
|
|
147
|
+
const absentAsUndefined = (effect) => effect.pipe(Effect.catchIf((error) => error.status === 404, () => Effect.succeed(void 0)));
|
|
148
|
+
const TeamRow = Schema.Struct({
|
|
149
|
+
id: Schema.Number,
|
|
150
|
+
slug: Schema.String
|
|
151
|
+
});
|
|
152
|
+
const TeamRows = Schema.Array(TeamRow);
|
|
153
|
+
const ManagementApiLive = () => Layer.effect(ManagementApi, Effect.gen(function* () {
|
|
154
|
+
const credentials = yield* Credentials;
|
|
155
|
+
const client = Effect.cached(credentials.pipe(Effect.map((resolved) => createManagementClient(Redacted.value(resolved.accessToken))))).pipe(Effect.flatten);
|
|
156
|
+
const request = (operation, run) => client.pipe(Effect.flatMap((c) => settle(operation, () => run(c))));
|
|
157
|
+
const requestVoid = (operation, run) => client.pipe(Effect.flatMap((c) => settleVoid(operation, () => run(c))));
|
|
158
|
+
const deploymentRequest = (operation, deploymentUrlOrName, deployKey, run) => settle(operation, () => run(createDeploymentClient(deploymentUrlOrName, Redacted.value(deployKey))));
|
|
159
|
+
const resolveTeamId = (team) => Effect.gen(function* () {
|
|
160
|
+
if (typeof team === "number") return team;
|
|
161
|
+
if (/^\d+$/.test(team)) return Number(team);
|
|
162
|
+
const resolved = yield* credentials;
|
|
163
|
+
const match = (yield* Effect.tryPromise({
|
|
164
|
+
try: async () => {
|
|
165
|
+
const response = await fetch("https://api.convex.dev/api/dashboard/teams", { headers: { Authorization: `Bearer ${Redacted.value(resolved.accessToken)}` } });
|
|
166
|
+
if (!response.ok) throw new Error(`team lookup for "${team}" failed with HTTP ${response.status}`);
|
|
167
|
+
return Schema.decodeUnknownSync(TeamRows)(await response.json());
|
|
168
|
+
},
|
|
169
|
+
catch: (cause) => new ConvexApiError({
|
|
170
|
+
operation: "resolve team",
|
|
171
|
+
status: 0,
|
|
172
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
173
|
+
})
|
|
174
|
+
})).find((row) => row.slug === team);
|
|
175
|
+
if (match === void 0) return yield* new ConvexApiError({
|
|
176
|
+
operation: "resolve team",
|
|
177
|
+
status: 404,
|
|
178
|
+
message: `no team with slug "${team}" for this token`
|
|
179
|
+
});
|
|
180
|
+
return match.id;
|
|
181
|
+
});
|
|
182
|
+
const isCredentialToken = (secret) => credentials.pipe(Effect.map((resolved) => tokenPart(Redacted.value(resolved.accessToken)) === tokenPart(secret)));
|
|
183
|
+
return {
|
|
184
|
+
request,
|
|
185
|
+
requestVoid,
|
|
186
|
+
deploymentRequest,
|
|
187
|
+
resolveTeamId,
|
|
188
|
+
isCredentialToken
|
|
189
|
+
};
|
|
190
|
+
}));
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/Deployment.ts
|
|
193
|
+
const Deployment = Resource("Convex.Deployment", { defaultRemovalPolicy: "retain" });
|
|
194
|
+
/** The props name another deployment than the one in state. */
|
|
195
|
+
var DeploymentIdentityChange = class extends Schema.TaggedError()("DeploymentIdentityChange", {
|
|
196
|
+
/** Name of the deployment in state, such as `happy-animal-123`. */
|
|
197
|
+
deployment: Schema.String,
|
|
198
|
+
field: Schema.Literals([
|
|
199
|
+
"projectId",
|
|
200
|
+
"type",
|
|
201
|
+
"name"
|
|
202
|
+
]),
|
|
203
|
+
current: Schema.String,
|
|
204
|
+
requested: Schema.String
|
|
205
|
+
}) {
|
|
206
|
+
get message() {
|
|
207
|
+
return `Convex.Deployment cannot change the ${this.field} of deployment ${this.deployment} from ${this.current} to ${this.requested}. Set ${this.field} back to ${this.current}. For another deployment, create a new resource with a new logical id.`;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/** The project has no default production deployment for a `prod` resource to take over. */
|
|
211
|
+
var ProductionDeploymentNotFound = class extends Schema.TaggedError()("ProductionDeploymentNotFound", { projectId: Schema.Number }) {
|
|
212
|
+
get message() {
|
|
213
|
+
return `Project ${this.projectId} has no default production deployment, and Convex.Deployment does not create one. Create it in the Convex dashboard, or with \`npx convex deployment create --type prod --default\` in the project, then run the deploy again.`;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
/** More than one deployment of the project has the name, so the lookup cannot choose one. */
|
|
217
|
+
var AmbiguousDeployment = class extends Schema.TaggedError()("AmbiguousDeployment", {
|
|
218
|
+
projectId: Schema.Number,
|
|
219
|
+
type: Schema.Literals(["dev", "preview"]),
|
|
220
|
+
name: Schema.String,
|
|
221
|
+
deployments: Schema.Array(Schema.String)
|
|
222
|
+
}) {
|
|
223
|
+
get message() {
|
|
224
|
+
return `Project ${this.projectId} has ${this.deployments.length} ${this.type} deployments named "${this.name}" (${this.deployments.join(", ")}). Convex.Deployment does not choose one. Delete the extra deployments in the Convex dashboard, or use another name.`;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* The URL of a Convex cloud deployment: `https://<name>.convex.cloud`, or
|
|
229
|
+
* `https://<name>.<region>.convex.cloud` outside US East.
|
|
230
|
+
*/
|
|
231
|
+
const CLOUD_URL = /^https:\/\/[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud$/;
|
|
232
|
+
/**
|
|
233
|
+
* The HTTP actions URL of a deployment: the client URL with `.convex.site`
|
|
234
|
+
* for `.convex.cloud`, as Convex sets CONVEX_SITE_URL. Custom domains are
|
|
235
|
+
* not covered.
|
|
236
|
+
*/
|
|
237
|
+
const siteUrlOf = (url) => url.replace(/\.convex\.cloud$/, ".convex.site");
|
|
238
|
+
/** One row of list_deployments, GET /deployments/{name}, and create_deployment. Only the fields the provider uses. */
|
|
239
|
+
const DeploymentRow$1 = Schema.Struct({
|
|
240
|
+
name: Schema.String,
|
|
241
|
+
projectId: Schema.Number,
|
|
242
|
+
deploymentType: Schema.String,
|
|
243
|
+
isDefault: Schema.Boolean,
|
|
244
|
+
reference: Schema.String,
|
|
245
|
+
/** Set in list_deployments and create_deployment rows only: GET /deployments/{name} answers null. */
|
|
246
|
+
previewIdentifier: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
247
|
+
deploymentUrl: Schema.String.check(Schema.isPattern(CLOUD_URL))
|
|
248
|
+
});
|
|
249
|
+
const DeploymentRows$1 = Schema.Array(DeploymentRow$1);
|
|
250
|
+
const attributesOf = (row, type, previewName) => ({
|
|
251
|
+
projectId: row.projectId,
|
|
252
|
+
type,
|
|
253
|
+
name: row.name,
|
|
254
|
+
reference: row.reference,
|
|
255
|
+
previewName: type === "preview" ? previewName : void 0,
|
|
256
|
+
url: row.deploymentUrl,
|
|
257
|
+
siteUrl: siteUrlOf(row.deploymentUrl)
|
|
258
|
+
});
|
|
259
|
+
/** The `name` prop of the deployment in state: the dev reference or the preview name. */
|
|
260
|
+
const nameOf = (attributes) => attributes.type === "dev" ? attributes.reference : attributes.type === "preview" ? attributes.previewName : void 0;
|
|
261
|
+
/**
|
|
262
|
+
* Fails with {@link DeploymentIdentityChange} when the props name another
|
|
263
|
+
* deployment than `current`: the attributes in state, or a live row.
|
|
264
|
+
*/
|
|
265
|
+
const checkIdentity$2 = (current, news) => {
|
|
266
|
+
const change = (field, from, to) => Effect.fail(new DeploymentIdentityChange({
|
|
267
|
+
deployment: current.name,
|
|
268
|
+
field,
|
|
269
|
+
current: from,
|
|
270
|
+
requested: to
|
|
271
|
+
}));
|
|
272
|
+
if (current.projectId !== news.projectId) return change("projectId", String(current.projectId), String(news.projectId));
|
|
273
|
+
if (current.type !== news.type) return change("type", current.type, news.type);
|
|
274
|
+
const currentName = nameOf(current);
|
|
275
|
+
if (news.type !== "prod" && currentName !== news.name) return change("name", currentName ?? "(none)", news.name);
|
|
276
|
+
return Effect.void;
|
|
277
|
+
};
|
|
278
|
+
const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
|
|
279
|
+
const api = yield* ManagementApi;
|
|
280
|
+
/** The live deployment, or undefined after a 404. GET /deployments/{name} answers no preview identifier. */
|
|
281
|
+
const getByName = (name) => absentAsUndefined(api.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)));
|
|
282
|
+
const listDeployments = (projectId, type, isDefault) => api.request("list deployments", (client) => client.GET("/projects/{project_id}/list_deployments", { params: {
|
|
283
|
+
path: { project_id: projectId },
|
|
284
|
+
query: {
|
|
285
|
+
deploymentType: type,
|
|
286
|
+
...isDefault === void 0 ? {} : { isDefault }
|
|
287
|
+
}
|
|
288
|
+
} })).pipe(Effect.map((body) => Schema.decodeUnknownSync(DeploymentRows$1)(body)));
|
|
289
|
+
/** The project's default production deployment, or undefined. */
|
|
290
|
+
const production = (projectId) => listDeployments(projectId, "prod", true).pipe(Effect.map((rows) => {
|
|
291
|
+
const row = rows.find((candidate) => candidate.deploymentType === "prod" && candidate.isDefault);
|
|
292
|
+
return row === void 0 ? void 0 : attributesOf(row, "prod", void 0);
|
|
293
|
+
}));
|
|
294
|
+
/**
|
|
295
|
+
* The one dev deployment with this reference, or the one preview
|
|
296
|
+
* deployment with this preview name, or undefined.
|
|
297
|
+
*/
|
|
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));
|
|
300
|
+
if (matches.length > 1) return yield* new AmbiguousDeployment({
|
|
301
|
+
projectId,
|
|
302
|
+
type,
|
|
303
|
+
name,
|
|
304
|
+
deployments: matches.map((row) => row.name)
|
|
305
|
+
});
|
|
306
|
+
return matches[0];
|
|
307
|
+
});
|
|
308
|
+
/** The deployment that the props name, for a resource without state. */
|
|
309
|
+
const lookup = (news) => news.type === "prod" ? production(news.projectId) : findByName(news.projectId, news.type, news.name).pipe(Effect.map((row) => row === void 0 ? void 0 : attributesOf(row, news.type, news.name)));
|
|
310
|
+
return {
|
|
311
|
+
stables: [
|
|
312
|
+
"projectId",
|
|
313
|
+
"type",
|
|
314
|
+
"previewName"
|
|
315
|
+
],
|
|
316
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
317
|
+
if (!isResolved(news) || output === void 0) return void 0;
|
|
318
|
+
yield* checkIdentity$2(output, news);
|
|
319
|
+
if (news.type === "prod") return (yield* absentAsUndefined(production(news.projectId)))?.name === output.name ? void 0 : { action: "update" };
|
|
320
|
+
return (yield* getByName(output.name)) === void 0 ? { action: "update" } : void 0;
|
|
321
|
+
}),
|
|
322
|
+
read: Effect.fn(function* ({ olds, output }) {
|
|
323
|
+
if (output?.type === "prod") return yield* absentAsUndefined(production(output.projectId));
|
|
324
|
+
if (output !== void 0) {
|
|
325
|
+
const live = yield* getByName(output.name);
|
|
326
|
+
return live === void 0 ? void 0 : attributesOf(live, output.type, output.previewName);
|
|
327
|
+
}
|
|
328
|
+
const found = yield* lookup(olds);
|
|
329
|
+
if (found === void 0) return void 0;
|
|
330
|
+
return olds.type === "prod" ? found : Unowned(found);
|
|
331
|
+
}),
|
|
332
|
+
reconcile: Effect.fn(function* ({ fqn, news, output }) {
|
|
333
|
+
if (output !== void 0) yield* checkIdentity$2(output, news);
|
|
334
|
+
if (news.type === "prod") {
|
|
335
|
+
const current = yield* production(news.projectId);
|
|
336
|
+
if (current === void 0) return yield* new ProductionDeploymentNotFound({ projectId: news.projectId });
|
|
337
|
+
return current;
|
|
338
|
+
}
|
|
339
|
+
if (output !== void 0) {
|
|
340
|
+
const live = yield* getByName(output.name);
|
|
341
|
+
if (live !== void 0) {
|
|
342
|
+
if (live.deploymentType !== output.type) return yield* new DeploymentIdentityChange({
|
|
343
|
+
deployment: live.name,
|
|
344
|
+
field: "type",
|
|
345
|
+
current: live.deploymentType,
|
|
346
|
+
requested: news.type
|
|
347
|
+
});
|
|
348
|
+
const current = attributesOf(live, output.type, output.previewName);
|
|
349
|
+
yield* checkIdentity$2(current, news);
|
|
350
|
+
return current;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const existing = yield* lookup(news);
|
|
354
|
+
if (existing !== void 0) {
|
|
355
|
+
if (!(yield* shouldAdopt(fqn))) return yield* new OwnedBySomeoneElse({
|
|
356
|
+
message: `Project ${news.projectId} already has ${news.type} deployment ${existing.name} named "${news.name}". Re-run with --adopt to take it over, or use another name.`,
|
|
357
|
+
resourceType: Deployment.Type,
|
|
358
|
+
physicalName: existing.name
|
|
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
|
|
367
|
+
}
|
|
368
|
+
}));
|
|
369
|
+
return attributesOf(Schema.decodeUnknownSync(DeploymentRow$1)(created), news.type, news.name);
|
|
370
|
+
}),
|
|
371
|
+
delete: Effect.fn(function* ({ output }) {
|
|
372
|
+
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.`);
|
|
373
|
+
if (output.type === "prod") return yield* keep;
|
|
374
|
+
const live = yield* getByName(output.name);
|
|
375
|
+
if (live === void 0) return;
|
|
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
|
|
711
|
+
});
|
|
712
|
+
}
|
|
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
|
+
}
|
|
918
|
+
}, session);
|
|
919
|
+
const output = `${stdout}\n${stderr}`;
|
|
920
|
+
const deployed = parseDeployOutput(output);
|
|
921
|
+
if (deployed === void 0) return yield* new DeployOutputError({ outputTail: stripVTControlCharacters(output).split(/\r?\n|\r/).filter((line) => line.trim() !== "").slice(-20).join("\n") });
|
|
922
|
+
return {
|
|
923
|
+
...deployed,
|
|
924
|
+
hash: yield* hashOf(news)
|
|
925
|
+
};
|
|
926
|
+
}),
|
|
927
|
+
delete: () => Effect.void
|
|
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 } },
|
|
1082
|
+
body: {
|
|
1083
|
+
name,
|
|
1084
|
+
...news.allowedActions === void 0 ? {} : { allowedActions: [...news.allowedActions] }
|
|
1085
|
+
}
|
|
1086
|
+
})).pipe(Effect.map((body) => Schema.decodeUnknownSync(CreatedDeployKey)(body).deployKey)),
|
|
1087
|
+
isCredentialToken: api.isCredentialToken
|
|
1088
|
+
});
|
|
1089
|
+
return {
|
|
1090
|
+
uniqueName,
|
|
1091
|
+
keyId,
|
|
1092
|
+
deployment: news.deployment,
|
|
1093
|
+
deployKey: secret
|
|
1094
|
+
};
|
|
1095
|
+
}),
|
|
1096
|
+
delete: Effect.fn(function* ({ output }) {
|
|
1097
|
+
const secret = yield* secretToDelete({
|
|
1098
|
+
keyId: output.keyId,
|
|
1099
|
+
secret: output.deployKey
|
|
1100
|
+
}, `Convex.DeployKey "${output.uniqueName}" on ${output.deployment}`);
|
|
1101
|
+
if (secret === void 0) return;
|
|
1102
|
+
yield* absentAsUndefined(deleteKey(output.deployment, secret));
|
|
1103
|
+
})
|
|
1104
|
+
};
|
|
1105
|
+
}));
|
|
1106
|
+
//#endregion
|
|
1107
|
+
//#region src/PreviewDeployKey.ts
|
|
1108
|
+
/** GET /projects/{project_id}/list_preview_deploy_keys */
|
|
1109
|
+
const PreviewDeployKeyList = Schema.Struct({ items: Schema.Array(ListedKey) });
|
|
1110
|
+
/** POST /projects/{project_id}/create_preview_deploy_key */
|
|
1111
|
+
const CreatedPreviewDeployKey = Schema.Struct({ previewDeployKey: Schema.String });
|
|
1112
|
+
const PreviewDeployKey = Resource("Convex.PreviewDeployKey");
|
|
1113
|
+
const PreviewDeployKeyProvider = () => Provider.effect(PreviewDeployKey, Effect.gen(function* () {
|
|
1114
|
+
const api = yield* ManagementApi;
|
|
1115
|
+
const listKeys = (projectId) => api.request("list preview deploy keys", (client) => client.GET("/projects/{project_id}/list_preview_deploy_keys", { params: { path: { project_id: projectId } } })).pipe(Effect.map((body) => Schema.decodeUnknownSync(PreviewDeployKeyList)(body).items));
|
|
1116
|
+
/**
|
|
1117
|
+
* `id` is the secret: Convex accepts it, and it matches only the key
|
|
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
|
+
});
|
|
1150
|
+
return {
|
|
1151
|
+
uniqueName,
|
|
1152
|
+
keyId,
|
|
1153
|
+
projectId: news.projectId,
|
|
1154
|
+
previewDeployKey: secret
|
|
1155
|
+
};
|
|
1156
|
+
}),
|
|
1157
|
+
delete: Effect.fn(function* ({ output }) {
|
|
1158
|
+
const secret = yield* secretToDelete({
|
|
1159
|
+
keyId: output.keyId,
|
|
1160
|
+
secret: output.previewDeployKey
|
|
1161
|
+
}, `Convex.PreviewDeployKey "${output.uniqueName}" in project ${output.projectId}`);
|
|
1162
|
+
if (secret === void 0) return;
|
|
1163
|
+
yield* absentAsUndefined(deleteKey(output.projectId, secret));
|
|
1164
|
+
})
|
|
1165
|
+
};
|
|
1166
|
+
}));
|
|
1167
|
+
//#endregion
|
|
1168
|
+
//#region src/Project.ts
|
|
1169
|
+
const Project = Resource("Convex.Project", { defaultRemovalPolicy: "retain" });
|
|
1170
|
+
/** The props change the team of an existing project. Convex cannot move a project. */
|
|
1171
|
+
var ProjectTeamChange = class extends Schema.TaggedError()("ProjectTeamChange", {
|
|
1172
|
+
projectId: Schema.Number,
|
|
1173
|
+
teamId: Schema.Number,
|
|
1174
|
+
requestedTeamId: Schema.Number
|
|
1175
|
+
}) {
|
|
1176
|
+
get message() {
|
|
1177
|
+
return `Convex.Project cannot move project ${this.projectId} from team ${this.teamId} to team ${this.requestedTeamId}. Set team back to ${this.teamId}. To use the other team, create a new resource with a new logical id and move the data yourself.`;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
/**
|
|
1181
|
+
* The props change `deploymentType` or `deploymentRegion` of an existing
|
|
1182
|
+
* project. Convex uses them only when it creates the project.
|
|
1183
|
+
*/
|
|
1184
|
+
var ProjectDeploymentChange = class extends Schema.TaggedError()("ProjectDeploymentChange", {
|
|
1185
|
+
projectId: Schema.Number,
|
|
1186
|
+
field: Schema.Literals(["deploymentType", "deploymentRegion"]),
|
|
1187
|
+
current: Schema.UndefinedOr(Schema.String),
|
|
1188
|
+
requested: Schema.UndefinedOr(Schema.String)
|
|
1189
|
+
}) {
|
|
1190
|
+
get message() {
|
|
1191
|
+
const show = (value) => value === void 0 ? "(not set)" : value;
|
|
1192
|
+
return `Convex.Project cannot change ${this.field} of project ${this.projectId} from ${show(this.current)} to ${show(this.requested)}: Convex uses it only when it creates a project. Set ${this.field} back to ${show(this.current)}. For a new deployment, create a new resource with a new logical id and move the data yourself.`;
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
/** More than one project in the team has the name, so the lookup cannot choose one. */
|
|
1196
|
+
var AmbiguousProject = class extends Schema.TaggedError()("AmbiguousProject", {
|
|
1197
|
+
teamId: Schema.Number,
|
|
1198
|
+
name: Schema.String,
|
|
1199
|
+
projectIds: Schema.Array(Schema.Number)
|
|
1200
|
+
}) {
|
|
1201
|
+
get message() {
|
|
1202
|
+
return `Team ${this.teamId} has ${this.projectIds.length} projects named "${this.name}" (ids ${this.projectIds.join(", ")}). Convex.Project does not choose one. Rename the projects in the Convex dashboard so that only one has this name, or use a different name.`;
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
/** Convex answered a create call but then did not return the project. */
|
|
1206
|
+
var CreatedProjectNotFound = class extends Schema.TaggedError()("CreatedProjectNotFound", { projectId: Schema.Number }) {
|
|
1207
|
+
get message() {
|
|
1208
|
+
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
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
/** Fields of GET /projects/{id}, PATCH /projects/{id}, and GET /teams/{team}/projects rows that the provider uses. */
|
|
1212
|
+
const ProjectDetails = Schema.Struct({
|
|
1213
|
+
id: Schema.Number,
|
|
1214
|
+
name: Schema.String,
|
|
1215
|
+
slug: Schema.String,
|
|
1216
|
+
teamId: Schema.Number,
|
|
1217
|
+
prodDeploymentName: Schema.NullOr(Schema.String)
|
|
1218
|
+
});
|
|
1219
|
+
/** GET /teams/{team_id}/projects: one page of projects. */
|
|
1220
|
+
const ProjectPage = Schema.Struct({
|
|
1221
|
+
items: Schema.Array(ProjectDetails),
|
|
1222
|
+
pagination: Schema.Struct({
|
|
1223
|
+
hasMore: Schema.Boolean,
|
|
1224
|
+
nextCursor: Schema.optionalKey(Schema.NullOr(Schema.String))
|
|
1225
|
+
})
|
|
1226
|
+
});
|
|
1227
|
+
/** POST /teams/{team_id}/create_project. Only the id: everything else comes from GET /projects/{id}. */
|
|
1228
|
+
const CreatedProject = Schema.Struct({ projectId: Schema.Number });
|
|
1229
|
+
const DeploymentRow = Schema.Struct({
|
|
1230
|
+
name: Schema.String,
|
|
1231
|
+
deploymentType: Schema.String,
|
|
1232
|
+
isDefault: Schema.Boolean,
|
|
1233
|
+
deploymentUrl: Schema.String
|
|
1234
|
+
});
|
|
1235
|
+
const DeploymentRows = Schema.Array(DeploymentRow);
|
|
1236
|
+
/**
|
|
1237
|
+
* Fails with {@link ProjectDeploymentChange} when a create-time prop
|
|
1238
|
+
* changed. An omitted `deploymentType` means `prod`.
|
|
1239
|
+
*/
|
|
1240
|
+
const checkCreateTimeProps = (projectId, olds, news) => {
|
|
1241
|
+
const oldType = olds.deploymentType ?? "prod";
|
|
1242
|
+
const newType = news.deploymentType ?? "prod";
|
|
1243
|
+
if (oldType !== newType) return Effect.fail(new ProjectDeploymentChange({
|
|
1244
|
+
projectId,
|
|
1245
|
+
field: "deploymentType",
|
|
1246
|
+
current: oldType,
|
|
1247
|
+
requested: newType
|
|
1248
|
+
}));
|
|
1249
|
+
if (olds.deploymentRegion !== news.deploymentRegion) return Effect.fail(new ProjectDeploymentChange({
|
|
1250
|
+
projectId,
|
|
1251
|
+
field: "deploymentRegion",
|
|
1252
|
+
current: olds.deploymentRegion,
|
|
1253
|
+
requested: news.deploymentRegion
|
|
1254
|
+
}));
|
|
1255
|
+
return Effect.void;
|
|
1256
|
+
};
|
|
1257
|
+
const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
1258
|
+
const api = yield* ManagementApi;
|
|
1259
|
+
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)));
|
|
1260
|
+
const attributesOf = (details) => prodDeployment(details.id).pipe(Effect.map((prod) => ({
|
|
1261
|
+
projectId: details.id,
|
|
1262
|
+
slug: details.slug,
|
|
1263
|
+
name: details.name,
|
|
1264
|
+
teamId: details.teamId,
|
|
1265
|
+
prodDeploymentName: prod?.name ?? details.prodDeploymentName ?? void 0,
|
|
1266
|
+
prodDeploymentUrl: prod?.deploymentUrl
|
|
1267
|
+
})));
|
|
1268
|
+
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)));
|
|
1269
|
+
/**
|
|
1270
|
+
* The one project in the team with exactly this name, or undefined.
|
|
1271
|
+
* Reads every page. `q` narrows the listing on the server, and the
|
|
1272
|
+
* exact match here decides. Pattern from Confect
|
|
1273
|
+
* `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
|
|
1274
|
+
* (https://github.com/rjdellecese/confect, ISC license).
|
|
1275
|
+
*/
|
|
1276
|
+
const findByName = (teamId, name) => Effect.gen(function* () {
|
|
1277
|
+
const matches = [];
|
|
1278
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
1279
|
+
let cursor;
|
|
1280
|
+
while (true) {
|
|
1281
|
+
const body = yield* api.request("list projects", (client) => client.GET("/teams/{team_id}/projects", { params: {
|
|
1282
|
+
path: { team_id: teamId },
|
|
1283
|
+
query: {
|
|
1284
|
+
q: name,
|
|
1285
|
+
...cursor === void 0 ? {} : { cursor }
|
|
1286
|
+
}
|
|
1287
|
+
} }));
|
|
1288
|
+
const page = Schema.decodeUnknownSync(ProjectPage)(body);
|
|
1289
|
+
matches.push(...page.items.filter((row) => row.name === name));
|
|
1290
|
+
if (!page.pagination.hasMore) break;
|
|
1291
|
+
const next = page.pagination.nextCursor;
|
|
1292
|
+
if (next === void 0 || next === null || seenCursors.has(next)) return yield* new ConvexApiError({
|
|
1293
|
+
operation: "list projects",
|
|
1294
|
+
status: 0,
|
|
1295
|
+
code: "InvalidPagination",
|
|
1296
|
+
message: `The project listing of team ${teamId} reports more pages but gives no new cursor. Convex.Project stops so that it does not miss a project named "${name}".`
|
|
1297
|
+
});
|
|
1298
|
+
seenCursors.add(next);
|
|
1299
|
+
cursor = next;
|
|
1300
|
+
}
|
|
1301
|
+
if (matches.length > 1) return yield* new AmbiguousProject({
|
|
1302
|
+
teamId,
|
|
1303
|
+
name,
|
|
1304
|
+
projectIds: matches.map((row) => row.id)
|
|
1305
|
+
});
|
|
1306
|
+
return matches[0];
|
|
1307
|
+
});
|
|
1308
|
+
/**
|
|
1309
|
+
* Brings an existing project to the desired name. The team cannot
|
|
1310
|
+
* change, so a project in another team fails.
|
|
1311
|
+
*/
|
|
1312
|
+
const converge = (details, teamId, name) => Effect.gen(function* () {
|
|
1313
|
+
if (details.teamId !== teamId) return yield* new ProjectTeamChange({
|
|
1314
|
+
projectId: details.id,
|
|
1315
|
+
teamId: details.teamId,
|
|
1316
|
+
requestedTeamId: teamId
|
|
1317
|
+
});
|
|
1318
|
+
if (details.name === name) return yield* attributesOf(details);
|
|
1319
|
+
const updated = yield* api.request("update project", (client) => client.PATCH("/projects/{project_id}", {
|
|
1320
|
+
params: { path: { project_id: details.id } },
|
|
1321
|
+
body: { name }
|
|
1322
|
+
}));
|
|
1323
|
+
return yield* attributesOf(Schema.decodeUnknownSync(ProjectDetails)(updated));
|
|
1324
|
+
});
|
|
1325
|
+
return {
|
|
1326
|
+
stables: [
|
|
1327
|
+
"projectId",
|
|
1328
|
+
"slug",
|
|
1329
|
+
"teamId"
|
|
1330
|
+
],
|
|
1331
|
+
diff: Effect.fn(function* ({ olds, news, output }) {
|
|
1332
|
+
if (!isResolved(news)) return void 0;
|
|
1333
|
+
if (output === void 0) return void 0;
|
|
1334
|
+
if (olds.team !== news.team) {
|
|
1335
|
+
const requestedTeamId = yield* api.resolveTeamId(news.team);
|
|
1336
|
+
if (requestedTeamId !== output.teamId) return yield* new ProjectTeamChange({
|
|
1337
|
+
projectId: output.projectId,
|
|
1338
|
+
teamId: output.teamId,
|
|
1339
|
+
requestedTeamId
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
return olds.name === news.name ? void 0 : { action: "update" };
|
|
1343
|
+
}),
|
|
1344
|
+
read: Effect.fn(function* ({ olds, output }) {
|
|
1345
|
+
if (output !== void 0) {
|
|
1346
|
+
const details = yield* getById(output.projectId);
|
|
1347
|
+
return details === void 0 ? void 0 : yield* attributesOf(details);
|
|
1348
|
+
}
|
|
1349
|
+
const existing = yield* findByName(yield* api.resolveTeamId(olds.team), olds.name);
|
|
1350
|
+
return existing === void 0 ? void 0 : Unowned(yield* attributesOf(existing));
|
|
1351
|
+
}),
|
|
1352
|
+
reconcile: Effect.fn(function* ({ fqn, olds, news, output }) {
|
|
1353
|
+
const teamId = yield* api.resolveTeamId(news.team);
|
|
1354
|
+
if (output !== void 0) {
|
|
1355
|
+
if (olds !== void 0) yield* checkCreateTimeProps(output.projectId, olds, news);
|
|
1356
|
+
if (output.teamId !== teamId) return yield* new ProjectTeamChange({
|
|
1357
|
+
projectId: output.projectId,
|
|
1358
|
+
teamId: output.teamId,
|
|
1359
|
+
requestedTeamId: teamId
|
|
1360
|
+
});
|
|
1361
|
+
const details = yield* getById(output.projectId);
|
|
1362
|
+
if (details !== void 0) return yield* converge(details, teamId, news.name);
|
|
1363
|
+
} else {
|
|
1364
|
+
const existing = yield* findByName(teamId, news.name);
|
|
1365
|
+
if (existing !== void 0) {
|
|
1366
|
+
if (!(yield* shouldAdopt(fqn))) return yield* new OwnedBySomeoneElse({
|
|
1367
|
+
message: `Convex project ${existing.id} in team ${teamId} already has the name "${news.name}". Re-run with --adopt to take it over, or use a different name.`,
|
|
1368
|
+
resourceType: Project.Type,
|
|
1369
|
+
physicalName: news.name
|
|
1370
|
+
});
|
|
1371
|
+
return yield* converge(existing, teamId, news.name);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const created = yield* api.request("create project", (client) => client.POST("/teams/{team_id}/create_project", {
|
|
1375
|
+
params: { path: { team_id: teamId } },
|
|
1376
|
+
body: {
|
|
1377
|
+
projectName: news.name,
|
|
1378
|
+
deploymentType: news.deploymentType ?? "prod",
|
|
1379
|
+
...news.deploymentRegion === void 0 ? {} : { deploymentRegion: news.deploymentRegion }
|
|
1380
|
+
}
|
|
1381
|
+
}));
|
|
1382
|
+
const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
|
|
1383
|
+
const details = yield* getById(projectId);
|
|
1384
|
+
if (details === void 0) return yield* new CreatedProjectNotFound({ projectId });
|
|
1385
|
+
return yield* converge(details, teamId, news.name);
|
|
1386
|
+
}),
|
|
1387
|
+
delete: Effect.fn(function* ({ output }) {
|
|
1388
|
+
yield* absentAsUndefined(api.requestVoid("delete project", (client) => client.POST("/projects/{project_id}/delete", { params: { path: { project_id: output.projectId } } })));
|
|
1389
|
+
})
|
|
1390
|
+
};
|
|
1391
|
+
}));
|
|
1392
|
+
//#endregion
|
|
1393
|
+
//#region src/Providers.ts
|
|
1394
|
+
var Providers = class extends Provider.ProviderCollection()("Convex") {};
|
|
1395
|
+
/**
|
|
1396
|
+
* All Convex resource providers. Pass a credentials layer to override the
|
|
1397
|
+
* default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login).
|
|
1398
|
+
*/
|
|
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);
|
|
1409
|
+
//#endregion
|
|
1410
|
+
export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, Code, CodePushFailed, ConvexApiError, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DefaultEnvironmentVariable, Deploy, DeployArgumentError, DeployKey, DeployKeyIsCredential, DeployKeyMismatch, DeployKeyRecoveryRequired, DeployOutputError, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, EnvironmentVariable, ManagementApi, PreviewDeployKey, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectTeamChange, Providers, PushTargetMismatch, VariableIdentityChange, deployArgs, fromConfig, fromToken, parseDeployOutput, providers };
|
|
1411
|
+
|
|
11
1412
|
//# sourceMappingURL=index.js.map
|