@samebase/alchemy-convex 0.1.0 → 0.2.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 (51) hide show
  1. package/README.md +85 -17
  2. package/lib/index.d.ts +352 -9
  3. package/lib/index.d.ts.map +1 -1
  4. package/lib/index.js +962 -10
  5. package/lib/index.js.map +1 -1
  6. package/package.json +7 -4
  7. package/src/Adoption.ts +30 -0
  8. package/src/DefaultEnvironmentVariable.ts +127 -29
  9. package/src/Deploy.ts +37 -15
  10. package/src/DeployKey.ts +221 -60
  11. package/src/EnvironmentVariable.ts +174 -41
  12. package/src/ManagementApi.ts +57 -1
  13. package/src/PreviewDeployKey.ts +65 -38
  14. package/src/Project.ts +236 -52
  15. package/src/index.ts +9 -0
  16. package/lib/Credentials.d.ts +0 -39
  17. package/lib/Credentials.d.ts.map +0 -1
  18. package/lib/Credentials.js +0 -49
  19. package/lib/Credentials.js.map +0 -1
  20. package/lib/DefaultEnvironmentVariable.d.ts +0 -58
  21. package/lib/DefaultEnvironmentVariable.d.ts.map +0 -1
  22. package/lib/DefaultEnvironmentVariable.js +0 -93
  23. package/lib/DefaultEnvironmentVariable.js.map +0 -1
  24. package/lib/Deploy.d.ts +0 -62
  25. package/lib/Deploy.d.ts.map +0 -1
  26. package/lib/Deploy.js +0 -92
  27. package/lib/Deploy.js.map +0 -1
  28. package/lib/DeployKey.d.ts +0 -48
  29. package/lib/DeployKey.d.ts.map +0 -1
  30. package/lib/DeployKey.js +0 -104
  31. package/lib/DeployKey.js.map +0 -1
  32. package/lib/EnvironmentVariable.d.ts +0 -37
  33. package/lib/EnvironmentVariable.d.ts.map +0 -1
  34. package/lib/EnvironmentVariable.js +0 -65
  35. package/lib/EnvironmentVariable.js.map +0 -1
  36. package/lib/ManagementApi.d.ts +0 -50
  37. package/lib/ManagementApi.d.ts.map +0 -1
  38. package/lib/ManagementApi.js +0 -103
  39. package/lib/ManagementApi.js.map +0 -1
  40. package/lib/PreviewDeployKey.d.ts +0 -34
  41. package/lib/PreviewDeployKey.d.ts.map +0 -1
  42. package/lib/PreviewDeployKey.js +0 -78
  43. package/lib/PreviewDeployKey.js.map +0 -1
  44. package/lib/Project.d.ts +0 -29
  45. package/lib/Project.d.ts.map +0 -1
  46. package/lib/Project.js +0 -114
  47. package/lib/Project.js.map +0 -1
  48. package/lib/Providers.d.ts +0 -13
  49. package/lib/Providers.d.ts.map +0 -1
  50. package/lib/Providers.js +0 -30
  51. package/lib/Providers.js.map +0 -1
package/src/Project.ts CHANGED
@@ -1,28 +1,39 @@
1
1
  // Convex.Project: a project in a Convex team, with its default production deployment.
2
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.
3
+ // Deleting a project deletes all of its deployments and data, so this
4
+ // resource never replaces a project and keeps it by default:
5
+ //
6
+ // - A name change is an update in place: PATCH /projects/{project_id} with
7
+ // the new name. The slug stays the same.
8
+ // - `team`, `deploymentType`, and `deploymentRegion` cannot change on an
9
+ // existing project. A change fails with a typed error that names the fix.
10
+ // - The removal policy is `retain`. Alchemy deletes the project only when the
11
+ // resource has `RemovalPolicy.destroy()`.
12
+ // - With state, every call addresses the project by `output.projectId`. Only
13
+ // a resource without state looks a project up by name, and it takes over a
14
+ // project that it finds only when adoption is on.
5
15
  import { Resource } from "alchemy";
6
- import { Unowned } from "alchemy/AdoptPolicy";
16
+ import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
7
17
  import { isResolved } from "alchemy/Diff";
8
18
  import * as Provider from "alchemy/Provider";
