@samebase/alchemy-convex 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/lib/HubClient-C_nvjlvW.js +1938 -0
- package/lib/HubClient-C_nvjlvW.js.map +1 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +46 -0
- package/lib/cli.js.map +1 -0
- package/lib/index.d.ts +38 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -1806
- package/lib/index.js.map +1 -1
- package/package.json +4 -1
- package/src/HubClient.ts +258 -0
- package/src/Providers.ts +3 -2
- package/src/cli.ts +62 -0
- package/src/index.ts +7 -0
- package/src/store/functions.ts +8 -6
package/lib/index.js
CHANGED
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
import {
|
|
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, a as OutdatedBase, b as AmbiguousDeployment, c as RUN_RECORD_KEY, d as withRunRecord, f as AmbiguousProject, g as ProjectNotFound, h as ProjectDeploymentChange, j as VariablesOwnedBySomeoneElse, k as EnvironmentNeedsDeployKey, l as RunRecordError, m as Project, n as HubClientFailed, o as STORE_PROJECT_NAME, p as CreatedProjectNotFound, r as HubClientProvider, s as state, t as HubClient, u as runRecord, v as ProjectTeamChange, w as DeploymentResource, x as Deployment, y as AbsoluteRootDir, z as CredentialsError } from "./HubClient-C_nvjlvW.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([
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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, HubClient, HubClientFailed, 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 };
|
|
1846
47
|
|
|
1847
48
|
//# sourceMappingURL=index.js.map
|