cvx-kit 0.1.1 → 0.1.3
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 +31 -0
- package/README.md +3 -0
- package/dist/auth.d.mts +16 -0
- package/dist/auth.mjs +1 -1
- package/dist/auth.mjs.map +1 -1
- package/dist/components/approvals/workflow.d.mts +1 -1
- package/dist/components/foundation/client.d.mts +14 -8
- 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 +14 -5
- 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/operation.d.mts +74 -0
- package/dist/components/foundation/operation.mjs +2 -0
- package/dist/components/foundation/operation.mjs.map +1 -0
- package/dist/zod-table.d.mts +4 -4
- package/dist/zod-table.mjs +1 -1
- package/dist/zod-table.mjs.map +1 -1
- package/docs/auth.md +49 -0
- package/docs/commands.md +130 -15
- package/docs/llms.md +1 -0
- package/docs/upgrading.md +42 -0
- package/docs/zod-table.md +22 -0
- package/package.json +13 -11
- package/scripts/extract-changelog.mjs +1 -1
- package/scripts/smoke-packed-components.mjs +25 -24
- package/scripts/test-packed-ids.mjs +157 -0
- package/src/test.ts +2 -1
- package/docs/plans/2026-08-14-001-feat-v0.1.0-feature-set-plan.md +0 -461
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation.mjs","names":[],"sources":["../../../src/components/foundation/operation.ts"],"sourcesContent":["import type {\n\tAuditEntryInput,\n\tAuditedRegistry,\n\tAuditedOperation,\n\tAnyCommandMiddleware,\n} from './client'\nimport type {\n\tCommandInput,\n\tCommandHandlerResult,\n\tParseable,\n\tSchemaInput,\n} from './modules/command/command'\n\ntype MaybePromise<T> = T | Promise<T>\nexport type CommandPreparation<Result> =\n\t| { kind: 'replay'; result: unknown }\n\t| { kind: 'execute'; complete?: (result: Result) => MaybePromise<void> }\ntype Parsed<S extends Parseable<unknown>> = ReturnType<S['parse']>\ndeclare const extension: unique symbol\ndeclare const hostContext: unique symbol\nexport type OperationHostContext<Context> = {\n\treadonly [hostContext]?: (context: Context) => void\n}\nexport type OperationExtension<Definition> = Definition extends {\n\treadonly [extension]?: infer Extension\n}\n\t? Extension\n\t: unknown\n\ntype MiddlewareInput<Context, Input, Output, Extension> = {\n\toperation: string\n\tcontext: Context\n\tcommand: Input\n\tnext: keyof Extension extends never\n\t\t? (options?: { context?: Extension }) => Promise<Output>\n\t\t: (options: { context: Extension }) => Promise<Output>\n}\ntype LocalMiddleware<C, I, O, E> = (\n\tinput: MiddlewareInput<C, I, O, E>,\n) => Promise<O>\ntype AuditFor<Aggregate extends string> = Omit<\n\tAuditEntryInput,\n\t'classification' | 'aggregate'\n> & {\n\taggregate: { type: Aggregate; id: string }\n}\nexport type TypedOperation<\n\tContext,\n\tInput extends Parseable<unknown>,\n\tOutput extends Parseable<unknown>,\n\tAggregates extends readonly string[],\n\tExtension,\n> = OperationHostContext<Context> & {\n\treadonly command: Input\n\treadonly result: Output\n\treadonly classification: string\n\treadonly permission?: string\n\t/** Called after permission, inside observation; returns invocation-local hooks. */\n\treadonly prepare?: (\n\t\tcontext: Context,\n\t\tcommand: Parsed<Input>,\n\t) => MaybePromise<CommandPreparation<Parsed<Output>>>\n\t/** Validates stored outputs when result transforms accept a different input shape. */\n\treadonly replayResult?: Parseable<Parsed<Output>>\n\treadonly aggregates?: Aggregates\n\treadonly guard?: (\n\t\tcontext: Context & Extension,\n\t\tcommand: Parsed<Input>,\n\t) => MaybePromise<void>\n\treadonly audit: (\n\t\tresolution: { command: Parsed<Input>; result: Parsed<Output> },\n\t\tcontext: Context,\n\t) => MaybePromise<AuditFor<Aggregates[number]> | null>\n\treadonly [extension]?: Extension\n} & (keyof Extension extends never\n\t\t? {\n\t\t\t\treadonly middleware?: readonly LocalMiddleware<\n\t\t\t\t\tContext,\n\t\t\t\t\tParsed<Input>,\n\t\t\t\t\tSchemaInput<Output>,\n\t\t\t\t\tExtension\n\t\t\t\t>[]\n\t\t\t}\n\t\t: {\n\t\t\t\treadonly middleware: readonly [\n\t\t\t\t\tLocalMiddleware<\n\t\t\t\t\t\tContext,\n\t\t\t\t\t\tParsed<Input>,\n\t\t\t\t\t\tSchemaInput<Output>,\n\t\t\t\t\t\tExtension\n\t\t\t\t\t>,\n\t\t\t\t\t...LocalMiddleware<\n\t\t\t\t\t\tContext & Extension,\n\t\t\t\t\t\tParsed<Input>,\n\t\t\t\t\t\tSchemaInput<Output>,\n\t\t\t\t\t\tRecord<never, never>\n\t\t\t\t\t>[],\n\t\t\t\t]\n\t\t\t})\n\ntype RegistryInput<Context, Operations extends AuditedRegistry> = {\n\t[Key in Extract<keyof Operations, string>]: {\n\t\toperation: Key\n\t\tdefinition: Operations[Key]\n\t\tcontext: Context\n\t\tcommand: CommandInput<Operations, Key>\n\t\tnext: () => Promise<CommandHandlerResult<Operations, Key>>\n\t}\n}[Extract<keyof Operations, string>]\n\ntype StoredOperation<\n\tContext,\n\tInput extends Parseable<unknown>,\n\tOutput extends Parseable<unknown>,\n\tAggregates extends readonly string[],\n\tExtension,\n> = Omit<\n\tTypedOperation<Context, Input, Output, Aggregates, Extension>,\n\t'middleware'\n> &\n\tPick<AuditedOperation, 'middleware'>\n\n/** Binds host context once; each operation infers its own parsed schemas. */\nexport function operationFactory<\n\tContext,\n\tExtension extends Record<string, unknown> = Record<never, never>,\n>() {\n\treturn {\n\t\toperation<\n\t\t\tconst Input extends Parseable<unknown>,\n\t\t\tconst Output extends Parseable<unknown>,\n\t\t\tconst Aggregates extends readonly string[] = readonly string[],\n\t\t>(\n\t\t\tdefinition: TypedOperation<\n\t\t\t\tContext,\n\t\t\t\tInput,\n\t\t\t\tOutput,\n\t\t\t\tAggregates,\n\t\t\t\tExtension\n\t\t\t>,\n\t\t): StoredOperation<Context, Input, Output, Aggregates, Extension> {\n\t\t\t// The kernel selects this definition and parses its input before\n\t\t\t// invoking the heterogeneous middleware registry. Erase only that\n\t\t\t// storage seam; callbacks were checked against these schemas above.\n\t\t\treturn definition as StoredOperation<\n\t\t\t\tContext,\n\t\t\t\tInput,\n\t\t\t\tOutput,\n\t\t\t\tAggregates,\n\t\t\t\tExtension\n\t\t\t>\n\t\t},\n\t\tregistryMiddleware<const Operations extends AuditedRegistry>(\n\t\t\toperations: Operations & { readonly [Key in keyof Operations]: OperationHostContext<Context> },\n\t\t\tmiddleware: (\n\t\t\t\tinput: RegistryInput<Context, Operations>,\n\t\t\t) => Promise<unknown>,\n\t\t): AnyCommandMiddleware {\n\t\t\tfunction ownsDefinition(\n\t\t\t\tinput: Parameters<AnyCommandMiddleware>[0],\n\t\t\t): input is Parameters<AnyCommandMiddleware>[0] &\n\t\t\t\tRegistryInput<Context, Operations> {\n\t\t\t\treturn (\n\t\t\t\t\tObject.prototype.hasOwnProperty.call(\n\t\t\t\t\t\toperations,\n\t\t\t\t\t\tinput.operation,\n\t\t\t\t\t) && operations[input.operation] === input.definition\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn (input) => {\n\t\t\t\tif (!ownsDefinition(input)) {\n\t\t\t\t\tthrow new CommandMiddlewareRegistryError(input.operation)\n\t\t\t\t}\n\t\t\t\treturn middleware(input)\n\t\t\t}\n\t\t},\n\t}\n}\n\nclass CommandMiddlewareRegistryError extends Error {\n\treadonly code = 'COMMAND_MIDDLEWARE_REGISTRY_MISMATCH'\n\tconstructor(operation: string) {\n\t\tsuper(\n\t\t\t`Typed middleware does not own the definition of operation \"${operation}\"`,\n\t\t)\n\t\tthis.name = 'CommandMiddlewareRegistryError'\n\t}\n}\n"],"mappings":"AA2HA,SAAgB,GAGZ,CACH,MAAO,CACN,UAKC,EAOiE,CAIjE,OAAO,CAOR,EACA,mBACC,EACA,EAGuB,CACvB,SAAS,EACR,EAEmC,CACnC,OACC,OAAO,UAAU,eAAe,KAC/B,EACA,EAAM,SACP,GAAK,EAAW,EAAM,aAAe,EAAM,UAE7C,CACA,MAAQ,IAAU,CACjB,GAAI,CAAC,EAAe,CAAK,EACxB,MAAM,IAAI,EAA+B,EAAM,SAAS,EAEzD,OAAO,EAAW,CAAK,CACxB,CACD,CACD,CACD,CAEA,IAAM,EAAN,cAA6C,KAAM,CAClD,KAAgB,uCAChB,YAAY,EAAmB,CAC9B,MACC,8DAA8D,EAAU,EACzE,EACA,KAAK,KAAO,gCACb,CACD"}
|
package/dist/zod-table.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
@@ -23,7 +23,7 @@ type TableBoundaryOptions<Fields extends Shape, ServerFields extends readonly Sh
|
|
|
23
23
|
publicFields?: PublicFields;
|
|
24
24
|
};
|
|
25
25
|
/** Defines one table and derives its storage, document, write, and DTO boundaries. */
|
|
26
|
-
declare function zodTable<Table extends string, Fields extends Shape, const ServerFields extends readonly ShapeKey<Fields>[] = readonly [], const CommandFields extends readonly ShapeKey<Fields>[] = readonly [], const PublicFields extends readonly ShapeKey<Fields & TimestampShape>[] = readonly []>(tableName: Table, fields: (id: typeof zid) => Fields, options?: TableBoundaryOptions<Fields, ServerFields, CommandFields, PublicFields>): {
|
|
26
|
+
declare function zodTable<Table extends string, Fields extends Shape, const ServerFields extends readonly ShapeKey<Fields>[] = readonly [], const CommandFields extends readonly ShapeKey<Fields>[] = readonly [], const PublicFields extends readonly ShapeKey<Fields & TimestampShape>[] = readonly []>(tableName: Table, fields: (id: typeof zid$1) => Fields, options?: TableBoundaryOptions<Fields, ServerFields, CommandFields, PublicFields>): {
|
|
27
27
|
tableName: Table;
|
|
28
28
|
schema: z.ZodObject<((keyof Fields & "_creationTime") | (keyof Fields & "_id") extends never ? (Fields & {
|
|
29
29
|
createdAt: z.ZodOptional<z.ZodNumber>;
|
|
@@ -118,7 +118,7 @@ type TenantShape = typeof tenantShape;
|
|
|
118
118
|
* trigger forbids reassigning it. Expose it via publicFields only when a DTO
|
|
119
119
|
* genuinely needs it.
|
|
120
120
|
*/
|
|
121
|
-
declare function tenantTable<Table extends string, Fields extends Shape, const ServerFields extends readonly ShapeKey<Fields>[] = readonly [], const CommandFields extends readonly ShapeKey<Fields>[] = readonly [], const PublicFields extends readonly ShapeKey<Fields & TenantShape & TimestampShape>[] = readonly []>(tableName: Table, fields: (id: typeof zid) => Fields, options?: TableBoundaryOptions<Fields & TenantShape, readonly (ServerFields[number] | 'tenant')[], CommandFields, PublicFields>): ReturnType<typeof zodTable<Table, Fields & TenantShape, readonly (ServerFields[number] | 'tenant')[], CommandFields, PublicFields>>;
|
|
121
|
+
declare function tenantTable<Table extends string, Fields extends Shape, const ServerFields extends readonly ShapeKey<Fields>[] = readonly [], const CommandFields extends readonly ShapeKey<Fields>[] = readonly [], const PublicFields extends readonly ShapeKey<Fields & TenantShape & TimestampShape>[] = readonly []>(tableName: Table, fields: (id: typeof zid$1) => Fields, options?: TableBoundaryOptions<Fields & TenantShape, readonly (ServerFields[number] | 'tenant')[], CommandFields, PublicFields>): ReturnType<typeof zodTable<Table, Fields & TenantShape, readonly (ServerFields[number] | 'tenant')[], CommandFields, PublicFields>>;
|
|
122
122
|
/**
|
|
123
123
|
* Merges per-module table maps into the application schema map, rejecting
|
|
124
124
|
* duplicate table names across modules — the module-registry combinator for
|
|
@@ -171,5 +171,5 @@ declare function zodVariantTable<Table extends string, Schema extends z.ZodType>
|
|
|
171
171
|
declare function jsonSafeZid<Table extends string>(tableName: Table): z.ZodType<GenericId<Table>, string>;
|
|
172
172
|
type ZodObjectOmit<Schema extends z.ZodObject<any, any>, OmitMask extends z.util.Mask<keyof Schema['shape']>> = Schema extends z.ZodObject<infer ObjectShape, infer Config> ? z.ZodObject<z.util.Flatten<Omit<ObjectShape, Extract<keyof ObjectShape, keyof OmitMask>>>, Config> : never;
|
|
173
173
|
//#endregion
|
|
174
|
-
export { TIMESTAMP_FIELDS, TableBoundaryOptions, createModule, jsonSafeZid, paginated, tenantTable, zodTable, zodVariantTable };
|
|
174
|
+
export { TIMESTAMP_FIELDS, TableBoundaryOptions, convexToZod, createModule, jsonSafeZid, paginated, tenantTable, zid, zodTable, zodToConvex, zodVariantTable };
|
|
175
175
|
//# 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=s.extend({_id:r(e),_creationTime:c.number()}),f=c.object(y(i,[...u,...n.serverFields??[]])).strict(),p=f.partial(),m=c.object(v(i,n.commandFields??[])).strict(),h=c.object(v(i,n.publicFields??[])).strict(),g=e=>h.parse(Object.fromEntries(Object.keys(h.shape).map(t=>[t,e[t]])));function b(e){return e?f.omit(e):f}function x(e){return e?p.omit(e):p}return{tableName:e,schema:d,storage:s,insertSchema:f,updateSchema:p,commandInput:m,publicDto:h,toPublicDto:g,table:o(a(s)),insert:b,update:x,tools:{insert:m,update:c.object({data:m.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){return c.string().describe(`Convex document id for table "${e}"`)}function v(e,t){return Object.fromEntries(t.map(t=>[t,e[t]]))}function y(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\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\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 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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"}
|
package/docs/auth.md
CHANGED
|
@@ -97,6 +97,55 @@ role/organization from that check replaces the JWT's. Verification **fails
|
|
|
97
97
|
closed**: any error or org mismatch throws `FORBIDDEN`. Omit
|
|
98
98
|
`verifyMembership` to let actions trust the JWT like queries do.
|
|
99
99
|
|
|
100
|
+
## Resolving org/role from the database (`resolveOrganization`)
|
|
101
|
+
|
|
102
|
+
Some identity providers issue tokens that carry **no org claims** — custom
|
|
103
|
+
credentials, magic codes, or apps that keep memberships in their own tables.
|
|
104
|
+
For those, configure `resolveOrganization` to derive organization authority
|
|
105
|
+
from the app's own data instead of the JWT.
|
|
106
|
+
|
|
107
|
+
When configured, the hook is the organization authority — claim parsing is
|
|
108
|
+
skipped entirely. Returning `null` **or throwing** rejects with `FORBIDDEN`
|
|
109
|
+
(fail closed); a missing identity or user is still `UNAUTHENTICATED`; the
|
|
110
|
+
returned `roleSlug` still passes through `mapRole` like a claim would.
|
|
111
|
+
|
|
112
|
+
The handler must be **action-safe**: in actions the ctx has no `db`, so branch
|
|
113
|
+
on its presence and fall back to an internal query:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { internal } from './_generated/api'
|
|
117
|
+
|
|
118
|
+
resolveOrganization: async ({ ctx, user }) => {
|
|
119
|
+
const membership =
|
|
120
|
+
'db' in ctx
|
|
121
|
+
? await ctx.db
|
|
122
|
+
.query('companyUsers')
|
|
123
|
+
.withIndex('by_user', (q) => q.eq('userId', user.id))
|
|
124
|
+
.first()
|
|
125
|
+
: await ctx.runQuery(internal.memberships.byUser, { userId: user.id })
|
|
126
|
+
if (!membership || !membership.active) return null // revoked ⇒ FORBIDDEN
|
|
127
|
+
return { organizationId: membership.orgId, roleSlug: membership.roleSlug }
|
|
128
|
+
},
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Resolver obligations:
|
|
132
|
+
|
|
133
|
+
- Bind the lookup to the **verified** user — `user.id` (or
|
|
134
|
+
`identity.subject`), never a caller-influenced or non-unique attribute.
|
|
135
|
+
- Resolve revoked/inactive/expired memberships to `null`, so revocation
|
|
136
|
+
actually takes effect fail-closed.
|
|
137
|
+
|
|
138
|
+
Composition: `mapRole` still applies to the returned slug; with tenancy
|
|
139
|
+
configured, `ctx.tenant` derives from the hook's org
|
|
140
|
+
(`security.tenancy.resolve` is unchanged); `verifyMembership` for actions is
|
|
141
|
+
unchanged and receives the hook-resolved `organizationId` — in actions its
|
|
142
|
+
live result supersedes the hook's role/org, exactly as it supersedes claims.
|
|
143
|
+
|
|
144
|
+
Cost: one DB read per authenticated call (claims are free); actions with
|
|
145
|
+
`verifyMembership` configured also pay the live check. When debugging, note
|
|
146
|
+
that a misconfigured hook surfaces as blanket `FORBIDDEN` — errors are
|
|
147
|
+
swallowed fail-closed, never rethrown.
|
|
148
|
+
|
|
100
149
|
## Mutations are structurally trigger-wrapped
|
|
101
150
|
|
|
102
151
|
When `triggers` (or `wrapDB`) is configured, every `authMutation`,
|
package/docs/commands.md
CHANGED
|
@@ -51,9 +51,13 @@ operation** — auditing is type-enforced, not opt-in.
|
|
|
51
51
|
import { z } from 'zod'
|
|
52
52
|
import { Command } from '../../foundation'
|
|
53
53
|
import { documents } from './schema'
|
|
54
|
+
import { zid } from 'cvx-kit/zod-table'
|
|
55
|
+
import type { MutationCtx } from '../../_generated/server'
|
|
56
|
+
|
|
57
|
+
const typed = Command.withContext<MutationCtx>()
|
|
54
58
|
|
|
55
59
|
const operations = {
|
|
56
|
-
'documents.rename':
|
|
60
|
+
'documents.rename': typed.operation({
|
|
57
61
|
command: documents.commandInput.extend({ id: zid('documents') }),
|
|
58
62
|
result: z.object({ ok: z.literal(true) }).strict(),
|
|
59
63
|
classification: 'business',
|
|
@@ -89,14 +93,20 @@ The public API layer then wraps the executor in an `authMutation` and passes
|
|
|
89
93
|
Foundation's injected `checkPermission(ctx, { permission, operation })`
|
|
90
94
|
runs; throw to deny. Declaring a permission with no injected checker
|
|
91
95
|
**fails closed** (`COMMAND_PERMISSION_NOT_CONFIGURED`).
|
|
92
|
-
4. **
|
|
96
|
+
4. **Prepare**, when configured: obtain an invocation-local replay decision
|
|
97
|
+
or completion closure. Replay validates its durable result, then goes
|
|
98
|
+
directly to observation, skipping middleware, both guards, handler and audit.
|
|
99
|
+
5. **Middleware and guards** — registry middleware wraps operation middleware,
|
|
100
|
+
which wraps both guards and the handler. The default guard is the
|
|
101
|
+
registry-wide guard passed as the Command's second
|
|
93
102
|
argument (`new Command(operations, { guard })`), if any.
|
|
94
|
-
|
|
103
|
+
Then the operation's own `guard(ctx, command)` checks
|
|
95
104
|
preconditions like state-machine legality, ownership beyond roles, or
|
|
96
105
|
invariants over the parsed command. Throw to deny — nothing has run yet,
|
|
97
106
|
so a denial is always clean.
|
|
98
|
-
6. **Run the handler
|
|
99
|
-
|
|
107
|
+
6. **Run the handler and unwind middleware**; parse the final chain output
|
|
108
|
+
through the `result` schema exactly once. Middleware sees schema inputs;
|
|
109
|
+
audit, completion, and the executor receive validated schema outputs.
|
|
100
110
|
7. **Audit**: call the operation's `audit({ command, result }, ctx)`. If it
|
|
101
111
|
returns non-null, the entry (plus the operation's `classification`) is
|
|
102
112
|
written through the injected `writeAudit` — **in the same transaction** as
|
|
@@ -106,7 +116,9 @@ The public API layer then wraps the executor in an `authMutation` and passes
|
|
|
106
116
|
allowlist throws (`COMMAND_AGGREGATE_NOT_DECLARED`) — the audit
|
|
107
117
|
vocabulary is enforced, not advisory. The per-operation catalog is
|
|
108
118
|
introspectable as `commands.aggregates` for tests.
|
|
109
|
-
8. **
|
|
119
|
+
8. **Complete**: await the closure returned by `prepare`, including when audit
|
|
120
|
+
returns null. A failure propagates inside the observation boundary.
|
|
121
|
+
9. **Emit the observation**: `{ operation, classification, outcome,
|
|
110
122
|
errorCode?, durationMs }` — completed, denied, or failed per
|
|
111
123
|
`classifyError`.
|
|
112
124
|
|
|
@@ -150,11 +162,11 @@ a handler in disguise.
|
|
|
150
162
|
|
|
151
163
|
### Middleware — composable, next()-based
|
|
152
164
|
|
|
153
|
-
For wrap-around concerns (timing, tracing, context enrichment
|
|
154
|
-
|
|
165
|
+
For wrap-around concerns (timing, tracing, context enrichment) that two
|
|
166
|
+
disconnected callbacks can't express, both kernels take
|
|
155
167
|
Express/TanStack-style middleware. The chain runs **inside** the pipeline's
|
|
156
168
|
invariants: after the permission check, around [guards → handler], before the
|
|
157
|
-
result-schema
|
|
169
|
+
result-schema parse, aggregate allowlist, and audit — so middleware can
|
|
158
170
|
never skip authorization, return an invalid result, or desynchronize audit
|
|
159
171
|
from effects.
|
|
160
172
|
|
|
@@ -188,16 +200,119 @@ guard → operation guard → handler. `next()` returns the downstream result;
|
|
|
188
200
|
middleware, guards, and the handler. Skipping `next()` short-circuits
|
|
189
201
|
(guards and handler never run — the returned value still must satisfy the
|
|
190
202
|
result schema); calling it twice throws `COMMAND_MIDDLEWARE_NEXT_REUSED`.
|
|
191
|
-
Whatever leaves the chain is
|
|
203
|
+
Whatever leaves the chain is parsed through the operation's strict result
|
|
192
204
|
schema — a middleware cannot fabricate an invalid result.
|
|
193
205
|
|
|
206
|
+
### Schema-inferred callbacks and context extensions (0.1.3)
|
|
207
|
+
|
|
208
|
+
`Command.withContext<Ctx>()` binds the host context for operation definitions.
|
|
209
|
+
Its `operation()` infers parsed command fields, validated audit result fields,
|
|
210
|
+
and the declared aggregate vocabulary. Operation middleware is contextual:
|
|
211
|
+
write inline callbacks without `Command.middleware`, whose legacy low-level
|
|
212
|
+
command and result boundary intentionally remains unknown.
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const typed = Command.withContext<Ctx>()
|
|
216
|
+
const operations = {
|
|
217
|
+
rename: typed.operation({
|
|
218
|
+
command: z.object({ title: z.string().transform(value => value.length) }),
|
|
219
|
+
result: z.object({ ok: z.boolean() }),
|
|
220
|
+
classification: 'business',
|
|
221
|
+
aggregates: ['document'],
|
|
222
|
+
guard: (ctx, command) => requireTitleLength(ctx, command.title), // number
|
|
223
|
+
middleware: [async ({ command, next }) => {
|
|
224
|
+
const result = await next() // { ok: boolean }, before validation
|
|
225
|
+
logLength(command.title)
|
|
226
|
+
return result
|
|
227
|
+
}],
|
|
228
|
+
audit: ({ command, result }, ctx) => ({
|
|
229
|
+
operation: 'rename', actorId: ctx.actorId,
|
|
230
|
+
aggregate: { type: 'document', id: String(command.title) },
|
|
231
|
+
metadata: { ok: result.ok },
|
|
232
|
+
}),
|
|
233
|
+
}),
|
|
234
|
+
}
|
|
235
|
+
const middleware = typed.registryMiddleware(operations, async input => {
|
|
236
|
+
if (input.operation === 'rename') {
|
|
237
|
+
input.command.title.toFixed() // discriminator retains schema correlation
|
|
238
|
+
}
|
|
239
|
+
return input.next()
|
|
240
|
+
})
|
|
241
|
+
const commands = new Command<Ctx, typeof operations>(operations, {
|
|
242
|
+
middleware: [middleware],
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Bind registry middleware to the same registry it describes. A heterogeneous
|
|
247
|
+
registry callback returns a union; narrowing `input.operation` preserves
|
|
248
|
+
the matching `input.command` and `input.next()` result type.
|
|
249
|
+
Reusing it with a different operation definition throws
|
|
250
|
+
`COMMAND_MIDDLEWARE_REGISTRY_MISMATCH` before the typed callback runs.
|
|
251
|
+
|
|
252
|
+
To propagate a required middleware extension to guards and handlers, use
|
|
253
|
+
`Command.withContext<Ctx, { traceId: string }>()`. Its operations require a
|
|
254
|
+
nonempty middleware tuple; the first layer must pass the declared extension
|
|
255
|
+
to `next({ context: { traceId } })` whenever it runs the downstream chain.
|
|
256
|
+
Later layers, guards and handlers see `Ctx & { traceId: string }`.
|
|
257
|
+
Audit and prepare receive the original context. The original context object
|
|
258
|
+
is not mutated. Short circuits still undergo final result validation.
|
|
259
|
+
|
|
260
|
+
### Transactional completion and replay (0.1.3)
|
|
261
|
+
|
|
262
|
+
Use `prepare(ctx, parsedCommand)` for host-owned idempotency. It runs after
|
|
263
|
+
permission and returns either `{ kind: 'replay', result: durableValue }` or
|
|
264
|
+
`{ kind: 'execute', complete: async validatedResult => { ... } }`.
|
|
265
|
+
The completion closure captures per-invocation state, including a fingerprint
|
|
266
|
+
derived from the parsed command and authenticated actor. Do not cache that
|
|
267
|
+
state in a shared context or module variable.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
prepare: async (ctx, command) => {
|
|
271
|
+
const key = scopedKey(ctx.actorId, command.idempotencyKey)
|
|
272
|
+
const fingerprint = fingerprintOf(command)
|
|
273
|
+
const receipt = await loadReceipt(ctx, key)
|
|
274
|
+
if (receipt) {
|
|
275
|
+
if (receipt.fingerprint !== fingerprint) throw new Error('KEY_REUSED')
|
|
276
|
+
return { kind: 'replay', result: receipt.result }
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
kind: 'execute',
|
|
280
|
+
complete: async result => {
|
|
281
|
+
await saveReceipt(ctx, { key, fingerprint, result })
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Storage, actor/tenant scoping, fingerprint comparison, retention, and conflict
|
|
288
|
+
policy remain host responsibilities. Put authorization required on replay in
|
|
289
|
+
the injected permission checker or prepare lookup; both guards are skipped
|
|
290
|
+
on replay. A permission slug without a checker still fails closed. Ordinary
|
|
291
|
+
middleware short-circuiting is not this replay path and still audits.
|
|
292
|
+
|
|
293
|
+
Replays are parsed through `result` by default. If that schema transforms its
|
|
294
|
+
input, store the completion callback's validated output and supply
|
|
295
|
+
`replayResult` with an output validator (for example `z.number().int()` when
|
|
296
|
+
`result` transforms a string into a number). This prevents reapplying an input
|
|
297
|
+
transform to an already transformed durable result. An invalid replay fails
|
|
298
|
+
inside observation; successful replay does not invoke completion again.
|
|
299
|
+
|
|
300
|
+
Completion is awaited after result validation, audit resolution, aggregate
|
|
301
|
+
validation, and audit writing, even on audit-null paths. Errors from prepare,
|
|
302
|
+
replay validation, audit, or completion propagate and are passed to the host's
|
|
303
|
+
error classifier. Observation success means this command finished; the
|
|
304
|
+
enclosing mutation has not necessarily committed yet.
|
|
305
|
+
|
|
194
306
|
The Query kernel takes the same shape: `new Query({ defaults, middleware,
|
|
195
307
|
execute })` plus per-executor `middleware: [...]`, with `Query.middleware`
|
|
196
308
|
as the typing helper. Kernel middleware runs before executor middleware,
|
|
197
309
|
inside the host's injected `execute` policy.
|
|
198
310
|
|
|
199
|
-
|
|
200
|
-
|
|
311
|
+
For atomic rollback, let failures escape the enclosing Convex mutation.
|
|
312
|
+
Catching and swallowing a failure inside that mutation can commit earlier
|
|
313
|
+
business, audit, and completion writes. Rethrow after effects, or use the
|
|
314
|
+
effect-aware `executeResultBoundary` below. External I/O in actions is not
|
|
315
|
+
rolled back; these transaction guarantees concern Convex mutation writes.
|
|
201
316
|
|
|
202
317
|
### Dynamic dispatch
|
|
203
318
|
|
|
@@ -269,9 +384,9 @@ knowing. Metadata defaults are set at construction and merged per-executor.
|
|
|
269
384
|
else destructures from it.
|
|
270
385
|
2. Never deep-import the component's `modules/*` or `result.ts` paths from app
|
|
271
386
|
code — the `client.ts` facade is the contract.
|
|
272
|
-
3.
|
|
273
|
-
|
|
274
|
-
mutations.
|
|
387
|
+
3. Use `Command.withContext<MutationCtx>()` for typed callbacks and construct
|
|
388
|
+
the Command with a compatible host context. Legacy `Command.operation`
|
|
389
|
+
keeps its low-level callback signature. Commands with writes run in mutations.
|
|
275
390
|
4. Name operations `domain.verb` and error codes `UPPER_SNAKE` or your
|
|
276
391
|
telemetry silently disappears (see regexes above).
|
|
277
392
|
5. Keep operation registries frozen (`as const`) in the domain's
|
package/docs/llms.md
CHANGED
|
@@ -114,6 +114,7 @@ export const { authQuery, authMutation, authAction, adminQuery, adminMutation,
|
|
|
114
114
|
adminRoles: ['admin'],
|
|
115
115
|
triggers,
|
|
116
116
|
verifyMembership: async ({ userId, organizationId }) => { /* live check; actions only */ },
|
|
117
|
+
resolveOrganization: async ({ ctx, identity, user }) => { /* org+role from app tables; overrides claims; null/throw ⇒ FORBIDDEN */ },
|
|
117
118
|
})
|
|
118
119
|
|
|
119
120
|
// convex/foundation.ts
|
package/docs/upgrading.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Upgrading to 0.1.0 — the new way of things
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
Install `convex-helpers` as a direct peer, using one compatible instance:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add cvx-kit@0.1.3 convex-helpers@0.1.124
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The peer range is `>=0.1.123 <0.2.0`. Remove overrides that force a
|
|
12
|
+
separate helper copy and regenerate the installation without discarding
|
|
13
|
+
unrelated dependency choices. This release is validated with Convex 1.45.0,
|
|
14
|
+
Zod 4.5.4, and convex-test 0.0.56.
|
|
15
|
+
|
|
16
|
+
Existing `Command.operation` and `Command.middleware` remain available.
|
|
17
|
+
For callback inference, use `Command.withContext<MutationCtx>().operation`;
|
|
18
|
+
see `commands.md` for typed middleware and transactional completion.
|
|
19
|
+
|
|
20
|
+
Result transforms now run once, after middleware. A handler and middleware
|
|
21
|
+
return the result schema's **input** type; audit, completion, and the executor
|
|
22
|
+
receive its **output**. Middleware reading `await next()` sees the unparsed
|
|
23
|
+
handler result. Remove workarounds that compensate for double transforms.
|
|
24
|
+
When storing transformed outputs for replay, supply an output validator as
|
|
25
|
+
`replayResult`; otherwise replay uses the ordinary result schema.
|
|
26
|
+
Executor callers supply the command schema's input type; guards, prepare,
|
|
27
|
+
middleware, and handlers receive its parsed output. For example, a string
|
|
28
|
+
transformed to a number is passed as a string to the executor and observed
|
|
29
|
+
as a number by its callbacks.
|
|
30
|
+
|
|
31
|
+
Test registration no longer needs a registrar adapter or assertion:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { convexTest } from 'convex-test'
|
|
35
|
+
import { registerFoundation, registerApprovals } from 'cvx-kit/test'
|
|
36
|
+
import schema from './schema'
|
|
37
|
+
|
|
38
|
+
const t = convexTest(schema, import.meta.glob('./**/*.ts'))
|
|
39
|
+
registerFoundation(t)
|
|
40
|
+
registerApprovals(t)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Both helpers accept an optional custom component path as their second argument.
|
|
44
|
+
|
|
3
45
|
## Approvals packaging repair (0.1.x patch)
|
|
4
46
|
|
|
5
47
|
`cvx-kit@0.1.0` published component schemas as `schema.mjs`, which Convex does
|
package/docs/zod-table.md
CHANGED
|
@@ -27,6 +27,28 @@ export const documents = zodTable(
|
|
|
27
27
|
|
|
28
28
|
## What you get back
|
|
29
29
|
|
|
30
|
+
### Shared ID validation (0.1.3)
|
|
31
|
+
|
|
32
|
+
Install one compatible `convex-helpers` peer (`>=0.1.123 <0.2.0`) alongside
|
|
33
|
+
cvx-kit. ID metadata lives in a helper-instance registry: copying schemas
|
|
34
|
+
between separately installed helper instances can silently turn an ID into
|
|
35
|
+
`v.any()`. An exact nested dependency previously caused that failure even
|
|
36
|
+
when both helper versions were compatible. Fresh npm and Bun consumer tests
|
|
37
|
+
now exercise storage, command, DTO, and nested ID conversion with a different
|
|
38
|
+
compatible host version.
|
|
39
|
+
|
|
40
|
+
Prefer the matching kit exports when deriving validators from kit schemas:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { zid, zodToConvex, convexToZod } from 'cvx-kit/zod-table'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Do not force conflicting nested helper copies with overrides or reuse schema
|
|
47
|
+
objects across separate bundles. The upstream converter cannot recognize an
|
|
48
|
+
unregistered foreign ID and does not throw; peer installation prevents the
|
|
49
|
+
supported package graph from creating that ambiguity. Verify critical ID
|
|
50
|
+
validators retain `kind === 'id'` and the expected `tableName` after installation.
|
|
51
|
+
|
|
30
52
|
| Property | What it is | Where you use it |
|
|
31
53
|
|---|---|---|
|
|
32
54
|
| `table` | `defineTable(...)` from the storage shape | `convex/schema.ts` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cvx-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"typecheck": "tsc --noEmit",
|
|
93
93
|
"test": "vp test run --exclude test/package-contents.test.mjs",
|
|
94
94
|
"test:package": "bun run build && vp test run test/package-contents.test.mjs",
|
|
95
|
+
"test:package:ids": "bun run build && node scripts/test-packed-ids.mjs",
|
|
95
96
|
"test:package:smoke:bun": "bun scripts/smoke-packed-components.mjs bun",
|
|
96
97
|
"test:package:smoke:npm": "node scripts/smoke-packed-components.mjs npm",
|
|
97
98
|
"docs:sync": "rm -f docs/*.md && cp src/docs/*.md docs/",
|
|
@@ -101,22 +102,23 @@
|
|
|
101
102
|
"release:minor": "npm version minor && git push --follow-tags"
|
|
102
103
|
},
|
|
103
104
|
"dependencies": {
|
|
104
|
-
"@convex-dev/workflow": "0.4.
|
|
105
|
-
"convex-audit-log": "^0.2.0"
|
|
106
|
-
"convex-helpers": "0.1.122"
|
|
105
|
+
"@convex-dev/workflow": "0.4.6",
|
|
106
|
+
"convex-audit-log": "^0.2.0"
|
|
107
107
|
},
|
|
108
108
|
"peerDependencies": {
|
|
109
|
-
"convex": "
|
|
110
|
-
"
|
|
109
|
+
"convex-helpers": ">=0.1.123 <0.2.0",
|
|
110
|
+
"convex": "^1.45.0",
|
|
111
|
+
"zod": "^4.5.4"
|
|
111
112
|
},
|
|
112
113
|
"devDependencies": {
|
|
114
|
+
"convex-helpers": "0.1.124",
|
|
113
115
|
"@edge-runtime/vm": "^5.0.0",
|
|
114
|
-
"convex": "1.
|
|
115
|
-
"convex-test": "^0.0.
|
|
116
|
-
"publint": "^0.3.
|
|
116
|
+
"convex": "1.45.0",
|
|
117
|
+
"convex-test": "^0.0.56",
|
|
118
|
+
"publint": "^0.3.24",
|
|
117
119
|
"typescript": "7.0.2",
|
|
118
|
-
"vite-plus": "0.
|
|
119
|
-
"zod": "4.4
|
|
120
|
+
"vite-plus": "0.3.0",
|
|
121
|
+
"zod": "4.5.4"
|
|
120
122
|
},
|
|
121
123
|
"keywords": [
|
|
122
124
|
"convex",
|
|
@@ -11,7 +11,7 @@ if (!version) {
|
|
|
11
11
|
}
|
|
12
12
|
const changelog = readFileSync(new URL('../CHANGELOG.md', import.meta.url), 'utf8')
|
|
13
13
|
const pattern = new RegExp(
|
|
14
|
-
String.raw`^## \[${version.replace(/\./g, String.raw`\.`)}\][^\n]*\n([\s\S]*?)(?=^## \[
|
|
14
|
+
String.raw`^## \[${version.replace(/\./g, String.raw`\.`)}\][^\n]*\n([\s\S]*?)(?=^## \[|(?![\s\S]))`,
|
|
15
15
|
'm',
|
|
16
16
|
)
|
|
17
17
|
const match = changelog.match(pattern)
|
|
@@ -18,9 +18,10 @@ if (installer !== 'bun' && installer !== 'npm') {
|
|
|
18
18
|
const root = join(import.meta.dirname, '..')
|
|
19
19
|
const temporaryRoot = mkdtempSync(join(tmpdir(), 'cvx-kit-smoke-'))
|
|
20
20
|
const fixture = join(temporaryRoot, 'fixture')
|
|
21
|
-
const
|
|
21
|
+
const packageManifest = JSON.parse(
|
|
22
22
|
readFileSync(join(root, 'package.json'), 'utf8'),
|
|
23
|
-
)
|
|
23
|
+
)
|
|
24
|
+
const packageVersion = packageManifest.version
|
|
24
25
|
const tarball = join(temporaryRoot, `cvx-kit-${packageVersion}.tgz`)
|
|
25
26
|
|
|
26
27
|
function run(command, args, cwd = fixture) {
|
|
@@ -72,11 +73,12 @@ try {
|
|
|
72
73
|
type: 'module',
|
|
73
74
|
dependencies: {
|
|
74
75
|
'cvx-kit': `file:${tarball}`,
|
|
75
|
-
convex:
|
|
76
|
-
zod:
|
|
76
|
+
convex: packageManifest.devDependencies.convex,
|
|
77
|
+
zod: packageManifest.devDependencies.zod,
|
|
78
|
+
'convex-helpers': packageManifest.devDependencies['convex-helpers'],
|
|
77
79
|
},
|
|
78
80
|
devDependencies: {
|
|
79
|
-
'vite-plus':
|
|
81
|
+
'vite-plus': packageManifest.devDependencies['vite-plus'],
|
|
80
82
|
},
|
|
81
83
|
},
|
|
82
84
|
null,
|
|
@@ -327,27 +329,26 @@ export const status = query({
|
|
|
327
329
|
)
|
|
328
330
|
}
|
|
329
331
|
|
|
330
|
-
|
|
331
|
-
let
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const currentStatus = JSON.parse(
|
|
341
|
-
runConvex('run', 'smoke:status', JSON.stringify({ runId })),
|
|
342
|
-
)
|
|
343
|
-
if (currentStatus.state === 'approved') {
|
|
344
|
-
decision = currentStatus
|
|
345
|
-
break
|
|
346
|
-
}
|
|
347
|
-
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
332
|
+
// start schedules the workflow; a successful start is not decision readiness.
|
|
333
|
+
let readyStatus
|
|
334
|
+
const readyDeadline = Date.now() + 60_000
|
|
335
|
+
while (Date.now() < readyDeadline) {
|
|
336
|
+
readyStatus = JSON.parse(
|
|
337
|
+
runConvex('run', 'smoke:status', JSON.stringify({ runId })),
|
|
338
|
+
)
|
|
339
|
+
if (readyStatus?.run.state !== 'pending' ||
|
|
340
|
+
['failed', 'canceled', 'completed'].includes(readyStatus?.execution?.type)) {
|
|
341
|
+
throw new Error(`Approval ended before decision readiness: ${JSON.stringify(readyStatus)}`)
|
|
348
342
|
}
|
|
343
|
+
if (readyStatus.run.currentStepKey === 'releaseDecision') break
|
|
344
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
349
345
|
}
|
|
350
|
-
if (
|
|
346
|
+
if (readyStatus?.run.currentStepKey !== 'releaseDecision') {
|
|
347
|
+
throw new Error(`Approval did not become decision-ready: ${JSON.stringify(readyStatus)}`)
|
|
348
|
+
}
|
|
349
|
+
const decision = JSON.parse(
|
|
350
|
+
runConvex('run', 'smoke:decide', JSON.stringify({ runId })),
|
|
351
|
+
)
|
|
351
352
|
if (decision.state !== 'approved') {
|
|
352
353
|
throw new Error(`Unexpected decision response: ${JSON.stringify(decision)}`)
|
|
353
354
|
}
|