@samebase/alchemy-convex 0.12.0 → 0.14.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.
@@ -0,0 +1,212 @@
1
+ // The functions of the state store, for each app that serves the store. Two
2
+ // apps do: the store app of this package (src/store/convex), which
3
+ // Convex.state() pushes to the samebase project of a team, and the base app of
4
+ // a team, which mounts the alchemyState component next to its own components
5
+ // in the same project. A push replaces every function of a deployment, so both
6
+ // apps serve these functions with the same names. Exported as
7
+ // `@samebase/alchemy-convex/state/functions`.
8
+ //
9
+ // Each `state:*` function forwards to the component function of the same
10
+ // name, with the same results; an absent `run` or `stage` goes to the
11
+ // component as null. These names and arguments are the wire contract of
12
+ // STORE_VERSION. Samebase reads `stacks`, `getOutput`, `readStack`, and
13
+ // `reader:repository` by name: a renamed function is a version bump of the
14
+ // store and of Samebase.
15
+ import { internalMutationGeneric, internalQueryGeneric, queryGeneric } from "convex/server";
16
+ import { type Infer, v } from "convex/values";
17
+ import type { ComponentApi } from "./component/_generated/component.js";
18
+ import { env } from "./component/_generated/server.js";
19
+ import { repositoryReply } from "./component/reader.js";
20
+ import { runRecord } from "./component/runRecord.js";
21
+ import { stackReply } from "./component/state.js";
22
+ import { STORE_VERSION } from "./convex/version.js";
23
+
24
+ /**
25
+ * The app that serves the store. `seed`: the store app of this package, which
26
+ * Convex.state() pushes. `base`: the base app of the team, which
27
+ * Convex.state() never pushes over.
28
+ */
29
+ const storeHost = v.union(v.literal("seed"), v.literal("base"));
30
+
31
+ /**
32
+ * The internal `state:*` functions over the mounted component `alchemyState`.
33
+ * `state:host` answers `host`, the app that serves them. Export each one
34
+ * by name from the app's `convex/state.ts`.
35
+ */
36
+ export const stateFunctions = (alchemyState: ComponentApi, host: Infer<typeof storeHost>) => {
37
+ const state = alchemyState.state;
38
+ return {
39
+ /** The wire version of this store, compared with the client's STORE_VERSION. */
40
+ version: internalQueryGeneric({
41
+ args: {},
42
+ returns: v.number(),
43
+ handler: async () => STORE_VERSION,
44
+ }),
45
+
46
+ /** The app that serves this store. Convex.state() reads it before it pushes over an older store. */
47
+ host: internalQueryGeneric({
48
+ args: {},
49
+ returns: storeHost,
50
+ handler: async () => host,
51
+ }),
52
+
53
+ /** Stacks with at least one resource or one output, sorted. */
54
+ stacks: internalQueryGeneric({
55
+ args: {},
56
+ returns: v.array(v.string()),
57
+ handler: (ctx) => ctx.runQuery(state.stacks, {}),
58
+ }),
59
+
60
+ /** Stages of a stack with at least one resource or one output, sorted. */
61
+ stages: internalQueryGeneric({
62
+ args: { stack: v.string() },
63
+ returns: v.array(v.string()),
64
+ handler: (ctx, args) => ctx.runQuery(state.stages, { stack: args.stack }),
65
+ }),
66
+
67
+ list: internalQueryGeneric({
68
+ args: { stack: v.string(), stage: v.string() },
69
+ returns: v.array(v.string()),
70
+ handler: (ctx, args) => ctx.runQuery(state.list, { stack: args.stack, stage: args.stage }),
71
+ }),
72
+
73
+ get: internalQueryGeneric({
74
+ args: { stack: v.string(), stage: v.string(), fqn: v.string() },
75
+ returns: v.union(v.string(), v.null()),
76
+ handler: (ctx, args) =>
77
+ ctx.runQuery(state.get, { stack: args.stack, stage: args.stage, fqn: args.fqn }),
78
+ }),
79
+
80
+ /** Resources whose state has `status: "replaced"`. */
81
+ replaced: internalQueryGeneric({
82
+ args: { stack: v.string(), stage: v.string() },
83
+ returns: v.array(v.string()),
84
+ handler: (ctx, args) =>
85
+ ctx.runQuery(state.replaced, { stack: args.stack, stage: args.stage }),
86
+ }),
87
+
88
+ set: internalMutationGeneric({
89
+ args: {
90
+ stack: v.string(),
91
+ stage: v.string(),
92
+ fqn: v.string(),
93
+ value: v.string(),
94
+ run: v.optional(runRecord),
95
+ },
96
+ returns: v.null(),
97
+ handler: (ctx, args) =>
98
+ ctx.runMutation(state.set, {
99
+ stack: args.stack,
100
+ stage: args.stage,
101
+ fqn: args.fqn,
102
+ value: args.value,
103
+ run: args.run ?? null,
104
+ }),
105
+ }),
106
+
107
+ remove: internalMutationGeneric({
108
+ args: { stack: v.string(), stage: v.string(), fqn: v.string(), run: v.optional(runRecord) },
109
+ returns: v.null(),
110
+ handler: (ctx, args) =>
111
+ ctx.runMutation(state.remove, {
112
+ stack: args.stack,
113
+ stage: args.stage,
114
+ fqn: args.fqn,
115
+ run: args.run ?? null,
116
+ }),
117
+ }),
118
+
119
+ /** Deletes the resources and outputs of one stage, or of every stage of the stack. */
120
+ removeStack: internalMutationGeneric({
121
+ args: { stack: v.string(), stage: v.optional(v.string()) },
122
+ returns: v.null(),
123
+ handler: (ctx, args) =>
124
+ ctx.runMutation(state.removeStack, { stack: args.stack, stage: args.stage ?? null }),
125
+ }),
126
+
127
+ getOutput: internalQueryGeneric({
128
+ args: { stack: v.string(), stage: v.string() },
129
+ returns: v.union(v.string(), v.null()),
130
+ handler: (ctx, args) =>
131
+ ctx.runQuery(state.getOutput, { stack: args.stack, stage: args.stage }),
132
+ }),
133
+
134
+ setOutput: internalMutationGeneric({
135
+ args: { stack: v.string(), stage: v.string(), value: v.string(), run: v.optional(runRecord) },
136
+ returns: v.null(),
137
+ handler: (ctx, args) =>
138
+ ctx.runMutation(state.setOutput, {
139
+ stack: args.stack,
140
+ stage: args.stage,
141
+ value: args.value,
142
+ run: args.run ?? null,
143
+ }),
144
+ }),
145
+
146
+ /** Every stage of a stack with its resources, output, and current record, in one call. Since store version 2. */
147
+ readStack: internalQueryGeneric({
148
+ args: { stack: v.string() },
149
+ returns: stackReply,
150
+ handler: (ctx, args) => ctx.runQuery(state.readStack, { stack: args.stack }),
151
+ }),
152
+ };
153
+ };
154
+
155
+ const repositoryName = /^[^/]+\/[^/]+$/;
156
+
157
+ /**
158
+ * The `reader:*` functions over the mounted component `alchemyState`. Export
159
+ * each one by name from the app's `convex/reader.ts`. They stay in the app:
160
+ * `authorize` signs and `repository` verifies with `ctx.auth`, which a
161
+ * component does not have. The rows and the redaction are in the component.
162
+ * The token audience is `${CONVEX_CLOUD_URL}/alchemy-reader`, the application
163
+ * id of authConfig.ts. CONVEX_CLOUD_URL is a system variable of the
164
+ * deployment, the same for the app and the component, so the typed `env` of
165
+ * the component's codegen gives it.
166
+ */
167
+ export const readerFunctions = (alchemyState: ComponentApi) => ({
168
+ /** Only a deployment credential can exchange an approved repository for a read token. */
169
+ authorize: internalMutationGeneric({
170
+ args: { repository: v.string(), subject: v.string() },
171
+ returns: v.object({ token: v.string() }),
172
+ handler: async (ctx, args) => {
173
+ if (!repositoryName.test(args.repository) || args.subject === "") {
174
+ throw new Error("A repository and subject are required.");
175
+ }
176
+ return {
177
+ token: await ctx.auth.signJwt({
178
+ expiresInSeconds: 300,
179
+ claims: {
180
+ sub: args.subject,
181
+ aud: `${env.CONVEX_CLOUD_URL}/alchemy-reader`,
182
+ repository: args.repository,
183
+ scope: "alchemy:read",
184
+ },
185
+ }),
186
+ };
187
+ },
188
+ }),
189
+
190
+ /** A native subscription reads only the repository named in its verified capability. */
191
+ repository: queryGeneric({
192
+ args: {},
193
+ returns: repositoryReply,
194
+ handler: async (ctx) => {
195
+ const identity = await ctx.auth.getUserIdentity();
196
+ // Only a token of the deployment's own issuer reads, the one that `authorize` signs: a base
197
+ // can accept other providers too, and a token of one of those with the same claims does not.
198
+ if (
199
+ identity === null ||
200
+ identity.issuer !== env.CONVEX_CLOUD_URL ||
201
+ identity.scope !== "alchemy:read" ||
202
+ typeof identity.repository !== "string" ||
203
+ !repositoryName.test(identity.repository)
204
+ ) {
205
+ throw new Error("Alchemy repository read access is required.");
206
+ }
207
+ return await ctx.runQuery(alchemyState.reader.repository, {
208
+ repository: identity.repository,
209
+ });
210
+ },
211
+ }),
212
+ });
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "alchemy-state",
2
+ "name": "samebase",
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "peerDependencies": {