@samebase/alchemy-convex 0.15.0 → 0.17.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.
@@ -1,23 +1,123 @@
1
- // The state functions of the store app of this package, a seed: Convex.state()
2
- // pushes this directory to the samebase project of a team. The raw state
3
- // functions are internal; reader.ts exposes authenticated, redacted reads.
4
- // The functions come from src/store/functions.ts, which a base app imports
5
- // too. Each export has its own name, so that Convex finds it.
6
- import { stateFunctions } from "../functions.js";
1
+ // The state functions of the alchemy-state-store project of a team: one project for each
2
+ // Convex team, pushed by Convex.state() from this directory. These raw state
3
+ // functions are internal. reader.ts exposes authenticated, redacted reads.
4
+ //
5
+ // The rows are in the alchemyState component (src/store/component). Each
6
+ // function here forwards to the component function of the same name, with the
7
+ // same results; an absent `run` or `stage` goes to the component as null.
8
+ // These names and arguments are the wire contract of STORE_VERSION. Samebase
9
+ // reads `stacks`, `getOutput`, and `readStack` by name: a renamed function is
10
+ // a version bump of the store and of Samebase.
11
+ import { v } from "convex/values";
12
+ import { runRecord } from "../component/runRecord.ts";
13
+ import { stackReply } from "../component/state.ts";
7
14
  import { components } from "./_generated/api.js";
15
+ import { internalMutation, internalQuery } from "./_generated/server.js";
16
+ import { STORE_VERSION } from "./version.ts";
8
17
 
9
- export const {
10
- version,
11
- host,
12
- stacks,
13
- stages,
14
- list,
15
- get,
16
- replaced,
17
- set,
18
- remove,
19
- removeStack,
20
- getOutput,
21
- setOutput,
22
- readStack,
23
- } = stateFunctions(components.alchemyState, "seed");
18
+ const state = components.alchemyState.state;
19
+
20
+ /** The wire version of this store, compared with the client's STORE_VERSION. */
21
+ export const version = internalQuery({
22
+ args: {},
23
+ returns: v.number(),
24
+ handler: async () => STORE_VERSION,
25
+ });
26
+
27
+ /** Stacks with at least one resource or one output, sorted. */
28
+ export const stacks = internalQuery({
29
+ args: {},
30
+ returns: v.array(v.string()),
31
+ handler: (ctx) => ctx.runQuery(state.stacks, {}),
32
+ });
33
+
34
+ /** Stages of a stack with at least one resource or one output, sorted. */
35
+ export const stages = internalQuery({
36
+ args: { stack: v.string() },
37
+ returns: v.array(v.string()),
38
+ handler: (ctx, args) => ctx.runQuery(state.stages, { stack: args.stack }),
39
+ });
40
+
41
+ export const list = internalQuery({
42
+ args: { stack: v.string(), stage: v.string() },
43
+ returns: v.array(v.string()),
44
+ handler: (ctx, args) => ctx.runQuery(state.list, { stack: args.stack, stage: args.stage }),
45
+ });
46
+
47
+ export const get = internalQuery({
48
+ args: { stack: v.string(), stage: v.string(), fqn: v.string() },
49
+ returns: v.union(v.string(), v.null()),
50
+ handler: (ctx, args) =>
51
+ ctx.runQuery(state.get, { stack: args.stack, stage: args.stage, fqn: args.fqn }),
52
+ });
53
+
54
+ /** Resources whose state has `status: "replaced"`. */
55
+ export const replaced = internalQuery({
56
+ args: { stack: v.string(), stage: v.string() },
57
+ returns: v.array(v.string()),
58
+ handler: (ctx, args) => ctx.runQuery(state.replaced, { stack: args.stack, stage: args.stage }),
59
+ });
60
+
61
+ export const set = internalMutation({
62
+ args: {
63
+ stack: v.string(),
64
+ stage: v.string(),
65
+ fqn: v.string(),
66
+ value: v.string(),
67
+ run: v.optional(runRecord),
68
+ },
69
+ returns: v.null(),
70
+ handler: (ctx, args) =>
71
+ ctx.runMutation(state.set, {
72
+ stack: args.stack,
73
+ stage: args.stage,
74
+ fqn: args.fqn,
75
+ value: args.value,
76
+ run: args.run ?? null,
77
+ }),
78
+ });
79
+
80
+ export const remove = internalMutation({
81
+ args: { stack: v.string(), stage: v.string(), fqn: v.string(), run: v.optional(runRecord) },
82
+ returns: v.null(),
83
+ handler: (ctx, args) =>
84
+ ctx.runMutation(state.remove, {
85
+ stack: args.stack,
86
+ stage: args.stage,
87
+ fqn: args.fqn,
88
+ run: args.run ?? null,
89
+ }),
90
+ });
91
+
92
+ /** Deletes the resources and outputs of one stage, or of every stage of the stack. */
93
+ export const removeStack = internalMutation({
94
+ args: { stack: v.string(), stage: v.optional(v.string()) },
95
+ returns: v.null(),
96
+ handler: (ctx, args) =>
97
+ ctx.runMutation(state.removeStack, { stack: args.stack, stage: args.stage ?? null }),
98
+ });
99
+
100
+ export const getOutput = internalQuery({
101
+ args: { stack: v.string(), stage: v.string() },
102
+ returns: v.union(v.string(), v.null()),
103
+ handler: (ctx, args) => ctx.runQuery(state.getOutput, { stack: args.stack, stage: args.stage }),
104
+ });
105
+
106
+ export const setOutput = internalMutation({
107
+ args: { stack: v.string(), stage: v.string(), value: v.string(), run: v.optional(runRecord) },
108
+ returns: v.null(),
109
+ handler: (ctx, args) =>
110
+ ctx.runMutation(state.setOutput, {
111
+ stack: args.stack,
112
+ stage: args.stage,
113
+ value: args.value,
114
+ run: args.run ?? null,
115
+ }),
116
+ });
117
+
118
+ /** Every stage of a stack with its resources, output, and current record, in one call. Since store version 2. */
119
+ export const readStack = internalQuery({
120
+ args: { stack: v.string() },
121
+ returns: stackReply,
122
+ handler: (ctx, args) => ctx.runQuery(state.readStack, { stack: args.stack }),
123
+ });
@@ -1,13 +1,9 @@
1
- // The wire contract of the store: the `state:version` query answers it, and
2
- // Convex.state() pushes the store app when a seed answers an older one or the
3
- // deployment has no `version` yet. It fails the run when a base answers an
4
- // older one, and on a newer one.
5
- //
6
- // Each consumer of a team needs this exact number, so a raise moves every
7
- // consumer of the team at once. Raise it only when a function that a released
8
- // client calls changes its arguments, its result, or its meaning, or goes
9
- // away. A new function does not raise it. The store and its readers (Samebase)
10
- // move together on a raise: a renamed function is a raise on both sides.
1
+ // The wire version of the store: the `state:version` query answers it, and
2
+ // Convex.state() pushes this directory when the store of the team answers an
3
+ // older one or has no `version` yet, and fails the run on a newer one. Raise
4
+ // it only when a function that a released client calls changes or goes away:
5
+ // each consumer needs the exact number, so a raise moves every consumer of a
6
+ // team at once.
11
7
  //
