@samebase/alchemy-convex 0.2.0 → 0.4.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/src/DeployKey.ts DELETED
@@ -1,332 +0,0 @@
1
- // Convex.DeployKey: a deploy key for one deployment, such as the key
2
- // `npx convex deploy` reads from CONVEX_DEPLOY_KEY.
3
- //
4
- // Convex returns the secret only once, from create_deploy_key, and the
5
- // response has no id. The listing shows each key with a numeric id and a
6
- // name: the requested name, or, when a listed key already has that name, the
7
- // requested name plus a suffix such as " (870993b4-ffe6-4911-adbd-e29f7fd712f2)".
8
- //
9
- // One resource must never revoke the key of another resource:
10
- // - The requested name is the `name` prop plus a hash of the resource's fqn
11
- // and instance id, so two resources with the same `name` request
12
- // different names.
13
- // - After the create, exactly one listed key must have the requested name.
14
- // Otherwise the create fails with a typed error that lists the keys. State
15
- // keeps that key's numeric id.
16
- // - Delete sends the secret as `id`, so it can revoke only the key that this
17
- // resource created. It never deletes by a listed name.
18
- // - With an OAuth token as the Management API credential, Convex returns
19
- // that token as the "new" key. The provider never stores, revokes, or
20
- // deletes such a key: the create fails with DeployKeyIsCredential.
21
- // - Delete runs only for a key with a `keyId` in state. A 0.1.x key, which
22
- // has none, stays in Convex with a warning.
23
- // A key cannot change after creation, so every property change is a replacement.
24
- import { createHash } from "node:crypto";
25
- import { Resource } from "alchemy";
26
- import { havePropsChanged, isResolved } from "alchemy/Diff";
27
- import * as Provider from "alchemy/Provider";
28
- import * as Effect from "effect/Effect";
29
- import * as Redacted from "effect/Redacted";
30
- import * as Schema from "effect/Schema";
31
- import type { components } from "@convex-dev/platform/managementApi";
32
- import {
33
- absentAsUndefined,
34
- type ConvexApiError,
35
- ManagementApi,
36
- retryIdempotentWrite,
37
- } from "./ManagementApi.ts";
38
- import type { Providers } from "./Providers.ts";
39
-
40
- /** Actions a deploy key can be limited to, from the generated schema. */
41
- export type DeployKeyAction = NonNullable<
42
- components["schemas"]["PlatformCreateDeployKeyArgs"]["allowedActions"]
43
- >[number];
44
-
45
- /**
46
- * True when `listedName` is how Convex lists a key created as `name`: the
47
- * requested name itself, or the requested name, a space, and one
48
- * parenthesized suffix without spaces. The suffix check keeps `my-key` from
49
- * matching a key created as `my-key (old)`.
50
- */
51
- const isListedAs = (listedName: string, name: string) => {
52
- const prefix = `${name} (`;
53
- return (
54
- listedName === name ||
55
- (listedName.startsWith(prefix) && /^[^\s()]+\)$/.test(listedName.slice(prefix.length)))
56
- );
57
- };
58
-
59
- /**
60
- * True when the listing still has the key in state: by numeric id, or by
61
- * listed name for state written by 0.1.x, which has no id.
62
- */
63
- export const isKeyListed = (
64
- entries: ReadonlyArray<typeof ListedKey.Type>,
65
- key: { readonly keyId: number | undefined; readonly uniqueName: string },
66
- ): boolean =>
67
- entries.some((entry) =>
68
- key.keyId === undefined ? entry.name === key.uniqueName : entry.id === key.keyId,
69
- );
70
-
71
- /** The listed keys that Convex shows for a key created as `name`. */
72
- export const keysListedAs = (
73
- entries: ReadonlyArray<typeof ListedKey.Type>,
74
- name: string,
75
- ): ReadonlyArray<typeof ListedKey.Type> => entries.filter((entry) => isListedAs(entry.name, name));
76
-
77
- /**
78
- * The name that a resource instance requests: `name`, a dash, and the first
79
- * 12 hex characters of sha256("<fqn>:<instanceId>"). Two resources, or the
80
- * old and the new generation of one replaced resource, never request the
81
- * same name. Pattern from Confect `packages/alchemy/src/internal/ResourceIdentity.ts`
82
- * (https://github.com/rjdellecese/confect, ISC license).
83
- */
84
- export const requestedKeyName = (name: string, fqn: string, instanceId: string): string =>
85
- `${name}-${createHash("sha256").update(`${fqn}:${instanceId}`).digest("hex").slice(0, 12)}`;
86
-
87
- /** One row of list_deploy_keys and list_preview_deploy_keys. Only the fields the providers use. */
88
- export const ListedKey = Schema.Struct({
89
- id: Schema.Number,
90
- name: Schema.String,
91
- });
92
-
93
- /** GET /deployments/{deployment_name}/list_deploy_keys */
94
- export const DeployKeyList = Schema.Array(ListedKey);
95
-
96
- /** POST /deployments/{deployment_name}/create_deploy_key */
97
- export const CreatedDeployKey = Schema.Struct({ deployKey: Schema.String });
98
-
99
- /**
100
- * A key cannot be tied to one resource: before the create, listed keys
101
- * already have the requested name, or after the create, not exactly one has it.
102
- */
103
- export class DeployKeyRecoveryRequired extends Schema.TaggedError<DeployKeyRecoveryRequired>()(
104
- "DeployKeyRecoveryRequired",
105
- {
106
- /** Where the keys live, such as `deployment happy-otter-123` or `project 3145389`. */
107
- target: Schema.String,
108
- /** The requested name. */
109
- name: Schema.String,
110
- /** The listed keys with the requested name. */
111
- keys: Schema.Array(ListedKey),
112
- /** True when this run created a key before the listing check failed. */
113
- afterCreate: Schema.Boolean,
114
- },
115
- ) {
116
- override get message() {
117
- const listed =
118
- this.keys.length === 0
119
- ? "no key"
120
- : this.keys.map((key) => `"${key.name}" (id ${key.id})`).join(", ");
121
- return this.afterCreate
122
- ? `Convex created deploy key "${this.name}" on ${this.target}, but the listing shows ${listed} with that name, not exactly one, so this resource cannot track the new key. Alchemy does not revoke it. Delete the keys with that name in the Convex dashboard, then run the deploy again.`
123
- : `${this.target} already lists ${listed} with the name "${this.name}", but Alchemy state has no such key. An interrupted run probably created it, and its secret is lost. Delete these keys in the Convex dashboard, then run the deploy again.`;
124
- }
125
- }
126
-
127
- /**
128
- * Convex returned the Management API credential itself as the new key. This
129
- * happens with an OAuth token: Convex creates no key. A delete of the
130
- * returned secret would revoke the credential, so the resource does not keep it.
131
- */
132
- export class DeployKeyIsCredential extends Schema.TaggedError<DeployKeyIsCredential>()(
133
- "DeployKeyIsCredential",
134
- { target: Schema.String, name: Schema.String },
135
- ) {
136
- override get message() {
137
- return `Convex returned the Management API credential itself as deploy key "${this.name}" on ${this.target}. With an OAuth token, Convex creates no new key, and deleting the returned key would revoke the OAuth token. Alchemy does not keep it. Use a team access token in CONVEX_ACCESS_TOKEN, or the Convex CLI login.`;
138
- }
139
- }
140
-
141
- /**
142
- * Creates a key named `name` and ties it to the caller. Fails before the
143
- * create when keys with that name already exist. After the create, fails
144
- * when the secret is the Management API credential itself, or when the
145
- * listing does not show exactly one key with that name. It never revokes a
146
- * key: the returned secret can be a shared credential.
147
- */
148
- export const createTrackedKey = (options: {
149
- readonly target: string;
150
- readonly name: string;
151
- readonly list: Effect.Effect<ReadonlyArray<typeof ListedKey.Type>, ConvexApiError>;
152
- readonly create: Effect.Effect<string, ConvexApiError>;
153
- readonly isCredentialToken: (secret: string) => Effect.Effect<boolean>;
154
- }) =>
155
- Effect.gen(function* () {
156
- const { target, name } = options;
157
- const existing = keysListedAs(yield* options.list, name);
158
- if (existing.length > 0) {
159
- return yield* new DeployKeyRecoveryRequired({
160
- target,
161
- name,
162
- keys: existing,
163
- afterCreate: false,
164
- });
165
- }
166
- const secret = yield* options.create;
167
- if (yield* options.isCredentialToken(secret)) {
168
- return yield* new DeployKeyIsCredential({ target, name });
169
- }
170
- const listed = keysListedAs(yield* options.list, name);
171
- const [only] = listed;
172
- if (only === undefined || listed.length !== 1) {
173
- return yield* new DeployKeyRecoveryRequired({
174
- target,
175
- name,
176
- keys: listed,
177
- afterCreate: true,
178
- });
179
- }
180
- return { keyId: only.id, uniqueName: only.name, secret: Redacted.make(secret) };
181
- });
182
-
183
- /**
184
- * The secret to send to a delete call, or undefined when a delete is not
185
- * safe. Only a key with a `keyId` in state is known to be a new key that
186
- * this resource owns: createTrackedKey found exactly one new listed key, and
187
- * the secret was not the credential. State from 0.1.x has no `keyId`, and its
188
- * secret can be an OAuth token that Convex returned as a key, so a delete
189
- * could revoke that OAuth token. Such a key stays, and a warning names it.
190
- */
191
- export const secretToDelete = (
192
- key: {
193
- readonly keyId: number | undefined;
194
- readonly secret: Redacted.Redacted<string> | undefined;
195
- },
196
- label: string,
197
- ) =>
198
- Effect.gen(function* () {
199
- if (key.keyId === undefined || !Redacted.isRedacted(key.secret)) {
200
- yield* Effect.logWarning(
201
- `${label} has no verified key id and secret in state (state from 0.1.x, for example), so Alchemy does not delete it. Delete it in the Convex dashboard.`,
202
- );
203
- return undefined;
204
- }
205
- return Redacted.value(key.secret);
206
- });
207
-
208
- export interface DeployKeyProps {
209
- /** Deployment name, such as `project.prodDeploymentName`. */
210
- readonly deployment: string;
211
- /**
212
- * Key name. The provider requests this name, a dash, and a hash of the
213
- * resource identity, so two resources with one `name` get different keys.
214
- */
215
- readonly name: string;
216
- /** Limits the key to these actions. Convex grants every deployment action when omitted. */
217
- readonly allowedActions?: readonly DeployKeyAction[];
218
- }
219
-
220
- export interface DeployKeyAttributes {
221
- /** The name Convex lists the key under, such as "ci-3f2a1b0c9d8e". */
222
- readonly uniqueName: string;
223
- /** Numeric id of the key in the listing. Undefined in state written by 0.1.x. */
224
- readonly keyId: number | undefined;
225
- readonly deployment: string;
226
- /** The secret, such as `prod:<deployment>|<token>`. Convex returns it only at creation. */
227
- readonly deployKey: Redacted.Redacted<string>;
228
- }
229
-
230
- export type DeployKey = Resource<
231
- "Convex.DeployKey",
232
- DeployKeyProps,
233
- DeployKeyAttributes,
234
- never,
235
- Providers
236
- >;
237
- export const DeployKey = Resource<DeployKey>("Convex.DeployKey");
238
-
239
- export const DeployKeyProvider = () =>
240
- Provider.effect(
241
- DeployKey,
242
- Effect.gen(function* () {
243
- const api = yield* ManagementApi;
244
-
245
- const listKeys = (deployment: string) =>
246
- api
247
- .request("list deploy keys", (client) =>
248
- client.GET("/deployments/{deployment_name}/list_deploy_keys", {
249
- params: { path: { deployment_name: deployment } },
250
- }),
251
- )
252
- .pipe(Effect.map((body) => Schema.decodeUnknownSync(DeployKeyList)(body)));
253
-
254
- /**
255
- * `id` is the secret: Convex accepts it, and it matches only the key
256
- * that this resource created. A second delete answers 404.
257
- */
258
- const deleteKey = (deployment: string, secret: string) =>
259
- retryIdempotentWrite(
260
- api.requestVoid("delete deploy key", (client) =>
261
- client.POST("/deployments/{deployment_name}/delete_deploy_key", {
262
- params: { path: { deployment_name: deployment } },
263
- body: { id: secret },
264
- }),
265
- ),
266
- );
267
-
268
- return {
269
- stables: ["uniqueName", "deployment"],
270
-
271
- diff: ({ olds, news, output }) => {
272
- if (!isResolved(news)) return Effect.succeed(undefined);
273
- // State written without the secret cannot hand a key to dependents,
274
- // and Convex cannot return it again, so only a new key helps.
275
- return Effect.succeed(
276
- (output !== undefined && output.deployKey === undefined) || havePropsChanged(olds, news)
277
- ? ({ action: "replace" } as const)
278
- : ({ action: "noop" } as const),
279
- );
280
- },
281
-
282
- read: Effect.fn(function* ({ output }) {
283
- // A key that is not in state has no known secret, so it is never adopted.
284
- if (output === undefined) return undefined;
285
- const entries = yield* absentAsUndefined(listKeys(output.deployment));
286
- return entries !== undefined && isKeyListed(entries, output) ? output : undefined;
287
- }),
288
-
289
- reconcile: Effect.fn(function* ({ fqn, instanceId, news, output }) {
290
- if (output !== undefined) {
291
- // The secret cannot be read back. A key that still exists keeps the one in state.
292
- if (isKeyListed(yield* listKeys(news.deployment), output)) return output;
293
- }
294
- const name = requestedKeyName(news.name, fqn, instanceId);
295
- const { keyId, uniqueName, secret } = yield* createTrackedKey({
296
- target: `deployment ${news.deployment}`,
297
- name,
298
- list: listKeys(news.deployment),
299
- create: api
300
- .request("create deploy key", (client) =>
301
- client.POST("/deployments/{deployment_name}/create_deploy_key", {
302
- params: { path: { deployment_name: news.deployment } },
303
- body: {
304
- name,
305
- ...(news.allowedActions === undefined
306
- ? {}
307
- : { allowedActions: [...news.allowedActions] }),
308
- },
309
- }),
310
- )
311
- .pipe(
312
- Effect.map((body) => Schema.decodeUnknownSync(CreatedDeployKey)(body).deployKey),
313
- ),
314
- isCredentialToken: api.isCredentialToken,
315
- });
316
- return { uniqueName, keyId, deployment: news.deployment, deployKey: secret };
317
- }),
318
-
319
- delete: Effect.fn(function* ({ output }) {
320
- // Only the secret names this key alone: 0.1.x state can hold the
321
- // listed name of another resource's key.
322
- const secret = yield* secretToDelete(
323
- { keyId: output.keyId, secret: output.deployKey },
324
- `Convex.DeployKey "${output.uniqueName}" on ${output.deployment}`,
325
- );
326
- if (secret === undefined) return;
327
- // 404 DeployKeyNotFound: an earlier attempt or someone else deleted it.
328
- yield* absentAsUndefined(deleteKey(output.deployment, secret));
329
- }),
330
- };
331
- }),
332
- );
@@ -1,262 +0,0 @@
1
- // Convex.EnvironmentVariable: one environment variable on one deployment.
2
- //
3
- // Environment variables live on the deployment API
4
- // (https://<deployment>.convex.cloud/api/v1), which authenticates with a
5
- // deploy key, not with the Management API token. Setting a variable and
6
- // removing it are the same call: update_environment_variables with
7
- // `value: null` removes the variable.
8
- //
9
- // A variable that exists but is not in state belongs to someone else: its
10
- // old value is lost when this resource writes it. Alchemy takes it over only
11
- // with --adopt.
12
- //
13
- // The variable's name and deployment cannot change. A change fails with
14
- // VariableIdentityChange: use a new resource with a new logical id. A
15
- // replacement could let the engine delete an old generation that names the
16
- // live variable, and a rename in place needs a cleanup that can fail and
17
- // leave a variable or a revoked key behind.
18
- //
19
- // The attributes keep the deploy key of the variable's deployment. After a
20
- // refused change, the props can name another deployment and its key, so
21
- // read and delete use the attributes, not the props.
22
- import { createDeploymentClient } from "@convex-dev/platform";
23
- import { Resource } from "alchemy";
24
- import { isResolved } from "alchemy/Diff";
25
- import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
26
- import * as Provider from "alchemy/Provider";
27
- import * as Effect from "effect/Effect";
28
- import * as Redacted from "effect/Redacted";
29
- import * as Schema from "effect/Schema";
30
- import { shouldAdopt } from "./Adoption.ts";
31
- import {
32
- absentAsUndefined,
33
- ManagementApi,
34
- type ManagementApiService,
35
- retryIdempotentWrite,
36
- settleVoid,
37
- } from "./ManagementApi.ts";
38
- import type { Providers } from "./Providers.ts";
39
-
40
- /**
41
- * The variable's identity when the listing has `name`. Own keys only: a
42
- * valid variable name such as `toString` must not match the object prototype.
43
- */
44
- export const findVariable = (
45
- list: typeof EnvironmentVariableList.Type,
46
- deployment: string,
47
- name: string,
48
- ): { readonly deployment: string; readonly name: string } | undefined =>
49
- Object.hasOwn(list.environmentVariables, name) ? { deployment, name } : undefined;
50
-
51
- /**
52
- * One attempt to set one variable, or to remove it when `value` is null. The
53
- * deployment answers 200 with an empty body, so this reads no response data.
54
- */
55
- const writeVariable = (
56
- deployment: string,
57
- deployKey: Redacted.Redacted<string>,
58
- name: string,
59
- value: string | Redacted.Redacted<string> | null,
60
- ) =>
61
- settleVoid("update environment variables", () =>
62
- createDeploymentClient(deployment, Redacted.value(deployKey)).POST(
63
- "/update_environment_variables",
64
- {
65
- body: {
66
- changes: [{ name, value: Redacted.isRedacted(value) ? Redacted.value(value) : value }],
67
- },
68
- },
69
- ),
70
- );
71
-
72
- /**
73
- * Sets one variable, or removes it when `value` is null. The call sets a
74
- * value, so it runs again after a write conflict or a 5xx answer, for example
75
- * when another resource writes a variable of the same deployment at the same
76
- * time. Use it only where the variable is already ours to set.
77
- */
78
- export const updateVariable = (
79
- deployment: string,
80
- deployKey: Redacted.Redacted<string>,
81
- name: string,
82
- value: string | Redacted.Redacted<string> | null,
83
- ) => retryIdempotentWrite(writeVariable(deployment, deployKey, name, value));
84
-
85
- /** GET https://<deployment>.convex.cloud/api/v1/list_environment_variables */
86
- export const EnvironmentVariableList = Schema.Struct({
87
- environmentVariables: Schema.Record(Schema.String, Schema.String),
88
- });
89
-
90
- /** The variable's identity, or undefined when the variable or the deployment is absent. */
91
- const readVariable = (
92
- api: ManagementApiService,
93
- deployment: string,
94
- deployKey: Redacted.Redacted<string>,
95
- name: string,
96
- ) =>
97
- // 404: the deployment no longer exists, so neither does the variable.
98
- absentAsUndefined(
99
- api.deploymentRequest("list environment variables", deployment, deployKey, (client) =>
100
- client.GET("/list_environment_variables"),
101
- ),
102
- ).pipe(
103
- Effect.map((body) =>
104
- body === undefined
105
- ? undefined
106
- : findVariable(Schema.decodeUnknownSync(EnvironmentVariableList)(body), deployment, name),
107
- ),
108
- );
109
-
110
- export interface EnvironmentVariableProps {
111
- /** Deployment name, or its URL for deployments outside US East. */
112
- readonly deployment: string;
113
- /** Deploy key for `deployment`, such as `deployKey.deployKey`. */
114
- readonly deployKey: Redacted.Redacted<string>;
115
- readonly name: string;
116
- readonly value: string | Redacted.Redacted<string>;
117
- }
118
-
119
- export interface EnvironmentVariableAttributes {
120
- readonly deployment: string;
121
- readonly name: string;
122
- /** Deploy key for `deployment`, for read and delete. Undefined in state from 0.1.x. */
123
- readonly deployKey: Redacted.Redacted<string> | undefined;
124
- }
125
-
126
- export type EnvironmentVariable = Resource<
127
- "Convex.EnvironmentVariable",
128
- EnvironmentVariableProps,
129
- EnvironmentVariableAttributes,
130
- never,
131
- Providers
132
- >;
133
- export const EnvironmentVariable = Resource<EnvironmentVariable>("Convex.EnvironmentVariable");
134
-
135
- /**
136
- * The props name another variable than the one in state. Used by
137
- * Convex.EnvironmentVariable and Convex.DefaultEnvironmentVariable.
138
- */
139
- export class VariableIdentityChange extends Schema.TaggedError<VariableIdentityChange>()(
140
- "VariableIdentityChange",
141
- {
142
- resourceType: Schema.String,
143
- field: Schema.Literals(["name", "deployment", "projectId", "deploymentType"]),
144
- current: Schema.String,
145
- requested: Schema.String,
146
- },
147
- ) {
148
- override get message() {
149
- return `${this.resourceType} cannot change its ${this.field} from ${this.current} to ${this.requested}. Create a new resource with a new logical id for the new name or deployment, and remove this one.`;
150
- }
151
- }
152
-
153
- /** Fails with {@link VariableIdentityChange} when the props name another variable than the one in state. */
154
- const checkIdentity = (output: EnvironmentVariableAttributes, news: EnvironmentVariableProps) =>
155
- output.name !== news.name
156
- ? Effect.fail(
157
- new VariableIdentityChange({
158
- resourceType: EnvironmentVariable.Type,
159
- field: "name",
160
- current: output.name,
161
- requested: news.name,
162
- }),
163
- )
164
- : output.deployment !== news.deployment
165
- ? Effect.fail(
166
- new VariableIdentityChange({
167
- resourceType: EnvironmentVariable.Type,
168
- field: "deployment",
169
- current: output.deployment,
170
- requested: news.deployment,
171
- }),
172
- )
173
- : Effect.void;
174
-
175
- /**
176
- * The deploy key for the variable in state: from the attributes, or, for
177
- * state from 0.1.x, from props that name the same deployment.
178
- */
179
- const keyOf = (
180
- output: EnvironmentVariableAttributes,
181
- olds: EnvironmentVariableProps | undefined,
182
- ): Redacted.Redacted<string> | undefined =>
183
- output.deployKey ?? (olds?.deployment === output.deployment ? olds.deployKey : undefined);
184
-
185
- export const EnvironmentVariableProvider = () =>
186
- Provider.effect(
187
- EnvironmentVariable,
188
- Effect.gen(function* () {
189
- const api = yield* ManagementApi;
190
-
191
- return {
192
- // Compares with the attributes: after a refused change, `olds` are
193
- // the refused props. Otherwise undefined: the engine updates when any
194
- // prop changed and compares Redacted values, such as the value and
195
- // the deploy key, by content.
196
- diff: Effect.fn(function* ({ news, output }) {
197
- if (!isResolved(news) || output === undefined) return undefined;
198
- yield* checkIdentity(output, news);
199
- return undefined;
200
- }),
201
-
202
- read: Effect.fn(function* ({ olds, output }) {
203
- if (output !== undefined) {
204
- const key = keyOf(output, olds);
205
- // Without a key for the deployment in state, keep the state as it is.
206
- if (key === undefined) return output;
207
- const found = yield* readVariable(api, output.deployment, key, output.name);
208
- return found === undefined ? undefined : output;
209
- }
210
- const found = yield* readVariable(api, olds.deployment, olds.deployKey, olds.name);
211
- // No state: the variable belongs to someone else until the user passes --adopt.
212
- return found === undefined ? undefined : Unowned({ ...found, deployKey: olds.deployKey });
213
- }),
214
-
215
- reconcile: Effect.fn(function* ({ fqn, news, output }) {
216
- // Also here: with an unresolved prop in plan, diff could not check.
217
- if (output !== undefined) yield* checkIdentity(output, news);
218
- const write = writeVariable(news.deployment, news.deployKey, news.name, news.value);
219
- if (output !== undefined || (yield* shouldAdopt(fqn))) {
220
- // In state, or adopted: the variable is ours to set.
221
- yield* retryIdempotentWrite(write);
222
- } else {
223
- // Not in state: overwriting a variable that exists loses its
224
- // value, so that needs adoption. Each attempt checks first, so a
225
- // retry after a write conflict never overwrites a variable that
226
- // another writer created in the meantime.
227
- yield* retryIdempotentWrite(
228
- Effect.gen(function* () {
229
- const existing = yield* readVariable(
230
- api,
231
- news.deployment,
232
- news.deployKey,
233
- news.name,
234
- );
235
- if (existing !== undefined) {
236
- return yield* new OwnedBySomeoneElse({
237
- message: `Environment variable ${news.name} already exists on deployment ${news.deployment}. Re-run with --adopt to take it over and overwrite its value, or use a different name.`,
238
- resourceType: EnvironmentVariable.Type,
239
- physicalName: news.name,
240
- });
241
- }
242
- yield* write;
243
- }),
244
- );
245
- }
246
- return { deployment: news.deployment, name: news.name, deployKey: news.deployKey };
247
- }),
248
-
249
- delete: Effect.fn(function* ({ olds, output }) {
250
- const key = keyOf(output, olds);
251
- if (key === undefined) {
252
- yield* Effect.logWarning(
253
- `Environment variable ${output.name} on deployment ${output.deployment} stays: Alchemy has no deploy key for that deployment. Remove it in the Convex dashboard.`,
254
- );
255
- return;
256
- }
257
- // Removing an absent variable succeeds; 404 means the deployment is gone.
258
- yield* absentAsUndefined(updateVariable(output.deployment, key, output.name, null));
259
- }),
260
- };
261
- }),
262
- );