cvx-kit 0.1.7 → 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/README.md +59 -43
- package/dist/components/foundation/client.d.mts +8 -0
- package/dist/components/foundation/client.mjs.map +1 -1
- package/dist/modules/effect/api-runtime.d.mts +14 -4
- package/dist/modules/effect/api-runtime.mjs +1 -1
- package/dist/modules/effect/api-runtime.mjs.map +1 -1
- package/dist/modules/effect/api-zod.d.mts +5 -5
- package/dist/modules/effect/api-zod.mjs.map +1 -1
- package/dist/modules/effect/api.d.mts +6 -6
- package/dist/modules/effect/api.mjs.map +1 -1
- package/dist/modules/effect/command.d.mts +6 -2
- package/dist/modules/effect/command.mjs +1 -1
- package/dist/modules/effect/command.mjs.map +1 -1
- package/dist/modules/effect/query.d.mts +5 -1
- package/dist/modules/effect/query.mjs +1 -1
- package/dist/modules/effect/query.mjs.map +1 -1
- package/docs/architecture.md +3 -3
- package/docs/auth.md +10 -4
- package/docs/command-context.md +150 -0
- package/docs/commands-legacy.md +511 -0
- package/docs/commands.md +113 -451
- package/docs/conventions.md +15 -5
- package/docs/crud.md +4 -0
- package/docs/domain-operations.md +1 -1
- package/docs/effect.md +14 -3
- package/docs/llms.md +69 -45
- package/docs/migration.md +1 -1
- package/docs/upgrading.md +9 -0
- package/package.json +1 -1
- package/scripts/test-packed-effect.mjs +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.mjs","names":[],"sources":["../../../src/modules/effect/command.ts"],"sourcesContent":["import { Effect, Exit } from 'effect'\nimport type { AuditEntryInput } from '../../components/foundation/client'\nimport type { ContractSchema } from '../contracts/contract'\nimport {\n\tselectOperation,\n\ttype SelectedOperation,\n\ttype OperationPublicResult,\n} from '../contracts/exposure'\nimport { decodeEffectContract } from './schema'\n// oxlint-disable-next-line cvx/component-boundaries -- SAFETY: Internal optional adapter shares the Effect-free Foundation lifecycle and types without entering the Convex component deployment graph.\nimport {\n\tCommandPermissionError,\n\texecuteCommandLifecycle,\n\ttype ExecutionKind,\n\ttype LifecycleAlgebra,\n\ttype LifecyclePreparation,\n} from '../../components/foundation/modules/command/lifecycle'\n// oxlint-disable-next-line cvx/component-boundaries -- SAFETY: Internal optional adapter shares the Effect-free Foundation lifecycle and types without entering the Convex component deployment graph.\nimport type { Observability } from '../../components/foundation/modules/observability/observability'\nimport { normalizeEffect, observationFailure } from './normalize'\nimport {\n\teffectOperationFactory,\n\ttype CallbackError,\n\ttype CallbackRequirements,\n\ttype EffectValue,\n\ttype EffectOperationArgument,\n\ttype EffectOperationInput,\n\ttype EffectOperationHandlerResult,\n\ttype EffectPreparation,\n\ttype EffectOperationResult,\n\ttype EffectOperationError,\n\ttype EffectOperationRequirements,\n\ttype CheckedEffectOperation,\n\ttype UnwrappedDefaultError,\n\ttype UnwrappedDefaultRequirements,\n\ttype ValidatedEffectOperations,\n} from './operation'\n\ntype Supported<Value> = Value | PromiseLike<Value> | Effect.Effect<Value, unknown, unknown>\n// Heterogeneous storage constraints preserve concrete callbacks in inferred definitions.\n// oxlint-disable-next-line anti-slop/no-unknown-returns -- Internal callback storage; selected definitions retain concrete return contracts.\ntype Callback = (...arguments_: never[]) => Supported<unknown>\n// oxlint-disable-next-line anti-slop/no-unknown-returns -- Resolver constraint captures its concrete return in Resolve; it never exposes this storage type at exec.\ntype Resolver = (host: never) => Supported<unknown>\ntype HostOf<Resolve extends Resolver> = Parameters<Resolve>[0]\ntype DomainOf<Resolve extends Resolver> = EffectValue<ReturnType<Resolve>>\ntype Operation = {\n\tinput: ContractSchema\n\tresult: ContractSchema\n\tclassification: string\n\thandler: Callback\n\taudit: Callback\n\tguard?: Callback\n\tprepare?: Callback\n\tmiddleware?: Callback\n\tpermission?: string\n\taggregates?: readonly string[]\n\treplayResult?: ContractSchema\n}\ntype Registry<Context> = Readonly<\n\tRecord<\n\t\tstring,\n\t\tPick<Operation, 'input' | 'result' | 'classification' | 'handler' | 'audit'> &\n\t\t\tCheckedEffectOperation<Context>\n\t>\n>\nexport type EffectCommandDependencies = {\n\tobservability: Observability\n\tcheckPermission?: (\n\t\thost: never,\n\t\tinput: { permission: string; operation: string },\n\t) => Supported<void>\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Audit writer storage; its concrete return/error/services remain in Dependencies.\n\twriteAudit: (host: never, entry: AuditEntryInput) => Supported<unknown>\n}\ntype HostParameter<Policy> = [Exclude<Policy, undefined>] extends [never]\n\t? unknown\n\t: // oxlint-disable-next-line anti-slop/no-unknown-returns -- Conditional parameter-extraction predicate, not an executable callback contract.\n\t\tExclude<Policy, undefined> extends (...arguments_: infer Arguments) => Supported<unknown>\n\t\t? Arguments extends []\n\t\t\t? unknown\n\t\t\t: Arguments[0]\n\t\t: unknown\ntype PolicyHost<Dependencies extends EffectCommandDependencies> = HostParameter<\n\tDependencies['writeAudit']\n> &\n\tHostParameter<Dependencies['checkPermission']>\ntype HostCompatible<Resolve extends Resolver, Dependencies extends EffectCommandDependencies> = [\n\tHostOf<Resolve>,\n] extends [PolicyHost<Dependencies>]\n\t? unknown\n\t: { context: never }\ntype Factory<Resolve extends Resolver, Guard> = ReturnType<\n\ttypeof effectOperationFactory<\n\t\tDomainOf<Resolve>,\n\t\tRecord<never, never>,\n\t\tNoInfer<CallbackError<Guard>>,\n\t\tNoInfer<CallbackRequirements<Guard>>\n\t>\n>\nexport type EffectCommandConfiguration<\n\tResolve extends Resolver,\n\tOperations extends Registry<DomainOf<Resolve>>,\n\tGuard,\n> = {\n\tcontext: Resolve\n\toperations: (\n\t\tdefine: Factory<Resolve, Guard>,\n\t) => Operations & NoInfer<ValidatedEffectOperations<Operations>>\n\tdefaults?: { guard?: (context: DomainOf<Resolve>) => Guard }\n}\ntype ErrorOf<Definition, Resolve, Dependencies, Guard> =\n\t| EffectOperationError<Definition>\n\t| CallbackError<Resolve | Dependencies[keyof Dependencies]>\n\t| UnwrappedDefaultError<Definition, Guard>\ntype RequirementsOf<Definition, Resolve, Dependencies, Guard> =\n\t| EffectOperationRequirements<Definition>\n\t| CallbackRequirements<Resolve | Dependencies[keyof Dependencies]>\n\t| UnwrappedDefaultRequirements<Definition, Guard>\ninterface EffectExecution extends ExecutionKind {\n\treadonly type: Effect.Effect<this['value'], unknown, unknown>\n}\nconst effectLifecycle: LifecycleAlgebra<EffectExecution> = {\n\tsucceed: Effect.succeed,\n\tsuspend: Effect.suspend,\n\tflatMap: Effect.flatMap,\n}\ntype RuntimeOperation<Context, Input, Raw, Result> = {\n\tinput: ContractSchema\n\tresult: ContractSchema\n\tclassification: string\n\tpermission?: string\n\taggregates?: readonly string[]\n\treplayResult?: ContractSchema\n\thandler: (input: Input, context: Context) => Supported<Raw>\n\tguard?: (context: Context, input: Input) => Supported<void>\n\tprepare?: (\n\t\tcontext: Context,\n\t\tinput: Input,\n\t) => Supported<EffectPreparation<Result, unknown, unknown>>\n\taudit: (\n\t\tresolution: { command: Input; result: Result },\n\t\tcontext: Context,\n\t) => Supported<Omit<AuditEntryInput, 'classification'> | null>\n\tmiddleware?: (input: {\n\t\toperation: string\n\t\tcommand: Input\n\t\tinput: Input\n\t\tcontext: Context\n\t\tnext: (options?: { context?: object }) => Effect.Effect<Raw, unknown, unknown>\n\t}) => Supported<Raw>\n}\n\n/** Internal registry constructor: the public Foundation binds host policies once. */\nexport function createEffectCommand<\n\tResolve extends Resolver,\n\tconst Operations extends Registry<DomainOf<Resolve>>,\n\tconst Dependencies extends EffectCommandDependencies,\n\tGuard = never,\n>(\n\tconfiguration: EffectCommandConfiguration<Resolve, Operations, Guard> &\n\t\tDependencies &\n\t\tHostCompatible<Resolve, Dependencies>,\n) {\n\tconst operations: Operations =\n\t\tconfiguration.operations(\n\t\t\teffectOperationFactory<\n\t\t\t\tDomainOf<Resolve>,\n\t\t\t\tRecord<never, never>,\n\t\t\t\tCallbackError<Guard>,\n\t\t\t\tCallbackRequirements<Guard>\n\t\t\t>(),\n\t\t)\n\treturn {\n\t\texpose<const Owner extends symbol, const Key extends Extract<keyof Operations, string>>(\n\t\t\towner: Owner,\n\t\t\toperation: Key,\n\t\t): SelectedOperation<\n\t\t\tOwner,\n\t\t\tKey,\n\t\t\tOperations[Key]['input'],\n\t\t\tOperationPublicResult<Operations[Key]>\n\t\t> {\n\t\t\tif (!Object.hasOwn(operations, operation))\n\t\t\t\tthrow Error('The selected command operation is not configured')\n\t\t\treturn selectOperation(owner, 'mutation', operation, operations[operation])\n\t\t},\n\t\texec<Key extends Extract<keyof Operations, string>>(\n\t\t\toperation: Key,\n\t\t\tinput: EffectOperationArgument<Operations[Key]>,\n\t\t\thost: HostOf<Resolve>,\n\t\t): Effect.Effect<\n\t\t\tEffectOperationResult<Operations[Key]>,\n\t\t\tErrorOf<Operations[Key], Resolve, Dependencies, Guard>,\n\t\t\tRequirementsOf<Operations[Key], Resolve, Dependencies, Guard>\n\t\t> {\n\t\t\tconst execution = Effect.suspend(() => {\n\t\t\t\tif (!Object.prototype.hasOwnProperty.call(operations, operation))\n\t\t\t\t\tthrow Error('The selected command operation is not configured')\n\t\t\t\t// SAFETY: this helper checked selected callbacks against their parsed schemas and domain context.\n\t\t\t\tconst definition = operations[operation] as Operations[Key] &\n\t\t\t\t\tRuntimeOperation<\n\t\t\t\t\t\tDomainOf<Resolve>,\n\t\t\t\t\t\tEffectOperationInput<Operations[Key]>,\n\t\t\t\t\t\tEffectOperationHandlerResult<Operations[Key]>,\n\t\t\t\t\t\tEffectOperationResult<Operations[Key]>\n\t\t\t\t\t>\n\t\t\t\t// SAFETY: selected command input was parsed by its own paired schema.\n\t\t\t\treturn Effect.flatMap(decodeEffectContract(definition.input, input), (decoded) => {\n\t\t\t\t\t// SAFETY: the selected operation's decoder owns this concrete domain input.\n\t\t\t\t\tconst command = decoded as EffectOperationInput<Operations[Key]>\n\t\t\t\t\tconst observation = configuration.observability.start({\n\t\t\t\t\t\toperation,\n\t\t\t\t\t\tclassification: definition.classification,\n\t\t\t\t\t})\n\t\t\t\t\t// SAFETY: resolver identity owns its concrete invocation return and host parameter.\n\t\t\t\t\tconst resolve = configuration.context as (host: HostOf<Resolve>) => ReturnType<Resolve>\n\t\t\t\t\treturn normalizeEffect(() => resolve(host)).pipe(\n\t\t\t\t\t\tEffect.flatMap((context) => {\n\t\t\t\t\t\t\tconst lifecycle = executeCommandLifecycle<\n\t\t\t\t\t\t\t\tEffectExecution,\n\t\t\t\t\t\t\t\tDomainOf<Resolve>,\n\t\t\t\t\t\t\t\tEffectOperationInput<Operations[Key]>,\n\t\t\t\t\t\t\t\tEffectOperationHandlerResult<Operations[Key]>,\n\t\t\t\t\t\t\t\tEffectOperationResult<Operations[Key]>\n\t\t\t\t\t\t\t>(effectLifecycle, {\n\t\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\t\tclassification: definition.classification,\n\t\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\t\tcommand,\n\t\t\t\t\t\t\t\tpermission:\n\t\t\t\t\t\t\t\t\tdefinition.permission === undefined\n\t\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t\t: () =>\n\t\t\t\t\t\t\t\t\t\t\t\tnormalizeEffect(() => {\n\t\t\t\t\t\t\t\t\t\t\t\t\tif (!configuration.checkPermission)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tthrow new CommandPermissionError(operation)\n\t\t\t\t\t\t\t\t\t\t\t\t\t// SAFETY: HostCompatible proves this invocation satisfies the configured policy host.\n\t\t\t\t\t\t\t\t\t\t\t\t\treturn configuration.checkPermission(host as never, {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tpermission: definition.permission!,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t}).pipe(Effect.asVoid),\n\t\t\t\t\t\t\t\tprepare: () =>\n\t\t\t\t\t\t\t\t\tnormalizeEffect(() => definition.prepare?.(context, command)).pipe(\n\t\t\t\t\t\t\t\t\t\tEffect.map((preparation) => {\n\t\t\t\t\t\t\t\t\t\t\tif (preparation?.kind !== 'execute') return preparation\n\t\t\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\t\t\tkind: 'execute',\n\t\t\t\t\t\t\t\t\t\t\t\tcomplete: preparation.complete\n\t\t\t\t\t\t\t\t\t\t\t\t\t? (result) =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnormalizeEffect(() => preparation.complete!(result)).pipe(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tEffect.asVoid,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t\t\t} satisfies LifecyclePreparation<\n\t\t\t\t\t\t\t\t\t\t\t\tEffectExecution,\n\t\t\t\t\t\t\t\t\t\t\t\tEffectOperationResult<Operations[Key]>\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t// SAFETY: the selected final replay contract owns this result; operation types retain its E/R.\n\t\t\t\t\t\t\t\tdecodeReplay: (value) =>\n\t\t\t\t\t\t\t\t\tdecodeEffectContract(\n\t\t\t\t\t\t\t\t\t\tdefinition.replayResult ?? definition.result,\n\t\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t\t) as Effect.Effect<EffectOperationResult<Operations[Key]>, unknown, unknown>,\n\t\t\t\t\t\t\t\tmiddleware: definition.middleware\n\t\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t\t(input_) =>\n\t\t\t\t\t\t\t\t\t\t\t\tnormalizeCommandResult(() =>\n\t\t\t\t\t\t\t\t\t\t\t\t\tdefinition.middleware!({ ...input_, input: command }),\n\t\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t\t: [],\n\t\t\t\t\t\t\t\tdefaultGuard: (enriched) =>\n\t\t\t\t\t\t\t\t\tnormalizeEffect(() => configuration.defaults?.guard?.(enriched)).pipe(\n\t\t\t\t\t\t\t\t\t\tEffect.asVoid,\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tguard: (enriched) =>\n\t\t\t\t\t\t\t\t\tnormalizeEffect(() => definition.guard?.(enriched, command)).pipe(Effect.asVoid),\n\t\t\t\t\t\t\t\trun: (enriched) =>\n\t\t\t\t\t\t\t\t\tnormalizeCommandResult(() => definition.handler(command, enriched)),\n\t\t\t\t\t\t\t\t// SAFETY: the selected contract owns the decoded result; the public operation retains decoder E/R.\n\t\t\t\t\t\t\t\tdecodeResult: (value) =>\n\t\t\t\t\t\t\t\t\tdecodeEffectContract(definition.result, value) as Effect.Effect<\n\t\t\t\t\t\t\t\t\t\tEffectOperationResult<Operations[Key]>,\n\t\t\t\t\t\t\t\t\t\tunknown,\n\t\t\t\t\t\t\t\t\t\tunknown\n\t\t\t\t\t\t\t\t\t>,\n\t\t\t\t\t\t\t\taudit: (result) =>\n\t\t\t\t\t\t\t\t\tnormalizeEffect(() => definition.audit({ command, result }, context)),\n\t\t\t\t\t\t\t\twriteAudit: (entry) =>\n\t\t\t\t\t\t\t\t\tnormalizeEffect(() => {\n\t\t\t\t\t\t\t\t\t\t// SAFETY: HostCompatible proves this invocation satisfies the configured writer host.\n\t\t\t\t\t\t\t\t\t\treturn configuration.writeAudit(host as never, entry)\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\taggregates: definition.aggregates,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\treturn lifecycle\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tEffect.onExit((exit) =>\n\t\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\t\tif (Exit.isSuccess(exit)) observation.completed()\n\t\t\t\t\t\t\t\telse observation.failed(observationFailure(exit.cause))\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t)\n\t\t\t\t})\n\t\t\t})\n\t\t\t// SAFETY: dispatch selects exactly this operation; callbacks preserve the concrete definition/policy E/R.\n\t\t\treturn execution as Effect.Effect<\n\t\t\t\tEffectOperationResult<Operations[Key]>,\n\t\t\t\tErrorOf<Operations[Key], Resolve, Dependencies, Guard>,\n\t\t\t\tRequirementsOf<Operations[Key], Resolve, Dependencies, Guard>\n\t\t\t>\n\t\t},\n\t}\n}\n\n/** Bind trusted application policies without exposing a loose public command kernel. */\nexport function bindEffectCommand<const Dependencies extends EffectCommandDependencies>(\n\tdependencies: Dependencies,\n) {\n\treturn function command<\n\t\tResolve extends Resolver,\n\t\tconst Operations extends Registry<DomainOf<Resolve>>,\n\t\tGuard = never,\n\t>(\n\t\tconfiguration: EffectCommandConfiguration<Resolve, Operations, Guard> &\n\t\t\tHostCompatible<Resolve, Dependencies>,\n\t): ReturnType<typeof createEffectCommand<Resolve, Operations, Dependencies, Guard>> {\n\t\treturn createEffectCommand<Resolve, Operations, Dependencies, Guard>({\n\t\t\t...configuration,\n\t\t\t...dependencies,\n\t\t})\n\t}\n}\n\nfunction normalizeCommandResult<Value>(\n\tinvoke: () => Supported<Value>,\n): Effect.Effect<Value, unknown, unknown> {\n\t// SAFETY: selected definition helper checks the callback's normalized success against the schema input.\n\treturn normalizeEffect(invoke) as Effect.Effect<Value, unknown, unknown>\n}\n"],"mappings":"6ZA0HA,MAAM,EAAqD,CAC1D,QAAS,EAAO,QAChB,QAAS,EAAO,QAChB,QAAS,EAAO,OACjB,EA4BA,SAAgB,EAMf,EAGC,CACD,IAAM,EACL,EAAc,WACb,EAKE,CACH,EACD,MAAO,CACN,OACC,EACA,EAMC,CACD,GAAI,CAAC,OAAO,OAAO,EAAY,CAAS,EACvC,MAAM,MAAM,kDAAkD,EAC/D,OAAO,EAAgB,EAAO,WAAY,EAAW,EAAW,EAAU,CAC3E,EACA,KACC,EACA,EACA,EAKC,CAoHD,OAnHkB,EAAO,YAAc,CACtC,GAAI,CAAC,OAAO,UAAU,eAAe,KAAK,EAAY,CAAS,EAC9D,MAAM,MAAM,kDAAkD,EAE/D,IAAM,EAAa,EAAW,GAQ9B,OAAO,EAAO,QAAQ,EAAqB,EAAW,MAAO,CAAK,EAAI,GAAY,CAEjF,IAAM,EAAU,EACV,EAAc,EAAc,cAAc,MAAM,CACrD,YACA,eAAgB,EAAW,cAC5B,CAAC,EAEK,EAAU,EAAc,QAC9B,OAAO,MAAsB,EAAQ,CAAI,CAAC,CAAC,CAAC,KAC3C,EAAO,QAAS,GACG,EAMhB,EAAiB,CAClB,YACA,eAAgB,EAAW,eAC3B,UACA,UACA,WACC,EAAW,aAAe,IAAA,GACvB,IAAA,OAEA,MAAsB,CACrB,GAAI,CAAC,EAAc,gBAClB,MAAM,IAAI,EAAuB,CAAS,EAE3C,OAAO,EAAc,gBAAgB,EAAe,CACnD,WAAY,EAAW,WACvB,WACD,CAAC,CACF,CAAC,CAAC,CAAC,KAAK,EAAO,MAAM,EACzB,YACC,MAAsB,EAAW,UAAU,EAAS,CAAO,CAAC,CAAC,CAAC,KAC7D,EAAO,IAAK,GACP,GAAa,OAAS,UACnB,CACN,KAAM,UACN,SAAU,EAAY,SAClB,GACD,MAAsB,EAAY,SAAU,CAAM,CAAC,CAAC,CAAC,KACpD,EAAO,MACR,EACA,IAAA,EACJ,EAT4C,CAa5C,CACF,EAED,aAAe,GACd,EACC,EAAW,cAAgB,EAAW,OACtC,CACD,EACD,WAAY,EAAW,WACpB,CACC,GACA,MACC,EAAW,WAAY,CAAE,GAAG,EAAQ,MAAO,CAAQ,CAAC,CACrD,CACF,EACC,CAAC,EACJ,aAAe,GACd,MAAsB,EAAc,UAAU,QAAQ,CAAQ,CAAC,CAAC,CAAC,KAChE,EAAO,MACR,EACD,MAAQ,GACP,MAAsB,EAAW,QAAQ,EAAU,CAAO,CAAC,CAAC,CAAC,KAAK,EAAO,MAAM,EAChF,IAAM,GACL,MAA6B,EAAW,QAAQ,EAAS,CAAQ,CAAC,EAEnE,aAAe,GACd,EAAqB,EAAW,OAAQ,CAAK,EAK9C,MAAQ,GACP,MAAsB,EAAW,MAAM,CAAE,UAAS,QAAO,EAAG,CAAO,CAAC,EACrE,WAAa,GACZ,MAEQ,EAAc,WAAW,EAAe,CAAK,CACpD,EACF,WAAY,EAAW,UACxB,CACe,CACf,EACD,EAAO,OAAQ,GACd,EAAO,SAAW,CACb,EAAK,UAAU,CAAI,EAAG,EAAY,UAAU,EAC3C,EAAY,OAAO,EAAmB,EAAK,KAAK,CAAC,CACvD,CAAC,CACF,CACD,CACD,CAAC,CACF,CAEe,CAKhB,CACD,CACD,CAGA,SAAgB,EACf,EACC,CACD,OAAO,SAKN,EAEmF,CACnF,OAAO,EAA8D,CACpE,GAAG,EACH,GAAG,CACJ,CAAC,CACF,CACD,CAEA,SAAS,EACR,EACyC,CAEzC,OAAO,EAAgB,CAAM,CAC9B"}
|
|
1
|
+
{"version":3,"file":"command.mjs","names":[],"sources":["../../../src/modules/effect/command.ts"],"sourcesContent":["import { Effect, Exit } from 'effect'\nimport type { AuditEntryInput } from '../../components/foundation/client'\nimport type { ContractSchema } from '../contracts/contract'\nimport {\n\tselectOperation,\n\ttype SelectedOperation,\n\ttype OperationPublicResult,\n} from '../contracts/exposure'\nimport { decodeEffectContract } from './schema'\n// oxlint-disable-next-line cvx/component-boundaries -- SAFETY: Internal optional adapter shares the Effect-free Foundation lifecycle and types without entering the Convex component deployment graph.\nimport {\n\tCommandPermissionError,\n\texecuteCommandLifecycle,\n\ttype ExecutionKind,\n\ttype LifecycleAlgebra,\n\ttype LifecyclePreparation,\n} from '../../components/foundation/modules/command/lifecycle'\n// oxlint-disable-next-line cvx/component-boundaries -- SAFETY: Internal optional adapter shares the Effect-free Foundation lifecycle and types without entering the Convex component deployment graph.\nimport type { Observability } from '../../components/foundation/modules/observability/observability'\nimport { normalizeEffect, observationFailure } from './normalize'\nimport {\n\teffectOperationFactory,\n\ttype CallbackError,\n\ttype CallbackRequirements,\n\ttype EffectValue,\n\ttype EffectOperationArgument,\n\ttype EffectOperationInput,\n\ttype EffectOperationHandlerResult,\n\ttype EffectPreparation,\n\ttype EffectOperationResult,\n\ttype EffectOperationError,\n\ttype EffectOperationRequirements,\n\ttype CheckedEffectOperation,\n\ttype UnwrappedDefaultError,\n\ttype UnwrappedDefaultRequirements,\n\ttype ValidatedEffectOperations,\n} from './operation'\n\ntype Supported<Value> = Value | PromiseLike<Value> | Effect.Effect<Value, unknown, unknown>\n// Heterogeneous storage constraints preserve concrete callbacks in inferred definitions.\n// oxlint-disable-next-line anti-slop/no-unknown-returns -- Internal callback storage; selected definitions retain concrete return contracts.\ntype Callback = (...arguments_: never[]) => Supported<unknown>\n// oxlint-disable-next-line anti-slop/no-unknown-returns -- Resolver constraint captures its concrete return in Resolve; it never exposes this storage type at exec.\ntype Resolver = (host: never) => Supported<unknown>\ntype HostOf<Resolve extends Resolver> = Parameters<Resolve>[0]\ntype DomainOf<Resolve extends Resolver> = EffectValue<ReturnType<Resolve>>\ntype Operation = {\n\tinput: ContractSchema\n\tresult: ContractSchema\n\tclassification: string\n\thandler: Callback\n\taudit: Callback\n\tguard?: Callback\n\tprepare?: Callback\n\tmiddleware?: Callback\n\tpermission?: string\n\taggregates?: readonly string[]\n\treplayResult?: ContractSchema\n}\ntype Registry<Context> = Readonly<\n\tRecord<\n\t\tstring,\n\t\tPick<Operation, 'input' | 'result' | 'classification' | 'handler' | 'audit'> &\n\t\t\tCheckedEffectOperation<Context>\n\t>\n>\nexport type EffectCommandDependencies = {\n\tobservability: Observability\n\tcheckPermission?: (\n\t\thost: never,\n\t\tinput: { permission: string; operation: string },\n\t) => Supported<void>\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Audit writer storage; its concrete return/error/services remain in Dependencies.\n\twriteAudit: (host: never, entry: AuditEntryInput) => Supported<unknown>\n}\ntype HostParameter<Policy> = [Exclude<Policy, undefined>] extends [never]\n\t? unknown\n\t: // oxlint-disable-next-line anti-slop/no-unknown-returns -- Conditional parameter-extraction predicate, not an executable callback contract.\n\t\tExclude<Policy, undefined> extends (...arguments_: infer Arguments) => Supported<unknown>\n\t\t? Arguments extends []\n\t\t\t? unknown\n\t\t\t: Arguments[0]\n\t\t: unknown\ntype PolicyHost<Dependencies extends EffectCommandDependencies> = HostParameter<\n\tDependencies['writeAudit']\n> &\n\tHostParameter<Dependencies['checkPermission']>\ntype HostCompatible<Resolve extends Resolver, Dependencies extends EffectCommandDependencies> = [\n\tHostOf<Resolve>,\n] extends [PolicyHost<Dependencies>]\n\t? unknown\n\t: { context: never }\ntype Factory<Resolve extends Resolver, Guard> = ReturnType<\n\ttypeof effectOperationFactory<\n\t\tDomainOf<Resolve>,\n\t\tRecord<never, never>,\n\t\tNoInfer<CallbackError<Guard>>,\n\t\tNoInfer<CallbackRequirements<Guard>>\n\t>\n>\nexport type EffectCommandConfiguration<\n\tResolve extends Resolver,\n\tOperations extends Registry<DomainOf<Resolve>>,\n\tGuard,\n> = {\n\tcontext: Resolve\n\toperations: (\n\t\tdefine: Factory<Resolve, Guard>,\n\t) => Operations & NoInfer<ValidatedEffectOperations<Operations>>\n\tdefaults?: { guard?: (context: DomainOf<Resolve>) => Guard }\n}\ntype ErrorOf<Definition, Resolve, Dependencies, Guard> =\n\t| EffectOperationError<Definition>\n\t| CallbackError<Resolve | Dependencies[keyof Dependencies]>\n\t| UnwrappedDefaultError<Definition, Guard>\ntype RequirementsOf<Definition, Resolve, Dependencies, Guard> =\n\t| EffectOperationRequirements<Definition>\n\t| CallbackRequirements<Resolve | Dependencies[keyof Dependencies]>\n\t| UnwrappedDefaultRequirements<Definition, Guard>\ninterface EffectExecution extends ExecutionKind {\n\treadonly type: Effect.Effect<this['value'], unknown, unknown>\n}\nconst effectLifecycle: LifecycleAlgebra<EffectExecution> = {\n\tsucceed: Effect.succeed,\n\tsuspend: Effect.suspend,\n\tflatMap: Effect.flatMap,\n}\ntype RuntimeOperation<Context, Input, Raw, Result> = {\n\tinput: ContractSchema\n\tresult: ContractSchema\n\tclassification: string\n\tpermission?: string\n\taggregates?: readonly string[]\n\treplayResult?: ContractSchema\n\thandler: (input: Input, context: Context) => Supported<Raw>\n\tguard?: (context: Context, input: Input) => Supported<void>\n\tprepare?: (\n\t\tcontext: Context,\n\t\tinput: Input,\n\t) => Supported<EffectPreparation<Result, unknown, unknown>>\n\taudit: (\n\t\tresolution: { command: Input; result: Result },\n\t\tcontext: Context,\n\t) => Supported<Omit<AuditEntryInput, 'classification'> | null>\n\tmiddleware?: (input: {\n\t\toperation: string\n\t\tcommand: Input\n\t\tinput: Input\n\t\tcontext: Context\n\t\tnext: (options?: { context?: object }) => Effect.Effect<Raw, unknown, unknown>\n\t}) => Supported<Raw>\n}\n\n/** Internal registry constructor: the public Foundation binds host policies once. */\nexport function createEffectCommand<\n\tResolve extends Resolver,\n\tconst Operations extends Registry<DomainOf<Resolve>>,\n\tconst Dependencies extends EffectCommandDependencies,\n\tGuard = never,\n>(\n\tconfiguration: EffectCommandConfiguration<Resolve, Operations, Guard> &\n\t\tDependencies &\n\t\tHostCompatible<Resolve, Dependencies>,\n) {\n\tconst operations: Operations =\n\t\tconfiguration.operations(\n\t\t\teffectOperationFactory<\n\t\t\t\tDomainOf<Resolve>,\n\t\t\t\tRecord<never, never>,\n\t\t\t\tCallbackError<Guard>,\n\t\t\t\tCallbackRequirements<Guard>\n\t\t\t>(),\n\t\t)\n\tconst exec = <Key extends Extract<keyof Operations, string>>(\n\t\toperation: Key,\n\t\tinput: EffectOperationArgument<Operations[Key]>,\n\t\thost: HostOf<Resolve>,\n\t): Effect.Effect<\n\t\tEffectOperationResult<Operations[Key]>,\n\t\tErrorOf<Operations[Key], Resolve, Dependencies, Guard>,\n\t\tRequirementsOf<Operations[Key], Resolve, Dependencies, Guard>\n\t> => {\n\t\tconst execution = Effect.suspend(() => {\n\t\t\tif (!Object.prototype.hasOwnProperty.call(operations, operation))\n\t\t\t\tthrow Error('The selected command operation is not configured')\n\t\t\t// SAFETY: this helper checked selected callbacks against their parsed schemas and domain context.\n\t\t\tconst definition = operations[operation] as Operations[Key] &\n\t\t\t\tRuntimeOperation<\n\t\t\t\t\tDomainOf<Resolve>,\n\t\t\t\t\tEffectOperationInput<Operations[Key]>,\n\t\t\t\t\tEffectOperationHandlerResult<Operations[Key]>,\n\t\t\t\t\tEffectOperationResult<Operations[Key]>\n\t\t\t\t>\n\t\t\t// SAFETY: selected command input was parsed by its own paired schema.\n\t\t\treturn Effect.flatMap(decodeEffectContract(definition.input, input), (decoded) => {\n\t\t\t\t// SAFETY: the selected operation's decoder owns this concrete domain input.\n\t\t\t\tconst command = decoded as EffectOperationInput<Operations[Key]>\n\t\t\t\tconst observation = configuration.observability.start({\n\t\t\t\t\toperation,\n\t\t\t\t\tclassification: definition.classification,\n\t\t\t\t})\n\t\t\t\t// SAFETY: resolver identity owns its concrete invocation return and host parameter.\n\t\t\t\tconst resolve = configuration.context as (host: HostOf<Resolve>) => ReturnType<Resolve>\n\t\t\t\treturn normalizeEffect(() => resolve(host)).pipe(\n\t\t\t\t\tEffect.flatMap((context) => {\n\t\t\t\t\t\tconst lifecycle = executeCommandLifecycle<\n\t\t\t\t\t\t\tEffectExecution,\n\t\t\t\t\t\t\tDomainOf<Resolve>,\n\t\t\t\t\t\t\tEffectOperationInput<Operations[Key]>,\n\t\t\t\t\t\t\tEffectOperationHandlerResult<Operations[Key]>,\n\t\t\t\t\t\t\tEffectOperationResult<Operations[Key]>\n\t\t\t\t\t\t>(effectLifecycle, {\n\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\tclassification: definition.classification,\n\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\tcommand,\n\t\t\t\t\t\t\tpermission:\n\t\t\t\t\t\t\t\tdefinition.permission === undefined\n\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t: () =>\n\t\t\t\t\t\t\t\t\t\t\tnormalizeEffect(() => {\n\t\t\t\t\t\t\t\t\t\t\t\tif (!configuration.checkPermission)\n\t\t\t\t\t\t\t\t\t\t\t\t\tthrow new CommandPermissionError(operation)\n\t\t\t\t\t\t\t\t\t\t\t\t// SAFETY: HostCompatible proves this invocation satisfies the configured policy host.\n\t\t\t\t\t\t\t\t\t\t\t\treturn configuration.checkPermission(host as never, {\n\t\t\t\t\t\t\t\t\t\t\t\t\tpermission: definition.permission!,\n\t\t\t\t\t\t\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t}).pipe(Effect.asVoid),\n\t\t\t\t\t\t\tprepare: () =>\n\t\t\t\t\t\t\t\tnormalizeEffect(() => definition.prepare?.(context, command)).pipe(\n\t\t\t\t\t\t\t\t\tEffect.map((preparation) => {\n\t\t\t\t\t\t\t\t\t\tif (preparation?.kind !== 'execute') return preparation\n\t\t\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\t\t\tkind: 'execute',\n\t\t\t\t\t\t\t\t\t\t\tcomplete: preparation.complete\n\t\t\t\t\t\t\t\t\t\t\t\t? (result) =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tnormalizeEffect(() => preparation.complete!(result)).pipe(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tEffect.asVoid,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\t\t\t\t\t\t} satisfies LifecyclePreparation<\n\t\t\t\t\t\t\t\t\t\t\tEffectExecution,\n\t\t\t\t\t\t\t\t\t\t\tEffectOperationResult<Operations[Key]>\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t// SAFETY: the selected final replay contract owns this result; operation types retain its E/R.\n\t\t\t\t\t\t\tdecodeReplay: (value) =>\n\t\t\t\t\t\t\t\tdecodeEffectContract(\n\t\t\t\t\t\t\t\t\tdefinition.replayResult ?? definition.result,\n\t\t\t\t\t\t\t\t\tvalue,\n\t\t\t\t\t\t\t\t) as Effect.Effect<EffectOperationResult<Operations[Key]>, unknown, unknown>,\n\t\t\t\t\t\t\tmiddleware: definition.middleware\n\t\t\t\t\t\t\t\t? [\n\t\t\t\t\t\t\t\t\t\t(input_) =>\n\t\t\t\t\t\t\t\t\t\t\tnormalizeCommandResult(() =>\n\t\t\t\t\t\t\t\t\t\t\t\tdefinition.middleware!({ ...input_, input: command }),\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t]\n\t\t\t\t\t\t\t\t: [],\n\t\t\t\t\t\t\tdefaultGuard: (enriched) =>\n\t\t\t\t\t\t\t\tnormalizeEffect(() => configuration.defaults?.guard?.(enriched)).pipe(\n\t\t\t\t\t\t\t\t\tEffect.asVoid,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tguard: (enriched) =>\n\t\t\t\t\t\t\t\tnormalizeEffect(() => definition.guard?.(enriched, command)).pipe(Effect.asVoid),\n\t\t\t\t\t\t\trun: (enriched) =>\n\t\t\t\t\t\t\t\tnormalizeCommandResult(() => definition.handler(command, enriched)),\n\t\t\t\t\t\t\t// SAFETY: the selected contract owns the decoded result; the public operation retains decoder E/R.\n\t\t\t\t\t\t\tdecodeResult: (value) =>\n\t\t\t\t\t\t\t\tdecodeEffectContract(definition.result, value) as Effect.Effect<\n\t\t\t\t\t\t\t\t\tEffectOperationResult<Operations[Key]>,\n\t\t\t\t\t\t\t\t\tunknown,\n\t\t\t\t\t\t\t\t\tunknown\n\t\t\t\t\t\t\t\t>,\n\t\t\t\t\t\t\taudit: (result) =>\n\t\t\t\t\t\t\t\tnormalizeEffect(() => definition.audit({ command, result }, context)),\n\t\t\t\t\t\t\twriteAudit: (entry) =>\n\t\t\t\t\t\t\t\tnormalizeEffect(() => {\n\t\t\t\t\t\t\t\t\t// SAFETY: HostCompatible proves this invocation satisfies the configured writer host.\n\t\t\t\t\t\t\t\t\treturn configuration.writeAudit(host as never, entry)\n\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\taggregates: definition.aggregates,\n\t\t\t\t\t\t})\n\t\t\t\t\t\treturn lifecycle\n\t\t\t\t\t}),\n\t\t\t\t\tEffect.onExit((exit) =>\n\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\tif (Exit.isSuccess(exit)) observation.completed()\n\t\t\t\t\t\t\telse observation.failed(observationFailure(exit.cause))\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t})\n\t\t})\n\t\t// SAFETY: dispatch selects exactly this operation; callbacks preserve the concrete definition/policy E/R.\n\t\treturn execution as Effect.Effect<\n\t\t\tEffectOperationResult<Operations[Key]>,\n\t\t\tErrorOf<Operations[Key], Resolve, Dependencies, Guard>,\n\t\t\tRequirementsOf<Operations[Key], Resolve, Dependencies, Guard>\n\t\t>\n\t}\n\treturn {\n\t\t/** Bind invocation context without resolving it or running an operation. */\n\t\twithContext(context: HostOf<Resolve>) {\n\t\t\treturn {\n\t\t\t\texec: <Key extends Extract<keyof Operations, string>>(\n\t\t\t\t\toperation: Key,\n\t\t\t\t\tinput: EffectOperationArgument<Operations[Key]>,\n\t\t\t\t) => exec(operation, input, context),\n\t\t\t}\n\t\t},\n\t\texpose<const Owner extends symbol, const Key extends Extract<keyof Operations, string>>(\n\t\t\towner: Owner,\n\t\t\toperation: Key,\n\t\t): SelectedOperation<\n\t\t\tOwner,\n\t\t\tKey,\n\t\t\tOperations[Key]['input'],\n\t\t\tOperationPublicResult<Operations[Key]>\n\t\t> {\n\t\t\tif (!Object.hasOwn(operations, operation))\n\t\t\t\tthrow Error('The selected command operation is not configured')\n\t\t\treturn selectOperation(owner, 'mutation', operation, operations[operation])\n\t\t},\n\t\texec,\n\t}\n}\n\n/** Bind trusted application policies without exposing a loose public command kernel. */\nexport function bindEffectCommand<const Dependencies extends EffectCommandDependencies>(\n\tdependencies: Dependencies,\n) {\n\treturn function command<\n\t\tResolve extends Resolver,\n\t\tconst Operations extends Registry<DomainOf<Resolve>>,\n\t\tGuard = never,\n\t>(\n\t\tconfiguration: EffectCommandConfiguration<Resolve, Operations, Guard> &\n\t\t\tHostCompatible<Resolve, Dependencies>,\n\t): ReturnType<typeof createEffectCommand<Resolve, Operations, Dependencies, Guard>> {\n\t\treturn createEffectCommand<Resolve, Operations, Dependencies, Guard>({\n\t\t\t...configuration,\n\t\t\t...dependencies,\n\t\t})\n\t}\n}\n\nfunction normalizeCommandResult<Value>(\n\tinvoke: () => Supported<Value>,\n): Effect.Effect<Value, unknown, unknown> {\n\t// SAFETY: selected definition helper checks the callback's normalized success against the schema input.\n\treturn normalizeEffect(invoke) as Effect.Effect<Value, unknown, unknown>\n}\n"],"mappings":"6ZA0HA,MAAM,EAAqD,CAC1D,QAAS,EAAO,QAChB,QAAS,EAAO,QAChB,QAAS,EAAO,OACjB,EA4BA,SAAgB,EAMf,EAGC,CACD,IAAM,EACL,EAAc,WACb,EAKE,CACH,EACK,GACL,EACA,EACA,IAMkB,EAAO,YAAc,CACtC,GAAI,CAAC,OAAO,UAAU,eAAe,KAAK,EAAY,CAAS,EAC9D,MAAM,MAAM,kDAAkD,EAE/D,IAAM,EAAa,EAAW,GAQ9B,OAAO,EAAO,QAAQ,EAAqB,EAAW,MAAO,CAAK,EAAI,GAAY,CAEjF,IAAM,EAAU,EACV,EAAc,EAAc,cAAc,MAAM,CACrD,YACA,eAAgB,EAAW,cAC5B,CAAC,EAEK,EAAU,EAAc,QAC9B,OAAO,MAAsB,EAAQ,CAAI,CAAC,CAAC,CAAC,KAC3C,EAAO,QAAS,GACG,EAMhB,EAAiB,CAClB,YACA,eAAgB,EAAW,eAC3B,UACA,UACA,WACC,EAAW,aAAe,IAAA,GACvB,IAAA,OAEA,MAAsB,CACrB,GAAI,CAAC,EAAc,gBAClB,MAAM,IAAI,EAAuB,CAAS,EAE3C,OAAO,EAAc,gBAAgB,EAAe,CACnD,WAAY,EAAW,WACvB,WACD,CAAC,CACF,CAAC,CAAC,CAAC,KAAK,EAAO,MAAM,EACzB,YACC,MAAsB,EAAW,UAAU,EAAS,CAAO,CAAC,CAAC,CAAC,KAC7D,EAAO,IAAK,GACP,GAAa,OAAS,UACnB,CACN,KAAM,UACN,SAAU,EAAY,SAClB,GACD,MAAsB,EAAY,SAAU,CAAM,CAAC,CAAC,CAAC,KACpD,EAAO,MACR,EACA,IAAA,EACJ,EAT4C,CAa5C,CACF,EAED,aAAe,GACd,EACC,EAAW,cAAgB,EAAW,OACtC,CACD,EACD,WAAY,EAAW,WACpB,CACC,GACA,MACC,EAAW,WAAY,CAAE,GAAG,EAAQ,MAAO,CAAQ,CAAC,CACrD,CACF,EACC,CAAC,EACJ,aAAe,GACd,MAAsB,EAAc,UAAU,QAAQ,CAAQ,CAAC,CAAC,CAAC,KAChE,EAAO,MACR,EACD,MAAQ,GACP,MAAsB,EAAW,QAAQ,EAAU,CAAO,CAAC,CAAC,CAAC,KAAK,EAAO,MAAM,EAChF,IAAM,GACL,MAA6B,EAAW,QAAQ,EAAS,CAAQ,CAAC,EAEnE,aAAe,GACd,EAAqB,EAAW,OAAQ,CAAK,EAK9C,MAAQ,GACP,MAAsB,EAAW,MAAM,CAAE,UAAS,QAAO,EAAG,CAAO,CAAC,EACrE,WAAa,GACZ,MAEQ,EAAc,WAAW,EAAe,CAAK,CACpD,EACF,WAAY,EAAW,UACxB,CACe,CACf,EACD,EAAO,OAAQ,GACd,EAAO,SAAW,CACb,EAAK,UAAU,CAAI,EAAG,EAAY,UAAU,EAC3C,EAAY,OAAO,EAAmB,EAAK,KAAK,CAAC,CACvD,CAAC,CACF,CACD,CACD,CAAC,CACF,CAEe,EAMhB,MAAO,CAEN,YAAY,EAA0B,CACrC,MAAO,CACN,MACC,EACA,IACI,EAAK,EAAW,EAAO,CAAO,CACpC,CACD,EACA,OACC,EACA,EAMC,CACD,GAAI,CAAC,OAAO,OAAO,EAAY,CAAS,EACvC,MAAM,MAAM,kDAAkD,EAC/D,OAAO,EAAgB,EAAO,WAAY,EAAW,EAAW,EAAU,CAC3E,EACA,MACD,CACD,CAGA,SAAgB,EACf,EACC,CACD,OAAO,SAKN,EAEmF,CACnF,OAAO,EAA8D,CACpE,GAAG,EACH,GAAG,CACJ,CAAC,CACF,CACD,CAEA,SAAS,EACR,EACyC,CAEzC,OAAO,EAAgB,CAAM,CAC9B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContractSchema } from "../contracts/contract.mjs";
|
|
1
|
+
import { ContractOutput, ContractSchema } from "../contracts/contract.mjs";
|
|
2
2
|
import { OperationPublicResult, SelectedOperation } from "../contracts/exposure.mjs";
|
|
3
3
|
import { Observability, ObservabilityOptions } from "../../components/foundation/modules/observability/observability.mjs";
|
|
4
4
|
import { CallbackError, CallbackRequirements, CheckedEffectOperation, EffectOperationArgument, EffectOperationError, EffectOperationRequirements, EffectOperationResult, EffectValue, UnwrappedDefaultError, UnwrappedDefaultRequirements, ValidatedEffectOperations, effectOperationFactory } from "./operation.mjs";
|
|
@@ -38,6 +38,10 @@ declare function createEffectQuery<Host, ContextReturned, const Operations exten
|
|
|
38
38
|
}): {
|
|
39
39
|
exec: <const Key extends OperationKey<Operations>>(operation: Key, input: EffectOperationArgument<Operations[Key]>, host: Host) => Effect.Effect<EffectOperationResult<Operations[Key]>, EffectOperationError<Operations[Key]> | CallbackError<ContextReturned | PermissionReturned> | UnwrappedDefaultError<Operations[Key], GuardReturned>, EffectOperationRequirements<Operations[Key]> | CallbackRequirements<ContextReturned | PermissionReturned> | UnwrappedDefaultRequirements<Operations[Key], GuardReturned>>;
|
|
40
40
|
expose: <const Owner extends symbol, const Key extends OperationKey<Operations>>(owner: Owner, operation: Key) => SelectedOperation<Owner, Key, Operations[Key]['input'], OperationPublicResult<Operations[Key]>>;
|
|
41
|
+
/** Bind invocation context without resolving it or running an operation. */
|
|
42
|
+
withContext(context: Host): {
|
|
43
|
+
exec: <const Key extends OperationKey<Operations>>(operation: Key, input: EffectOperationArgument<Operations[Key]>) => Effect.Effect<ContractOutput<Operations[Key]["result"]>, UnwrappedDefaultError<Operations[Key], GuardReturned> | CallbackError<ContextReturned | PermissionReturned> | EffectOperationError<Operations[Key]>, UnwrappedDefaultRequirements<Operations[Key], GuardReturned> | CallbackRequirements<ContextReturned | PermissionReturned> | EffectOperationRequirements<Operations[Key]>>;
|
|
44
|
+
};
|
|
41
45
|
};
|
|
42
46
|
type Policy<Dependencies> = 'checkPermission' extends keyof Dependencies ? Dependencies['checkPermission'] : never;
|
|
43
47
|
type CallbackHost<Callback> = Callback extends ((host: infer Host, input: never) => unknown) ? Host : never;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{selectOperation as e}from"../contracts/exposure.mjs";import{effectOperationFactory as t}from"./operation.mjs";import{Observability as n}from"../../components/foundation/modules/observability/observability.mjs";import{decodeEffectContract as r}from"./schema.mjs";import{normalizeEffect as i,observationFailure as a}from"./normalize.mjs";import{Effect as o,Exit as s}from"effect";var c=class extends Error{code=`QUERY_MIDDLEWARE_NEXT_REUSED`;name=`QueryMiddlewareError`;constructor(){super(`A query middleware called next() more than once`)}},l=class extends Error{code=`QUERY_OPERATION_NOT_CONFIGURED`;name=`QueryConfigurationError`;constructor(){super(`The selected query operation is not configured`)}},u=class extends Error{code=`QUERY_PERMISSION_CHECKER_MISSING`;name=`QueryPermissionError`;constructor(){super(`Query permission requires an injected permission checker`)}};function d(d){let f=t(),p=d.operations(f),m=d.observability instanceof n?d.observability:new n(d.observability??{enabled:!1,classifyError:()=>({outcome:`failed`,errorCode:`UNEXPECTED`})});function h(e,t,n){return o.suspend(()=>{if(!Object.prototype.hasOwnProperty.call(p,e))return o.die(new l);let f=p[e];return o.flatMap(r(f.input,t),t=>{let l=m.start({operation:e,classification:f.classification??`query`});return o.gen(function*(){if(f.permission!==void 0){let t=d.checkPermission;if(!t)return yield*o.die(new u);yield*i(()=>t(n,{permission:f.permission,operation:e}))}let a=yield*i(()=>d.context(n)),s={...d.defaults?.metadata,...f.metadata},l=e=>o.gen(function*(){let n=d.defaults?.guard;return n&&(yield*i(()=>n(e))),f.guard&&(yield*i(()=>f.guard(e,t))),yield*i(()=>f.handler(t,e))}),p=!1,m=f.middleware,h=m?yield*i(()=>m({operation:e,input:t,command:t,context:a,metadata:s,next:e=>o.suspend(()=>p?o.die(new c):(p=!0,l(e?.context?Object.assign({},a,e.context):a)))})):yield*l(a);return yield*r(f.result,h)}).pipe(o.onExit(e=>o.sync(()=>{s.isSuccess(e)?l.completed():l.failed(a(e.cause))})))})})}function g(t,n){if(!Object.hasOwn(p,n))throw new l;return e(t,`query`,n,p[n])}return{exec:h,expose:g}}function f(e){return function(t){let n=e.checkPermission;return d({...t,observability:e.observability,checkPermission:e.checkPermission?n:void 0})}}export{f as bindEffectQuery,d as createEffectQuery};
|
|
1
|
+
import{selectOperation as e}from"../contracts/exposure.mjs";import{effectOperationFactory as t}from"./operation.mjs";import{Observability as n}from"../../components/foundation/modules/observability/observability.mjs";import{decodeEffectContract as r}from"./schema.mjs";import{normalizeEffect as i,observationFailure as a}from"./normalize.mjs";import{Effect as o,Exit as s}from"effect";var c=class extends Error{code=`QUERY_MIDDLEWARE_NEXT_REUSED`;name=`QueryMiddlewareError`;constructor(){super(`A query middleware called next() more than once`)}},l=class extends Error{code=`QUERY_OPERATION_NOT_CONFIGURED`;name=`QueryConfigurationError`;constructor(){super(`The selected query operation is not configured`)}},u=class extends Error{code=`QUERY_PERMISSION_CHECKER_MISSING`;name=`QueryPermissionError`;constructor(){super(`Query permission requires an injected permission checker`)}};function d(d){let f=t(),p=d.operations(f),m=d.observability instanceof n?d.observability:new n(d.observability??{enabled:!1,classifyError:()=>({outcome:`failed`,errorCode:`UNEXPECTED`})});function h(e,t,n){return o.suspend(()=>{if(!Object.prototype.hasOwnProperty.call(p,e))return o.die(new l);let f=p[e];return o.flatMap(r(f.input,t),t=>{let l=m.start({operation:e,classification:f.classification??`query`});return o.gen(function*(){if(f.permission!==void 0){let t=d.checkPermission;if(!t)return yield*o.die(new u);yield*i(()=>t(n,{permission:f.permission,operation:e}))}let a=yield*i(()=>d.context(n)),s={...d.defaults?.metadata,...f.metadata},l=e=>o.gen(function*(){let n=d.defaults?.guard;return n&&(yield*i(()=>n(e))),f.guard&&(yield*i(()=>f.guard(e,t))),yield*i(()=>f.handler(t,e))}),p=!1,m=f.middleware,h=m?yield*i(()=>m({operation:e,input:t,command:t,context:a,metadata:s,next:e=>o.suspend(()=>p?o.die(new c):(p=!0,l(e?.context?Object.assign({},a,e.context):a)))})):yield*l(a);return yield*r(f.result,h)}).pipe(o.onExit(e=>o.sync(()=>{s.isSuccess(e)?l.completed():l.failed(a(e.cause))})))})})}function g(t,n){if(!Object.hasOwn(p,n))throw new l;return e(t,`query`,n,p[n])}return{exec:h,expose:g,withContext(e){return{exec:(t,n)=>h(t,n,e)}}}}function f(e){return function(t){let n=e.checkPermission;return d({...t,observability:e.observability,checkPermission:e.checkPermission?n:void 0})}}export{f as bindEffectQuery,d as createEffectQuery};
|
|
2
2
|
//# sourceMappingURL=query.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.mjs","names":[],"sources":["../../../src/modules/effect/query.ts"],"sourcesContent":["import { Effect, Exit } from 'effect'\nimport type { ContractSchema } from '../contracts/contract'\nimport {\n\tselectOperation,\n\ttype SelectedOperation,\n\ttype OperationPublicResult,\n} from '../contracts/exposure'\nimport { decodeEffectContract } from './schema'\n// oxlint-disable-next-line cvx/component-boundaries -- SAFETY: Internal optional adapter shares the Effect-free Foundation lifecycle and types without entering the Convex component deployment graph.\nimport {\n\tObservability,\n\ttype ObservabilityOptions,\n} from '../../components/foundation/modules/observability/observability'\nimport { normalizeEffect, observationFailure } from './normalize'\nimport {\n\teffectOperationFactory,\n\ttype CallbackError,\n\ttype CallbackRequirements,\n\ttype EffectOperationArgument,\n\ttype EffectOperationError,\n\ttype EffectOperationRequirements,\n\ttype EffectOperationResult,\n\ttype EffectValue,\n\ttype CheckedEffectOperation,\n\ttype UnwrappedDefaultError,\n\ttype UnwrappedDefaultRequirements,\n\ttype ValidatedEffectOperations,\n} from './operation'\n\ntype Supported<Value> = Value | PromiseLike<Value> | Effect.Effect<Value, unknown, unknown>\ntype QueryDefinition = {\n\tinput: ContractSchema\n\tresult: ContractSchema\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Internal heterogeneous storage; selected schemas restore the concrete result at exec.\n\thandler: (input: never, context: never) => unknown\n\tclassification?: string\n\tpermission?: string\n\tmetadata?: object\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Internal hook storage retains the concrete definition separately.\n\tguard?: (context: never, input: never) => unknown\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Internal hook storage retains the concrete definition separately.\n\tmiddleware?: (input: never) => unknown\n}\ntype QueryRegistry<Context> = Readonly<\n\tRecord<\n\t\tstring,\n\t\tPick<QueryDefinition, 'input' | 'result' | 'handler'> &\n\t\t\tCheckedEffectOperation<Context> & {\n\t\t\t\tprepare?: never\n\t\t\t\taudit?: never\n\t\t\t\taggregates?: never\n\t\t\t\treplayResult?: never\n\t\t\t}\n\t>\n>\ntype OperationKey<Operations> = Extract<keyof Operations, string>\n\nclass QueryMiddlewareError extends Error {\n\treadonly code = 'QUERY_MIDDLEWARE_NEXT_REUSED'\n\treadonly name = 'QueryMiddlewareError'\n\tconstructor() {\n\t\tsuper('A query middleware called next() more than once')\n\t}\n}\nclass QueryConfigurationError extends Error {\n\treadonly code = 'QUERY_OPERATION_NOT_CONFIGURED'\n\treadonly name = 'QueryConfigurationError'\n\tconstructor() {\n\t\tsuper('The selected query operation is not configured')\n\t}\n}\nclass QueryPermissionError extends Error {\n\treadonly code = 'QUERY_PERMISSION_CHECKER_MISSING'\n\treadonly name = 'QueryPermissionError'\n\tconstructor() {\n\t\tsuper('Query permission requires an injected permission checker')\n\t}\n}\n\n/** Injectable domain queries compose lazily; the API chooses native function kind. */\nexport function createEffectQuery<\n\tHost,\n\tContextReturned,\n\tconst Operations extends QueryRegistry<EffectValue<ContextReturned>>,\n\tPermissionReturned = never,\n\tGuardReturned extends Supported<void> = never,\n>(configuration: {\n\tcontext: (host: Host) => ContextReturned\n\toperations: (\n\t\tdefinitions: ReturnType<\n\t\t\ttypeof effectOperationFactory<\n\t\t\t\tEffectValue<ContextReturned>,\n\t\t\t\tRecord<never, never>,\n\t\t\t\tNoInfer<CallbackError<GuardReturned>>,\n\t\t\t\tNoInfer<CallbackRequirements<GuardReturned>>\n\t\t\t>\n\t\t>,\n\t) => Operations & NoInfer<ValidatedEffectOperations<Operations>>\n\tcheckPermission?: (\n\t\thost: Host,\n\t\tinput: { permission: string; operation: string },\n\t) => PermissionReturned\n\tdefaults?: { metadata?: object; guard?: (context: EffectValue<ContextReturned>) => GuardReturned }\n\tobservability?: ObservabilityOptions | Observability\n}) {\n\tconst definitions = effectOperationFactory<\n\t\tEffectValue<ContextReturned>,\n\t\tRecord<never, never>,\n\t\tCallbackError<GuardReturned>,\n\t\tCallbackRequirements<GuardReturned>\n\t>()\n\tconst operations: Operations = configuration.operations(definitions)\n\tconst observability =\n\t\tconfiguration.observability instanceof Observability\n\t\t\t? configuration.observability\n\t\t\t: new Observability(\n\t\t\t\t\tconfiguration.observability ?? {\n\t\t\t\t\t\tenabled: false,\n\t\t\t\t\t\tclassifyError: () => ({ outcome: 'failed', errorCode: 'UNEXPECTED' }),\n\t\t\t\t\t},\n\t\t\t\t)\n\n\tfunction exec<const Key extends OperationKey<Operations>>(\n\t\toperation: Key,\n\t\tinput: EffectOperationArgument<Operations[Key]>,\n\t\thost: Host,\n\t): Effect.Effect<\n\t\tEffectOperationResult<Operations[Key]>,\n\t\t| EffectOperationError<Operations[Key]>\n\t\t| CallbackError<ContextReturned | PermissionReturned>\n\t\t| UnwrappedDefaultError<Operations[Key], GuardReturned>,\n\t\t| EffectOperationRequirements<Operations[Key]>\n\t\t| CallbackRequirements<ContextReturned | PermissionReturned>\n\t\t| UnwrappedDefaultRequirements<Operations[Key], GuardReturned>\n\t> {\n\t\tconst execution = Effect.suspend(() => {\n\t\t\tif (!Object.prototype.hasOwnProperty.call(operations, operation)) {\n\t\t\t\treturn Effect.die(new QueryConfigurationError())\n\t\t\t}\n\t\t\t// SAFETY: operation helpers check hooks before this heterogeneous storage seam.\n\t\t\tconst definition: QueryDefinition = operations[operation]\n\t\t\treturn Effect.flatMap(decodeEffectContract(definition.input, input), (parsedInput) => {\n\t\t\t\tconst observation = observability.start({\n\t\t\t\t\toperation,\n\t\t\t\t\tclassification: definition.classification ?? 'query',\n\t\t\t\t})\n\t\t\t\tconst run = Effect.gen(function* () {\n\t\t\t\t\tif (definition.permission !== undefined) {\n\t\t\t\t\t\tconst check = configuration.checkPermission\n\t\t\t\t\t\tif (!check) return yield* Effect.die(new QueryPermissionError())\n\t\t\t\t\t\tyield* normalizeEffect(() =>\n\t\t\t\t\t\t\tcheck(host, { permission: definition.permission!, operation }),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tconst context = yield* normalizeEffect(() => configuration.context(host))\n\t\t\t\t\tconst metadata = { ...configuration.defaults?.metadata, ...definition.metadata }\n\t\t\t\t\tconst core = (current: EffectValue<ContextReturned>) =>\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tconst guard = configuration.defaults?.guard\n\t\t\t\t\t\t\tif (guard) yield* normalizeEffect(() => guard(current))\n\t\t\t\t\t\t\tif (definition.guard)\n\t\t\t\t\t\t\t\tyield* normalizeEffect(() => {\n\t\t\t\t\t\t\t\t\t// SAFETY: selected schemas and context own the stored guard parameters.\n\t\t\t\t\t\t\t\t\treturn definition.guard!(current as never, parsedInput as never)\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\treturn yield* normalizeEffect(() => {\n\t\t\t\t\t\t\t\t// SAFETY: selected schemas and context own the stored handler parameters.\n\t\t\t\t\t\t\t\treturn definition.handler(parsedInput as never, current as never)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t\tlet usedNext = false\n\t\t\t\t\tconst middleware = definition.middleware\n\t\t\t\t\tconst output = middleware\n\t\t\t\t\t\t? yield* normalizeEffect(() => {\n\t\t\t\t\t\t\t\t// SAFETY: the selected helper checked this middleware's input and next contract.\n\t\t\t\t\t\t\t\treturn middleware({\n\t\t\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\t\t\tinput: parsedInput,\n\t\t\t\t\t\t\t\t\tcommand: parsedInput,\n\t\t\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\t\t\tmetadata,\n\t\t\t\t\t\t\t\t\tnext: (options?: { context?: object }) =>\n\t\t\t\t\t\t\t\t\t\tEffect.suspend(() => {\n\t\t\t\t\t\t\t\t\t\t\tif (usedNext) return Effect.die(new QueryMiddlewareError())\n\t\t\t\t\t\t\t\t\t\t\tusedNext = true\n\t\t\t\t\t\t\t\t\t\t\treturn core(\n\t\t\t\t\t\t\t\t\t\t\t\toptions?.context ? Object.assign({}, context, options.context) : context,\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t} as never)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: yield* core(context)\n\t\t\t\t\treturn yield* decodeEffectContract(definition.result, output)\n\t\t\t\t})\n\t\t\t\treturn run.pipe(\n\t\t\t\t\tEffect.onExit((exit) =>\n\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\tif (Exit.isSuccess(exit)) observation.completed()\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\tobservation.failed(observationFailure(exit.cause))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t})\n\t\t})\n\t\t// SAFETY: selected schemas own A; concrete callbacks and the wrapped middleware\n\t\t// own E/R. No interpreter or runner executes this Effect before the caller does.\n\t\treturn execution as ReturnType<typeof exec<Key>>\n\t}\n\tfunction expose<const Owner extends symbol, const Key extends OperationKey<Operations>>(\n\t\towner: Owner,\n\t\toperation: Key,\n\t): SelectedOperation<\n\t\tOwner,\n\t\tKey,\n\t\tOperations[Key]['input'],\n\t\tOperationPublicResult<Operations[Key]>\n\t> {\n\t\tif (!Object.hasOwn(operations, operation)) throw new QueryConfigurationError()\n\t\treturn selectOperation(owner, 'query', operation, operations[operation])\n\t}\n\treturn { exec, expose }\n}\n\ntype Policy<Dependencies> = 'checkPermission' extends keyof Dependencies\n\t? Dependencies['checkPermission']\n\t: never\n// oxlint-disable-next-line anti-slop/no-unknown-returns -- Conditional type extracts the host; no unknown result is exposed by the registry.\ntype CallbackHost<Callback> = Callback extends (host: infer Host, input: never) => unknown\n\t? Host\n\t: never\ntype CallbackReturned<Callback> = Callback extends (...arguments_: never[]) => infer Returned\n\t? Returned\n\t: never\ntype PolicyHost<Dependencies> = [CallbackHost<Policy<Dependencies>>] extends [never]\n\t? unknown\n\t: CallbackHost<Policy<Dependencies>>\ntype PolicyReturned<Dependencies> = CallbackReturned<Policy<Dependencies>>\nexport type EffectQueryDependencies = {\n\tobservability: Observability\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Host policy storage is erased; the bound generic dependency retains its concrete E/R.\n\tcheckPermission?: (host: never, input: { permission: string; operation: string }) => unknown\n}\n\n/** The public Foundation binds trusted application policies once. */\nexport function bindEffectQuery<const Dependencies extends EffectQueryDependencies>(\n\tdependencies: Dependencies,\n) {\n\treturn function query<\n\t\tHost extends PolicyHost<Dependencies>,\n\t\tContextReturned,\n\t\tconst Operations extends QueryRegistry<EffectValue<ContextReturned>>,\n\t\tGuardReturned extends Supported<void> = never,\n\t>(configuration: {\n\t\tcontext: (host: Host) => ContextReturned\n\t\toperations: (\n\t\t\tdefinitions: ReturnType<\n\t\t\t\ttypeof effectOperationFactory<\n\t\t\t\t\tEffectValue<ContextReturned>,\n\t\t\t\t\tRecord<never, never>,\n\t\t\t\t\tNoInfer<CallbackError<GuardReturned>>,\n\t\t\t\t\tNoInfer<CallbackRequirements<GuardReturned>>\n\t\t\t\t>\n\t\t\t>,\n\t\t) => Operations & NoInfer<ValidatedEffectOperations<Operations>>\n\t\tdefaults?: {\n\t\t\tmetadata?: object\n\t\t\tguard?: (context: EffectValue<ContextReturned>) => GuardReturned\n\t\t}\n\t}): ReturnType<\n\t\ttypeof createEffectQuery<\n\t\t\tHost,\n\t\t\tContextReturned,\n\t\t\tOperations,\n\t\t\tPolicyReturned<Dependencies>,\n\t\t\tGuardReturned\n\t\t>\n\t> {\n\t\t// SAFETY: Host is constrained to the trusted checker's host type; its concrete\n\t\t// return is retained while restoring the erased invocation argument slot.\n\t\tconst checkPermission = dependencies.checkPermission as (\n\t\t\thost: Host,\n\t\t\tinput: { permission: string; operation: string },\n\t\t) => PolicyReturned<Dependencies>\n\t\treturn createEffectQuery<\n\t\t\tHost,\n\t\t\tContextReturned,\n\t\t\tOperations,\n\t\t\tPolicyReturned<Dependencies>,\n\t\t\tGuardReturned\n\t\t>({\n\t\t\t...configuration,\n\t\t\tobservability: dependencies.observability,\n\t\t\tcheckPermission: dependencies.checkPermission ? checkPermission : undefined,\n\t\t})\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"query.mjs","names":[],"sources":["../../../src/modules/effect/query.ts"],"sourcesContent":["import { Effect, Exit } from 'effect'\nimport type { ContractSchema } from '../contracts/contract'\nimport {\n\tselectOperation,\n\ttype SelectedOperation,\n\ttype OperationPublicResult,\n} from '../contracts/exposure'\nimport { decodeEffectContract } from './schema'\n// oxlint-disable-next-line cvx/component-boundaries -- SAFETY: Internal optional adapter shares the Effect-free Foundation lifecycle and types without entering the Convex component deployment graph.\nimport {\n\tObservability,\n\ttype ObservabilityOptions,\n} from '../../components/foundation/modules/observability/observability'\nimport { normalizeEffect, observationFailure } from './normalize'\nimport {\n\teffectOperationFactory,\n\ttype CallbackError,\n\ttype CallbackRequirements,\n\ttype EffectOperationArgument,\n\ttype EffectOperationError,\n\ttype EffectOperationRequirements,\n\ttype EffectOperationResult,\n\ttype EffectValue,\n\ttype CheckedEffectOperation,\n\ttype UnwrappedDefaultError,\n\ttype UnwrappedDefaultRequirements,\n\ttype ValidatedEffectOperations,\n} from './operation'\n\ntype Supported<Value> = Value | PromiseLike<Value> | Effect.Effect<Value, unknown, unknown>\ntype QueryDefinition = {\n\tinput: ContractSchema\n\tresult: ContractSchema\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Internal heterogeneous storage; selected schemas restore the concrete result at exec.\n\thandler: (input: never, context: never) => unknown\n\tclassification?: string\n\tpermission?: string\n\tmetadata?: object\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Internal hook storage retains the concrete definition separately.\n\tguard?: (context: never, input: never) => unknown\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Internal hook storage retains the concrete definition separately.\n\tmiddleware?: (input: never) => unknown\n}\ntype QueryRegistry<Context> = Readonly<\n\tRecord<\n\t\tstring,\n\t\tPick<QueryDefinition, 'input' | 'result' | 'handler'> &\n\t\t\tCheckedEffectOperation<Context> & {\n\t\t\t\tprepare?: never\n\t\t\t\taudit?: never\n\t\t\t\taggregates?: never\n\t\t\t\treplayResult?: never\n\t\t\t}\n\t>\n>\ntype OperationKey<Operations> = Extract<keyof Operations, string>\n\nclass QueryMiddlewareError extends Error {\n\treadonly code = 'QUERY_MIDDLEWARE_NEXT_REUSED'\n\treadonly name = 'QueryMiddlewareError'\n\tconstructor() {\n\t\tsuper('A query middleware called next() more than once')\n\t}\n}\nclass QueryConfigurationError extends Error {\n\treadonly code = 'QUERY_OPERATION_NOT_CONFIGURED'\n\treadonly name = 'QueryConfigurationError'\n\tconstructor() {\n\t\tsuper('The selected query operation is not configured')\n\t}\n}\nclass QueryPermissionError extends Error {\n\treadonly code = 'QUERY_PERMISSION_CHECKER_MISSING'\n\treadonly name = 'QueryPermissionError'\n\tconstructor() {\n\t\tsuper('Query permission requires an injected permission checker')\n\t}\n}\n\n/** Injectable domain queries compose lazily; the API chooses native function kind. */\nexport function createEffectQuery<\n\tHost,\n\tContextReturned,\n\tconst Operations extends QueryRegistry<EffectValue<ContextReturned>>,\n\tPermissionReturned = never,\n\tGuardReturned extends Supported<void> = never,\n>(configuration: {\n\tcontext: (host: Host) => ContextReturned\n\toperations: (\n\t\tdefinitions: ReturnType<\n\t\t\ttypeof effectOperationFactory<\n\t\t\t\tEffectValue<ContextReturned>,\n\t\t\t\tRecord<never, never>,\n\t\t\t\tNoInfer<CallbackError<GuardReturned>>,\n\t\t\t\tNoInfer<CallbackRequirements<GuardReturned>>\n\t\t\t>\n\t\t>,\n\t) => Operations & NoInfer<ValidatedEffectOperations<Operations>>\n\tcheckPermission?: (\n\t\thost: Host,\n\t\tinput: { permission: string; operation: string },\n\t) => PermissionReturned\n\tdefaults?: { metadata?: object; guard?: (context: EffectValue<ContextReturned>) => GuardReturned }\n\tobservability?: ObservabilityOptions | Observability\n}) {\n\tconst definitions = effectOperationFactory<\n\t\tEffectValue<ContextReturned>,\n\t\tRecord<never, never>,\n\t\tCallbackError<GuardReturned>,\n\t\tCallbackRequirements<GuardReturned>\n\t>()\n\tconst operations: Operations = configuration.operations(definitions)\n\tconst observability =\n\t\tconfiguration.observability instanceof Observability\n\t\t\t? configuration.observability\n\t\t\t: new Observability(\n\t\t\t\t\tconfiguration.observability ?? {\n\t\t\t\t\t\tenabled: false,\n\t\t\t\t\t\tclassifyError: () => ({ outcome: 'failed', errorCode: 'UNEXPECTED' }),\n\t\t\t\t\t},\n\t\t\t\t)\n\n\tfunction exec<const Key extends OperationKey<Operations>>(\n\t\toperation: Key,\n\t\tinput: EffectOperationArgument<Operations[Key]>,\n\t\thost: Host,\n\t): Effect.Effect<\n\t\tEffectOperationResult<Operations[Key]>,\n\t\t| EffectOperationError<Operations[Key]>\n\t\t| CallbackError<ContextReturned | PermissionReturned>\n\t\t| UnwrappedDefaultError<Operations[Key], GuardReturned>,\n\t\t| EffectOperationRequirements<Operations[Key]>\n\t\t| CallbackRequirements<ContextReturned | PermissionReturned>\n\t\t| UnwrappedDefaultRequirements<Operations[Key], GuardReturned>\n\t> {\n\t\tconst execution = Effect.suspend(() => {\n\t\t\tif (!Object.prototype.hasOwnProperty.call(operations, operation)) {\n\t\t\t\treturn Effect.die(new QueryConfigurationError())\n\t\t\t}\n\t\t\t// SAFETY: operation helpers check hooks before this heterogeneous storage seam.\n\t\t\tconst definition: QueryDefinition = operations[operation]\n\t\t\treturn Effect.flatMap(decodeEffectContract(definition.input, input), (parsedInput) => {\n\t\t\t\tconst observation = observability.start({\n\t\t\t\t\toperation,\n\t\t\t\t\tclassification: definition.classification ?? 'query',\n\t\t\t\t})\n\t\t\t\tconst run = Effect.gen(function* () {\n\t\t\t\t\tif (definition.permission !== undefined) {\n\t\t\t\t\t\tconst check = configuration.checkPermission\n\t\t\t\t\t\tif (!check) return yield* Effect.die(new QueryPermissionError())\n\t\t\t\t\t\tyield* normalizeEffect(() =>\n\t\t\t\t\t\t\tcheck(host, { permission: definition.permission!, operation }),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tconst context = yield* normalizeEffect(() => configuration.context(host))\n\t\t\t\t\tconst metadata = { ...configuration.defaults?.metadata, ...definition.metadata }\n\t\t\t\t\tconst core = (current: EffectValue<ContextReturned>) =>\n\t\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\t\tconst guard = configuration.defaults?.guard\n\t\t\t\t\t\t\tif (guard) yield* normalizeEffect(() => guard(current))\n\t\t\t\t\t\t\tif (definition.guard)\n\t\t\t\t\t\t\t\tyield* normalizeEffect(() => {\n\t\t\t\t\t\t\t\t\t// SAFETY: selected schemas and context own the stored guard parameters.\n\t\t\t\t\t\t\t\t\treturn definition.guard!(current as never, parsedInput as never)\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\treturn yield* normalizeEffect(() => {\n\t\t\t\t\t\t\t\t// SAFETY: selected schemas and context own the stored handler parameters.\n\t\t\t\t\t\t\t\treturn definition.handler(parsedInput as never, current as never)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t\tlet usedNext = false\n\t\t\t\t\tconst middleware = definition.middleware\n\t\t\t\t\tconst output = middleware\n\t\t\t\t\t\t? yield* normalizeEffect(() => {\n\t\t\t\t\t\t\t\t// SAFETY: the selected helper checked this middleware's input and next contract.\n\t\t\t\t\t\t\t\treturn middleware({\n\t\t\t\t\t\t\t\t\toperation,\n\t\t\t\t\t\t\t\t\tinput: parsedInput,\n\t\t\t\t\t\t\t\t\tcommand: parsedInput,\n\t\t\t\t\t\t\t\t\tcontext,\n\t\t\t\t\t\t\t\t\tmetadata,\n\t\t\t\t\t\t\t\t\tnext: (options?: { context?: object }) =>\n\t\t\t\t\t\t\t\t\t\tEffect.suspend(() => {\n\t\t\t\t\t\t\t\t\t\t\tif (usedNext) return Effect.die(new QueryMiddlewareError())\n\t\t\t\t\t\t\t\t\t\t\tusedNext = true\n\t\t\t\t\t\t\t\t\t\t\treturn core(\n\t\t\t\t\t\t\t\t\t\t\t\toptions?.context ? Object.assign({}, context, options.context) : context,\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t} as never)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: yield* core(context)\n\t\t\t\t\treturn yield* decodeEffectContract(definition.result, output)\n\t\t\t\t})\n\t\t\t\treturn run.pipe(\n\t\t\t\t\tEffect.onExit((exit) =>\n\t\t\t\t\t\tEffect.sync(() => {\n\t\t\t\t\t\t\tif (Exit.isSuccess(exit)) observation.completed()\n\t\t\t\t\t\t\telse {\n\t\t\t\t\t\t\t\tobservation.failed(observationFailure(exit.cause))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t})\n\t\t})\n\t\t// SAFETY: selected schemas own A; concrete callbacks and the wrapped middleware\n\t\t// own E/R. No interpreter or runner executes this Effect before the caller does.\n\t\treturn execution as ReturnType<typeof exec<Key>>\n\t}\n\tfunction expose<const Owner extends symbol, const Key extends OperationKey<Operations>>(\n\t\towner: Owner,\n\t\toperation: Key,\n\t): SelectedOperation<\n\t\tOwner,\n\t\tKey,\n\t\tOperations[Key]['input'],\n\t\tOperationPublicResult<Operations[Key]>\n\t> {\n\t\tif (!Object.hasOwn(operations, operation)) throw new QueryConfigurationError()\n\t\treturn selectOperation(owner, 'query', operation, operations[operation])\n\t}\n\treturn {\n\t\texec,\n\t\texpose,\n\t\t/** Bind invocation context without resolving it or running an operation. */\n\t\twithContext(context: Host) {\n\t\t\treturn {\n\t\t\t\texec: <const Key extends OperationKey<Operations>>(\n\t\t\t\t\toperation: Key,\n\t\t\t\t\tinput: EffectOperationArgument<Operations[Key]>,\n\t\t\t\t) => exec(operation, input, context),\n\t\t\t}\n\t\t},\n\t}\n}\n\ntype Policy<Dependencies> = 'checkPermission' extends keyof Dependencies\n\t? Dependencies['checkPermission']\n\t: never\n// oxlint-disable-next-line anti-slop/no-unknown-returns -- Conditional type extracts the host; no unknown result is exposed by the registry.\ntype CallbackHost<Callback> = Callback extends (host: infer Host, input: never) => unknown\n\t? Host\n\t: never\ntype CallbackReturned<Callback> = Callback extends (...arguments_: never[]) => infer Returned\n\t? Returned\n\t: never\ntype PolicyHost<Dependencies> = [CallbackHost<Policy<Dependencies>>] extends [never]\n\t? unknown\n\t: CallbackHost<Policy<Dependencies>>\ntype PolicyReturned<Dependencies> = CallbackReturned<Policy<Dependencies>>\nexport type EffectQueryDependencies = {\n\tobservability: Observability\n\t// oxlint-disable-next-line anti-slop/no-unknown-returns -- Host policy storage is erased; the bound generic dependency retains its concrete E/R.\n\tcheckPermission?: (host: never, input: { permission: string; operation: string }) => unknown\n}\n\n/** The public Foundation binds trusted application policies once. */\nexport function bindEffectQuery<const Dependencies extends EffectQueryDependencies>(\n\tdependencies: Dependencies,\n) {\n\treturn function query<\n\t\tHost extends PolicyHost<Dependencies>,\n\t\tContextReturned,\n\t\tconst Operations extends QueryRegistry<EffectValue<ContextReturned>>,\n\t\tGuardReturned extends Supported<void> = never,\n\t>(configuration: {\n\t\tcontext: (host: Host) => ContextReturned\n\t\toperations: (\n\t\t\tdefinitions: ReturnType<\n\t\t\t\ttypeof effectOperationFactory<\n\t\t\t\t\tEffectValue<ContextReturned>,\n\t\t\t\t\tRecord<never, never>,\n\t\t\t\t\tNoInfer<CallbackError<GuardReturned>>,\n\t\t\t\t\tNoInfer<CallbackRequirements<GuardReturned>>\n\t\t\t\t>\n\t\t\t>,\n\t\t) => Operations & NoInfer<ValidatedEffectOperations<Operations>>\n\t\tdefaults?: {\n\t\t\tmetadata?: object\n\t\t\tguard?: (context: EffectValue<ContextReturned>) => GuardReturned\n\t\t}\n\t}): ReturnType<\n\t\ttypeof createEffectQuery<\n\t\t\tHost,\n\t\t\tContextReturned,\n\t\t\tOperations,\n\t\t\tPolicyReturned<Dependencies>,\n\t\t\tGuardReturned\n\t\t>\n\t> {\n\t\t// SAFETY: Host is constrained to the trusted checker's host type; its concrete\n\t\t// return is retained while restoring the erased invocation argument slot.\n\t\tconst checkPermission = dependencies.checkPermission as (\n\t\t\thost: Host,\n\t\t\tinput: { permission: string; operation: string },\n\t\t) => PolicyReturned<Dependencies>\n\t\treturn createEffectQuery<\n\t\t\tHost,\n\t\t\tContextReturned,\n\t\t\tOperations,\n\t\t\tPolicyReturned<Dependencies>,\n\t\t\tGuardReturned\n\t\t>({\n\t\t\t...configuration,\n\t\t\tobservability: dependencies.observability,\n\t\t\tcheckPermission: dependencies.checkPermission ? checkPermission : undefined,\n\t\t})\n\t}\n}\n"],"mappings":"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"}
|
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
|
@@ -91,12 +91,12 @@ class Actor extends Context.Service<
|
|
|
91
91
|
>()('app/Actor') {}
|
|
92
92
|
|
|
93
93
|
export function createEffectAccountApi<DataModel extends GenericDataModel>(
|
|
94
|
-
|
|
94
|
+
baseAuthMutation: ReturnType<typeof createAuthFunctions<DataModel>>['authMutation'],
|
|
95
95
|
) {
|
|
96
|
-
const
|
|
96
|
+
const authMutation = effectZodApiBuilder(baseAuthMutation, {
|
|
97
97
|
services: (ctx) => Context.make(Actor, ctx.actor),
|
|
98
98
|
})
|
|
99
|
-
const currentActor =
|
|
99
|
+
const currentActor = authMutation({
|
|
100
100
|
args: {},
|
|
101
101
|
returns: z.object({ userId: z.string(), organizationId: z.string() }),
|
|
102
102
|
handler: () =>
|
|
@@ -105,7 +105,7 @@ export function createEffectAccountApi<DataModel extends GenericDataModel>(
|
|
|
105
105
|
return { userId: actor.userId, organizationId: actor.organizationId }
|
|
106
106
|
}),
|
|
107
107
|
})
|
|
108
|
-
return {
|
|
108
|
+
return { authMutation, currentActor }
|
|
109
109
|
}
|
|
110
110
|
```
|
|
111
111
|
|
|
@@ -305,3 +305,9 @@ errors: { throw: (input) => { throw App.errors.from(input) } }
|
|
|
305
305
|
3. Reads go through `ctx.include(...)` with an explicit limit.
|
|
306
306
|
4. Use `ctx.actor` as the actor reference in audits and approvals — never
|
|
307
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.
|