@samebase/alchemy-convex 0.1.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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +146 -0
  3. package/lib/Credentials.d.ts +39 -0
  4. package/lib/Credentials.d.ts.map +1 -0
  5. package/lib/Credentials.js +49 -0
  6. package/lib/Credentials.js.map +1 -0
  7. package/lib/DefaultEnvironmentVariable.d.ts +58 -0
  8. package/lib/DefaultEnvironmentVariable.d.ts.map +1 -0
  9. package/lib/DefaultEnvironmentVariable.js +93 -0
  10. package/lib/DefaultEnvironmentVariable.js.map +1 -0
  11. package/lib/Deploy.d.ts +62 -0
  12. package/lib/Deploy.d.ts.map +1 -0
  13. package/lib/Deploy.js +92 -0
  14. package/lib/Deploy.js.map +1 -0
  15. package/lib/DeployKey.d.ts +48 -0
  16. package/lib/DeployKey.d.ts.map +1 -0
  17. package/lib/DeployKey.js +104 -0
  18. package/lib/DeployKey.js.map +1 -0
  19. package/lib/EnvironmentVariable.d.ts +37 -0
  20. package/lib/EnvironmentVariable.d.ts.map +1 -0
  21. package/lib/EnvironmentVariable.js +65 -0
  22. package/lib/EnvironmentVariable.js.map +1 -0
  23. package/lib/ManagementApi.d.ts +50 -0
  24. package/lib/ManagementApi.d.ts.map +1 -0
  25. package/lib/ManagementApi.js +103 -0
  26. package/lib/ManagementApi.js.map +1 -0
  27. package/lib/PreviewDeployKey.d.ts +34 -0
  28. package/lib/PreviewDeployKey.d.ts.map +1 -0
  29. package/lib/PreviewDeployKey.js +78 -0
  30. package/lib/PreviewDeployKey.js.map +1 -0
  31. package/lib/Project.d.ts +29 -0
  32. package/lib/Project.d.ts.map +1 -0
  33. package/lib/Project.js +114 -0
  34. package/lib/Project.js.map +1 -0
  35. package/lib/Providers.d.ts +13 -0
  36. package/lib/Providers.d.ts.map +1 -0
  37. package/lib/Providers.js +30 -0
  38. package/lib/Providers.js.map +1 -0
  39. package/lib/index.d.ts +10 -0
  40. package/lib/index.d.ts.map +1 -0
  41. package/lib/index.js +11 -0
  42. package/lib/index.js.map +1 -0
  43. package/package.json +71 -0
  44. package/src/Credentials.ts +78 -0
  45. package/src/DefaultEnvironmentVariable.ts +165 -0
  46. package/src/Deploy.ts +171 -0
  47. package/src/DeployKey.ts +171 -0
  48. package/src/EnvironmentVariable.ts +129 -0
  49. package/src/ManagementApi.ts +193 -0
  50. package/src/PreviewDeployKey.ts +126 -0
  51. package/src/Project.ts +191 -0
  52. package/src/Providers.ts +51 -0
  53. package/src/index.ts +45 -0