9
19
  import * as Effect from "effect/Effect";
10
20
  import * as Schema from "effect/Schema";
11
21
  import type { components } from "@convex-dev/platform/managementApi";
12
- import { absentAsUndefined, ManagementApi } from "./ManagementApi.ts";
22
+ import { shouldAdopt } from "./Adoption.ts";
23
+ import { absentAsUndefined, ConvexApiError, ManagementApi } from "./ManagementApi.ts";
13
24
  import type { Providers } from "./Providers.ts";
14
25
 
15
26
  /** Region names the Management API accepts, from the generated schema. */
16
27
  export type DeploymentRegion = components["schemas"]["RegionName"];
17
28
 
18
29
  export interface ProjectProps {
19
- /** Team slug or numeric team id. A slug costs one extra lookup per run. */
30
+ /** Team slug or numeric team id. A slug costs one extra lookup per run. Cannot change. */
20
31
  readonly team: string | number;
21
- /** Display name. Convex derives the slug from it. */
32
+ /** Display name. Convex derives the slug from it at creation. A change renames the project. */
22
33
  readonly name: string;
23
- /** Region of the deployment created with the project. Defaults to the team default. */
34
+ /** Region of the deployment created with the project. Defaults to the team default. Cannot change. */
24
35
  readonly deploymentRegion?: DeploymentRegion;
25
- /** Deployment created with the project. Defaults to `prod`. */
36
+ /** Deployment created with the project. Defaults to `prod`. Cannot change. */
26
37
  readonly deploymentType?: "prod" | "dev";
27
38
  }
28
39
 
@@ -36,9 +47,59 @@ export interface ProjectAttributes {
36
47
  }
37
48
 
38
49
  export type Project = Resource<"Convex.Project", ProjectProps, ProjectAttributes, never, Providers>;
