@samebase/alchemy-convex 0.27.3 → 0.29.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 +57 -51
- package/lib/{HubClient-Bs5_j18U.js → HubClient-HeGoVOLO.js} +92 -69
- package/lib/HubClient-HeGoVOLO.js.map +1 -0
- package/lib/cli.js +4 -4
- package/lib/cli.js.map +1 -1
- package/lib/index.d.ts +29 -25
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/src/Deployment.ts +17 -7
- package/src/ManagementApi.ts +84 -26
- package/src/Project.ts +39 -53
- package/src/State.ts +13 -19
- package/src/cli.ts +11 -11
- package/lib/HubClient-Bs5_j18U.js.map +0 -1
package/README.md
CHANGED
|
@@ -44,8 +44,8 @@ export default Alchemy.Stack(
|
|
|
44
44
|
const backend =
|
|
45
45
|
stage === "prod"
|
|
46
46
|
? yield* Convex.Deployment("Backend", {
|
|
47
|
-
// An existing project. A new one: {
|
|
48
|
-
project: yield* Convex.Project("Project", {
|
|
47
|
+
// An existing project of the team of the token. A new one: { name: "my-app" }.
|
|
48
|
+
project: yield* Convex.Project("Project", { slug: "my-app" }),
|
|
49
49
|
type: "prod",
|
|
50
50
|
source: "./convex",
|
|
51
51
|
env: { AUTH_SECRET: Config.Redacted("AUTH_SECRET") },
|
|
@@ -81,7 +81,7 @@ relative to it:
|
|
|
81
81
|
|
|
82
82
|
```ts
|
|
83
83
|
Effect.gen(function* () {
|
|
84
|
-
const project = yield* Convex.Project("Project", {
|
|
84
|
+
const project = yield* Convex.Project("Project", { slug: "my-app" });
|
|
85
85
|
const backend = yield* Convex.Deployment("Backend", {
|
|
86
86
|
project,
|
|
87
87
|
type: "prod",
|
|
@@ -166,34 +166,36 @@ values explicitly: `Convex.providers(Convex.fromValues({ accessToken, deployKey
|
|
|
166
166
|
|
|
167
167
|
## State in Convex
|
|
168
168
|
|
|
169
|
-
`state: Convex.state(
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
`state: Convex.state()` keeps the Alchemy state of every stack and stage of a Convex team in one
|
|
170
|
+
project of that team, `alchemy-state-store`, the same name as the Cloudflare state store of Alchemy,
|
|
171
|
+
so a repository holds one secret and a stack can create and destroy its own project.
|
|
172
172
|
[docs/design/convex-state.md](docs/design/convex-state.md) records the decisions and the
|
|
173
173
|
measurements.
|
|
174
174
|
|
|
175
175
|
```ts
|
|
176
176
|
export default Alchemy.Stack(
|
|
177
177
|
process.env["GITHUB_REPOSITORY"] ?? "my-org/my-app", // the stack name: the repository
|
|
178
|
-
{ providers: Convex.providers(), state: Convex.state(
|
|
178
|
+
{ providers: Convex.providers(), state: Convex.state() },
|
|
179
179
|
Effect.gen(function* () {
|
|
180
180
|
/* ... */
|
|
181
181
|
}),
|
|
182
182
|
);
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
Every run needs the team token in `CONVEX_ACCESS_TOKEN`, or the Convex CLI login on a laptop.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
`
|
|
190
|
-
|
|
191
|
-
store
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
185
|
+
Every run needs the team token in `CONVEX_ACCESS_TOKEN`, or the Convex CLI login on a laptop. The
|
|
186
|
+
state and the projects are in the team of that credential. A team access token and an OAuth token of
|
|
187
|
+
a team name their team. The Convex CLI login names no team, so the run uses the team of the
|
|
188
|
+
deployment that the Convex CLI selected: `CONVEX_DEPLOYMENT` in the environment or in `.env.local`,
|
|
189
|
+
which `npx convex dev` writes. A local or anonymous deployment is in no team, so the run stops and
|
|
190
|
+
asks for a cloud deployment. The first run of a team creates the project `alchemy-state-store` and
|
|
191
|
+
pushes the store functions to its production deployment with the Convex CLI of the app; a later
|
|
192
|
+
version of this package pushes a newer store when it finds an older one. A store that is newer than
|
|
193
|
+
the package fails the run with both versions, so the repositories of a team upgrade the package
|
|
194
|
+
together. Each run makes a deploy key of that deployment that can only run the store's internal
|
|
195
|
+
functions, and deletes it at the end. Rows are in the tables `resources` and `outputs` of the
|
|
196
|
+
component `alchemyState` in the dashboard, under the stack name. The store has no lock: run one
|
|
197
|
+
deploy of a stage at a time, such as with a CI concurrency group. The store and its readers
|
|
198
|
+
(Samebase) are released and upgraded together.
|
|
197
199
|
|
|
198
200
|
Store version 3 supports native subscriptions. A trusted server checks repository access, then calls
|
|
199
201
|
internal `reader:authorize({ repository, subject })` with a deployment credential. It gives the
|
|
@@ -264,20 +266,22 @@ call.
|
|
|
264
266
|
|
|
265
267
|
## Resources
|
|
266
268
|
|
|
267
|
-
- `Convex.Project`: `{
|
|
268
|
-
default deployment, or takes over one with that exact name with
|
|
269
|
-
renames the project in place. `{
|
|
270
|
-
over with `--adopt`; it never creates or renames a project.
|
|
271
|
-
`teamId`, `prodDeploymentName`, `prodDeploymentUrl`.
|
|
269
|
+
- `Convex.Project`: `{ name, deploymentRegion?, deploymentType? }` creates a project with its
|
|
270
|
+
default deployment in the team of the credential, or takes over one with that exact name with
|
|
271
|
+
`--adopt`. A changed `name` renames the project in place. `{ slug }` is an existing project of
|
|
272
|
+
that team, found by its slug and taken over with `--adopt`; it never creates or renames a project.
|
|
273
|
+
Outputs `projectId`, `slug`, `name`, `teamId`, `prodDeploymentName`, `prodDeploymentUrl`.
|
|
272
274
|
- `Convex.Deployment`: `{ project, type: "prod" }` or `{ project, type: "dev" | "preview", name }`,
|
|
273
275
|
each with optional `rootDir`, `source`, and `env`. Outputs `name` (such as `happy-animal-123`),
|
|
274
276
|
`url`, `siteUrl`, `type`, `projectId`, `reference`, `previewName`, `hash`, and `envNames`.
|
|
275
277
|
- `prod` is the project's default production deployment (with a deploy key: the key's production
|
|
276
278
|
deployment). The resource never creates or deletes one. A production deployment with code that
|
|
277
279
|
is not in state is taken over only with `--adopt`.
|
|
278
|
-
- `dev` creates a dev deployment with `name` as its reference
|
|
279
|
-
|
|
280
|
-
|
|
280
|
+
- `dev` creates a dev deployment with `name` as its reference. With `--adopt`, `name` can also be
|
|
281
|
+
the Convex name of a dev deployment that exists, such as `happy-animal-123` from `.env.local`,
|
|
282
|
+
which `npx convex dev` made: the resource takes it over. A name that matches nothing creates a
|
|
283
|
+
dev deployment with that reference. `preview` creates a preview deployment with `name` as its
|
|
284
|
+
preview name. An expired preview is created again on the next deploy.
|
|
281
285
|
- `rootDir` is the app directory, relative to the directory where the deploy runs, `"."` by
|
|
282
286
|
default. It must be relative (`AbsoluteRootDir`) and have a package.json (`ConvexAppNotFound`).
|
|
283
287
|
There is no search in the directories above it. When convex.json puts the functions outside
|
|
@@ -371,27 +375,29 @@ variables. Each callback remains one exact registered URI.
|
|
|
371
375
|
|
|
372
376
|
### The development stage
|
|
373
377
|
|
|
374
|
-
A laptop
|
|
375
|
-
`
|
|
376
|
-
|
|
378
|
+
A laptop gets its client from a stage `dev-<name>` of the app's stack, run on purpose, not at each
|
|
379
|
+
dev start: `npx convex dev` has made and selected the dev deployment and pushed the code, and then
|
|
380
|
+
`alchemy deploy --stage dev-<name>` gives that deployment its variables. The stage creates nothing
|
|
381
|
+
and pushes nothing. It has three rows on the deployment that `.env.local` selects, taken over by its
|
|
382
|
+
Convex name:
|
|
377
383
|
|
|
378
384
|
```ts
|
|
379
|
-
//
|
|
385
|
+
// `selected` is the name in CONVEX_DEPLOYMENT of .env.local, such as happy-animal-123, and
|
|
386
|
+
// `project` is the project of stage `prod`.
|
|
380
387
|
const prod = Convex.SharedHubClient.read("HubClient", { stage: "prod" });
|
|
381
|
-
const
|
|
388
|
+
const site =
|
|
382
389
|
yield *
|
|
383
|
-
Convex.Deployment("
|
|
390
|
+
Convex.Deployment("Verification", {
|
|
384
391
|
project,
|
|
385
392
|
type: "dev",
|
|
386
|
-
name:
|
|
387
|
-
source: "./convex",
|
|
393
|
+
name: selected,
|
|
388
394
|
env: { SAMEBASE_REPOSITORY_ID: prod.repository },
|
|
389
395
|
}).pipe(adopt(true));
|
|
390
396
|
const hub =
|
|
391
397
|
yield *
|
|
392
398
|
Convex.SharedHubClient("HubClient", {
|
|
393
399
|
app: "scout",
|
|
394
|
-
redirectUri: Output.interpolate`${
|
|
400
|
+
redirectUri: Output.interpolate`${site.siteUrl}/auth/hub/callback`,
|
|
395
401
|
deploymentType: "development",
|
|
396
402
|
initialAccessToken: prod.initialAccessToken,
|
|
397
403
|
});
|
|
@@ -400,7 +406,7 @@ yield *
|
|
|
400
406
|
Convex.Deployment("Site", {
|
|
401
407
|
project,
|
|
402
408
|
type: "dev",
|
|
403
|
-
name:
|
|
409
|
+
name: selected,
|
|
404
410
|
env: {
|
|
405
411
|
HUB_ISSUER: variables.issuer,
|
|
406
412
|
HUB_CLIENT_ID: variables.clientId,
|
|
@@ -412,11 +418,13 @@ yield *
|
|
|
412
418
|
The imports are `alchemy/AdoptPolicy` for `adopt` and `alchemy/Output` for `Output`.
|
|
413
419
|
|
|
414
420
|
1. `read` gives the initial access token and the repository id from the `HubClient` row of stage
|
|
415
|
-
`prod`.
|
|
416
|
-
2. "
|
|
417
|
-
`
|
|
418
|
-
|
|
419
|
-
|
|
421
|
+
`prod`.
|
|
422
|
+
2. "Verification" sets `SAMEBASE_REPOSITORY_ID`, which the site serves as
|
|
423
|
+
`/samebase-verification.json`. `adopt` takes over the deployment and the variables that the dev
|
|
424
|
+
start set before.
|
|
425
|
+
3. "HubClient" asks the hub for a development client on the deployment's site URL with the token. A
|
|
426
|
+
later run renews it. A sign-in renews it too, so a laptop in use never needs the stage again; a
|
|
427
|
+
development client expires 14 days after its last run or sign-in.
|
|
420
428
|
4. "Site" writes the three variables. It removes only a name that leaves its own `env`, so
|
|
421
429
|
`SAMEBASE_REPOSITORY_ID` stays.
|
|
422
430
|
|
|
@@ -455,8 +463,8 @@ repository `owner/name`. A stack without the stage of the pull request has no ca
|
|
|
455
463
|
|
|
456
464
|
- `Convex.Project` and `Convex.Deployment` have the removal policy `retain`. `alchemy destroy`
|
|
457
465
|
forgets them and Convex keeps them. Add `RemovalPolicy.destroy()` to delete with the resource.
|
|
458
|
-
- A project is never replaced. A change of `
|
|
459
|
-
stops the deploy with a typed error that names the fix.
|
|
466
|
+
- A project is never replaced. A change of `slug`, `deploymentType`, or `deploymentRegion`, or a
|
|
467
|
+
credential of another team, stops the deploy with a typed error that names the fix.
|
|
460
468
|
- A deployment keeps its `project`, `type`, and `name`; a change stops the deploy with
|
|
461
469
|
`DeploymentIdentityChange`. A production deployment is never deleted, also not with
|
|
462
470
|
`RemovalPolicy.destroy()`, and also not a dev deployment that became one.
|
|
@@ -488,8 +496,6 @@ repository `owner/name`. A stack without the stage of the pull request has no ca
|
|
|
488
496
|
deployment must answer there.
|
|
489
497
|
- A run that stops between a deployment create and the state write leaves a deployment that state
|
|
490
498
|
does not know. The next run stops with `OwnedBySomeoneElse`; run it with `--adopt`.
|
|
491
|
-
- Team slugs for `{ team, name }` resolve through an unofficial dashboard endpoint. Pass the numeric
|
|
492
|
-
team id to avoid it. `{ team, slug }` uses the official endpoint.
|
|
493
499
|
- Not covered: deployment regions and classes, custom domains, log streams, teams and members,
|
|
494
500
|
components, snapshots.
|
|
495
501
|
|
|
@@ -506,11 +512,11 @@ pnpm run test:live # real API calls, see below
|
|
|
506
512
|
|
|
507
513
|
Unit tests run against recorded Management API payloads in `test/fixtures/management/` and a fake
|
|
508
514
|
Convex CLI. Live tests run only with `ALCHEMY_CONVEX_LIVE=1`, which `pnpm run test:live` sets. They
|
|
509
|
-
need a
|
|
510
|
-
Each test creates a project whose name starts with
|
|
511
|
-
`tmp-alchemy-convex-`), deletes it at the end, also after a
|
|
512
|
-
project, or a deployment of a project, without that prefix. A file
|
|
513
|
-
for example `pnpm run test:live test/live/plane.live.test.ts`.
|
|
515
|
+
need a team access token of `ALCHEMY_CONVEX_LIVE_TEAM` in `CONVEX_ACCESS_TOKEN`: the projects are in
|
|
516
|
+
the team of the token. Each test creates a project whose name starts with
|
|
517
|
+
`ALCHEMY_CONVEX_LIVE_PREFIX` (default `tmp-alchemy-convex-`), deletes it at the end, also after a
|
|
518
|
+
failure, and refuses to delete a project, or a deployment of a project, without that prefix. A file
|
|
519
|
+
argument runs one live file only, for example `pnpm run test:live test/live/plane.live.test.ts`.
|
|
514
520
|
|
|
515
521
|
Releases: change `version` in `package.json` in a pull request. After the merge,
|
|
516
522
|
`.github/workflows/release.yml` publishes that version from `main` with npm trusted publishing.
|
|
@@ -18,10 +18,11 @@ import { diffBindings, isResolved } from "alchemy/Diff";
|
|
|
18
18
|
import * as Provider from "alchemy/Provider";
|
|
19
19
|
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
|
|
20
20
|
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { parseEnv, promisify, stripVTControlCharacters } from "node:util";
|
|
21
23
|
import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
|
|
22
24
|
import * as Schedule from "effect/Schedule";
|
|
23
25
|
import { createHash } from "node:crypto";
|
|
24
|
-
import { promisify, stripVTControlCharacters } from "node:util";
|
|
25
26
|
import { hashDirectory } from "alchemy/Command/Memo";
|
|
26
27
|
import * as Clock from "effect/Clock";
|
|
27
28
|
import * as ChildProcess from "effect/process/ChildProcess";
|
|
@@ -173,6 +174,28 @@ const TeamRow = Schema.Struct({
|
|
|
173
174
|
const TeamRows = Schema.Array(TeamRow);
|
|
174
175
|
/** The first page of GET /teams/{team_id}/projects: every row carries the slug of the team. */
|
|
175
176
|
const ProjectSlugRows = Schema.Struct({ items: Schema.Array(Schema.Struct({ teamSlug: Schema.String })) });
|
|
177
|
+
/** GET /deployments/{name}: the project of the deployment. */
|
|
178
|
+
const DeploymentProject = Schema.Struct({ projectId: Schema.Number });
|
|
179
|
+
/** GET /projects/{id}: the team of the project. */
|
|
180
|
+
const ProjectTeam = Schema.Struct({ teamId: Schema.Number });
|
|
181
|
+
/**
|
|
182
|
+
* The cloud deployment that the Convex CLI selected: CONVEX_DEPLOYMENT in the environment or in
|
|
183
|
+
* `.env.local` of the working directory, as `npx convex dev` writes it (`dev:happy-animal-123`),
|
|
184
|
+
* without the type prefix. Undefined when neither has it, and for a local or anonymous deployment
|
|
185
|
+
* (`local:local-team-app`, `anonymous:anonymous-app`), which runs on this machine and is in no team.
|
|
186
|
+
*/
|
|
187
|
+
const selectedDeployment = () => {
|
|
188
|
+
const fromEnv = process.env["CONVEX_DEPLOYMENT"];
|
|
189
|
+
const value = fromEnv !== void 0 && fromEnv !== "" ? fromEnv : (() => {
|
|
190
|
+
try {
|
|
191
|
+
return parseEnv(readFileSync(".env.local", "utf8"))["CONVEX_DEPLOYMENT"];
|
|
192
|
+
} catch {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
const name = /^(?:(?:dev|prod|preview):)?([^:]+)$/.exec(value ?? "")?.[1];
|
|
197
|
+
return name === void 0 || name.startsWith("anonymous-") ? void 0 : name;
|
|
198
|
+
};
|
|
176
199
|
/** GET /token_details: the team of a team token, or the project of a project token. */
|
|
177
200
|
const TokenDetails = Schema.Union([Schema.Struct({
|
|
178
201
|
type: Schema.Literal("teamToken"),
|
|
@@ -194,17 +217,36 @@ const makeService = (source) => {
|
|
|
194
217
|
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)))));
|
|
195
218
|
/** The slug of the team, from the first project of its listing, or undefined for a team without one. Read once. */
|
|
196
219
|
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));
|
|
220
|
+
const projectTokenError = (projectId) => new ConvexApiError({
|
|
221
|
+
operation: "resolve team",
|
|
222
|
+
status: 403,
|
|
223
|
+
message: `the access token belongs to project ${projectId}, and a project token cannot create projects. Use a team access token.`
|
|
224
|
+
});
|
|
225
|
+
const teamId = Effect.runSync(Effect.cached(Effect.gen(function* () {
|
|
226
|
+
const details = yield* tokenDetails;
|
|
227
|
+
if (details?.type === "teamToken") return details.teamId;
|
|
228
|
+
if (details?.type === "projectToken") return yield* projectTokenError(details.projectId);
|
|
229
|
+
const selected = selectedDeployment();
|
|
230
|
+
if (selected === void 0) return yield* new ConvexApiError({
|
|
231
|
+
operation: "resolve team",
|
|
232
|
+
status: 0,
|
|
233
|
+
message: "the Convex CLI login names no team, and no cloud deployment is selected. Run `npx convex dev` once with a cloud deployment."
|
|
234
|
+
});
|
|
235
|
+
const deployment = yield* request("get deployment", (c) => c.GET("/deployments/{deployment_name}", { params: { path: { deployment_name: selected } } }));
|
|
236
|
+
const { projectId } = Schema.decodeUnknownSync(DeploymentProject)(deployment);
|
|
237
|
+
const project = yield* request("get project", (c) => c.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } }));
|
|
238
|
+
return Schema.decodeUnknownSync(ProjectTeam)(project).teamId;
|
|
239
|
+
})));
|
|
197
240
|
const resolveTeamId = (team) => Effect.gen(function* () {
|
|
198
241
|
if (typeof team === "number") return team;
|
|
199
|
-
if (
|
|
242
|
+
if (/^\d+$/.test(team)) return Number(team);
|
|
200
243
|
const details = yield* tokenDetails;
|
|
201
244
|
if (details?.type === "teamToken") {
|
|
202
|
-
if (team === void 0) return details.teamId;
|
|
203
245
|
const slug = yield* teamSlug(details.teamId);
|
|
204
246
|
if (slug === void 0) return yield* new ConvexApiError({
|
|
205
247
|
operation: "resolve team",
|
|
206
248
|
status: 0,
|
|
207
|
-
message: `team "${team}" cannot be checked against the access token: team ${details.teamId} has no project yet. Set team to ${details.teamId}
|
|
249
|
+
message: `team "${team}" cannot be checked against the access token: team ${details.teamId} has no project yet. Set team to ${details.teamId}.`
|
|
208
250
|
});
|
|
209
251
|
if (slug !== team) return yield* new ConvexApiError({
|
|
210
252
|
operation: "resolve team",
|
|
@@ -213,16 +255,7 @@ const makeService = (source) => {
|
|
|
213
255
|
});
|
|
214
256
|
return details.teamId;
|
|
215
257
|
}
|
|
216
|
-
if (details?.type === "projectToken") return yield*
|
|
217
|
-
operation: "resolve team",
|
|
218
|
-
status: 403,
|
|
219
|
-
message: `the access token belongs to project ${details.projectId}, and a project token cannot create projects. Use a team access token.`
|
|
220
|
-
});
|
|
221
|
-
if (team === void 0) return yield* new ConvexApiError({
|
|
222
|
-
operation: "resolve team",
|
|
223
|
-
status: 0,
|
|
224
|
-
message: "set team to the slug or the id of the Convex team: this access token does not name a team."
|
|
225
|
-
});
|
|
258
|
+
if (details?.type === "projectToken") return yield* projectTokenError(details.projectId);
|
|
226
259
|
const resolved = yield* credentials;
|
|
227
260
|
const match = (yield* Effect.tryPromise({
|
|
228
261
|
try: async () => {
|
|
@@ -253,6 +286,7 @@ const makeService = (source) => {
|
|
|
253
286
|
request,
|
|
254
287
|
requestVoid,
|
|
255
288
|
deploymentRequest,
|
|
289
|
+
teamId,
|
|
256
290
|
resolveTeamId,
|
|
257
291
|
isCredentialToken,
|
|
258
292
|
isOAuthCredential,
|
|
@@ -782,7 +816,8 @@ const checkIdentity = (current, news) => {
|
|
|
782
816
|
if (current.projectId !== news.project.projectId) return change("projectId", String(current.projectId), String(news.project.projectId));
|
|
783
817
|
if (current.type !== news.type) return change("type", current.type, news.type);
|
|
784
818
|
const currentName = nameOf(current);
|
|
785
|
-
|
|
819
|
+
const named = news.type === "dev" && current.name === news.name;
|
|
820
|
+
if (news.type !== "prod" && currentName !== news.name && !named) return change("name", currentName ?? "(none)", news.name);
|
|
786
821
|
return Effect.void;
|
|
787
822
|
};
|
|
788
823
|
const addressOf = (props) => ({
|
|
@@ -824,11 +859,13 @@ const DeploymentProvider = () => Provider.effect(DeploymentResource, Effect.gen(
|
|
|
824
859
|
/** The project's default production deployment, or undefined. */
|
|
825
860
|
const production = (management, projectId) => productionRow(management, projectId).pipe(Effect.map((row) => row === void 0 ? void 0 : targetOf(row, "prod", void 0)));
|
|
826
861
|
/**
|
|
827
|
-
* The one dev deployment with this reference, or the
|
|
828
|
-
* deployment with this preview name, or undefined.
|
|
862
|
+
* The one dev deployment with this reference or this Convex name, or the
|
|
863
|
+
* one preview deployment with this preview name, or undefined. The name
|
|
864
|
+
* form takes the dev deployment that `npx convex dev` made, such as the
|
|
865
|
+
* one `.env.local` selects, which has a reference the stack never chose.
|
|
829
866
|
*/
|
|
830
867
|
const findByName = (management, projectId, type, name) => Effect.gen(function* () {
|
|
831
|
-
const matches = (yield* listDeployments(management, projectId, type)).filter((row) => row.deploymentType === type && (type === "dev" ? row.reference === name : row.previewIdentifier === name));
|
|
868
|
+
const matches = (yield* listDeployments(management, projectId, type)).filter((row) => row.deploymentType === type && (type === "dev" ? row.reference === name || row.name === name : row.previewIdentifier === name));
|
|
832
869
|
if (matches.length > 1) return yield* new AmbiguousDeployment({
|
|
833
870
|
projectId,
|
|
834
871
|
type,
|
|
@@ -843,7 +880,7 @@ const DeploymentProvider = () => Provider.effect(DeploymentResource, Effect.gen(
|
|
|
843
880
|
const deployKeyTarget = (key, deployment, address) => Effect.gen(function* () {
|
|
844
881
|
const info = Schema.decodeUnknownSync(DeploymentInfo)(yield* api.deploymentRequest("get deployment info", deployment, key, (client) => client.GET("/deployment_info")));
|
|
845
882
|
const reference = info.reference ?? "";
|
|
846
|
-
if (info.projectId !== address.projectId || info.deploymentType !== address.type || address.type === "dev" && reference !== address.name) return;
|
|
883
|
+
if (info.projectId !== address.projectId || info.deploymentType !== address.type || address.type === "dev" && reference !== address.name && deployment !== address.name) return;
|
|
847
884
|
const urls = Schema.decodeUnknownSync(CanonicalUrls)(yield* api.deploymentRequest("get canonical urls", deployment, key, (client) => client.GET("/get_canonical_urls")));
|
|
848
885
|
return {
|
|
849
886
|
projectId: info.projectId,
|
|
@@ -1094,14 +1131,14 @@ const DeploymentProvider = () => Provider.effect(DeploymentResource, Effect.gen(
|
|
|
1094
1131
|
//#endregion
|
|
1095
1132
|
//#region src/Project.ts
|
|
1096
1133
|
const Project = Resource("Convex.Project", { defaultRemovalPolicy: "retain" });
|
|
1097
|
-
/** The
|
|
1134
|
+
/** The credential is of another team than the project. Convex cannot move a project. */
|
|
1098
1135
|
var ProjectTeamChange = class extends Schema.TaggedError()("ProjectTeamChange", {
|
|
1099
1136
|
projectId: Schema.Number,
|
|
1100
1137
|
teamId: Schema.Number,
|
|
1101
1138
|
requestedTeamId: Schema.Number
|
|
1102
1139
|
}) {
|
|
1103
1140
|
get message() {
|
|
1104
|
-
return `Convex.Project cannot move project ${this.projectId} from team ${this.teamId} to team ${this.requestedTeamId}.
|
|
1141
|
+
return `Convex.Project cannot move project ${this.projectId} from team ${this.teamId} to team ${this.requestedTeamId}, the team of the credential. Use a token of team ${this.teamId}, or with the Convex CLI login, select a deployment of team ${this.teamId}. To use the other team, create a new resource with a new logical id and move the data yourself.`;
|
|
1105
1142
|
}
|
|
1106
1143
|
};
|
|
1107
1144
|
/**
|
|
@@ -1137,11 +1174,11 @@ var CreatedProjectNotFound = class extends Schema.TaggedError()("CreatedProjectN
|
|
|
1137
1174
|
};
|
|
1138
1175
|
/** No project in the team has the slug. A `slug` resource never creates one. */
|
|
1139
1176
|
var ProjectNotFound = class extends Schema.TaggedError()("ProjectNotFound", {
|
|
1140
|
-
|
|
1177
|
+
teamId: Schema.Number,
|
|
1141
1178
|
slug: Schema.String
|
|
1142
1179
|
}) {
|
|
1143
1180
|
get message() {
|
|
1144
|
-
return `Team ${this.
|
|
1181
|
+
return `Team ${this.teamId} 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.`;
|
|
1145
1182
|
}
|
|
1146
1183
|
};
|
|
1147
1184
|
/** The props change the slug of a `slug` resource: that is another project. */
|
|
@@ -1203,8 +1240,6 @@ const checkCreateTimeProps = (projectId, olds, news) => {
|
|
|
1203
1240
|
}));
|
|
1204
1241
|
return Effect.void;
|
|
1205
1242
|
};
|
|
1206
|
-
/** True when `team`, a slug or a numeric id, names the team of the project. */
|
|
1207
|
-
const isTeamOf = (details, team) => typeof team === "number" || /^\d+$/.test(team) ? details.teamId === Number(team) : details.teamSlug === team;
|
|
1208
1243
|
/** The row of the project's default production deployment, or undefined. */
|
|
1209
1244
|
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)));
|
|
1210
1245
|
/** GET /projects/{id}, or undefined after a 404. */
|
|
@@ -1278,9 +1313,9 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1278
1313
|
prodDeploymentUrl: prod?.deploymentUrl
|
|
1279
1314
|
})));
|
|
1280
1315
|
const getById = (projectId) => getProjectById(api, projectId);
|
|
1281
|
-
/** The project with this slug in the team, or undefined.
|
|
1282
|
-
const getBySlug = (
|
|
1283
|
-
team_id_or_slug: String(
|
|
1316
|
+
/** The project with this slug in the team, or undefined. */
|
|
1317
|
+
const getBySlug = (teamId, slug) => absentAsUndefined(api.request("get project by slug", (client) => client.GET("/teams/{team_id_or_slug}/projects/{project_slug}", { params: { path: {
|
|
1318
|
+
team_id_or_slug: String(teamId),
|
|
1284
1319
|
project_slug: slug
|
|
1285
1320
|
} } }))).pipe(Effect.map((body) => body === void 0 ? void 0 : Schema.decodeUnknownSync(ProjectDetails)(body)));
|
|
1286
1321
|
const findByName = (teamId, name) => findProjectByName(api, teamId, name);
|
|
@@ -1310,45 +1345,37 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1310
1345
|
diff: Effect.fn(function* ({ olds, news, output }) {
|
|
1311
1346
|
if (!isResolved(news)) return void 0;
|
|
1312
1347
|
if (output === void 0) return void 0;
|
|
1313
|
-
|
|
1348
|
+
const teamId = yield* api.teamId;
|
|
1349
|
+
if (teamId !== output.teamId) return yield* new ProjectTeamChange({
|
|
1350
|
+
projectId: output.projectId,
|
|
1351
|
+
teamId: output.teamId,
|
|
1352
|
+
requestedTeamId: teamId
|
|
1353
|
+
});
|
|
1354
|
+
if (news.slug !== void 0) {
|
|
1314
1355
|
if (news.slug !== output.slug) return yield* new ProjectSlugChange({
|
|
1315
1356
|
projectId: output.projectId,
|
|
1316
1357
|
slug: output.slug,
|
|
1317
1358
|
requestedSlug: news.slug
|
|
1318
1359
|
});
|
|
1319
|
-
if (olds.team !== news.team) {
|
|
1320
|
-
const details = yield* getById(output.projectId);
|
|
1321
|
-
if (details !== void 0 && !isTeamOf(details, news.team)) return yield* new ProjectTeamChange({
|
|
1322
|
-
projectId: output.projectId,
|
|
1323
|
-
teamId: output.teamId,
|
|
1324
|
-
requestedTeamId: yield* api.resolveTeamId(news.team)
|
|
1325
|
-
});
|
|
1326
|
-
}
|
|
1327
1360
|
return { action: "noop" };
|
|
1328
1361
|
}
|
|
1329
|
-
|
|
1330
|
-
const requestedTeamId = yield* api.resolveTeamId(news.team);
|
|
1331
|
-
if (requestedTeamId !== output.teamId) return yield* new ProjectTeamChange({
|
|
1332
|
-
projectId: output.projectId,
|
|
1333
|
-
teamId: output.teamId,
|
|
1334
|
-
requestedTeamId
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
return "name" in olds && olds.name === news.name ? void 0 : { action: "update" };
|
|
1362
|
+
return olds.name === news.name ? void 0 : { action: "update" };
|
|
1338
1363
|
}),
|
|
1339
1364
|
read: Effect.fn(function* ({ olds, output }) {
|
|
1340
1365
|
if (output !== void 0) {
|
|
1341
1366
|
const details = yield* getById(output.projectId);
|
|
1342
1367
|
return details === void 0 ? void 0 : yield* attributesOf(details);
|
|
1343
1368
|
}
|
|
1344
|
-
const
|
|
1369
|
+
const teamId = yield* api.teamId;
|
|
1370
|
+
const existing = olds.slug !== void 0 ? yield* getBySlug(teamId, olds.slug) : yield* findByName(teamId, olds.name);
|
|
1345
1371
|
return existing === void 0 ? void 0 : Unowned(yield* attributesOf(existing));
|
|
1346
1372
|
}),
|
|
1347
1373
|
reconcile: Effect.fn(function* ({ fqn, olds, news, output }) {
|
|
1348
|
-
|
|
1349
|
-
|
|
1374
|
+
const teamId = yield* api.teamId;
|
|
1375
|
+
if (news.slug !== void 0) {
|
|
1376
|
+
const details = output === void 0 ? yield* getBySlug(teamId, news.slug) : yield* getById(output.projectId);
|
|
1350
1377
|
if (details === void 0) return yield* new ProjectNotFound({
|
|
1351
|
-
|
|
1378
|
+
teamId,
|
|
1352
1379
|
slug: news.slug
|
|
1353
1380
|
});
|
|
1354
1381
|
if (details.slug !== news.slug) return yield* new ProjectSlugChange({
|
|
@@ -1356,10 +1383,10 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1356
1383
|
slug: details.slug,
|
|
1357
1384
|
requestedSlug: news.slug
|
|
1358
1385
|
});
|
|
1359
|
-
if (
|
|
1386
|
+
if (details.teamId !== teamId) return yield* new ProjectTeamChange({
|
|
1360
1387
|
projectId: details.id,
|
|
1361
1388
|
teamId: details.teamId,
|
|
1362
|
-
requestedTeamId:
|
|
1389
|
+
requestedTeamId: teamId
|
|
1363
1390
|
});
|
|
1364
1391
|
if (output === void 0 && !(yield* shouldAdopt(fqn))) return yield* new OwnedBySomeoneElse({
|
|
1365
1392
|
message: `Convex project ${details.id} ("${news.slug}") exists and is not in this stack's state. Re-run with --adopt to take it over.`,
|
|
@@ -1368,9 +1395,8 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1368
1395
|
});
|
|
1369
1396
|
return yield* attributesOf(details);
|
|
1370
1397
|
}
|
|
1371
|
-
const teamId = yield* api.resolveTeamId(news.team);
|
|
1372
1398
|
if (output !== void 0) {
|
|
1373
|
-
if (olds !== void 0 &&
|
|
1399
|
+
if (olds !== void 0 && olds.name !== void 0) yield* checkCreateTimeProps(output.projectId, olds, news);
|
|
1374
1400
|
if (output.teamId !== teamId) return yield* new ProjectTeamChange({
|
|
1375
1401
|
projectId: output.projectId,
|
|
1376
1402
|
teamId: output.teamId,
|
|
@@ -1712,8 +1738,8 @@ const ensureSealed = (options, deployment, functions) => Effect.gen(function* ()
|
|
|
1712
1738
|
* the run whose project is not the oldest deletes its own, which holds
|
|
1713
1739
|
* nothing yet, and uses the oldest, so that every run ends on one store.
|
|
1714
1740
|
*/
|
|
1715
|
-
const createStoreProject = (api, teamId
|
|
1716
|
-
yield* Effect.logInfo(`Convex state: creating the project ${STORE_PROJECT_NAME} in team ${
|
|
1741
|
+
const createStoreProject = (api, teamId) => Effect.gen(function* () {
|
|
1742
|
+
yield* Effect.logInfo(`Convex state: creating the project ${STORE_PROJECT_NAME} in team ${teamId}`);
|
|
1717
1743
|
const created = yield* createProject(api, teamId, {
|
|
1718
1744
|
name: STORE_PROJECT_NAME,
|
|
1719
1745
|
deploymentType: "prod",
|
|
@@ -1732,9 +1758,8 @@ const createStoreProject = (api, teamId, team) => Effect.gen(function* () {
|
|
|
1732
1758
|
*/
|
|
1733
1759
|
const makeConvexState = (options) => Effect.gen(function* () {
|
|
1734
1760
|
const { api } = options;
|
|
1735
|
-
const teamId = yield* api.
|
|
1736
|
-
const
|
|
1737
|
-
const project = (yield* findProjectByName(api, teamId, "alchemy-state-store")) ?? (yield* createStoreProject(api, teamId, team));
|
|
1761
|
+
const teamId = yield* api.teamId;
|
|
1762
|
+
const project = (yield* findProjectByName(api, teamId, "alchemy-state-store")) ?? (yield* createStoreProject(api, teamId));
|
|
1738
1763
|
const deployment = yield* prodDeploymentOf(api, project.id);
|
|
1739
1764
|
if (deployment === void 0) return yield* new ProductionDeploymentNotFound({ projectId: project.id });
|
|
1740
1765
|
const keys = [];
|
|
@@ -1750,7 +1775,7 @@ const makeConvexState = (options) => Effect.gen(function* () {
|
|
|
1750
1775
|
})))) };
|
|
1751
1776
|
let version = yield* storeVersion(functions);
|
|
1752
1777
|
if (version === void 0 || version < 5) {
|
|
1753
|
-
if (version === void 0 && deployment.lastDeployTime) return yield* failure(`project ${STORE_PROJECT_NAME} (${project.id}) of team ${
|
|
1778
|
+
if (version === void 0 && deployment.lastDeployTime) return yield* failure(`project ${STORE_PROJECT_NAME} (${project.id}) of team ${teamId} has code that is not the state store, and Convex.state does not push over it. Rename that project in the Convex dashboard.`);
|
|
1754
1779
|
version = yield* storeVersion(functions);
|
|
1755
1780
|
if (version === void 0 || version < 5) {
|
|
1756
1781
|
yield* pushStore(options, deployment);
|
|
@@ -1799,14 +1824,13 @@ const makeConvexState = (options) => Effect.gen(function* () {
|
|
|
1799
1824
|
};
|
|
1800
1825
|
});
|
|
1801
1826
|
/**
|
|
1802
|
-
* Alchemy state in the `alchemy-state-store` project of
|
|
1803
|
-
* in CONVEX_ACCESS_TOKEN or the Convex CLI login
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
* stack never touches Convex.
|
|
1827
|
+
* Alchemy state in the `alchemy-state-store` project of the team of the credential: the team token
|
|
1828
|
+
* in CONVEX_ACCESS_TOKEN, which names its team, or the Convex CLI login, with the team of the
|
|
1829
|
+
* deployment that the CLI selected (CONVEX_DEPLOYMENT, see ManagementApi.teamId). The first use of
|
|
1830
|
+
* the run finds or creates the project and pushes the store when it is missing or older.
|
|
1831
|
+
* `alchemy plan` on another stack never touches Convex.
|
|
1808
1832
|
*/
|
|
1809
|
-
const state = (
|
|
1833
|
+
const state = () => Layer.effect(State, Effect.gen(function* () {
|
|
1810
1834
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
1811
1835
|
const scope = yield* Scope.Scope;
|
|
1812
1836
|
const context = yield* Effect.context();
|
|
@@ -1817,7 +1841,6 @@ const state = (options = {}) => Layer.effect(State, Effect.gen(function* () {
|
|
|
1817
1841
|
env: process.env
|
|
1818
1842
|
}).pipe(Effect.catch((error) => Effect.logWarning(`Convex state: the stage outputs of this run carry no record. ${error.message}`).pipe(Effect.as(void 0))));
|
|
1819
1843
|
return yield* makeConvexState({
|
|
1820
|
-
team: options.team,
|
|
1821
1844
|
api,
|
|
1822
1845
|
spawner,
|
|
1823
1846
|
scope,
|
|
@@ -1989,4 +2012,4 @@ const registerDevelopmentClient = (options) => Effect.gen(function* () {
|
|
|
1989
2012
|
//#endregion
|
|
1990
2013
|
export { PushTargetMismatch as A, DEPLOY_KEY_ENV as B, DeploymentProvider as C, ConvexAppNotFound as D, CodePushFailed as E, ManagementApi as F, fromValues as H, ManagementApiLive as I, ACCESS_TOKEN_ENV as L, installedCli as M, parseDeployOutput as N, ConvexCliNotFound as O, ConvexApiError as P, Credentials as R, DeploymentIdentityChange as S, ProductionDeploymentNotFound as T, fromConfig as V, ProjectProvider as _, registerDevelopmentClient as a, AmbiguousDeployment as b, RUN_RECORD_KEY as c, withRunRecord as d, AmbiguousProject as f, ProjectNotFound as g, ProjectDeploymentChange as h, HubClientProvider as i, VariablesOwnedBySomeoneElse as j, EnvironmentNeedsDeployKey as k, RunRecordError as l, Project as m, HubClient as n, STORE_PROJECT_NAME as o, CreatedProjectNotFound as p, HubClientFailed as r, state as s, HUB_PROJECT_NAME as t, runRecord as u, ProjectTeamChange as v, DeploymentResource as w, Deployment as x, AbsoluteRootDir as y, CredentialsError as z };
|
|
1991
2014
|
|
|
1992
|
-
//# sourceMappingURL=HubClient-
|
|
2015
|
+
//# sourceMappingURL=HubClient-HeGoVOLO.js.map
|