@@ -0,0 +1,193 @@
1
+ // Effect wrapper around the official Convex platform client.
2
+ //
3
+ // `@convex-dev/platform` is an openapi-fetch client: every call resolves to
4
+ // `{ data, error, response }`. This service turns that into an Effect with one
5
+ // typed error, resolves credentials lazily, and validates the fields the
6
+ // provider relies on at runtime.
7
+ import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
8
+ import * as Context from "effect/Context";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Layer from "effect/Layer";
11
+ import * as Redacted from "effect/Redacted";
12
+ import * as Schema from "effect/Schema";
13
+ import { Credentials } from "./Credentials.ts";
14
+
15
+ export type ManagementClient = ReturnType<typeof createManagementClient>;
16
+ export type DeploymentClient = ReturnType<typeof createDeploymentClient>;
17
+
18
+ /** Shape of every openapi-fetch result we consume. */
19
+ export interface FetchResult<T> {
20
+ readonly data?: T;
21
+ readonly error?: unknown;
22
+ readonly response: Response;
23
+ }
24
+
25
+ /**
26
+ * One error for every control-plane and deployment call. Resources branch on
27
+ * `status` (404 means absent for read and delete), so the class is load
28
+ * bearing, not decorative.
29
+ */
30
+ export class ConvexApiError extends Schema.TaggedError<ConvexApiError>()("ConvexApiError", {
31
+ operation: Schema.String,
32
+ status: Schema.Number,
33
+ code: Schema.optionalKey(Schema.String),
34
+ message: Schema.String,
35
+ }) {}
36
+
37
+ const ErrorBody = Schema.Struct({
38
+ code: Schema.optionalKey(Schema.String),
39
+ message: Schema.optionalKey(Schema.String),
40
+ });
41
+
42
+ const toApiError = (operation: string, result: FetchResult<unknown>) => {
43
+ const body = Schema.decodeUnknownSync(Schema.NullishOr(ErrorBody))(result.error ?? null);
44
+ const code = body?.code;
45
+ return new ConvexApiError({
46
+ operation,
47
+ status: result.response.status,
48
+ ...(code === undefined ? {} : { code }),
49
+ message: body?.message ?? `${operation} failed with HTTP ${result.response.status}`,
50
+ });
51
+ };
52
+
53
+ export interface ManagementApiService {
54
+ /** Run one management call with the resolved credentials. */
55
+ readonly request: <T>(
56
+ operation: string,
57
+ run: (client: ManagementClient) => Promise<FetchResult<T>>,
58
+ ) => Effect.Effect<T, ConvexApiError>;
59
+ /** Run one management call whose success response has no body. */
60
+ readonly requestVoid: (
61
+ operation: string,
62
+ run: (client: ManagementClient) => Promise<FetchResult<unknown>>,
63
+ ) => Effect.Effect<void, ConvexApiError>;
64
+ /** Run one deployment-scoped call authenticated with a deploy key. */
65
+ readonly deploymentRequest: <T>(
66
+ operation: string,
67
+ deploymentUrlOrName: string,
68
+ deployKey: Redacted.Redacted<string>,
69
+ run: (client: DeploymentClient) => Promise<FetchResult<T>>,
70
+ ) => Effect.Effect<T, ConvexApiError>;
71
+ /** Team id for a slug or numeric id. */
72
+ readonly resolveTeamId: (team: string | number) => Effect.Effect<number, ConvexApiError>;
73
+ }
74
+
75
+ export class ManagementApi extends Context.Service<ManagementApi, ManagementApiService>()(
76
+ "@samebase/alchemy-convex/ManagementApi",
77
+ ) {}
78
+
79
+ const attempt = <T>(operation: string, run: () => Promise<FetchResult<T>>) =>
80
+ Effect.tryPromise({
81
+ try: run,
82
+ catch: (cause) =>
83
+ new ConvexApiError({
84
+ operation,
85
+ status: 0,
86
+ message: cause instanceof Error ? cause.message : String(cause),
87
+ }),
88
+ });
89
+
90
+ /** Reads `data` or fails with the HTTP status and the API's own code and message. */
91
+ export const settle = <T>(
92
+ operation: string,
93
+ run: () => Promise<FetchResult<T>>,
94
+ ): Effect.Effect<T, ConvexApiError> =>
95
+ attempt(operation, run).pipe(
96
+ Effect.flatMap((result) =>
97
+ result.response.ok && result.data !== undefined
98
+ ? Effect.succeed(result.data)
99
+ : Effect.fail(toApiError(operation, result)),
100
+ ),
101
+ );
102
+
103
+ /** For endpoints that answer with an empty body, such as delete calls. */
104
+ export const settleVoid = (
105
+ operation: string,
106
+ run: () => Promise<FetchResult<unknown>>,
107
+ ): Effect.Effect<void, ConvexApiError> =>
108
+ attempt(operation, run).pipe(
109
+ Effect.flatMap((result) =>
110
+ result.response.ok ? Effect.void : Effect.fail(toApiError(operation, result)),
111
+ ),
112
+ );
113
+
114
+ /** Turns a 404 into `undefined` so read and delete stay idempotent. */
115
+ export const absentAsUndefined = <A, R>(
116
+ effect: Effect.Effect<A, ConvexApiError, R>,
117
+ ): Effect.Effect<A | undefined, ConvexApiError, R> =>
118
+ effect.pipe(
119
+ Effect.catchIf(
120
+ (error) => error.status === 404,
121
+ () => Effect.succeed(undefined),
122
+ ),
123
+ );
124
+
125
+ const TeamRow = Schema.Struct({ id: Schema.Number, slug: Schema.String });
126
+ const TeamRows = Schema.Array(TeamRow);
127
+
128
+ export const ManagementApiLive = (): Layer.Layer<ManagementApi, never, Credentials> =>
129
+ Layer.effect(
130
+ ManagementApi,
131
+ Effect.gen(function* () {
132
+ const credentials = yield* Credentials;
133
+ const client = Effect.cached(
134
+ credentials.pipe(
135
+ Effect.map((resolved) => createManagementClient(Redacted.value(resolved.accessToken))),
136
+ ),
137
+ ).pipe(Effect.flatten);
138
+
139
+ const request: ManagementApiService["request"] = (operation, run) =>
140
+ client.pipe(Effect.flatMap((c) => settle(operation, () => run(c))));
141
+
142
+ const requestVoid: ManagementApiService["requestVoid"] = (operation, run) =>
143
+ client.pipe(Effect.flatMap((c) => settleVoid(operation, () => run(c))));
144
+
145
+ const deploymentRequest: ManagementApiService["deploymentRequest"] = (
146
+ operation,
147
+ deploymentUrlOrName,
148
+ deployKey,
149
+ run,
150
+ ) =>
151
+ settle(operation, () =>
152
+ run(createDeploymentClient(deploymentUrlOrName, Redacted.value(deployKey))),
153
+ );
154
+
155
+ // The Management API addresses teams by numeric id. The only slug lookup
156
+ // it offers is per project, so a slug falls back to the dashboard's team
157
+ // list, which is unofficial. Pass the numeric team id to skip this.
158
+ const resolveTeamId: ManagementApiService["resolveTeamId"] = (team) =>
159
+ Effect.gen(function* () {
160
+ if (typeof team === "number") return team;
161
+ if (/^\d+$/.test(team)) return Number(team);
162
+ const resolved = yield* credentials;
163
+ const rows = yield* Effect.tryPromise({
164
+ try: async () => {
165
+ const response = await fetch("https://api.convex.dev/api/dashboard/teams", {
166
+ headers: { Authorization: `Bearer ${Redacted.value(resolved.accessToken)}` },
167
+ });
168
+ if (!response.ok) {
169
+ throw new Error(`team lookup for "${team}" failed with HTTP ${response.status}`);
170
+ }
171
+ return Schema.decodeUnknownSync(TeamRows)(await response.json());
172
+ },
173
+ catch: (cause) =>
174
+ new ConvexApiError({
175
+ operation: "resolve team",
176
+ status: 0,
177
+ message: cause instanceof Error ? cause.message : String(cause),
178
+ }),
179
+ });
180
+ const match = rows.find((row) => row.slug === team);
181
+ if (match === undefined) {
182
+ return yield* new ConvexApiError({
183
+ operation: "resolve team",
184
+ status: 404,
185
+ message: `no team with slug "${team}" for this token`,
186
+ });
187
+ }
188
+ return match.id;
189
+ });
190
+
191
+ return { request, requestVoid, deploymentRequest, resolveTeamId };
192
+ }),
193
+ );
@@ -0,0 +1,126 @@
1
+ // Convex.PreviewDeployKey: a project key that creates and deploys preview
2
+ // deployments, such as the key CI passes to `npx convex deploy` for a branch.
3
+ //
4
+ // Convex returns the secret only once, from create_preview_deploy_key. The
5
+ // listing shows each key under a unique name, the requested name plus a short
6
+ // id suffix, and delete_preview_deploy_key accepts that unique name. A key
7
+ // cannot be changed after creation, so every property change is a replacement.
8
+ import { Resource } from "alchemy";
9
+ import { havePropsChanged, isResolved } from "alchemy/Diff";
10
+ import * as Provider from "alchemy/Provider";
11
+ import * as Effect from "effect/Effect";
12
+ import * as Redacted from "effect/Redacted";
13
+ import * as Schema from "effect/Schema";
14
+ import { ListedKey, selectCreatedKey } from "./DeployKey.ts";
15
+ import { absentAsUndefined, ManagementApi } from "./ManagementApi.ts";
16
+ import type { Providers } from "./Providers.ts";
17
+
18
+ /** GET /projects/{project_id}/list_preview_deploy_keys */
19
+ export const PreviewDeployKeyList = Schema.Struct({ items: Schema.Array(ListedKey) });
20
+
21
+ /** POST /projects/{project_id}/create_preview_deploy_key */
22
+ export const CreatedPreviewDeployKey = Schema.Struct({ previewDeployKey: Schema.String });
23
+
24
+ export interface PreviewDeployKeyProps {
25
+ /** Numeric project id, such as `project.projectId`. */
26
+ readonly projectId: number;
27
+ /** Requested key name. Convex appends a short id to make it unique. */
28
+ readonly name: string;
29
+ }
30
+
31
+ export interface PreviewDeployKeyAttributes {
32
+ /** The name Convex lists the key under, such as "ci (6308e666)". */
33
+ readonly uniqueName: string;
34
+ readonly projectId: number;
35
+ /** The secret, such as `preview:<team>:<project>|<token>`. Convex returns it only at creation. */
36
+ readonly previewDeployKey: Redacted.Redacted<string>;
37
+ }
38
+
39
+ export type PreviewDeployKey = Resource<
40
+ "Convex.PreviewDeployKey",
41
+ PreviewDeployKeyProps,
42
+ PreviewDeployKeyAttributes,
43
+ never,
44
+ Providers
45
+ >;
46
+ export const PreviewDeployKey = Resource<PreviewDeployKey>("Convex.PreviewDeployKey");
47
+
48
+ export const PreviewDeployKeyProvider = () =>
49
+ Provider.effect(
50
+ PreviewDeployKey,
51
+ Effect.gen(function* () {
52
+ const api = yield* ManagementApi;
53
+
54
+ const listKeys = (projectId: number) =>
55
+ api
56
+ .request("list preview deploy keys", (client) =>
57
+ client.GET("/projects/{project_id}/list_preview_deploy_keys", {
58
+ params: { path: { project_id: projectId } },
59
+ }),
60
+ )
61
+ .pipe(Effect.map((body) => Schema.decodeUnknownSync(PreviewDeployKeyList)(body).items));
62
+
63
+ return {
64
+ stables: ["uniqueName", "projectId"],
65
+
66
+ diff: ({ olds, news, output }) => {
67
+ if (!isResolved(news)) return Effect.succeed(undefined);
68
+ // State written without the secret cannot hand a key to dependents,
69
+ // and Convex cannot return it again, so only a new key helps.
70
+ return Effect.succeed(
71
+ (output !== undefined && output.previewDeployKey === undefined) ||
72
+ havePropsChanged(olds, news)
73
+ ? ({ action: "replace" } as const)
74
+ : ({ action: "noop" } as const),
75
+ );
76
+ },
77
+
78
+ read: Effect.fn(function* ({ output }) {
79
+ // A key that is not in state has no known secret, so it is never adopted.
80
+ if (output === undefined) return undefined;
81
+ const entries = yield* absentAsUndefined(listKeys(output.projectId));
82
+ return entries?.some((entry) => entry.name === output.uniqueName) ? output : undefined;
83
+ }),
84
+
85
+ reconcile: Effect.fn(function* ({ news, output }) {
86
+ if (output !== undefined) {
87
+ // The secret cannot be read back. A key that still exists keeps the one in state.
88
+ const entries = yield* listKeys(news.projectId);
89
+ if (entries.some((entry) => entry.name === output.uniqueName)) return output;
90
+ }
91
+ const created = yield* api.request("create preview deploy key", (client) =>
92
+ client.POST("/projects/{project_id}/create_preview_deploy_key", {
93
+ params: { path: { project_id: news.projectId } },
94
+ body: { name: news.name },
95
+ }),
96
+ );
97
+ const { previewDeployKey } = Schema.decodeUnknownSync(CreatedPreviewDeployKey)(created);
98
+ const listed = selectCreatedKey(yield* listKeys(news.projectId), news.name);
99
+ if (listed === undefined) {
100
+ return yield* Effect.die(
101
+ new Error(
102
+ `Convex created preview deploy key "${news.name}" in project ${news.projectId} but does not list it`,
103
+ ),
104
+ );
105
+ }
106
+ return {
107
+ uniqueName: listed.name,
108
+ projectId: news.projectId,
109
+ previewDeployKey: Redacted.make(previewDeployKey),
110
+ };
111
+ }),
112
+
113
+ delete: Effect.fn(function* ({ output }) {
114
+ // 404 PreviewDeployKeyNotFound: an earlier attempt or someone else deleted it.
115
+ yield* absentAsUndefined(
116
+ api.requestVoid("delete preview deploy key", (client) =>
117
+ client.POST("/projects/{project_id}/delete_preview_deploy_key", {
118
+ params: { path: { project_id: output.projectId } },
119
+ body: { id: output.uniqueName },
120
+ }),
121
+ ),
122
+ );
123
+ }),
124
+ };
125
+ }),
126
+ );
package/src/Project.ts ADDED
@@ -0,0 +1,191 @@
1
+ // Convex.Project: a project in a Convex team, with its default production deployment.
2
+ //
3
+ // The Management API has no project update endpoint, so every property change
4
+ // is a replacement. Deleting a project deletes all of its deployments and data.
5
+ import { Resource } from "alchemy";
6
+ import { Unowned } from "alchemy/AdoptPolicy";
7
+ import { isResolved } from "alchemy/Diff";
8
+ import * as Provider from "alchemy/Provider";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Schema from "effect/Schema";
11
+ import type { components } from "@convex-dev/platform/managementApi";
12
+ import { absentAsUndefined, ManagementApi } from "./ManagementApi.ts";
13
+ import type { Providers } from "./Providers.ts";
14
+
15
+ /** Region names the Management API accepts, from the generated schema. */
16
+ export type DeploymentRegion = components["schemas"]["RegionName"];
17
+
18
+ export interface ProjectProps {
19
+ /** Team slug or numeric team id. A slug costs one extra lookup per run. */
20
+ readonly team: string | number;
21
+ /** Display name. Convex derives the slug from it. */
22
+ readonly name: string;
23
+ /** Region of the deployment created with the project. Defaults to the team default. */
24
+ readonly deploymentRegion?: DeploymentRegion;
25
+ /** Deployment created with the project. Defaults to `prod`. */
26
+ readonly deploymentType?: "prod" | "dev";
27
+ }
28
+
29
+ export interface ProjectAttributes {
30
+ readonly projectId: number;
31
+ readonly slug: string;
32
+ readonly name: string;
33
+ readonly teamId: number;
34
+ readonly prodDeploymentName: string | undefined;
35
+ readonly prodDeploymentUrl: string | undefined;
36
+ }
37
+
38
+ export type Project = Resource<"Convex.Project", ProjectProps, ProjectAttributes, never, Providers>;
39
+ export const Project = Resource<Project>("Convex.Project");
40
+
41
+ /** Fields of GET /projects/{id} and GET /teams/{team}/projects that the provider uses. */
42
+ const ProjectDetails = Schema.Struct({
43
+ id: Schema.Number,
44
+ name: Schema.String,
45
+ slug: Schema.String,
46
+ teamId: Schema.Number,
47
+ prodDeploymentName: Schema.NullOr(Schema.String),
48
+ });
49
+ const ProjectList = Schema.Struct({ items: Schema.Array(ProjectDetails) });
50
+
51
+ const CreatedProject = Schema.Struct({
52
+ projectId: Schema.Number,
53
+ slug: Schema.String,
54
+ deploymentName: Schema.NullOr(Schema.String),
55
+ deploymentUrl: Schema.NullOr(Schema.String),
56
+ });
57
+
58
+ const DeploymentRow = Schema.Struct({
59
+ name: Schema.String,
60
+ deploymentType: Schema.String,
61
+ isDefault: Schema.Boolean,
62
+ deploymentUrl: Schema.String,
63
+ });
64
+ const DeploymentRows = Schema.Array(DeploymentRow);
65
+
66
+ export const ProjectProvider = () =>
67
+ Provider.effect(
68
+ Project,
69
+ Effect.gen(function* () {
70
+ const api = yield* ManagementApi;
71
+
72
+ const prodDeployment = (projectId: number) =>
73
+ api
74
+ .request("list deployments", (client) =>
75
+ client.GET("/projects/{project_id}/list_deployments", {
76
+ params: { path: { project_id: projectId } },
77
+ }),
78
+ )
79
+ .pipe(
80
+ Effect.map((rows) =>
81
+ Schema.decodeUnknownSync(DeploymentRows)(rows).find(
82
+ (row) => row.deploymentType === "prod" && row.isDefault,
83
+ ),
84
+ ),
85
+ );
86
+
87
+ const attributesOf = (details: typeof ProjectDetails.Type) =>
88
+ prodDeployment(details.id).pipe(
89
+ Effect.map((prod): ProjectAttributes => ({
90
+ projectId: details.id,
91
+ slug: details.slug,
92
+ name: details.name,
93
+ teamId: details.teamId,
94
+ prodDeploymentName: prod?.name ?? details.prodDeploymentName ?? undefined,
95
+ prodDeploymentUrl: prod?.deploymentUrl,
96
+ })),
97
+ );
98
+
99
+ const getById = (projectId: number) =>
100
+ absentAsUndefined(
101
+ api.request("get project", (client) =>
102
+ client.GET("/projects/{project_id}", { params: { path: { project_id: projectId } } }),
103
+ ),
104
+ ).pipe(
105
+ Effect.map((body) =>
106
+ body === undefined ? undefined : Schema.decodeUnknownSync(ProjectDetails)(body),
107
+ ),
108
+ );
109
+
110
+ const findByName = (team: string | number, name: string) =>
111
+ Effect.gen(function* () {
112
+ const teamId = yield* api.resolveTeamId(team);
113
+ const list = yield* api.request("list projects", (client) =>
114
+ client.GET("/teams/{team_id}/projects", { params: { path: { team_id: teamId } } }),
115
+ );
116
+ return Schema.decodeUnknownSync(ProjectList)(list).items.find(
117
+ (row) => row.name === name || row.slug === name,
118
+ );
119
+ });
120
+
121
+ return {
122
+ stables: ["projectId", "slug", "teamId"],
123
+
124
+ diff: ({ olds, news }) => {
125
+ if (!isResolved(news)) return Effect.succeed(undefined);
126
+ const changed =
127
+ olds.team !== news.team ||
128
+ olds.name !== news.name ||
129
+ olds.deploymentRegion !== news.deploymentRegion ||
130
+ olds.deploymentType !== news.deploymentType;
131
+ return Effect.succeed(
132
+ changed ? ({ action: "replace" } as const) : ({ action: "noop" } as const),
133
+ );
134
+ },
135
+
136
+ read: Effect.fn(function* ({ olds, output }) {
137
+ if (output !== undefined) {
138
+ const details = yield* getById(output.projectId);
139
+ return details === undefined ? undefined : yield* attributesOf(details);
140
+ }
141
+ // No state: an existing project with this name belongs to someone
142
+ // else until the user passes --adopt. Convex has no ownership tags.
143
+ const existing = yield* findByName(olds.team, olds.name);
144
+ return existing === undefined ? undefined : Unowned(yield* attributesOf(existing));
145
+ }),
146
+
147
+ reconcile: Effect.fn(function* ({ news, output }) {
148
+ if (output !== undefined) {
149
+ // Properties are all create-time; diff maps changes to replace.
150
+ const details = yield* getById(output.projectId);
151
+ if (details !== undefined) return yield* attributesOf(details);
152
+ }
153
+ // A crash after create and before state write is recovered by read;
154
+ // a project created by an earlier attempt is reused here too.
155
+ const existing = yield* findByName(news.team, news.name);
156
+ if (existing !== undefined) return yield* attributesOf(existing);
157
+ const teamId = yield* api.resolveTeamId(news.team);
158
+ const created = yield* api.request("create project", (client) =>
159
+ client.POST("/teams/{team_id}/create_project", {
160
+ params: { path: { team_id: teamId } },
161
+ body: {
162
+ projectName: news.name,
163
+ deploymentType: news.deploymentType ?? "prod",
164
+ ...(news.deploymentRegion === undefined
165
+ ? {}
166
+ : { deploymentRegion: news.deploymentRegion }),
167
+ },
168
+ }),
169
+ );
170
+ const parsed = Schema.decodeUnknownSync(CreatedProject)(created);
171
+ const details = yield* getById(parsed.projectId);
172
+ if (details === undefined) {
173
+ return yield* Effect.die(
174
+ new Error(`Convex created project ${parsed.projectId} but it cannot be read back`),
175
+ );
176
+ }
177
+ return yield* attributesOf(details);
178
+ }),
179
+
180
+ delete: Effect.fn(function* ({ output }) {
181
+ yield* absentAsUndefined(
182
+ api.requestVoid("delete project", (client) =>
183
+ client.POST("/projects/{project_id}/delete", {
184
+ params: { path: { project_id: output.projectId } },
185
+ }),
186
+ ),
187
+ );
188
+ }),
189
+ };
190
+ }),
191
+ );
@@ -0,0 +1,51 @@
1
+ // The provider collection users merge into a stack:
2
+ // providers: Layer.mergeAll(Cloudflare.providers(), Convex.providers())
3
+ import { CommandExecutorLive } from "alchemy/Command";
4
+ import * as Provider from "alchemy/Provider";
5
+ import * as Layer from "effect/Layer";
6
+ import { type Credentials, fromConfig } from "./Credentials.ts";
7
+ import {
8
+ DefaultEnvironmentVariable,
9
+ DefaultEnvironmentVariableProvider,
10
+ } from "./DefaultEnvironmentVariable.ts";
11
+ import { Deploy, DeployProvider } from "./Deploy.ts";
12
+ import { DeployKey, DeployKeyProvider } from "./DeployKey.ts";
13
+ import { EnvironmentVariable, EnvironmentVariableProvider } from "./EnvironmentVariable.ts";
14
+ import { ManagementApiLive } from "./ManagementApi.ts";
15
+ import { PreviewDeployKey, PreviewDeployKeyProvider } from "./PreviewDeployKey.ts";
16
+ import { Project, ProjectProvider } from "./Project.ts";
17
+
18
+ export class Providers extends Provider.ProviderCollection<Providers>()("Convex") {}
19
+
20
+ /**
21
+ * All Convex resource providers. Pass a credentials layer to override the
22
+ * default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login).
23
+ */
24
+ export const providers = (credentials: Layer.Layer<Credentials> = fromConfig()) =>
25
+ Layer.effect(
26
+ Providers,
27
+ Provider.collection([
28
+ Project,
29
+ DeployKey,
30
+ PreviewDeployKey,
31
+ EnvironmentVariable,
32
+ DefaultEnvironmentVariable,
33
+ Deploy,
34
+ ]),
35
+ ).pipe(
36
+ Layer.provide(
37
+ Layer.mergeAll(
38
+ ProjectProvider(),
39
+ DeployKeyProvider(),
40
+ PreviewDeployKeyProvider(),
41
+ EnvironmentVariableProvider(),
42
+ DefaultEnvironmentVariableProvider(),
43
+ DeployProvider(),
44
+ ),
45
+ ),
46
+ Layer.provide(ManagementApiLive()),
47
+ Layer.provide(credentials),
48
+ // Convex.Deploy runs the Convex CLI through Alchemy's command executor.
49
+ Layer.provide(CommandExecutorLive()),
50
+ Layer.orDie,
51
+ );
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ // @samebase/alchemy-convex: Convex resources for Alchemy v2.
2
+ export {
3
+ ACCESS_TOKEN_ENV,
4
+ Credentials,
5
+ CredentialsError,
6
+ fromConfig,
7
+ fromToken,
8
+ } from "./Credentials.ts";
9
+ export {
10
+ DefaultEnvironmentVariable,
11
+ type DefaultEnvironmentVariableAttributes,
12
+ type DefaultEnvironmentVariableProps,
13
+ type DeploymentType,
14
+ } from "./DefaultEnvironmentVariable.ts";
15
+ export {
16
+ Deploy,
17
+ type DeployAttributes,
18
+ deployArgs,
19
+ type DeployProps,
20
+ parseDeployOutput,
21
+ } from "./Deploy.ts";
22
+ export {
23
+ DeployKey,
24
+ type DeployKeyAction,
25
+ type DeployKeyAttributes,
26
+ type DeployKeyProps,
27
+ } from "./DeployKey.ts";
28
+ export {
29
+ EnvironmentVariable,
30
+ type EnvironmentVariableAttributes,
31
+ type EnvironmentVariableProps,
32
+ } from "./EnvironmentVariable.ts";
33
+ export { ConvexApiError, ManagementApi } from "./ManagementApi.ts";
34
+ export {
35
+ PreviewDeployKey,
36
+ type PreviewDeployKeyAttributes,
37
+ type PreviewDeployKeyProps,
38
+ } from "./PreviewDeployKey.ts";
39
+ export {
40
+ type DeploymentRegion,
41
+ Project,
42
+ type ProjectAttributes,
43
+ type ProjectProps,
44
+ } from "./Project.ts";
45
+ export { Providers, providers } from "./Providers.ts";