@samebase/alchemy-convex 0.14.1 → 0.16.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/lib/index.js CHANGED
@@ -1,35 +1,10 @@
1
- import { createRequire } from "node:module";
1
+ import { A as PushTargetMismatch, B as DEPLOY_KEY_ENV, C as DeploymentProvider, D as ConvexAppNotFound, E as CodePushFailed, F as ManagementApi, H as fromValues, I as ManagementApiLive, L as ACCESS_TOKEN_ENV, N as parseDeployOutput, O as ConvexCliNotFound, P as ConvexApiError, R as Credentials, S as DeploymentIdentityChange, T as ProductionDeploymentNotFound, V as fromConfig, _ as ProjectProvider, b as AmbiguousDeployment, c as RUN_RECORD_KEY, d as withRunRecord, f as AmbiguousProject, g as ProjectNotFound, h as ProjectDeploymentChange, i as HubClientProvider, j as VariablesOwnedBySomeoneElse, k as EnvironmentNeedsDeployKey, l as RunRecordError, m as Project, n as HubClient, o as STORE_PROJECT_NAME, p as CreatedProjectNotFound, r as HubClientFailed, s as state, t as HUB_PROJECT_NAME, u as runRecord, v as ProjectTeamChange, w as DeploymentResource, x as Deployment, y as AbsoluteRootDir, z as CredentialsError } from "./HubClient-BYBV7cF5.js";
2
2
  import { toPath } from "alchemy/FQN";
3
3
  import { Stack } from "alchemy/Stack";
4
4
  import * as Effect from "effect/Effect";
5
5
  import * as Schema from "effect/Schema";
6
- import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
7
- import { homedir, tmpdir } from "node:os";
8
- import { dirname, join, posix, relative, resolve, sep, win32 } from "node:path";
9
- import * as Config from "effect/Config";
10
- import * as Context from "effect/Context";
11
6
  import * as Layer from "effect/Layer";
12
- import * as Option from "effect/Option";
13
- import * as Redacted from "effect/Redacted";
14
- import * as Result from "effect/Result";
15
- import { Resource } from "alchemy";
16
- import { AdoptPolicy, OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
17
- import { diffBindings, isResolved } from "alchemy/Diff";
18
7
  import * as Provider from "alchemy/Provider";
19
- import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
20
- import { AlchemyContext } from "alchemy/AlchemyContext";
21
- import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
22
- import * as Schedule from "effect/Schedule";
23
- import { createHash } from "node:crypto";
24
- import { promisify, stripVTControlCharacters } from "node:util";
25
- import { hashDirectory } from "alchemy/Command/Memo";
26
- import * as Clock from "effect/Clock";
27
- import * as ChildProcess from "effect/process/ChildProcess";
28
- import * as Stream from "effect/Stream";
29
- import { execFile } from "node:child_process";
30
- import { fileURLToPath } from "node:url";
31
- import * as Scope from "effect/Scope";
32
- import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveState } from "alchemy/State";
33
8
  //#region src/Component.ts
