@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,254 @@
1
+ import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
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 Layer from "effect/Layer";
7
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
8
+ import * as Redacted from "effect/Redacted";
9
+ import * as Schema from "effect/Schema";
10
+
11
+ import * as ConvexClient from "./ConvexClient";
12
+ import type { Providers } from "./Providers";
13
+ import { checkpointCreate, shouldAdopt } from "./internal/Lifecycle";
14
+
15
+ export interface EnvironmentVariablesProps {
16
+ readonly url: string;
17
+ readonly deployKey: Redacted.Redacted<string>;
18
+ readonly variables: Readonly<
19
+ Record<string, string | Redacted.Redacted<string>>
20
+ >;
21
+ }
22
+
23
+ export interface EnvironmentVariablesAttributes {
24
+ readonly url: string;
25
+ readonly deployKey: Redacted.Redacted<string>;
26
+ readonly ownedNames: ReadonlyArray<string>;
27
+ readonly variables: Readonly<Record<string, Redacted.Redacted<string>>>;
28
+ readonly originals: Readonly<Record<string, Redacted.Redacted<string>>>;
29
+ }
30
+
31
+ export type EnvironmentVariables = Resource<
32
+ "Convex.EnvironmentVariables",
33
+ EnvironmentVariablesProps,
34
+ EnvironmentVariablesAttributes,
35
+ never,
36
+ Providers
37
+ >;
38
+ export const EnvironmentVariables = Resource<EnvironmentVariables>(
39
+ "Convex.EnvironmentVariables",
40
+ );
41
+
42
+ export class EnvironmentTargetChange extends Schema.TaggedError<EnvironmentTargetChange>()(
43
+ "EnvironmentTargetChange",
44
+ { url: Schema.String },
45
+ ) {}
46
+
47
+ export const provider = Effect.gen(function* () {
48
+ const client = yield* ConvexClient.ConvexClient;
49
+ const redacted = (value: string | Redacted.Redacted<string>) =>
50
+ typeof value === "string" ? Redacted.make(value) : value;
51
+ const valueAt = (
52
+ variables: Readonly<Record<string, Redacted.Redacted<string>>>,
53
+ name: string,
54
+ ) => (Object.hasOwn(variables, name) ? variables[name] : undefined);
55
+ const select = (
56
+ variables: Readonly<Record<string, Redacted.Redacted<string>>>,
57
+ names: ReadonlyArray<string>,
58
+ ) =>
59
+ Object.fromEntries(
60
+ names.flatMap((name) =>
61
+ !Object.hasOwn(variables, name) ? [] : [[name, variables[name]]],
62
+ ),
63
+ );
64
+ return {
65
+ diff: Effect.fn("EnvironmentVariables.diff")(function* ({ news, output }) {
66
+ if (!isResolved(news) || !output) return;
67
+ if (news.url !== output.url)
68
+ return yield* new EnvironmentTargetChange({
69
+ url: output.url,
70
+ });
71
+ const names = Object.keys(news.variables);
72
+ if (
73
+ names.length !== output.ownedNames.length ||
74
+ names.some((name) => !output.ownedNames.includes(name))
75
+ )
76
+ return { action: "update" };
77
+ const variables = yield* client.listEnvironmentVariables(
78
+ news.url,
79
+ news.deployKey,
80
+ );
81
+ for (const [name, value] of Object.entries(news.variables)) {
82
+ const observed = valueAt(variables, name);
83
+ if (
84
+ observed === undefined ||
85
+ Redacted.value(observed) !== Redacted.value(redacted(value))
86
+ )
87
+ return { action: "update" };
88
+ }
89
+ }),
90
+ read: Effect.fn("EnvironmentVariables.read")(function* ({ olds, output }) {
91
+ const url = output?.url ?? olds.url;
92
+ if (url === undefined || olds.deployKey === undefined) return;
93
+ const observed = yield* client.listEnvironmentVariables(
94
+ url,
95
+ olds.deployKey,
96
+ );
97
+ const ownedNames = output?.ownedNames ?? Object.keys(olds.variables);
98
+ const variables = select(observed, ownedNames);
99
+ if (output) return { ...output, deployKey: olds.deployKey, variables };
100
+ if (Object.keys(variables).length === 0) return undefined;
101
+ return Unowned({
102
+ url,
103
+ deployKey: olds.deployKey,
104
+ ownedNames,
105
+ variables,
106
+ originals: variables,
107
+ });
108
+ }),
109
+ reconcile: Effect.fn("EnvironmentVariables.reconcile")(function* ({
110
+ fqn,
111
+ instanceId,
112
+ news,
113
+ output,
114
+ }) {
115
+ if (!output) yield* checkpointCreate(fqn, instanceId, news);
116
+ if (output && output.url !== news.url)
117
+ return yield* new EnvironmentTargetChange({
118
+ url: output.url,
119
+ });
120
+ const observed = yield* client.listEnvironmentVariables(
121
+ news.url,
122
+ news.deployKey,
123
+ );
124
+ const ownedNames = Object.keys(news.variables);
125
+ if (!output && (yield* shouldAdopt(fqn))) {
126
+ const variables = select(observed, ownedNames);
127
+ output = {
128
+ url: news.url,
129
+ deployKey: news.deployKey,
130
+ ownedNames,
131
+ variables,
132
+ originals: variables,
133
+ };
134
+ yield* checkpointCreate(fqn, instanceId, news, output);
135
+ }
136
+ const previousNames = output?.ownedNames ?? [];
137
+ for (const name of ownedNames) {
138
+ if (
139
+ !previousNames.includes(name) &&
140
+ valueAt(observed, name) !== undefined
141
+ )
142
+ return yield* new OwnedBySomeoneElse({
143
+ message:
144
+ "An existing environment variable requires explicit adoption in a separate resource.",
145
+ resourceType: EnvironmentVariables.Type,
146
+ physicalName: name,
147
+ });
148
+ }
149
+ const desired = Object.fromEntries(
150
+ Object.entries(news.variables).map(([name, value]) => [
151
+ name,
152
+ redacted(value),
153
+ ]),
154
+ );
155
+ const changes: Array<{
156
+ name: string;
157
+ value?: Redacted.Redacted<string>;
158
+ }> = [];
159
+ for (const [name, value] of Object.entries(desired)) {
160
+ const current = valueAt(observed, name);
161
+ if (
162
+ current === undefined ||
163
+ Redacted.value(current) !== Redacted.value(value)
164
+ )
165
+ changes.push({ name, value });
166
+ }
167
+ for (const name of previousNames) {
168
+ if (Object.hasOwn(desired, name)) continue;
169
+ const original = output ? valueAt(output.originals, name) : undefined;
170
+ const current = valueAt(observed, name);
171
+ if (original !== undefined) {
172
+ if (
173
+ current === undefined ||
174
+ Redacted.value(current) !== Redacted.value(original)
175
+ )
176
+ changes.push({ name, value: original });
177
+ } else if (current !== undefined) changes.push({ name });
178
+ }
179
+ if (changes.length > 0)
180
+ yield* client.updateEnvironmentVariables(
181
+ news.url,
182
+ news.deployKey,
183
+ changes,
184
+ );
185
+ return {
186
+ url: news.url,
187
+ deployKey: news.deployKey,
188
+ ownedNames,
189
+ variables: desired,
190
+ originals: select(output?.originals ?? {}, ownedNames),
191
+ };
192
+ }),
193
+ delete: Effect.fn("EnvironmentVariables.delete")(function* ({ output }) {
194
+ const observed = yield* client
195
+ .listEnvironmentVariables(output.url, output.deployKey)
196
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
197
+ if (observed === undefined) return;
198
+ const changes: Array<{
199
+ name: string;
200
+ value?: Redacted.Redacted<string>;
201
+ }> = [];
202
+ for (const name of output.ownedNames) {
203
+ const original = valueAt(output.originals, name);
204
+ const current = valueAt(observed, name);
205
+ if (original !== undefined) {
206
+ if (
207
+ current === undefined ||
208
+ Redacted.value(current) !== Redacted.value(original)
209
+ )
210
+ changes.push({ name, value: original });
211
+ } else if (current !== undefined) changes.push({ name });
212
+ }
213
+ if (changes.length > 0)
214
+ yield* client
215
+ .updateEnvironmentVariables(output.url, output.deployKey, changes)
216
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
217
+ }),
218
+ } satisfies Provider.ProviderServiceInput<EnvironmentVariables>;
219
+ });
220
+
221
+ /**
222
+ * Under `alchemy dev` variables are managed on the local backend. A backend
223
+ * that is not running keeps its variables in its retained data, so removing
224
+ * them is skipped rather than failing the apply.
225
+ */
226
+ export const localProvider = Effect.map(
227
+ provider,
228
+ (service) =>
229
+ ({
230
+ ...service,
231
+ delete: (input) =>
232
+ // oxlint-disable-next-line effecttsgo/any-unknown-in-error-context -- Alchemy's provider input type erases the delete error channel.
233
+ service.delete(input).pipe(
234
+ Effect.catchIf(
235
+ (error) =>
236
+ error._tag === "ConvexApiError" &&
237
+ error.code === "TransportError",
238
+ () => Effect.void,
239
+ ),
240
+ ),
241
+ }) satisfies Provider.ProviderServiceInput<EnvironmentVariables>,
242
+ );
243
+
244
+ export const layer = Provider.effect(EnvironmentVariables, provider);
245
+ export const localLayer = Provider.effect(
246
+ EnvironmentVariables,
247
+ localProvider,
248
+ ).pipe(
249
+ Layer.provide(
250
+ ConvexClient.layer({ mode: "local" }).pipe(
251
+ Layer.provide(FetchHttpClient.layer),
252
+ ),
253
+ ),
254
+ );
package/src/Project.ts ADDED
@@ -0,0 +1,205 @@
1
+ import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
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 Schema from "effect/Schema";
7
+
8
+ import { ConvexClient } from "./ConvexClient";
9
+ import type { Providers } from "./Providers";
10
+ import { resourceName } from "./internal/ResourceIdentity";
11
+ import { checkpointCreate, shouldAdopt } from "./internal/Lifecycle";
12
+
13
+ export interface ProjectProps {
14
+ readonly teamId: number;
15
+ readonly name?: string;
16
+ }
17
+
18
+ export interface ProjectAttributes {
19
+ readonly projectId: number;
20
+ readonly teamId: number;
21
+ readonly name: string;
22
+ readonly slug: string;
23
+ }
24
+
25
+ export type Project = Resource<
26
+ "Convex.Project",
27
+ ProjectProps,
28
+ ProjectAttributes,
29
+ never,
30
+ Providers
31
+ >;
32
+ export const Project = Resource<Project>("Convex.Project", {
33
+ defaultRemovalPolicy: "retain",
34
+ });
35
+
36
+ export class ProjectTeamChange extends Schema.TaggedError<ProjectTeamChange>()(
37
+ "ProjectTeamChange",
38
+ {
39
+ projectId: Schema.Finite,
40
+ teamId: Schema.Finite,
41
+ requestedTeamId: Schema.Finite,
42
+ },
43
+ ) {}
44
+
45
+ export class AmbiguousProject extends Schema.TaggedError<AmbiguousProject>()(
46
+ "AmbiguousProject",
47
+ {
48
+ teamId: Schema.Finite,
49
+ name: Schema.String,
50
+ projectIds: Schema.Array(Schema.Finite),
51
+ },
52
+ ) {}
53
+
54
+ export const provider = Effect.gen(function* () {
55
+ const client = yield* ConvexClient;
56
+ const attributes = (project: {
57
+ id: number;
58
+ teamId: number;
59
+ name: string;
60
+ slug: string;
61
+ }): ProjectAttributes => ({
62
+ projectId: project.id,
63
+ teamId: project.teamId,
64
+ name: project.name,
65
+ slug: project.slug,
66
+ });
67
+ const find = Effect.fn("Project.find")(function* (
68
+ teamId: number,
69
+ name: string,
70
+ ) {
71
+ const matches = (yield* client.listProjects(teamId)).filter(
72
+ (project) => project.name === name,
73
+ );
74
+ if (matches.length > 1)
75
+ return yield* new AmbiguousProject({
76
+ teamId,
77
+ name,
78
+ projectIds: matches.map((project) => project.id),
79
+ });
80
+ return matches[0];
81
+ });
82
+ return {
83
+ diff: Effect.fn("Project.diff")(function* ({ news, output }) {
84
+ if (!isResolved(news) || !output) return;
85
+ if (output.teamId !== news.teamId)
86
+ return yield* new ProjectTeamChange({
87
+ projectId: output.projectId,
88
+ teamId: output.teamId,
89
+ requestedTeamId: news.teamId,
90
+ });
91
+ const project = yield* client
92
+ .getProject(output.projectId)
93
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
94
+ if (!project) return { action: "update" };
95
+ if (project.teamId !== news.teamId)
96
+ return yield* new ProjectTeamChange({
97
+ projectId: project.id,
98
+ teamId: project.teamId,
99
+ requestedTeamId: news.teamId,
100
+ });
101
+ if ((news.name ?? output.name) !== project.name)
102
+ return { action: "update" };
103
+ }),
104
+ read: Effect.fn("Project.read")(function* ({
105
+ fqn,
106
+ instanceId,
107
+ olds,
108
+ output,
109
+ }) {
110
+ if (output) {
111
+ const project = yield* client
112
+ .getProject(output.projectId)
113
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
114
+ return project ? attributes(project) : undefined;
115
+ }
116
+ if (olds.teamId === undefined) return;
117
+ const project = yield* find(
118
+ olds.teamId,
119
+ olds.name ?? (yield* resourceName("project", fqn, instanceId)),
120
+ );
121
+ return project ? Unowned(attributes(project)) : undefined;
122
+ }),
123
+ reconcile: Effect.fn("Project.reconcile")(function* ({
124
+ fqn,
125
+ instanceId,
126
+ news,
127
+ output,
128
+ }) {
129
+ if (!output) yield* checkpointCreate(fqn, instanceId, news);
130
+ if (output && output.teamId !== news.teamId)
131
+ return yield* new ProjectTeamChange({
132
+ projectId: output.projectId,
133
+ teamId: output.teamId,
134
+ requestedTeamId: news.teamId,
135
+ });
136
+ let project = output
137
+ ? yield* client
138
+ .getProject(output.projectId)
139
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void))
140
+ : undefined;
141
+ const name =
142
+ news.name ??
143
+ output?.name ??
144
+ (yield* resourceName("project", fqn, instanceId));
145
+ if (!project) {
146
+ const existing = yield* find(news.teamId, name);
147
+ if (existing && !(yield* shouldAdopt(fqn)))
148
+ return yield* new OwnedBySomeoneElse({
149
+ message: "An existing project requires explicit adoption.",
150
+ resourceType: Project.Type,
151
+ physicalName: name,
152
+ });
153
+ if (existing) {
154
+ project = existing;
155
+ yield* checkpointCreate(fqn, instanceId, news, attributes(existing));
156
+ } else {
157
+ const created = yield* client.createProject(news.teamId, {
158
+ projectName: name,
159
+ });
160
+ project = yield* client.getProject(created.id);
161
+ }
162
+ }
163
+ if (project.teamId !== news.teamId)
164
+ return yield* new ProjectTeamChange({
165
+ projectId: project.id,
166
+ teamId: project.teamId,
167
+ requestedTeamId: news.teamId,
168
+ });
169
+ if (project.name !== name)
170
+ project = yield* client.updateProject(project.id, {
171
+ name,
172
+ });
173
+ return attributes(project);
174
+ }),
175
+ delete: Effect.fn("Project.delete")(function* ({ output }) {
176
+ yield* client
177
+ .deleteProject(output.projectId)
178
+ .pipe(Effect.catchTag("ConvexNotFound", () => Effect.void));
179
+ }),
180
+ } satisfies Provider.ProviderServiceInput<Project>;
181
+ });
182
+
183
+ /**
184
+ * Under `alchemy dev` a project needs no Convex account: it is a fixed local
185
+ * identity that local deployments ignore.
186
+ */
187
+ export const localProvider = Effect.succeed({
188
+ reconcile: Effect.fn("Project.localReconcile")(function* ({
189
+ fqn,
190
+ instanceId,
191
+ news,
192
+ }) {
193
+ const name = news.name ?? (yield* resourceName("project", fqn, instanceId));
194
+ return {
195
+ projectId: 0,
196
+ teamId: news.teamId,
197
+ name,
198
+ slug: name,
199
+ } satisfies ProjectAttributes;
200
+ }),
201
+ delete: () => Effect.void,
202
+ } satisfies Provider.ProviderServiceInput<Project>);
203
+
204
+ export const layer = Provider.effect(Project, provider);
205
+ export const localLayer = Provider.effect(Project, localProvider);
@@ -0,0 +1,67 @@
1
+ import * as ProviderLayer from "alchemy/Local/ProviderLayer";
2
+ import * as Provider from "alchemy/Provider";
3
+ import type * as Redacted from "effect/Redacted";
4
+ import * as Layer from "effect/Layer";
5
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
6
+ import * as Code from "./Code";
7
+ import * as ConvexClient from "./ConvexClient";
8
+ import * as Deployment from "./Deployment";
9
+ import * as DeployKey from "./DeployKey";
10
+ import * as EnvironmentVariables from "./EnvironmentVariables";
11
+ import * as Project from "./Project";
12
+ import * as CommandRunner from "./internal/CommandRunner";
13
+ import * as PushLock from "./internal/PushLock";
14
+
15
+ export class Providers extends Provider.ProviderCollection<Providers>()(
16
+ "@confect/alchemy/Providers",
17
+ ) {}
18
+
19
+ /**
20
+ * Register Convex resources without eagerly resolving management credentials.
21
+ * `alchemy deploy` resolves each resource to its cloud provider; `alchemy dev`
22
+ * resolves the same resources to local providers backed by a local Convex
23
+ * backend, so one stack definition serves both.
24
+ */
25
+ export const providers = (options?: {
26
+ readonly token?: Redacted.Redacted<string>;
27
+ }) =>
28
+ Layer.effect(
29
+ Providers,
30
+ Provider.collection([
31
+ Project.Project,
32
+ Deployment.Deployment,
33
+ DeployKey.DeployKey,
34
+ EnvironmentVariables.EnvironmentVariables,
35
+ Code.Code,
36
+ ]),
37
+ ).pipe(
38
+ Layer.provide(
39
+ Layer.mergeAll(
40
+ ProviderLayer.dual(Project.Project, {
41
+ live: () => Project.layer,
42
+ local: () => Project.localLayer,
43
+ }),
44
+ ProviderLayer.dual(Deployment.Deployment, {
45
+ live: () => Deployment.layer,
46
+ local: () => Deployment.localLayer,
47
+ }),
48
+ ProviderLayer.dual(DeployKey.DeployKey, {
49
+ live: () => DeployKey.layer,
50
+ local: () => DeployKey.localLayer,
51
+ }),
52
+ ProviderLayer.dual(EnvironmentVariables.EnvironmentVariables, {
53
+ live: () => EnvironmentVariables.layer,
54
+ local: () => EnvironmentVariables.localLayer,
55
+ }),
56
+ ProviderLayer.dual(Code.Code, {
57
+ live: () => Code.layer,
58
+ local: () => Code.localLayer,
59
+ }),
60
+ ),
61
+ ),
62
+ Layer.provide(
63
+ ConvexClient.layer(options).pipe(Layer.provide(FetchHttpClient.layer)),
64
+ ),
65
+ Layer.provide(CommandRunner.layer),
66
+ Layer.provide(PushLock.layer()),
67
+ );
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * as Convex from "./Convex";
2
+ export * as Confect from "./Confect";
@@ -0,0 +1,81 @@
1
+ import type { ProviderMode } from "alchemy/ProviderMode";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Redacted from "effect/Redacted";
4
+ import * as Result from "effect/Result";
5
+ import * as Schema from "effect/Schema";
6
+ import type { CodeProps } from "../Code";
7
+ import { CommandRunner, type PushTarget } from "./CommandRunner";
8
+ import * as Loopback from "./Loopback";
9
+ import { PushLock } from "./PushLock";
10
+
11
+ export class InvalidDeploymentTarget extends Schema.TaggedError<InvalidDeploymentTarget>()(
12
+ "InvalidDeploymentTarget",
13
+ { reason: Schema.Literals(["deployment", "deployKey"]) },
14
+ ) {}
15
+
16
+ const deploymentName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
+
18
+ const cloudTarget = (
19
+ props: CodeProps,
20
+ ): Result.Result<PushTarget, InvalidDeploymentTarget> => {
21
+ const { name, url } = props.deployment;
22
+ if (
23
+ !deploymentName.test(name) ||
24
+ !new RegExp(
25
+ `^https://${name}(?:\\.[a-z]{2}-[a-z]+-[0-9]+)?\\.convex\\.cloud$`,
26
+ ).test(url)
27
+ )
28
+ return Result.fail(new InvalidDeploymentTarget({ reason: "deployment" }));
29
+ const key =
30
+ /^(?:dev|prod|preview|custom):([a-z0-9]+(?:-[a-z0-9]+)*)\|[a-zA-Z0-9+/=_-]+$/.exec(
31
+ Redacted.value(props.deployKey),
32
+ );
33
+ if (!key || key[1] !== name)
34
+ return Result.fail(new InvalidDeploymentTarget({ reason: "deployKey" }));
35
+ return Result.succeed({
36
+ _tag: "Cloud",
37
+ deploymentName: name,
38
+ deployKey: props.deployKey,
39
+ });
40
+ };
41
+
42
+ const localTarget = (
43
+ props: CodeProps,
44
+ ): Result.Result<PushTarget, InvalidDeploymentTarget> => {
45
+ const { name, url } = props.deployment;
46
+ if (!deploymentName.test(name) || !Loopback.isOrigin(url))
47
+ return Result.fail(new InvalidDeploymentTarget({ reason: "deployment" }));
48
+ const key = /^([a-z0-9]+(?:-[a-z0-9]+)*)\|[0-9a-f]+$/.exec(
49
+ Redacted.value(props.deployKey),
50
+ );
51
+ if (!key || key[1] !== name)
52
+ return Result.fail(new InvalidDeploymentTarget({ reason: "deployKey" }));
53
+ return Result.succeed({
54
+ _tag: "SelfHosted",
55
+ deploymentName: name,
56
+ url,
57
+ adminKey: props.deployKey,
58
+ });
59
+ };
60
+
61
+ const targets = { live: cloudTarget, local: localTarget } satisfies Record<
62
+ ProviderMode,
63
+ (props: CodeProps) => Result.Result<PushTarget, InvalidDeploymentTarget>
64
+ >;
65
+
66
+ export const deploy = Effect.fn("Code.deploy")(function* (
67
+ props: CodeProps,
68
+ mode: ProviderMode = "live",
69
+ ) {
70
+ const target = yield* Effect.fromResult(targets[mode](props));
71
+ const runner = yield* CommandRunner;
72
+ const lock = yield* PushLock;
73
+ yield* lock.withLock(
74
+ target.deploymentName,
75
+ Effect.gen(function* () {
76
+ if (props.prepare) yield* runner.prepare(props.prepare, props.cwd);
77
+ yield* runner.deploy(target, props.cwd);
78
+ }),
79
+ );
80
+ return { name: props.deployment.name, url: props.deployment.url };
81
+ });