@samebase/alchemy-convex 0.4.1 → 0.6.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.
package/src/State.ts ADDED
@@ -0,0 +1,435 @@
1
+ // Alchemy state in Convex: `state: Convex.state({ team })` in `Alchemy.Stack`.
2
+ //
3
+ // One project for each Convex team, `alchemy-state`, holds the state of
4
+ // every stack and stage of the team in its production deployment: the
5
+ // tables `resources (stack, stage, fqn, value)` and `outputs (stack, stage,
6
+ // value)`, served by the functions of src/store/convex. The store lives
7
+ // outside every resource that a stack manages, so a stack can create and
8
+ // destroy its own project. See docs/design/convex-state.md.
9
+ //
10
+ // Bootstrap, on the first call of the run: the team token (CONVEX_ACCESS_TOKEN,
11
+ // or the Convex CLI login) finds the project by name, or creates it; a deploy
12
+ // key of its production deployment that can run internal queries and
13
+ // mutations, and nothing else, is created for the run and deleted at its
14
+ // end; the `state:version` query says whether the deployment has the store
15
+ // of this version. Without one, or with an older one, the installed Convex
16
+ // CLI pushes src/store/convex to it, with a push key that is deleted after
17
+ // the push. The functions are internal, so only POST /api/function with such
18
+ // a key reaches them: a public function of a root app answers anyone who
19
+ // knows the deployment URL (measured). A key that stops working during a
20
+ // long run (it expires after two hours) is replaced once per call.
21
+ //
22
+ // Two first runs of a team can both create a project named `alchemy-state`.
23
+ // The run whose project is not the oldest deletes its own, before it writes
24
+ // anything, and uses the oldest: both runs end on one store.
25
+ //
26
+ // The function names are a wire contract: Samebase reads `stacks` and
27
+ // `getOutput` by name, and an older client calls an older set of names. A
28
+ // name is never renamed or removed. STORE_VERSION rises when a function
29
+ // changes in a way that an older client cannot use, and a client never
30
+ // pushes an older store over a newer one.
31
+ import { dirname, join } from "node:path";
32
+ import { fileURLToPath } from "node:url";
33
+ import * as Effect from "effect/Effect";
34
+ import * as Layer from "effect/Layer";
35
+ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
36
+ import * as Redacted from "effect/Redacted";
37
+ import * as Schedule from "effect/Schedule";
38
+ import * as Schema from "effect/Schema";
39
+ import * as Scope from "effect/Scope";
40
+ import {
41
+ encodeState,
42
+ type PersistedState,
43
+ type ReplacedResourceState,
44
+ reviveState,
45
+ State,
46
+ type StateService,
47
+ STATE_STORE_VERSION,
48
+ StateStoreError,
49
+ } from "alchemy/State";
50
+ import { fromConfig } from "./Credentials.ts";
51
+ import { ProductionDeploymentNotFound } from "./Deployment.ts";
52
+ import {
53
+ absentAsUndefined,
54
+ ManagementApi,
55
+ ManagementApiLive,
56
+ type ManagementApiService,
57
+ retryIdempotentWrite,
58
+ } from "./ManagementApi.ts";
59
+ import {
60
+ createProject,
61
+ findProjectByName,
62
+ listProjectsByName,
63
+ prodDeploymentOf,
64
+ } from "./Project.ts";
65
+ import {
66
+ createDeployKey,
67
+ deleteDeployKey,
68
+ installedCli,
69
+ push,
70
+ PUSH_KEY_ACTIONS,
71
+ withPushKey,
72
+ } from "./Push.ts";
73
+ import { STORE_VERSION } from "./store/convex/version.ts";
74
+
75
+ /** The name of the state project in each team. */
76
+ export const STORE_PROJECT_NAME = "alchemy-state";
77
+
78
+ /**
79
+ * The actions of the key of one run: the internal queries and mutations of
80
+ * the store, nothing else. Measured: `deployment:data:view` and
81
+ * `deployment:data:write` do not run an internal function.
82
+ */
83
+ export const RUN_KEY_ACTIONS = [
84
+ "deployment:functions:runInternalQueries",
85
+ "deployment:functions:runInternalMutations",
86
+ ] as const;
87
+
88
+ /** The body of POST /api/function, with HTTP 200 for a function error too. */
89
+ const Reply = Schema.Union([
90
+ Schema.Struct({ status: Schema.Literal("success"), value: Schema.Unknown }),
91
+ Schema.Struct({ status: Schema.Literal("error"), errorMessage: Schema.String }),
92
+ ]);
93
+ type Reply = typeof Reply.Type;
94
+
95
+ /** The error reply of /api/function for a function that the deployment does not have (recorded). */
96
+ const MISSING_FUNCTION = /Could not find function for '/;
97
+
98
+ /** HTTP 401 or 403 from the function API: the key is gone or expired (recorded: 403 without a key). */
99
+ const isUnauthorized = (error: StateStoreError) =>
100
+ error.http?.status === 401 || error.http?.status === 403;
101
+
102
+ const failure = (message: string) => new StateStoreError({ message: `Convex state: ${message}` });
103
+
104
+ /** No reply, HTTP 429, or HTTP 5xx: a new attempt can succeed. Every call of the store is idempotent. */
105
+ const isTransient = (error: StateStoreError) =>
106
+ error.http !== undefined &&
107
+ (error.http.status === undefined || error.http.status === 429 || error.http.status >= 500);
108
+
109
+ const post = (
110
+ url: string,
111
+ headers: Record<string, string>,
112
+ body: unknown,
113
+ ): Effect.Effect<unknown, StateStoreError> =>
114
+ Effect.tryPromise({
115
+ try: () =>
116
+ fetch(url, {
117
+ method: "POST",
118
+ headers: { "Content-Type": "application/json", ...headers },
119
+ body: JSON.stringify(body),
120
+ }),
121
+ catch: () => new StateStoreError({ message: `Convex state: POST ${url} failed`, http: {} }),
122
+ }).pipe(
123
+ Effect.flatMap((response) =>
124
+ response.ok || response.status === 560
125
+ ? Effect.tryPromise({
126
+ try: (): Promise<unknown> => response.json(),
127
+ catch: () => failure(`${url} gave no JSON`),
128
+ })
129
+ : Effect.fail(
130
+ new StateStoreError({
131
+ message: `Convex state: POST ${url} failed with HTTP ${response.status}`,
132
+ http: { status: response.status },
133
+ }),
134
+ ),
135
+ ),
136
+ Effect.retry({
137
+ while: isTransient,
138
+ schedule: Schedule.exponential("100 millis").pipe(Schedule.jittered),
139
+ times: 5,
140
+ }),
141
+ );
142
+
143
+ const decode =
144
+ <S extends Schema.Codec<unknown, unknown>>(schema: S, what: string) =>
145
+ (value: unknown) =>
146
+ Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => failure(`bad ${what}`)));
147
+
148
+ /** The function API of the store deployment, with a deploy key. */
149
+ export interface FunctionApi {
150
+ /** POST /api/function, the call that `convex run` makes: the reply, which can be a function error. */
151
+ readonly call: (
152
+ path: string,
153
+ args: Record<string, string | undefined>,
154
+ ) => Effect.Effect<Reply, StateStoreError>;
155
+ }
156
+
157
+ export const makeFunctionApi = (url: string, key: Redacted.Redacted<string>): FunctionApi => ({
158
+ call: (path, args) =>
159
+ post(
160
+ `${url}/api/function`,
161
+ { Authorization: `Convex ${Redacted.value(key)}` },
162
+ { path, args, format: "json" },
163
+ ).pipe(Effect.flatMap(decode(Reply, `reply of ${path}`))),
164
+ });
165
+
166
+ /** The value of a reply, or the Convex message, which names the function and the cause, such as a missing key action. */
167
+ const valueOf = (reply: Reply) =>
168
+ reply.status === "success"
169
+ ? Effect.succeed(reply.value)
170
+ : Effect.fail(failure(reply.errorMessage.trim()));
171
+
172
+ /** The functions of the store with Alchemy's encoding: the JSON text of encodeState. */
173
+ const remoteOf = (api: FunctionApi) => {
174
+ const run = (name: string, args: Record<string, string | undefined>) =>
175
+ api.call(`state:${name}`, args).pipe(Effect.flatMap(valueOf));
176
+ const strings = (name: string, args: Record<string, string>) =>
177
+ run(name, args).pipe(Effect.flatMap(decode(Schema.Array(Schema.String), name)));
178
+ const optional = (name: string, args: Record<string, string>) =>
179
+ run(name, args).pipe(Effect.flatMap(decode(Schema.NullOr(Schema.String), name)));
180
+ const encode = (value: unknown) => JSON.stringify(encodeState(value));
181
+ return {
182
+ listStacks: () => strings("stacks", {}),
183
+ listStages: (stack: string) => strings("stages", { stack }),
184
+ list: (r: { stack: string; stage: string }) =>
185
+ strings("list", { stack: r.stack, stage: r.stage }),
186
+ // Like Alchemy's own stores, the store gives back the state that it got.
187
+ get: (r: { stack: string; stage: string; fqn: string }) =>
188
+ optional("get", { stack: r.stack, stage: r.stage, fqn: r.fqn }).pipe(
189
+ Effect.map((text): PersistedState | undefined =>
190
+ text === null ? undefined : JSON.parse(text, reviveState),
191
+ ),
192
+ ),
193
+ getReplacedResources: (r: { stack: string; stage: string }) =>
194
+ strings("replaced", { stack: r.stack, stage: r.stage }).pipe(
195
+ Effect.map((texts) =>
196
+ texts.map((text): ReplacedResourceState => JSON.parse(text, reviveState)),
197
+ ),
198
+ ),
199
+ getOutput: (r: { stack: string; stage: string }) =>
200
+ optional("getOutput", { stack: r.stack, stage: r.stage }).pipe(
201
+ Effect.map((text): unknown => (text === null ? undefined : JSON.parse(text, reviveState))),
202
+ ),
203
+ set: (r: { stack: string; stage: string; fqn: string; value: PersistedState }) =>
204
+ run("set", { stack: r.stack, stage: r.stage, fqn: r.fqn, value: encode(r.value) }),
205
+ delete: (r: { stack: string; stage: string; fqn: string }) =>
206
+ run("remove", { stack: r.stack, stage: r.stage, fqn: r.fqn }),
207
+ deleteStack: (r: { stack: string; stage?: string }) =>
208
+ run("removeStack", { stack: r.stack, stage: r.stage }),
209
+ setOutput: (r: { stack: string; stage: string; value: unknown }) =>
210
+ run("setOutput", { stack: r.stack, stage: r.stage, value: encode(r.value) }),
211
+ };
212
+ };
213
+
214
+ /** The version that the store answers, or undefined when the deployment has no `state:version`. */
215
+ const storeVersion = (api: FunctionApi) =>
216
+ api
217
+ .call("state:version", {})
218
+ .pipe(
219
+ Effect.flatMap((reply) =>
220
+ reply.status === "error" && MISSING_FUNCTION.test(reply.errorMessage)
221
+ ? Effect.succeed(undefined)
222
+ : valueOf(reply).pipe(Effect.flatMap(decode(Schema.Number, "store version"))),
223
+ ),
224
+ );
225
+
226
+ /**
227
+ * The functions directory of the store app, `src/store/convex` of this
228
+ * package. This file is `src/State.ts` in a checkout and a chunk in `lib/`
229
+ * in the installed package, which ships `src` too, so the directory is
230
+ * `../src/store/convex` from either place.
231
+ */
232
+ export const storeDirectory = () =>
233
+ join(dirname(fileURLToPath(import.meta.url)), "..", "src", "store", "convex");
234
+
235
+ export interface StoreOptions {
236
+ /** Team slug or numeric team id of the `alchemy-state` project. */
237
+ readonly team: string | number;
238
+ readonly api: ManagementApiService;
239
+ readonly spawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
240
+ /** The key of the run is deleted when this scope closes. */
241
+ readonly scope: Scope.Scope;
242
+ /** The functions directory that an outdated store gets: {@link storeDirectory}. */
243
+ readonly store: string;
244
+ }
245
+
246
+ /**
247
+ * Pushes `store` to the production deployment of the state project with the
248
+ * installed Convex CLI: no typecheck, no codegen, the key only in the
249
+ * environment of the CLI.
250
+ */
251
+ const pushStore = (
252
+ options: StoreOptions,
253
+ deployment: { readonly name: string; readonly deploymentUrl: string },
254
+ ) =>
255
+ Effect.gen(function* () {
256
+ const cli = yield* installedCli(join(options.store, ".."));
257
+ yield* Effect.logInfo(
258
+ `Convex state: pushing the store version ${STORE_VERSION} to ${deployment.name}`,
259
+ );
260
+ yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) =>
261
+ push(options.spawner, key, {
262
+ deployment: deployment.name,
263
+ url: deployment.deploymentUrl,
264
+ source: {
265
+ path: options.store,
266
+ command: [...cli, "deploy", "--typecheck", "disable", "--codegen", "disable"],
267
+ },
268
+ env: {},
269
+ previousEnvNames: [],
270
+ adopt: false,
271
+ previewName: undefined,
272
+ }),
273
+ );
274
+ });
275
+
276
+ /**
277
+ * Creates the state project of the team. Two first runs can both create one:
278
+ * the run whose project is not the oldest deletes its own, which holds
279
+ * nothing yet, and uses the oldest, so that every run ends on one store.
280
+ */
281
+ const createStoreProject = (api: ManagementApiService, teamId: number, team: string | number) =>
282
+ Effect.gen(function* () {
283
+ yield* Effect.logInfo(
284
+ `Convex state: creating the project ${STORE_PROJECT_NAME} in team ${team}`,
285
+ );
286
+ const created = yield* createProject(api, teamId, {
287
+ name: STORE_PROJECT_NAME,
288
+ deploymentType: "prod",
289
+ deploymentRegion: undefined,
290
+ });
291
+ const all = yield* listProjectsByName(api, teamId, STORE_PROJECT_NAME);
292
+ const oldest = all.reduce((a, b) => (b.id < a.id ? b : a), created);
293
+ if (oldest.id === created.id) return created;
294
+ yield* Effect.logInfo(
295
+ `Convex state: another run created ${STORE_PROJECT_NAME} ${oldest.id} first; project ${created.id} is deleted again.`,
296
+ );
297
+ yield* absentAsUndefined(
298
+ retryIdempotentWrite(
299
+ api.requestVoid("delete project", (client) =>
300
+ client.POST("/projects/{project_id}/delete", {
301
+ params: { path: { project_id: created.id } },
302
+ }),
303
+ ),
304
+ ),
305
+ );
306
+ return oldest;
307
+ });
308
+
309
+ /**
310
+ * The StateService over the `alchemy-state` project of the team: found or
311
+ * created, its store pushed when it is missing or older, and read and
312
+ * written with the keys of this run.
313
+ */
314
+ export const makeConvexState = (options: StoreOptions) =>
315
+ Effect.gen(function* () {
316
+ const { api, team } = options;
317
+ const teamId = yield* api.resolveTeamId(team);
318
+ const found = yield* findProjectByName(api, teamId, STORE_PROJECT_NAME);
319
+ const project = found ?? (yield* createStoreProject(api, teamId, team));
320
+ const deployment = yield* prodDeploymentOf(api, project.id);
321
+ if (deployment === undefined) {
322
+ return yield* new ProductionDeploymentNotFound({ projectId: project.id });
323
+ }
324
+
325
+ // The keys of this run, deleted together when the scope closes. A call
326
+ // that the current key no longer authorizes gets one new key and one
327
+ // more attempt.
328
+ const keys: Redacted.Redacted<string>[] = [];
329
+ yield* Scope.addFinalizer(
330
+ options.scope,
331
+ Effect.suspend(() =>
332
+ Effect.forEach(keys, (key) => deleteDeployKey(api, deployment.name, key), {
333
+ discard: true,
334
+ }),
335
+ ),
336
+ );
337
+ const mint = createDeployKey(api, deployment.name, "alchemy-state", RUN_KEY_ACTIONS).pipe(
338
+ Effect.map((key) => {
339
+ keys.push(key);
340
+ return makeFunctionApi(deployment.deploymentUrl, key);
341
+ }),
342
+ Effect.mapError((error) => failure(`no key for ${deployment.name}: ${error.message}`)),
343
+ );
344
+ let current = yield* mint;
345
+ const functions: FunctionApi = {
346
+ call: (path, args) =>
347
+ current.call(path, args).pipe(
348
+ Effect.catchIf(isUnauthorized, () =>
349
+ mint.pipe(
350
+ Effect.flatMap((next) => {
351
+ current = next;
352
+ return next.call(path, args);
353
+ }),
354
+ ),
355
+ ),
356
+ ),
357
+ };
358
+
359
+ const version = yield* storeVersion(functions);
360
+ if (version === undefined || version < STORE_VERSION) {
361
+ // Code without `state:version` is another app with the name of the store.
362
+ if (version === undefined && deployment.lastDeployTime) {
363
+ return yield* failure(
364
+ `project ${STORE_PROJECT_NAME} (${project.id}) of team ${team} has code that is not the state store, and Convex.state does not push over it. Rename that project in the Convex dashboard.`,
365
+ );
366
+ }
367
+ // Another run can push a newer store in the meantime: read again right
368
+ // before the push. The push itself has no lock (see the design note).
369
+ const now = yield* storeVersion(functions);
370
+ if (now === undefined || now < STORE_VERSION) {
371
+ yield* pushStore(options, deployment);
372
+ const pushed = yield* storeVersion(functions);
373
+ if (pushed === undefined || pushed < STORE_VERSION) {
374
+ return yield* failure(
375
+ `${deployment.name} answers store version ${pushed ?? "none"} after the push of version ${STORE_VERSION}.`,
376
+ );
377
+ }
378
+ }
379
+ } else if (version > STORE_VERSION) {
380
+ // The names are a wire contract, so a newer store serves this client.
381
+ yield* Effect.logInfo(
382
+ `Convex state: the store of team ${team} is version ${version}, newer than version ${STORE_VERSION} of this package, and it stays.`,
383
+ );
384
+ }
385
+
386
+ const remote = remoteOf(functions);
387
+ const service: StateService = {
388
+ id: "convex",
389
+ getVersion: () => Effect.succeed(STATE_STORE_VERSION),
390
+ listStacks: remote.listStacks,
391
+ listStages: remote.listStages,
392
+ list: remote.list,
393
+ get: remote.get,
394
+ getReplacedResources: remote.getReplacedResources,
395
+ getOutput: remote.getOutput,
396
+ set: (request) => remote.set(request).pipe(Effect.as(request.value)),
397
+ delete: (request) => remote.delete(request).pipe(Effect.asVoid),
398
+ deleteStack: (request) => remote.deleteStack(request).pipe(Effect.asVoid),
399
+ setOutput: (request) => remote.setOutput(request).pipe(Effect.as(request.value)),
400
+ };
401
+ return service;
402
+ });
403
+
404
+ /**
405
+ * Alchemy state in the `alchemy-state` project of `team`, with the team token
406
+ * in CONVEX_ACCESS_TOKEN or the Convex CLI login. The first use of the run
407
+ * finds or creates the project and pushes the store when it is missing or
408
+ * older. `alchemy plan` on another stack never touches Convex.
409
+ */
410
+ export const state = (options: {
411
+ readonly team: string | number;
412
+ }): Layer.Layer<State, never, ChildProcessSpawner.ChildProcessSpawner> =>
413
+ Layer.effect(
414
+ State,
415
+ Effect.gen(function* () {
416
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
417
+ const scope = yield* Scope.Scope;
418
+ // The stack's ConfigProvider, for the read of the token on first use.
419
+ const context = yield* Effect.context<never>();
420
+ return yield* Effect.cached(
421
+ Effect.gen(function* () {
422
+ const api = yield* ManagementApi.pipe(
423
+ Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))),
424
+ );
425
+ return yield* makeConvexState({
426
+ team: options.team,
427
+ api,
428
+ spawner,
429
+ scope,
430
+ store: storeDirectory(),
431
+ });
432
+ }).pipe(Effect.provideContext(context), Effect.orDie),
433
+ );
434
+ }),
435
+ );
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export {
28
28
  ProjectTeamChange,
29
29
  } from "./Project.ts";