39
- export const Project = Resource<Project>("Convex.Project");
50
+ export const Project = Resource<Project>("Convex.Project", { defaultRemovalPolicy: "retain" });
51
+
52
+ /** The props change the team of an existing project. Convex cannot move a project. */
53
+ export class ProjectTeamChange extends Schema.TaggedError<ProjectTeamChange>()(
54
+ "ProjectTeamChange",
55
+ { projectId: Schema.Number, teamId: Schema.Number, requestedTeamId: Schema.Number },
56
+ ) {
57
+ override get message() {
58
+ return `Convex.Project cannot move project ${this.projectId} from team ${this.teamId} to team ${this.requestedTeamId}. Set team back to ${this.teamId}. To use the other team, create a new resource with a new logical id and move the data yourself.`;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * The props change `deploymentType` or `deploymentRegion` of an existing
64
+ * project. Convex uses them only when it creates the project.
65
+ */
66
+ export class ProjectDeploymentChange extends Schema.TaggedError<ProjectDeploymentChange>()(
67
+ "ProjectDeploymentChange",
68
+ {
69
+ projectId: Schema.Number,
70
+ field: Schema.Literals(["deploymentType", "deploymentRegion"]),
71
+ current: Schema.UndefinedOr(Schema.String),
72
+ requested: Schema.UndefinedOr(Schema.String),
73
+ },
74
+ ) {
75
+ override get message() {
76
+ const show = (value: string | undefined) => (value === undefined ? "(not set)" : value);
77
+ return `Convex.Project cannot change ${this.field} of project ${this.projectId} from ${show(this.current)} to ${show(this.requested)}: Convex uses it only when it creates a project. Set ${this.field} back to ${show(this.current)}. For a new deployment, create a new resource with a new logical id and move the data yourself.`;
78
+ }
79
+ }
80
+
81
+ /** More than one project in the team has the name, so the lookup cannot choose one. */
82
+ export class AmbiguousProject extends Schema.TaggedError<AmbiguousProject>()("AmbiguousProject", {
83
+ teamId: Schema.Number,
84
+ name: Schema.String,
85
+ projectIds: Schema.Array(Schema.Number),
86
+ }) {
87
+ override get message() {
88
+ return `Team ${this.teamId} has ${this.projectIds.length} projects named "${this.name}" (ids ${this.projectIds.join(", ")}). Convex.Project does not choose one. Rename the projects in the Convex dashboard so that only one has this name, or use a different name.`;
89
+ }
90
+ }
91
+
92
+ /** Convex answered a create call but then did not return the project. */
93
+ export class CreatedProjectNotFound extends Schema.TaggedError<CreatedProjectNotFound>()(
94
+ "CreatedProjectNotFound",
95
+ { projectId: Schema.Number },
96
+ ) {
97
+ override get message() {
98
+ return `Convex created project ${this.projectId} but GET /projects/${this.projectId} did not return it. Run the deploy again with --adopt to take over project ${this.projectId}.`;
99
+ }
100
+ }
40
101
 
41
- /** Fields of GET /projects/{id} and GET /teams/{team}/projects that the provider uses. */
102
+ /** Fields of GET /projects/{id}, PATCH /projects/{id}, and GET /teams/{team}/projects rows that the provider uses. */
42
103
  const ProjectDetails = Schema.Struct({
43
104
  id: Schema.Number,
44
105
  name: Schema.String,
@@ -46,15 +107,20 @@ const ProjectDetails = Schema.Struct({
46
107
  teamId: Schema.Number,
47
108
  prodDeploymentName: Schema.NullOr(Schema.String),
48
109
  });
49
- const ProjectList = Schema.Struct({ items: Schema.Array(ProjectDetails) });
110
+ type ProjectDetails = typeof ProjectDetails.Type;
50
111
 
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),
112
+ /** GET /teams/{team_id}/projects: one page of projects. */
113
+ export const ProjectPage = Schema.Struct({
114
+ items: Schema.Array(ProjectDetails),
115
+ pagination: Schema.Struct({
116
+ hasMore: Schema.Boolean,
117
+ nextCursor: Schema.optionalKey(Schema.NullOr(Schema.String)),
118
+ }),
56
119
  });
57
120
 
121
+ /** POST /teams/{team_id}/create_project. Only the id: everything else comes from GET /projects/{id}. */
122
+ const CreatedProject = Schema.Struct({ projectId: Schema.Number });
123
+
58
124
  const DeploymentRow = Schema.Struct({
59
125
  name: Schema.String,
60
126
  deploymentType: Schema.String,
@@ -63,6 +129,36 @@ const DeploymentRow = Schema.Struct({
63
129
  });
64
130
  const DeploymentRows = Schema.Array(DeploymentRow);
65
131
 
132
+ /**
133
+ * Fails with {@link ProjectDeploymentChange} when a create-time prop
134
+ * changed. An omitted `deploymentType` means `prod`.
135
+ */
136
+ const checkCreateTimeProps = (projectId: number, olds: ProjectProps, news: ProjectProps) => {
137
+ const oldType = olds.deploymentType ?? "prod";
138
+ const newType = news.deploymentType ?? "prod";
139
+ if (oldType !== newType) {
140
+ return Effect.fail(
141
+ new ProjectDeploymentChange({
142
+ projectId,
143
+ field: "deploymentType",
144
+ current: oldType,
145
+ requested: newType,
146
+ }),
147
+ );
148
+ }
149
+ if (olds.deploymentRegion !== news.deploymentRegion) {
150
+ return Effect.fail(
151
+ new ProjectDeploymentChange({
152
+ projectId,
153
+ field: "deploymentRegion",
154
+ current: olds.deploymentRegion,
155
+ requested: news.deploymentRegion,
156
+ }),
157
+ );
158
+ }
159
+ return Effect.void;
160
+ };
161
+
66
162
  export const ProjectProvider = () =>
67
163
  Provider.effect(
68
164
  Project,
@@ -84,7 +180,7 @@ export const ProjectProvider = () =>
84
180
  ),
85
181
  );
86
182
 
87
- const attributesOf = (details: typeof ProjectDetails.Type) =>
183
+ const attributesOf = (details: ProjectDetails) =>
88
184
  prodDeployment(details.id).pipe(
89
185
  Effect.map((prod): ProjectAttributes => ({
90
186
  projectId: details.id,
@@ -107,31 +203,100 @@ export const ProjectProvider = () =>
107
203
  ),
108
204
  );
109
205
 
110
- const findByName = (team: string | number, name: string) =>
206
+ /**
207
+ * The one project in the team with exactly this name, or undefined.
208
+ * Reads every page. `q` narrows the listing on the server, and the
209
+ * exact match here decides. Pattern from Confect
210
+ * `packages/alchemy/src/ConvexClient.ts` and `Project.ts`
211
+ * (https://github.com/rjdellecese/confect, ISC license).
212
+ */
213
+ const findByName = (teamId: number, name: string) =>
111
214
  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,
215
+ const matches: ProjectDetails[] = [];
216
+ const seenCursors = new Set<string>();
217
+ let cursor: string | undefined;
218
+ while (true) {
219
+ const body = yield* api.request("list projects", (client) =>
220
+ client.GET("/teams/{team_id}/projects", {
221
+ params: {
222
+ path: { team_id: teamId },
223
+ query: { q: name, ...(cursor === undefined ? {} : { cursor }) },
224
+ },
225
+ }),
226
+ );
227
+ const page = Schema.decodeUnknownSync(ProjectPage)(body);
228
+ matches.push(...page.items.filter((row) => row.name === name));
229
+ if (!page.pagination.hasMore) break;
230
+ const next = page.pagination.nextCursor;
231
+ if (next === undefined || next === null || seenCursors.has(next)) {
232
+ return yield* new ConvexApiError({
233
+ operation: "list projects",
234
+ status: 0,
235
+ code: "InvalidPagination",
236
+ message: `The project listing of team ${teamId} reports more pages but gives no new cursor. Convex.Project stops so that it does not miss a project named "${name}".`,
237
+ });
238
+ }
239
+ seenCursors.add(next);
240
+ cursor = next;
241
+ }
242
+ if (matches.length > 1) {
243
+ return yield* new AmbiguousProject({
244
+ teamId,
245
+ name,
246
+ projectIds: matches.map((row) => row.id),
247
+ });
248
+ }
249
+ return matches[0];
250
+ });
251
+
252
+ /**
253
+ * Brings an existing project to the desired name. The team cannot
254
+ * change, so a project in another team fails.
255
+ */
256
+ const converge = (details: ProjectDetails, teamId: number, name: string) =>
257
+ Effect.gen(function* () {
258
+ if (details.teamId !== teamId) {
259
+ return yield* new ProjectTeamChange({
260
+ projectId: details.id,
261
+ teamId: details.teamId,
262
+ requestedTeamId: teamId,
263
+ });
264
+ }
265
+ if (details.name === name) return yield* attributesOf(details);
266
+ const updated = yield* api.request("update project", (client) =>
267
+ client.PATCH("/projects/{project_id}", {
268
+ params: { path: { project_id: details.id } },
269
+ body: { name },
270
+ }),
118
271
  );
272
+ return yield* attributesOf(Schema.decodeUnknownSync(ProjectDetails)(updated));
119
273
  });
120
274
 
121
275
  return {
122
276
  stables: ["projectId", "slug", "teamId"],
123
277
 
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
- },
278
+ diff: Effect.fn(function* ({ olds, news, output }) {
279
+ if (!isResolved(news)) return undefined;
280
+ // Nothing exists yet, so nothing can be lost.
281
+ if (output === undefined) return undefined;
282
+ // A deploymentType or deploymentRegion change fails in reconcile,
283
+ // before any API call. Reconcile compares with the props of the last
284
+ // successful deploy. Here, `olds` can be the props of a refused
285
+ // update, so a check here would also refuse the props that fix it.
286
+ if (olds.team !== news.team) {
287
+ const requestedTeamId = yield* api.resolveTeamId(news.team);
288
+ if (requestedTeamId !== output.teamId) {
289
+ return yield* new ProjectTeamChange({
290
+ projectId: output.projectId,
291
+ teamId: output.teamId,
292
+ requestedTeamId,
293
+ });
294
+ }
295
+ }
296
+ // Never "replace". A name change is an update; reconcile renames
297
+ // the project with PATCH. Otherwise the engine compares the props.
298
+ return olds.name === news.name ? undefined : ({ action: "update" } as const);
299
+ }),
135
300
 
136
301
  read: Effect.fn(function* ({ olds, output }) {
137
302
  if (output !== undefined) {
@@ -140,21 +305,42 @@ export const ProjectProvider = () =>
140
305
  }
141
306
  // No state: an existing project with this name belongs to someone
142
307
  // else until the user passes --adopt. Convex has no ownership tags.
143
- const existing = yield* findByName(olds.team, olds.name);
308
+ const existing = yield* findByName(yield* api.resolveTeamId(olds.team), olds.name);
144
309
  return existing === undefined ? undefined : Unowned(yield* attributesOf(existing));
145
310
  }),
146
311
 
147
- reconcile: Effect.fn(function* ({ news, output }) {
312
+ reconcile: Effect.fn(function* ({ fqn, olds, news, output }) {
313
+ const teamId = yield* api.resolveTeamId(news.team);
148
314
  if (output !== undefined) {
149
- // Properties are all create-time; diff maps changes to replace.
315
+ // State names the project. Never look it up by name here.
316
+ if (olds !== undefined) yield* checkCreateTimeProps(output.projectId, olds, news);
317
+ if (output.teamId !== teamId) {
318
+ return yield* new ProjectTeamChange({
319
+ projectId: output.projectId,
320
+ teamId: output.teamId,
321
+ requestedTeamId: teamId,
322
+ });
323
+ }
150
324
  const details = yield* getById(output.projectId);
151
- if (details !== undefined) return yield* attributesOf(details);
325
+ if (details !== undefined) return yield* converge(details, teamId, news.name);
326
+ // The project in state was deleted outside Alchemy. Creating a
327
+ // new project below loses nothing.
328
+ } else {
329
+ // No state: a project with this name is someone else's unless
330
+ // adoption is on. The engine checked this in plan; this check
331
+ // covers a project that appeared after the plan.
332
+ const existing = yield* findByName(teamId, news.name);
333
+ if (existing !== undefined) {
334
+ if (!(yield* shouldAdopt(fqn))) {
335
+ return yield* new OwnedBySomeoneElse({
336
+ message: `Convex project ${existing.id} in team ${teamId} already has the name "${news.name}". Re-run with --adopt to take it over, or use a different name.`,
337
+ resourceType: Project.Type,
338
+ physicalName: news.name,
339
+ });
340
+ }
341
+ return yield* converge(existing, teamId, news.name);
342
+ }
152
343
  }
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
344
  const created = yield* api.request("create project", (client) =>
159
345
  client.POST("/teams/{team_id}/create_project", {
160
346
  params: { path: { team_id: teamId } },
@@ -167,16 +353,14 @@ export const ProjectProvider = () =>
167
353
  },
168
354
  }),
169
355
  );
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);
356
+ // Identity comes from the create response, never from a name lookup.
357
+ const { projectId } = Schema.decodeUnknownSync(CreatedProject)(created);
358
+ const details = yield* getById(projectId);
359
+ if (details === undefined) return yield* new CreatedProjectNotFound({ projectId });
360
+ return yield* converge(details, teamId, news.name);
178
361
  }),
179
362
 
363
+ // Runs only with `RemovalPolicy.destroy()`: the default policy is `retain`.
180
364
  delete: Effect.fn(function* ({ output }) {
181
365
  yield* absentAsUndefined(
182
366
  api.requestVoid("delete project", (client) =>
package/src/index.ts CHANGED
@@ -14,8 +14,10 @@ export {
14
14
  } from "./DefaultEnvironmentVariable.ts";
15
15
  export {
16
16
  Deploy,
17
+ DeployArgumentError,
17
18
  type DeployAttributes,
18
19
  deployArgs,
20
+ DeployOutputError,
19
21
  type DeployProps,
20
22
  parseDeployOutput,
21
23
  } from "./Deploy.ts";
@@ -23,12 +25,15 @@ export {
23
25
  DeployKey,
24
26
  type DeployKeyAction,
25
27
  type DeployKeyAttributes,
28
+ DeployKeyIsCredential,
26
29
  type DeployKeyProps,
30
+ DeployKeyRecoveryRequired,
27
31
  } from "./DeployKey.ts";
28
32
  export {
29
33
  EnvironmentVariable,
30
34
  type EnvironmentVariableAttributes,
31
35
  type EnvironmentVariableProps,
36
+ VariableIdentityChange,
32
37
  } from "./EnvironmentVariable.ts";
33
38
  export { ConvexApiError, ManagementApi } from "./ManagementApi.ts";
34
39
  export {
@@ -37,9 +42,13 @@ export {
37
42
  type PreviewDeployKeyProps,
38
43
  } from "./PreviewDeployKey.ts";
39
44
  export {
45
+ AmbiguousProject,
46
+ CreatedProjectNotFound,
40
47
  type DeploymentRegion,
41
48
  Project,
49
+ ProjectDeploymentChange,
42
50
  type ProjectAttributes,
43
51
  type ProjectProps,
52
+ ProjectTeamChange,
44
53
  } from "./Project.ts";
45
54
  export { Providers, providers } from "./Providers.ts";
@@ -1,39 +0,0 @@
1
- import * as Context from "effect/Context";
2
- import * as Effect from "effect/Effect";
3
- import * as Layer from "effect/Layer";
4
- import * as Redacted from "effect/Redacted";
5
- import * as Schema from "effect/Schema";
6
- export interface ResolvedCredentials {
7
- /** Bearer token accepted by https://api.convex.dev/v1: a team access token, an OAuth token, or the Convex CLI login. */
8
- readonly accessToken: Redacted.Redacted<string>;
9
- /** Where the token came from, for error messages only. */
10
- readonly source: "env" | "explicit" | "convex-cli-login";
11
- }
12
- declare const Credentials_base: Context.ServiceClass<Credentials, "@samebase/alchemy-convex/Credentials", Effect.Effect<ResolvedCredentials, never, never>>;
13
- export declare class Credentials extends Credentials_base {
14
- }
15
- /** Environment variable read by {@link fromConfig}. */
16
- export declare const ACCESS_TOKEN_ENV = "CONVEX_ACCESS_TOKEN";
17
- /** The Convex CLI stores its device login here after `npx convex login`. */
18
- export declare const convexCliConfigPath: () => string;
19
- /**
20
- * Token the Convex CLI saved on this machine. It works on the Management API
21
- * for the teams the logged-in user belongs to. Fails when nobody is logged in.
22
- */
23
- export declare const readConvexCliLogin: Effect.Effect<ResolvedCredentials, CredentialsError>;
24
- declare const CredentialsError_base: Schema.Class<CredentialsError, Schema.TaggedStruct<"CredentialsError", {
25
- readonly reason: Schema.String;
26
- }>, import("effect/Cause").YieldableError>;
27
- export declare class CredentialsError extends CredentialsError_base {
28
- }
29
- /**
30
- * Default resolution: `CONVEX_ACCESS_TOKEN` from the stack ConfigProvider
31
- * (process env, `.env`, stack secrets), then the Convex CLI login on this
32
- * machine. CI sets the variable; a developer who ran `npx convex login` needs
33
- * nothing.
34
- */
35
- export declare const fromConfig: () => Layer.Layer<Credentials>;
36
- /** Explicit token, for tests or callers that already hold a value. */
37
- export declare const fromToken: (accessToken: Redacted.Redacted<string>) => Layer.Layer<Credentials>;
38
- export {};
39
- //# sourceMappingURL=Credentials.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Credentials.d.ts","sourceRoot":"","sources":["../src/Credentials.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,MAAM,WAAW,mBAAmB;IAClC,wHAAwH;IACxH,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChD,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,UAAU,GAAG,kBAAkB,CAAC;CAC1D;;AAED,qBAAa,WAAY,SAAQ,gBAEhC;CAAG;AAEJ,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,wBAAwB,CAAC;AAEtD,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,cAAkD,CAAC;AAInF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAAC,mBAAmB,EAAE,gBAAgB,CAYnF,CAAC;;;;AAEF,qBAAa,gBAAiB,SAAQ,qBAEpC;CAAG;AAEL;;;;;GAKG;AACH,eAAO,MAAM,UAAU,QAAO,KAAK,CAAC,KAAK,CAAC,WAAW,CAUlD,CAAC;AAEJ,sEAAsE;AACtE,eAAO,MAAM,SAAS,GAAI,aAAa,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAG,KAAK,CAAC,KAAK,CAAC,WAAW,CACA,CAAC"}
@@ -1,49 +0,0 @@
1
- // Convex control-plane credentials for the provider.
2
- //
3
- // The service value is an Effect so that building the providers layer never
4
- // touches the environment or the filesystem. The token is read on the first
5
- // lifecycle call, which keeps `alchemy plan` on an unrelated stack free of
6
- // Convex credential errors.
7
- import { readFile } from "node:fs/promises";
8
- import { homedir } from "node:os";
9
- import { join } from "node:path";
10
- import * as Config from "effect/Config";
11
- import * as Context from "effect/Context";
12
- import * as Effect from "effect/Effect";
13
- import * as Layer from "effect/Layer";
14
- import * as Redacted from "effect/Redacted";
15
- import * as Schema from "effect/Schema";
16
- export class Credentials extends Context.Service()("@samebase/alchemy-convex/Credentials") {
17
- }
18
- /** Environment variable read by {@link fromConfig}. */
19
- export const ACCESS_TOKEN_ENV = "CONVEX_ACCESS_TOKEN";
20
- /** The Convex CLI stores its device login here after `npx convex login`. */
21
- export const convexCliConfigPath = () => join(homedir(), ".convex", "config.json");
22
- const CliConfig = Schema.Struct({ accessToken: Schema.String });
23
- /**
24
- * Token the Convex CLI saved on this machine. It works on the Management API
25
- * for the teams the logged-in user belongs to. Fails when nobody is logged in.
26
- */
27
- export const readConvexCliLogin = Effect.gen(function* () {
28
- const path = convexCliConfigPath();
29
- const text = yield* Effect.tryPromise({
30
- try: () => readFile(path, "utf8"),
31
- catch: () => new CredentialsError({ reason: `no Convex CLI login at ${path}` }),
32
- });
33
- const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(CliConfig))(text).pipe(Effect.mapError(() => new CredentialsError({ reason: `${path} has no accessToken` })));
34
- return { accessToken: Redacted.make(parsed.accessToken), source: "convex-cli-login" };
35
- });
36
- export class CredentialsError extends Schema.TaggedError()("CredentialsError", {
37
- reason: Schema.String,
38
- }) {
39
- }
40
- /**
41
- * Default resolution: `CONVEX_ACCESS_TOKEN` from the stack ConfigProvider
42
- * (process env, `.env`, stack secrets), then the Convex CLI login on this
43
- * machine. CI sets the variable; a developer who ran `npx convex login` needs
44
- * nothing.
45
- */
46
- export const fromConfig = () => Layer.succeed(Credentials, Effect.cached(Config.Redacted(ACCESS_TOKEN_ENV).pipe(Effect.map((accessToken) => ({ accessToken, source: "env" })), Effect.catch(() => readConvexCliLogin), Effect.orDie)).pipe(Effect.flatten));
47
- /** Explicit token, for tests or callers that already hold a value. */
48
- export const fromToken = (accessToken) => Layer.succeed(Credentials, Effect.succeed({ accessToken, source: "explicit" }));
49
- //# sourceMappingURL=Credentials.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Credentials.js","sourceRoot":"","sources":["../src/Credentials.ts"],"names":[],"mappings":"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"}
@@ -1,58 +0,0 @@
1
- import { Resource } from "alchemy";
2
- import * as Provider from "alchemy/Provider";
3
- import * as Effect from "effect/Effect";
4
- import * as Redacted from "effect/Redacted";
5
- import * as Schema from "effect/Schema";
6
- import type { components } from "@convex-dev/platform/managementApi";
7
- import { ManagementApi, type ManagementApiService } from "./ManagementApi.ts";
8
- import type { Providers } from "./Providers.ts";
9
- /** Deployment types a default can target, from the generated schema. */
10
- export type DeploymentType = components["schemas"]["DeploymentType"];
11
- /**
12
- * Attributes when the listing has `name` for `deploymentType`. An unfiltered
13
- * listing merges the deployment types that share one value into one row, so
14
- * the match checks membership in `deploymentTypes`.
15
- */
16
- export declare const findDefault: (list: typeof DefaultEnvironmentVariableList.Type, projectId: number, name: string, deploymentType: DeploymentType) => DefaultEnvironmentVariableAttributes | undefined;
17
- /**
18
- * Lists the defaults named `name` for `deploymentType`. Convex filters on
19
- * both, so the listing has at most one row and needs no pagination.
20
- */
21
- export declare const listDefaults: (api: ManagementApiService, projectId: number, name: string, deploymentType: DeploymentType) => Effect.Effect<{
22
- readonly items: readonly {
23
- readonly name: string;
24
- readonly deploymentTypes: readonly string[];
25
- }[];
26
- }, import("./ManagementApi.ts").ConvexApiError, never>;
27
- /**
28
- * Sets one default, or removes it when `value` is null. Convex answers 200
29
- * with an empty body, so this reads no response data.
30
- */
31
- export declare const updateDefault: (api: ManagementApiService, projectId: number, name: string, deploymentType: DeploymentType, value: string | Redacted.Redacted<string> | null) => Effect.Effect<void, import("./ManagementApi.ts").ConvexApiError, never>;
32
- /**
33
- * GET /projects/{project_id}/list_default_environment_variables. Only the
34
- * fields the provider uses: the row's `value` is the secret in plain text.
35
- */
36
- export declare const DefaultEnvironmentVariableList: Schema.Struct<{
37
- readonly items: Schema.$Array<Schema.Struct<{
38
- readonly name: Schema.String;
39
- readonly deploymentTypes: Schema.$Array<Schema.String>;
40
- }>>;
41
- }>;
42
- export interface DefaultEnvironmentVariableProps {
43
- /** Numeric project id, such as `project.projectId`. */
44
- readonly projectId: number;
45
- readonly name: string;
46
- readonly value: string | Redacted.Redacted<string>;
47
- /** Deployments of this type that Convex creates from now on get the variable. */
48
- readonly deploymentType: DeploymentType;
49
- }
50
- export interface DefaultEnvironmentVariableAttributes {
51
- readonly projectId: number;
52
- readonly name: string;
53
- readonly deploymentType: DeploymentType;
54
- }
55
- export type DefaultEnvironmentVariable = Resource<"Convex.DefaultEnvironmentVariable", DefaultEnvironmentVariableProps, DefaultEnvironmentVariableAttributes, never, Providers>;
56
- export declare const DefaultEnvironmentVariable: import("alchemy").ResourceClass<DefaultEnvironmentVariable>;
57
- export declare const DefaultEnvironmentVariableProvider: () => import("effect/Layer").Layer<Provider.Provider<DefaultEnvironmentVariable>, never, ManagementApi>;
58
- //# sourceMappingURL=DefaultEnvironmentVariable.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"DefaultEnvironmentVariable.d.ts","sourceRoot":"","sources":["../src/DefaultEnvironmentVariable.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,EAAqB,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACjG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,wEAAwE;AACxE,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,gBAAgB,CAAC,CAAC;AAErE;;;;GAIG;AACH,eAAO,MAAM,WAAW,GACtB,MAAM,OAAO,8BAA8B,CAAC,IAAI,EAChD,WAAW,MAAM,EACjB,MAAM,MAAM,EACZ,gBAAgB,cAAc,KAC7B,oCAAoC,GAAG,SAG3B,CAAC;AAEhB;;;GAGG;AACH,eAAO,MAAM,YAAY,GACvB,KAAK,oBAAoB,EACzB,WAAW,MAAM,EACjB,MAAM,MAAM,EACZ,gBAAgB,cAAc;;;;;sDAQ+D,CAAC;AAEhG;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,KAAK,oBAAoB,EACzB,WAAW,MAAM,EACjB,MAAM,MAAM,EACZ,gBAAgB,cAAc,EAC9B,OAAO,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,4EAe/C,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,8BAA8B;;;;;EAIzC,CAAC;AAEH,MAAM,WAAW,+BAA+B;IAC9C,uDAAuD;IACvD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnD,iFAAiF;IACjF,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC;AAED,MAAM,WAAW,oCAAoC;IACnD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC;AAED,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAC/C,mCAAmC,EACnC,+BAA+B,EAC/B,oCAAoC,EACpC,KAAK,EACL,SAAS,CACV,CAAC;AACF,eAAO,MAAM,0BAA0B,6DAEtC,CAAC;AAEF,eAAO,MAAM,kCAAkC,yGAgD5C,CAAC"}