@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.
- package/LICENSE +7 -0
- package/README.md +22 -2
- package/dist/Backend.d.ts +45 -0
- package/dist/Backend.d.ts.map +1 -0
- package/dist/Backend.js +38 -0
- package/dist/Backend.js.map +1 -0
- package/dist/Code.d.ts +80 -0
- package/dist/Code.d.ts.map +1 -0
- package/dist/Code.js +29 -0
- package/dist/Code.js.map +1 -0
- package/dist/Confect.d.ts +2 -0
- package/dist/Confect.d.ts.map +1 -0
- package/dist/Confect.js +8 -0
- package/dist/Confect.js.map +1 -0
- package/dist/Convex.d.ts +7 -0
- package/dist/Convex.d.ts.map +1 -0
- package/dist/Convex.js +21 -0
- package/dist/Convex.js.map +1 -0
- package/dist/ConvexClient.d.ts +215 -0
- package/dist/ConvexClient.d.ts.map +1 -0
- package/dist/ConvexClient.js +224 -0
- package/dist/ConvexClient.js.map +1 -0
- package/dist/DeployKey.d.ts +120 -0
- package/dist/DeployKey.d.ts.map +1 -0
- package/dist/DeployKey.js +151 -0
- package/dist/DeployKey.js.map +1 -0
- package/dist/Deployment.d.ts +113 -0
- package/dist/Deployment.d.ts.map +1 -0
- package/dist/Deployment.js +160 -0
- package/dist/Deployment.js.map +1 -0
- package/dist/EnvironmentVariables.d.ts +155 -0
- package/dist/EnvironmentVariables.d.ts.map +1 -0
- package/dist/EnvironmentVariables.js +135 -0
- package/dist/EnvironmentVariables.js.map +1 -0
- package/dist/Project.d.ts +100 -0
- package/dist/Project.d.ts.map +1 -0
- package/dist/Project.js +123 -0
- package/dist/Project.js.map +1 -0
- package/dist/Providers.d.ts +17 -0
- package/dist/Providers.d.ts.map +1 -0
- package/dist/Providers.js +46 -0
- package/dist/Providers.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/internal/CodeDeployment.d.ts +17 -0
- package/dist/internal/CodeDeployment.d.ts.map +1 -0
- package/dist/internal/CodeDeployment.js +53 -0
- package/dist/internal/CodeDeployment.js.map +1 -0
- package/dist/internal/CommandRunner.d.ts +55 -0
- package/dist/internal/CommandRunner.d.ts.map +1 -0
- package/dist/internal/CommandRunner.js +126 -0
- package/dist/internal/CommandRunner.js.map +1 -0
- package/dist/internal/FileLock.d.ts +30 -0
- package/dist/internal/FileLock.d.ts.map +1 -0
- package/dist/internal/FileLock.js +83 -0
- package/dist/internal/FileLock.js.map +1 -0
- package/dist/internal/Lifecycle.d.ts +11 -0
- package/dist/internal/Lifecycle.d.ts.map +1 -0
- package/dist/internal/Lifecycle.js +45 -0
- package/dist/internal/Lifecycle.js.map +1 -0
- package/dist/internal/LocalBackend.d.ts +63 -0
- package/dist/internal/LocalBackend.d.ts.map +1 -0
- package/dist/internal/LocalBackend.js +333 -0
- package/dist/internal/LocalBackend.js.map +1 -0
- package/dist/internal/LocalSidecar.d.ts +3 -0
- package/dist/internal/LocalSidecar.d.ts.map +1 -0
- package/dist/internal/LocalSidecar.js +7 -0
- package/dist/internal/LocalSidecar.js.map +1 -0
- package/dist/internal/Loopback.d.ts +18 -0
- package/dist/internal/Loopback.d.ts.map +1 -0
- package/dist/internal/Loopback.js +31 -0
- package/dist/internal/Loopback.js.map +1 -0
- package/dist/internal/PushLock.d.ts +36 -0
- package/dist/internal/PushLock.d.ts.map +1 -0
- package/dist/internal/PushLock.js +61 -0
- package/dist/internal/PushLock.js.map +1 -0
- package/dist/internal/ResourceIdentity.d.ts +3 -0
- package/dist/internal/ResourceIdentity.d.ts.map +1 -0
- package/dist/internal/ResourceIdentity.js +10 -0
- package/dist/internal/ResourceIdentity.js.map +1 -0
- package/dist/internal/UserCache.d.ts +12 -0
- package/dist/internal/UserCache.d.ts.map +1 -0
- package/dist/internal/UserCache.js +25 -0
- package/dist/internal/UserCache.js.map +1 -0
- package/dist/internal/Zip.d.ts +15 -0
- package/dist/internal/Zip.d.ts.map +1 -0
- package/dist/internal/Zip.js +65 -0
- package/dist/internal/Zip.js.map +1 -0
- package/package.json +65 -3
- package/src/Backend.ts +101 -0
- package/src/Code.ts +55 -0
- package/src/Confect.ts +1 -0
- package/src/Convex.ts +18 -0
- package/src/ConvexClient.ts +544 -0
- package/src/DeployKey.ts +256 -0
- package/src/Deployment.ts +277 -0
- package/src/EnvironmentVariables.ts +254 -0
- package/src/Project.ts +205 -0
- package/src/Providers.ts +67 -0
- package/src/index.ts +2 -0
- package/src/internal/CodeDeployment.ts +81 -0
- package/src/internal/CommandRunner.ts +251 -0
- package/src/internal/FileLock.ts +136 -0
- package/src/internal/Lifecycle.ts +49 -0
- package/src/internal/LocalBackend.ts +492 -0
- package/src/internal/LocalSidecar.ts +9 -0
- package/src/internal/Loopback.ts +34 -0
- package/src/internal/PushLock.ts +99 -0
- package/src/internal/ResourceIdentity.ts +20 -0
- package/src/internal/UserCache.ts +25 -0
- package/src/internal/Zip.ts +108 -0
package/src/DeployKey.ts
ADDED
|
@@ -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);
|