30
30
  export { Providers, providers } from "./Providers.ts";
31
+ export { state, STORE_PROJECT_NAME } from "./State.ts";
31
32
  export {
32
33
  CodePushFailed,
33
34
  ConvexAppNotFound,
@@ -0,0 +1,51 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type * as state from "../state.js";
12
+ import type * as version from "../version.js";
13
+
14
+ import type {
15
+ ApiFromModules,
16
+ FilterApi,
17
+ FunctionReference,
18
+ } from "convex/server";
19
+
20
+ declare const fullApi: ApiFromModules<{
21
+ state: typeof state;
22
+ version: typeof version;
23
+ }>;
24
+
25
+ /**
26
+ * A utility for referencing Convex functions in your app's public API.
27
+ *
28
+ * Usage:
29
+ * ```js
30
+ * const myFunctionReference = api.myModule.myFunction;
31
+ * ```
32
+ */
33
+ export declare const api: FilterApi<
34
+ typeof fullApi,
35
+ FunctionReference<any, "public">
36
+ >;
37
+
38
+ /**
39
+ * A utility for referencing Convex functions in your app's internal API.
40
+ *
41
+ * Usage:
42
+ * ```js
43
+ * const myFunctionReference = internal.myModule.myFunction;
44
+ * ```
45
+ */
46
+ export declare const internal: FilterApi<
47
+ typeof fullApi,
48
+ FunctionReference<any, "internal">
49
+ >;
50
+
51
+ export declare const components: {};
@@ -0,0 +1,23 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import { anyApi, componentsGeneric } from "convex/server";
12
+
13
+ /**
14
+ * A utility for referencing Convex functions in your app's API.
15
+ *
16
+ * Usage:
17
+ * ```js
18
+ * const myFunctionReference = api.myModule.myFunction;
19
+ * ```
20
+ */
21
+ export const api = anyApi;
22
+ export const internal = anyApi;
23
+ export const components = componentsGeneric();
@@ -0,0 +1,60 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated data model types.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type {
12
+ DataModelFromSchemaDefinition,
13
+ DocumentByName,
14
+ TableNamesInDataModel,
15
+ SystemTableNames,
16
+ } from "convex/server";
17
+ import type { GenericId } from "convex/values";
18
+ import schema from "../schema.js";
19
+
20
+ /**
21
+ * The names of all of your Convex tables.
22
+ */
23
+ export type TableNames = TableNamesInDataModel<DataModel>;
24
+
25
+ /**
26
+ * The type of a document stored in Convex.
27
+ *
28
+ * @typeParam TableName - A string literal type of the table name (like "users").
29
+ */
30
+ export type Doc<TableName extends TableNames> = DocumentByName<
31
+ DataModel,
32
+ TableName
33
+ >;
34
+
35
+ /**
36
+ * An identifier for a document in Convex.
37
+ *
38
+ * Convex documents are uniquely identified by their `Id`, which is accessible
39
+ * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
40
+ *
41
+ * Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
42
+ *
43
+ * IDs are just strings at runtime, but this type can be used to distinguish them from other
44
+ * strings when type checking.
45
+ *
46
+ * @typeParam TableName - A string literal type of the table name (like "users").
47
+ */
48
+ export type Id<TableName extends TableNames | SystemTableNames> =
49
+ GenericId<TableName>;
50
+
51
+ /**
52
+ * A type describing your Convex data model.
53
+ *
54
+ * This type includes information about what tables you have, the type of
55
+ * documents stored in those tables, and the indexes defined on them.
56
+ *
57
+ * This type is used to parameterize methods like `queryGeneric` and
58
+ * `mutationGeneric` to make them type-safe.
59
+ */
60
+ export type DataModel = DataModelFromSchemaDefinition<typeof schema>;