@gaganref/confect-alchemy 0.0.0-stage → 10.3.1-fork.2

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.
Files changed (113) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +22 -2
  3. package/dist/Backend.d.ts +45 -0
  4. package/dist/Backend.d.ts.map +1 -0
  5. package/dist/Backend.js +38 -0
  6. package/dist/Backend.js.map +1 -0
  7. package/dist/Code.d.ts +80 -0
  8. package/dist/Code.d.ts.map +1 -0
  9. package/dist/Code.js +29 -0
  10. package/dist/Code.js.map +1 -0
  11. package/dist/Confect.d.ts +2 -0
  12. package/dist/Confect.d.ts.map +1 -0
  13. package/dist/Confect.js +8 -0
  14. package/dist/Confect.js.map +1 -0
  15. package/dist/Convex.d.ts +7 -0
  16. package/dist/Convex.d.ts.map +1 -0
  17. package/dist/Convex.js +21 -0
  18. package/dist/Convex.js.map +1 -0
  19. package/dist/ConvexClient.d.ts +215 -0
  20. package/dist/ConvexClient.d.ts.map +1 -0
  21. package/dist/ConvexClient.js +224 -0
  22. package/dist/ConvexClient.js.map +1 -0
  23. package/dist/DeployKey.d.ts +120 -0
  24. package/dist/DeployKey.d.ts.map +1 -0
  25. package/dist/DeployKey.js +151 -0
  26. package/dist/DeployKey.js.map +1 -0
  27. package/dist/Deployment.d.ts +113 -0
  28. package/dist/Deployment.d.ts.map +1 -0
  29. package/dist/Deployment.js +160 -0
  30. package/dist/Deployment.js.map +1 -0
  31. package/dist/EnvironmentVariables.d.ts +155 -0
  32. package/dist/EnvironmentVariables.d.ts.map +1 -0
  33. package/dist/EnvironmentVariables.js +135 -0
  34. package/dist/EnvironmentVariables.js.map +1 -0
  35. package/dist/Project.d.ts +100 -0
  36. package/dist/Project.d.ts.map +1 -0
  37. package/dist/Project.js +123 -0
  38. package/dist/Project.js.map +1 -0
  39. package/dist/Providers.d.ts +17 -0
  40. package/dist/Providers.d.ts.map +1 -0
  41. package/dist/Providers.js +46 -0
  42. package/dist/Providers.js.map +1 -0
  43. package/dist/_virtual/_rolldown/runtime.js +13 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +3 -0
  47. package/dist/internal/CodeDeployment.d.ts +17 -0
  48. package/dist/internal/CodeDeployment.d.ts.map +1 -0
  49. package/dist/internal/CodeDeployment.js +53 -0
  50. package/dist/internal/CodeDeployment.js.map +1 -0
  51. package/dist/internal/CommandRunner.d.ts +55 -0
  52. package/dist/internal/CommandRunner.d.ts.map +1 -0
  53. package/dist/internal/CommandRunner.js +126 -0
  54. package/dist/internal/CommandRunner.js.map +1 -0
  55. package/dist/internal/FileLock.d.ts +30 -0
  56. package/dist/internal/FileLock.d.ts.map +1 -0
  57. package/dist/internal/FileLock.js +83 -0
  58. package/dist/internal/FileLock.js.map +1 -0
  59. package/dist/internal/Lifecycle.d.ts +11 -0
  60. package/dist/internal/Lifecycle.d.ts.map +1 -0
  61. package/dist/internal/Lifecycle.js +45 -0
  62. package/dist/internal/Lifecycle.js.map +1 -0
  63. package/dist/internal/LocalBackend.d.ts +63 -0
  64. package/dist/internal/LocalBackend.d.ts.map +1 -0
  65. package/dist/internal/LocalBackend.js +333 -0
  66. package/dist/internal/LocalBackend.js.map +1 -0
  67. package/dist/internal/LocalSidecar.d.ts +3 -0
  68. package/dist/internal/LocalSidecar.d.ts.map +1 -0
  69. package/dist/internal/LocalSidecar.js +7 -0
  70. package/dist/internal/LocalSidecar.js.map +1 -0
  71. package/dist/internal/Loopback.d.ts +18 -0
  72. package/dist/internal/Loopback.d.ts.map +1 -0
  73. package/dist/internal/Loopback.js +31 -0
  74. package/dist/internal/Loopback.js.map +1 -0
  75. package/dist/internal/PushLock.d.ts +36 -0
  76. package/dist/internal/PushLock.d.ts.map +1 -0
  77. package/dist/internal/PushLock.js +61 -0
  78. package/dist/internal/PushLock.js.map +1 -0
  79. package/dist/internal/ResourceIdentity.d.ts +3 -0
  80. package/dist/internal/ResourceIdentity.d.ts.map +1 -0
  81. package/dist/internal/ResourceIdentity.js +10 -0
  82. package/dist/internal/ResourceIdentity.js.map +1 -0
  83. package/dist/internal/UserCache.d.ts +12 -0
  84. package/dist/internal/UserCache.d.ts.map +1 -0
  85. package/dist/internal/UserCache.js +25 -0
  86. package/dist/internal/UserCache.js.map +1 -0
  87. package/dist/internal/Zip.d.ts +15 -0
  88. package/dist/internal/Zip.d.ts.map +1 -0
  89. package/dist/internal/Zip.js +65 -0
  90. package/dist/internal/Zip.js.map +1 -0
  91. package/package.json +65 -3
  92. package/src/Backend.ts +101 -0
  93. package/src/Code.ts +55 -0
  94. package/src/Confect.ts +1 -0
  95. package/src/Convex.ts +18 -0
  96. package/src/ConvexClient.ts +544 -0
  97. package/src/DeployKey.ts +256 -0
  98. package/src/Deployment.ts +277 -0
  99. package/src/EnvironmentVariables.ts +254 -0
  100. package/src/Project.ts +205 -0
  101. package/src/Providers.ts +67 -0
  102. package/src/index.ts +2 -0
  103. package/src/internal/CodeDeployment.ts +81 -0
  104. package/src/internal/CommandRunner.ts +251 -0
  105. package/src/internal/FileLock.ts +136 -0
  106. package/src/internal/Lifecycle.ts +49 -0
  107. package/src/internal/LocalBackend.ts +492 -0
  108. package/src/internal/LocalSidecar.ts +9 -0
  109. package/src/internal/Loopback.ts +34 -0
  110. package/src/internal/PushLock.ts +99 -0
  111. package/src/internal/ResourceIdentity.ts +20 -0
  112. package/src/internal/UserCache.ts +25 -0
  113. package/src/internal/Zip.ts +108 -0
