cvx-kit 0.1.6 → 0.1.8
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 +23 -0
- package/README.md +63 -38
- package/dist/agent-tools.d.mts +2 -1
- package/dist/agent-tools.mjs +1 -1
- package/dist/agent-tools.mjs.map +1 -1
- package/dist/components/approvals/audit.mjs +1 -1
- package/dist/components/foundation/client.d.mts +16 -7
- 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 +50 -10
- 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/command/lifecycle.d.mts +81 -0
- package/dist/components/foundation/modules/command/lifecycle.mjs +2 -0
- package/dist/components/foundation/modules/command/lifecycle.mjs.map +1 -0
- package/dist/components/foundation/modules/observability/observability.d.mts +8 -0
- 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/operation.d.mts +6 -6
- package/dist/components/foundation/operation.mjs.map +1 -1
- package/dist/contracts.d.mts +4 -0
- package/dist/contracts.mjs +1 -0
- package/dist/crud.d.mts +2 -2
- package/dist/crud.mjs.map +1 -1
- package/dist/effect.d.mts +12 -0
- package/dist/effect.mjs +1 -0
- package/dist/errors.d.mts +2 -1
- package/dist/errors.mjs +1 -1
- package/dist/errors.mjs.map +1 -1
- package/dist/idempotency.d.mts +2 -0
- package/dist/idempotency.mjs +1 -0
- package/dist/modules/contracts/contract.d.mts +70 -0
- package/dist/modules/contracts/contract.mjs +2 -0
- package/dist/modules/contracts/contract.mjs.map +1 -0
- package/dist/modules/contracts/convex.d.mts +22 -0
- package/dist/modules/contracts/convex.mjs +2 -0
- package/dist/modules/contracts/convex.mjs.map +1 -0
- package/dist/modules/contracts/errors.d.mts +44 -0
- package/dist/modules/contracts/errors.mjs +2 -0
- package/dist/modules/contracts/errors.mjs.map +1 -0
- package/dist/modules/contracts/exposure.d.mts +105 -0
- package/dist/modules/contracts/exposure.mjs +2 -0
- package/dist/modules/contracts/exposure.mjs.map +1 -0
- package/dist/modules/contracts/idempotency.d.mts +87 -0
- package/dist/modules/contracts/idempotency.mjs +2 -0
- package/dist/modules/contracts/idempotency.mjs.map +1 -0
- package/dist/modules/contracts/standard.d.mts +16 -0
- package/dist/modules/contracts/standard.mjs +2 -0
- package/dist/modules/contracts/standard.mjs.map +1 -0
- package/dist/modules/contracts/workflow.d.mts +167 -0
- package/dist/modules/contracts/workflow.mjs +2 -0
- package/dist/modules/contracts/workflow.mjs.map +1 -0
- package/dist/modules/effect/agent-tools.d.mts +2 -0
- package/dist/modules/effect/agent-tools.mjs +1 -0
- package/dist/modules/effect/api-runtime.d.mts +38 -0
- package/dist/modules/effect/api-runtime.mjs +2 -0
- package/dist/modules/effect/api-runtime.mjs.map +1 -0
- package/dist/modules/effect/api-zod.d.mts +35 -0
- package/dist/modules/effect/api-zod.mjs +2 -0
- package/dist/modules/effect/api-zod.mjs.map +1 -0
- package/dist/modules/effect/api.d.mts +21 -0
- package/dist/modules/effect/api.mjs +2 -0
- package/dist/modules/effect/api.mjs.map +1 -0
- package/dist/modules/effect/client.d.mts +11 -0
- package/dist/modules/effect/client.mjs +1 -0
- package/dist/modules/effect/command.d.mts +63 -0
- package/dist/modules/effect/command.mjs +2 -0
- package/dist/modules/effect/command.mjs.map +1 -0
- package/dist/modules/effect/crud.d.mts +785 -0
- package/dist/modules/effect/crud.mjs +2 -0
- package/dist/modules/effect/crud.mjs.map +1 -0
- package/dist/modules/effect/errors.d.mts +8 -0
- package/dist/modules/effect/errors.mjs +2 -0
- package/dist/modules/effect/errors.mjs.map +1 -0
- package/dist/modules/effect/foundation.d.mts +21 -0
- package/dist/modules/effect/foundation.mjs +2 -0
- package/dist/modules/effect/foundation.mjs.map +1 -0
- package/dist/modules/effect/idempotency.d.mts +31 -0
- package/dist/modules/effect/idempotency.mjs +2 -0
- package/dist/modules/effect/idempotency.mjs.map +1 -0
- package/dist/modules/effect/normalize.d.mts +10 -0
- package/dist/modules/effect/normalize.mjs +2 -0
- package/dist/modules/effect/normalize.mjs.map +1 -0
- package/dist/modules/effect/operation.d.mts +141 -0
- package/dist/modules/effect/operation.mjs +2 -0
- package/dist/modules/effect/operation.mjs.map +1 -0
- package/dist/modules/effect/query.d.mts +69 -0
- package/dist/modules/effect/query.mjs +2 -0
- package/dist/modules/effect/query.mjs.map +1 -0
- package/dist/modules/effect/schema.d.mts +33 -0
- package/dist/modules/effect/schema.mjs +2 -0
- package/dist/modules/effect/schema.mjs.map +1 -0
- package/dist/modules/effect/workflow.d.mts +77 -0
- package/dist/modules/effect/workflow.mjs +2 -0
- package/dist/modules/effect/workflow.mjs.map +1 -0
- package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +79 -0
- package/dist/workflow.d.mts +2 -0
- package/dist/workflow.mjs +1 -0
- package/docs/architecture.md +3 -3
- package/docs/auth.md +71 -0
- package/docs/command-context.md +150 -0
- package/docs/commands-legacy.md +511 -0
- package/docs/commands.md +143 -363
- package/docs/conventions.md +15 -5
- package/docs/crud.md +4 -0
- package/docs/domain-operations.md +615 -0
- package/docs/effect.md +395 -0
- package/docs/llms.md +69 -45
- package/docs/migration.md +1 -1
- package/docs/upgrading.md +9 -0
- package/package.json +33 -1
- package/scripts/convex-smoke-env.mjs +33 -0
- package/scripts/smoke-packed-components.mjs +9 -2
- package/scripts/smoke-packed-effect.mjs +439 -0
- package/scripts/test-packed-effect.mjs +460 -0
- package/scripts/test-packed-ids.mjs +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ContractInput, ContractOutput, ContractSchema } from "../contracts/contract.mjs";
|
|
2
|
+
import { ContractDecoderError, ContractDecoderRequirements } from "./schema.mjs";
|
|
3
|
+
import { CallbackError, CallbackRequirements, EffectValue } from "./operation.mjs";
|
|
4
|
+
import { IdempotencyInvocation } from "../contracts/idempotency.mjs";
|
|
5
|
+
import { EffectIdempotencyPreparation } from "./idempotency.mjs";
|
|
6
|
+
import { WorkflowBindingError, WorkflowExecution, WorkflowHostState, WorkflowNativeStatus, WorkflowOutcome, WorkflowTransition } from "../contracts/workflow.mjs";
|
|
7
|
+
import { Effect } from "effect";
|
|
8
|
+
//#region src/modules/effect/workflow.d.ts
|
|
9
|
+
type Supported<A> = A | PromiseLike<A> | Effect.Effect<A, unknown, unknown>;
|
|
10
|
+
/** Services exist only inside a new native invocation; ordinary throws/rejections remain defects. */
|
|
11
|
+
declare function effectWorkflowAttempt<Args, Authorize, Execute>(configuration: {
|
|
12
|
+
authorize: (execution: WorkflowExecution<Args>) => Authorize;
|
|
13
|
+
execute: (execution: WorkflowExecution<Args>, authority: EffectValue<Authorize>) => Execute;
|
|
14
|
+
}): (input: WorkflowExecution<Args>) => Effect.Effect<EffectValue<Execute>, WorkflowBindingError | CallbackError<Authorize | Execute>, CallbackRequirements<Authorize | Execute>>;
|
|
15
|
+
/** Native mutation promise rejects outside the child transaction before mapping declared terminal details. */
|
|
16
|
+
declare function effectTerminalWorkflowAttempt<Args, Authorize, const Result extends ContractSchema, const Failure extends ContractSchema, Terminal extends Supported<ContractInput<Failure> | undefined>>(configuration: ({
|
|
17
|
+
kind: 'mutation';
|
|
18
|
+
atomicJournalProof: 'host-verified';
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'action';
|
|
21
|
+
protectedMutation: EffectReceiptProtectedWorkflowMutation<Args, unknown, unknown, unknown>;
|
|
22
|
+
}) & {
|
|
23
|
+
authorize: (execution: WorkflowExecution<Args>) => Authorize;
|
|
24
|
+
invokeMutation: (execution: WorkflowExecution<Args>, authority: EffectValue<Authorize>) => Promise<ContractInput<Result>>;
|
|
25
|
+
terminal: (error: unknown) => Terminal;
|
|
26
|
+
result: Result;
|
|
27
|
+
failure: Failure;
|
|
28
|
+
}): (input: WorkflowExecution<Args>) => Effect.Effect<WorkflowOutcome<ContractInput<Result>, ContractOutput<Failure>>, WorkflowBindingError | CallbackError<Authorize | Terminal> | ContractDecoderError<Result | Failure>, CallbackRequirements<Authorize | Terminal> | ContractDecoderRequirements<Result | Failure>>;
|
|
29
|
+
declare const protectedMutation: unique symbol;
|
|
30
|
+
interface EffectReceiptProtectedWorkflowMutation<Args, Result, Error, Requirements> {
|
|
31
|
+
readonly [protectedMutation]: {
|
|
32
|
+
args: Args;
|
|
33
|
+
result: Result;
|
|
34
|
+
};
|
|
35
|
+
readonly handler: (execution: WorkflowExecution<Args>) => Effect.Effect<Result, Error, Requirements>;
|
|
36
|
+
}
|
|
37
|
+
type Completion<Prepare> = Extract<EffectValue<Prepare>, {
|
|
38
|
+
kind: 'execute';
|
|
39
|
+
}> extends {
|
|
40
|
+
complete: (...arguments_: never[]) => infer Returned;
|
|
41
|
+
} ? Returned : never;
|
|
42
|
+
/** Host runs the handler in the secured inner native mutation transaction, including receipt completion. */
|
|
43
|
+
declare function effectReceiptWorkflowMutation<Args, Authorize, const Replay extends ContractSchema, Prepare extends Supported<EffectIdempotencyPreparation<ContractOutput<Replay>, unknown, unknown>>, Execute extends Supported<ContractOutput<Replay>>>(configuration: {
|
|
44
|
+
authorize: (execution: WorkflowExecution<Args>) => Authorize;
|
|
45
|
+
receipt: (authority: EffectValue<Authorize>) => {
|
|
46
|
+
replayResult: Replay;
|
|
47
|
+
prepare: (invocation: IdempotencyInvocation) => Prepare;
|
|
48
|
+
};
|
|
49
|
+
invocation: (execution: WorkflowExecution<Args>, authority: EffectValue<Authorize>) => IdempotencyInvocation;
|
|
50
|
+
execute: (execution: WorkflowExecution<Args>, authority: EffectValue<Authorize>) => Execute;
|
|
51
|
+
}): EffectReceiptProtectedWorkflowMutation<Args, ContractOutput<Replay>, WorkflowBindingError | CallbackError<Authorize | Prepare | Execute | Completion<Prepare>> | ContractDecoderError<Replay>, CallbackRequirements<Authorize | Prepare | Execute | Completion<Prepare>> | ContractDecoderRequirements<Replay>>;
|
|
52
|
+
type StateResult<Read> = EffectValue<Read> extends WorkflowHostState<infer Result, unknown> ? Result : never;
|
|
53
|
+
type StateFailure<Read> = EffectValue<Read> extends WorkflowHostState<unknown, infer Failure> ? Failure : never;
|
|
54
|
+
/** The host still owns native manager calls and transactional/CAS writes; each method retains only its used channels. */
|
|
55
|
+
declare function effectWorkflowStatusBindings<Authorize, Read extends Supported<WorkflowHostState>, Write, Native extends Supported<WorkflowNativeStatus>, Receipt extends Supported<WorkflowOutcome<StateResult<Read>, StateFailure<Read>> | undefined>, Cancel, Restart>(configuration: {
|
|
56
|
+
authorize: () => Authorize;
|
|
57
|
+
read: () => Read;
|
|
58
|
+
write: (expectedGeneration: number, state: WorkflowHostState<StateResult<Read>, StateFailure<Read>>) => Write;
|
|
59
|
+
nativeStatus: () => Native;
|
|
60
|
+
receiptOutcome: () => Receipt;
|
|
61
|
+
cancelNative: () => Cancel;
|
|
62
|
+
restartNative: () => Restart;
|
|
63
|
+
}): {
|
|
64
|
+
callback(expectedGeneration: number, nativeStatus: WorkflowNativeStatus, outcome?: WorkflowOutcome<StateResult<Read>, StateFailure<Read>>): Effect.Effect<WorkflowTransition<StateResult<Read>, StateFailure<Read>>, WorkflowBindingError | CallbackError<Authorize> | CallbackError<Read> | CallbackError<Write>, CallbackRequirements<Authorize> | CallbackRequirements<Read> | CallbackRequirements<Write>>;
|
|
65
|
+
reconcile(expectedGeneration: number): Effect.Effect<WorkflowTransition<StateResult<Read>, StateFailure<Read>>, WorkflowBindingError | CallbackError<Authorize> | CallbackError<Native> | CallbackError<Read> | CallbackError<Receipt> | CallbackError<Write>, CallbackRequirements<Authorize> | CallbackRequirements<Native> | CallbackRequirements<Read> | CallbackRequirements<Receipt> | CallbackRequirements<Write>>;
|
|
66
|
+
cancel(expectedGeneration: number): Effect.Effect<WorkflowTransition<StateResult<Read>, StateFailure<Read>>, WorkflowBindingError | CallbackError<Authorize> | CallbackError<Cancel> | CallbackError<Read> | CallbackError<Write>, CallbackRequirements<Authorize> | CallbackRequirements<Cancel> | CallbackRequirements<Read> | CallbackRequirements<Write>>;
|
|
67
|
+
restart(expectedGeneration: number): Effect.Effect<{
|
|
68
|
+
kind: 'ignored';
|
|
69
|
+
state: WorkflowHostState<StateResult<Read>, StateFailure<Read>>;
|
|
70
|
+
} | {
|
|
71
|
+
kind: 'applied';
|
|
72
|
+
state: WorkflowHostState<StateResult<Read>, StateFailure<Read>>;
|
|
73
|
+
}, WorkflowBindingError | CallbackError<Authorize> | CallbackError<Read> | CallbackError<Restart> | CallbackError<Write>, CallbackRequirements<Authorize> | CallbackRequirements<Read> | CallbackRequirements<Restart> | CallbackRequirements<Write>>;
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { EffectReceiptProtectedWorkflowMutation, effectReceiptWorkflowMutation, effectTerminalWorkflowAttempt, effectWorkflowAttempt, effectWorkflowStatusBindings };
|
|
77
|
+
//# sourceMappingURL=workflow.d.mts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{decodeEffectContract as e}from"./schema.mjs";import{normalizeEffect as t}from"./normalize.mjs";import{WorkflowBindingError as n,workflowCancel as r,workflowExecution as i,workflowRestart as a,workflowTransition as o}from"../contracts/workflow.mjs";import{Effect as s}from"effect";function c(e){return n=>s.gen(function*(){let r=yield*f(n),i=yield*t(()=>e.authorize(r));return yield*t(()=>e.execute(r,i))})}function l(n){return r=>s.gen(function*(){let i=yield*f(r),a=yield*t(()=>n.authorize(i)),o=yield*s.tryPromise({try:()=>n.invokeMutation(i,a),catch:e=>e}).pipe(s.map(e=>({kind:`success`,value:e})),s.catch(e=>s.succeed({kind:`rejected`,error:e})));if(o.kind===`rejected`){let r=yield*t(()=>n.terminal(o.error));if(r===void 0)return yield*s.die(o.error);let a=yield*e(n.failure,r);return yield*f({...i,args:a}),{version:1,kind:`failed`,error:a}}return yield*e(n.result,o.value),yield*f({...i,args:o.value}),{version:1,kind:`succeeded`,value:o.value}})}function u(n){return{handler:r=>s.gen(function*(){let i=yield*f(r),a=yield*t(()=>n.authorize(i)),o=n.receipt(a),s=yield*t(()=>o.prepare(n.invocation(i,a)));if(s.kind===`replay`)return yield*e(o.replayResult,s.result);let c=yield*t(()=>n.execute(i,a)),l=s.complete;return yield*t(()=>l(c)),c})}}function d(e){function n(){return t(()=>e.read()).pipe(s.map(e=>e))}function i(r,i,a){return s.gen(function*(){let s=yield*n(),c={generation:r,nativeStatus:i},l=yield*p(()=>o(s,a===void 0?c:{...c,outcome:a}));return l.kind===`applied`&&(yield*t(()=>e.write(r,l.state))),l})}return{callback(n,r,a){return t(()=>e.authorize()).pipe(s.flatMap(()=>i(n,r,a)))},reconcile(n){return s.gen(function*(){return yield*t(()=>e.authorize()),yield*i(n,yield*t(()=>e.nativeStatus()),yield*t(()=>e.receiptOutcome()))})},cancel(i){return s.gen(function*(){yield*t(()=>e.authorize());let a=yield*n(),o=yield*p(()=>r(a,i));return o.kind===`applied`&&a.status===`running`&&(yield*t(()=>e.cancelNative()),yield*t(()=>e.write(i,o.state))),o})},restart(r){return s.gen(function*(){yield*t(()=>e.authorize());let i=yield*n();if(i.generation!==r)return{kind:`ignored`,state:i};let o=yield*p(()=>a(i));return yield*t(()=>e.restartNative()),yield*t(()=>e.write(r,o)),{kind:`applied`,state:o}})}}}function f(e){return p(()=>i(e))}function p(e){return s.try({try:e,catch:e=>{if(e instanceof n)return e;throw e}})}export{u as effectReceiptWorkflowMutation,l as effectTerminalWorkflowAttempt,c as effectWorkflowAttempt,d as effectWorkflowStatusBindings};
|
|
2
|
+
//# sourceMappingURL=workflow.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.mjs","names":[],"sources":["../../../src/modules/effect/workflow.ts"],"sourcesContent":["import { Effect } from 'effect'\nimport type { ContractInput, ContractOutput, ContractSchema } from '../contracts/contract'\nimport {\n\tWorkflowBindingError,\n\tworkflowExecution,\n\tworkflowTransition,\n\tworkflowCancel,\n\tworkflowRestart,\n\ttype WorkflowExecution,\n\ttype WorkflowOutcome,\n\ttype WorkflowHostState,\n\ttype WorkflowNativeStatus,\n} from '../contracts/workflow'\nimport type { IdempotencyInvocation } from '../contracts/idempotency'\nimport type { EffectIdempotencyPreparation } from './idempotency'\nimport type { CallbackError, CallbackRequirements, EffectValue } from './operation'\nimport { normalizeEffect } from './normalize'\nimport {\n\tdecodeEffectContract,\n\ttype ContractDecoderError,\n\ttype ContractDecoderRequirements,\n} from './schema'\n\ntype Supported<A> = A | PromiseLike<A> | Effect.Effect<A, unknown, unknown>\n/** Services exist only inside a new native invocation; ordinary throws/rejections remain defects. */\nexport function effectWorkflowAttempt<Args, Authorize, Execute>(configuration: {\n\tauthorize: (execution: WorkflowExecution<Args>) => Authorize\n\texecute: (execution: WorkflowExecution<Args>, authority: EffectValue<Authorize>) => Execute\n}) {\n\treturn (\n\t\tinput: WorkflowExecution<Args>,\n\t): Effect.Effect<\n\t\tEffectValue<Execute>,\n\t\tWorkflowBindingError | CallbackError<Authorize | Execute>,\n\t\tCallbackRequirements<Authorize | Execute>\n\t> =>\n\t\tEffect.gen(function* () {\n\t\t\tconst execution = yield* descriptor(input)\n\t\t\tconst authority = yield* normalizeEffect(() => configuration.authorize(execution))\n\t\t\treturn yield* normalizeEffect(() => configuration.execute(execution, authority))\n\t\t})\n}\n/** Native mutation promise rejects outside the child transaction before mapping declared terminal details. */\nexport function effectTerminalWorkflowAttempt<\n\tArgs,\n\tAuthorize,\n\tconst Result extends ContractSchema,\n\tconst Failure extends ContractSchema,\n\tTerminal extends Supported<ContractInput<Failure> | undefined>,\n>(\n\tconfiguration: (\n\t\t| { kind: 'mutation'; atomicJournalProof: 'host-verified' }\n\t\t| {\n\t\t\t\tkind: 'action'\n\t\t\t\tprotectedMutation: EffectReceiptProtectedWorkflowMutation<Args, unknown, unknown, unknown>\n\t\t }\n\t) & {\n\t\tauthorize: (execution: WorkflowExecution<Args>) => Authorize\n\t\tinvokeMutation: (\n\t\t\texecution: WorkflowExecution<Args>,\n\t\t\tauthority: EffectValue<Authorize>,\n\t\t) => Promise<ContractInput<Result>>\n\t\t// oxlint-disable-next-line anti-slop/no-unknown-parameters -- A rejected native mutation can throw any value; the host terminal callback classifies it before failure contract validation.\n\t\tterminal: (error: unknown) => Terminal\n\t\tresult: Result\n\t\tfailure: Failure\n\t},\n) {\n\treturn (\n\t\tinput: WorkflowExecution<Args>,\n\t): Effect.Effect<\n\t\tWorkflowOutcome<ContractInput<Result>, ContractOutput<Failure>>,\n\t\t| WorkflowBindingError\n\t\t| CallbackError<Authorize | Terminal>\n\t\t| ContractDecoderError<Result | Failure>,\n\t\tCallbackRequirements<Authorize | Terminal> | ContractDecoderRequirements<Result | Failure>\n\t> =>\n\t\tEffect.gen(function* () {\n\t\t\tconst execution = yield* descriptor(input)\n\t\t\tconst authority = yield* normalizeEffect(() => configuration.authorize(execution))\n\t\t\tconst native = yield* Effect.tryPromise({\n\t\t\t\ttry: () => configuration.invokeMutation(execution, authority),\n\t\t\t\tcatch: (error) => error,\n\t\t\t}).pipe(\n\t\t\t\tEffect.map((value) => ({ kind: 'success' as const, value })),\n\t\t\t\tEffect.catch((error) => Effect.succeed({ kind: 'rejected' as const, error })),\n\t\t\t)\n\t\t\tif (native.kind === 'rejected') {\n\t\t\t\tconst failure = yield* normalizeEffect(() => configuration.terminal(native.error))\n\t\t\t\tif (failure === undefined) return yield* Effect.die(native.error)\n\t\t\t\t// SAFETY: Terminal is constrained to this exact failure wire input or undefined.\n\t\t\t\tconst error = yield* decodeEffectContract(\n\t\t\t\t\tconfiguration.failure,\n\t\t\t\t\tfailure as ContractInput<Failure>,\n\t\t\t\t)\n\t\t\t\tyield* descriptor({ ...execution, args: error })\n\t\t\t\treturn { version: 1 as const, kind: 'failed' as const, error }\n\t\t\t}\n\t\t\tyield* decodeEffectContract(configuration.result, native.value)\n\t\t\tyield* descriptor({ ...execution, args: native.value })\n\t\t\treturn { version: 1 as const, kind: 'succeeded' as const, value: native.value }\n\t\t})\n}\ndeclare const protectedMutation: unique symbol\nexport interface EffectReceiptProtectedWorkflowMutation<Args, Result, Error, Requirements> {\n\treadonly [protectedMutation]: { args: Args; result: Result }\n\treadonly handler: (\n\t\texecution: WorkflowExecution<Args>,\n\t) => Effect.Effect<Result, Error, Requirements>\n}\ntype Completion<Prepare> =\n\tExtract<EffectValue<Prepare>, { kind: 'execute' }> extends {\n\t\tcomplete: (...arguments_: never[]) => infer Returned\n\t}\n\t\t? Returned\n\t\t: never\n/** Host runs the handler in the secured inner native mutation transaction, including receipt completion. */\nexport function effectReceiptWorkflowMutation<\n\tArgs,\n\tAuthorize,\n\tconst Replay extends ContractSchema,\n\tPrepare extends Supported<EffectIdempotencyPreparation<ContractOutput<Replay>, unknown, unknown>>,\n\tExecute extends Supported<ContractOutput<Replay>>,\n>(configuration: {\n\tauthorize: (execution: WorkflowExecution<Args>) => Authorize\n\treceipt: (authority: EffectValue<Authorize>) => {\n\t\treplayResult: Replay\n\t\tprepare: (invocation: IdempotencyInvocation) => Prepare\n\t}\n\tinvocation: (\n\t\texecution: WorkflowExecution<Args>,\n\t\tauthority: EffectValue<Authorize>,\n\t) => IdempotencyInvocation\n\texecute: (execution: WorkflowExecution<Args>, authority: EffectValue<Authorize>) => Execute\n}): EffectReceiptProtectedWorkflowMutation<\n\tArgs,\n\tContractOutput<Replay>,\n\t| WorkflowBindingError\n\t| CallbackError<Authorize | Prepare | Execute | Completion<Prepare>>\n\t| ContractDecoderError<Replay>,\n\t| CallbackRequirements<Authorize | Prepare | Execute | Completion<Prepare>>\n\t| ContractDecoderRequirements<Replay>\n> {\n\tconst handler = (input: WorkflowExecution<Args>) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst execution = yield* descriptor(input)\n\t\t\tconst authority = yield* normalizeEffect(() => configuration.authorize(execution))\n\t\t\tconst adapter = configuration.receipt(authority)\n\t\t\tconst preparation = yield* normalizeEffect(() =>\n\t\t\t\tadapter.prepare(configuration.invocation(execution, authority)),\n\t\t\t)\n\t\t\tif (preparation.kind === 'replay')\n\t\t\t\treturn yield* decodeEffectContract(adapter.replayResult, preparation.result)\n\t\t\tconst value = yield* normalizeEffect(() => configuration.execute(execution, authority))\n\t\t\t// SAFETY: the execute branch came from this exact Prepare value; recover its complete callback rather than the broad constraint.\n\t\t\tconst complete = preparation.complete as (\n\t\t\t\tresult: ContractOutput<Replay>,\n\t\t\t) => Completion<Prepare>\n\t\t\t// SAFETY: Execute's supported success is the exact final replay output contract.\n\t\t\tyield* normalizeEffect(() => complete(value as ContractOutput<Replay>))\n\t\t\t// SAFETY: Execute's supported success is the exact final replay output contract.\n\t\t\treturn value as ContractOutput<Replay>\n\t\t})\n\t// SAFETY: only private receipt evidence is asserted; handler's inferred E/R remain checked against the public return.\n\treturn { handler } as ReceiptEvidence<Args, ContractOutput<Replay>, typeof handler>\n}\ntype ReceiptEvidence<Args, Result, Handler> = {\n\thandler: Handler\n\treadonly [protectedMutation]: { args: Args; result: Result }\n}\ntype StateResult<Read> =\n\tEffectValue<Read> extends WorkflowHostState<infer Result, unknown> ? Result : never\ntype StateFailure<Read> =\n\tEffectValue<Read> extends WorkflowHostState<unknown, infer Failure> ? Failure : never\n/** The host still owns native manager calls and transactional/CAS writes; each method retains only its used channels. */\nexport function effectWorkflowStatusBindings<\n\tAuthorize,\n\tRead extends Supported<WorkflowHostState>,\n\tWrite,\n\tNative extends Supported<WorkflowNativeStatus>,\n\tReceipt extends Supported<WorkflowOutcome<StateResult<Read>, StateFailure<Read>> | undefined>,\n\tCancel,\n\tRestart,\n>(configuration: {\n\tauthorize: () => Authorize\n\tread: () => Read\n\twrite: (\n\t\texpectedGeneration: number,\n\t\tstate: WorkflowHostState<StateResult<Read>, StateFailure<Read>>,\n\t) => Write\n\tnativeStatus: () => Native\n\treceiptOutcome: () => Receipt\n\tcancelNative: () => Cancel\n\trestartNative: () => Restart\n}) {\n\ttype State = WorkflowHostState<StateResult<Read>, StateFailure<Read>>\n\tfunction read() {\n\t\t// SAFETY: Read's supported success is the host state whose outcome types are extracted above.\n\t\treturn normalizeEffect(() => configuration.read()).pipe(Effect.map((state) => state as State))\n\t}\n\tfunction apply(\n\t\texpectedGeneration: number,\n\t\tnativeStatus: WorkflowNativeStatus,\n\t\toutcome?: WorkflowOutcome<StateResult<Read>, StateFailure<Read>>,\n\t) {\n\t\treturn Effect.gen(function* () {\n\t\t\tconst current = yield* read()\n\t\t\tconst update = { generation: expectedGeneration, nativeStatus }\n\t\t\tconst transition = yield* protocol(() =>\n\t\t\t\tworkflowTransition(current, outcome === undefined ? update : { ...update, outcome }),\n\t\t\t)\n\t\t\tif (transition.kind === 'applied')\n\t\t\t\tyield* normalizeEffect(() => configuration.write(expectedGeneration, transition.state))\n\t\t\treturn transition\n\t\t})\n\t}\n\treturn {\n\t\tcallback(\n\t\t\texpectedGeneration: number,\n\t\t\tnativeStatus: WorkflowNativeStatus,\n\t\t\toutcome?: WorkflowOutcome<StateResult<Read>, StateFailure<Read>>,\n\t\t) {\n\t\t\treturn normalizeEffect(() => configuration.authorize()).pipe(\n\t\t\t\tEffect.flatMap(() => apply(expectedGeneration, nativeStatus, outcome)),\n\t\t\t)\n\t\t},\n\t\treconcile(expectedGeneration: number) {\n\t\t\treturn Effect.gen(function* () {\n\t\t\t\tyield* normalizeEffect(() => configuration.authorize())\n\t\t\t\tconst native = yield* normalizeEffect(() => configuration.nativeStatus())\n\t\t\t\tconst outcome = yield* normalizeEffect(() => configuration.receiptOutcome())\n\t\t\t\t// SAFETY: Native and Receipt are constrained to these exact host transport values.\n\t\t\t\treturn yield* apply(\n\t\t\t\t\texpectedGeneration,\n\t\t\t\t\tnative as WorkflowNativeStatus,\n\t\t\t\t\toutcome as WorkflowOutcome<StateResult<Read>, StateFailure<Read>> | undefined,\n\t\t\t\t)\n\t\t\t})\n\t\t},\n\t\tcancel(expectedGeneration: number) {\n\t\t\treturn Effect.gen(function* () {\n\t\t\t\tyield* normalizeEffect(() => configuration.authorize())\n\t\t\t\tconst current = yield* read()\n\t\t\t\tconst transition = yield* protocol(() => workflowCancel(current, expectedGeneration))\n\t\t\t\tif (transition.kind === 'applied' && current.status === 'running') {\n\t\t\t\t\tyield* normalizeEffect(() => configuration.cancelNative())\n\t\t\t\t\tyield* normalizeEffect(() => configuration.write(expectedGeneration, transition.state))\n\t\t\t\t}\n\t\t\t\treturn transition\n\t\t\t})\n\t\t},\n\t\trestart(expectedGeneration: number) {\n\t\t\treturn Effect.gen(function* () {\n\t\t\t\tyield* normalizeEffect(() => configuration.authorize())\n\t\t\t\tconst current = yield* read()\n\t\t\t\tif (current.generation !== expectedGeneration)\n\t\t\t\t\treturn { kind: 'ignored' as const, state: current }\n\t\t\t\tconst state = yield* protocol(() => workflowRestart(current))\n\t\t\t\tyield* normalizeEffect(() => configuration.restartNative())\n\t\t\t\tyield* normalizeEffect(() => configuration.write(expectedGeneration, state))\n\t\t\t\treturn { kind: 'applied' as const, state }\n\t\t\t})\n\t\t},\n\t}\n}\nfunction descriptor<Args>(\n\tvalue: WorkflowExecution<Args>,\n): Effect.Effect<WorkflowExecution<Args>, WorkflowBindingError> {\n\treturn protocol(() => workflowExecution(value))\n}\nfunction protocol<A>(evaluate: () => A): Effect.Effect<A, WorkflowBindingError> {\n\treturn Effect.try({\n\t\ttry: evaluate,\n\t\tcatch: (error) => {\n\t\t\tif (error instanceof WorkflowBindingError) return error\n\t\t\tthrow error\n\t\t},\n\t})\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region node_modules/@standard-schema/spec/dist/index.d.ts
|
|
2
|
+
/** The Standard Typed interface. This is a base type extended by other specs. */
|
|
3
|
+
interface StandardTypedV1<Input = unknown, Output = Input> {
|
|
4
|
+
/** The Standard properties. */
|
|
5
|
+
readonly "~standard": StandardTypedV1.Props<Input, Output>;
|
|
6
|
+
}
|
|
7
|
+
declare namespace StandardTypedV1 {
|
|
8
|
+
/** The Standard Typed properties interface. */
|
|
9
|
+
interface Props<Input = unknown, Output = Input> {
|
|
10
|
+
/** The version number of the standard. */
|
|
11
|
+
readonly version: 1;
|
|
12
|
+
/** The vendor name of the schema library. */
|
|
13
|
+
readonly vendor: string;
|
|
14
|
+
/** Inferred types associated with the schema. */
|
|
15
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
16
|
+
}
|
|
17
|
+
/** The Standard Typed types interface. */
|
|
18
|
+
interface Types<Input = unknown, Output = Input> {
|
|
19
|
+
/** The input type of the schema. */
|
|
20
|
+
readonly input: Input;
|
|
21
|
+
/** The output type of the schema. */
|
|
22
|
+
readonly output: Output;
|
|
23
|
+
}
|
|
24
|
+
/** Infers the input type of a Standard Typed. */
|
|
25
|
+
type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["input"];
|
|
26
|
+
/** Infers the output type of a Standard Typed. */
|
|
27
|
+
type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["output"];
|
|
28
|
+
}
|
|
29
|
+
/** The Standard Schema interface. */
|
|
30
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
31
|
+
/** The Standard Schema properties. */
|
|
32
|
+
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
|
|
33
|
+
}
|
|
34
|
+
declare namespace StandardSchemaV1 {
|
|
35
|
+
/** The Standard Schema properties interface. */
|
|
36
|
+
interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
|
|
37
|
+
/** Validates unknown input values. */
|
|
38
|
+
readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => Result<Output> | Promise<Result<Output>>;
|
|
39
|
+
}
|
|
40
|
+
/** The result interface of the validate function. */
|
|
41
|
+
type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
42
|
+
/** The result interface if validation succeeds. */
|
|
43
|
+
interface SuccessResult<Output> {
|
|
44
|
+
/** The typed output value. */
|
|
45
|
+
readonly value: Output;
|
|
46
|
+
/** A falsy value for `issues` indicates success. */
|
|
47
|
+
readonly issues?: undefined;
|
|
48
|
+
}
|
|
49
|
+
interface Options {
|
|
50
|
+
/** Explicit support for additional vendor-specific parameters, if needed. */
|
|
51
|
+
readonly libraryOptions?: Record<string, unknown> | undefined;
|
|
52
|
+
}
|
|
53
|
+
/** The result interface if validation fails. */
|
|
54
|
+
interface FailureResult {
|
|
55
|
+
/** The issues of failed validation. */
|
|
56
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
57
|
+
}
|
|
58
|
+
/** The issue interface of the failure output. */
|
|
59
|
+
interface Issue {
|
|
60
|
+
/** The error message of the issue. */
|
|
61
|
+
readonly message: string;
|
|
62
|
+
/** The path of the issue, if any. */
|
|
63
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
64
|
+
}
|
|
65
|
+
/** The path segment interface of the issue. */
|
|
66
|
+
interface PathSegment {
|
|
67
|
+
/** The key representing a path segment. */
|
|
68
|
+
readonly key: PropertyKey;
|
|
69
|
+
}
|
|
70
|
+
/** The Standard types interface. */
|
|
71
|
+
interface Types<Input = unknown, Output = Input> extends StandardTypedV1.Types<Input, Output> {}
|
|
72
|
+
/** Infers the input type of a Standard. */
|
|
73
|
+
type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
|
|
74
|
+
/** Infers the output type of a Standard. */
|
|
75
|
+
type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { StandardSchemaV1, StandardTypedV1 };
|
|
79
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { ReceiptProtectedWorkflowMutation, WorkflowBinding, WorkflowBindingError, WorkflowExecution, WorkflowHostState, WorkflowHostUpdate, WorkflowNativeStatus, WorkflowOutcome, WorkflowReceiptAdapter, WorkflowStepArguments, WorkflowStepOptions, WorkflowStepRunner, WorkflowTransition, receiptWorkflowMutation, runWorkflowStep, terminalWorkflowAttempt, workflowAttempt, workflowBinding, workflowCancel, workflowExecution, workflowReceiptInvocation, workflowRestart, workflowStatusBindings, workflowTransition } from "./modules/contracts/workflow.mjs";
|
|
2
|
+
export { type ReceiptProtectedWorkflowMutation, type WorkflowBinding, WorkflowBindingError, type WorkflowExecution, type WorkflowHostState, type WorkflowHostUpdate, type WorkflowNativeStatus, type WorkflowOutcome, type WorkflowReceiptAdapter, type WorkflowStepArguments, type WorkflowStepOptions, type WorkflowStepRunner, type WorkflowTransition, receiptWorkflowMutation, runWorkflowStep, terminalWorkflowAttempt, workflowAttempt, workflowBinding, workflowCancel, workflowExecution, workflowReceiptInvocation, workflowRestart, workflowStatusBindings, workflowTransition };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{WorkflowBindingError as e,receiptWorkflowMutation as t,runWorkflowStep as n,terminalWorkflowAttempt as r,workflowAttempt as i,workflowBinding as a,workflowCancel as o,workflowExecution as s,workflowReceiptInvocation as c,workflowRestart as l,workflowStatusBindings as u,workflowTransition as d}from"./modules/contracts/workflow.mjs";export{e as WorkflowBindingError,t as receiptWorkflowMutation,n as runWorkflowStep,r as terminalWorkflowAttempt,i as workflowAttempt,a as workflowBinding,o as workflowCancel,s as workflowExecution,c as workflowReceiptInvocation,l as workflowRestart,u as workflowStatusBindings,d as workflowTransition};
|
package/docs/architecture.md
CHANGED
|
@@ -13,7 +13,7 @@ convex/
|
|
|
13
13
|
convex.config.ts # component mounting — each app.use(x) exactly once
|
|
14
14
|
schema.ts # defineSchema(domainTables) — nothing else
|
|
15
15
|
functions.ts # createAuthFunctions(...) — the ONLY place raw builders appear
|
|
16
|
-
foundation.ts # const { Command, Query, observability } =
|
|
16
|
+
foundation.ts # const { Command, Query, observability } = createEffectFoundation(...)
|
|
17
17
|
approvals.ts # const approvals = new Approvals(components.approvals)
|
|
18
18
|
audit.ts # writeAuditEntry over convex-audit-log + admin readers
|
|
19
19
|
triggers.ts # the single Triggers registry + helper registrations
|
|
@@ -27,7 +27,7 @@ convex/
|
|
|
27
27
|
schema.ts # zodTable declaration (shapes only)
|
|
28
28
|
table.ts # indexes attached to <zodTable>.table
|
|
29
29
|
constants.ts # finite vocabularies (UPPER_SNAKE readonly tuples)
|
|
30
|
-
commands.ts #
|
|
30
|
+
commands.ts # Command({ context, operations }) with handlers inside command({...})
|
|
31
31
|
queries.ts # domain read logic
|
|
32
32
|
approval.ts # approvals.define(...) workflow definitions
|
|
33
33
|
*_functions.ts # internal callbacks (systemMutation/systemAction)
|
|
@@ -57,7 +57,7 @@ Every kit surface takes its policy as a parameter:
|
|
|
57
57
|
|
|
58
58
|
- `createAuthFunctions` receives the generated builders, `getAuthUser`,
|
|
59
59
|
`mapRole`, `verifyMembership`, `triggers`, `errors`.
|
|
60
|
-
- `
|
|
60
|
+
- `createEffectFoundation` receives `classifyError`, `writeAudit`, the `enabled` flag.
|
|
61
61
|
- `Approvals` receives only its component reference; host data crosses as
|
|
62
62
|
opaque validated strings.
|
|
63
63
|
|
package/docs/auth.md
CHANGED
|
@@ -64,6 +64,71 @@ export const {
|
|
|
64
64
|
Every function in the app is then built from these — raw `query`/`mutation`
|
|
65
65
|
imports from `_generated/server` appear **only** in this file.
|
|
66
66
|
|
|
67
|
+
## Shared Effect execution after authentication
|
|
68
|
+
|
|
69
|
+
Wrap the constructors returned by `createAuthFunctions` with
|
|
70
|
+
`effectZodApiBuilder` from `cvx-kit/effect`. The original constructor resolves
|
|
71
|
+
authentication, authorization, tenant policy, and the wrapped database before
|
|
72
|
+
the service provider runs. Keep that ordering when adding Effect; use the Zod
|
|
73
|
+
adapter for these custom constructors.
|
|
74
|
+
|
|
75
|
+
This factory accepts the configured `authMutation` from the setup above. The
|
|
76
|
+
actor service comes from trusted `ctx.actor`, with no actor or organization
|
|
77
|
+
argument from callers:
|
|
78
|
+
|
|
79
|
+
<!-- packed-effect-example -->
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import type { GenericDataModel } from 'convex/server'
|
|
83
|
+
import { createAuthFunctions } from 'cvx-kit/auth'
|
|
84
|
+
import { effectZodApiBuilder } from 'cvx-kit/effect'
|
|
85
|
+
import { Context, Effect } from 'effect'
|
|
86
|
+
import { z } from 'zod'
|
|
87
|
+
|
|
88
|
+
class Actor extends Context.Service<
|
|
89
|
+
Actor,
|
|
90
|
+
{ userId: string; organizationId: string; role: string }
|
|
91
|
+
>()('app/Actor') {}
|
|
92
|
+
|
|
93
|
+
export function createEffectAccountApi<DataModel extends GenericDataModel>(
|
|
94
|
+
baseAuthMutation: ReturnType<typeof createAuthFunctions<DataModel>>['authMutation'],
|
|
95
|
+
) {
|
|
96
|
+
const authMutation = effectZodApiBuilder(baseAuthMutation, {
|
|
97
|
+
services: (ctx) => Context.make(Actor, ctx.actor),
|
|
98
|
+
})
|
|
99
|
+
const currentActor = authMutation({
|
|
100
|
+
args: {},
|
|
101
|
+
returns: z.object({ userId: z.string(), organizationId: z.string() }),
|
|
102
|
+
handler: () =>
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const actor = yield* Actor
|
|
105
|
+
return { userId: actor.userId, organizationId: actor.organizationId }
|
|
106
|
+
}),
|
|
107
|
+
})
|
|
108
|
+
return { authMutation, currentActor }
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Call `createEffectAccountApi(authMutation)` with the constructor exported by
|
|
113
|
+
your `convex/functions.ts`, then export `currentActor` from the account API
|
|
114
|
+
module. The factory preserves the configured auth policy; it does not choose
|
|
115
|
+
an identity provider or membership authority.
|
|
116
|
+
|
|
117
|
+
Declare the provider once per constructor, then reuse the builder across
|
|
118
|
+
endpoints. Database services should capture the provider's `ctx.db`, so
|
|
119
|
+
trigger and RLS enforcement also applies to Effect-driven writes. Services
|
|
120
|
+
are created per invocation; do not cache an authenticated context in a module
|
|
121
|
+
singleton. Query providers receive readers; HTTP integrations belong in
|
|
122
|
+
actions.
|
|
123
|
+
|
|
124
|
+
The adapter runs the composed Effect within an invocation scope and awaits
|
|
125
|
+
finalizers before returning. Domain errors can be mapped with handler pipes
|
|
126
|
+
or shared `mapError`; they remain thrown failures at the native boundary so
|
|
127
|
+
mutation writes roll back. Native argument/result validators remain in place.
|
|
128
|
+
Effect's inferred server-side error and service channels are not a typed
|
|
129
|
+
transport error contract. See [the Effect guide](./effect.md) for complete
|
|
130
|
+
domain registries, error mapping, scoped services, and tracing.
|
|
131
|
+
|
|
67
132
|
## The constructor families
|
|
68
133
|
|
|
69
134
|
| Constructor | Visibility | Auth | Extra |
|
|
@@ -240,3 +305,9 @@ errors: { throw: (input) => { throw App.errors.from(input) } }
|
|
|
240
305
|
3. Reads go through `ctx.include(...)` with an explicit limit.
|
|
241
306
|
4. Use `ctx.actor` as the actor reference in audits and approvals — never
|
|
242
307
|
re-derive identity in a handler.
|
|
308
|
+
|
|
309
|
+
Optional request context can be added through the existing Effect API adapter's
|
|
310
|
+
`context` callback. Bind selected registries with `commands.withContext(value)`
|
|
311
|
+
and consume `ctx.commands.exec(name, input)` through the application's existing
|
|
312
|
+
function helper names. Keep dependencies local to each feature; explicit execution
|
|
313
|
+
and input-only command handlers remain supported. See [optional context injection](./command-context.md).
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Optional command context injection
|
|
2
|
+
|
|
3
|
+
Bind context once at a function boundary when it makes endpoint code clearer.
|
|
4
|
+
`withContext(value)` accepts whatever the registry's resolver accepts: a number,
|
|
5
|
+
a service bundle, a request context, or another bound command group. It returns
|
|
6
|
+
a small handle whose `exec(name, input)` still produces a lazy Effect. Binding
|
|
7
|
+
neither resolves context nor executes a command. Explicit
|
|
8
|
+
`commands.exec(name, input, context)` remains supported.
|
|
9
|
+
|
|
10
|
+
## Keep existing function names
|
|
11
|
+
|
|
12
|
+
Add context through `effectZodApiBuilder` around the existing custom constructor.
|
|
13
|
+
Export it under your application's ordinary `authMutation`, `adminQuery`,
|
|
14
|
+
`organizationMutation`, or other function name. These are application choices;
|
|
15
|
+
the library does not require a new helper family. The same option works on
|
|
16
|
+
`effectApiBuilder` for native builders. `services` is optional when your Effects
|
|
17
|
+
need no services.
|
|
18
|
+
|
|
19
|
+
The complete example below uses a custom authenticated mutation to make the
|
|
20
|
+
context flow standalone. A kit application can supply
|
|
21
|
+
`createAuthFunctions(...).authMutation` instead. Its authenticated, secured
|
|
22
|
+
context is constructed before the additions callback runs.
|
|
23
|
+
|
|
24
|
+
<!-- packed-effect-example -->
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Effect } from 'effect'
|
|
28
|
+
import { mutationGeneric } from 'convex/server'
|
|
29
|
+
import { zCustomMutation } from 'convex-helpers/server/zod4'
|
|
30
|
+
import { createEffectFoundation, effectZodApiBuilder } from 'cvx-kit/effect'
|
|
31
|
+
import { z } from 'zod'
|
|
32
|
+
|
|
33
|
+
const foundation = createEffectFoundation({
|
|
34
|
+
observability: {
|
|
35
|
+
enabled: false,
|
|
36
|
+
classifyError: () => ({ outcome: 'failed', errorCode: 'COMMAND_FAILED' }),
|
|
37
|
+
},
|
|
38
|
+
// Standalone demonstration only: production applications provide their audit writer.
|
|
39
|
+
writeAudit: () => undefined,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const notifications = foundation.Command({
|
|
43
|
+
context: (dependencies: { format: (message: string) => string }) => dependencies,
|
|
44
|
+
operations: ({ command }) => ({
|
|
45
|
+
format: command({
|
|
46
|
+
input: z.string(), result: z.string(), classification: 'business',
|
|
47
|
+
handler: (message, context) => context.format(message),
|
|
48
|
+
audit: () => null,
|
|
49
|
+
}),
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const tasks = foundation.Command({
|
|
54
|
+
context: (dependencies: {
|
|
55
|
+
nextNumber: () => number,
|
|
56
|
+
notifications: ReturnType<typeof notifications.withContext>,
|
|
57
|
+
}) => dependencies,
|
|
58
|
+
operations: ({ command }) => ({
|
|
59
|
+
create: command({
|
|
60
|
+
input: z.string(), result: z.string(), classification: 'business',
|
|
61
|
+
handler: (title, context) => context.notifications.exec(
|
|
62
|
+
'format', `${context.nextNumber()}:${title}`,
|
|
63
|
+
),
|
|
64
|
+
audit: () => null,
|
|
65
|
+
}),
|
|
66
|
+
}),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const baseAuthMutation = zCustomMutation(mutationGeneric, {
|
|
70
|
+
args: {},
|
|
71
|
+
input: async (ctx) => {
|
|
72
|
+
const identity = await ctx.auth.getUserIdentity()
|
|
73
|
+
if (!identity) throw Error('UNAUTHORIZED')
|
|
74
|
+
return { ctx: { actor: identity.subject }, args: {} }
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// In a real app, keep this composition in the tasks feature's functions module.
|
|
79
|
+
export const authMutation = effectZodApiBuilder(baseAuthMutation, {
|
|
80
|
+
context: (ctx) => {
|
|
81
|
+
const messages = notifications.withContext({
|
|
82
|
+
format: (message) => `${ctx.actor}:${message}`,
|
|
83
|
+
})
|
|
84
|
+
return {
|
|
85
|
+
tasks: tasks.withContext({ nextNumber: () => 42, notifications: messages }),
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export const createTask = authMutation({
|
|
91
|
+
args: { title: z.string() },
|
|
92
|
+
returns: z.string(),
|
|
93
|
+
handler: (ctx, input) => ctx.tasks.exec('create', input.title),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// An input-only command needs no domain dependencies.
|
|
97
|
+
const echo = foundation.Command({
|
|
98
|
+
context: () => undefined,
|
|
99
|
+
operations: ({ command }) => ({
|
|
100
|
+
echo: command({
|
|
101
|
+
input: z.string(), result: z.string(), classification: 'business',
|
|
102
|
+
handler: (input) => input, audit: () => null,
|
|
103
|
+
}),
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
export const echoEffect = echo.exec('echo', 'hello', undefined)
|
|
107
|
+
export const boundEchoEffect = echo.withContext(undefined).exec('echo', 'hello')
|
|
108
|
+
// A program can compose further work; the API adapter owns execution.
|
|
109
|
+
export const composedEcho = Effect.map(boundEchoEffect, (value) => value.length)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Context additions are optional and cannot replace existing fields, including
|
|
113
|
+
`db`, `auth`, or a custom constructor's authenticated actor. Type checking rejects
|
|
114
|
+
known collisions with declared fields. Runtime checking also protects inherited
|
|
115
|
+
properties (such as `toString`) and keys from broadly typed records or erased
|
|
116
|
+
types; those collisions cannot always be determined from the declared context
|
|
117
|
+
type. Additions can
|
|
118
|
+
return a plain record of own properties, or a Promise or Effect of that record.
|
|
119
|
+
Wrap class instances and services in named properties, such as `{ pricingService }`,
|
|
120
|
+
rather than returning an instance as the additions object. The adapter provides services first, then
|
|
121
|
+
runs initialization and the handler in the same invocation scope. Missing
|
|
122
|
+
initializer services are rejected when registering the endpoint, just like
|
|
123
|
+
missing handler services. Initialization failures use the configured error
|
|
124
|
+
projection and release acquired resources.
|
|
125
|
+
|
|
126
|
+
## Applications with 25–50 entities
|
|
127
|
+
|
|
128
|
+
Organize by feature or workflow rather than making every entity a property of a
|
|
129
|
+
single global context. A tasks helper can bind tasks and notifications; a billing
|
|
130
|
+
helper can bind billing and pricing. Each registry declares its own narrow
|
|
131
|
+
resolver input. Share stable service implementations at module scope, and build
|
|
132
|
+
request-dependent handles within the context callback.
|
|
133
|
+
|
|
134
|
+
Keep shared identity or tenant information small. Inject a business function or
|
|
135
|
+
service only into the features that use it. Pass bound commands only where a
|
|
136
|
+
workflow calls them; avoid mutual command dependencies. Bindings hold references
|
|
137
|
+
and do not initialize the referenced registry. Large dependency graphs are
|
|
138
|
+
application architecture choices, not a required library container.
|
|
139
|
+
|
|
140
|
+
If a feature needs no injected commands, omit `context` additions. If an endpoint
|
|
141
|
+
benefits from explicit execution, pass its context directly. An input-only
|
|
142
|
+
handler can ignore domain context, but permission and audit policies may still
|
|
143
|
+
need the original trusted invocation context. Preserve those policies when
|
|
144
|
+
choosing the registry resolver and binding input.
|
|
145
|
+
|
|
146
|
+
Nested calls return Effects and compose in the current program. Do not call
|
|
147
|
+
`Effect.runPromise` inside command handlers; the function adapter runs the
|
|
148
|
+
complete program once. Command schema checks, permissions, audit, and completion
|
|
149
|
+
remain active for each nested command. Keep database mutation work in a mutation
|
|
150
|
+
boundary so Convex can roll it back if execution or result validation fails.
|