12
8
  // 1: `version`, `stacks`, `stages`, `list`, `get`, `replaced`, `getOutput`,
13
9
  // `set`, `remove`, `removeStack`, `setOutput` (0.6.0).
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "samebase",
2
+ "name": "alchemy-state-store",
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "peerDependencies": {
@@ -1,17 +0,0 @@
1
- // The auth config of each app that serves the store: the app verifies the
2
- // read tokens that `reader:authorize` signs with the key of its own
3
- // deployment. Exported as `@samebase/alchemy-convex/state/auth.config`.
4
- import type { AuthConfig } from "convex/server";
5
-
6
- /** The auth config of the store readers on the deployment at `cloudUrl`, the app's `env.CONVEX_CLOUD_URL`. */
7
- export const authConfig = (cloudUrl: string): AuthConfig => ({
8
- providers: [
9
- {
10
- type: "customJwt",
11
- issuer: cloudUrl,
12
- applicationID: `${cloudUrl}/alchemy-reader`,
13
- jwks: `${cloudUrl}/.well-known/jwks.json`,
14
- algorithm: "ES256",
15
- },
16
- ],
17
- });
@@ -1,214 +0,0 @@
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
- // Custom claims are bracket reads: a base app compiles this file under its own tsconfig,
197
- // which can set noPropertyAccessFromIndexSignature.
198
- const scope = identity?.["scope"];
199
- const repository = identity?.["repository"];
200
- // Only a token of the deployment's own issuer reads, the one that `authorize` signs: a base
201
- // can accept other providers too, and a token of one of those with the same claims does not.
202
- if (
203
- identity === null ||
204
- identity.issuer !== env.CONVEX_CLOUD_URL ||
205
- scope !== "alchemy:read" ||
206
- typeof repository !== "string" ||
207
- !repositoryName.test(repository)
208
- ) {
209
- throw new Error("Alchemy repository read access is required.");
210
- }
211
- return await ctx.runQuery(alchemyState.reader.repository, { repository });
212
- },
213
- }),
214
- });