@samebase/alchemy-convex 0.5.0 → 0.6.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 +21 -16
- package/lib/index.d.ts +11 -8
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +234 -157
- package/lib/index.js.map +1 -1
- package/package.json +2 -3
- package/src/Deployment.ts +9 -2
- package/src/Project.ts +125 -88
- package/src/Push.ts +74 -46
- package/src/State.ts +278 -159
- package/src/index.ts +1 -1
- package/src/{component/_generated/api.ts → store/convex/_generated/api.d.ts} +9 -8
- package/src/store/convex/_generated/api.js +23 -0
- package/src/{component/_generated/server.ts → store/convex/_generated/server.d.ts} +18 -25
- package/src/store/convex/_generated/server.js +101 -0
- package/src/{component → store/convex}/state.ts +96 -46
- package/src/store/convex/version.ts +6 -0
- package/src/store/package.json +8 -0
- package/src/component/_generated/component.ts +0 -92
- package/src/component/convex.config.ts +0 -5
- /package/src/{component/_generated/dataModel.ts → store/convex/_generated/dataModel.d.ts} +0 -0
- /package/src/{component → store/convex}/schema.ts +0 -0
package/lib/index.js
CHANGED
|
@@ -25,7 +25,9 @@ import { hashDirectory } from "alchemy/Command/Memo";
|
|
|
25
25
|
import * as Clock from "effect/Clock";
|
|
26
26
|
import * as ChildProcess from "effect/process/ChildProcess";
|
|
27
27
|
import * as Stream from "effect/Stream";
|
|
28
|
-
import {
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import * as Scope from "effect/Scope";
|
|
30
|
+
import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveState } from "alchemy/State";
|
|
29
31
|
//#region src/Credentials.ts
|
|
30
32
|
var Credentials = class extends Context.Service()("@samebase/alchemy-convex/Credentials") {};
|
|
31
33
|
/** Environment variable for the access token, read by {@link fromConfig}. */
|
|
@@ -227,7 +229,7 @@ const sourceOf = (source) => typeof source === "string" ? {
|
|
|
227
229
|
};
|
|
228
230
|
/** Lifetime of a push key. Long enough for a slow push with index backfills. */
|
|
229
231
|
const PUSH_KEY_LIFETIME_MS = 72e5;
|
|
230
|
-
/** The only actions a
|
|
232
|
+
/** The only actions a key for `convex deploy` gets. */
|
|
231
233
|
const PUSH_KEY_ACTIONS = [
|
|
232
234
|
"deployment:deploy",
|
|
233
235
|
"deployment:env:view",
|
|
@@ -329,7 +331,7 @@ const parseDeployOutput = (text) => {
|
|
|
329
331
|
};
|
|
330
332
|
/** The last non-empty lines of `text`, without escape codes and with every secret replaced. */
|
|
331
333
|
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");
|
|
332
|
-
const valueOf = (value) => Redacted.isRedacted(value) ? Redacted.value(value) : value;
|
|
334
|
+
const valueOf$1 = (value) => Redacted.isRedacted(value) ? Redacted.value(value) : value;
|
|
333
335
|
/** The app directory: the nearest directory at or above the parent of `source` with a package.json. */
|
|
334
336
|
const appDirectoryOf = (source) => Effect.tryPromise({
|
|
335
337
|
try: async () => {
|
|
@@ -397,7 +399,7 @@ const pushHash = (options) => Effect.gen(function* () {
|
|
|
397
399
|
lockfile: true
|
|
398
400
|
}
|
|
399
401
|
})));
|
|
400
|
-
const env = Object.entries(options.env).map(([name, value]) => [name, valueOf(value)]).sort(([a], [b]) => a < b ? -1 : 1);
|
|
402
|
+
const env = Object.entries(options.env).map(([name, value]) => [name, valueOf$1(value)]).sort(([a], [b]) => a < b ? -1 : 1);
|
|
401
403
|
return createHash("sha256").update(JSON.stringify({
|
|
402
404
|
files,
|
|
403
405
|
deployment: options.deployment,
|
|
@@ -424,29 +426,41 @@ const cliEnvironment = (home) => ({
|
|
|
424
426
|
XDG_CONFIG_HOME: home
|
|
425
427
|
});
|
|
426
428
|
/**
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* A failed delete is logged: the key expires anyway.
|
|
429
|
+
* A deploy key of `deployment` that expires after PUSH_KEY_LIFETIME_MS, with
|
|
430
|
+
* only `allowedActions`, or every action when that is undefined. Delete it
|
|
431
|
+
* with {@link deleteDeployKey} when the work is done.
|
|
431
432
|
*/
|
|
432
|
-
const
|
|
433
|
+
const createDeployKey = (api, deployment, name, allowedActions) => Effect.gen(function* () {
|
|
433
434
|
const now = yield* Clock.currentTimeMillis;
|
|
434
435
|
const created = yield* api.request("create deploy key", (client) => client.POST("/deployments/{deployment_name}/create_deploy_key", {
|
|
435
436
|
params: { path: { deployment_name: deployment } },
|
|
436
437
|
body: {
|
|
437
|
-
name
|
|
438
|
-
...
|
|
438
|
+
name,
|
|
439
|
+
...allowedActions === void 0 ? {} : { allowedActions: [...allowedActions] },
|
|
439
440
|
expiresAt: now + PUSH_KEY_LIFETIME_MS
|
|
440
441
|
}
|
|
441
442
|
}));
|
|
442
443
|
return Redacted.make(Schema.decodeUnknownSync(CreatedDeployKey)(created).deployKey);
|
|
443
|
-
})
|
|
444
|
+
});
|
|
445
|
+
/**
|
|
446
|
+
* Deletes a key that {@link createDeployKey} made. The delete is skipped for
|
|
447
|
+
* a key that is the OAuth credential. A failed delete is logged: the key
|
|
448
|
+
* expires anyway.
|
|
449
|
+
*/
|
|
450
|
+
const deleteDeployKey = (api, deployment, key) => Effect.gen(function* () {
|
|
444
451
|
if (yield* api.isCredentialToken(Redacted.value(key))) return;
|
|
445
452
|
yield* absentAsUndefined(retryIdempotentWrite(api.requestVoid("delete deploy key", (client) => client.POST("/deployments/{deployment_name}/delete_deploy_key", {
|
|
446
453
|
params: { path: { deployment_name: deployment } },
|
|
447
454
|
body: { id: Redacted.value(key) }
|
|
448
455
|
}))));
|
|
449
|
-
}).pipe(Effect.catch((error) => Effect.logWarning(`Convex.Deployment could not delete its
|
|
456
|
+
}).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.`)));
|
|
457
|
+
/**
|
|
458
|
+
* Creates a deploy key for one push, runs `use` with it, and deletes it
|
|
459
|
+
* afterwards, also after a failure. `allowedActions` is PUSH_KEY_ACTIONS for
|
|
460
|
+
* `convex deploy`, or undefined for a push command, which can do more than a
|
|
461
|
+
* push, such as the uploads of convex-hosting through `convex run`.
|
|
462
|
+
*/
|
|
463
|
+
const withPushKey = (api, deployment, allowedActions, use) => Effect.acquireUseRelease(createDeployKey(api, deployment, "alchemy-push", allowedActions), use, (key) => deleteDeployKey(api, deployment, key));
|
|
450
464
|
/** Every variable of the deployment with its value. */
|
|
451
465
|
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));
|
|
452
466
|
/** One call that sets variables, or removes those whose value is null. */
|
|
@@ -487,15 +501,15 @@ const push = (spawner, key, options) => Effect.gen(function* () {
|
|
|
487
501
|
} : yield* retryIdempotentWrite(Effect.gen(function* () {
|
|
488
502
|
const current = yield* listVariables(options.url, key);
|
|
489
503
|
if (!options.adopt) {
|
|
490
|
-
const taken = env.filter(([name, value]) => !options.previousEnvNames.includes(name) && Object.hasOwn(current, name) && current[name] !== valueOf(value)).map(([name]) => name);
|
|
504
|
+
const taken = env.filter(([name, value]) => !options.previousEnvNames.includes(name) && Object.hasOwn(current, name) && current[name] !== valueOf$1(value)).map(([name]) => name);
|
|
491
505
|
if (taken.length > 0) return yield* new VariablesOwnedBySomeoneElse({
|
|
492
506
|
deployment: options.deployment,
|
|
493
507
|
names: taken
|
|
494
508
|
});
|
|
495
509
|
}
|
|
496
|
-
const changes = [...env.filter(([name, value]) => current[name] !== valueOf(value)).map(([name, value]) => ({
|
|
510
|
+
const changes = [...env.filter(([name, value]) => current[name] !== valueOf$1(value)).map(([name, value]) => ({
|
|
497
511
|
name,
|
|
498
|
-
value: valueOf(value)
|
|
512
|
+
value: valueOf$1(value)
|
|
499
513
|
})), ...removed.filter((name) => Object.hasOwn(current, name)).map((name) => ({
|
|
500
514
|
name,
|
|
501
515
|
value: null
|
|
@@ -934,7 +948,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
934
948
|
}) : access.kind === "previewKey" ? push(spawner, access.key, {
|
|
935
949
|
...options,
|
|
936
950
|
previewName: target.previewName
|
|
937
|
-
}) : withPushKey(access.api, target.name, news.source, (key) => push(spawner, key, {
|
|
951
|
+
}) : withPushKey(access.api, target.name, typeof news.source === "object" && news.source.command !== void 0 ? void 0 : PUSH_KEY_ACTIONS, (key) => push(spawner, key, {
|
|
938
952
|
...options,
|
|
939
953
|
previewName: void 0
|
|
940
954
|
}))).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));
|
|
@@ -1042,7 +1056,9 @@ const DeploymentRow = Schema.Struct({
|
|
|
1042
1056
|
name: Schema.String,
|
|
1043
1057
|
deploymentType: Schema.String,
|
|
1044
1058
|
isDefault: Schema.Boolean,
|
|
1045
|
-
deploymentUrl: Schema.String
|
|
1059
|
+
deploymentUrl: Schema.String,
|
|
1060
|
+
/** Absent or null until the first push. */
|
|
1061
|
+
lastDeployTime: Schema.optionalKey(Schema.NullOr(Schema.Number))
|
|
1046
1062
|
});
|
|
1047
1063
|
const DeploymentRows = Schema.Array(DeploymentRow);
|
|
1048
1064
|
/**
|
|
@@ -1068,10 +1084,71 @@ const checkCreateTimeProps = (projectId, olds, news) => {
|
|
|
1068
1084
|
};
|
|
1069
1085
|
/** True when `team`, a slug or a numeric id, names the team of the project. */
|
|
1070
1086
|
const isTeamOf = (details, team) => typeof team === "number" || /^\d+$/.test(team) ? details.teamId === Number(team) : details.teamSlug === team;
|
|
1087
|
+
/** The row of the project's default production deployment, or undefined. */
|
|
1088
|
+
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)));
|
|
1089
|
+
/** GET /projects/{id}, or undefined after a 404. */
|
|
1090
|
+
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)));
|
|
1091
|
+
/**
|
|
1092
|
+
* Every project in the team with exactly this name, in the order of the
|
|
1093
|
+
* listing. Reads every page. `q` narrows the listing on the server, and the
|
|
1094
|
+
* exact match here decides. Pattern from Confect
|
|
1095
|
+
* `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
|
|
1096
|
+
* (https://github.com/rjdellecese/confect, ISC license).
|
|
1097
|
+
*/
|
|
1098
|
+
const listProjectsByName = (api, teamId, name) => Effect.gen(function* () {
|
|
1099
|
+
const matches = [];
|
|
1100
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
1101
|
+
let cursor;
|
|
1102
|
+
while (true) {
|
|
1103
|
+
const body = yield* api.request("list projects", (client) => client.GET("/teams/{team_id}/projects", { params: {
|
|
1104
|
+
path: { team_id: teamId },
|
|
1105
|
+
query: {
|
|
1106
|
+
q: name,
|
|
1107
|
+
...cursor === void 0 ? {} : { cursor }
|
|
1108
|
+
}
|
|
1109
|
+
} }));
|
|
1110
|
+
const page = Schema.decodeUnknownSync(ProjectPage)(body);
|
|
1111
|
+
matches.push(...page.items.filter((row) => row.name === name));
|
|
1112
|
+
if (!page.pagination.hasMore) break;
|
|
1113
|
+
const next = page.pagination.nextCursor;
|
|
1114
|
+
if (next === void 0 || next === null || seenCursors.has(next)) return yield* new ConvexApiError({
|
|
1115
|
+
operation: "list projects",
|
|
1116
|
+
status: 0,
|
|
1117
|
+
code: "InvalidPagination",
|
|
1118
|
+
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}".`
|
|
1119
|
+
});
|
|
1120
|
+
seenCursors.add(next);
|
|
1121
|
+
cursor = next;
|
|
1122
|
+
}
|
|
1123
|
+
return matches;
|
|
1124
|
+
});
|
|
1125
|
+
/** The one project in the team with exactly this name, or undefined. More than one is an error. */
|
|
1126
|
+
const findProjectByName = (api, teamId, name) => listProjectsByName(api, teamId, name).pipe(Effect.flatMap((matches) => matches.length > 1 ? new AmbiguousProject({
|
|
1127
|
+
teamId,
|
|
1128
|
+
name,
|
|
1129
|
+
projectIds: matches.map((row) => row.id)
|
|
1130
|
+
}) : Effect.succeed(matches[0])));
|
|
1131
|
+
/**
|
|
1132
|
+
* POST /teams/{team_id}/create_project, then GET /projects/{id}. The identity
|
|
1133
|
+
* comes from the create response, never from a name lookup.
|
|
1134
|
+
*/
|
|
1135
|
+
const createProject = (api, teamId, props) => Effect.gen(function* () {
|
|
1136
|
+
const created = yield* api.request("create project", (client) => client.POST("/teams/{team_id}/create_project", {
|
|
1137
|
+
params: { path: { team_id: teamId } },
|
|
1138
|
+
body: {
|
|
1139
|
+
projectName: props.name,
|
|
1140
|
+
deploymentType: props.deploymentType,
|
|
1141
|
+
...props.deploymentRegion === void 0 ? {} : { deploymentRegion: props.deploymentRegion }
|
|
1142
|
+
}
|
|
1143
|
+
}));
|
|
1144
|
+
const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
|
|
1145
|
+
const details = yield* getProjectById(api, projectId);
|
|
1146
|
+
if (details === void 0) return yield* new CreatedProjectNotFound({ projectId });
|
|
1147
|
+
return details;
|
|
1148
|
+
});
|
|
1071
1149
|
const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
1072
1150
|
const api = yield* ManagementApi;
|
|
1073
|
-
const
|
|
1074
|
-
const attributesOf = (details) => prodDeployment(details.id).pipe(Effect.map((prod) => ({
|
|
1151
|
+
const attributesOf = (details) => prodDeploymentOf(api, details.id).pipe(Effect.map((prod) => ({
|
|
1075
1152
|
projectId: details.id,
|
|
1076
1153
|
slug: details.slug,
|
|
1077
1154
|
name: details.name,
|
|
@@ -1079,51 +1156,13 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1079
1156
|
prodDeploymentName: prod?.name ?? details.prodDeploymentName ?? void 0,
|
|
1080
1157
|
prodDeploymentUrl: prod?.deploymentUrl
|
|
1081
1158
|
})));
|
|
1082
|
-
const getById = (projectId) =>
|
|
1159
|
+
const getById = (projectId) => getProjectById(api, projectId);
|
|
1083
1160
|
/** The project with this slug in the team, or undefined. Official endpoint; the team can be a slug. */
|
|
1084
1161
|
const getBySlug = (team, slug) => absentAsUndefined(api.request("get project by slug", (client) => client.GET("/teams/{team_id_or_slug}/projects/{project_slug}", { params: { path: {
|
|
1085
1162
|
team_id_or_slug: String(team),
|
|
1086
1163
|
project_slug: slug
|
|
1087
1164
|
} } }))).pipe(Effect.map((body) => body === void 0 ? void 0 : Schema.decodeUnknownSync(ProjectDetails)(body)));
|
|
1088
|
-
|
|
1089
|
-
* The one project in the team with exactly this name, or undefined.
|
|
1090
|
-
* Reads every page. `q` narrows the listing on the server, and the
|
|
1091
|
-
* exact match here decides. Pattern from Confect
|
|
1092
|
-
* `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
|
|
1093
|
-
* (https://github.com/rjdellecese/confect, ISC license).
|
|
1094
|
-
*/
|
|
1095
|
-
const findByName = (teamId, name) => Effect.gen(function* () {
|
|
1096
|
-
const matches = [];
|
|
1097
|
-
const seenCursors = /* @__PURE__ */ new Set();
|
|
1098
|
-
let cursor;
|
|
1099
|
-
while (true) {
|
|
1100
|
-
const body = yield* api.request("list projects", (client) => client.GET("/teams/{team_id}/projects", { params: {
|
|
1101
|
-
path: { team_id: teamId },
|
|
1102
|
-
query: {
|
|
1103
|
-
q: name,
|
|
1104
|
-
...cursor === void 0 ? {} : { cursor }
|
|
1105
|
-
}
|
|
1106
|
-
} }));
|
|
1107
|
-
const page = Schema.decodeUnknownSync(ProjectPage)(body);
|
|
1108
|
-
matches.push(...page.items.filter((row) => row.name === name));
|
|
1109
|
-
if (!page.pagination.hasMore) break;
|
|
1110
|
-
const next = page.pagination.nextCursor;
|
|
1111
|
-
if (next === void 0 || next === null || seenCursors.has(next)) return yield* new ConvexApiError({
|
|
1112
|
-
operation: "list projects",
|
|
1113
|
-
status: 0,
|
|
1114
|
-
code: "InvalidPagination",
|
|
1115
|
-
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}".`
|
|
1116
|
-
});
|
|
1117
|
-
seenCursors.add(next);
|
|
1118
|
-
cursor = next;
|
|
1119
|
-
}
|
|
1120
|
-
if (matches.length > 1) return yield* new AmbiguousProject({
|
|
1121
|
-
teamId,
|
|
1122
|
-
name,
|
|
1123
|
-
projectIds: matches.map((row) => row.id)
|
|
1124
|
-
});
|
|
1125
|
-
return matches[0];
|
|
1126
|
-
});
|
|
1165
|
+
const findByName = (teamId, name) => findProjectByName(api, teamId, name);
|
|
1127
1166
|
/**
|
|
1128
1167
|
* Brings an existing project to the desired name. The team cannot
|
|
1129
1168
|
* change, so a project in another team fails.
|
|
@@ -1229,17 +1268,11 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1229
1268
|
return yield* converge(existing, teamId, news.name);
|
|
1230
1269
|
}
|
|
1231
1270
|
}
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
...news.deploymentRegion === void 0 ? {} : { deploymentRegion: news.deploymentRegion }
|
|
1238
|
-
}
|
|
1239
|
-
}));
|
|
1240
|
-
const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
|
|
1241
|
-
const details = yield* getById(projectId);
|
|
1242
|
-
if (details === void 0) return yield* new CreatedProjectNotFound({ projectId });
|
|
1271
|
+
const details = yield* createProject(api, teamId, {
|
|
1272
|
+
name: news.name,
|
|
1273
|
+
deploymentType: news.deploymentType ?? "prod",
|
|
1274
|
+
deploymentRegion: news.deploymentRegion
|
|
1275
|
+
});
|
|
1243
1276
|
return yield* converge(details, teamId, news.name);
|
|
1244
1277
|
}),
|
|
1245
1278
|
delete: Effect.fn(function* ({ output }) {
|
|
@@ -1258,11 +1291,15 @@ var Providers = class extends Provider.ProviderCollection()("Convex") {};
|
|
|
1258
1291
|
const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([Project, Deployment])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.orDie);
|
|
1259
1292
|
//#endregion
|
|
1260
1293
|
//#region src/State.ts
|
|
1261
|
-
/**
|
|
1262
|
-
const
|
|
1263
|
-
/**
|
|
1264
|
-
|
|
1265
|
-
|
|
1294
|
+
/** The name of the state project in each team. */
|
|
1295
|
+
const STORE_PROJECT_NAME = "alchemy-state";
|
|
1296
|
+
/**
|
|
1297
|
+
* The actions of the key of one run: the internal queries and mutations of
|
|
1298
|
+
* the store, nothing else. Measured: `deployment:data:view` and
|
|
1299
|
+
* `deployment:data:write` do not run an internal function.
|
|
1300
|
+
*/
|
|
1301
|
+
const RUN_KEY_ACTIONS = ["deployment:functions:runInternalQueries", "deployment:functions:runInternalMutations"];
|
|
1302
|
+
/** The body of POST /api/function, with HTTP 200 for a function error too. */
|
|
1266
1303
|
const Reply = Schema.Union([Schema.Struct({
|
|
1267
1304
|
status: Schema.Literal("success"),
|
|
1268
1305
|
value: Schema.Unknown
|
|
@@ -1270,11 +1307,10 @@ const Reply = Schema.Union([Schema.Struct({
|
|
|
1270
1307
|
status: Schema.Literal("error"),
|
|
1271
1308
|
errorMessage: Schema.String
|
|
1272
1309
|
})]);
|
|
1273
|
-
/** The
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
}));
|
|
1310
|
+
/** The error reply of /api/function for a function that the deployment does not have (recorded). */
|
|
1311
|
+
const MISSING_FUNCTION = /Could not find function for '/;
|
|
1312
|
+
/** HTTP 401 or 403 from the function API: the key is gone or expired (recorded: 403 without a key). */
|
|
1313
|
+
const isUnauthorized = (error) => error.http?.status === 401 || error.http?.status === 403;
|
|
1278
1314
|
const failure = (message) => new StateStoreError({ message: `Convex state: ${message}` });
|
|
1279
1315
|
/** No reply, HTTP 429, or HTTP 5xx: a new attempt can succeed. Every call of the store is idempotent. */
|
|
1280
1316
|
const isTransient = (error) => error.http !== void 0 && (error.http.status === void 0 || error.http.status === 429 || error.http.status >= 500);
|
|
@@ -1303,22 +1339,16 @@ const post = (url, headers, body) => Effect.tryPromise({
|
|
|
1303
1339
|
times: 5
|
|
1304
1340
|
}));
|
|
1305
1341
|
const decode = (schema, what) => (value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => failure(`bad ${what}`)));
|
|
1306
|
-
|
|
1307
|
-
* The deployment URL of a deploy key. The Convex CLI makes the same unofficial
|
|
1308
|
-
* call for every deploy key (convex 1.46 `cli/lib/deploymentSelection.js`).
|
|
1309
|
-
*/
|
|
1310
|
-
const urlForKey = (key) => post("https://api.convex.dev/api/deployment/url_for_key", {}, { deployKey: Redacted.value(key) }).pipe(Effect.flatMap(decode(Schema.String, "deployment URL")));
|
|
1311
|
-
const makeFunctionApi = (url, key) => ({ call: (path, args, componentPath) => post(`${url}/api/${componentPath === void 0 ? "query" : "function"}`, { Authorization: `Convex ${Redacted.value(key)}` }, {
|
|
1312
|
-
...componentPath === void 0 ? {} : { componentPath },
|
|
1342
|
+
const makeFunctionApi = (url, key) => ({ call: (path, args) => post(`${url}/api/function`, { Authorization: `Convex ${Redacted.value(key)}` }, {
|
|
1313
1343
|
path,
|
|
1314
1344
|
args,
|
|
1315
1345
|
format: "json"
|
|
1316
|
-
}).pipe(Effect.flatMap(decode(Reply, `reply of ${path}`))
|
|
1317
|
-
/** The
|
|
1318
|
-
const
|
|
1319
|
-
/** The
|
|
1346
|
+
}).pipe(Effect.flatMap(decode(Reply, `reply of ${path}`))) });
|
|
1347
|
+
/** The value of a reply, or the Convex message, which names the function and the cause, such as a missing key action. */
|
|
1348
|
+
const valueOf = (reply) => reply.status === "success" ? Effect.succeed(reply.value) : Effect.fail(failure(reply.errorMessage.trim()));
|
|
1349
|
+
/** The functions of the store with Alchemy's encoding: the JSON text of encodeState. */
|
|
1320
1350
|
const remoteOf = (api) => {
|
|
1321
|
-
const run = (name, args) => api.call(`state:${name}`, args
|
|
1351
|
+
const run = (name, args) => api.call(`state:${name}`, args).pipe(Effect.flatMap(valueOf));
|
|
1322
1352
|
const strings = (name, args) => run(name, args).pipe(Effect.flatMap(decode(Schema.Array(Schema.String), name)));
|
|
1323
1353
|
const optional = (name, args) => run(name, args).pipe(Effect.flatMap(decode(Schema.NullOr(Schema.String), name)));
|
|
1324
1354
|
const encode = (value) => JSON.stringify(encodeState(value));
|
|
@@ -1364,84 +1394,131 @@ const remoteOf = (api) => {
|
|
|
1364
1394
|
})
|
|
1365
1395
|
};
|
|
1366
1396
|
};
|
|
1367
|
-
|
|
1397
|
+
/** The version that the store answers, or undefined when the deployment has no `state:version`. */
|
|
1398
|
+
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")))));
|
|
1368
1399
|
/**
|
|
1369
|
-
* The
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1400
|
+
* The functions directory of the store app, `src/store/convex` of this
|
|
1401
|
+
* package. This file is `src/State.ts` in a checkout and a chunk in `lib/`
|
|
1402
|
+
* in the installed package, which ships `src` too, so the directory is
|
|
1403
|
+
* `../src/store/convex` from either place.
|
|
1372
1404
|
*/
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1405
|
+
const storeDirectory = () => join(dirname(fileURLToPath(import.meta.url)), "..", "src", "store", "convex");
|
|
1406
|
+
/**
|
|
1407
|
+
* Pushes `store` to the production deployment of the state project with the
|
|
1408
|
+
* installed Convex CLI: no typecheck, no codegen, the key only in the
|
|
1409
|
+
* environment of the CLI.
|
|
1410
|
+
*/
|
|
1411
|
+
const pushStore = (options, deployment) => Effect.gen(function* () {
|
|
1412
|
+
const cli = yield* installedCli(join(options.store, ".."));
|
|
1413
|
+
yield* Effect.logInfo(`Convex state: pushing the store version 1 to ${deployment.name}`);
|
|
1414
|
+
yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) => push(options.spawner, key, {
|
|
1415
|
+
deployment: deployment.name,
|
|
1416
|
+
url: deployment.deploymentUrl,
|
|
1417
|
+
source: {
|
|
1418
|
+
path: options.store,
|
|
1419
|
+
command: [
|
|
1420
|
+
...cli,
|
|
1421
|
+
"deploy",
|
|
1422
|
+
"--typecheck",
|
|
1423
|
+
"disable",
|
|
1424
|
+
"--codegen",
|
|
1425
|
+
"disable"
|
|
1426
|
+
]
|
|
1427
|
+
},
|
|
1428
|
+
env: {},
|
|
1429
|
+
previousEnvNames: [],
|
|
1430
|
+
adopt: false,
|
|
1431
|
+
previewName: void 0
|
|
1432
|
+
}));
|
|
1433
|
+
});
|
|
1434
|
+
/**
|
|
1435
|
+
* Creates the state project of the team. Two first runs can both create one:
|
|
1436
|
+
* the run whose project is not the oldest deletes its own, which holds
|
|
1437
|
+
* nothing yet, and uses the oldest, so that every run ends on one store.
|
|
1438
|
+
*/
|
|
1439
|
+
const createStoreProject = (api, teamId, team) => Effect.gen(function* () {
|
|
1440
|
+
yield* Effect.logInfo(`Convex state: creating the project ${STORE_PROJECT_NAME} in team ${team}`);
|
|
1441
|
+
const created = yield* createProject(api, teamId, {
|
|
1442
|
+
name: STORE_PROJECT_NAME,
|
|
1443
|
+
deploymentType: "prod",
|
|
1444
|
+
deploymentRegion: void 0
|
|
1412
1445
|
});
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1446
|
+
const oldest = (yield* listProjectsByName(api, teamId, STORE_PROJECT_NAME)).reduce((a, b) => b.id < a.id ? b : a, created);
|
|
1447
|
+
if (oldest.id === created.id) return created;
|
|
1448
|
+
yield* Effect.logInfo(`Convex state: another run created ${STORE_PROJECT_NAME} ${oldest.id} first; project ${created.id} is deleted again.`);
|
|
1449
|
+
yield* absentAsUndefined(retryIdempotentWrite(api.requestVoid("delete project", (client) => client.POST("/projects/{project_id}/delete", { params: { path: { project_id: created.id } } }))));
|
|
1450
|
+
return oldest;
|
|
1451
|
+
});
|
|
1452
|
+
/**
|
|
1453
|
+
* The StateService over the `alchemy-state` project of the team: found or
|
|
1454
|
+
* created, its store pushed when it is missing or older, and read and
|
|
1455
|
+
* written with the keys of this run.
|
|
1456
|
+
*/
|
|
1457
|
+
const makeConvexState = (options) => Effect.gen(function* () {
|
|
1458
|
+
const { api, team } = options;
|
|
1459
|
+
const teamId = yield* api.resolveTeamId(team);
|
|
1460
|
+
const project = (yield* findProjectByName(api, teamId, "alchemy-state")) ?? (yield* createStoreProject(api, teamId, team));
|
|
1461
|
+
const deployment = yield* prodDeploymentOf(api, project.id);
|
|
1462
|
+
if (deployment === void 0) return yield* new ProductionDeploymentNotFound({ projectId: project.id });
|
|
1463
|
+
const keys = [];
|
|
1464
|
+
yield* Scope.addFinalizer(options.scope, Effect.suspend(() => Effect.forEach(keys, (key) => deleteDeployKey(api, deployment.name, key), { discard: true })));
|
|
1465
|
+
const mint = createDeployKey(api, deployment.name, "alchemy-state", RUN_KEY_ACTIONS).pipe(Effect.map((key) => {
|
|
1466
|
+
keys.push(key);
|
|
1467
|
+
return makeFunctionApi(deployment.deploymentUrl, key);
|
|
1468
|
+
}), Effect.mapError((error) => failure(`no key for ${deployment.name}: ${error.message}`)));
|
|
1469
|
+
let current = yield* mint;
|
|
1470
|
+
const functions = { call: (path, args) => current.call(path, args).pipe(Effect.catchIf(isUnauthorized, () => mint.pipe(Effect.flatMap((next) => {
|
|
1471
|
+
current = next;
|
|
1472
|
+
return next.call(path, args);
|
|
1473
|
+
})))) };
|
|
1474
|
+
const version = yield* storeVersion(functions);
|
|
1475
|
+
if (version === void 0 || version < 1) {
|
|
1476
|
+
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.`);
|
|
1477
|
+
const now = yield* storeVersion(functions);
|
|
1478
|
+
if (now === void 0 || now < 1) {
|
|
1479
|
+
yield* pushStore(options, deployment);
|
|
1480
|
+
const pushed = yield* storeVersion(functions);
|
|
1481
|
+
if (pushed === void 0 || pushed < 1) return yield* failure(`${deployment.name} answers store version ${pushed ?? "none"} after the push of version 1.`);
|
|
1482
|
+
}
|
|
1483
|
+
} else if (version > 1) yield* Effect.logInfo(`Convex state: the store of team ${team} is version ${version}, newer than version 1 of this package, and it stays.`);
|
|
1484
|
+
const remote = remoteOf(functions);
|
|
1415
1485
|
return {
|
|
1416
1486
|
id: "convex",
|
|
1417
1487
|
getVersion: () => Effect.succeed(STATE_STORE_VERSION),
|
|
1418
|
-
listStacks:
|
|
1419
|
-
listStages:
|
|
1420
|
-
list:
|
|
1421
|
-
get:
|
|
1422
|
-
getReplacedResources:
|
|
1423
|
-
getOutput:
|
|
1424
|
-
set: (request) =>
|
|
1425
|
-
delete: (request) =>
|
|
1426
|
-
deleteStack: (request) =>
|
|
1427
|
-
setOutput: (request) =>
|
|
1488
|
+
listStacks: remote.listStacks,
|
|
1489
|
+
listStages: remote.listStages,
|
|
1490
|
+
list: remote.list,
|
|
1491
|
+
get: remote.get,
|
|
1492
|
+
getReplacedResources: remote.getReplacedResources,
|
|
1493
|
+
getOutput: remote.getOutput,
|
|
1494
|
+
set: (request) => remote.set(request).pipe(Effect.as(request.value)),
|
|
1495
|
+
delete: (request) => remote.delete(request).pipe(Effect.asVoid),
|
|
1496
|
+
deleteStack: (request) => remote.deleteStack(request).pipe(Effect.asVoid),
|
|
1497
|
+
setOutput: (request) => remote.setOutput(request).pipe(Effect.as(request.value))
|
|
1428
1498
|
};
|
|
1429
1499
|
});
|
|
1430
1500
|
/**
|
|
1431
|
-
* Alchemy state in the
|
|
1432
|
-
*
|
|
1433
|
-
* the project
|
|
1434
|
-
* `
|
|
1501
|
+
* Alchemy state in the `alchemy-state` project of `team`, with the team token
|
|
1502
|
+
* in CONVEX_ACCESS_TOKEN or the Convex CLI login. The first use of the run
|
|
1503
|
+
* finds or creates the project and pushes the store when it is missing or
|
|
1504
|
+
* older. `alchemy plan` on another stack never touches Convex.
|
|
1435
1505
|
*/
|
|
1436
|
-
const state = () => Layer.effect(State, Effect.gen(function* () {
|
|
1506
|
+
const state = (options) => Layer.effect(State, Effect.gen(function* () {
|
|
1507
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
1508
|
+
const scope = yield* Scope.Scope;
|
|
1437
1509
|
const context = yield* Effect.context();
|
|
1438
1510
|
return yield* Effect.cached(Effect.gen(function* () {
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1511
|
+
const api = yield* ManagementApi.pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));
|
|
1512
|
+
return yield* makeConvexState({
|
|
1513
|
+
team: options.team,
|
|
1514
|
+
api,
|
|
1515
|
+
spawner,
|
|
1516
|
+
scope,
|
|
1517
|
+
store: storeDirectory()
|
|
1518
|
+
});
|
|
1442
1519
|
}).pipe(Effect.provideContext(context), Effect.orDie));
|
|
1443
1520
|
}));
|
|
1444
1521
|
//#endregion
|
|
1445
|
-
export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch,
|
|
1522
|
+
export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, fromConfig, fromValues, parseDeployOutput, providers, state };
|
|
1446
1523
|
|
|
1447
1524
|
//# sourceMappingURL=index.js.map
|