cvx-kit 0.1.2 → 0.1.4
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/CHANGELOG.md +38 -0
- package/README.md +69 -24
- package/dist/agent-tools.d.mts +28 -12
- package/dist/agent-tools.mjs +1 -1
- package/dist/agent-tools.mjs.map +1 -1
- package/dist/auth.d.mts +4 -2
- package/dist/auth.mjs +1 -1
- package/dist/auth.mjs.map +1 -1
- package/dist/components/approvals/_generated/api.d.mts +1 -1
- package/dist/components/approvals/audit.mjs +1 -1
- package/dist/components/approvals/audit.mjs.map +1 -1
- package/dist/components/approvals/client.d.mts +1 -1
- package/dist/components/approvals/client.mjs +1 -1
- package/dist/components/approvals/client.mjs.map +1 -1
- package/dist/components/approvals/constants.mjs.map +1 -1
- package/dist/components/approvals/decisions.d.mts +6 -7
- package/dist/components/approvals/decisions.mjs +1 -1
- package/dist/components/approvals/decisions.mjs.map +1 -1
- package/dist/components/approvals/functions.mjs +1 -1
- package/dist/components/approvals/functions.mjs.map +1 -1
- package/dist/components/approvals/health.mjs.map +1 -1
- package/dist/components/approvals/requests.mjs.map +1 -1
- package/dist/components/approvals/validators.mjs.map +1 -1
- package/dist/components/approvals/workflow.d.mts +1 -1
- package/dist/components/approvals/workflow.mjs +1 -1
- package/dist/components/approvals/workflow.mjs.map +1 -1
- package/dist/components/approvals/workflow_steps.d.mts +3 -3
- package/dist/components/approvals/workflow_steps.mjs +1 -1
- package/dist/components/approvals/workflow_steps.mjs.map +1 -1
- package/dist/components/foundation/client.d.mts +27 -28
- package/dist/components/foundation/client.mjs +1 -1
- package/dist/components/foundation/client.mjs.map +1 -1
- package/dist/components/foundation/modules/command/command.d.mts +17 -8
- package/dist/components/foundation/modules/command/command.mjs +1 -1
- package/dist/components/foundation/modules/command/command.mjs.map +1 -1
- package/dist/components/foundation/modules/observability/observability.d.mts +1 -1
- package/dist/components/foundation/modules/observability/observability.mjs +1 -1
- package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
- package/dist/components/foundation/modules/query/query.d.mts +8 -8
- package/dist/components/foundation/modules/query/query.mjs +1 -1
- package/dist/components/foundation/modules/query/query.mjs.map +1 -1
- package/dist/components/foundation/operation.d.mts +74 -0
- package/dist/components/foundation/operation.mjs +2 -0
- package/dist/components/foundation/operation.mjs.map +1 -0
- package/dist/components/foundation/result.d.mts +1 -1
- package/dist/components/foundation/result.mjs.map +1 -1
- package/dist/components/foundation/telemetry.mjs +1 -1
- package/dist/components/foundation/telemetry.mjs.map +1 -1
- package/dist/crud.d.mts +177 -16
- package/dist/crud.mjs +1 -1
- package/dist/crud.mjs.map +1 -1
- package/dist/middleware.d.mts +7 -7
- package/dist/middleware.mjs +1 -1
- package/dist/middleware.mjs.map +1 -1
- package/dist/oxlint.d.mts +9 -0
- package/dist/oxlint.mjs +2 -0
- package/dist/oxlint.mjs.map +1 -0
- package/dist/tenancy.mjs +1 -1
- package/dist/tenancy.mjs.map +1 -1
- package/dist/triggers.mjs +1 -1
- package/dist/triggers.mjs.map +1 -1
- package/dist/webhooks.d.mts +17 -106
- package/dist/webhooks.mjs.map +1 -1
- package/dist/zod-table.d.mts +47 -91
- package/dist/zod-table.mjs +1 -1
- package/dist/zod-table.mjs.map +1 -1
- package/docs/approvals.md +57 -56
- package/docs/architecture.md +1 -1
- package/docs/auth.md +57 -33
- package/docs/commands.md +159 -44
- package/docs/conventions.md +40 -40
- package/docs/crud.md +5 -5
- package/docs/llms.md +117 -71
- package/docs/maintainability.md +4 -4
- package/docs/migration.md +33 -33
- package/docs/tenancy.md +5 -8
- package/docs/triggers.md +4 -2
- package/docs/upgrading.md +74 -7
- package/docs/zod-table.md +70 -45
- package/package.json +41 -28
- package/scripts/convex-config-bridge.mjs +7 -18
- package/scripts/extract-changelog.mjs +1 -1
- package/scripts/smoke-packed-components.mjs +35 -75
- package/scripts/test-packed-ids.mjs +149 -0
- package/scripts/test-packed-oxlint.mjs +121 -0
- package/src/test.ts +9 -17
- package/docs/plans/2026-08-14-001-feat-v0.1.0-feature-set-plan.md +0 -461
- package/docs/plans/2026-08-21-0930-feat-resolve-organization-hook-plan.md +0 -178
package/dist/webhooks.d.mts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ErrorFactory } from "./errors.mjs";
|
|
2
|
+
import { TableBoundary } from "./zod-table.mjs";
|
|
3
|
+
import { FunctionReference, GenericDataModel, GenericMutationCtx } from "convex/server";
|
|
2
4
|
import { z } from "zod";
|
|
3
5
|
//#region src/webhooks.d.ts
|
|
4
6
|
/**
|
|
@@ -11,108 +13,12 @@ import { z } from "zod";
|
|
|
11
13
|
* longer than the provider's redelivery horizon (a cleanup cron over
|
|
12
14
|
* `receivedAt` — see docs/webhooks.md).
|
|
13
15
|
*/
|
|
14
|
-
declare function webhookEventsTable(): {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
_id: import("convex-helpers/server/zod4").Zid<"webhookEvents">;
|
|
21
|
-
_creationTime: z.ZodNumber;
|
|
22
|
-
eventKey: z.ZodString;
|
|
23
|
-
source: z.ZodString;
|
|
24
|
-
receivedAt: z.ZodNumber;
|
|
25
|
-
}, z.core.$strict>;
|
|
26
|
-
storage: z.ZodObject<{
|
|
27
|
-
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
28
|
-
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
29
|
-
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
30
|
-
eventKey: z.ZodString;
|
|
31
|
-
source: z.ZodString;
|
|
32
|
-
receivedAt: z.ZodNumber;
|
|
33
|
-
}, z.core.$strict>;
|
|
34
|
-
insertSchema: z.ZodObject<Omit<{
|
|
35
|
-
eventKey: z.ZodString;
|
|
36
|
-
source: z.ZodString;
|
|
37
|
-
receivedAt: z.ZodNumber;
|
|
38
|
-
}, "archivedAt" | "createdAt" | "updatedAt">, z.core.$strip>;
|
|
39
|
-
updateSchema: z.ZodObject<{
|
|
40
|
-
eventKey: z.ZodOptional<z.ZodString>;
|
|
41
|
-
source: z.ZodOptional<z.ZodString>;
|
|
42
|
-
receivedAt: z.ZodOptional<z.ZodNumber>;
|
|
43
|
-
}, z.core.$strip>;
|
|
44
|
-
commandInput: z.ZodObject<{}, z.core.$strict>;
|
|
45
|
-
publicDto: z.ZodObject<{}, z.core.$strict>;
|
|
46
|
-
toPublicDto: (row: {
|
|
47
|
-
createdAt?: number | undefined;
|
|
48
|
-
updatedAt?: number | undefined;
|
|
49
|
-
archivedAt?: number | undefined;
|
|
50
|
-
eventKey: string;
|
|
51
|
-
source: string;
|
|
52
|
-
receivedAt: number;
|
|
53
|
-
}) => Record<string, never>;
|
|
54
|
-
table: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
55
|
-
createdAt?: number | undefined;
|
|
56
|
-
updatedAt?: number | undefined;
|
|
57
|
-
archivedAt?: number | undefined;
|
|
58
|
-
eventKey: string;
|
|
59
|
-
source: string;
|
|
60
|
-
receivedAt: number;
|
|
61
|
-
}, {
|
|
62
|
-
createdAt: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
63
|
-
updatedAt: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
64
|
-
archivedAt: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
65
|
-
eventKey: import("convex/values").VString<string, "required">;
|
|
66
|
-
source: import("convex/values").VString<string, "required">;
|
|
67
|
-
receivedAt: import("convex/values").VFloat64<number, "required">;
|
|
68
|
-
}, "required", "archivedAt" | "createdAt" | "eventKey" | "receivedAt" | "source" | "updatedAt">, {}, {}, {}>;
|
|
69
|
-
insert: {
|
|
70
|
-
(): z.ZodObject<Omit<{
|
|
71
|
-
eventKey: z.ZodString;
|
|
72
|
-
source: z.ZodString;
|
|
73
|
-
receivedAt: z.ZodNumber;
|
|
74
|
-
}, "archivedAt" | "createdAt" | "updatedAt">, z.core.$strip>;
|
|
75
|
-
<OmitMask extends z.util.Mask<"eventKey" | "receivedAt" | "source">>(omit: OmitMask): z.ZodObject<Omit<Omit<{
|
|
76
|
-
eventKey: z.ZodString;
|
|
77
|
-
source: z.ZodString;
|
|
78
|
-
receivedAt: z.ZodNumber;
|
|
79
|
-
}, "archivedAt" | "createdAt" | "updatedAt">, Extract<"eventKey", keyof OmitMask> | Extract<"receivedAt", keyof OmitMask> | Extract<"source", keyof OmitMask>> extends (infer T) ? { [k in keyof T]: T[k]; } : never, z.core.$strip>;
|
|
80
|
-
};
|
|
81
|
-
update: {
|
|
82
|
-
(): z.ZodObject<{
|
|
83
|
-
eventKey: z.ZodOptional<z.ZodString>;
|
|
84
|
-
source: z.ZodOptional<z.ZodString>;
|
|
85
|
-
receivedAt: z.ZodOptional<z.ZodNumber>;
|
|
86
|
-
}, z.core.$strip>;
|
|
87
|
-
<OmitMask extends z.util.Mask<"eventKey" | "receivedAt" | "source">>(omit: OmitMask): z.ZodObject<Omit<{
|
|
88
|
-
eventKey: z.ZodOptional<z.ZodString>;
|
|
89
|
-
source: z.ZodOptional<z.ZodString>;
|
|
90
|
-
receivedAt: z.ZodOptional<z.ZodNumber>;
|
|
91
|
-
}, Extract<"eventKey", keyof OmitMask> | Extract<"receivedAt", keyof OmitMask> | Extract<"source", keyof OmitMask>> extends (infer T) ? { [k in keyof T]: T[k]; } : never, z.core.$strip>;
|
|
92
|
-
};
|
|
93
|
-
tools: {
|
|
94
|
-
insert: z.ZodObject<{}, z.core.$strict>;
|
|
95
|
-
update: z.ZodObject<{
|
|
96
|
-
data: z.ZodObject<{}, z.core.$strict>;
|
|
97
|
-
id: z.ZodType<import("convex/values").GenericId<"webhookEvents">, string, z.core.$ZodTypeInternals<import("convex/values").GenericId<"webhookEvents">, string>>;
|
|
98
|
-
}, z.core.$strict>;
|
|
99
|
-
id: z.ZodObject<{
|
|
100
|
-
id: z.ZodType<import("convex/values").GenericId<"webhookEvents">, string, z.core.$ZodTypeInternals<import("convex/values").GenericId<"webhookEvents">, string>>;
|
|
101
|
-
}, z.core.$strict>;
|
|
102
|
-
};
|
|
103
|
-
};
|
|
104
|
-
type DedupContext = {
|
|
105
|
-
db: {
|
|
106
|
-
query: (table: never) => {
|
|
107
|
-
withIndex: (index: never, range: (builder: {
|
|
108
|
-
eq: (field: never, value: never) => unknown;
|
|
109
|
-
}) => unknown) => {
|
|
110
|
-
first: () => Promise<unknown | null>;
|
|
111
|
-
};
|
|
112
|
-
};
|
|
113
|
-
insert: (table: never, value: never) => Promise<unknown>;
|
|
114
|
-
};
|
|
115
|
-
};
|
|
16
|
+
declare function webhookEventsTable(): TableBoundary<"webhookEvents", {
|
|
17
|
+
eventKey: z.ZodString;
|
|
18
|
+
source: z.ZodString;
|
|
19
|
+
receivedAt: z.ZodNumber;
|
|
20
|
+
}, readonly [], readonly [], readonly []>;
|
|
21
|
+
type DedupContext = Pick<GenericMutationCtx<GenericDataModel>, 'db'>;
|
|
116
22
|
/**
|
|
117
23
|
* Transactional insert-if-absent dedup guard. Call this FIRST inside the
|
|
118
24
|
* internal mutation the boundary delegates to — dedup must live in the
|
|
@@ -155,10 +61,15 @@ type WebhookBoundaryConfig = {
|
|
|
155
61
|
* boundary.handle(ctx, request, internal.domain.vendor.functions.applyEvent)) })
|
|
156
62
|
*/
|
|
157
63
|
declare function createWebhookBoundary(config: WebhookBoundaryConfig): {
|
|
158
|
-
handle: (ctx: {
|
|
159
|
-
runMutation: (target:
|
|
160
|
-
}, request: Request, target:
|
|
64
|
+
handle: <Result>(ctx: {
|
|
65
|
+
runMutation: (target: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>, args: WebhookDelivery) => Promise<Result>;
|
|
66
|
+
}, request: Request, target: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>) => Promise<Response>;
|
|
67
|
+
};
|
|
68
|
+
type WebhookDelivery = {
|
|
69
|
+
eventKey: string;
|
|
70
|
+
payload: string;
|
|
71
|
+
source: string;
|
|
161
72
|
};
|
|
162
73
|
//#endregion
|
|
163
|
-
export { WebhookBoundaryConfig, createWebhookBoundary, recordWebhookEvent, webhookEventsTable };
|
|
74
|
+
export { WebhookBoundaryConfig, WebhookDelivery, createWebhookBoundary, recordWebhookEvent, webhookEventsTable };
|
|
164
75
|
//# sourceMappingURL=webhooks.d.mts.map
|
package/dist/webhooks.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhooks.mjs","names":[],"sources":["../src/webhooks.ts"],"sourcesContent":["import { z } from 'zod'\nimport { defaultErrors, type ErrorFactory } from './errors'\nimport { zodTable } from './zod-table'\n\n/**\n * The host-owned dedup table (the kit gains no tables). Register it in the\n * schema with the by_eventKey index:\n *\n * webhookEvents: webhookEventsTable().table.index('by_eventKey', ['eventKey'])\n *\n * Rows are host-owned data: prune them on a retention window comfortably\n * longer than the provider's redelivery horizon (a cleanup cron over\n * `receivedAt` — see docs/webhooks.md).\n */\nexport function webhookEventsTable() {\n\treturn zodTable('webhookEvents', () => ({\n\t\teventKey: z.string(),\n\t\tsource: z.string(),\n\t\treceivedAt: z.number(),\n\t}))\n}\n\ntype DedupContext =
|
|
1
|
+
{"version":3,"file":"webhooks.mjs","names":[],"sources":["../src/webhooks.ts"],"sourcesContent":["import type { GenericMutationCtx, GenericDataModel, FunctionReference } from 'convex/server'\nimport { z } from 'zod'\nimport { defaultErrors, type ErrorFactory } from './errors'\nimport { zodTable } from './zod-table'\n\n/**\n * The host-owned dedup table (the kit gains no tables). Register it in the\n * schema with the by_eventKey index:\n *\n * webhookEvents: webhookEventsTable().table.index('by_eventKey', ['eventKey'])\n *\n * Rows are host-owned data: prune them on a retention window comfortably\n * longer than the provider's redelivery horizon (a cleanup cron over\n * `receivedAt` — see docs/webhooks.md).\n */\nexport function webhookEventsTable() {\n\treturn zodTable('webhookEvents', () => ({\n\t\teventKey: z.string(),\n\t\tsource: z.string(),\n\t\treceivedAt: z.number(),\n\t}))\n}\n\ntype DedupContext = Pick<GenericMutationCtx<GenericDataModel>, 'db'>\n\n/**\n * Transactional insert-if-absent dedup guard. Call this FIRST inside the\n * internal mutation the boundary delegates to — dedup must live in the\n * mutation (transactional), never in the action. Returns duplicate: true\n * when the natural key was already recorded; the caller then no-ops.\n */\nexport async function recordWebhookEvent(\n\tctx: DedupContext,\n\tinput: { key: string; source?: string; table?: string },\n): Promise<{ duplicate: boolean }> {\n\tconst table = input.table ?? 'webhookEvents'\n\tconst existing = await ctx.db\n\t\t.query(table)\n\t\t.withIndex('by_eventKey', (builder) => builder.eq('eventKey', input.key))\n\t\t.first()\n\tif (existing) return { duplicate: true }\n\tawait ctx.db.insert(table, {\n\t\teventKey: input.key,\n\t\tsource: input.source ?? 'unknown',\n\t\treceivedAt: Date.now(),\n\t})\n\treturn { duplicate: false }\n}\n\nexport type WebhookBoundaryConfig = {\n\t/**\n\t * Verifies the provider signature over the RAW body bytes — the helper\n\t * hands it `request.text()` BEFORE any parsing. Implementations must use\n\t * a constant-time comparison (e.g. crypto.subtle HMAC verify) and read\n\t * the secret from a Convex environment variable. Return false or throw\n\t * to reject.\n\t */\n\tverify: (raw: string, request: Request) => boolean | Promise<boolean>\n\t/**\n\t * Derives the natural dedup key — the `${event}:${id}:${updatedAt}`\n\t * pattern. Receives the raw body and the request; parse inside if needed.\n\t */\n\teventKey: (raw: string, request: Request) => string | Promise<string>\n\tsource?: string\n\terrors?: ErrorFactory\n}\n\n/**\n * The sanctioned webhook pattern as code: verify (raw body, fail closed) →\n * derive natural key → delegate to a host internal mutation whose FIRST act\n * is recordWebhookEvent (transactional dedup). Use inside the host's\n * httpAction:\n *\n * const boundary = createWebhookBoundary({ verify, eventKey })\n * http.route({ path: '/webhooks/vendor', method: 'POST',\n * handler: httpAction((ctx, request) =>\n * boundary.handle(ctx, request, internal.domain.vendor.functions.applyEvent)) })\n */\nexport function createWebhookBoundary(config: WebhookBoundaryConfig) {\n\tconst errors = config.errors ?? defaultErrors\n\n\tasync function handle<Result>(\n\t\tctx: {\n\t\t\trunMutation: (\n\t\t\t\ttarget: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>,\n\t\t\t\targs: WebhookDelivery,\n\t\t\t) => Promise<Result>\n\t\t},\n\t\trequest: Request,\n\t\ttarget: FunctionReference<'mutation', 'internal', WebhookDelivery, Result>,\n\t): Promise<Response> {\n\t\tconst raw = await request.text()\n\t\tlet verified = false\n\t\ttry {\n\t\t\tverified = await config.verify(raw, request)\n\t\t} catch {\n\t\t\tverified = false\n\t\t}\n\t\tif (!verified) {\n\t\t\terrors.throw({\n\t\t\t\tcode: 'WEBHOOK_SIGNATURE_INVALID',\n\t\t\t\tmessage: 'Webhook signature verification failed',\n\t\t\t})\n\t\t}\n\t\tconst key = await config.eventKey(raw, request)\n\t\tawait ctx.runMutation(target, {\n\t\t\teventKey: key,\n\t\t\tpayload: raw,\n\t\t\tsource: config.source ?? 'unknown',\n\t\t})\n\t\treturn new Response(null, { status: 200 })\n\t}\n\n\treturn { handle }\n}\n\nexport type WebhookDelivery = { eventKey: string; payload: string; source: string }\n"],"mappings":"gHAeA,SAAgB,GAAqB,CACpC,OAAO,EAAS,qBAAwB,CACvC,SAAU,EAAE,OAAO,EACnB,OAAQ,EAAE,OAAO,EACjB,WAAY,EAAE,OAAO,CACtB,EAAE,CACH,CAUA,eAAsB,EACrB,EACA,EACkC,CAClC,IAAM,EAAQ,EAAM,OAAS,gBAW7B,OANI,MAJmB,EAAI,GACzB,MAAM,CAAK,CAAC,CACZ,UAAU,cAAgB,GAAY,EAAQ,GAAG,WAAY,EAAM,GAAG,CAAC,CAAC,CACxE,MAAM,EACa,CAAE,UAAW,EAAK,GACvC,MAAM,EAAI,GAAG,OAAO,EAAO,CAC1B,SAAU,EAAM,IAChB,OAAQ,EAAM,QAAU,UACxB,WAAY,KAAK,IAAI,CACtB,CAAC,EACM,CAAE,UAAW,EAAM,EAC3B,CA+BA,SAAgB,EAAsB,EAA+B,CACpE,IAAM,EAAS,EAAO,QAAU,EAEhC,eAAe,EACd,EAMA,EACA,EACoB,CACpB,IAAM,EAAM,MAAM,EAAQ,KAAK,EAC3B,EAAW,GACf,GAAI,CACH,EAAW,MAAM,EAAO,OAAO,EAAK,CAAO,CAC5C,MAAQ,CACP,EAAW,EACZ,CACK,GACJ,EAAO,MAAM,CACZ,KAAM,4BACN,QAAS,uCACV,CAAC,EAEF,IAAM,EAAM,MAAM,EAAO,SAAS,EAAK,CAAO,EAM9C,OALA,MAAM,EAAI,YAAY,EAAQ,CAC7B,SAAU,EACV,QAAS,EACT,OAAQ,EAAO,QAAU,SAC1B,CAAC,EACM,IAAI,SAAS,KAAM,CAAE,OAAQ,GAAI,CAAC,CAC1C,CAEA,MAAO,CAAE,QAAO,CACjB"}
|
package/dist/zod-table.d.mts
CHANGED
|
@@ -1,115 +1,62 @@
|
|
|
1
|
-
import { ConvexValidatorFromZod, zid } from "convex-helpers/server/zod4";
|
|
1
|
+
import { ConvexValidatorFromZod, convexToZod, zid, zid as zid$1, zodToConvex } from "convex-helpers/server/zod4";
|
|
2
2
|
import { TableDefinition } from "convex/server";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { GenericId } from "convex/values";
|
|
5
5
|
//#region src/zod-table.d.ts
|
|
6
|
-
type
|
|
7
|
-
type
|
|
6
|
+
type FieldSchemas = Record<string, z.ZodType>;
|
|
7
|
+
type FieldKey<Fields extends FieldSchemas> = Extract<keyof Fields, string>;
|
|
8
8
|
/**
|
|
9
9
|
* Opinionated lifecycle timestamps — every table has them, server-owned,
|
|
10
10
|
* always excluded from insert/command boundaries. Maintained by the
|
|
11
11
|
* `timestamps` trigger helper (see cvx-kit/triggers).
|
|
12
12
|
*/
|
|
13
|
-
declare const
|
|
13
|
+
declare const timestampFields: {
|
|
14
14
|
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
15
15
|
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
16
16
|
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
17
17
|
};
|
|
18
|
-
type
|
|
18
|
+
type TimestampFields = typeof timestampFields;
|
|
19
19
|
declare const TIMESTAMP_FIELDS: readonly ['createdAt', 'updatedAt', 'archivedAt'];
|
|
20
|
-
type TableBoundaryOptions<Fields extends
|
|
20
|
+
type TableBoundaryOptions<Fields extends FieldSchemas, ServerFields extends readonly FieldKey<Fields>[], CommandFields extends readonly FieldKey<Fields>[], PublicFields extends readonly FieldKey<Fields & TimestampFields>[]> = {
|
|
21
21
|
serverFields?: ServerFields;
|
|
22
22
|
commandFields?: CommandFields;
|
|
23
23
|
publicFields?: PublicFields;
|
|
24
24
|
};
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
type StrictObject<Fields extends FieldSchemas> = z.ZodObject<Fields, z.core.$strict>;
|
|
26
|
+
type WriteFields<Fields extends FieldSchemas, ServerFields extends readonly FieldKey<Fields>[]> = Omit<Fields & TimestampFields, ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number]>;
|
|
27
|
+
type PartialFields<Fields extends FieldSchemas> = { [Key in keyof Fields]: z.ZodOptional<Fields[Key]>; };
|
|
28
|
+
/** Named schema relationships keep generated declarations bounded and consumers fully typed. */
|
|
29
|
+
type TableBoundary<Table extends string, Fields extends FieldSchemas, ServerFields extends readonly FieldKey<Fields>[], CommandFields extends readonly FieldKey<Fields>[], PublicFields extends readonly FieldKey<Fields & TimestampFields>[]> = {
|
|
27
30
|
tableName: Table;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
_id: import("convex-helpers/server/zod4").Zid<Table>;
|
|
41
|
-
_creationTime: z.ZodNumber;
|
|
42
|
-
}) extends (infer T) ? { [k in keyof T]: T[k]; } : never, z.core.$strict>;
|
|
43
|
-
storage: z.ZodObject<Fields & {
|
|
44
|
-
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
45
|
-
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
46
|
-
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
47
|
-
} extends (infer T_1) ? { -readonly [P in keyof T_1]: T_1[P]; } : never, z.core.$strict>;
|
|
48
|
-
insertSchema: z.ZodObject<Omit<Fields, "archivedAt" | "createdAt" | "updatedAt" | ServerFields[number]>, z.core.$strip>;
|
|
49
|
-
updateSchema: z.ZodObject<Omit<Fields, "archivedAt" | "createdAt" | "updatedAt" | ServerFields[number]> extends (infer T_3 extends Readonly<{
|
|
50
|
-
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
51
|
-
}>) ? { -readonly [k in keyof T_3]: z.ZodOptional<T_3[k]>; } : never, z.core.$strip>;
|
|
52
|
-
commandInput: z.ZodObject<Pick<Fields & {
|
|
53
|
-
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
54
|
-
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
55
|
-
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
56
|
-
}, CommandFields[number]> extends (infer T_4) ? { -readonly [P in keyof T_4]: T_4[P]; } : never, z.core.$strict>;
|
|
57
|
-
publicDto: z.ZodObject<Pick<Fields & {
|
|
58
|
-
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
59
|
-
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
60
|
-
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
61
|
-
}, PublicFields[number]> extends (infer T_5) ? { -readonly [P in keyof T_5]: T_5[P]; } : never, z.core.$strict>;
|
|
62
|
-
toPublicDto: (row: z.input<z.ZodObject<Fields & {
|
|
63
|
-
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
64
|
-
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
65
|
-
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
66
|
-
} extends (infer T_1) ? { -readonly [P in keyof T_1]: T_1[P]; } : never, z.core.$strict>>) => z.core.$InferObjectOutput<Pick<Fields & {
|
|
67
|
-
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
68
|
-
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
69
|
-
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
70
|
-
}, PublicFields[number]> extends (infer T_5) ? { -readonly [P in keyof T_5]: T_5[P]; } : never, {}>;
|
|
71
|
-
table: TableDefinition<ConvexValidatorFromZod<z.ZodObject<Fields & {
|
|
72
|
-
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
73
|
-
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
74
|
-
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
75
|
-
} extends (infer T_1) ? { -readonly [P in keyof T_1]: T_1[P]; } : never, z.core.$strict>, 'required'>>;
|
|
76
|
-
insert: {
|
|
77
|
-
(): z.ZodObject<Omit<Fields, "archivedAt" | "createdAt" | "updatedAt" | ServerFields[number]>, z.core.$strip>;
|
|
78
|
-
<OmitMask extends z.util.Mask<Exclude<keyof Fields, "archivedAt" | "createdAt" | "updatedAt" | ServerFields[number]>>>(omit: OmitMask): ZodObjectOmit<z.ZodObject<Omit<Fields, "archivedAt" | "createdAt" | "updatedAt" | ServerFields[number]>, z.core.$strip>, OmitMask>;
|
|
79
|
-
};
|
|
80
|
-
update: {
|
|
81
|
-
(): z.ZodObject<Omit<Fields, "archivedAt" | "createdAt" | "updatedAt" | ServerFields[number]> extends (infer T_3 extends Readonly<{
|
|
82
|
-
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
83
|
-
}>) ? { -readonly [k in keyof T_3]: z.ZodOptional<T_3[k]>; } : never, z.core.$strip>;
|
|
84
|
-
<OmitMask extends z.util.Mask<Exclude<keyof Fields, "archivedAt" | "createdAt" | "updatedAt" | ServerFields[number]>>>(omit: OmitMask): ZodObjectOmit<z.ZodObject<Omit<Fields, "archivedAt" | "createdAt" | "updatedAt" | ServerFields[number]> extends (infer T_3 extends Readonly<{
|
|
85
|
-
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
86
|
-
}>) ? { -readonly [k in keyof T_3]: z.ZodOptional<T_3[k]>; } : never, z.core.$strip>, OmitMask>;
|
|
87
|
-
};
|
|
31
|
+
storage: StrictObject<Fields & TimestampFields>;
|
|
32
|
+
schema: ReturnType<typeof documentSchema<Fields & TimestampFields, Table>>;
|
|
33
|
+
insertSchema: StrictObject<WriteFields<Fields, ServerFields>>;
|
|
34
|
+
updateSchema: StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>;
|
|
35
|
+
commandInput: StrictObject<Pick<Fields & TimestampFields, CommandFields[number]>>;
|
|
36
|
+
publicDto: StrictObject<Pick<Fields & TimestampFields, PublicFields[number]>>;
|
|
37
|
+
toPublicDto: (row: z.input<StrictObject<Fields & TimestampFields>>) => z.output<StrictObject<Pick<Fields & TimestampFields, PublicFields[number]>>>;
|
|
38
|
+
table: TableDefinition<ConvexValidatorFromZod<StrictObject<Fields & TimestampFields>, 'required'>>;
|
|
39
|
+
insert(): StrictObject<WriteFields<Fields, ServerFields>>;
|
|
40
|
+
insert<Mask extends z.util.Mask<keyof WriteFields<Fields, ServerFields>>>(omit: Mask): ZodObjectOmit<StrictObject<WriteFields<Fields, ServerFields>>, Mask>;
|
|
41
|
+
update(): StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>;
|
|
42
|
+
update<Mask extends z.util.Mask<keyof WriteFields<Fields, ServerFields>>>(omit: Mask): ZodObjectOmit<StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>, Mask>;
|
|
88
43
|
tools: {
|
|
89
|
-
insert:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
updatedAt: z.ZodOptional<z.ZodNumber>;
|
|
98
|
-
archivedAt: z.ZodOptional<z.ZodNumber>;
|
|
99
|
-
}, CommandFields[number]> extends (infer T_4) ? { -readonly [P in keyof T_4]: T_4[P]; } : never) extends (infer T_6 extends Readonly<{
|
|
100
|
-
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
101
|
-
}>) ? { -readonly [k in keyof T_6]: z.ZodOptional<T_6[k]>; } : never, z.core.$strict>;
|
|
102
|
-
id: z.ZodType<GenericId<Table>, string, z.core.$ZodTypeInternals<GenericId<Table>, string>>;
|
|
103
|
-
}, z.core.$strict>;
|
|
104
|
-
id: z.ZodObject<{
|
|
105
|
-
id: z.ZodType<GenericId<Table>, string, z.core.$ZodTypeInternals<GenericId<Table>, string>>;
|
|
106
|
-
}, z.core.$strict>;
|
|
44
|
+
insert: StrictObject<Pick<Fields & TimestampFields, CommandFields[number]>>;
|
|
45
|
+
update: StrictObject<{
|
|
46
|
+
data: StrictObject<PartialFields<Pick<Fields & TimestampFields, CommandFields[number]>>>;
|
|
47
|
+
id: z.ZodType<GenericId<Table>, string>;
|
|
48
|
+
}>;
|
|
49
|
+
id: StrictObject<{
|
|
50
|
+
id: z.ZodType<GenericId<Table>, string>;
|
|
51
|
+
}>;
|
|
107
52
|
};
|
|
108
53
|
};
|
|
109
|
-
|
|
54
|
+
/** Defines one table and derives its storage, document, write, and DTO boundaries. */
|
|
55
|
+
declare function zodTable<Table extends string, Fields extends FieldSchemas, const ServerFields extends readonly FieldKey<Fields>[] = readonly [], const CommandFields extends readonly FieldKey<Fields>[] = readonly [], const PublicFields extends readonly FieldKey<Fields & TimestampFields>[] = readonly []>(tableName: Table, fields: (id: typeof zid$1) => Fields, options?: TableBoundaryOptions<Fields, ServerFields, CommandFields, PublicFields>): TableBoundary<Table, Fields, ServerFields, CommandFields, PublicFields>;
|
|
56
|
+
declare const tenantFields: {
|
|
110
57
|
tenant: z.ZodString;
|
|
111
58
|
};
|
|
112
|
-
type
|
|
59
|
+
type TenantFields = typeof tenantFields;
|
|
113
60
|
/**
|
|
114
61
|
* A zodTable whose rows are tenant-owned: injects the server-owned `tenant`
|
|
115
62
|
* field (the tenancy boundary — see cvx-kit/tenancy). Like timestamps, it is
|
|
@@ -118,7 +65,9 @@ type TenantShape = typeof tenantShape;
|
|
|
118
65
|
* trigger forbids reassigning it. Expose it via publicFields only when a DTO
|
|
119
66
|
* genuinely needs it.
|
|
120
67
|
*/
|
|
121
|
-
declare function tenantTable<Table extends string, Fields extends
|
|
68
|
+
declare function tenantTable<Table extends string, Fields extends FieldSchemas, const ServerFields extends readonly FieldKey<Fields>[] = readonly [], const CommandFields extends readonly FieldKey<Fields>[] = readonly [], const PublicFields extends readonly FieldKey<Fields & TenantFields & TimestampFields>[] = readonly []>(tableName: Table, fields: (id: typeof zid$1) => Fields, options?: TableBoundaryOptions<Fields & TenantFields, readonly (ServerFields[number] | 'tenant')[], CommandFields, PublicFields>): TableBoundary<Table, Fields & {
|
|
69
|
+
tenant: z.ZodString;
|
|
70
|
+
}, readonly ("tenant" | ServerFields[number])[], CommandFields, PublicFields>;
|
|
122
71
|
/**
|
|
123
72
|
* Merges per-module table maps into the application schema map, rejecting
|
|
124
73
|
* duplicate table names across modules — the module-registry combinator for
|
|
@@ -169,7 +118,14 @@ declare function zodVariantTable<Table extends string, Schema extends z.ZodType>
|
|
|
169
118
|
* LLM-tool inputs stay primitive, while keeping the Id type at compile time.
|
|
170
119
|
*/
|
|
171
120
|
declare function jsonSafeZid<Table extends string>(tableName: Table): z.ZodType<GenericId<Table>, string>;
|
|
172
|
-
type ZodObjectOmit<Schema extends z.ZodObject<any, any>, OmitMask extends z.util.Mask<keyof Schema['shape']>> = Schema extends z.ZodObject<infer
|
|
121
|
+
type ZodObjectOmit<Schema extends z.ZodObject<any, any>, OmitMask extends z.util.Mask<keyof Schema['shape']>> = Schema extends z.ZodObject<infer ObjectFields, infer Config> ? z.ZodObject<z.util.Flatten<Omit<ObjectFields, Extract<keyof ObjectFields, keyof OmitMask>>>, Config> : never;
|
|
122
|
+
declare function documentSchema<Fields extends FieldSchemas, Table extends string>(storage: StrictObject<Fields>, tableName: Table): z.ZodObject<(keyof Fields & ("_creationTime" | "_id") extends never ? Fields & {
|
|
123
|
+
_id: import("convex-helpers/server/zod4").Zid<Table>;
|
|
124
|
+
_creationTime: z.ZodNumber;
|
|
125
|
+
} : { [K in keyof Fields as K extends "_creationTime" | "_id" ? never : K]: Fields[K]; } & {
|
|
126
|
+
_id: import("convex-helpers/server/zod4").Zid<Table>;
|
|
127
|
+
_creationTime: z.ZodNumber;
|
|
128
|
+
}) extends (infer T) ? { [k in keyof T]: T[k]; } : never, z.core.$strict>;
|
|
173
129
|
//#endregion
|
|
174
|
-
export { TIMESTAMP_FIELDS, TableBoundaryOptions, createModule, jsonSafeZid, paginated, tenantTable, zodTable, zodVariantTable };
|
|
130
|
+
export { TIMESTAMP_FIELDS, TableBoundary, TableBoundaryOptions, convexToZod, createModule, jsonSafeZid, paginated, tenantTable, zid, zodTable, zodToConvex, zodVariantTable };
|
|
175
131
|
//# sourceMappingURL=zod-table.d.mts.map
|
package/dist/zod-table.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{convexToZod as e,
|
|
1
|
+
import{convexToZod as e,convexToZod as t,zid as n,zid as r,zodToConvex as i,zodToConvex as a}from"convex-helpers/server/zod4";import{defineTable as o,paginationOptsValidator as s}from"convex/server";import{z as c}from"zod";const l={createdAt:c.number().optional(),updatedAt:c.number().optional(),archivedAt:c.number().optional()},u=[`createdAt`,`updatedAt`,`archivedAt`];function d(e,t,n={}){let i={...t(r),...l},s=c.object(i).strict(),d=v(s,e),f=[...u,...n.serverFields??[]],p=n.commandFields??[],m=n.publicFields??[],h=c.object(b(i,f)).strict(),g=h.partial(),x=c.object(y(i,p)).strict(),S=c.object(y(i,m)).strict(),C=e=>S.parse(Object.fromEntries(Object.keys(S.shape).map(t=>[t,e[t]])));function w(e){return e?h.omit(e):h}function T(e){return e?g.omit(e):g}return{tableName:e,schema:d,storage:s,insertSchema:h,updateSchema:g,commandInput:x,publicDto:S,toPublicDto:C,table:o(a(s)),insert:w,update:T,tools:{insert:x,update:c.object({data:x.partial(),id:_(e)}).strict(),id:c.object({id:_(e)}).strict()}}}const f={tenant:c.string().min(1)};function p(e,t,n={}){return d(e,e=>({...t(e),...f}),{...n,serverFields:[...n.serverFields??[],`tenant`]})}function m(...e){let t={};for(let n of e)for(let[e,r]of Object.entries(n)){if(e in t)throw Error(`Table "${e}" is declared by more than one module`);t[e]=r}return t}function h(e){return{args:{paginationOpts:t(s)},result:c.object({page:c.array(e),isDone:c.boolean(),continueCursor:c.string(),splitCursor:c.string().nullable().optional(),pageStatus:c.enum([`SplitRecommended`,`SplitRequired`]).nullable().optional()})}}function g(e,t){return{tableName:e,storage:t,insertSchema:t,table:o(a(t))}}function _(e){let t=r(e);return c.string().refine(e=>t.safeParse(e).success).describe(`Convex document id for table "${e}"; validated against the table by Convex`)}function v(e,t){return e.extend({_id:r(t),_creationTime:c.number()})}function y(e,t){return Object.fromEntries(t.map(t=>[t,e[t]]))}function b(e,t){let n=new Set(t);return Object.fromEntries(Object.entries(e).filter(([e])=>!n.has(e)))}export{u as TIMESTAMP_FIELDS,e as convexToZod,m as createModule,_ as jsonSafeZid,h as paginated,p as tenantTable,n as zid,d as zodTable,i as zodToConvex,g as zodVariantTable};
|
|
2
2
|
//# sourceMappingURL=zod-table.mjs.map
|
package/dist/zod-table.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod-table.mjs","names":[],"sources":["../src/zod-table.ts"],"sourcesContent":["import {\n\ttype ConvexValidatorFromZod,\n\tconvexToZod,\n\tzid,\n\tzodToConvex,\n} from 'convex-helpers/server/zod4'\nimport {\n\tdefineTable,\n\tpaginationOptsValidator,\n\ttype TableDefinition,\n} from 'convex/server'\nimport type { GenericId } from 'convex/values'\nimport { z } from 'zod'\n\ntype Shape = Record<string, z.ZodType>\ntype ShapeKey<Fields extends Shape> = Extract<keyof Fields, string>\n\n/**\n * Opinionated lifecycle timestamps — every table has them, server-owned,\n * always excluded from insert/command boundaries. Maintained by the\n * `timestamps` trigger helper (see cvx-kit/triggers).\n */\nconst timestampShape = {\n\tcreatedAt: z.number().optional(),\n\tupdatedAt: z.number().optional(),\n\tarchivedAt: z.number().optional(),\n} satisfies Shape\n\ntype TimestampShape = typeof timestampShape\n\nexport const TIMESTAMP_FIELDS = [\n\t'createdAt',\n\t'updatedAt',\n\t'archivedAt',\n] as const\n\nexport type TableBoundaryOptions<\n\tFields extends Shape,\n\tServerFields extends readonly ShapeKey<Fields>[],\n\tCommandFields extends readonly ShapeKey<Fields>[],\n\tPublicFields extends readonly ShapeKey<Fields & TimestampShape>[],\n> = {\n\tserverFields?: ServerFields\n\tcommandFields?: CommandFields\n\tpublicFields?: PublicFields\n}\n\n/** Defines one table and derives its storage, document, write, and DTO boundaries. */\nexport function zodTable<\n\tTable extends string,\n\tFields extends Shape,\n\tconst ServerFields extends readonly ShapeKey<Fields>[] = readonly [],\n\tconst CommandFields extends readonly ShapeKey<Fields>[] = readonly [],\n\tconst PublicFields extends readonly ShapeKey<\n\t\tFields & TimestampShape\n\t>[] = readonly [],\n>(\n\ttableName: Table,\n\tfields: (id: typeof zid) => Fields,\n\toptions: TableBoundaryOptions<\n\t\tFields,\n\t\tServerFields,\n\t\tCommandFields,\n\t\tPublicFields\n\t> = {},\n) {\n\tconst shape = { ...fields(zid), ...timestampShape } as Fields &\n\t\tTimestampShape\n\tconst storage = z.object(shape).strict()\n\tconst schema = storage.extend({\n\t\t_id: zid(tableName),\n\t\t_creationTime: z.number(),\n\t})\n\tconst insertSchema = z\n\t\t.object(\n\t\t\tomitShape(shape, [\n\t\t\t\t...TIMESTAMP_FIELDS,\n\t\t\t\t...(options.serverFields ?? ([] as never)),\n\t\t\t] as never),\n\t\t)\n\t\t.strict() as z.ZodObject<\n\t\tOmit<Fields, ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number]>\n\t>\n\tconst updateSchema = insertSchema.partial()\n\tconst commandInput = z\n\t\t.object(pickShape(shape, options.commandFields ?? ([] as never)))\n\t\t.strict()\n\tconst publicDto = z\n\t\t.object(pickShape(shape, options.publicFields ?? ([] as never)))\n\t\t.strict()\n\tconst toPublicDto = (row: z.input<typeof storage>) =>\n\t\tpublicDto.parse(\n\t\t\tObject.fromEntries(\n\t\t\t\tObject.keys(publicDto.shape).map((key) => [\n\t\t\t\t\tkey,\n\t\t\t\t\t(row as Record<string, unknown>)[key],\n\t\t\t\t]),\n\t\t\t),\n\t\t)\n\n\ttype InsertSchema = typeof insertSchema\n\ttype UpdateSchema = typeof updateSchema\n\ttype InsertMask = z.util.Mask<keyof InsertSchema['shape']>\n\ttype UpdateMask = z.util.Mask<keyof UpdateSchema['shape']>\n\n\tfunction insert(): InsertSchema\n\tfunction insert<OmitMask extends InsertMask>(\n\t\tomit: OmitMask,\n\t): ZodObjectOmit<InsertSchema, OmitMask>\n\tfunction insert(omit?: InsertMask) {\n\t\treturn omit ? insertSchema.omit(omit as never) : insertSchema\n\t}\n\n\tfunction update(): UpdateSchema\n\tfunction update<OmitMask extends UpdateMask>(\n\t\tomit: OmitMask,\n\t): ZodObjectOmit<UpdateSchema, OmitMask>\n\tfunction update(omit?: UpdateMask) {\n\t\treturn omit ? updateSchema.omit(omit as never) : updateSchema\n\t}\n\n\treturn {\n\t\ttableName,\n\t\tschema,\n\t\tstorage,\n\t\tinsertSchema,\n\t\tupdateSchema,\n\t\tcommandInput,\n\t\tpublicDto,\n\t\ttoPublicDto,\n\t\ttable: defineTable(zodToConvex(storage)) as TableDefinition<\n\t\t\tConvexValidatorFromZod<typeof storage, 'required'>\n\t\t>,\n\t\tinsert,\n\t\tupdate,\n\t\ttools: {\n\t\t\t// jsonSafeZid (not zid): tool masks feed generated JSON schemas, so\n\t\t\t// ids must present as plain strings while keeping the Id<...> type.\n\t\t\tinsert: commandInput,\n\t\t\tupdate: z\n\t\t\t\t.object({ data: commandInput.partial(), id: jsonSafeZid(tableName) })\n\t\t\t\t.strict(),\n\t\t\tid: z.object({ id: jsonSafeZid(tableName) }).strict(),\n\t\t},\n\t}\n}\n\nconst tenantShape = {\n\ttenant: z.string().min(1),\n} satisfies Shape\n\ntype TenantShape = typeof tenantShape\n\n/**\n * A zodTable whose rows are tenant-owned: injects the server-owned `tenant`\n * field (the tenancy boundary — see cvx-kit/tenancy). Like timestamps, it is\n * excluded from insert/update/command boundaries; handlers stamp it from\n * ctx.tenant, row-level security matches on it, and the tenantOwnership\n * trigger forbids reassigning it. Expose it via publicFields only when a DTO\n * genuinely needs it.\n */\nexport function tenantTable<\n\tTable extends string,\n\tFields extends Shape,\n\tconst ServerFields extends readonly ShapeKey<Fields>[] = readonly [],\n\tconst CommandFields extends readonly ShapeKey<Fields>[] = readonly [],\n\tconst PublicFields extends readonly ShapeKey<\n\t\tFields & TenantShape & TimestampShape\n\t>[] = readonly [],\n>(\n\ttableName: Table,\n\tfields: (id: typeof zid) => Fields,\n\toptions: TableBoundaryOptions<\n\t\tFields & TenantShape,\n\t\treadonly (ServerFields[number] | 'tenant')[],\n\t\tCommandFields,\n\t\tPublicFields\n\t> = {},\n) {\n\treturn zodTable(\n\t\ttableName,\n\t\t(id) => ({ ...fields(id), ...tenantShape }),\n\t\t{\n\t\t\t...options,\n\t\t\tserverFields: [\n\t\t\t\t...((options.serverFields ?? []) as readonly (\n\t\t\t\t\t| ServerFields[number]\n\t\t\t\t\t| 'tenant'\n\t\t\t\t)[]),\n\t\t\t\t'tenant',\n\t\t\t] as never,\n\t\t} as never,\n\t) as ReturnType<\n\t\ttypeof zodTable<\n\t\t\tTable,\n\t\t\tFields & TenantShape,\n\t\t\treadonly (ServerFields[number] | 'tenant')[],\n\t\t\tCommandFields,\n\t\t\tPublicFields\n\t\t>\n\t>\n}\n\n/**\n * Merges per-module table maps into the application schema map, rejecting\n * duplicate table names across modules — the module-registry combinator for\n * domain/table.ts: `defineSchema(createModule(catalogTables, salesTables))`.\n */\nexport function createModule<\n\tconst Maps extends readonly Record<string, TableDefinition<any>>[],\n>(...maps: Maps): UnionToIntersection<Maps[number]> {\n\tconst combined: Record<string, TableDefinition<any>> = {}\n\tfor (const map of maps) {\n\t\tfor (const [tableName, definition] of Object.entries(map)) {\n\t\t\tif (tableName in combined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Table \"${tableName}\" is declared by more than one module`,\n\t\t\t\t)\n\t\t\t}\n\t\t\tcombined[tableName] = definition\n\t\t}\n\t}\n\treturn combined as UnionToIntersection<Maps[number]>\n}\n\ntype UnionToIntersection<Union> = (\n\tUnion extends unknown\n\t\t? (member: Union) => void\n\t\t: never\n) extends (member: infer Intersection) => void\n\t? Intersection\n\t: never\n\n/**\n * Cursor-pagination boundary schemas bridged from Convex's own validators:\n * `args.paginationOpts` for function args, `result` for the page shape.\n * Pair with `include(...).paginate(opts)` (cvx-kit/auth). The result object\n * is deliberately non-strict — Convex may attach engine fields (splitCursor,\n * pageStatus); the page items themselves are validated against the dto.\n * Note: under row-level security, pages may be SHORTER than numItems when\n * rules reject rows mid-page; cursors remain correct.\n */\nexport function paginated<Dto extends z.ZodType>(dto: Dto) {\n\treturn {\n\t\targs: { paginationOpts: convexToZod(paginationOptsValidator) },\n\t\tresult: z.object({\n\t\t\tpage: z.array(dto),\n\t\t\tisDone: z.boolean(),\n\t\t\tcontinueCursor: z.string(),\n\t\t\tsplitCursor: z.string().nullable().optional(),\n\t\t\tpageStatus: z\n\t\t\t\t.enum(['SplitRecommended', 'SplitRequired'])\n\t\t\t\t.nullable()\n\t\t\t\t.optional(),\n\t\t}),\n\t}\n}\n\n/** Preserves discriminated-union storage while retaining the same table owner. */\nexport function zodVariantTable<Table extends string, Schema extends z.ZodType>(\n\ttableName: Table,\n\tstorage: Schema,\n) {\n\treturn {\n\t\ttableName,\n\t\tstorage,\n\t\tinsertSchema: storage,\n\t\ttable: defineTable(zodToConvex(storage)),\n\t}\n}\n\n/**\n * A zid that presents as a plain string in generated JSON schemas so\n * LLM-tool inputs stay primitive, while keeping the Id type at compile time.\n */\nexport function jsonSafeZid<Table extends string>(tableName: Table) {\n\treturn z\n\t\t.string()\n\t\t.describe(\n\t\t\t`Convex document id for table \"${tableName}\"`,\n\t\t) as unknown as z.ZodType<GenericId<Table>, string>\n}\n\ntype ZodObjectOmit<\n\tSchema extends z.ZodObject<any, any>,\n\tOmitMask extends z.util.Mask<keyof Schema['shape']>,\n> =\n\tSchema extends z.ZodObject<infer ObjectShape, infer Config>\n\t\t? z.ZodObject<\n\t\t\t\tz.util.Flatten<\n\t\t\t\t\tOmit<ObjectShape, Extract<keyof ObjectShape, keyof OmitMask>>\n\t\t\t\t>,\n\t\t\t\tConfig\n\t\t\t>\n\t\t: never\n\nfunction pickShape<\n\tFields extends Shape,\n\tconst Keys extends readonly ShapeKey<Fields>[],\n>(fields: Fields, keys: Keys) {\n\treturn Object.fromEntries(keys.map((key) => [key, fields[key]])) as Pick<\n\t\tFields,\n\t\tKeys[number]\n\t>\n}\n\nfunction omitShape<\n\tFields extends Shape,\n\tconst Keys extends readonly ShapeKey<Fields>[],\n>(fields: Fields, keys: Keys) {\n\tconst omitted = new Set<string>(keys)\n\treturn Object.fromEntries(\n\t\tObject.entries(fields).filter(([key]) => !omitted.has(key)),\n\t) as Omit<Fields, Keys[number]>\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"zod-table.mjs","names":["zid","zodToConvex","convexToZod"],"sources":["../src/zod-table.ts"],"sourcesContent":["import {\n\tconvexToZod,\n\tzid,\n\tzodToConvex,\n\ttype ConvexValidatorFromZod,\n} from 'convex-helpers/server/zod4'\nimport { defineTable, paginationOptsValidator, type TableDefinition } from 'convex/server'\nimport type { GenericId } from 'convex/values'\nimport { z } from 'zod'\n\n// Use this entry point when deriving validators from kit-owned schemas so\n// the producer and converter consult the same convex-helpers ID registry.\nexport { zid, zodToConvex, convexToZod } from 'convex-helpers/server/zod4'\n\ntype FieldSchemas = Record<string, z.ZodType>\ntype FieldKey<Fields extends FieldSchemas> = Extract<keyof Fields, string>\n\n/**\n * Opinionated lifecycle timestamps — every table has them, server-owned,\n * always excluded from insert/command boundaries. Maintained by the\n * `timestamps` trigger helper (see cvx-kit/triggers).\n */\nconst timestampFields = {\n\tcreatedAt: z.number().optional(),\n\tupdatedAt: z.number().optional(),\n\tarchivedAt: z.number().optional(),\n} satisfies FieldSchemas\n\ntype TimestampFields = typeof timestampFields\n\nexport const TIMESTAMP_FIELDS = ['createdAt', 'updatedAt', 'archivedAt'] as const\n\nexport type TableBoundaryOptions<\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n\tCommandFields extends readonly FieldKey<Fields>[],\n\tPublicFields extends readonly FieldKey<Fields & TimestampFields>[],\n> = {\n\tserverFields?: ServerFields\n\tcommandFields?: CommandFields\n\tpublicFields?: PublicFields\n}\n\ntype StrictObject<Fields extends FieldSchemas> = z.ZodObject<Fields, z.core.$strict>\ntype WriteFields<\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n> = Omit<Fields & TimestampFields, ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number]>\ntype PartialFields<Fields extends FieldSchemas> = {\n\t[Key in keyof Fields]: z.ZodOptional<Fields[Key]>\n}\n\n/** Named schema relationships keep generated declarations bounded and consumers fully typed. */\nexport type TableBoundary<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tServerFields extends readonly FieldKey<Fields>[],\n\tCommandFields extends readonly FieldKey<Fields>[],\n\tPublicFields extends readonly FieldKey<Fields & TimestampFields>[],\n> = {\n\ttableName: Table\n\tstorage: StrictObject<Fields & TimestampFields>\n\tschema: ReturnType<typeof documentSchema<Fields & TimestampFields, Table>>\n\tinsertSchema: StrictObject<WriteFields<Fields, ServerFields>>\n\tupdateSchema: StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>\n\tcommandInput: StrictObject<Pick<Fields & TimestampFields, CommandFields[number]>>\n\tpublicDto: StrictObject<Pick<Fields & TimestampFields, PublicFields[number]>>\n\ttoPublicDto: (\n\t\trow: z.input<StrictObject<Fields & TimestampFields>>,\n\t) => z.output<StrictObject<Pick<Fields & TimestampFields, PublicFields[number]>>>\n\ttable: TableDefinition<ConvexValidatorFromZod<StrictObject<Fields & TimestampFields>, 'required'>>\n\tinsert(): StrictObject<WriteFields<Fields, ServerFields>>\n\tinsert<Mask extends z.util.Mask<keyof WriteFields<Fields, ServerFields>>>(\n\t\tomit: Mask,\n\t): ZodObjectOmit<StrictObject<WriteFields<Fields, ServerFields>>, Mask>\n\tupdate(): StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>\n\tupdate<Mask extends z.util.Mask<keyof WriteFields<Fields, ServerFields>>>(\n\t\tomit: Mask,\n\t): ZodObjectOmit<StrictObject<PartialFields<WriteFields<Fields, ServerFields>>>, Mask>\n\ttools: {\n\t\tinsert: StrictObject<Pick<Fields & TimestampFields, CommandFields[number]>>\n\t\tupdate: StrictObject<{\n\t\t\tdata: StrictObject<PartialFields<Pick<Fields & TimestampFields, CommandFields[number]>>>\n\t\t\tid: z.ZodType<GenericId<Table>, string>\n\t\t}>\n\t\tid: StrictObject<{ id: z.ZodType<GenericId<Table>, string> }>\n\t}\n}\n\n/** Defines one table and derives its storage, document, write, and DTO boundaries. */\nexport function zodTable<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tconst ServerFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst CommandFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst PublicFields extends readonly FieldKey<Fields & TimestampFields>[] = readonly [],\n>(\n\ttableName: Table,\n\tfields: (id: typeof zid) => Fields,\n\toptions: TableBoundaryOptions<Fields, ServerFields, CommandFields, PublicFields> = {},\n): TableBoundary<Table, Fields, ServerFields, CommandFields, PublicFields> {\n\tconst columns: Fields & TimestampFields = { ...fields(zid), ...timestampFields }\n\tconst storage: z.ZodObject<Fields & TimestampFields, z.core.$strict> = z.object(columns).strict()\n\tconst schema = documentSchema(storage, tableName)\n\tconst omittedKeys: readonly (ServerFields[number] | (typeof TIMESTAMP_FIELDS)[number])[] = [\n\t\t...TIMESTAMP_FIELDS,\n\t\t...(options.serverFields ?? []),\n\t]\n\tconst commandKeys: readonly CommandFields[number][] = options.commandFields ?? []\n\tconst publicKeys: readonly PublicFields[number][] = options.publicFields ?? []\n\tconst insertSchema: z.ZodObject<WriteFields<Fields, ServerFields>, z.core.$strict> = z\n\t\t.object(omitFields(columns, omittedKeys))\n\t\t.strict()\n\tconst updateSchema = insertSchema.partial()\n\tconst commandInput: z.ZodObject<\n\t\tPick<Fields & TimestampFields, CommandFields[number]>,\n\t\tz.core.$strict\n\t> = z.object(pickFields(columns, commandKeys)).strict()\n\tconst publicDto: z.ZodObject<\n\t\tPick<Fields & TimestampFields, PublicFields[number]>,\n\t\tz.core.$strict\n\t> = z.object(pickFields(columns, publicKeys)).strict()\n\tconst toPublicDto = (row: z.input<typeof storage>) =>\n\t\tpublicDto.parse(\n\t\t\tObject.fromEntries(\n\t\t\t\tObject.keys(publicDto.shape).map((key) => {\n\t\t\t\t\t// SAFETY: publicDto contains only keys selected from the storage fields.\n\t\t\t\t\treturn [key, row[key as keyof typeof row]]\n\t\t\t\t}),\n\t\t\t),\n\t\t)\n\n\ttype InsertSchema = typeof insertSchema\n\ttype UpdateSchema = typeof updateSchema\n\ttype InsertMask = z.util.Mask<keyof InsertSchema['shape']>\n\ttype UpdateMask = z.util.Mask<keyof UpdateSchema['shape']>\n\n\tfunction insert(): InsertSchema\n\tfunction insert<OmitMask extends InsertMask>(\n\t\tomit: OmitMask,\n\t): ZodObjectOmit<InsertSchema, OmitMask>\n\tfunction insert(omit?: InsertMask) {\n\t\t// SAFETY: InsertMask contains only insertSchema keys; the overload retains Zod's exact mask relationship.\n\t\treturn omit ? insertSchema.omit(omit as never) : insertSchema\n\t}\n\n\tfunction update(): UpdateSchema\n\tfunction update<OmitMask extends UpdateMask>(\n\t\tomit: OmitMask,\n\t): ZodObjectOmit<UpdateSchema, OmitMask>\n\tfunction update(omit?: UpdateMask) {\n\t\t// SAFETY: UpdateMask contains only updateSchema keys; the overload retains Zod's exact mask relationship.\n\t\treturn omit ? updateSchema.omit(omit as never) : updateSchema\n\t}\n\n\tconst table: TableDefinition<ConvexValidatorFromZod<typeof storage, 'required'>> = defineTable(\n\t\tzodToConvex(storage),\n\t)\n\treturn {\n\t\ttableName,\n\t\tschema,\n\t\tstorage,\n\t\tinsertSchema,\n\t\tupdateSchema,\n\t\tcommandInput,\n\t\tpublicDto,\n\t\ttoPublicDto,\n\t\ttable,\n\t\tinsert,\n\t\tupdate,\n\t\ttools: {\n\t\t\t// jsonSafeZid (not zid): tool masks feed generated JSON schemas, so\n\t\t\t// ids must present as plain strings while keeping the Id<...> type.\n\t\t\tinsert: commandInput,\n\t\t\tupdate: z.object({ data: commandInput.partial(), id: jsonSafeZid(tableName) }).strict(),\n\t\t\tid: z.object({ id: jsonSafeZid(tableName) }).strict(),\n\t\t},\n\t}\n}\n\nconst tenantFields = {\n\ttenant: z.string().min(1),\n} satisfies FieldSchemas\n\ntype TenantFields = typeof tenantFields\n\n/**\n * A zodTable whose rows are tenant-owned: injects the server-owned `tenant`\n * field (the tenancy boundary — see cvx-kit/tenancy). Like timestamps, it is\n * excluded from insert/update/command boundaries; handlers stamp it from\n * ctx.tenant, row-level security matches on it, and the tenantOwnership\n * trigger forbids reassigning it. Expose it via publicFields only when a DTO\n * genuinely needs it.\n */\nexport function tenantTable<\n\tTable extends string,\n\tFields extends FieldSchemas,\n\tconst ServerFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst CommandFields extends readonly FieldKey<Fields>[] = readonly [],\n\tconst PublicFields extends readonly FieldKey<Fields & TenantFields & TimestampFields>[] =\n\t\treadonly [],\n>(\n\ttableName: Table,\n\tfields: (id: typeof zid) => Fields,\n\toptions: TableBoundaryOptions<\n\t\tFields & TenantFields,\n\t\treadonly (ServerFields[number] | 'tenant')[],\n\t\tCommandFields,\n\t\tPublicFields\n\t> = {},\n) {\n\treturn zodTable<\n\t\tTable,\n\t\tFields & TenantFields,\n\t\treadonly (ServerFields[number] | 'tenant')[],\n\t\tCommandFields,\n\t\tPublicFields\n\t>(tableName, (id) => ({ ...fields(id), ...tenantFields }), {\n\t\t...options,\n\t\tserverFields: [...(options.serverFields ?? []), 'tenant'],\n\t})\n}\n\n/**\n * Merges per-module table maps into the application schema map, rejecting\n * duplicate table names across modules — the module-registry combinator for\n * domain/table.ts: `defineSchema(createModule(catalogTables, salesTables))`.\n */\nexport function createModule<const Maps extends readonly Record<string, TableDefinition<any>>[]>(\n\t...maps: Maps\n): UnionToIntersection<Maps[number]> {\n\tconst combined: Record<string, TableDefinition<any>> = {}\n\tfor (const map of maps) {\n\t\tfor (const [tableName, definition] of Object.entries(map)) {\n\t\t\tif (tableName in combined) {\n\t\t\t\tthrow new Error(`Table \"${tableName}\" is declared by more than one module`)\n\t\t\t}\n\t\t\tcombined[tableName] = definition\n\t\t}\n\t}\n\t// SAFETY: every input entry is copied, and duplicate keys are rejected above.\n\treturn combined as UnionToIntersection<Maps[number]>\n}\n\ntype UnionToIntersection<Union> = (\n\tUnion extends unknown ? (member: Union) => void : never\n) extends (member: infer Intersection) => void\n\t? Intersection\n\t: never\n\n/**\n * Cursor-pagination boundary schemas bridged from Convex's own validators:\n * `args.paginationOpts` for function args, `result` for the page shape.\n * Pair with `include(...).paginate(opts)` (cvx-kit/auth). The result object\n * is deliberately non-strict — Convex may attach engine fields (splitCursor,\n * pageStatus); the page items themselves are validated against the dto.\n * Note: under row-level security, pages may be SHORTER than numItems when\n * rules reject rows mid-page; cursors remain correct.\n */\nexport function paginated<Dto extends z.ZodType>(dto: Dto) {\n\treturn {\n\t\targs: { paginationOpts: convexToZod(paginationOptsValidator) },\n\t\tresult: z.object({\n\t\t\tpage: z.array(dto),\n\t\t\tisDone: z.boolean(),\n\t\t\tcontinueCursor: z.string(),\n\t\t\tsplitCursor: z.string().nullable().optional(),\n\t\t\tpageStatus: z.enum(['SplitRecommended', 'SplitRequired']).nullable().optional(),\n\t\t}),\n\t}\n}\n\n/** Preserves discriminated-union storage while retaining the same table owner. */\nexport function zodVariantTable<Table extends string, Schema extends z.ZodType>(\n\ttableName: Table,\n\tstorage: Schema,\n) {\n\treturn {\n\t\ttableName,\n\t\tstorage,\n\t\tinsertSchema: storage,\n\t\ttable: defineTable(zodToConvex(storage)),\n\t}\n}\n\n/**\n * A zid that presents as a plain string in generated JSON schemas so\n * LLM-tool inputs stay primitive, while keeping the Id type at compile time.\n */\nexport function jsonSafeZid<Table extends string>(\n\ttableName: Table,\n): z.ZodType<GenericId<Table>, string> {\n\tconst id = zid(tableName)\n\treturn z\n\t\t.string()\n\t\t.refine((value): value is GenericId<Table> => id.safeParse(value).success)\n\t\t.describe(`Convex document id for table \"${tableName}\"; validated against the table by Convex`)\n}\n\ntype ZodObjectOmit<\n\tSchema extends z.ZodObject<any, any>,\n\tOmitMask extends z.util.Mask<keyof Schema['shape']>,\n> =\n\tSchema extends z.ZodObject<infer ObjectFields, infer Config>\n\t\t? z.ZodObject<\n\t\t\t\tz.util.Flatten<Omit<ObjectFields, Extract<keyof ObjectFields, keyof OmitMask>>>,\n\t\t\t\tConfig\n\t\t\t>\n\t\t: never\n\nfunction documentSchema<Fields extends FieldSchemas, Table extends string>(\n\tstorage: StrictObject<Fields>,\n\ttableName: Table,\n) {\n\treturn storage.extend({ _id: zid(tableName), _creationTime: z.number() })\n}\n\nfunction pickFields<Fields extends FieldSchemas, const Keys extends readonly FieldKey<Fields>[]>(\n\tfields: Fields,\n\tkeys: Keys,\n) {\n\t// SAFETY: each entry uses a requested key and its original validator, preserving Pick.\n\treturn Object.fromEntries(keys.map((key) => [key, fields[key]])) as Pick<Fields, Keys[number]>\n}\n\nfunction omitFields<Fields extends FieldSchemas, const Keys extends readonly FieldKey<Fields>[]>(\n\tfields: Fields,\n\tkeys: Keys,\n) {\n\tconst omitted = new Set<string>(keys)\n\t// SAFETY: filtering removes exactly the supplied keys and keeps every remaining validator.\n\treturn Object.fromEntries(Object.entries(fields).filter(([key]) => !omitted.has(key))) as Omit<\n\t\tFields,\n\t\tKeys[number]\n\t>\n}\n"],"mappings":"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"}
|
package/docs/approvals.md
CHANGED
|
@@ -45,7 +45,7 @@ export const approvals = new Approvals(components.approvals)
|
|
|
45
45
|
|
|
46
46
|
## Defining a workflow — in the owning domain
|
|
47
47
|
|
|
48
|
-
Workflow
|
|
48
|
+
Workflow _definitions_ are business logic and live in the domain that owns the
|
|
49
49
|
resource, not in the root facade:
|
|
50
50
|
|
|
51
51
|
```ts
|
|
@@ -54,38 +54,38 @@ import { approvals } from '../../approvals'
|
|
|
54
54
|
import { internal } from '../../_generated/api'
|
|
55
55
|
|
|
56
56
|
export const publishApproval = approvals.define({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
name: 'documentPublish',
|
|
58
|
+
steps: [
|
|
59
|
+
approvals.decision('managerDecision', {
|
|
60
|
+
decisions: ['approved', 'rejected'],
|
|
61
|
+
quorum: { kind: 'count', approvals: 1 },
|
|
62
|
+
makerChecker: true, // requester cannot decide
|
|
63
|
+
expiresAfterMs: 7 * 24 * 60 * 60 * 1000,
|
|
64
|
+
}),
|
|
65
|
+
approvals.branch('applyDecision', {
|
|
66
|
+
approvedStepKey: 'publishDocument',
|
|
67
|
+
rejectedStepKey: 'notifyRejected',
|
|
68
|
+
}),
|
|
69
|
+
approvals.mutation('publishDocument', {
|
|
70
|
+
handler: internal.domain.documents.approval_functions.applyDecision,
|
|
71
|
+
}),
|
|
72
|
+
approvals.notify('notifyRejected', {
|
|
73
|
+
handler: internal.domain.documents.approval_functions.notifyRejected,
|
|
74
|
+
retry: true,
|
|
75
|
+
}),
|
|
76
|
+
],
|
|
77
77
|
})
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
### Step kinds
|
|
81
81
|
|
|
82
|
-
| Step
|
|
83
|
-
|
|
84
|
-
| `decision(key, { decisions, quorum, makerChecker?, expiresAfterMs? })` | waits for humans
|
|
85
|
-
| `branch(key, { approvedStepKey, rejectedStepKey })`
|
|
86
|
-
| `mutation(key, { handler, retry? })`
|
|
87
|
-
| `action(key, { handler, retry? })`
|
|
88
|
-
| `notify(key, { handler, retry? })`
|
|
82
|
+
| Step | Runs | Notes |
|
|
83
|
+
| ---------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------------------- |
|
|
84
|
+
| `decision(key, { decisions, quorum, makerChecker?, expiresAfterMs? })` | waits for humans | quorum is `{ kind: 'count', approvals: n }`; expiry resolves the run as `expired` |
|
|
85
|
+
| `branch(key, { approvedStepKey, rejectedStepKey })` | routing | jumps by the preceding decision's outcome |
|
|
86
|
+
| `mutation(key, { handler, retry? })` | internal mutation handle | transactional apply step |
|
|
87
|
+
| `action(key, { handler, retry? })` | internal action handle | side effects (third parties) |
|
|
88
|
+
| `notify(key, { handler, retry? })` | internal action handle | semantically a notification; same contract as action |
|
|
89
89
|
|
|
90
90
|
Limits (enforced by the component's validators): ≤ 32 steps per workflow,
|
|
91
91
|
≤ 8 decisions per decision step, names ≤ 96 chars, references ≤ 256 chars,
|
|
@@ -103,23 +103,23 @@ workflow's steps in a breaking way, change the key.
|
|
|
103
103
|
```ts
|
|
104
104
|
// start — from an authMutation, with ctx.actor as the requester
|
|
105
105
|
const { runId } = await publishApproval.start(ctx, {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
scopeRef: ctx.org.organizationId, // opaque tenancy
|
|
107
|
+
resourceType: 'document',
|
|
108
|
+
resourceRef: documentId, // opaque to the component
|
|
109
|
+
requester: ctx.actor,
|
|
110
|
+
metadata: { title }, // bounded string map
|
|
111
111
|
})
|
|
112
112
|
|
|
113
113
|
// decide — a manager approves or rejects
|
|
114
114
|
await publishApproval.decide(ctx, { runId, decision: 'approved', reason }, ctx.actor)
|
|
115
115
|
|
|
116
116
|
// observe from a host query
|
|
117
|
-
await publishApproval.status(ctx, runId)
|
|
117
|
+
await publishApproval.status(ctx, runId) // pending | approved | rejected | expired | canceled
|
|
118
118
|
await publishApproval.evidence(ctx, runId) // per-step decision evidence
|
|
119
119
|
await publishApproval.list(ctx, {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
scopeRef,
|
|
121
|
+
state: 'pending',
|
|
122
|
+
paginationOpts: { cursor: null, numItems: 20 },
|
|
123
123
|
})
|
|
124
124
|
|
|
125
125
|
// manage
|
|
@@ -140,16 +140,16 @@ trust:
|
|
|
140
140
|
import { approvalCallbackArgs } from 'cvx-kit/components/approvals'
|
|
141
141
|
|
|
142
142
|
export const applyDecision = systemMutation({
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
143
|
+
args: approvalCallbackArgs,
|
|
144
|
+
handler: async (ctx, input) => {
|
|
145
|
+
if (input.resourceType !== 'document') throw new Error('WRONG_RESOURCE')
|
|
146
|
+
const id = ctx.db.normalizeId('documents', input.resourceRef)
|
|
147
|
+
if (!id) throw new Error('BAD_REF')
|
|
148
|
+
const document = await ctx.db.get(id)
|
|
149
|
+
// guard against stale runs: the doc must still point at this run
|
|
150
|
+
if (document?.approvalRunId !== input.runId) return
|
|
151
|
+
await ctx.db.patch(id, { reviewState: 'published' })
|
|
152
|
+
},
|
|
153
153
|
})
|
|
154
154
|
```
|
|
155
155
|
|
|
@@ -168,14 +168,15 @@ available inside components:
|
|
|
168
168
|
|
|
169
169
|
```ts
|
|
170
170
|
export const approvalInbox = authQuery({
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
171
|
+
args: {
|
|
172
|
+
state: z.enum(['pending', 'approved', 'rejected', 'expired', 'canceled']).optional(),
|
|
173
|
+
paginationOpts: convexToZod(paginationOptsValidator),
|
|
174
|
+
},
|
|
175
|
+
handler: (ctx, args) =>
|
|
176
|
+
publishApproval.list(ctx, {
|
|
177
|
+
scopeRef: ctx.org.organizationId,
|
|
178
|
+
...args,
|
|
179
|
+
}),
|
|
179
180
|
})
|
|
180
181
|
```
|
|
181
182
|
|
|
@@ -187,7 +188,7 @@ scope/state query.
|
|
|
187
188
|
## Rules and footguns
|
|
188
189
|
|
|
189
190
|
1. Root facade (`convex/approvals.ts`) instantiates the client; workflow
|
|
190
|
-
|
|
191
|
+
_definitions_ live in the owning domain.
|
|
191
192
|
2. Handlers get opaque strings — always `normalizeId`, re-check
|
|
192
193
|
`resourceType`, and verify the resource still references the run.
|
|
193
194
|
3. `makerChecker: true` means the requester's decisions are rejected — design
|
package/docs/architecture.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
cvx-kit was extracted from a production Convex application (~12k LOC backend).
|
|
4
4
|
This document describes the architecture that application follows and that the
|
|
5
5
|
kit is designed to reproduce: strict ownership boundaries, injected policy,
|
|
6
|
-
portable components, and conventions that are
|
|
6
|
+
portable components, and conventions that are _executable_ (enforced by tests)
|
|
7
7
|
rather than aspirational.
|
|
8
8
|
|
|
9
9
|
## The layers
|