cvx-kit 0.1.11 → 0.1.13
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 +35 -0
- package/README.md +1 -0
- package/dist/contracts.d.mts +2 -1
- package/dist/contracts.mjs +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +1 -1
- package/dist/modules/contracts/binding.d.mts +26 -0
- package/dist/modules/contracts/binding.mjs +2 -0
- package/dist/modules/contracts/binding.mjs.map +1 -0
- package/dist/modules/contracts/domain.d.mts +59 -0
- package/dist/modules/contracts/domain.mjs +2 -0
- package/dist/modules/contracts/domain.mjs.map +1 -0
- package/dist/modules/effect/command.d.mts +39 -1
- package/dist/modules/effect/command.mjs +1 -1
- package/dist/modules/effect/command.mjs.map +1 -1
- package/dist/modules/effect/crud.d.mts +6 -0
- package/dist/modules/effect/domain.d.mts +72 -0
- package/dist/modules/effect/domain.mjs +2 -0
- package/dist/modules/effect/domain.mjs.map +1 -0
- package/dist/modules/effect/operation.d.mts +4 -3
- package/dist/modules/effect/operation.mjs.map +1 -1
- package/dist/modules/effect/query.d.mts +24 -8
- package/dist/modules/effect/query.mjs +1 -1
- package/dist/modules/effect/query.mjs.map +1 -1
- package/dist/modules/foundation/foundation.d.mts +109 -0
- package/dist/modules/foundation/foundation.mjs +2 -0
- package/dist/modules/foundation/foundation.mjs.map +1 -0
- package/dist/modules/foundation/operation.d.mts +140 -0
- package/dist/modules/foundation/operation.mjs +2 -0
- package/dist/modules/foundation/operation.mjs.map +1 -0
- package/dist/oxlint/ownership.mjs +1 -1
- package/dist/oxlint/ownership.mjs.map +1 -1
- package/dist/oxlint/reserved-files.d.mts +6 -0
- package/dist/oxlint/reserved-files.mjs +2 -0
- package/dist/oxlint/reserved-files.mjs.map +1 -0
- package/dist/oxlint.d.mts +1 -1
- package/dist/oxlint.mjs +1 -1
- package/dist/oxlint.mjs.map +1 -1
- package/docs/commands-legacy.md +3 -1
- package/docs/conventions.md +7 -3
- package/docs/domain-contracts.md +148 -0
- package/docs/effect.md +2 -0
- package/docs/oxlint.md +4 -0
- package/package.json +1 -1
- package/scripts/test-packed-effect.mjs +14 -0
- package/scripts/test-packed-oxlint.mjs +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Optional domain contracts
|
|
2
|
+
|
|
3
|
+
Declare schemas and command classification once in `contracts.ts`, then attach
|
|
4
|
+
the declaration to a Command or Query registry. Errors belong at the root beside
|
|
5
|
+
commands and queries. Either section and the error contract may be omitted.
|
|
6
|
+
|
|
7
|
+
The regular registry uses `createFoundation` from `cvx-kit`; the Effect registry
|
|
8
|
+
uses `createEffectFoundation` from `cvx-kit/effect`. Both accept the same contract
|
|
9
|
+
and handler-first implementation helpers. Existing inline Effect definitions and the
|
|
10
|
+
older `new Foundation(...)` constructors continue to work.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
import { createFoundation } from 'cvx-kit'
|
|
15
|
+
import { defineDomainContract } from 'cvx-kit/contracts'
|
|
16
|
+
import { defineErrorContract } from 'cvx-kit/errors'
|
|
17
|
+
|
|
18
|
+
const contract = defineDomainContract({
|
|
19
|
+
errors: defineErrorContract({ Missing: { message: 'Task missing', details: {} } }),
|
|
20
|
+
commands: {
|
|
21
|
+
'tasks.create': {
|
|
22
|
+
input: z.string().transform(Number),
|
|
23
|
+
result: z.number().transform((count) => ({ count })),
|
|
24
|
+
classification: 'business',
|
|
25
|
+
},
|
|
26
|
+
'tasks.close': {
|
|
27
|
+
input: z.object({ id: z.string() }),
|
|
28
|
+
result: z.boolean(),
|
|
29
|
+
classification: 'business',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
queries: { 'tasks.get': { input: z.number(), result: z.number() } },
|
|
33
|
+
})
|
|
34
|
+
const { Command, Query } = createFoundation({
|
|
35
|
+
observability: {
|
|
36
|
+
enabled: false,
|
|
37
|
+
classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }),
|
|
38
|
+
},
|
|
39
|
+
writeAudit: (_host: { actorId: string }, entry) => {
|
|
40
|
+
console.log(entry.operation)
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
const commands = Command({
|
|
44
|
+
contract,
|
|
45
|
+
context: (host: { actorId: string }) => host,
|
|
46
|
+
audit: ({ operation, input, result }, context) => {
|
|
47
|
+
if (operation !== 'tasks.create') return null
|
|
48
|
+
return {
|
|
49
|
+
operation,
|
|
50
|
+
actorId: context.actorId,
|
|
51
|
+
aggregate: { type: 'task', id: String(input) },
|
|
52
|
+
metadata: { count: result.count },
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
operations: ({ command }) => ({
|
|
56
|
+
'tasks.create': command.handler((input) => input + 1),
|
|
57
|
+
'tasks.close': command.handler((input) => input.id.length > 0, { audit: () => null }),
|
|
58
|
+
}),
|
|
59
|
+
})
|
|
60
|
+
const queries = Query({
|
|
61
|
+
contract,
|
|
62
|
+
context: (host: { actorId: string }) => host,
|
|
63
|
+
operations: ({ query }) => ({ 'tasks.get': query.handler((input) => input) }),
|
|
64
|
+
})
|
|
65
|
+
const created = await commands.exec('tasks.create', '2', { actorId: 'actor' })
|
|
66
|
+
const count = await queries.withContext({ actorId: 'actor' }).exec('tasks.get', created.count)
|
|
67
|
+
void count
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Each map must implement exactly its declared keys, including maps supplied through
|
|
71
|
+
variables. The object key selects the contract entry and infers the handler and
|
|
72
|
+
option callback types through `command.handler(fn, options?)` or
|
|
73
|
+
`query.handler(fn, options?)`. Keep the map directly in `operations` for contextual
|
|
74
|
+
inference. For separately assembled maps, the existing named helpers provide explicit
|
|
75
|
+
contract selection without relying on the enclosing object for inference. Input, result,
|
|
76
|
+
classification, replayResult, and wire come from the declaration and cannot be
|
|
77
|
+
overridden in the implementation. Construction snapshots operation records and
|
|
78
|
+
map membership without cloning validators or running user callbacks.
|
|
79
|
+
|
|
80
|
+
Options include guard, middleware, metadata, and permission; commands also allow
|
|
81
|
+
prepare, aggregates, and audit overrides. Options are optional when a root audit
|
|
82
|
+
is configured; commands without one require an audit in their options. Existing
|
|
83
|
+
`command['tasks.create']({ handler, ...options })` and named query helpers remain
|
|
84
|
+
supported. Named helpers retain their exact key binding.
|
|
85
|
+
|
|
86
|
+
The handler receives decoded input, returns the result schema's accepted input,
|
|
87
|
+
and execution returns its decoded result. Normal handlers and hooks return values
|
|
88
|
+
or Promises. Effect handlers and hooks may also return Effects; their error and
|
|
89
|
+
service channels remain part of the selected operation's type. For example, with
|
|
90
|
+
`Command` from `createEffectFoundation`:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { Effect } from 'effect'
|
|
94
|
+
import { createEffectFoundation } from 'cvx-kit/effect'
|
|
95
|
+
|
|
96
|
+
const effectFoundation = createEffectFoundation({
|
|
97
|
+
observability: {
|
|
98
|
+
enabled: false,
|
|
99
|
+
classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }),
|
|
100
|
+
},
|
|
101
|
+
writeAudit: (_host: { actorId: string }) => undefined,
|
|
102
|
+
})
|
|
103
|
+
const effectCommands = effectFoundation.Command({
|
|
104
|
+
contract,
|
|
105
|
+
context: (host: { actorId: string }) => host,
|
|
106
|
+
audit: () => null,
|
|
107
|
+
operations: ({ command }) => ({
|
|
108
|
+
'tasks.create': command.handler((input) => Effect.succeed(input + 1)),
|
|
109
|
+
'tasks.close': command.handler((input) => Effect.succeed(input.id.length > 0)),
|
|
110
|
+
}),
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The same contract owns both implementations; selecting a different adapter does
|
|
115
|
+
not require repeating schemas.
|
|
116
|
+
|
|
117
|
+
For independent wire projections, use the existing `operationContract` helper
|
|
118
|
+
inside a domain declaration. It infers the projector's parameter from the result
|
|
119
|
+
schema; provide both its replayResult and wire fields. A command may instead
|
|
120
|
+
supply only replayResult directly in its domain entry. A query cannot prepare,
|
|
121
|
+
audit, declare aggregates, or declare replayResult.
|
|
122
|
+
|
|
123
|
+
A root audit receives `{ operation, input, result }` and resolved base context.
|
|
124
|
+
Its operation discriminates the corresponding input and result types. An
|
|
125
|
+
operation's audit override receives the existing `{ command, result }` envelope
|
|
126
|
+
and replaces the root audit. Exactly one callback runs after fresh-result
|
|
127
|
+
validation. Returning null skips the write. Replay skips handlers, audits,
|
|
128
|
+
writes, and completion; permission still runs before replay. Each command needs
|
|
129
|
+
a definite audit source. A definite override excludes the root audit's Effect
|
|
130
|
+
channels; an optional override retains channels from both possible callbacks.
|
|
131
|
+
|
|
132
|
+
Inline registries can also use root audit, leaving schemas and classification
|
|
133
|
+
on their operation definitions. The legacy constructor API keeps its existing
|
|
134
|
+
context-first execution signatures; the new regular registry uses input-first
|
|
135
|
+
handlers and `exec(operation, input, host)` like Effect.
|
|
136
|
+
|
|
137
|
+
Pass `contract.errors` explicitly to the existing `createEffectApi({ errors })`
|
|
138
|
+
option. Attaching a contract does not change a globally configured API. The
|
|
139
|
+
original error-contract instance still owns projection: foreign and forged errors
|
|
140
|
+
become UnknownFailure. Wire exposure remains explicit through `.expose(owner, key)`.
|
|
141
|
+
|
|
142
|
+
Enable `cvx/reserved-domain-file-anatomy` together with
|
|
143
|
+
`cvx/domain-file-responsibilities` to enforce reserved filenames. A domain's
|
|
144
|
+
commands.ts and queries.ts each export one genuine bound registry. Contracts are
|
|
145
|
+
declarative; schema.ts may declare several zodTable shapes; table.ts owns their
|
|
146
|
+
topology, and domain/table.ts uses createModule to assemble module maps. The rules
|
|
147
|
+
recognize aliases, namespaces, and the root foundation.ts facade. They do not
|
|
148
|
+
require absent reserved files to be created.
|
package/docs/effect.md
CHANGED
|
@@ -399,3 +399,5 @@ Optional request context can also be added through the existing Effect API adapt
|
|
|
399
399
|
and consume `ctx.commands.exec(name, input)` through the application's existing
|
|
400
400
|
function helper names. Keep dependencies local to each feature; explicit execution
|
|
401
401
|
and input-only command handlers remain supported. See [optional context injection](./command-context.md).
|
|
402
|
+
|
|
403
|
+
See [Optional domain contracts](./domain-contracts.md) for shared declarations, named implementations, and registry audit defaults in both Effect and Promise registries.
|
package/docs/oxlint.md
CHANGED
|
@@ -144,3 +144,7 @@ Shared ownership follows static runtime imports through shared helpers. A shared
|
|
|
144
144
|
file reached from only one domain is reported for relocation. Unused code,
|
|
145
145
|
type-only consumers, and computed dependencies are not treated as proof of shared
|
|
146
146
|
ownership; their ownership still needs review.
|
|
147
|
+
|
|
148
|
+
## Reserved domain files
|
|
149
|
+
|
|
150
|
+
Enable `cvx/reserved-domain-file-anatomy` to require exported Command/Query registries, declarative contracts, zodTable shapes, and table topology or createModule assembly. Constructor provenance is traced through imported aliases, namespaces, direct Foundation factories, and the root foundation.ts facade. Pair it with `cvx/domain-file-responsibilities` to reject handlers and database/runtime work in contracts.ts. Backend scope and test/generated exclusions apply; absent files are not required.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cvx-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Reusable Convex application kit: foundation kernels, approvals component, zod table boundaries, WorkOS-backed auth function constructors, and audited command protocol.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"component",
|
|
@@ -29,6 +29,7 @@ const extraTypeFixtures = [
|
|
|
29
29
|
join(root, 'README.md'),
|
|
30
30
|
join(root, 'src/docs/effect.md'),
|
|
31
31
|
join(root, 'src/docs/commands.md'),
|
|
32
|
+
join(root, 'src/docs/domain-contracts.md'),
|
|
32
33
|
join(root, 'src/docs/command-context.md'),
|
|
33
34
|
join(root, 'src/docs/commands-legacy.md'),
|
|
34
35
|
join(root, 'src/docs/auth.md'),
|
|
@@ -54,6 +55,7 @@ function run(command, args, cwd) {
|
|
|
54
55
|
function copyPublicTypeFixture(fixture, filename, publicImport) {
|
|
55
56
|
const source = readFileSync(join(root, 'test', filename), 'utf8')
|
|
56
57
|
.replaceAll(publicImport, 'cvx-kit/effect')
|
|
58
|
+
.replaceAll('../src/index', 'cvx-kit')
|
|
57
59
|
.replaceAll('../src/errors', 'cvx-kit/errors')
|
|
58
60
|
.replaceAll('../src/contracts', 'cvx-kit/contracts')
|
|
59
61
|
.replaceAll('../src/idempotency', 'cvx-kit/idempotency')
|
|
@@ -126,6 +128,8 @@ function copyExtraTypeFixtures(fixture, paths) {
|
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
const legacyRuntime = `
|
|
131
|
+
import { createFoundation } from 'cvx-kit'
|
|
132
|
+
import { defineDomainContract } from 'cvx-kit/contracts'
|
|
129
133
|
import { strict as assert } from 'node:assert'
|
|
130
134
|
import { createRequire } from 'node:module'
|
|
131
135
|
import { Foundation } from 'cvx-kit/components/foundation'
|
|
@@ -210,6 +214,10 @@ assert.equal(replay.kind, 'replay')
|
|
|
210
214
|
assert.deepEqual(await decodeContract(idempotency.replayResult, replay.result), { value: 'final!' })
|
|
211
215
|
assert.equal(authorizations, 2)
|
|
212
216
|
assert.equal(ledger.length, 1)
|
|
217
|
+
const domain = defineDomainContract({ commands: { save: { input: z.string().transform(Number), result: z.number().transform(count => ({ count })), classification: 'business' } } })
|
|
218
|
+
const regular = createFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: () => undefined })
|
|
219
|
+
const domainCommands = regular.Command({ contract: domain, context: host => host, audit: () => null, operations: ({ command }) => ({ save: command.save({ handler: value => value + 1 }) }) })
|
|
220
|
+
assert.deepEqual(await domainCommands.exec('save', '2', {}), { count: 3 })
|
|
213
221
|
`
|
|
214
222
|
|
|
215
223
|
const legacyTypes = `
|
|
@@ -387,6 +395,9 @@ try {
|
|
|
387
395
|
writeFileSync(join(fixture, 'legacy-types.ts'), legacyTypes)
|
|
388
396
|
const typeFiles = [
|
|
389
397
|
'legacy-types.ts',
|
|
398
|
+
copyPublicTypeFixture(fixture, 'domain-contract-types.ts', '../src/effect'),
|
|
399
|
+
copyPublicTypeFixture(fixture, 'domain-foundation-types.ts', '../src/effect'),
|
|
400
|
+
copyPublicTypeFixture(fixture, 'domain-contract-guide-types.ts', '../src/effect'),
|
|
390
401
|
copyPublicTypeFixture(fixture, 'operation-neutral-types.ts', '../src/effect'),
|
|
391
402
|
copyPublicTypeFixture(fixture, 'operation-agent-tools-types.ts', '../src/effect'),
|
|
392
403
|
]
|
|
@@ -394,6 +405,9 @@ try {
|
|
|
394
405
|
if (withEffect) {
|
|
395
406
|
for (const filename of [
|
|
396
407
|
'effect-foundation-types.ts',
|
|
408
|
+
'domain-effect-types.ts',
|
|
409
|
+
'domain-handler-types.ts',
|
|
410
|
+
'domain-exactness-types.ts',
|
|
397
411
|
'effect-api-types.ts',
|
|
398
412
|
'effect-api-zod-types.ts',
|
|
399
413
|
'effect-auth-types.ts',
|
|
@@ -9,7 +9,7 @@ const root = join(import.meta.dirname, '..')
|
|
|
9
9
|
const temporary = mkdtempSync(join(tmpdir(), 'cvx-kit-oxlint-'))
|
|
10
10
|
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
11
11
|
const ruleNames = Object.keys(rules)
|
|
12
|
-
assert.equal(ruleNames.length,
|
|
12
|
+
assert.equal(ruleNames.length, 21)
|
|
13
13
|
|
|
14
14
|
try {
|
|
15
15
|
run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)
|