@samebase/alchemy-convex 0.4.1 → 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 +26 -0
- package/lib/index.d.ts +16 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +334 -71
- package/lib/index.js.map +1 -1
- package/package.json +3 -1
- package/src/Deployment.ts +9 -2
- package/src/Project.ts +125 -88
- package/src/Push.ts +74 -46
- package/src/State.ts +435 -0
- package/src/index.ts +1 -0
- package/src/store/convex/_generated/api.d.ts +51 -0
- package/src/store/convex/_generated/api.js +23 -0
- package/src/store/convex/_generated/dataModel.d.ts +60 -0
- package/src/store/convex/_generated/server.d.ts +162 -0
- package/src/store/convex/_generated/server.js +101 -0
- package/src/store/convex/schema.ts +19 -0
- package/src/store/convex/state.ts +219 -0
- package/src/store/convex/version.ts +6 -0
- package/src/store/package.json +8 -0
package/README.md
CHANGED
|
@@ -90,6 +90,32 @@ reads them through the stack's config provider (process env, `.env`, stack secre
|
|
|
90
90
|
A key that does not fit the deployment is not used: the provider uses the access token. To pass
|
|
91
91
|
values explicitly: `Convex.providers(Convex.fromValues({ accessToken, deployKey }))`.
|
|
92
92
|
|
|
93
|
+
## State in Convex
|
|
94
|
+
|
|
95
|
+
`state: Convex.state({ team })` keeps the Alchemy state of every stack and stage of a Convex team in
|
|
96
|
+
one project of that team, `alchemy-state`, so a repository holds one secret and a stack can create
|
|
97
|
+
and destroy its own project. [docs/design/convex-state.md](docs/design/convex-state.md) records the
|
|
98
|
+
decisions and the measurements.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
export default Alchemy.Stack(
|
|
102
|
+
process.env["GITHUB_REPOSITORY"] ?? "my-org/my-app", // the stack name: the repository
|
|
103
|
+
{ providers: Convex.providers(), state: Convex.state({ team: "my-team" }) },
|
|
104
|
+
Effect.gen(function* () {
|
|
105
|
+
/* ... */
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Every run needs the team token in `CONVEX_ACCESS_TOKEN`, or the Convex CLI login on a laptop. The
|
|
111
|
+
first run of a team creates the project `alchemy-state` and pushes the store functions to its
|
|
112
|
+
production deployment with the Convex CLI of the app; a later version of this package pushes a
|
|
113
|
+
newer store when it finds an older one, and never an older one over a newer one. Each run makes a
|
|
114
|
+
deploy key of that deployment that can only run the store's internal functions, and deletes it at
|
|
115
|
+
the end. Rows are in the tables `resources` and `outputs` in the dashboard, under the stack name.
|
|
116
|
+
The store has no lock: run one deploy of a stage at a time, such as with a CI concurrency group.
|
|
117
|
+
The names of the store functions are a wire contract: never renamed or removed.
|
|
118
|
+
|
|
93
119
|
## Resources
|
|
94
120
|
|
|
95
121
|
- `Convex.Project`: `{ team, name, deploymentRegion?, deploymentType? }` creates a project with its
|
package/lib/index.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ import * as Redacted from "effect/Redacted";
|
|
|
5
5
|
import * as Schema from "effect/Schema";
|
|
6
6
|
import { Resource } from "alchemy";
|
|
7
7
|
import * as Provider from "alchemy/Provider";
|
|
8
|
-
import "effect/process/ChildProcessSpawner";
|
|
8
|
+
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
|
|
9
9
|
import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
|
|
10
|
+
import "effect/Scope";
|
|
11
|
+
import { State, StateStoreError } from "alchemy/State";
|
|
10
12
|
import { components } from "@convex-dev/platform/managementApi";
|
|
11
13
|
//#region src/Credentials.d.ts
|
|
12
14
|
interface ResolvedCredentials {
|
|
@@ -339,5 +341,18 @@ export declare class ProjectNotFound extends ProjectNotFound_base {
|
|
|
339
341
|
get message(): string;
|
|
340
342
|
}
|
|
341
343
|
//#endregion
|
|
344
|
+
//#region src/State.d.ts
|
|
345
|
+
/** The name of the state project in each team. */
|
|
346
|
+
export declare const STORE_PROJECT_NAME = "alchemy-state";
|
|
347
|
+
/**
|
|
348
|
+
* Alchemy state in the `alchemy-state` project of `team`, with the team token
|
|
349
|
+
* in CONVEX_ACCESS_TOKEN or the Convex CLI login. The first use of the run
|
|
350
|
+
* finds or creates the project and pushes the store when it is missing or
|
|
351
|
+
* older. `alchemy plan` on another stack never touches Convex.
|
|
352
|
+
*/
|
|
353
|
+
export declare const state: (options: {
|
|
354
|
+
readonly team: string | number;
|
|
355
|
+
}) => Layer.Layer<State, never, ChildProcessSpawner.ChildProcessSpawner>;
|
|
356
|
+
//#endregion
|
|
342
357
|
export type { DeploymentAttributes, DeploymentProps, DeploymentRegion, EnvValue, ProjectAttributes, ProjectProps, Source };
|
|
343
358
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Project.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Project.ts","../src/State.ts"],"mappings":";;;;;;;;;;;;;UA6BiB;;WAEN,aAAa,SAAS;;WAEtB;;UAGM;;WAEN,aAAa,OAAO,OAAO;;WAE3B,gBAAgB,OAAO;;WAEvB,WAAW,OAAO,OAAO,SAAS;;;qBAGhC,oBAAoB;;qBAKpB;;qBAGA;;;;qBAOA,yBAAyB;;;;;;;;;qBAiCzB,kBAAiB,MAAM,MAAM;;qBA0B7B,aAAc;WAChB,cAAc,SAAS;WACvB,YAAY,SAAS;MAC5B,MAAM,MAAM;;;KC3GJ,mBAAmB,kBAAkB;KACrC,mBAAmB,kBAAkB;;UAGhC,YAAY;WAClB,OAAO;WACP;WACA,UAAU;;;;;;;;;;;;;qBAQR,uBAAuB;UAuBnB;;WAEN,UAAU,GACjB,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;WAEb,cACP,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,0BACxC,OAAO,aAAa;;WAEhB,oBAAoB,GAC3B,mBACA,6BACA,WAAW,SAAS,kBACpB,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;WAEb,gBAAgB,0BAA0B,OAAO,eAAe;;;;;;;;WAQhE,oBAAoB,mBAAmB,OAAO;;;;;WAK9C,YAAY,OAAO,SAAS,qBAAqB;;;qBAM/C,sBAAsB;;;;qBClFtB,kBAAkB;;;;;;qBAOlB,YAAa,cAAa,MAAM,MAAM,iBAA2B,MAAA,MAAA,8CAAA,mCAAA,oDAAA;;;;KC+BlE,oBAAoB,SAAS;;;;;KAM7B;;WAIG;;;;;;;;WAQA;;;;;;;WAOA;;;;;;qBAkCF,0BAA0B;MAIxB;;;;;;qBAMF,0BAA0B;MAIxB;;;;;;;;;qBAMF,uBAAuB;MAMrB;;;;;;;qBAQF,2BAA2B;MAIzB;;;;;;;qBAMF,oCAAoC;MAIlC;;;;;;qBAMF,kCAAkC;MAIhC;;;qBAuCF,oBACX;EACG;EAAa;;;;UChIR;;WAEC;aAAoB;;;;;;;;WAOpB,SAAS;;WAET,MAAM,SAAS,eAAe;;KAG7B,mBACP;;WAEU;MAEV;;WAEU;;;;;WAKA;;;UAIL;WACC;WACA,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;UAGM,6BAA6B;;WAEnC;;WAEA;;YAGC,aAAa,8BAEvB,iBACA,6BAEA;qBAEW,8BAAU,cAAA;;;;;;;;;qBAKV,iCAAiC;MAU/B;;;;;;qBAMF,qCAAqC;MAInC;;;;;;;;;qBAMF,4BAA4B;MAS1B;;;;;KC5IH,mBAAmB;KAEnB;;WAGG;;WAEA;;WAEA,mBAAmB;;WAEnB;;;WAIA;;WAEA;;UAGE;WACN;WACA;WACA;WACA;WACA;WACA;;YAGC,UAAU,2BAA2B,cAAc,0BAA0B;qBAC5E,2BAAO,cAAA;;;;;;;qBAGP,0BAA0B;MAIxB;;;;;;;;;;;;qBASF,gCAAgC;MAS9B;;;;;;;;qBAOF,yBAAyB;MAKvB;;;;;;qBAMF,+BAA+B;MAI7B;;;;;;;qBAMF,wBAAwB;MAItB;;;;;qBChDF;;;;;;;qBA8UA,QAAS;WACX;MACP,MAAM,MAAM,cAAc,oBAAoB"}
|
package/lib/index.js
CHANGED
|
@@ -25,6 +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 { fileURLToPath } from "node:url";
|
|
29
|
+
import * as Scope from "effect/Scope";
|
|
30
|
+
import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveState } from "alchemy/State";
|
|
28
31
|
//#region src/Credentials.ts
|
|
29
32
|
var Credentials = class extends Context.Service()("@samebase/alchemy-convex/Credentials") {};
|
|
30
33
|
/** Environment variable for the access token, read by {@link fromConfig}. */
|
|
@@ -226,7 +229,7 @@ const sourceOf = (source) => typeof source === "string" ? {
|
|
|
226
229
|
};
|
|
227
230
|
/** Lifetime of a push key. Long enough for a slow push with index backfills. */
|
|
228
231
|
const PUSH_KEY_LIFETIME_MS = 72e5;
|
|
229
|
-
/** The only actions a
|
|
232
|
+
/** The only actions a key for `convex deploy` gets. */
|
|
230
233
|
const PUSH_KEY_ACTIONS = [
|
|
231
234
|
"deployment:deploy",
|
|
232
235
|
"deployment:env:view",
|
|
@@ -328,7 +331,7 @@ const parseDeployOutput = (text) => {
|
|
|
328
331
|
};
|
|
329
332
|
/** The last non-empty lines of `text`, without escape codes and with every secret replaced. */
|
|
330
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");
|
|
331
|
-
const valueOf = (value) => Redacted.isRedacted(value) ? Redacted.value(value) : value;
|
|
334
|
+
const valueOf$1 = (value) => Redacted.isRedacted(value) ? Redacted.value(value) : value;
|
|
332
335
|
/** The app directory: the nearest directory at or above the parent of `source` with a package.json. */
|
|
333
336
|
const appDirectoryOf = (source) => Effect.tryPromise({
|
|
334
337
|
try: async () => {
|
|
@@ -396,7 +399,7 @@ const pushHash = (options) => Effect.gen(function* () {
|
|
|
396
399
|
lockfile: true
|
|
397
400
|
}
|
|
398
401
|
})));
|
|
399
|
-
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);
|
|
400
403
|
return createHash("sha256").update(JSON.stringify({
|
|
401
404
|
files,
|
|
402
405
|
deployment: options.deployment,
|
|
@@ -423,29 +426,41 @@ const cliEnvironment = (home) => ({
|
|
|
423
426
|
XDG_CONFIG_HOME: home
|
|
424
427
|
});
|
|
425
428
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
* 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.
|
|
430
432
|
*/
|
|
431
|
-
const
|
|
433
|
+
const createDeployKey = (api, deployment, name, allowedActions) => Effect.gen(function* () {
|
|
432
434
|
const now = yield* Clock.currentTimeMillis;
|
|
433
435
|
const created = yield* api.request("create deploy key", (client) => client.POST("/deployments/{deployment_name}/create_deploy_key", {
|
|
434
436
|
params: { path: { deployment_name: deployment } },
|
|
435
437
|
body: {
|
|
436
|
-
name
|
|
437
|
-
...
|
|
438
|
+
name,
|
|
439
|
+
...allowedActions === void 0 ? {} : { allowedActions: [...allowedActions] },
|
|
438
440
|
expiresAt: now + PUSH_KEY_LIFETIME_MS
|
|
439
441
|
}
|
|
440
442
|
}));
|
|
441
443
|
return Redacted.make(Schema.decodeUnknownSync(CreatedDeployKey)(created).deployKey);
|
|
442
|
-
})
|
|
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* () {
|
|
443
451
|
if (yield* api.isCredentialToken(Redacted.value(key))) return;
|
|
444
452
|
yield* absentAsUndefined(retryIdempotentWrite(api.requestVoid("delete deploy key", (client) => client.POST("/deployments/{deployment_name}/delete_deploy_key", {
|
|
445
453
|
params: { path: { deployment_name: deployment } },
|
|
446
454
|
body: { id: Redacted.value(key) }
|
|
447
455
|
}))));
|
|
448
|
-
}).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));
|
|
449
464
|
/** Every variable of the deployment with its value. */
|
|
450
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));
|
|
451
466
|
/** One call that sets variables, or removes those whose value is null. */
|
|
@@ -486,15 +501,15 @@ const push = (spawner, key, options) => Effect.gen(function* () {
|
|
|
486
501
|
} : yield* retryIdempotentWrite(Effect.gen(function* () {
|
|
487
502
|
const current = yield* listVariables(options.url, key);
|
|
488
503
|
if (!options.adopt) {
|
|
489
|
-
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);
|
|
490
505
|
if (taken.length > 0) return yield* new VariablesOwnedBySomeoneElse({
|
|
491
506
|
deployment: options.deployment,
|
|
492
507
|
names: taken
|
|
493
508
|
});
|
|
494
509
|
}
|
|
495
|
-
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]) => ({
|
|
496
511
|
name,
|
|
497
|
-
value: valueOf(value)
|
|
512
|
+
value: valueOf$1(value)
|
|
498
513
|
})), ...removed.filter((name) => Object.hasOwn(current, name)).map((name) => ({
|
|
499
514
|
name,
|
|
500
515
|
value: null
|
|
@@ -933,7 +948,7 @@ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function
|
|
|
933
948
|
}) : access.kind === "previewKey" ? push(spawner, access.key, {
|
|
934
949
|
...options,
|
|
935
950
|
previewName: target.previewName
|
|
936
|
-
}) : 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, {
|
|
937
952
|
...options,
|
|
938
953
|
previewName: void 0
|
|
939
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));
|
|
@@ -1041,7 +1056,9 @@ const DeploymentRow = Schema.Struct({
|
|
|
1041
1056
|
name: Schema.String,
|
|
1042
1057
|
deploymentType: Schema.String,
|
|
1043
1058
|
isDefault: Schema.Boolean,
|
|
1044
|
-
deploymentUrl: Schema.String
|
|
1059
|
+
deploymentUrl: Schema.String,
|
|
1060
|
+
/** Absent or null until the first push. */
|
|
1061
|
+
lastDeployTime: Schema.optionalKey(Schema.NullOr(Schema.Number))
|
|
1045
1062
|
});
|
|
1046
1063
|
const DeploymentRows = Schema.Array(DeploymentRow);
|
|
1047
1064
|
/**
|
|
@@ -1067,10 +1084,71 @@ const checkCreateTimeProps = (projectId, olds, news) => {
|
|
|
1067
1084
|
};
|
|
1068
1085
|
/** True when `team`, a slug or a numeric id, names the team of the project. */
|
|
1069
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
|
+
});
|
|
1070
1149
|
const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
1071
1150
|
const api = yield* ManagementApi;
|
|
1072
|
-
const
|
|
1073
|
-
const attributesOf = (details) => prodDeployment(details.id).pipe(Effect.map((prod) => ({
|
|
1151
|
+
const attributesOf = (details) => prodDeploymentOf(api, details.id).pipe(Effect.map((prod) => ({
|
|
1074
1152
|
projectId: details.id,
|
|
1075
1153
|
slug: details.slug,
|
|
1076
1154
|
name: details.name,
|
|
@@ -1078,51 +1156,13 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1078
1156
|
prodDeploymentName: prod?.name ?? details.prodDeploymentName ?? void 0,
|
|
1079
1157
|
prodDeploymentUrl: prod?.deploymentUrl
|
|
1080
1158
|
})));
|
|
1081
|
-
const getById = (projectId) =>
|
|
1159
|
+
const getById = (projectId) => getProjectById(api, projectId);
|
|
1082
1160
|
/** The project with this slug in the team, or undefined. Official endpoint; the team can be a slug. */
|
|
1083
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: {
|
|
1084
1162
|
team_id_or_slug: String(team),
|
|
1085
1163
|
project_slug: slug
|
|
1086
1164
|
} } }))).pipe(Effect.map((body) => body === void 0 ? void 0 : Schema.decodeUnknownSync(ProjectDetails)(body)));
|
|
1087
|
-
|
|
1088
|
-
* The one project in the team with exactly this name, or undefined.
|
|
1089
|
-
* Reads every page. `q` narrows the listing on the server, and the
|
|
1090
|
-
* exact match here decides. Pattern from Confect
|
|
1091
|
-
* `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
|
|
1092
|
-
* (https://github.com/rjdellecese/confect, ISC license).
|
|
1093
|
-
*/
|
|
1094
|
-
const findByName = (teamId, name) => Effect.gen(function* () {
|
|
1095
|
-
const matches = [];
|
|
1096
|
-
const seenCursors = /* @__PURE__ */ new Set();
|
|
1097
|
-
let cursor;
|
|
1098
|
-
while (true) {
|
|
1099
|
-
const body = yield* api.request("list projects", (client) => client.GET("/teams/{team_id}/projects", { params: {
|
|
1100
|
-
path: { team_id: teamId },
|
|
1101
|
-
query: {
|
|
1102
|
-
q: name,
|
|
1103
|
-
...cursor === void 0 ? {} : { cursor }
|
|
1104
|
-
}
|
|
1105
|
-
} }));
|
|
1106
|
-
const page = Schema.decodeUnknownSync(ProjectPage)(body);
|
|
1107
|
-
matches.push(...page.items.filter((row) => row.name === name));
|
|
1108
|
-
if (!page.pagination.hasMore) break;
|
|
1109
|
-
const next = page.pagination.nextCursor;
|
|
1110
|
-
if (next === void 0 || next === null || seenCursors.has(next)) return yield* new ConvexApiError({
|
|
1111
|
-
operation: "list projects",
|
|
1112
|
-
status: 0,
|
|
1113
|
-
code: "InvalidPagination",
|
|
1114
|
-
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}".`
|
|
1115
|
-
});
|
|
1116
|
-
seenCursors.add(next);
|
|
1117
|
-
cursor = next;
|
|
1118
|
-
}
|
|
1119
|
-
if (matches.length > 1) return yield* new AmbiguousProject({
|
|
1120
|
-
teamId,
|
|
1121
|
-
name,
|
|
1122
|
-
projectIds: matches.map((row) => row.id)
|
|
1123
|
-
});
|
|
1124
|
-
return matches[0];
|
|
1125
|
-
});
|
|
1165
|
+
const findByName = (teamId, name) => findProjectByName(api, teamId, name);
|
|
1126
1166
|
/**
|
|
1127
1167
|
* Brings an existing project to the desired name. The team cannot
|
|
1128
1168
|
* change, so a project in another team fails.
|
|
@@ -1228,17 +1268,11 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
1228
1268
|
return yield* converge(existing, teamId, news.name);
|
|
1229
1269
|
}
|
|
1230
1270
|
}
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
...news.deploymentRegion === void 0 ? {} : { deploymentRegion: news.deploymentRegion }
|
|
1237
|
-
}
|
|
1238
|
-
}));
|
|
1239
|
-
const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
|
|
1240
|
-
const details = yield* getById(projectId);
|
|
1241
|
-
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
|
+
});
|
|
1242
1276
|
return yield* converge(details, teamId, news.name);
|
|
1243
1277
|
}),
|
|
1244
1278
|
delete: Effect.fn(function* ({ output }) {
|
|
@@ -1256,6 +1290,235 @@ var Providers = class extends Provider.ProviderCollection()("Convex") {};
|
|
|
1256
1290
|
*/
|
|
1257
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);
|
|
1258
1292
|
//#endregion
|
|
1259
|
-
|
|
1293
|
+
//#region src/State.ts
|
|
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. */
|
|
1303
|
+
const Reply = Schema.Union([Schema.Struct({
|
|
1304
|
+
status: Schema.Literal("success"),
|
|
1305
|
+
value: Schema.Unknown
|
|
1306
|
+
}), Schema.Struct({
|
|
1307
|
+
status: Schema.Literal("error"),
|
|
1308
|
+
errorMessage: Schema.String
|
|
1309
|
+
})]);
|
|
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;
|
|
1314
|
+
const failure = (message) => new StateStoreError({ message: `Convex state: ${message}` });
|
|
1315
|
+
/** No reply, HTTP 429, or HTTP 5xx: a new attempt can succeed. Every call of the store is idempotent. */
|
|
1316
|
+
const isTransient = (error) => error.http !== void 0 && (error.http.status === void 0 || error.http.status === 429 || error.http.status >= 500);
|
|
1317
|
+
const post = (url, headers, body) => Effect.tryPromise({
|
|
1318
|
+
try: () => fetch(url, {
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
headers: {
|
|
1321
|
+
"Content-Type": "application/json",
|
|
1322
|
+
...headers
|
|
1323
|
+
},
|
|
1324
|
+
body: JSON.stringify(body)
|
|
1325
|
+
}),
|
|
1326
|
+
catch: () => new StateStoreError({
|
|
1327
|
+
message: `Convex state: POST ${url} failed`,
|
|
1328
|
+
http: {}
|
|
1329
|
+
})
|
|
1330
|
+
}).pipe(Effect.flatMap((response) => response.ok || response.status === 560 ? Effect.tryPromise({
|
|
1331
|
+
try: () => response.json(),
|
|
1332
|
+
catch: () => failure(`${url} gave no JSON`)
|
|
1333
|
+
}) : Effect.fail(new StateStoreError({
|
|
1334
|
+
message: `Convex state: POST ${url} failed with HTTP ${response.status}`,
|
|
1335
|
+
http: { status: response.status }
|
|
1336
|
+
}))), Effect.retry({
|
|
1337
|
+
while: isTransient,
|
|
1338
|
+
schedule: Schedule.exponential("100 millis").pipe(Schedule.jittered),
|
|
1339
|
+
times: 5
|
|
1340
|
+
}));
|
|
1341
|
+
const decode = (schema, what) => (value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => failure(`bad ${what}`)));
|
|
1342
|
+
const makeFunctionApi = (url, key) => ({ call: (path, args) => post(`${url}/api/function`, { Authorization: `Convex ${Redacted.value(key)}` }, {
|
|
1343
|
+
path,
|
|
1344
|
+
args,
|
|
1345
|
+
format: "json"
|
|
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. */
|
|
1350
|
+
const remoteOf = (api) => {
|
|
1351
|
+
const run = (name, args) => api.call(`state:${name}`, args).pipe(Effect.flatMap(valueOf));
|
|
1352
|
+
const strings = (name, args) => run(name, args).pipe(Effect.flatMap(decode(Schema.Array(Schema.String), name)));
|
|
1353
|
+
const optional = (name, args) => run(name, args).pipe(Effect.flatMap(decode(Schema.NullOr(Schema.String), name)));
|
|
1354
|
+
const encode = (value) => JSON.stringify(encodeState(value));
|
|
1355
|
+
return {
|
|
1356
|
+
listStacks: () => strings("stacks", {}),
|
|
1357
|
+
listStages: (stack) => strings("stages", { stack }),
|
|
1358
|
+
list: (r) => strings("list", {
|
|
1359
|
+
stack: r.stack,
|
|
1360
|
+
stage: r.stage
|
|
1361
|
+
}),
|
|
1362
|
+
get: (r) => optional("get", {
|
|
1363
|
+
stack: r.stack,
|
|
1364
|
+
stage: r.stage,
|
|
1365
|
+
fqn: r.fqn
|
|
1366
|
+
}).pipe(Effect.map((text) => text === null ? void 0 : JSON.parse(text, reviveState))),
|
|
1367
|
+
getReplacedResources: (r) => strings("replaced", {
|
|
1368
|
+
stack: r.stack,
|
|
1369
|
+
stage: r.stage
|
|
1370
|
+
}).pipe(Effect.map((texts) => texts.map((text) => JSON.parse(text, reviveState)))),
|
|
1371
|
+
getOutput: (r) => optional("getOutput", {
|
|
1372
|
+
stack: r.stack,
|
|
1373
|
+
stage: r.stage
|
|
1374
|
+
}).pipe(Effect.map((text) => text === null ? void 0 : JSON.parse(text, reviveState))),
|
|
1375
|
+
set: (r) => run("set", {
|
|
1376
|
+
stack: r.stack,
|
|
1377
|
+
stage: r.stage,
|
|
1378
|
+
fqn: r.fqn,
|
|
1379
|
+
value: encode(r.value)
|
|
1380
|
+
}),
|
|
1381
|
+
delete: (r) => run("remove", {
|
|
1382
|
+
stack: r.stack,
|
|
1383
|
+
stage: r.stage,
|
|
1384
|
+
fqn: r.fqn
|
|
1385
|
+
}),
|
|
1386
|
+
deleteStack: (r) => run("removeStack", {
|
|
1387
|
+
stack: r.stack,
|
|
1388
|
+
stage: r.stage
|
|
1389
|
+
}),
|
|
1390
|
+
setOutput: (r) => run("setOutput", {
|
|
1391
|
+
stack: r.stack,
|
|
1392
|
+
stage: r.stage,
|
|
1393
|
+
value: encode(r.value)
|
|
1394
|
+
})
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
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")))));
|
|
1399
|
+
/**
|
|
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.
|
|
1404
|
+
*/
|
|
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
|
|
1445
|
+
});
|
|
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);
|
|
1485
|
+
return {
|
|
1486
|
+
id: "convex",
|
|
1487
|
+
getVersion: () => Effect.succeed(STATE_STORE_VERSION),
|
|
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))
|
|
1498
|
+
};
|
|
1499
|
+
});
|
|
1500
|
+
/**
|
|
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.
|
|
1505
|
+
*/
|
|
1506
|
+
const state = (options) => Layer.effect(State, Effect.gen(function* () {
|
|
1507
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
1508
|
+
const scope = yield* Scope.Scope;
|
|
1509
|
+
const context = yield* Effect.context();
|
|
1510
|
+
return yield* Effect.cached(Effect.gen(function* () {
|
|
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
|
+
});
|
|
1519
|
+
}).pipe(Effect.provideContext(context), Effect.orDie));
|
|
1520
|
+
}));
|
|
1521
|
+
//#endregion
|
|
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 };
|
|
1260
1523
|
|
|
1261
1524
|
//# sourceMappingURL=index.js.map
|