34
9
  /**
35
10
  * Associate a native namespace with an installed component. `component` is the
@@ -55,1366 +30,6 @@ const bindComponent = (options) => Effect.gen(function* () {
55
30
  });
56
31
  });
57
32
  //#endregion
58
- //#region src/Credentials.ts
59
- var Credentials = class extends Context.Service()("@samebase/alchemy-convex/Credentials") {};
60
- /** Environment variable for the access token, read by {@link fromConfig}. */
61
- const ACCESS_TOKEN_ENV = "CONVEX_ACCESS_TOKEN";
62
- /** Environment variable for the deploy key, read by {@link fromConfig}. The Convex CLI reads the same name. */
63
- const DEPLOY_KEY_ENV = "CONVEX_DEPLOY_KEY";
64
- /** The Convex CLI stores its device login here after `npx convex login`. */
65
- const convexCliConfigPath = () => join(homedir(), ".convex", "config.json");
66
- const CliConfig = Schema.Struct({ accessToken: Schema.String });
67
- var CredentialsError = class extends Schema.TaggedError()("CredentialsError", { reason: Schema.String }) {};
68
- /**
69
- * Token the Convex CLI saved on this machine. It works on the Management API
70
- * for the teams the logged-in user belongs to. Fails when nobody is logged in.
71
- */
72
- const readConvexCliLogin = Effect.gen(function* () {
73
- const path = convexCliConfigPath();
74
- const text = yield* Effect.tryPromise({
75
- try: () => readFile(path, "utf8"),
76
- 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}.` })
77
- });
78
- const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(CliConfig))(text).pipe(Effect.mapError(() => new CredentialsError({ reason: `${path} has no accessToken` })));
79
- return {
80
- accessToken: Redacted.make(parsed.accessToken),
81
- source: "convex-cli-login"
82
- };
83
- });
84
- /**
85
- * Default resolution through the stack ConfigProvider (process env, `.env`,
86
- * stack secrets): `CONVEX_ACCESS_TOKEN`, then the Convex CLI login on this
87
- * machine, for the access token; `CONVEX_DEPLOY_KEY` for the deploy key. A
88
- * developer who ran `npx convex login` needs nothing. CI sets only the
89
- * deploy key when the stack pushes to deployments that exist, and the access
90
- * token when the stack also creates projects or deployments.
91
- */
92
- const fromConfig = () => Layer.effect(Credentials, Effect.gen(function* () {
93
- const resolved = yield* Effect.cached(Config.Redacted(ACCESS_TOKEN_ENV).pipe(Effect.map((token) => ({
94
- accessToken: token,
95
- source: "env"
96
- })), Effect.catch(() => readConvexCliLogin), Effect.result));
97
- const deployKey = yield* Effect.cached(Config.option(Config.Redacted(DEPLOY_KEY_ENV)).pipe(Effect.map(Option.getOrUndefined), Effect.orDie));
98
- return {
99
- accessToken: resolved.pipe(Effect.flatMap(Effect.fromResult), Effect.orDie),
100
- hasAccessToken: resolved.pipe(Effect.map(Result.isSuccess)),
101
- deployKey
102
- };
103
- }));
104
- /** Explicit values, for tests or callers that already hold them. */
105
- const fromValues = (values) => Layer.succeed(Credentials, {
106
- accessToken: values.accessToken === void 0 ? Effect.die(new CredentialsError({ reason: "This step needs the Convex Management API, and fromValues got no accessToken." })) : Effect.succeed({
107
- accessToken: values.accessToken,
108
- source: "explicit"
109
- }),
110
- hasAccessToken: Effect.succeed(values.accessToken !== void 0),
111
- deployKey: Effect.succeed(values.deployKey)
112
- });
113
- //#endregion
114
- //#region src/Adoption.ts
115
- /** True when the resource `fqn` may take over an object that is not in state. */
116
- const shouldAdopt = (fqn) => Effect.gen(function* () {
117
- const stack = yield* Effect.serviceOption(Stack);
118
- const own = Option.isSome(stack) ? stack.value.resources[fqn]?.Adopt : void 0;
119
- if (own !== void 0) return own;
120
- const policy = yield* Effect.serviceOption(AdoptPolicy);
121
- if (Option.isSome(policy)) return policy.value;
122
- const context = yield* Effect.serviceOption(AlchemyContext);
123
- return Option.isSome(context) && context.value.adopt;
124
- });
125
- //#endregion
126
- //#region src/ManagementApi.ts
127
- /**
128
- * One error for every control-plane and deployment call. Resources branch on
129
- * `status` (404 means absent for read and delete), so the class is load
130
- * bearing, not decorative.
131
- */
132
- var ConvexApiError = class extends Schema.TaggedError()("ConvexApiError", {
133
- operation: Schema.String,
134
- status: Schema.Number,
135
- code: Schema.optionalKey(Schema.String),
136
- message: Schema.String
137
- }) {};
138
- const ErrorBody = Schema.Struct({
139
- code: Schema.optionalKey(Schema.String),
140
- message: Schema.optionalKey(Schema.String)
141
- });
142
- const toApiError = (operation, result) => {
143
- const body = Schema.decodeUnknownSync(Schema.NullishOr(ErrorBody))(result.error ?? null);
144
- const code = body?.code;
145
- return new ConvexApiError({
146
- operation,
147
- status: result.response.status,
148
- ...code === void 0 ? {} : { code },
149
- message: body?.message ?? `${operation} failed with HTTP ${result.response.status}`
150
- });
151
- };
152
- /** The part of a Convex token after the first "|", or the whole value when it has none. */
153
- const tokenPart = (value) => value.slice(value.indexOf("|") + 1);
154
- var ManagementApi = class extends Context.Service()("@samebase/alchemy-convex/ManagementApi") {};
155
- const attempt = (operation, run) => Effect.tryPromise({
156
- try: run,
157
- catch: (cause) => new ConvexApiError({
158
- operation,
159
- status: 0,
160
- message: cause instanceof Error ? cause.message : String(cause)
161
- })
162
- });
163
- /** Reads `data` or fails with the HTTP status and the API's own code and message. */
164
- 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))));
165
- /** For endpoints that answer with an empty body, such as delete calls. */
166
- const settleVoid = (operation, run) => attempt(operation, run).pipe(Effect.flatMap((result) => result.response.ok ? Effect.void : Effect.fail(toApiError(operation, result))));
167
- /**
168
- * True when an idempotent write can succeed on a new attempt:
169
- *
170
- * - A Convex write conflict. Two parallel update_environment_variables calls
171
- * on one deployment gave HTTP 503 `OptimisticConcurrencyControlFailure`,
172
- * "Data read or written in this mutation changed while it was being run",
173
- * in a live run. The platform codes `OCC` and `WriteConflict` and the
174
- * message "... changed while this mutation was being run ..." mean the same.
175
- * - Any other 5xx answer. Two parallel delete_preview_deploy_key calls on one
176
- * project gave HTTP 500 `InternalServerError`, "Your request couldn't be
177
- * completed. Try again later.", in a live run. The next attempt succeeded.
178
- */
179
- 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;
180
- /**
181
- * Runs an idempotent write again after a retryable Convex error: at most
182
- * four more attempts, with exponential backoff from 100 ms and jitter. Other
183
- * errors fail at once, and so does the last retryable error. Use it only for
184
- * a write that has the same result when it runs twice, such as setting a
185
- * value or deleting by id. Never for a create that does not check first.
186
- */
187
- const retryIdempotentWrite = (effect) => effect.pipe(Effect.retry({
188
- while: (error) => error instanceof ConvexApiError && isRetryableWriteError(error),
189
- times: 4,
190
- schedule: Schedule.exponential("100 millis").pipe(Schedule.jittered)
191
- }));
192
- /** Turns a 404 into `undefined` so read and delete stay idempotent. */
193
- const absentAsUndefined = (effect) => effect.pipe(Effect.catchIf((error) => error.status === 404, () => Effect.succeed(void 0)));
194
- const TeamRow = Schema.Struct({
195
- id: Schema.Number,
196
- slug: Schema.String
197
- });
198
- const TeamRows = Schema.Array(TeamRow);
199
- /** The first page of GET /teams/{team_id}/projects: every row carries the slug of the team. */
200
- const ProjectSlugRows = Schema.Struct({ items: Schema.Array(Schema.Struct({ teamSlug: Schema.String })) });
201
- /** GET /token_details: the team of a team token, or the project of a project token. */
202
- const TokenDetails = Schema.Union([Schema.Struct({
203
- type: Schema.Literal("teamToken"),
204
- teamId: Schema.Number
205
- }), Schema.Struct({
206
- type: Schema.Literal("projectToken"),
207
- projectId: Schema.Number
208
- })]);
209
- /** The service for one way to get the bearer token. */
210
- const makeService = (credentials) => {
211
- const client = Effect.cached(credentials.pipe(Effect.map((resolved) => createManagementClient(Redacted.value(resolved.accessToken))))).pipe(Effect.flatten);
212
- const request = (operation, run) => client.pipe(Effect.flatMap((c) => settle(operation, () => run(c))));
213
- const requestVoid = (operation, run) => client.pipe(Effect.flatMap((c) => settleVoid(operation, () => run(c))));
214
- const deploymentRequest = (operation, deploymentUrlOrName, deployKey, run) => settle(operation, () => run(createDeploymentClient(deploymentUrlOrName, Redacted.value(deployKey))));
215
- const tokenDetails = Effect.runSync(Effect.cached(request("get token details", (c) => c.GET("/token_details")).pipe(Effect.map((body) => Schema.decodeUnknownSync(TokenDetails)(body)), Effect.catchIf((error) => error.status === 403, () => Effect.succeed(void 0)))));
216
- /** The slug of the team, from the first project of its listing, or undefined for a team without one. Read once. */
217
- const teamSlug = (teamId) => request("list projects", (c) => c.GET("/teams/{team_id}/projects", { params: { path: { team_id: teamId } } })).pipe(Effect.map((body) => Schema.decodeUnknownSync(ProjectSlugRows)(body).items[0]?.teamSlug));
218
- const resolveTeamId = (team) => Effect.gen(function* () {
219
- if (typeof team === "number") return team;
220
- if (team !== void 0 && /^\d+$/.test(team)) return Number(team);
221
- const details = yield* tokenDetails;
222
- if (details?.type === "teamToken") {
223
- if (team === void 0) return details.teamId;
224
- const slug = yield* teamSlug(details.teamId);
225
- if (slug === void 0) return yield* new ConvexApiError({
226
- operation: "resolve team",
227
- status: 0,
228
- message: `team "${team}" cannot be checked against the access token: team ${details.teamId} has no project yet. Set team to ${details.teamId}, or leave it out.`
229
- });
230
- if (slug !== team) return yield* new ConvexApiError({
231
- operation: "resolve team",
232
- status: 403,
233
- message: `the access token belongs to team ${details.teamId} ("${slug}"), and team is "${team}". Use a token of "${team}", or set team to ${details.teamId}.`
234
- });
235
- return details.teamId;
236
- }
237
- if (details?.type === "projectToken") return yield* new ConvexApiError({
238
- operation: "resolve team",
239
- status: 403,
240
- message: `the access token belongs to project ${details.projectId}, and a project token cannot create projects. Use a team access token.`
241
- });
242
- if (team === void 0) return yield* new ConvexApiError({
243
- operation: "resolve team",
244
- status: 0,
245
- message: "set team to the slug or the id of the Convex team: this access token does not name a team."
246
- });
247
- const resolved = yield* credentials;
248
- const match = (yield* Effect.tryPromise({
249
- try: async () => {
250
- const response = await fetch("https://api.convex.dev/api/dashboard/teams", { headers: { Authorization: `Bearer ${Redacted.value(resolved.accessToken)}` } });
251
- if (!response.ok) throw new Error(`team lookup for "${team}" failed with HTTP ${response.status}`);
252
- return Schema.decodeUnknownSync(TeamRows)(await response.json());
253
- },
254
- catch: (cause) => new ConvexApiError({
255
- operation: "resolve team",
256
- status: 0,
257
- message: cause instanceof Error ? cause.message : String(cause)
258
- })
259
- })).find((row) => row.slug === team);
260
- if (match === void 0) return yield* new ConvexApiError({
261
- operation: "resolve team",
262
- status: 404,
263
- message: `no team with slug "${team}" for this token`
264
- });
265
- return match.id;
266
- });
267
- const isCredentialToken = (secret) => credentials.pipe(Effect.map((resolved) => tokenPart(Redacted.value(resolved.accessToken)) === tokenPart(secret)));
268
- const withToken = (token) => makeService(Effect.succeed({
269
- accessToken: token,
270
- source: "explicit"
271
- }));
272
- return {
273
- request,
274
- requestVoid,
275
- deploymentRequest,
276
- resolveTeamId,
277
- isCredentialToken,
278
- withToken
279
- };
280
- };
281
- const ManagementApiLive = () => Layer.effect(ManagementApi, Effect.gen(function* () {
282
- const credentials = yield* Credentials;
283
- return makeService(credentials.accessToken);
284
- }));
285
- //#endregion
286
- //#region src/Push.ts
287
- const sourceOf = (source) => typeof source === "string" ? {
288
- path: source,
289
- command: void 0,
290
- include: void 0
291
- } : {
292
- path: source.path,
293
- command: source.command,
294
- include: source.include
295
- };
296
- /** Lifetime of a push key. Long enough for a slow push with index backfills. */
297
- const PUSH_KEY_LIFETIME_MS = 72e5;
298
- /** The only actions a key for `convex deploy` gets. */
299
- const PUSH_KEY_ACTIONS = [
300
- "deployment:deploy",
301
- "deployment:env:view",
302
- "deployment:env:write"
303
- ];
304
- /** POST /deployments/{deployment_name}/create_deploy_key */
305
- const CreatedDeployKey = Schema.Struct({ deployKey: Schema.String });
306
- /** GET https://<deployment>.convex.cloud/api/v1/list_environment_variables */
307
- const EnvironmentVariableList = Schema.Struct({ environmentVariables: Schema.Record(Schema.String, Schema.String) });
308
- /** No `convex` package resolves from the app directory. */
309
- var ConvexCliNotFound = class extends Schema.TaggedError()("ConvexCliNotFound", { cwd: Schema.String }) {
310
- get message() {
311
- 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 rootDir to the directory of the app.`;
312
- }
313
- };
314
- /** The app directory (`rootDir`) has no package.json. */
315
- var ConvexAppNotFound = class extends Schema.TaggedError()("ConvexAppNotFound", {
316
- /** The app directory, resolved against the working directory of the deploy. */
317
- rootDir: Schema.String }) {
318
- get message() {
319
- return `Convex.Deployment found no package.json in ${this.rootDir}. Set rootDir to the directory of the Convex app, relative to the directory where the deploy runs, such as "apps/web".`;
320
- }
321
- };
322
- /** The Convex CLI failed, or exited 0 without the deployment URL. The output tail never contains the key. */
323
- var CodePushFailed = class extends Schema.TaggedError()("CodePushFailed", {
324
- /** Exit code of the CLI. Undefined when it did not start or a signal stopped it. */
325
- exitCode: Schema.UndefinedOr(Schema.Number),
326
- /** The last lines of the CLI output, with the deploy key removed. */
327
- outputTail: Schema.String
328
- }) {
329
- get message() {
330
- 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}`;
331
- }
332
- };
333
- /** The CLI pushed to another deployment than the one of the resource. Checked after the push. */
334
- var PushTargetMismatch = class extends Schema.TaggedError()("PushTargetMismatch", {
335
- expected: Schema.String,
336
- actual: Schema.String
337
- }) {
338
- get message() {
339
- 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.`;
340
- }
341
- };
342
- /** Variables in `env` exist on the deployment with other values, and adoption is off. */
343
- var VariablesOwnedBySomeoneElse = class extends Schema.TaggedError()("VariablesOwnedBySomeoneElse", {
344
- deployment: Schema.String,
345
- names: Schema.Array(Schema.String)
346
- }) {
347
- get message() {
348
- 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.`;
349
- }
350
- };
351
- /** A preview deploy key cannot set variables: the deployment API takes only a deploy key of the deployment. */
352
- var EnvironmentNeedsDeployKey = class extends Schema.TaggedError()("EnvironmentNeedsDeployKey", { deployment: Schema.String }) {
353
- get message() {
354
- 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.`;
355
- }
356
- };
357
- /**
358
- * What a deploy key targets, from its prefix: `<type>:<deployment>|<secret>`
359
- * (or `<deployment>|<secret>` in old keys) for one deployment, and
360
- * `preview:<team>:<project>|<secret>` for the preview deployments of a
361
- * project. Same rules as convex 1.46 `cli/lib/deployment.js`.
362
- */
363
- const keyTargetOf = (key) => {
364
- const bar = key.indexOf("|");
365
- if (bar <= 0 || /[\s#'"`]/.test(key)) return { kind: "other" };
366
- const [first, second, third, ...rest] = key.slice(0, bar).split(":");
367
- if (rest.length > 0) return { kind: "other" };
368
- if (first === "preview" && second && third) return {
369
- kind: "preview",
370
- team: second,
371
- project: third
372
- };
373
- if (third !== void 0) return { kind: "other" };
374
- if (second) return {
375
- kind: "deployment",
376
- deployment: second
377
- };
378
- if (first && second === void 0) return {
379
- kind: "deployment",
380
- deployment: first
381
- };
382
- return { kind: "other" };
383
- };
384
- /**
385
- * The CLI prints this line with `logFinishedStep` after the push succeeds, to
386
- * stderr (convex 1.46 `cli/lib/deploy2.js`). The host is
387
- * `<deployment>.convex.cloud` or `<deployment>.<region>.convex.cloud`.
388
- */
389
- const DEPLOYED_LINE = /Deployed Convex functions to (https:\/\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud)\/?(?=\s|$)/m;
390
- /** Reads the deployment URL from `convex deploy` output. ANSI escape codes are removed first. */
391
- const parseDeployOutput = (text) => {
392
- const match = DEPLOYED_LINE.exec(stripVTControlCharacters(text));
393
- const url = match?.[1];
394
- const deploymentName = match?.[2];
395
- return url === void 0 || deploymentName === void 0 ? void 0 : {
396
- url,
397
- deploymentName
398
- };
399
- };
400
- /** The last non-empty lines of `text`, without escape codes and with every secret replaced. */
401
- 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");
402
- const valueOf$1 = (value) => Redacted.isRedacted(value) ? Redacted.value(value) : value;
403
- /**
404
- * The app directory: `rootDir` resolved against the working directory of the
405
- * deploy. It must have a package.json. There is no search above it.
406
- */
407
- const appDirectoryOf = (rootDir) => {
408
- const directory = resolve(rootDir);
409
- return Effect.tryPromise({
410
- try: () => access(join(directory, "package.json")).then(() => directory),
411
- catch: () => new ConvexAppNotFound({ rootDir: directory })
412
- });
413
- };
414
- /** `source` is not the functions directory that the Convex CLI pushes from the app directory. */
415
- var SourceMismatch = class extends Schema.TaggedError()("SourceMismatch", {
416
- /** The app directory, resolved. */
417
- rootDir: Schema.String,
418
- source: Schema.String,
419
- functions: Schema.String
420
- }) {
421
- get message() {
422
- return `Convex.Deployment source is ${this.source}, but the Convex CLI pushes ${this.functions} from the app directory ${this.rootDir} (the "functions" field of convex.json, or convex/ by default). Set rootDir to the directory of the Convex app, and source to its functions directory relative to rootDir, such as "./convex".`;
423
- }
424
- };
425
- const ConvexJson = Schema.Struct({ functions: Schema.optionalKey(Schema.String) });
426
- /** Fails with {@link SourceMismatch} when `path` is not the functions directory of the app in `cwd`. */
427
- const checkFunctionsDirectory = (cwd, path) => Effect.gen(function* () {
428
- const text = yield* Effect.promise(() => readFile(join(cwd, "convex.json"), "utf8").catch(() => void 0));
429
- const configured = text === void 0 ? void 0 : Schema.decodeUnknownSync(Schema.fromJsonString(ConvexJson))(text).functions;
430
- const functions = resolve(cwd, configured ?? "convex");
431
- if (functions !== path) return yield* new SourceMismatch({
432
- rootDir: cwd,
433
- source: path,
434
- functions
435
- });
436
- });
437
- const CliManifest = Schema.Struct({ bin: Schema.Union([Schema.String, Schema.Record(Schema.String, Schema.String)]) });
438
- /** `[node, <convex bin>]` for the `convex` package that `cwd` resolves. Never downloads. */
439
- const installedCli = (cwd) => Effect.tryPromise({
440
- try: async () => {
441
- const manifestPath = createRequire(join(cwd, "package.json")).resolve("convex/package.json");
442
- const { bin } = Schema.decodeUnknownSync(Schema.fromJsonString(CliManifest))(await readFile(manifestPath, "utf8"));
443
- const entry = typeof bin === "string" ? bin : bin["convex"];
444
- if (entry === void 0) throw new Error("the convex package has no convex bin");
445
- return [process.execPath, resolve(dirname(manifestPath), entry)];
446
- },
447
- catch: () => new ConvexCliNotFound({ cwd })
448
- });
449
- /**
450
- * The content identity of a push: the files of the app directory that git
451
- * does not ignore (or `path`, the manifests, and the `include` globs), the
452
- * nearest lockfile, the target deployment, and the `env` names and values.
453
- * A change in a workspace package outside the app directory is not seen.
454
- */
455
- const pushHash = (options) => Effect.gen(function* () {
456
- const source = options.source === void 0 ? void 0 : sourceOf(options.source);
457
- const files = source === void 0 ? void 0 : yield* appDirectoryOf(options.rootDir).pipe(Effect.flatMap((cwd) => {
458
- const functions = relative(cwd, resolve(cwd, source.path)).split("\\").join("/");
459
- return hashDirectory({
460
- cwd,
461
- memo: source.include !== void 0 ? {
462
- include: [
463
- `${functions}/**`,
464
- "package.json",
465
- "convex.json",
466
- ...source.include
467
- ],
468
- lockfile: true
469
- } : functions === ".." || functions.startsWith("../") ? {
470
- include: ["**/*", `${functions}/**`],
471
- lockfile: true
472
- } : {}
473
- });
474
- }));
475
- const env = Object.entries(options.env).map(([name, value]) => [name, valueOf$1(value)]).sort(([a], [b]) => a < b ? -1 : 1);
476
- return createHash("sha256").update(JSON.stringify({
477
- files,
478
- deployment: options.deployment,
479
- env
480
- })).digest("hex");
481
- });
482
- /**
483
- * Runs `use` with an env file that holds the key, in a new temporary
484
- * directory, and removes the directory afterwards: on success, on failure,
485
- * and on interruption.
486
- */
487
- 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({
488
- envFile: join(home, "deploy.env"),
489
- home
490
- }))), (home) => Effect.promise(() => rm(home, {
491
- recursive: true,
492
- force: true
493
- })));
494
- /** This process's variables without `CONVEX_*`, with `home` as the home directory. */
495
- const cliEnvironment = (home) => ({
496
- ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("CONVEX_"))),
497
- HOME: home,
498
- USERPROFILE: home,
499
- XDG_CONFIG_HOME: home
500
- });
501
- /**
502
- * A deploy key of `deployment` that expires after PUSH_KEY_LIFETIME_MS, with
503
- * only `allowedActions`, or every action when that is undefined. Delete it
504
- * with {@link deleteDeployKey} when the work is done.
505
- */
506
- const createDeployKey = (api, deployment, name, allowedActions) => Effect.gen(function* () {
507
- const now = yield* Clock.currentTimeMillis;
508
- const created = yield* api.request("create deploy key", (client) => client.POST("/deployments/{deployment_name}/create_deploy_key", {
509
- params: { path: { deployment_name: deployment } },
510
- body: {
511
- name,
512
- ...allowedActions === void 0 ? {} : { allowedActions: [...allowedActions] },
513
- expiresAt: now + PUSH_KEY_LIFETIME_MS
514
- }
515
- }));
516
- return Redacted.make(Schema.decodeUnknownSync(CreatedDeployKey)(created).deployKey);
517
- });
518
- /**
519
- * Deletes a key that {@link createDeployKey} made. The delete is skipped for
520
- * a key that is the OAuth credential. A failed delete is logged: the key
521
- * expires anyway.
522
- */
523
- const deleteDeployKey = (api, deployment, key) => Effect.gen(function* () {
524
- if (yield* api.isCredentialToken(Redacted.value(key))) return;
525
- yield* absentAsUndefined(retryIdempotentWrite(api.requestVoid("delete deploy key", (client) => client.POST("/deployments/{deployment_name}/delete_deploy_key", {
526
- params: { path: { deployment_name: deployment } },
527
- body: { id: Redacted.value(key) }
528
- }))));
529
- }).pipe(Effect.catch((error) => Effect.logWarning(`Convex.Deployment could not delete its key on ${deployment} (${error.message}). The key expires on its own within ${PUSH_KEY_LIFETIME_MS / 6e4} minutes.`)));
530
- /**
531
- * Creates a deploy key for one push, runs `use` with it, and deletes it
532
- * afterwards, also after a failure. `allowedActions` is PUSH_KEY_ACTIONS for
533
- * `convex deploy`, or undefined for a push command, which can do more than a
534
- * push, such as the uploads of convex-hosting through `convex run`.
535
- */
536
- const withPushKey = (api, deployment, allowedActions, use) => Effect.acquireUseRelease(createDeployKey(api, deployment, "alchemy-push", allowedActions), use, (key) => deleteDeployKey(api, deployment, key));
537
- /** Every variable of the deployment with its value. */
538
- 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));
539
- /** One call that sets variables, or removes those whose value is null. */
540
- const writeVariables = (url, key, changes) => settleVoid("update environment variables", () => createDeploymentClient(url, Redacted.value(key)).POST("/update_environment_variables", { body: { changes: changes.map(({ name, value }) => ({
541
- name,
542
- value
543
- })) } }));
544
- /**
545
- * The app directory and the program of a push. Fails before any change when
546
- * `rootDir` has no package.json, when `source` is not the functions directory
547
- * of the app, or when the app has no installed Convex CLI and no push command.
548
- */
549
- const checkSource = (rootDir, input) => Effect.gen(function* () {
550
- const source = sourceOf(input);
551
- const cwd = yield* appDirectoryOf(rootDir);
552
- yield* checkFunctionsDirectory(cwd, resolve(cwd, source.path));
553
- return {
554
- cwd,
555
- program: source.command ?? (yield* installedCli(cwd))
556
- };
557
- });
558
- /**
559
- * Sets the `env` variables and pushes the functions of `source` to one
560
- * deployment with `key`: a deploy key of the deployment, or a preview deploy
561
- * key of its project with `previewName`. Returns the URL that the CLI
562
- * printed, or undefined without `source`.
563
- */
564
- const push = (spawner, key, options) => Effect.gen(function* () {
565
- const source = options.source === void 0 ? void 0 : sourceOf(options.source);
566
- const resolved = options.source === void 0 ? void 0 : yield* checkSource(options.rootDir, options.source);
567
- const cwd = resolved?.cwd;
568
- const program = resolved?.program;
569
- const env = Object.entries(options.env).sort(([a], [b]) => a < b ? -1 : 1);
570
- const removed = options.previousEnvNames.filter((name) => !Object.hasOwn(options.env, name));
571
- const written = env.length === 0 && removed.length === 0 ? {
572
- current: {},
573
- changes: []
574
- } : yield* retryIdempotentWrite(Effect.gen(function* () {
575
- const current = yield* listVariables(options.url, key);
576
- if (!options.adopt) {
577
- const taken = env.filter(([name, value]) => !options.previousEnvNames.includes(name) && Object.hasOwn(current, name) && current[name] !== valueOf$1(value)).map(([name]) => name);
578
- if (taken.length > 0) return yield* new VariablesOwnedBySomeoneElse({
579
- deployment: options.deployment,
580
- names: taken
581
- });
582
- }
583
- const changes = [...env.filter(([name, value]) => current[name] !== valueOf$1(value)).map(([name, value]) => ({
584
- name,
585
- value: valueOf$1(value)
586
- })), ...removed.filter((name) => Object.hasOwn(current, name)).map((name) => ({
587
- name,
588
- value: null
589
- }))];
590
- if (changes.length > 0) yield* writeVariables(options.url, key, changes);
591
- return {
592
- current,
593
- changes
594
- };
595
- }));
596
- /**
597
- * After a failed push, the variables go back to their earlier values, so
598
- * that the deployment matches the hash in state. A failed rollback is
599
- * logged: the push error is the one to report.
600
- */
601
- const rollback = written.changes.length === 0 ? Effect.void : retryIdempotentWrite(writeVariables(options.url, key, written.changes.map(({ name }) => ({
602
- name,
603
- value: Object.hasOwn(written.current, name) ? written.current[name] ?? null : null
604
- })))).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.`)));
605
- if (source === void 0 || cwd === void 0 || program === void 0) return void 0;
606
- return yield* runPush(spawner, key, {
607
- ...options,
608
- cwd,
609
- program,
610
- command: source.command
611
- }).pipe(Effect.onError(() => rollback));
612
- });
613
- /** Runs the installed CLI or the push command once, and checks the deployment that it names. */
614
- const runPush = (spawner, key, options) => Effect.gen(function* () {
615
- const { cwd, program } = options;
616
- const secret = Redacted.value(key);
617
- const secrets = [secret, secret.slice(secret.indexOf("|") + 1)].filter((value) => value.length >= 8);
618
- const result = yield* withKeyFile(secret, ({ envFile, home }) => Effect.gen(function* () {
619
- const [command, ...programArgs] = program;
620
- const args = options.command === void 0 ? [
621
- ...programArgs,
622
- "deploy",
623
- ...options.previewName === void 0 ? [] : ["--preview-name", options.previewName],
624
- "--env-file",
625
- envFile
626
- ] : programArgs;
627
- const child = yield* spawner.spawn(ChildProcess.make(command, args, {
628
- cwd,
629
- env: {
630
- ...cliEnvironment(home),
631
- ...options.command === void 0 ? {} : { CONVEX_DEPLOY_KEY: secret }
632
- },
633
- extendEnv: false,
634
- stdin: "ignore",
635
- stdout: "pipe",
636
- stderr: "pipe"
637
- }));
638
- const [exitCode, stdout, stderr] = yield* Effect.all([
639
- child.exitCode,
640
- Stream.mkString(Stream.decodeText(child.stdout)),
641
- Stream.mkString(Stream.decodeText(child.stderr))
642
- ], { concurrency: "unbounded" });
643
- return {
644
- exitCode: Number(exitCode),
645
- output: `${stdout}\n${stderr}`
646
- };
647
- }).pipe(Effect.scoped)).pipe(Effect.mapError((error) => new CodePushFailed({
648
- exitCode: void 0,
649
- outputTail: outputTail(error.message, secrets)
650
- })));
651
- const deployed = parseDeployOutput(result.output);
652
- if (result.exitCode !== 0 || deployed === void 0) return yield* new CodePushFailed({
653
- exitCode: result.exitCode,
654
- outputTail: outputTail(result.output, secrets)
655
- });
656
- if (deployed.deploymentName !== options.deployment) return yield* new PushTargetMismatch({
657
- expected: options.deployment,
658
- actual: deployed.deploymentName
659
- });
660
- return deployed.url;
661
- });
662
- //#endregion
663
- //#region src/Deployment.ts
664
- /** The resource class. Stacks call {@link Deployment}, which writes `rootDir` into the props. */
665
- const DeploymentResource = Resource("Convex.Deployment", { defaultRemovalPolicy: "retain" });
666
- /** `rootDir` in POSIX form, without "./" and a trailing slash, such as `apps/web` or ".". */
667
- const normalizeRootDir = (rootDir) => {
668
- const normalized = posix.normalize(rootDir.replaceAll("\\", "/"));
669
- return normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
670
- };
671
- /** `rootDir` is an absolute path. The declaration stops with it as a defect, before the plan. */
672
- var AbsoluteRootDir = class extends Schema.TaggedError()("AbsoluteRootDir", { rootDir: Schema.String }) {
673
- get message() {
674
- return `Convex.Deployment rootDir ${this.rootDir} is an absolute path. State keeps rootDir for every machine that runs the stack, so set it relative to the directory where the deploy runs, such as "apps/web".`;
675
- }
676
- };
677
- /**
678
- * `Convex.Deployment("id", props)`. The props go to state with `rootDir` in
679
- * POSIX form, and with "." when it is not set, so that every row names the
680
- * app directory of its deployment. An absolute `rootDir` stops the
681
- * declaration, so it never reaches the plan or state. The check dies,
682
- * because the program of `Alchemy.Stack` can fail only with a ConfigError.
683
- * `win32.isAbsolute` also answers true for a POSIX absolute path. The other
684
- * fields keep it usable where Alchemy takes a resource class, such as
685
- * `Provider.collection`.
686
- */
687
- const Deployment = Object.assign((id, props) => {
688
- const rootDir = props.rootDir ?? ".";
689
- return win32.isAbsolute(rootDir) ? Effect.die(new AbsoluteRootDir({ rootDir })) : DeploymentResource(id, {
690
- ...props,
691
- rootDir: normalizeRootDir(rootDir)
692
- });
693
- }, {
694
- Type: DeploymentResource.Type,
695
- Props: DeploymentResource.Props,
696
- Self: DeploymentResource.Self,
697
- Provider: DeploymentResource.Provider,
698
- Aliases: DeploymentResource.Aliases,
699
- ref: (id, options) => DeploymentResource.ref(id, options)
700
- });
701
- /** The props name another deployment than the one in state. */
702
- var DeploymentIdentityChange = class extends Schema.TaggedError()("DeploymentIdentityChange", {
703
- /** Name of the deployment in state, such as `happy-animal-123`. */
704
- deployment: Schema.String,
705
- field: Schema.Literals([
706
- "projectId",
707
- "type",
708
- "name"
709
- ]),
710
- current: Schema.String,
711
- requested: Schema.String
712
- }) {
713
- get message() {
714
- 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.`;
715
- }
716
- };
717
- /** The project has no default production deployment for a `prod` resource to take over. */
718
- var ProductionDeploymentNotFound = class extends Schema.TaggedError()("ProductionDeploymentNotFound", { projectId: Schema.Number }) {
719
- get message() {
720
- 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.`;
721
- }
722
- };
723
- /** More than one deployment of the project has the name, so the lookup cannot choose one. */
724
- var AmbiguousDeployment = class extends Schema.TaggedError()("AmbiguousDeployment", {
725
- projectId: Schema.Number,
726
- type: Schema.Literals(["dev", "preview"]),
727
- name: Schema.String,
728
- deployments: Schema.Array(Schema.String)
729
- }) {
730
- get message() {
731
- 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.`;
732
- }
733
- };
734
- /**
735
- * The URL of a Convex cloud deployment: `https://<name>.convex.cloud`, or
736
- * `https://<name>.<region>.convex.cloud` outside US East.
737
- */
738
- const CLOUD_URL = /^https:\/\/[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud$/;
739
- /**
740
- * The HTTP actions URL of a deployment: the client URL with `.convex.site`
741
- * for `.convex.cloud`, as Convex sets CONVEX_SITE_URL. Custom domains are
742
- * not covered.
743
- */
744
- const siteUrlOf = (url) => url.replace(/\.convex\.cloud$/, ".convex.site");
745
- /** One row of list_deployments, GET /deployments/{name}, and create_deployment. Only the fields the provider uses. */
746
- const DeploymentRow$1 = Schema.Struct({
747
- name: Schema.String,
748
- projectId: Schema.Number,
749
- deploymentType: Schema.String,
750
- isDefault: Schema.Boolean,
751
- reference: Schema.String,
752
- /** Set in list_deployments and create_deployment rows only: GET /deployments/{name} answers null. */
753
- previewIdentifier: Schema.optionalKey(Schema.NullOr(Schema.String)),
754
- deploymentUrl: Schema.String.check(Schema.isPattern(CLOUD_URL)),
755
- /** Absent or null until the first push: list_deployments leaves it out for a new deployment. */
756
- lastDeployTime: Schema.optionalKey(Schema.NullOr(Schema.Number))
757
- });
758
- const DeploymentRows$1 = Schema.Array(DeploymentRow$1);
759
- /** GET https://<deployment>/api/v1/deployment_info, for a cloud deployment. Only the fields the provider uses. */
760
- const DeploymentInfo = Schema.Struct({
761
- kind: Schema.Literal("cloud"),
762
- projectId: Schema.Number,
763
- deploymentType: Schema.String,
764
- reference: Schema.optionalKey(Schema.NullOr(Schema.String))
765
- });
766
- /** GET https://<deployment>/api/v1/get_canonical_urls */
767
- const CanonicalUrls = Schema.Struct({
768
- convexCloudUrl: Schema.String,
769
- convexSiteUrl: Schema.String
770
- });
771
- const targetOf = (row, type, previewName) => ({
772
- projectId: row.projectId,
773
- type,
774
- name: row.name,
775
- reference: row.reference,
776
- previewName: type === "preview" ? previewName : void 0,
777
- url: row.deploymentUrl,
778
- siteUrl: siteUrlOf(row.deploymentUrl)
779
- });
780
- /** True when the deployment has received a push. */
781
- const isDeployed = (row) => row.lastDeployTime !== void 0 && row.lastDeployTime !== null;
782
- /** The `name` prop of a deployment: the dev reference or the preview name. */
783
- const nameOf = (target) => target.type === "dev" ? target.reference : target.type === "preview" ? target.previewName : void 0;
784
- /**
785
- * Fails with {@link DeploymentIdentityChange} when the props name another
786
- * deployment than `current`: the attributes in state, or a live deployment.
787
- */
788
- const checkIdentity = (current, news) => {
789
- const change = (field, from, to) => Effect.fail(new DeploymentIdentityChange({
790
- deployment: current.name,
791
- field,
792
- current: from,
793
- requested: to
794
- }));
795
- if (current.projectId !== news.project.projectId) return change("projectId", String(current.projectId), String(news.project.projectId));
796
- if (current.type !== news.type) return change("type", current.type, news.type);
797
- const currentName = nameOf(current);
798
- if (news.type !== "prod" && currentName !== news.name) return change("name", currentName ?? "(none)", news.name);
799
- return Effect.void;
800
- };
801
- const addressOf = (props) => ({
802
- projectId: props.project.projectId,
803
- type: props.type,
804
- name: props.type === "prod" ? void 0 : props.name
805
- });
806
- /** True when an update writes or removes variables: `env` has names, or state has names to remove. */
807
- const changesEnv = (news, output) => Object.keys(news.env ?? {}).length > 0 || envNamesOf(output).length > 0;
808
- /** The `env` names in state. State from 0.3 has none. */
809
- const envNamesOf = (output) => output?.envNames ?? [];
810
- /** The address of the deployment in state. */
811
- const addressInState = (output) => ({
812
- projectId: output.projectId,
813
- type: output.type,
814
- name: nameOf(output)
815
- });
816
- const ProjectSlugs = Schema.Struct({
817
- slug: Schema.String,
818
- teamSlug: Schema.String
819
- });
820
- /** True for an answer that says this credential cannot read the object. */
821
- const isRefused = (error) => error.status === 400 || error.status === 401 || error.status === 403 || error.status === 404;
822
- const DeploymentProvider = () => Provider.effect(DeploymentResource, Effect.gen(function* () {
823
- const api = yield* ManagementApi;
824
- const credentials = yield* Credentials;
825
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
826
- /** The live deployment, or undefined after a 404. GET /deployments/{name} answers no preview identifier. */
827
- 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)));
828
- const listDeployments = (management, projectId, type, isDefault) => management.request("list deployments", (client) => client.GET("/projects/{project_id}/list_deployments", { params: {
829
- path: { project_id: projectId },
830
- query: {
831
- deploymentType: type,
832
- ...isDefault === void 0 ? {} : { isDefault }
833
- }
834
- } })).pipe(Effect.map((body) => Schema.decodeUnknownSync(DeploymentRows$1)(body)));
835
- /** The row of the project's default production deployment, or undefined. */
836
- const productionRow = (management, projectId) => listDeployments(management, projectId, "prod", true).pipe(Effect.map((rows) => rows.find((candidate) => candidate.deploymentType === "prod" && candidate.isDefault)));
837
- /** The project's default production deployment, or undefined. */
838
- const production = (management, projectId) => productionRow(management, projectId).pipe(Effect.map((row) => row === void 0 ? void 0 : targetOf(row, "prod", void 0)));
839
- /**
840
- * The one dev deployment with this reference, or the one preview
841
- * deployment with this preview name, or undefined.
842
- */
843
- const findByName = (management, projectId, type, name) => Effect.gen(function* () {
844
- const matches = (yield* listDeployments(management, projectId, type)).filter((row) => row.deploymentType === type && (type === "dev" ? row.reference === name : row.previewIdentifier === name));
845
- if (matches.length > 1) return yield* new AmbiguousDeployment({
846
- projectId,
847
- type,
848
- name,
849
- deployments: matches.map((row) => row.name)
850
- });
851
- return matches[0];
852
- });
853
- /** The deployment that the address names, through the Management API, or undefined. */
854
- 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)));
855
- /** The deployment of a deploy key, when it is the deployment that the address names. */
856
- const deployKeyTarget = (key, deployment, address) => Effect.gen(function* () {
857
- const info = Schema.decodeUnknownSync(DeploymentInfo)(yield* api.deploymentRequest("get deployment info", deployment, key, (client) => client.GET("/deployment_info")));
858
- const reference = info.reference ?? "";
859
- if (info.projectId !== address.projectId || info.deploymentType !== address.type || address.type === "dev" && reference !== address.name) return;
860
- const urls = Schema.decodeUnknownSync(CanonicalUrls)(yield* api.deploymentRequest("get canonical urls", deployment, key, (client) => client.GET("/get_canonical_urls")));
861
- return {
862
- projectId: info.projectId,
863
- type: address.type,
864
- name: deployment,
865
- reference,
866
- previewName: void 0,
867
- url: urls.convexCloudUrl,
868
- siteUrl: urls.convexSiteUrl
869
- };
870
- }).pipe(Effect.catchIf(isRefused, () => Effect.succeed(void 0)));
871
- /** True when the preview deploy key belongs to the project. */
872
- const previewKeyFits = (management, slugs, projectId) => management.request("get project", (client) => client.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } })).pipe(Effect.map((body) => {
873
- const project = Schema.decodeUnknownSync(ProjectSlugs)(body);
874
- return project.slug === slugs.project && project.teamSlug === slugs.team;
875
- }), Effect.catchIf(isRefused, () => Effect.succeed(false)));
876
- /**
877
- * The narrowest credential that reaches the deployment. See {@link Access}.
878
- * When `changesEnv`, a preview deploy key gives way to the access token,
879
- * because it cannot change variables.
880
- */
881
- const accessFor = (address, changesEnv) => Effect.gen(function* () {
882
- const key = yield* credentials.deployKey;
883
- if (key !== void 0) {
884
- const target = keyTargetOf(Redacted.value(key));
885
- if (target.kind === "deployment" && address.type !== "preview") {
886
- const found = yield* deployKeyTarget(key, target.deployment, address);
887
- if (found !== void 0) return {
888
- kind: "deployKey",
889
- key,
890
- target: found
891
- };
892
- }
893
- if (target.kind === "preview" && address.type === "preview") {
894
- const scoped = api.withToken(key);
895
- if (!(changesEnv && (yield* credentials.hasAccessToken)) && (yield* previewKeyFits(scoped, target, address.projectId))) return {
896
- kind: "previewKey",
897
- key,
898
- api: scoped
899
- };
900
- }
901
- }
902
- return {
903
- kind: "token",
904
- api
905
- };
906
- });
907
- /** The live deployment of a resource in state, or undefined when it is gone. */
908
- const live = (access, output) => Effect.gen(function* () {
909
- if (access.kind === "deployKey") return access.target;
910
- if (output.type === "prod") return yield* absentAsUndefined(production(access.api, output.projectId));
911
- const row = yield* getByName(access.api, output.name);
912
- return row === void 0 ? void 0 : targetOf(row, output.type, output.previewName);
913
- });
914
- /** The content identity of `source` and `env` on a deployment, or undefined when there is nothing to push. */
915
- const wantedHash = (news, rootDir, deployment) => news.source === void 0 && Object.keys(news.env ?? {}).length === 0 ? Effect.succeed(void 0) : pushHash({
916
- rootDir,
917
- source: news.source,
918
- deployment,
919
- env: news.env ?? {}
920
- });
921
- return {
922
- stables: [
923
- "projectId",
924
- "type",
925
- "previewName"
926
- ],
927
- diff: Effect.fn(function* ({ news, olds, output, oldBindings, newBindings }) {
928
- if (!isResolved(news) || output === void 0) return void 0;
929
- yield* checkIdentity(output, news);
930
- if (!isResolved(newBindings)) return void 0;
931
- const rootDir = news.rootDir ?? ".";
932
- if (news.source !== void 0) yield* checkSource(rootDir, news.source);
933
- if (olds.rootDir !== rootDir) return { action: "update" };
934
- const access = yield* accessFor(addressOf(news), changesEnv(news, output));
935
- const current = yield* live(access, output);
936
- if (current?.name !== output.name) return { action: "update" };
937
- if ((yield* wantedHash(news, rootDir, output.name)) !== output.hash) return { action: "update" };
938
- if (diffBindings(oldBindings, newBindings).every((binding) => binding.action === "noop")) return { action: "noop" };
939
- return {
940
- action: "update",
941
- stables: [
942
- "hash",
943
- "envNames",
944
- ...[
945
- "projectId",
946
- "type",
947
- "previewName",
948
- "name",
949
- "reference",
950
- "url",
951
- "siteUrl"
952
- ].filter((key) => current[key] === output[key])
953
- ]
954
- };
955
- }),
956
- read: Effect.fn(function* ({ olds, output }) {
957
- if (output !== void 0) {
958
- const access = yield* accessFor(addressInState(output), false);
959
- const current = yield* live(access, output);
960
- return current === void 0 ? void 0 : {
961
- ...output,
962
- ...current
963
- };
964
- }
965
- const address = addressOf(olds);
966
- const access = yield* accessFor(address, false);
967
- if (access.kind === "deployKey") return Unowned({
968
- ...access.target,
969
- hash: void 0,
970
- envNames: []
971
- });
972
- if (olds.type === "prod") {
973
- const row = yield* productionRow(access.api, address.projectId);
974
- if (row === void 0) return void 0;
975
- const attributes = {
976
- ...targetOf(row, "prod", void 0),
977
- hash: void 0,
978
- envNames: []
979
- };
980
- return isDeployed(row) ? Unowned(attributes) : attributes;
981
- }
982
- const found = yield* lookup(access.api, address);
983
- if (found === void 0) return void 0;
984
- return Unowned({
985
- ...found,
986
- hash: void 0,
987
- envNames: []
988
- });
989
- }),
990
- reconcile: Effect.fn(function* ({ fqn, news, output }) {
991
- if (output !== void 0) yield* checkIdentity(output, news);
992
- const rootDir = news.rootDir ?? ".";
993
- if (news.source !== void 0) yield* checkSource(rootDir, news.source);
994
- const address = addressOf(news);
995
- const access = yield* accessFor(address, changesEnv(news, output));
996
- const env = news.env ?? {};
997
- if (access.kind === "previewKey" && changesEnv(news, output)) return yield* new EnvironmentNeedsDeployKey({ deployment: output?.name ?? address.name ?? "(new)" });
998
- const adopt = yield* shouldAdopt(fqn);
999
- const refuse = (name) => new OwnedBySomeoneElse({
1000
- 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"}.`,
1001
- resourceType: DeploymentResource.Type,
1002
- physicalName: name
1003
- });
1004
- const { target, created } = yield* Effect.gen(function* () {
1005
- if (access.kind === "deployKey") {
1006
- if (output === void 0 && !adopt) return yield* refuse(access.target.name);
1007
- return {
1008
- target: access.target,
1009
- created: false
1010
- };
1011
- }
1012
- if (news.type === "prod") {
1013
- const row = yield* productionRow(access.api, address.projectId);
1014
- if (row === void 0) return yield* new ProductionDeploymentNotFound({ projectId: address.projectId });
1015
- if (output === void 0 && isDeployed(row) && !adopt) return yield* refuse(row.name);
1016
- return {
1017
- target: targetOf(row, "prod", void 0),
1018
- created: false
1019
- };
1020
- }
1021
- if (output !== void 0) {
1022
- const row = yield* getByName(access.api, output.name);
1023
- if (row !== void 0) {
1024
- if (row.deploymentType !== output.type) return yield* new DeploymentIdentityChange({
1025
- deployment: row.name,
1026
- field: "type",
1027
- current: row.deploymentType,
1028
- requested: news.type
1029
- });
1030
- const current = targetOf(row, output.type, output.previewName);
1031
- yield* checkIdentity(current, news);
1032
- return {
1033
- target: current,
1034
- created: false
1035
- };
1036
- }
1037
- }
1038
- const existing = yield* lookup(access.api, address);
1039
- if (existing !== void 0) {
1040
- if (!adopt) return yield* refuse(existing.name);
1041
- return {
1042
- target: existing,
1043
- created: false
1044
- };
1045
- }
1046
- const row = yield* access.api.request("create deployment", (client) => client.POST("/projects/{project_id}/create_deployment", {
1047
- params: { path: { project_id: address.projectId } },
1048
- body: {
1049
- type: news.type,
1050
- reference: news.name
1051
- }
1052
- }));
1053
- return {
1054
- target: targetOf(Schema.decodeUnknownSync(DeploymentRow$1)(row), news.type, news.name),
1055
- created: true
1056
- };
1057
- });
1058
- const previousEnvNames = output?.name === target.name ? envNamesOf(output) : [];
1059
- const before = yield* wantedHash(news, rootDir, target.name);
1060
- if (before === void 0 && previousEnvNames.length === 0) return {
1061
- ...target,
1062
- hash: void 0,
1063
- envNames: []
1064
- };
1065
- if (output?.name === target.name && before === output.hash) return {
1066
- ...target,
1067
- hash: output.hash,
1068
- envNames: previousEnvNames
1069
- };
1070
- const options = {
1071
- deployment: target.name,
1072
- url: target.url,
1073
- rootDir,
1074
- source: news.source,
1075
- env,
1076
- previousEnvNames,
1077
- adopt
1078
- };
1079
- yield* (access.kind === "deployKey" ? push(spawner, access.key, {
1080
- ...options,
1081
- previewName: void 0
1082
- }) : access.kind === "previewKey" ? push(spawner, access.key, {
1083
- ...options,
1084
- previewName: target.previewName
1085
- }) : withPushKey(access.api, target.name, typeof news.source === "object" && news.source.command !== void 0 ? void 0 : PUSH_KEY_ACTIONS, (key) => push(spawner, key, {
1086
- ...options,
1087
- previewName: void 0
1088
- }))).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));
1089
- return {
1090
- ...target,
1091
- hash: yield* wantedHash(news, rootDir, target.name),
1092
- envNames: Object.keys(env).sort()
1093
- };
1094
- }),
1095
- delete: Effect.fn(function* ({ output }) {
1096
- 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.`);
1097
- if (output.type === "prod") return yield* keep;
1098
- const access = yield* accessFor(addressInState(output), false);
1099
- const management = access.kind === "deployKey" ? api : access.api;
1100
- const row = yield* getByName(management, output.name);
1101
- if (row === void 0) return;
1102
- if (row.deploymentType === "prod") return yield* keep;
1103
- yield* absentAsUndefined(retryIdempotentWrite(management.requestVoid("delete deployment", (client) => client.POST("/deployments/{deployment_name}/delete", { params: { path: { deployment_name: output.name } } }))));
1104
- })
1105
- };
1106
- }));
1107
- //#endregion
1108
- //#region src/Project.ts
1109
- const Project = Resource("Convex.Project", { defaultRemovalPolicy: "retain" });
1110
- /** The props change the team of an existing project. Convex cannot move a project. */
1111
- var ProjectTeamChange = class extends Schema.TaggedError()("ProjectTeamChange", {
1112
- projectId: Schema.Number,
1113
- teamId: Schema.Number,
1114
- requestedTeamId: Schema.Number
1115
- }) {
1116
- get message() {
1117
- 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.`;
1118
- }
1119
- };
1120
- /**
1121
- * The props change `deploymentType` or `deploymentRegion` of an existing
1122
- * project. Convex uses them only when it creates the project.
1123
- */
1124
- var ProjectDeploymentChange = class extends Schema.TaggedError()("ProjectDeploymentChange", {
1125
- projectId: Schema.Number,
1126
- field: Schema.Literals(["deploymentType", "deploymentRegion"]),
1127
- current: Schema.UndefinedOr(Schema.String),
1128
- requested: Schema.UndefinedOr(Schema.String)
1129
- }) {
1130
- get message() {
1131
- const show = (value) => value === void 0 ? "(not set)" : value;
1132
- 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.`;
1133
- }
1134
- };
1135
- /** More than one project in the team has the name, so the lookup cannot choose one. */
1136
- var AmbiguousProject = class extends Schema.TaggedError()("AmbiguousProject", {
1137
- teamId: Schema.Number,
1138
- name: Schema.String,
1139
- projectIds: Schema.Array(Schema.Number)
1140
- }) {
1141
- get message() {
1142
- 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.`;
1143
- }
1144
- };
1145
- /** Convex answered a create call but then did not return the project. */
1146
- var CreatedProjectNotFound = class extends Schema.TaggedError()("CreatedProjectNotFound", { projectId: Schema.Number }) {
1147
- get message() {
1148
- 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}.`;
1149
- }
1150
- };
1151
- /** No project in the team has the slug. A `slug` resource never creates one. */
1152
- var ProjectNotFound = class extends Schema.TaggedError()("ProjectNotFound", {
1153
- team: Schema.String,
1154
- slug: Schema.String
1155
- }) {
1156
- get message() {
1157
- 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.`;
1158
- }
1159
- };
1160
- /** The props change the slug of a `slug` resource: that is another project. */
1161
- var ProjectSlugChange = class extends Schema.TaggedError()("ProjectSlugChange", {
1162
- projectId: Schema.Number,
1163
- slug: Schema.String,
1164
- requestedSlug: Schema.String
1165
- }) {
1166
- get message() {
1167
- 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.`;
1168
- }
1169
- };
1170
- /** Fields of GET /projects/{id}, PATCH /projects/{id}, and GET /teams/{team}/projects rows that the provider uses. */
1171
- const ProjectDetails = Schema.Struct({
1172
- id: Schema.Number,
1173
- name: Schema.String,
1174
- slug: Schema.String,
1175
- teamId: Schema.Number,
1176
- teamSlug: Schema.String,
1177
- prodDeploymentName: Schema.NullOr(Schema.String)
1178
- });
1179
- /** GET /teams/{team_id}/projects: one page of projects. */
1180
- const ProjectPage = Schema.Struct({
1181
- items: Schema.Array(ProjectDetails),
1182
- pagination: Schema.Struct({
1183
- hasMore: Schema.Boolean,
1184
- nextCursor: Schema.optionalKey(Schema.NullOr(Schema.String))
1185
- })
1186
- });
1187
- /** POST /teams/{team_id}/create_project. Only the id: everything else comes from GET /projects/{id}. */
1188
- const CreatedProject = Schema.Struct({ projectId: Schema.Number });
1189
- const DeploymentRow = Schema.Struct({
1190
- name: Schema.String,
1191
- deploymentType: Schema.String,
1192
- isDefault: Schema.Boolean,
1193
- deploymentUrl: Schema.String,
1194
- /** Absent or null until the first push. */
1195
- lastDeployTime: Schema.optionalKey(Schema.NullOr(Schema.Number))
1196
- });
1197
- const DeploymentRows = Schema.Array(DeploymentRow);
1198
- /**
1199
- * Fails with {@link ProjectDeploymentChange} when a create-time prop
1200
- * changed. An omitted `deploymentType` means `prod`.
1201
- */
1202
- const checkCreateTimeProps = (projectId, olds, news) => {
1203
- const oldType = olds.deploymentType ?? "prod";
1204
- const newType = news.deploymentType ?? "prod";
1205
- if (oldType !== newType) return Effect.fail(new ProjectDeploymentChange({
1206
- projectId,
1207
- field: "deploymentType",
1208
- current: oldType,
1209
- requested: newType
1210
- }));
1211
- if (olds.deploymentRegion !== news.deploymentRegion) return Effect.fail(new ProjectDeploymentChange({
1212
- projectId,
1213
- field: "deploymentRegion",
1214
- current: olds.deploymentRegion,
1215
- requested: news.deploymentRegion
1216
- }));
1217
- return Effect.void;
1218
- };
1219
- /** True when `team`, a slug or a numeric id, names the team of the project. */
1220
- const isTeamOf = (details, team) => typeof team === "number" || /^\d+$/.test(team) ? details.teamId === Number(team) : details.teamSlug === team;
1221
- /** The row of the project's default production deployment, or undefined. */
1222
- const prodDeploymentOf = (api, 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)));
1223
- /** GET /projects/{id}, or undefined after a 404. */
1224
- const getProjectById = (api, 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)));
1225
- /**
1226
- * Every project in the team with exactly this name, in the order of the
1227
- * listing. Reads every page. `q` narrows the listing on the server, and the
1228
- * exact match here decides. Pattern from Confect
1229
- * `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
1230
- * (https://github.com/rjdellecese/confect, ISC license).
1231
- */
1232
- const listProjectsByName = (api, teamId, name) => Effect.gen(function* () {
1233
- const matches = [];
1234
- const seenCursors = /* @__PURE__ */ new Set();
1235
- let cursor;
1236
- while (true) {
1237
- const body = yield* api.request("list projects", (client) => client.GET("/teams/{team_id}/projects", { params: {
1238
- path: { team_id: teamId },
1239
- query: {
1240
- q: name,
1241
- ...cursor === void 0 ? {} : { cursor }
1242
- }
1243
- } }));
1244
- const page = Schema.decodeUnknownSync(ProjectPage)(body);
1245
- matches.push(...page.items.filter((row) => row.name === name));
1246
- if (!page.pagination.hasMore) break;
1247
- const next = page.pagination.nextCursor;
1248
- if (next === void 0 || next === null || seenCursors.has(next)) return yield* new ConvexApiError({
1249
- operation: "list projects",
1250
- status: 0,
1251
- code: "InvalidPagination",
1252
- 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}".`
1253
- });
1254
- seenCursors.add(next);
1255
- cursor = next;
1256
- }
1257
- return matches;
1258
- });
1259
- /** The one project in the team with exactly this name, or undefined. More than one is an error. */
1260
- const findProjectByName = (api, teamId, name) => listProjectsByName(api, teamId, name).pipe(Effect.flatMap((matches) => matches.length > 1 ? new AmbiguousProject({
1261
- teamId,
1262
- name,
1263
- projectIds: matches.map((row) => row.id)
1264
- }) : Effect.succeed(matches[0])));
1265
- /**
1266
- * POST /teams/{team_id}/create_project, then GET /projects/{id}. The identity
1267
- * comes from the create response, never from a name lookup.
1268
- */
1269
- const createProject = (api, teamId, props) => Effect.gen(function* () {
1270
- const created = yield* api.request("create project", (client) => client.POST("/teams/{team_id}/create_project", {
1271
- params: { path: { team_id: teamId } },
1272
- body: {
1273
- projectName: props.name,
1274
- deploymentType: props.deploymentType,
1275
- ...props.deploymentRegion === void 0 ? {} : { deploymentRegion: props.deploymentRegion }
1276
- }
1277
- }));
1278
- const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
1279
- const details = yield* getProjectById(api, projectId);
1280
- if (details === void 0) return yield* new CreatedProjectNotFound({ projectId });
1281
- return details;
1282
- });
1283
- const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
1284
- const api = yield* ManagementApi;
1285
- const attributesOf = (details) => prodDeploymentOf(api, details.id).pipe(Effect.map((prod) => ({
1286
- projectId: details.id,
1287
- slug: details.slug,
1288
- name: details.name,
1289
- teamId: details.teamId,
1290
- prodDeploymentName: prod?.name ?? details.prodDeploymentName ?? void 0,
1291
- prodDeploymentUrl: prod?.deploymentUrl
1292
- })));
1293
- const getById = (projectId) => getProjectById(api, projectId);
1294
- /** The project with this slug in the team, or undefined. Official endpoint; the team can be a slug. */
1295
- const getBySlug = (team, slug) => absentAsUndefined(api.request("get project by slug", (client) => client.GET("/teams/{team_id_or_slug}/projects/{project_slug}", { params: { path: {
1296
- team_id_or_slug: String(team),
1297
- project_slug: slug
1298
- } } }))).pipe(Effect.map((body) => body === void 0 ? void 0 : Schema.decodeUnknownSync(ProjectDetails)(body)));
1299
- const findByName = (teamId, name) => findProjectByName(api, teamId, name);
1300
- /**
1301
- * Brings an existing project to the desired name. The team cannot
1302
- * change, so a project in another team fails.
1303
- */
1304
- const converge = (details, teamId, name) => Effect.gen(function* () {
1305
- if (details.teamId !== teamId) return yield* new ProjectTeamChange({
1306
- projectId: details.id,
1307
- teamId: details.teamId,
1308
- requestedTeamId: teamId
1309
- });
1310
- if (details.name === name) return yield* attributesOf(details);
1311
- const updated = yield* api.request("update project", (client) => client.PATCH("/projects/{project_id}", {
1312
- params: { path: { project_id: details.id } },
1313
- body: { name }
1314
- }));
1315
- return yield* attributesOf(Schema.decodeUnknownSync(ProjectDetails)(updated));
1316
- });
1317
- return {
1318
- stables: [
1319
- "projectId",
1320
- "slug",
1321
- "teamId"
1322
- ],
1323
- diff: Effect.fn(function* ({ olds, news, output }) {
1324
- if (!isResolved(news)) return void 0;
1325
- if (output === void 0) return void 0;
1326
- if ("slug" in news) {
1327
- if (news.slug !== output.slug) return yield* new ProjectSlugChange({
1328
- projectId: output.projectId,
1329
- slug: output.slug,
1330
- requestedSlug: news.slug
1331
- });
1332
- if (olds.team !== news.team) {
1333
- const details = yield* getById(output.projectId);
1334
- if (details !== void 0 && !isTeamOf(details, news.team)) return yield* new ProjectTeamChange({
1335
- projectId: output.projectId,
1336
- teamId: output.teamId,
1337
- requestedTeamId: yield* api.resolveTeamId(news.team)
1338
- });
1339
- }
1340
- return { action: "noop" };
1341
- }
1342
- if (olds.team !== news.team) {
1343
- const requestedTeamId = yield* api.resolveTeamId(news.team);
1344
- if (requestedTeamId !== output.teamId) return yield* new ProjectTeamChange({
1345
- projectId: output.projectId,
1346
- teamId: output.teamId,
1347
- requestedTeamId
1348
- });
1349
- }
1350
- return "name" in olds && olds.name === news.name ? void 0 : { action: "update" };
1351
- }),
1352
- read: Effect.fn(function* ({ olds, output }) {
1353
- if (output !== void 0) {
1354
- const details = yield* getById(output.projectId);
1355
- return details === void 0 ? void 0 : yield* attributesOf(details);
1356
- }
1357
- const existing = "slug" in olds ? yield* getBySlug(olds.team, olds.slug) : yield* findByName(yield* api.resolveTeamId(olds.team), olds.name);
1358
- return existing === void 0 ? void 0 : Unowned(yield* attributesOf(existing));
1359
- }),
1360
- reconcile: Effect.fn(function* ({ fqn, olds, news, output }) {
1361
- if ("slug" in news) {
1362
- const details = output === void 0 ? yield* getBySlug(news.team, news.slug) : yield* getById(output.projectId);
1363
- if (details === void 0) return yield* new ProjectNotFound({
1364
- team: String(news.team),
1365
- slug: news.slug
1366
- });
1367
- if (details.slug !== news.slug) return yield* new ProjectSlugChange({
1368
- projectId: details.id,
1369
- slug: details.slug,
1370
- requestedSlug: news.slug
1371
- });
1372
- if (!isTeamOf(details, news.team)) return yield* new ProjectTeamChange({
1373
- projectId: details.id,
1374
- teamId: details.teamId,
1375
- requestedTeamId: yield* api.resolveTeamId(news.team)
1376
- });
1377
- if (output === void 0 && !(yield* shouldAdopt(fqn))) return yield* new OwnedBySomeoneElse({
1378
- message: `Convex project ${details.id} ("${news.slug}") exists and is not in this stack's state. Re-run with --adopt to take it over.`,
1379
- resourceType: Project.Type,
1380
- physicalName: news.slug
1381
- });
1382
- return yield* attributesOf(details);
1383
- }
1384
- const teamId = yield* api.resolveTeamId(news.team);
1385
- if (output !== void 0) {
1386
- if (olds !== void 0 && "name" in olds) yield* checkCreateTimeProps(output.projectId, olds, news);
1387
- if (output.teamId !== teamId) return yield* new ProjectTeamChange({
1388
- projectId: output.projectId,
1389
- teamId: output.teamId,
1390
- requestedTeamId: teamId
1391
- });
1392
- const details = yield* getById(output.projectId);
1393
- if (details !== void 0) return yield* converge(details, teamId, news.name);
1394
- } else {
1395
- const existing = yield* findByName(teamId, news.name);
1396
- if (existing !== void 0) {
1397
- if (!(yield* shouldAdopt(fqn))) return yield* new OwnedBySomeoneElse({
1398
- 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.`,
1399
- resourceType: Project.Type,
1400
- physicalName: news.name
1401
- });
1402
- return yield* converge(existing, teamId, news.name);
1403
- }
1404
- }
1405
- const details = yield* createProject(api, teamId, {
1406
- name: news.name,
1407
- deploymentType: news.deploymentType ?? "prod",
1408
- deploymentRegion: news.deploymentRegion
1409
- });
1410
- return yield* converge(details, teamId, news.name);
1411
- }),
1412
- delete: Effect.fn(function* ({ output }) {
1413
- yield* absentAsUndefined(api.requestVoid("delete project", (client) => client.POST("/projects/{project_id}/delete", { params: { path: { project_id: output.projectId } } })));
1414
- })
1415
- };
1416
- }));
1417
- //#endregion
1418
33
  //#region src/Providers.ts
1419
34
  var Providers = class extends Provider.ProviderCollection()("Convex") {};
1420
35
  /**
@@ -1422,426 +37,12 @@ var Providers = class extends Provider.ProviderCollection()("Convex") {};
1422
37
  * default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login, and
1423
38
  * `CONVEX_DEPLOY_KEY`).
1424
39
  */
1425
- const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([Project, DeploymentResource])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.orDie);
1426
- //#endregion
1427
- //#region src/RunRecord.ts
1428
- /** The key of the record in the stage output document. */
1429
- const RUN_RECORD_KEY = "samebase.run";
1430
- /** The name of the stack file. Alchemy resolves `--config` against the working directory and does not expose it to a state layer, so the record assumes the default name. */
1431
- const STACK_FILE_NAME = "alchemy.run.ts";
1432
- var RunRecordError = class extends Schema.TaggedError()("RunRecordError", { reason: Schema.String }) {
1433
- get message() {
1434
- return `The run record of the stage output cannot be made: ${this.reason}`;
1435
- }
1436
- };
1437
- /** The part of the GitHub Actions event payload that the record reads. A push event has no `pull_request`. */
1438
- const EventPayload = Schema.Struct({ pull_request: Schema.optionalKey(Schema.Struct({
1439
- number: Schema.Number,
1440
- head: Schema.Struct({ sha: Schema.String })
1441
- })) });
1442
- /** `owner/name` of an https, ssh, or scp-like git URL with a host. A local path is not one. */
1443
- const REMOTE_URL = /^(?:[a-z+]+:\/\/[^/]+\/|[^/@:]+@[^:/]+:)([^/]+)\/([^/]+?)(?:\.git)?\/?$/;
1444
- const execFileAsync = promisify(execFile);
1445
- const now = Clock.currentTimeMillis.pipe(Effect.map((millis) => new Date(millis).toISOString()));
1446
- const git = (cwd, args) => Effect.tryPromise({
1447
- try: async () => (await execFileAsync("git", [
1448
- "-C",
1449
- cwd,
1450
- ...args
1451
- ], { encoding: "utf8" })).stdout.trim(),
1452
- catch: (error) => new RunRecordError({ reason: `git ${args.join(" ")} failed in ${cwd} (${error instanceof Error ? error.message.split("\n")[0] : String(error)})` })
1453
- });
1454
- /** The stack file from the repository root: the working directory below the git top level, plus {@link STACK_FILE_NAME}. */
1455
- const stackFileOf = (cwd) => Effect.gen(function* () {
1456
- const top = yield* git(cwd, ["rev-parse", "--show-toplevel"]);
1457
- const [realTop, realCwd] = yield* Effect.tryPromise({
1458
- try: () => Promise.all([realpath(top), realpath(cwd)]),
1459
- catch: () => new RunRecordError({ reason: `${cwd} is gone` })
1460
- });
1461
- return [...relative(realTop, realCwd).split(sep).filter((part) => part !== ""), STACK_FILE_NAME].join("/");
1462
- });
1463
- const variable = (env, name) => Effect.suspend(() => {
1464
- const value = env[name];
1465
- return value === void 0 || value === "" ? Effect.fail(new RunRecordError({ reason: `GITHUB_ACTIONS is true, and ${name} is not set` })) : Effect.succeed(value);
1466
- });
1467
- const fromGitHubActions = (where) => Effect.gen(function* () {
1468
- const { env } = where;
1469
- const repository = yield* variable(env, "GITHUB_REPOSITORY");
1470
- const refName = yield* variable(env, "GITHUB_REF_NAME");
1471
- const sha = yield* variable(env, "GITHUB_SHA");
1472
- const serverUrl = yield* variable(env, "GITHUB_SERVER_URL");
1473
- const runId = yield* variable(env, "GITHUB_RUN_ID");
1474
- const runAttempt = yield* variable(env, "GITHUB_RUN_ATTEMPT");
1475
- const eventPath = env["GITHUB_EVENT_PATH"];
1476
- const pullRequest = (eventPath === void 0 || eventPath === "" ? void 0 : yield* Effect.tryPromise({
1477
- try: () => readFile(eventPath, "utf8"),
1478
- catch: () => new RunRecordError({ reason: `GITHUB_EVENT_PATH ${eventPath} cannot be read` })
1479
- }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(EventPayload))), Effect.mapError((error) => error instanceof RunRecordError ? error : new RunRecordError({ reason: `GITHUB_EVENT_PATH ${eventPath} is not an event payload` }))))?.pull_request;
1480
- const headCommit = pullRequest?.head.sha ?? sha;
1481
- const headRef = env["GITHUB_HEAD_REF"];
1482
- const mergeRef = /^refs\/pull\/(\d+)\/merge$/.exec(env["GITHUB_REF"] ?? "");
1483
- return {
1484
- version: 1,
1485
- repository,
1486
- stackFile: yield* stackFileOf(where.cwd),
1487
- branch: headRef === void 0 || headRef === "" ? refName : headRef,
1488
- pullRequest: pullRequest?.number ?? (mergeRef === null ? null : Number(mergeRef[1])),
1489
- headCommit,
1490
- runUrl: `${serverUrl}/${repository}/actions/runs/${runId}/attempts/${runAttempt}`,
1491
- source: "github-actions",
1492
- recordedAt: yield* now
1493
- };
1494
- });
1495
- const fromGit = (where) => Effect.gen(function* () {
1496
- const { cwd } = where;
1497
- const origin = yield* git(cwd, [
1498
- "remote",
1499
- "get-url",
1500
- "origin"
1501
- ]);
1502
- const match = REMOTE_URL.exec(origin);
1503
- if (match === null) return yield* new RunRecordError({ reason: `the origin remote of ${cwd} is not a git URL with a host and owner/name: ${origin}` });
1504
- const branch = yield* git(cwd, [
1505
- "rev-parse",
1506
- "--abbrev-ref",
1507
- "HEAD"
1508
- ]);
1509
- if (branch === "HEAD") return yield* new RunRecordError({ reason: `the checkout at ${cwd} is on no branch (detached HEAD)` });
1510
- return {
1511
- version: 1,
1512
- repository: `${match[1]}/${match[2]}`,
1513
- stackFile: yield* stackFileOf(cwd),
1514
- branch,
1515
- pullRequest: null,
1516
- headCommit: yield* git(cwd, ["rev-parse", "HEAD"]),
1517
- runUrl: null,
1518
- source: "laptop",
1519
- recordedAt: yield* now
1520
- };
1521
- });
1522
- /**
1523
- * The record of this run: from the GitHub Actions variables and the event
1524
- * payload when GITHUB_ACTIONS is `true`, else from git in `cwd`, which needs
1525
- * a branch and an `origin` remote with `owner/name`. Fails when a value is
1526
- * missing; nothing is guessed.
1527
- */
1528
- const runRecord = (where) => where.env["GITHUB_ACTIONS"] === "true" ? fromGitHubActions(where) : fromGit(where);
1529
- /** A plain object, which a spread keeps whole. A Date, a Duration, or a Redacted value is an object too, and a spread loses it. */
1530
- const isPlainObject = (value) => {
1531
- if (typeof value !== "object" || value === null) return false;
1532
- const prototype = Object.getPrototypeOf(value);
1533
- return prototype === Object.prototype || prototype === null;
1534
- };
1535
- /**
1536
- * The output document of a stage: the stack's output plus the record under
1537
- * {@link RUN_RECORD_KEY}, or undefined for an output with no place for the
1538
- * key: a string, an array, null, or an object that is not plain, such as a
1539
- * Date. A value that the stack itself put under the key stays.
1540
- */
1541
- const withRunRecord = (output, record) => isPlainObject(output) ? "samebase.run" in output ? output : {
1542
- ...output,
1543
- [RUN_RECORD_KEY]: record
1544
- } : void 0;
1545
- //#endregion
1546
- //#region src/State.ts
1547
- /** The name of the state project in each team. */
1548
- const STORE_PROJECT_NAME = "samebase";
1549
- /**
1550
- * The actions of the key of one run: the internal queries and mutations of
1551
- * the store, nothing else. Measured: `deployment:data:view` and
1552
- * `deployment:data:write` do not run an internal function.
1553
- */
1554
- const RUN_KEY_ACTIONS = ["deployment:functions:runInternalQueries", "deployment:functions:runInternalMutations"];
1555
- /** The body of POST /api/function, with HTTP 200 for a function error too. */
1556
- const Reply = Schema.Union([Schema.Struct({
1557
- status: Schema.Literal("success"),
1558
- value: Schema.Unknown
1559
- }), Schema.Struct({
1560
- status: Schema.Literal("error"),
1561
- errorMessage: Schema.String
1562
- })]);
1563
- /** The error reply of /api/function for a function that the deployment does not have (recorded). */
1564
- const MISSING_FUNCTION = /Could not find function for '/;
1565
- /** HTTP 401 or 403 from the function API: the key is gone or expired (recorded: 403 without a key). */
1566
- const isUnauthorized = (error) => error.http?.status === 401 || error.http?.status === 403;
1567
- const failure = (message) => new StateStoreError({ message: `Convex state: ${message}` });
1568
- /** No reply, HTTP 429, or HTTP 5xx: a new attempt can succeed. Every call of the store is idempotent. */
1569
- const isTransient = (error) => error.http !== void 0 && (error.http.status === void 0 || error.http.status === 429 || error.http.status >= 500);
1570
- const post = (url, headers, body) => Effect.tryPromise({
1571
- try: () => fetch(url, {
1572
- method: "POST",
1573
- headers: {
1574
- "Content-Type": "application/json",
1575
- ...headers
1576
- },
1577
- body: JSON.stringify(body)
1578
- }),
1579
- catch: () => new StateStoreError({
1580
- message: `Convex state: POST ${url} failed`,
1581
- http: {}
1582
- })
1583
- }).pipe(Effect.flatMap((response) => response.ok || response.status === 560 ? Effect.tryPromise({
1584
- try: () => response.json(),
1585
- catch: () => failure(`${url} gave no JSON`)
1586
- }) : Effect.fail(new StateStoreError({
1587
- message: `Convex state: POST ${url} failed with HTTP ${response.status}`,
1588
- http: { status: response.status }
1589
- }))), Effect.retry({
1590
- while: isTransient,
1591
- schedule: Schedule.exponential("100 millis").pipe(Schedule.jittered),
1592
- times: 5
1593
- }));
1594
- const decode = (schema, what) => (value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => failure(`bad ${what}`)));
1595
- const makeFunctionApi = (url, key) => ({ call: (path, args) => post(`${url}/api/function`, { Authorization: `Convex ${Redacted.value(key)}` }, {
1596
- path,
1597
- args,
1598
- format: "json"
1599
- }).pipe(Effect.flatMap(decode(Reply, `reply of ${path}`))) });
1600
- /** The value of a reply, or the Convex message, which names the function and the cause, such as a missing key action. */
1601
- const valueOf = (reply) => reply.status === "success" ? Effect.succeed(reply.value) : Effect.fail(failure(reply.errorMessage.trim()));
1602
- /** The functions of the store with Alchemy's encoding: the JSON text of encodeState. */
1603
- const remoteOf = (api, record) => {
1604
- const run = (name, args) => api.call(`state:${name}`, args).pipe(Effect.flatMap(valueOf));
1605
- const write = (name, args) => Effect.gen(function* () {
1606
- const currentRun = record === void 0 ? void 0 : {
1607
- ...record,
1608
- recordedAt: new Date(yield* Clock.currentTimeMillis).toISOString()
1609
- };
1610
- return yield* run(name, {
1611
- ...args,
1612
- run: currentRun
1613
- });
1614
- });
1615
- const strings = (name, args) => run(name, args).pipe(Effect.flatMap(decode(Schema.Array(Schema.String), name)));
1616
- const optional = (name, args) => run(name, args).pipe(Effect.flatMap(decode(Schema.NullOr(Schema.String), name)));
1617
- const encode = (value) => JSON.stringify(encodeState(value));
1618
- return {
1619
- listStacks: () => strings("stacks", {}),
1620
- listStages: (stack) => strings("stages", { stack }),
1621
- list: (r) => strings("list", {
1622
- stack: r.stack,
1623
- stage: r.stage
1624
- }),
1625
- get: (r) => optional("get", {
1626
- stack: r.stack,
1627
- stage: r.stage,
1628
- fqn: r.fqn
1629
- }).pipe(Effect.map((text) => text === null ? void 0 : JSON.parse(text, reviveState))),
1630
- getReplacedResources: (r) => strings("replaced", {
1631
- stack: r.stack,
1632
- stage: r.stage
1633
- }).pipe(Effect.map((texts) => texts.map((text) => JSON.parse(text, reviveState)))),
1634
- getOutput: (r) => optional("getOutput", {
1635
- stack: r.stack,
1636
- stage: r.stage
1637
- }).pipe(Effect.map((text) => text === null ? void 0 : JSON.parse(text, reviveState))),
1638
- set: (r) => write("set", {
1639
- stack: r.stack,
1640
- stage: r.stage,
1641
- fqn: r.fqn,
1642
- value: encode(r.value)
1643
- }),
1644
- delete: (r) => write("remove", {
1645
- stack: r.stack,
1646
- stage: r.stage,
1647
- fqn: r.fqn
1648
- }),
1649
- deleteStack: (r) => run("removeStack", {
1650
- stack: r.stack,
1651
- stage: r.stage
1652
- }),
1653
- setOutput: (r) => write("setOutput", {
1654
- stack: r.stack,
1655
- stage: r.stage,
1656
- value: encode(r.value)
1657
- })
1658
- };
1659
- };
1660
- /** The version that the store answers, or undefined when the deployment has no `state:version`. */
1661
- const storeVersion = (api) => api.call("state:version", {}).pipe(Effect.flatMap((reply) => reply.status === "error" && MISSING_FUNCTION.test(reply.errorMessage) ? Effect.succeed(void 0) : valueOf(reply).pipe(Effect.flatMap(decode(Schema.Number, "store version")))));
1662
- /**
1663
- * The app that serves the store. A store without `state:host` is a seed: it
1664
- * was pushed before 0.14.0, and only the store app of this package served
1665
- * those stores.
1666
- */
1667
- const storeHost = (api) => api.call("state:host", {}).pipe(Effect.flatMap((reply) => reply.status === "error" && MISSING_FUNCTION.test(reply.errorMessage) ? Effect.succeed("seed") : valueOf(reply).pipe(Effect.flatMap(decode(Schema.Literals(["seed", "base"]), "store host")))));
1668
- /**
1669
- * The base app of the team serves an older store. Convex.state() never
1670
- * pushes its store app over a base, because the push replaces every function
1671
- * of the deployment and so erases the base app.
1672
- */
1673
- var OutdatedBase = class extends Schema.TaggedError()("OutdatedBase", {
1674
- team: Schema.Union([Schema.String, Schema.Number]),
1675
- deployment: Schema.String,
1676
- version: Schema.Number
1677
- }) {
1678
- get message() {
1679
- return `The base of team ${this.team} (${this.deployment}) answers store version ${this.version}, and this package needs store version 4. Convex.state does not push over a base. Upgrade the dependency @samebase/alchemy-convex of the base app to the version of this package, and push the base app.`;
1680
- }
1681
- };
1682
- /**
1683
- * The functions directory of the store app, `src/store/convex` of this
1684
- * package. This file is `src/State.ts` in a checkout and a chunk in `lib/`
1685
- * in the installed package, which ships `src` too, so the directory is
1686
- * `../src/store/convex` from either place. Its `convex.config.ts` mounts the
1687
- * alchemyState component of `src/store/component`, inside the same push root.
1688
- */
1689
- const storeDirectory = () => join(dirname(fileURLToPath(import.meta.url)), "..", "src", "store", "convex");
1690
- /**
1691
- * Pushes `store` to the production deployment of the state project with the
1692
- * installed Convex CLI: no typecheck, no codegen, the key only in the
1693
- * environment of the CLI.
1694
- */
1695
- const pushStore = (options, deployment) => Effect.gen(function* () {
1696
- const cli = yield* installedCli(join(options.store, ".."));
1697
- yield* Effect.logInfo(`Convex state: pushing the store version 4 to ${deployment.name}`);
1698
- yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) => push(options.spawner, key, {
1699
- deployment: deployment.name,
1700
- url: deployment.deploymentUrl,
1701
- rootDir: join(options.store, ".."),
1702
- source: {
1703
- path: options.store,
1704
- command: [
1705
- ...cli,
1706
- "deploy",
1707
- "--typecheck",
1708
- "disable",
1709
- "--codegen",
1710
- "disable"
1711
- ]
1712
- },
1713
- env: {},
1714
- previousEnvNames: [],
1715
- adopt: false,
1716
- previewName: void 0
1717
- }));
1718
- });
1719
- /**
1720
- * Creates the state project of the team. Two first runs can both create one:
1721
- * the run whose project is not the oldest deletes its own, which holds
1722
- * nothing yet, and uses the oldest, so that every run ends on one store.
1723
- */
1724
- const createStoreProject = (api, teamId, team) => Effect.gen(function* () {
1725
- yield* Effect.logInfo(`Convex state: creating the project ${STORE_PROJECT_NAME} in team ${team}`);
1726
- const created = yield* createProject(api, teamId, {
1727
- name: STORE_PROJECT_NAME,
1728
- deploymentType: "prod",
1729
- deploymentRegion: void 0
1730
- });
1731
- const oldest = (yield* listProjectsByName(api, teamId, STORE_PROJECT_NAME)).reduce((a, b) => b.id < a.id ? b : a, created);
1732
- if (oldest.id === created.id) return created;
1733
- yield* Effect.logInfo(`Convex state: another run created ${STORE_PROJECT_NAME} ${oldest.id} first; project ${created.id} is deleted again.`);
1734
- yield* absentAsUndefined(retryIdempotentWrite(api.requestVoid("delete project", (client) => client.POST("/projects/{project_id}/delete", { params: { path: { project_id: created.id } } }))));
1735
- return oldest;
1736
- });
1737
- /**
1738
- * The StateService over the `samebase` project of the team: found or
1739
- * created, its store app pushed when the store is missing or an older seed,
1740
- * and read and written with the keys of this run.
1741
- */
1742
- const makeConvexState = (options) => Effect.gen(function* () {
1743
- const { api } = options;
1744
- const teamId = yield* api.resolveTeamId(options.team);
1745
- const team = options.team ?? teamId;
1746
- const project = (yield* findProjectByName(api, teamId, "samebase")) ?? (yield* createStoreProject(api, teamId, team));
1747
- const deployment = yield* prodDeploymentOf(api, project.id);
1748
- if (deployment === void 0) return yield* new ProductionDeploymentNotFound({ projectId: project.id });
1749
- const keys = [];
1750
- yield* Scope.addFinalizer(options.scope, Effect.suspend(() => Effect.forEach(keys, (key) => deleteDeployKey(api, deployment.name, key), { discard: true })));
1751
- const mint = createDeployKey(api, deployment.name, STORE_PROJECT_NAME, RUN_KEY_ACTIONS).pipe(Effect.map((key) => {
1752
- keys.push(key);
1753
- return makeFunctionApi(deployment.deploymentUrl, key);
1754
- }), Effect.mapError((error) => failure(`no key for ${deployment.name}: ${error.message}`)));
1755
- let current = yield* mint;
1756
- const functions = { call: (path, args) => current.call(path, args).pipe(Effect.catchIf(isUnauthorized, () => mint.pipe(Effect.flatMap((next) => {
1757
- current = next;
1758
- return next.call(path, args);
1759
- })))) };
1760
- const needsPush = (found) => found === void 0 ? Effect.succeed(true) : found < 4 ? storeHost(functions).pipe(Effect.map((host) => host === "seed")) : Effect.succeed(false);
1761
- let version = yield* storeVersion(functions);
1762
- if (yield* needsPush(version)) {
1763
- if (version === void 0 && deployment.lastDeployTime) return yield* failure(`project ${STORE_PROJECT_NAME} (${project.id}) of team ${team} has code that is not the state store, and Convex.state does not push over it. Rename that project in the Convex dashboard.`);
1764
- version = yield* storeVersion(functions);
1765
- if (yield* needsPush(version)) {
1766
- yield* pushStore(options, deployment);
1767
- version = yield* storeVersion(functions);
1768
- }
1769
- }
1770
- if (version !== void 0 && version < 4 && (yield* storeHost(functions)) === "base") return yield* new OutdatedBase({
1771
- team,
1772
- deployment: deployment.name,
1773
- version
1774
- });
1775
- if (version !== 4) return yield* failure(`${deployment.name} answers store version ${version ?? "none"}, and this package needs store version 4.`);
1776
- const remote = remoteOf(functions, options.run);
1777
- /** The stack's output plus the record, when the run has one and the output has a place for it. */
1778
- const documentOf = (request) => Effect.gen(function* () {
1779
- const { run } = options;
1780
- if (run === void 0) return request.value;
1781
- const recordedAt = new Date(yield* Clock.currentTimeMillis).toISOString();
1782
- const document = withRunRecord(request.value, {
1783
- ...run,
1784
- recordedAt
1785
- });
1786
- if (document === void 0) {
1787
- yield* Effect.logWarning(`Convex state: the output of ${request.stack}/${request.stage} is not a plain object, so it carries no run record.`);
1788
- return request.value;
1789
- }
1790
- return document;
1791
- });
1792
- return {
1793
- id: "convex",
1794
- getVersion: () => Effect.succeed(STATE_STORE_VERSION),
1795
- listStacks: remote.listStacks,
1796
- listStages: remote.listStages,
1797
- list: remote.list,
1798
- get: remote.get,
1799
- getReplacedResources: remote.getReplacedResources,
1800
- getOutput: remote.getOutput,
1801
- set: (request) => remote.set(request).pipe(Effect.as(request.value)),
1802
- delete: (request) => remote.delete(request).pipe(Effect.asVoid),
1803
- deleteStack: (request) => remote.deleteStack(request).pipe(Effect.asVoid),
1804
- setOutput: (request) => Effect.gen(function* () {
1805
- const value = yield* documentOf(request);
1806
- yield* remote.setOutput({
1807
- stack: request.stack,
1808
- stage: request.stage,
1809
- value
1810
- });
1811
- return value;
1812
- })
1813
- };
1814
- });
1815
- /**
1816
- * Alchemy state in the `samebase` project of `team`, with the team token
1817
- * in CONVEX_ACCESS_TOKEN or the Convex CLI login. A team access token names
1818
- * its team, so `team` is optional with one; the Convex CLI login needs the
1819
- * slug or the id. The first use of the run finds or creates the project and
1820
- * pushes the store app when the store is missing or an older seed. An older
1821
- * base fails the run with {@link OutdatedBase}. `alchemy plan` on another
1822
- * stack never touches Convex.
1823
- */
1824
- const state = (options = {}) => Layer.effect(State, Effect.gen(function* () {
1825
- const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1826
- const scope = yield* Scope.Scope;
1827
- const context = yield* Effect.context();
1828
- return yield* Effect.cached(Effect.gen(function* () {
1829
- const api = yield* ManagementApi.pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));
1830
- const run = yield* runRecord({
1831
- cwd: process.cwd(),
1832
- env: process.env
1833
- }).pipe(Effect.catch((error) => Effect.logWarning(`Convex state: the stage outputs of this run carry no record. ${error.message}`).pipe(Effect.as(void 0))));
1834
- return yield* makeConvexState({
1835
- team: options.team,
1836
- api,
1837
- spawner,
1838
- scope,
1839
- store: storeDirectory(),
1840
- run
1841
- });
1842
- }).pipe(Effect.provideContext(context), Effect.orDie));
1843
- }));
40
+ const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([
41
+ Project,
42
+ DeploymentResource,
43
+ HubClient
44
+ ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider(), HubClientProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.orDie);
1844
45
  //#endregion
1845
- export { ACCESS_TOKEN_ENV, AbsoluteRootDir, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, OutdatedBase, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, RUN_RECORD_KEY, RunRecordError, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, bindComponent, fromConfig, fromValues, parseDeployOutput, providers, runRecord, state, withRunRecord };
46
+ export { ACCESS_TOKEN_ENV, AbsoluteRootDir, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, HUB_PROJECT_NAME, HubClient, HubClientFailed, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, RUN_RECORD_KEY, RunRecordError, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, bindComponent, fromConfig, fromValues, parseDeployOutput, providers, runRecord, state, withRunRecord };
1846
47
 
1847
48
  //# sourceMappingURL=index.js.map