@@ -0,0 +1,256 @@
1
+ import type { PlatformCreateDeployKeyArgs } from "@convex-dev/platform/managementApi";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import * as Provider from "alchemy/Provider";
4
+ import { Resource } from "alchemy/Resource";
5
+ import * as Effect from "effect/Effect";
6
+ import * as FileSystem from "effect/FileSystem";
7
+ import * as Path from "effect/Path";
8
+ import * as Redacted from "effect/Redacted";
9
+ import * as Schema from "effect/Schema";
10
+
11
+ import { ConvexClient } from "./ConvexClient";
12
+ import type { Providers } from "./Providers";
13
+ import { resourceName } from "./internal/ResourceIdentity";
14
+ import { checkpointCreate } from "./internal/Lifecycle";
15
+ import { adminKey, dataDirectories } from "./internal/LocalBackend";
16
+
17
+ export interface DeployKeyProps {
18
+ readonly deploymentName: string;
19
+ readonly name?: string;
20
+ readonly allowedActions?: ReadonlyArray<
21
+ NonNullable<PlatformCreateDeployKeyArgs["allowedActions"]>[number]
22
+ >;
23
+ }
24
+
25
+ export interface DeployKeyAttributes {
26
+ readonly deploymentName: string;
27
+ readonly keyId: number;
28
+ readonly name: string;
29
+ readonly deployKey: Redacted.Redacted<string>;
30
+ readonly allowedActions: ReadonlyArray<string>;
31
+ }
32
+
33
+ export type DeployKey = Resource<
34
+ "Convex.DeployKey",
35
+ DeployKeyProps,
36
+ DeployKeyAttributes,
37
+ never,
38
+ Providers
39
+ >;
40
+ export const DeployKey = Resource<DeployKey>("Convex.DeployKey");
41
+
42
+ export class DeployKeyRecoveryRequired extends Schema.TaggedError<DeployKeyRecoveryRequired>()(
43
+ "DeployKeyRecoveryRequired",
44
+ {
45
+ deploymentName: Schema.String,
46
+ name: Schema.String,
47
+ keyIds: Schema.Array(Schema.Finite),
48
+ },
49
+ ) {}
50
+
51
+ export class DeployKeyChangeRequiresNewResource extends Schema.TaggedError<DeployKeyChangeRequiresNewResource>()(
52
+ "DeployKeyChangeRequiresNewResource",
53
+ { deploymentName: Schema.String, keyId: Schema.Finite, field: Schema.String },
54
+ ) {}
55
+
56
+ export const provider = Effect.gen(function* () {
57
+ const client = yield* ConvexClient;
58
+ const hasName = (keyName: string, name: string) =>
59
+ keyName === name ||
60
+ (keyName.startsWith(`${name} (`) &&
61
+ /^[0-9a-f]{8}\)$/.test(keyName.slice(name.length + 2)));
62
+ const recover = (
63
+ deploymentName: string,
64
+ name: string,
65
+ keyIds: ReadonlyArray<number>,
66
+ ) => new DeployKeyRecoveryRequired({ deploymentName, name, keyIds });
67
+ return {
68
+ diff: Effect.fn("DeployKey.diff")(function* ({ news, output }) {
69
+ if (!isResolved(news) || !output) return;
70
+ for (const field of ["deploymentName", "name"] as const) {
71
+ if (news[field] !== undefined && news[field] !== output[field])
72
+ return yield* new DeployKeyChangeRequiresNewResource({
73
+ deploymentName: output.deploymentName,
74
+ keyId: output.keyId,
75
+ field,
76
+ });
77
+ }
78
+ const keys = yield* client.listDeployKeys(output.deploymentName);
79
+ const key = keys.find((candidate) => candidate.id === output.keyId);
80
+ if (!key || !Redacted.isRedacted(output.deployKey))
81
+ return yield* recover(output.deploymentName, output.name, [
82
+ output.keyId,
83
+ ]);
84
+ if (!hasName(key.name, news.name ?? output.name))
85
+ return yield* new DeployKeyChangeRequiresNewResource({
86
+ deploymentName: output.deploymentName,
87
+ keyId: output.keyId,
88
+ field: "name",
89
+ });
90
+ const allowedActions = news.allowedActions ?? output.allowedActions;
91
+ if (
92
+ new Set(allowedActions).size !== new Set(key.allowedActions).size ||
93
+ allowedActions.some((action) => !key.allowedActions.includes(action))
94
+ )
95
+ return yield* new DeployKeyChangeRequiresNewResource({
96
+ deploymentName: output.deploymentName,
97
+ keyId: output.keyId,
98
+ field: "allowedActions",
99
+ });
100
+ }),
101
+ read: Effect.fn("DeployKey.read")(function* ({
102
+ fqn,
103
+ instanceId,
104
+ olds,
105
+ output,
106
+ }) {
107
+ const deploymentName = output?.deploymentName ?? olds.deploymentName;
108
+ if (deploymentName === undefined) return;
109
+ const name =
110
+ output?.name ??
111
+ olds.name ??
112
+ (yield* resourceName("key", fqn, instanceId));
113
+ const keys = yield* client.listDeployKeys(deploymentName);
114
+ if (output) {
115
+ const key = keys.find((candidate) => candidate.id === output.keyId);
116
+ if (!key) return yield* recover(deploymentName, name, [output.keyId]);
117
+ if (!Redacted.isRedacted(output.deployKey))
118
+ return yield* recover(deploymentName, name, [key.id]);
119
+ return {
120
+ ...output,
121
+ allowedActions: key.allowedActions,
122
+ };
123
+ }
124
+ const matches = keys.filter((key) => hasName(key.name, name));
125
+ if (matches.length > 0)
126
+ return yield* recover(
127
+ deploymentName,
128
+ name,
129
+ matches.map((candidate) => candidate.id),
130
+ );
131
+ return undefined;
132
+ }),
133
+ reconcile: Effect.fn("DeployKey.reconcile")(function* ({
134
+ fqn,
135
+ instanceId,
136
+ news,
137
+ output,
138
+ }) {
139
+ if (!output) yield* checkpointCreate(fqn, instanceId, news);
140
+ const name =
141
+ news.name ??
142
+ output?.name ??
143
+ (yield* resourceName("key", fqn, instanceId));
144
+ if (output) {
145
+ for (const field of ["deploymentName", "name"] as const) {
146
+ if (news[field] !== undefined && news[field] !== output[field])
147
+ return yield* new DeployKeyChangeRequiresNewResource({
148
+ deploymentName: output.deploymentName,
149
+ keyId: output.keyId,
150
+ field,
151
+ });
152
+ }
153
+ const keys = yield* client.listDeployKeys(output.deploymentName);
154
+ const key = keys.find((candidate) => candidate.id === output.keyId);
155
+ if (!key || !Redacted.isRedacted(output.deployKey))
156
+ return yield* recover(output.deploymentName, name, [output.keyId]);
157
+ if (!hasName(key.name, name))
158
+ return yield* new DeployKeyChangeRequiresNewResource({
159
+ deploymentName: output.deploymentName,
160
+ keyId: output.keyId,
161
+ field: "name",
162
+ });
163
+ const allowedActions = news.allowedActions ?? output.allowedActions;
164
+ if (
165
+ new Set(allowedActions).size !== new Set(key.allowedActions).size ||
166
+ allowedActions.some((action) => !key.allowedActions.includes(action))
167
+ )
168
+ return yield* new DeployKeyChangeRequiresNewResource({
169
+ deploymentName: output.deploymentName,
170
+ keyId: output.keyId,
171
+ field: "allowedActions",
172
+ });
173
+ return {
174
+ ...output,
175
+ allowedActions: key.allowedActions,
176
+ };
177
+ }
178
+ const existing = (yield* client.listDeployKeys(
179
+ news.deploymentName,
180
+ )).filter((key) => hasName(key.name, name));
181
+ if (existing.length > 0)
182
+ return yield* recover(
183
+ news.deploymentName,
184
+ name,
185
+ existing.map((key) => key.id),
186
+ );
187
+ const created = yield* client.createDeployKey(news.deploymentName, {
188
+ name,
189
+ ...(news.allowedActions === undefined
190
+ ? {}
191
+ : { allowedActions: [...news.allowedActions] }),
192
+ });
193
+ const matches = (yield* client.listDeployKeys(
194
+ news.deploymentName,
195
+ )).filter((key) => hasName(key.name, name));
196
+ const key = matches[0];
197
+ if (matches.length !== 1 || !key)
198
+ return yield* recover(
199
+ news.deploymentName,
200
+ name,
201
+ matches.map((candidate) => candidate.id),
202
+ );
203
+ return {
204
+ deploymentName: news.deploymentName,
205
+ keyId: key.id,
206
+ name,
207
+ deployKey: created.deployKey,
208
+ allowedActions: key.allowedActions,
209
+ };
210
+ }),
211
+ delete: Effect.fn("DeployKey.delete")(function* ({ output }) {
212
+ yield* client
213
+ .deleteDeployKey(output.deploymentName, output.deployKey)
214
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
215
+ }),
216
+ } satisfies Provider.ProviderServiceInput<DeployKey>;
217
+ });
218
+
219
+ /**
220
+ * Under `alchemy dev` the deploy key is the admin key the local deployment
221
+ * generated. It is read on every apply, so a recreated backend's new key
222
+ * replaces the old one.
223
+ */
224
+ export const localProvider = Effect.gen(function* () {
225
+ const fs = yield* FileSystem.FileSystem;
226
+ const path = yield* Path.Path;
227
+ const directory = yield* dataDirectories;
228
+ return {
229
+ diff: () => Effect.succeed({ action: "update" }),
230
+ reconcile: Effect.fn("DeployKey.localReconcile")(function* ({
231
+ fqn,
232
+ instanceId,
233
+ news,
234
+ }) {
235
+ const name = news.name ?? (yield* resourceName("key", fqn, instanceId));
236
+ const deployKey = yield* adminKey(
237
+ news.deploymentName,
238
+ directory(news.deploymentName),
239
+ ).pipe(
240
+ Effect.provideService(FileSystem.FileSystem, fs),
241
+ Effect.provideService(Path.Path, path),
242
+ );
243
+ return {
244
+ deploymentName: news.deploymentName,
245
+ keyId: 0,
246
+ name,
247
+ deployKey,
248
+ allowedActions: news.allowedActions ?? [],
249
+ } satisfies DeployKeyAttributes;
250
+ }),
251
+ delete: () => Effect.void,
252
+ } satisfies Provider.ProviderServiceInput<DeployKey>;
253
+ });
254
+
255
+ export const layer = Provider.effect(DeployKey, provider);
256
+ export const localLayer = Provider.effect(DeployKey, localProvider);
@@ -0,0 +1,277 @@
1
+ import type {
2
+ CreateDeploymentType,
3
+ PlatformDeploymentResponse,
4
+ RegionName,
5
+ } from "@convex-dev/platform/managementApi";
6
+ import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
7
+ import { isResolved } from "alchemy/Diff";
8
+ import * as LocalProvider from "alchemy/Local/LocalProvider";
9
+ import * as Provider from "alchemy/Provider";
10
+ import { Resource } from "alchemy/Resource";
11
+ import { moduleExtension } from "alchemy/Util/Node";
12
+ import * as Effect from "effect/Effect";
13
+ import * as FileSystem from "effect/FileSystem";
14
+ import * as Schema from "effect/Schema";
15
+
16
+ import { ConvexClient } from "./ConvexClient";
17
+ import type { Providers } from "./Providers";
18
+ import { resourceName } from "./internal/ResourceIdentity";
19
+ import { checkpointCreate, shouldAdopt } from "./internal/Lifecycle";
20
+ import { boot, dataDirectories, remove } from "./internal/LocalBackend";
21
+
22
+ export interface DeploymentProps {
23
+ readonly projectId: number;
24
+ readonly type: CreateDeploymentType;
25
+ readonly reference?: string;
26
+ readonly region?: RegionName;
27
+ }
28
+
29
+ export interface DeploymentAttributes {
30
+ readonly projectId: number;
31
+ readonly name: string;
32
+ readonly reference: string;
33
+ readonly type: CreateDeploymentType;
34
+ readonly url: string;
35
+ /**
36
+ * Always set in the cloud; a local deployment echoes the requested region.
37
+ */
38
+ readonly region?: RegionName;
39
+ }
40
+
41
+ export type Deployment = Resource<
42
+ "Convex.Deployment",
43
+ DeploymentProps,
44
+ DeploymentAttributes,
45
+ never,
46
+ Providers
47
+ >;
48
+ export const Deployment = Resource<Deployment>("Convex.Deployment", {
49
+ defaultRemovalPolicy: "retain",
50
+ });
51
+
52
+ export class DeploymentIdentityChange extends Schema.TaggedError<DeploymentIdentityChange>()(
53
+ "DeploymentIdentityChange",
54
+ {
55
+ deploymentName: Schema.String,
56
+ field: Schema.String,
57
+ current: Schema.String,
58
+ requested: Schema.String,
59
+ },
60
+ ) {}
61
+
62
+ export class UnsupportedLocalDeployment extends Schema.TaggedError<UnsupportedLocalDeployment>()(
63
+ "UnsupportedLocalDeployment",
64
+ { deploymentName: Schema.String },
65
+ ) {}
66
+
67
+ export class AmbiguousDeployment extends Schema.TaggedError<AmbiguousDeployment>()(
68
+ "AmbiguousDeployment",
69
+ { projectId: Schema.Finite, reference: Schema.String },
70
+ ) {}
71
+
72
+ export const provider = Effect.gen(function* () {
73
+ const client = yield* ConvexClient;
74
+ const attributes = Effect.fn("Deployment.attributes")(function* (
75
+ deployment: PlatformDeploymentResponse,
76
+ ) {
77
+ if (deployment.kind !== "cloud")
78
+ return yield* new UnsupportedLocalDeployment({
79
+ deploymentName: deployment.name,
80
+ });
81
+ return {
82
+ projectId: deployment.projectId,
83
+ name: deployment.name,
84
+ reference: deployment.reference,
85
+ type: deployment.deploymentType,
86
+ url: deployment.deploymentUrl,
87
+ region: deployment.region,
88
+ } satisfies DeploymentAttributes;
89
+ });
90
+ const find = Effect.fn("Deployment.find")(function* (
91
+ projectId: number,
92
+ reference: string,
93
+ ) {
94
+ const matches = (yield* client.listDeployments(projectId)).filter(
95
+ (deployment) =>
96
+ deployment.kind === "cloud" && deployment.reference === reference,
97
+ );
98
+ if (matches.length > 1)
99
+ return yield* new AmbiguousDeployment({ projectId, reference });
100
+ return matches[0];
101
+ });
102
+ const validate = Effect.fn("Deployment.validate")(function* (
103
+ current: DeploymentAttributes,
104
+ news: DeploymentProps,
105
+ ) {
106
+ for (const field of ["projectId", "type", "region"] as const) {
107
+ if (news[field] !== undefined && current[field] !== news[field])
108
+ return yield* new DeploymentIdentityChange({
109
+ deploymentName: current.name,
110
+ field,
111
+ current: String(current[field]),
112
+ requested: String(news[field]),
113
+ });
114
+ }
115
+ });
116
+ return {
117
+ diff: Effect.fn("Deployment.diff")(function* ({ news, output }) {
118
+ if (!isResolved(news) || !output) return;
119
+ yield* validate(output, news);
120
+ const deployment = yield* client
121
+ .getDeployment(output.name)
122
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
123
+ if (!deployment) return { action: "update" };
124
+ const current = yield* attributes(deployment);
125
+ yield* validate(current, news);
126
+ if ((news.reference ?? output.reference) !== current.reference)
127
+ return { action: "update" };
128
+ }),
129
+ read: Effect.fn("Deployment.read")(function* ({
130
+ fqn,
131
+ instanceId,
132
+ olds,
133
+ output,
134
+ }) {
135
+ if (output) {
136
+ const deployment = yield* client
137
+ .getDeployment(output.name)
138
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
139
+ return deployment ? yield* attributes(deployment) : undefined;
140
+ }
141
+ if (olds.projectId === undefined) return;
142
+ const deployment = yield* find(
143
+ olds.projectId,
144
+ olds.reference ?? (yield* resourceName("deployment", fqn, instanceId)),
145
+ );
146
+ return deployment ? Unowned(yield* attributes(deployment)) : undefined;
147
+ }),
148
+ reconcile: Effect.fn("Deployment.reconcile")(function* ({
149
+ fqn,
150
+ instanceId,
151
+ news,
152
+ output,
153
+ }) {
154
+ if (!output) yield* checkpointCreate(fqn, instanceId, news);
155
+ if (output) yield* validate(output, news);
156
+ let deployment = output
157
+ ? yield* client
158
+ .getDeployment(output.name)
159
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void))
160
+ : undefined;
161
+ const reference =
162
+ news.reference ??
163
+ output?.reference ??
164
+ (yield* resourceName("deployment", fqn, instanceId));
165
+ if (!deployment) {
166
+ const existing = yield* find(news.projectId, reference);
167
+ if (existing && !(yield* shouldAdopt(fqn)))
168
+ return yield* new OwnedBySomeoneElse({
169
+ message: "An existing deployment requires explicit adoption.",
170
+ resourceType: Deployment.Type,
171
+ physicalName: reference,
172
+ });
173
+ if (existing) {
174
+ const adopted = yield* attributes(existing);
175
+ yield* validate(adopted, news);
176
+ yield* checkpointCreate(fqn, instanceId, news, adopted);
177
+ deployment = existing;
178
+ } else {
179
+ deployment = yield* client.createDeployment(news.projectId, {
180
+ type: news.type,
181
+ reference,
182
+ ...(news.region === undefined ? {} : { region: news.region }),
183
+ });
184
+ }
185
+ }
186
+ const current = yield* attributes(deployment);
187
+ yield* validate(current, news);
188
+ if (current.reference !== reference) {
189
+ const existing = yield* find(news.projectId, reference);
190
+ if (existing && existing.name !== current.name)
191
+ return yield* new OwnedBySomeoneElse({
192
+ message: "The requested deployment reference is already in use.",
193
+ resourceType: Deployment.Type,
194
+ physicalName: reference,
195
+ });
196
+ yield* client.updateDeployment(current.name, {
197
+ reference,
198
+ });
199
+ deployment = yield* client.getDeployment(current.name);
200
+ }
201
+ return yield* attributes(deployment);
202
+ }),
203
+ delete: Effect.fn("Deployment.delete")(function* ({ output }) {
204
+ yield* client
205
+ .deleteDeployment(output.name)
206
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
207
+ }),
208
+ } satisfies Provider.ProviderServiceInput<Deployment>;
209
+ });
210
+
211
+ interface LocalConfig {
212
+ readonly name: string;
213
+ readonly props: DeploymentProps;
214
+ }
215
+
216
+ /**
217
+ * Under `alchemy dev` a deployment is a local Convex backend this provider
218
+ * owns. Alchemy's dev sidecar hosts it, so the backend keeps running across
219
+ * reloads of the stack and stops when the dev session ends. Its data lives in
220
+ * `.alchemy/convex/<stack>/<stage>/<name>` and is removed only when the
221
+ * deployment is destroyed.
222
+ */
223
+ export const localLayer = LocalProvider.make(
224
+ Deployment,
225
+ new URL(
226
+ `./internal/LocalSidecar${moduleExtension(import.meta.url)}`,
227
+ import.meta.url,
228
+ ).href,
229
+ Effect.gen(function* () {
230
+ const fs = yield* FileSystem.FileSystem;
231
+ const directory = yield* dataDirectories;
232
+ return {
233
+ resolveConfig: Effect.fn("Deployment.localConfig")(function* ({
234
+ fqn,
235
+ instanceId,
236
+ news,
237
+ }) {
238
+ return {
239
+ name: yield* resourceName("deployment", fqn, instanceId),
240
+ props: news,
241
+ } satisfies LocalConfig;
242
+ }),
243
+ start: Effect.fn("Deployment.localStart")(function* ({
244
+ config,
245
+ invalidate,
246
+ }) {
247
+ const backend = yield* boot(config.name, directory(config.name));
248
+ yield* backend.exited.pipe(
249
+ Effect.andThen(invalidate),
250
+ Effect.forkScoped,
251
+ );
252
+ return {
253
+ projectId: config.props.projectId,
254
+ name: config.name,
255
+ reference: config.props.reference ?? config.name,
256
+ type: config.props.type,
257
+ url: backend.url,
258
+ ...(config.props.region === undefined
259
+ ? {}
260
+ : { region: config.props.region }),
261
+ } satisfies DeploymentAttributes;
262
+ }),
263
+ stop: Effect.fn("Deployment.localStop")(function* ({ fqn, instanceId }) {
264
+ const name = yield* resourceName("deployment", fqn, instanceId);
265
+ yield* remove(name, directory(name)).pipe(
266
+ Effect.provideService(FileSystem.FileSystem, fs),
267
+ );
268
+ }),
269
+ } satisfies LocalProvider.LocalProviderSpec<
270
+ Deployment,
271
+ LocalConfig,
272
+ Effect.Services<ReturnType<typeof boot>>
273
+ >;
274
+ }),
275
+ );
276
+
277
+ export const layer = Provider.effect(Deployment, provider);