cvx-kit 0.1.5 → 0.1.7
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 +41 -0
- package/README.md +21 -3
- package/dist/agent-tools.d.mts +2 -1
- package/dist/agent-tools.mjs +1 -1
- package/dist/agent-tools.mjs.map +1 -1
- package/dist/components/approvals/audit.mjs +1 -1
- package/dist/components/foundation/client.d.mts +8 -7
- package/dist/components/foundation/client.mjs +1 -1
- package/dist/components/foundation/client.mjs.map +1 -1
- package/dist/components/foundation/modules/command/command.d.mts +50 -10
- package/dist/components/foundation/modules/command/command.mjs +1 -1
- package/dist/components/foundation/modules/command/command.mjs.map +1 -1
- package/dist/components/foundation/modules/command/lifecycle.d.mts +81 -0
- package/dist/components/foundation/modules/command/lifecycle.mjs +2 -0
- package/dist/components/foundation/modules/command/lifecycle.mjs.map +1 -0
- package/dist/components/foundation/modules/observability/observability.d.mts +8 -0
- package/dist/components/foundation/modules/observability/observability.mjs +1 -1
- package/dist/components/foundation/modules/observability/observability.mjs.map +1 -1
- package/dist/components/foundation/operation.d.mts +6 -6
- package/dist/components/foundation/operation.mjs.map +1 -1
- package/dist/contracts.d.mts +4 -0
- package/dist/contracts.mjs +1 -0
- package/dist/crud.d.mts +2 -2
- package/dist/crud.mjs.map +1 -1
- package/dist/effect.d.mts +12 -0
- package/dist/effect.mjs +1 -0
- package/dist/errors.d.mts +2 -1
- package/dist/errors.mjs +1 -1
- package/dist/errors.mjs.map +1 -1
- package/dist/idempotency.d.mts +2 -0
- package/dist/idempotency.mjs +1 -0
- package/dist/modules/contracts/contract.d.mts +70 -0
- package/dist/modules/contracts/contract.mjs +2 -0
- package/dist/modules/contracts/contract.mjs.map +1 -0
- package/dist/modules/contracts/convex.d.mts +22 -0
- package/dist/modules/contracts/convex.mjs +2 -0
- package/dist/modules/contracts/convex.mjs.map +1 -0
- package/dist/modules/contracts/errors.d.mts +44 -0
- package/dist/modules/contracts/errors.mjs +2 -0
- package/dist/modules/contracts/errors.mjs.map +1 -0
- package/dist/modules/contracts/exposure.d.mts +105 -0
- package/dist/modules/contracts/exposure.mjs +2 -0
- package/dist/modules/contracts/exposure.mjs.map +1 -0
- package/dist/modules/contracts/idempotency.d.mts +87 -0
- package/dist/modules/contracts/idempotency.mjs +2 -0
- package/dist/modules/contracts/idempotency.mjs.map +1 -0
- package/dist/modules/contracts/standard.d.mts +16 -0
- package/dist/modules/contracts/standard.mjs +2 -0
- package/dist/modules/contracts/standard.mjs.map +1 -0
- package/dist/modules/contracts/workflow.d.mts +167 -0
- package/dist/modules/contracts/workflow.mjs +2 -0
- package/dist/modules/contracts/workflow.mjs.map +1 -0
- package/dist/modules/effect/agent-tools.d.mts +2 -0
- package/dist/modules/effect/agent-tools.mjs +1 -0
- package/dist/modules/effect/api-runtime.d.mts +28 -0
- package/dist/modules/effect/api-runtime.mjs +2 -0
- package/dist/modules/effect/api-runtime.mjs.map +1 -0
- package/dist/modules/effect/api-zod.d.mts +35 -0
- package/dist/modules/effect/api-zod.mjs +2 -0
- package/dist/modules/effect/api-zod.mjs.map +1 -0
- package/dist/modules/effect/api.d.mts +21 -0
- package/dist/modules/effect/api.mjs +2 -0
- package/dist/modules/effect/api.mjs.map +1 -0
- package/dist/modules/effect/client.d.mts +11 -0
- package/dist/modules/effect/client.mjs +1 -0
- package/dist/modules/effect/command.d.mts +59 -0
- package/dist/modules/effect/command.mjs +2 -0
- package/dist/modules/effect/command.mjs.map +1 -0
- package/dist/modules/effect/crud.d.mts +785 -0
- package/dist/modules/effect/crud.mjs +2 -0
- package/dist/modules/effect/crud.mjs.map +1 -0
- package/dist/modules/effect/errors.d.mts +8 -0
- package/dist/modules/effect/errors.mjs +2 -0
- package/dist/modules/effect/errors.mjs.map +1 -0
- package/dist/modules/effect/foundation.d.mts +21 -0
- package/dist/modules/effect/foundation.mjs +2 -0
- package/dist/modules/effect/foundation.mjs.map +1 -0
- package/dist/modules/effect/idempotency.d.mts +31 -0
- package/dist/modules/effect/idempotency.mjs +2 -0
- package/dist/modules/effect/idempotency.mjs.map +1 -0
- package/dist/modules/effect/normalize.d.mts +10 -0
- package/dist/modules/effect/normalize.mjs +2 -0
- package/dist/modules/effect/normalize.mjs.map +1 -0
- package/dist/modules/effect/operation.d.mts +141 -0
- package/dist/modules/effect/operation.mjs +2 -0
- package/dist/modules/effect/operation.mjs.map +1 -0
- package/dist/modules/effect/query.d.mts +65 -0
- package/dist/modules/effect/query.mjs +2 -0
- package/dist/modules/effect/query.mjs.map +1 -0
- package/dist/modules/effect/schema.d.mts +33 -0
- package/dist/modules/effect/schema.mjs +2 -0
- package/dist/modules/effect/schema.mjs.map +1 -0
- package/dist/modules/effect/workflow.d.mts +77 -0
- package/dist/modules/effect/workflow.mjs +2 -0
- package/dist/modules/effect/workflow.mjs.map +1 -0
- package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +79 -0
- package/dist/oxlint/architecture.d.mts +20 -0
- package/dist/oxlint/architecture.mjs +2 -0
- package/dist/oxlint/architecture.mjs.map +1 -0
- package/dist/oxlint/ast.d.mts +33 -0
- package/dist/oxlint/ast.mjs +2 -0
- package/dist/oxlint/ast.mjs.map +1 -0
- package/dist/oxlint/ownership.d.mts +62 -0
- package/dist/oxlint/ownership.mjs +2 -0
- package/dist/oxlint/ownership.mjs.map +1 -0
- package/dist/oxlint.d.mts +5 -2
- package/dist/oxlint.mjs +1 -1
- package/dist/oxlint.mjs.map +1 -1
- package/dist/workflow.d.mts +2 -0
- package/dist/workflow.mjs +1 -0
- package/docs/auth.md +65 -0
- package/docs/commands.md +118 -0
- package/docs/conventions.md +49 -21
- package/docs/domain-operations.md +615 -0
- package/docs/effect.md +384 -0
- package/docs/oxlint.md +74 -26
- package/docs/upgrading.md +13 -0
- package/package.json +37 -3
- package/scripts/convex-smoke-env.mjs +33 -0
- package/scripts/smoke-packed-components.mjs +9 -2
- package/scripts/smoke-packed-effect.mjs +439 -0
- package/scripts/test-packed-effect.mjs +457 -0
- package/scripts/test-packed-ids.mjs +1 -0
- package/scripts/test-packed-oxlint.mjs +24 -2
package/docs/effect.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Effect domain commands and queries
|
|
2
|
+
|
|
3
|
+
`cvx-kit/effect` adds cohesive command/query declarations and a shared API
|
|
4
|
+
runner. Handlers can return ordinary values, Promises, or Effects. Each
|
|
5
|
+
registry returns a lazy Effect from `exec(operation, input, host)`; the API
|
|
6
|
+
adapter provides request services and runs it once.
|
|
7
|
+
|
|
8
|
+
Install Effect explicitly when opting in:
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
bun add effect@4.0.1
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The optional peer range is `>=4.0.1 <5`. Existing imports and Promise-based
|
|
15
|
+
Foundation consumers work without Effect installed. Use the public facade;
|
|
16
|
+
the internal module paths are not supported imports.
|
|
17
|
+
|
|
18
|
+
## A complete domain-to-API example
|
|
19
|
+
|
|
20
|
+
These four TypeScript sections form one module and are compiled together
|
|
21
|
+
against the packed public package. The example uses Convex's generic builders
|
|
22
|
+
with a concrete schema so it is standalone. In an app, put the schema in
|
|
23
|
+
`convex/schema.ts`, import your generated `query`, `mutation`, `QueryCtx`, and
|
|
24
|
+
`MutationCtx` from `./_generated/server`, and split the remaining code into
|
|
25
|
+
services, domain registries, shared API setup, and endpoint modules.
|
|
26
|
+
|
|
27
|
+
The API obtains the actor from authenticated Convex identity. Caller arguments
|
|
28
|
+
contain only a document key and title. Both services constrain document reads
|
|
29
|
+
and writes by that actor; the application should also enforce its own roles
|
|
30
|
+
and tenant policies. The small permission checker below only requires a
|
|
31
|
+
nonempty trusted actor.
|
|
32
|
+
|
|
33
|
+
### Contracts and injectable capabilities
|
|
34
|
+
|
|
35
|
+
The reader has no write methods. The mutation provider will add the writer;
|
|
36
|
+
a native query cannot satisfy a command's writer requirement. Expected errors
|
|
37
|
+
are values in the Effect error channel, rather than thrown exceptions.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Context, Effect } from 'effect'
|
|
41
|
+
import {
|
|
42
|
+
defineSchema,
|
|
43
|
+
defineTable,
|
|
44
|
+
queryGeneric,
|
|
45
|
+
mutationGeneric,
|
|
46
|
+
type DataModelFromSchemaDefinition,
|
|
47
|
+
type QueryBuilder,
|
|
48
|
+
type MutationBuilder,
|
|
49
|
+
type GenericQueryCtx,
|
|
50
|
+
type GenericMutationCtx,
|
|
51
|
+
} from 'convex/server'
|
|
52
|
+
import { ConvexError, v } from 'convex/values'
|
|
53
|
+
import { z } from 'zod'
|
|
54
|
+
import { createEffectFoundation, effectApiBuilder } from 'cvx-kit/effect'
|
|
55
|
+
import type { AuditEntryInput } from 'cvx-kit/components/foundation'
|
|
56
|
+
|
|
57
|
+
export const schema = defineSchema({
|
|
58
|
+
documents: defineTable({ key: v.string(), owner: v.string(), title: v.string() }).index(
|
|
59
|
+
'by_owner_key',
|
|
60
|
+
['owner', 'key'],
|
|
61
|
+
),
|
|
62
|
+
audits: defineTable({ actor: v.string(), operation: v.string(), documentKey: v.string() }),
|
|
63
|
+
})
|
|
64
|
+
type DataModel = DataModelFromSchemaDefinition<typeof schema>
|
|
65
|
+
type QueryCtx = GenericQueryCtx<DataModel>
|
|
66
|
+
type MutationCtx = GenericMutationCtx<DataModel>
|
|
67
|
+
const nativeQuery: QueryBuilder<DataModel, 'public'> = queryGeneric
|
|
68
|
+
const nativeMutation: MutationBuilder<DataModel, 'public'> = mutationGeneric
|
|
69
|
+
|
|
70
|
+
type Host = { actor: string }
|
|
71
|
+
class DocumentMissing {
|
|
72
|
+
readonly _tag = 'DocumentMissing'
|
|
73
|
+
}
|
|
74
|
+
class RenameRejected {
|
|
75
|
+
readonly _tag = 'RenameRejected'
|
|
76
|
+
}
|
|
77
|
+
class AccessDenied {
|
|
78
|
+
readonly _tag = 'AccessDenied'
|
|
79
|
+
}
|
|
80
|
+
class DocumentReader extends Context.Service<
|
|
81
|
+
DocumentReader,
|
|
82
|
+
{ title: (key: string, actor: string) => Effect.Effect<string, DocumentMissing> }
|
|
83
|
+
>()('DocumentReader') {}
|
|
84
|
+
class DocumentWriter extends Context.Service<
|
|
85
|
+
DocumentWriter,
|
|
86
|
+
{
|
|
87
|
+
rename: (key: string, title: string, actor: string) => Effect.Effect<void, DocumentMissing>
|
|
88
|
+
audit: (actor: string, entry: AuditEntryInput) => Effect.Effect<void>
|
|
89
|
+
}
|
|
90
|
+
>()('DocumentWriter') {}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Cohesive domain declarations
|
|
94
|
+
|
|
95
|
+
`label` is an ordinary async handler, and `preview` uses a generator. `title` uses `Effect.fn` with explicit input
|
|
96
|
+
and host annotations. `rename` returns an `Effect.gen` program, yields the
|
|
97
|
+
query directly, then uses the injected writer. Its pipe converts the query's
|
|
98
|
+
missing-document error into a domain-specific rename rejection before adding
|
|
99
|
+
a span. The audit entry sits beside the command, and the shared audit policy
|
|
100
|
+
uses the same invocation's writer.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const foundation = createEffectFoundation({
|
|
104
|
+
observability: {
|
|
105
|
+
enabled: false,
|
|
106
|
+
classifyError: () => ({ outcome: 'failed' as const, errorCode: 'DOCUMENT_OPERATION_FAILED' }),
|
|
107
|
+
},
|
|
108
|
+
checkPermission: (host: Host) =>
|
|
109
|
+
host.actor.length > 0 ? Effect.void : Effect.fail(new AccessDenied()),
|
|
110
|
+
writeAudit: (host: Host, entry: AuditEntryInput) =>
|
|
111
|
+
Effect.gen(function* () {
|
|
112
|
+
const writer = yield* DocumentWriter
|
|
113
|
+
yield* writer.audit(host.actor, entry)
|
|
114
|
+
}),
|
|
115
|
+
})
|
|
116
|
+
const keyInput = z.object({ key: z.string().min(1) })
|
|
117
|
+
const renameInput = keyInput.extend({ title: z.string().trim().min(1) })
|
|
118
|
+
const titleResult = z.string().transform((title) => ({ title }))
|
|
119
|
+
|
|
120
|
+
export const documents = foundation.Query({
|
|
121
|
+
context: (host: Host) => host,
|
|
122
|
+
operations: ({ query }) => ({
|
|
123
|
+
label: query({
|
|
124
|
+
input: keyInput,
|
|
125
|
+
result: z.string(),
|
|
126
|
+
handler: async (input) => `Document ${input.key}`,
|
|
127
|
+
}),
|
|
128
|
+
preview: query({
|
|
129
|
+
input: keyInput,
|
|
130
|
+
result: titleResult,
|
|
131
|
+
permission: 'documents:read',
|
|
132
|
+
handler: (input, host) =>
|
|
133
|
+
Effect.gen(function* () {
|
|
134
|
+
const reader = yield* DocumentReader
|
|
135
|
+
return yield* reader.title(input.key, host.actor)
|
|
136
|
+
}),
|
|
137
|
+
}),
|
|
138
|
+
title: query({
|
|
139
|
+
input: keyInput,
|
|
140
|
+
result: titleResult,
|
|
141
|
+
permission: 'documents:read',
|
|
142
|
+
handler: Effect.fn('documents.title')(function* (
|
|
143
|
+
input: z.output<typeof keyInput>,
|
|
144
|
+
host: Host,
|
|
145
|
+
) {
|
|
146
|
+
const reader = yield* DocumentReader
|
|
147
|
+
return yield* reader.title(input.key, host.actor)
|
|
148
|
+
}),
|
|
149
|
+
}),
|
|
150
|
+
}),
|
|
151
|
+
})
|
|
152
|
+
export const documentCommands = foundation.Command({
|
|
153
|
+
context: (host: Host) => host,
|
|
154
|
+
operations: ({ command }) => ({
|
|
155
|
+
rename: command({
|
|
156
|
+
input: renameInput,
|
|
157
|
+
result: titleResult,
|
|
158
|
+
classification: 'business',
|
|
159
|
+
permission: 'documents:write',
|
|
160
|
+
handler: (input, host) =>
|
|
161
|
+
Effect.gen(function* () {
|
|
162
|
+
// A nested query stays in the same Effect; no inner runner.
|
|
163
|
+
yield* documents.exec('title', { key: input.key }, host)
|
|
164
|
+
const writer = yield* DocumentWriter
|
|
165
|
+
yield* writer.rename(input.key, input.title, host.actor)
|
|
166
|
+
return input.title
|
|
167
|
+
}).pipe(
|
|
168
|
+
Effect.catchTag('DocumentMissing', () => Effect.fail(new RenameRejected())),
|
|
169
|
+
Effect.withSpan('documents.rename'),
|
|
170
|
+
),
|
|
171
|
+
guard: (host) => (host.actor.length > 0 ? Effect.void : Effect.fail(new AccessDenied())),
|
|
172
|
+
middleware: ({ next }) => next().pipe(Effect.withSpan('documents.command')),
|
|
173
|
+
audit: ({ command: input, result }, host) => ({
|
|
174
|
+
actorId: host.actor,
|
|
175
|
+
operation: 'documents.rename',
|
|
176
|
+
aggregate: { type: 'document', id: input.key },
|
|
177
|
+
metadata: { title: result.title },
|
|
178
|
+
}),
|
|
179
|
+
}),
|
|
180
|
+
}),
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Request services and shared API setup
|
|
185
|
+
|
|
186
|
+
Construct the services once in each shared builder provider. Endpoint handlers
|
|
187
|
+
do not call `Context.make`, `Effect.runPromise`, or an extra execution helper.
|
|
188
|
+
The database Promise wrappers below intentionally preserve unexpected database
|
|
189
|
+
failures as defects. For external integrations with expected failures, use
|
|
190
|
+
`Effect.tryPromise` and return your own typed error.
|
|
191
|
+
|
|
192
|
+
`mapError` projects an expected failure to a small `ConvexError` payload. It
|
|
193
|
+
receives `unknown`, so narrow it before projection. Defects, interruption,
|
|
194
|
+
and combined causes do not become expected failures through this hook; the
|
|
195
|
+
adapter preserves those failure causes. The registered function still fails,
|
|
196
|
+
which is required for mutation rollback.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
function readerFor(ctx: QueryCtx): Context.Context<DocumentReader> {
|
|
200
|
+
return Context.make(DocumentReader, {
|
|
201
|
+
title: (key, actor) =>
|
|
202
|
+
Effect.gen(function* () {
|
|
203
|
+
const document = yield* Effect.promise(() =>
|
|
204
|
+
ctx.db
|
|
205
|
+
.query('documents')
|
|
206
|
+
.withIndex('by_owner_key', (q) => q.eq('owner', actor).eq('key', key))
|
|
207
|
+
.unique(),
|
|
208
|
+
)
|
|
209
|
+
if (!document) return yield* Effect.fail(new DocumentMissing())
|
|
210
|
+
return document.title
|
|
211
|
+
}),
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
function writerFor(ctx: MutationCtx): Context.Context<DocumentWriter> {
|
|
215
|
+
return Context.make(DocumentWriter, {
|
|
216
|
+
rename: (key, title, actor) =>
|
|
217
|
+
Effect.gen(function* () {
|
|
218
|
+
const document = yield* Effect.promise(() =>
|
|
219
|
+
ctx.db
|
|
220
|
+
.query('documents')
|
|
221
|
+
.withIndex('by_owner_key', (q) => q.eq('owner', actor).eq('key', key))
|
|
222
|
+
.unique(),
|
|
223
|
+
)
|
|
224
|
+
if (!document) return yield* Effect.fail(new DocumentMissing())
|
|
225
|
+
yield* Effect.promise(() => ctx.db.patch(document._id, { title }))
|
|
226
|
+
}),
|
|
227
|
+
audit: (actor, entry) =>
|
|
228
|
+
Effect.promise(() =>
|
|
229
|
+
ctx.db.insert('audits', {
|
|
230
|
+
actor,
|
|
231
|
+
operation: entry.operation,
|
|
232
|
+
documentKey: entry.aggregate.id,
|
|
233
|
+
}),
|
|
234
|
+
).pipe(Effect.asVoid),
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
function authenticatedHost(ctx: QueryCtx) {
|
|
238
|
+
return Effect.gen(function* () {
|
|
239
|
+
const identity = yield* Effect.promise(() => ctx.auth.getUserIdentity())
|
|
240
|
+
if (!identity) return yield* Effect.fail(new AccessDenied())
|
|
241
|
+
return { actor: identity.tokenIdentifier }
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
function projectExpectedError(error: unknown) {
|
|
245
|
+
if (error instanceof DocumentMissing || error instanceof RenameRejected)
|
|
246
|
+
return new ConvexError({ code: 'DOCUMENT_NOT_FOUND' })
|
|
247
|
+
if (error instanceof AccessDenied) return new ConvexError({ code: 'ACCESS_DENIED' })
|
|
248
|
+
return new Error('Document operation failed', { cause: error })
|
|
249
|
+
}
|
|
250
|
+
const queryApi = effectApiBuilder(nativeQuery, {
|
|
251
|
+
services: readerFor,
|
|
252
|
+
mapError: projectExpectedError,
|
|
253
|
+
})
|
|
254
|
+
const mutationApi = effectApiBuilder(nativeMutation, {
|
|
255
|
+
services: (ctx) => Context.merge(readerFor(ctx), writerFor(ctx)),
|
|
256
|
+
mapError: projectExpectedError,
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Consume the domain operations in the API
|
|
261
|
+
|
|
262
|
+
The Convex callback remains `(ctx, input)`; the domain callback and executor
|
|
263
|
+
use input before context/host. Both endpoints are normal native registrations
|
|
264
|
+
with argument and return validators. Each handler authenticates the request
|
|
265
|
+
and returns the domain Effect. The adapter supplies the services required by
|
|
266
|
+
the complete nested program.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
export const documentTitle = queryApi({
|
|
270
|
+
args: { key: v.string() },
|
|
271
|
+
returns: v.object({ title: v.string() }),
|
|
272
|
+
handler: (ctx, input) =>
|
|
273
|
+
Effect.gen(function* () {
|
|
274
|
+
const host = yield* authenticatedHost(ctx)
|
|
275
|
+
return yield* documents.exec('title', input, host)
|
|
276
|
+
}),
|
|
277
|
+
})
|
|
278
|
+
export const renameDocument = mutationApi({
|
|
279
|
+
args: { key: v.string(), title: v.string() },
|
|
280
|
+
returns: v.object({ title: v.string() }),
|
|
281
|
+
handler: (ctx, input) =>
|
|
282
|
+
Effect.gen(function* () {
|
|
283
|
+
const host = yield* authenticatedHost(ctx)
|
|
284
|
+
return yield* documentCommands.exec('rename', input, host)
|
|
285
|
+
}),
|
|
286
|
+
})
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Types and schema boundaries
|
|
290
|
+
|
|
291
|
+
The command accepts `z.input<typeof renameInput>`. Its handler sees
|
|
292
|
+
`z.output<typeof renameInput>`, so the title has already been trimmed. The
|
|
293
|
+
handler returns a string, which is the input of `titleResult`; the registry's
|
|
294
|
+
final result is `{ title: string }`, the output of that transform. Middleware
|
|
295
|
+
`next()` returns the handler result before the final result parser, so wrap
|
|
296
|
+
or transform that value according to the result schema's input contract.
|
|
297
|
+
|
|
298
|
+
TypeScript preserves the selected operation's success, expected error, and
|
|
299
|
+
service requirements. The title query requires `DocumentReader`; rename
|
|
300
|
+
requires `DocumentReader | DocumentWriter` and can fail with `AccessDenied`
|
|
301
|
+
or `RenameRejected`. The API builder checks that its provider supplies those
|
|
302
|
+
services. A reader-only provider cannot register rename, and an incompatible
|
|
303
|
+
handler result cannot pass its declared native return validator.
|
|
304
|
+
|
|
305
|
+
This is server-side Effect composition. Generated Convex references retain
|
|
306
|
+
native argument/result types and visibility; they do not carry Effect's typed
|
|
307
|
+
error or service channels over the transport. Clients receive ordinary Convex
|
|
308
|
+
results or errors.
|
|
309
|
+
|
|
310
|
+
For inferred declarations, put `handler` and `guard` before `middleware`, as
|
|
311
|
+
above. Middleware-first declarations need explicit callback and error/service
|
|
312
|
+
annotations where inference cannot recover the later handler's channels.
|
|
313
|
+
`Effect.fn` accepts typed function arguments; `Effect.gen` accepts a generator
|
|
314
|
+
whose yielded Effects determine its channels. Use `(input, context) =>
|
|
315
|
+
Effect.gen(function* () { ... })` when those values come from the declaration.
|
|
316
|
+
A constructed `Effect.gen` program cannot itself receive handler arguments.
|
|
317
|
+
|
|
318
|
+
## Hooks, replay, and execution scope
|
|
319
|
+
|
|
320
|
+
A command runs input parsing, observation setup and context resolution, then
|
|
321
|
+
permission checking, preparation, middleware, shared and operation guards,
|
|
322
|
+
handler, final result parsing, audit construction, aggregate checking, audit
|
|
323
|
+
writing, and completion. Observation records the outcome. Query permission
|
|
324
|
+
checking runs before its context resolver, followed by middleware, guards,
|
|
325
|
+
handler, and result parsing. Command audit/completion hooks do not exist on
|
|
326
|
+
queries.
|
|
327
|
+
|
|
328
|
+
Optional command `prepare(context, input)` returns `{ kind: 'execute',
|
|
329
|
+
complete }` or `{ kind: 'replay', result }`. The completion callback receives
|
|
330
|
+
the parsed final result. Permission checking still precedes replay. A replay
|
|
331
|
+
result is parsed with `replayResult` when supplied, otherwise with `result`,
|
|
332
|
+
and skips middleware, guards, handler, audit writing, and completion.
|
|
333
|
+
Middleware receives `operation`, parsed `input` (also `command`), `context`,
|
|
334
|
+
and `next`; it may provide an enriched context to `next({ context })`.
|
|
335
|
+
Each execution allows `next` once. Audit construction sees the original domain
|
|
336
|
+
context, and the audit writer sees the original trusted host.
|
|
337
|
+
|
|
338
|
+
The provider may return a `Context`, a Promise of a Context, or an Effect that
|
|
339
|
+
builds a Context. It runs separately for every native invocation. The shared
|
|
340
|
+
runner opens an Effect scope, evaluates the provider and handler, and awaits
|
|
341
|
+
scope finalizers before returning or throwing. Use `Effect.acquireRelease`
|
|
342
|
+
inside a provider for resources that actually need cleanup. Nested domain
|
|
343
|
+
operations share that runner and scope; do not run them independently inside
|
|
344
|
+
handlers. Cleanup failure remains an endpoint failure, including when it
|
|
345
|
+
combines with a handler failure.
|
|
346
|
+
|
|
347
|
+
## Convex boundaries and observed support
|
|
348
|
+
|
|
349
|
+
A domain query is a reusable read operation; its name does not choose the
|
|
350
|
+
native function kind. A native Convex query must perform deterministic database
|
|
351
|
+
reads. Native mutations can write, and a failed command, result validator,
|
|
352
|
+
audit writer, completion hook, or awaited cleanup rolls back that mutation's
|
|
353
|
+
writes. Actions are the place for HTTP and other external integrations.
|
|
354
|
+
An action's separate `ctx.runMutation` calls are separate transactions: a
|
|
355
|
+
later failure does not undo an earlier successful mutation.
|
|
356
|
+
|
|
357
|
+
The packed public facade was compiled and executed under npm and Bun with
|
|
358
|
+
Effect 4.0.1 and `convex-helpers` 0.1.123. Legacy packed consumers also ran with
|
|
359
|
+
Effect absent. Local Convex 1.45.0 checks exercised ordinary, generator, and
|
|
360
|
+
named-function query composition; request cleanup; expected and combined
|
|
361
|
+
failures; mutation validation and lifecycle rollback; and an action using
|
|
362
|
+
loopback HTTP with separate mutation transactions. `Effect.withSpan` was
|
|
363
|
+
accepted in those programs; this does not establish exported telemetry,
|
|
364
|
+
background fibers, arbitrary concurrency, or every Effect runtime capability
|
|
365
|
+
in Convex.
|
|
366
|
+
|
|
367
|
+
The standalone packed compiler includes `ESNext.Disposable` in its library
|
|
368
|
+
set for Effect 4 declarations. If your project's TypeScript environment does
|
|
369
|
+
not already provide those globals, include that library as well.
|
|
370
|
+
|
|
371
|
+
## Adopt one domain at a time
|
|
372
|
+
|
|
373
|
+
Keep existing Promise registries and API endpoints while adding the optional
|
|
374
|
+
facade for a new domain. Move each operation's handler beside its schemas and
|
|
375
|
+
hooks, switch domain callbacks to input-first arguments, and return the new
|
|
376
|
+
registry's lazy Effect to a shared API adapter. A normal async handler works
|
|
377
|
+
in the cohesive declaration without a generator; wrap expected Promise failures
|
|
378
|
+
only when you want a typed error channel.
|
|
379
|
+
|
|
380
|
+
The [command migration guide](./commands.md) shows a complete legacy-to-Effect
|
|
381
|
+
pair and the existing lifecycle contract. The [authentication guide](./auth.md)
|
|
382
|
+
shows how to wrap trusted custom builders with `effectZodApiBuilder`, so auth,
|
|
383
|
+
tenancy, validators, row-level policies, and triggers run through their
|
|
384
|
+
existing builder before Effect request services are provisioned.
|
package/docs/oxlint.md
CHANGED
|
@@ -5,8 +5,8 @@ checks applications using the kit against `conventions.md`, the authoritative
|
|
|
5
5
|
source for folder structure and file anatomy. The package repository has a
|
|
6
6
|
separate, unpublished plugin for its own `src/` layout.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
library authoring. See `upgrading.md` when migrating that configuration.
|
|
8
|
+
The consumer plugin was introduced in 0.1.5; the initial 0.1.4 plugin targeted
|
|
9
|
+
library authoring. The five architecture rules below were added in 0.1.6. See `upgrading.md` when migrating that configuration.
|
|
10
10
|
|
|
11
11
|
## Configuration
|
|
12
12
|
|
|
@@ -36,31 +36,36 @@ For another location, set `convexDir` on **each enabled rule**:
|
|
|
36
36
|
{ "cvx/no-raw-builders": ["error", { "convexDir": "src/server/convex" }] }
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
Source rules ignore files outside that directory, `_generated/`, `__tests__/`,
|
|
40
|
+
and test/spec files. `project-structure` reads the whole backend, including tests. They do not
|
|
41
41
|
offer automatic fixes for architectural moves.
|
|
42
42
|
|
|
43
43
|
## Coverage
|
|
44
44
|
|
|
45
45
|
Every name below has the `cvx/` prefix in lint configuration.
|
|
46
46
|
|
|
47
|
-
| Rule
|
|
48
|
-
|
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `no-
|
|
56
|
-
| `no-
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `no-
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
47
|
+
| Rule | Detects | Documentation |
|
|
48
|
+
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
49
|
+
| `project-structure` | Root folder allowlist, domain ownership, colocated runnable tests, misplaced tests, symlinks, parse failures, runtime domain-module cycles, and shared implementations with only one domain consumer. | Folder structure and Tests |
|
|
50
|
+
| `root-wiring-only` | Unknown root files without a component facade, root function declarations, nondelegating callbacks, branches, loops, direct writes/network calls, and inline root schema construction. | Root files |
|
|
51
|
+
| `application-orchestration` | Files without two domain dependencies, provider construction, external runtime dependencies, database access, validator construction, and business branches. | Module grain |
|
|
52
|
+
| `internal-function-ownership` | Internal/system registrations at root, in public API adapters, or in domain/shared. | Folder structure |
|
|
53
|
+
| `domain-file-responsibilities` | Impure rules, query writes/mutations, database access in declarative files, misplaced direct external I/O, and imports that violate these roles. | File anatomy |
|
|
54
|
+
| `root-facade-ownership` | `createAuthFunctions`, `createTriggers`, and `new Foundation` outside their root files, or repeated in one file. | `conventions.md`, Root files |
|
|
55
|
+
| `no-raw-builders` | Runtime imports of raw Convex builders outside `functions.ts`; namespace calls to raw generic builders. | `auth.md`; `llms.md`, Non-negotiable rules |
|
|
56
|
+
| `no-handwritten-references` | Calls to Convex `makeFunctionReference`, including import aliases. | `conventions.md`, Import rules |
|
|
57
|
+
| `domain-import-boundaries` | Reverse dependencies into `api/`, domain imports of `application/`, forbidden sibling imports, and API adapters importing another domain module. | `conventions.md`, Module grain and Import rules |
|
|
58
|
+
| `public-functions-in-api` | Calls to root `auth*`, `role*`, or `admin*` constructors outside `api/`. | `conventions.md`, Folder structure |
|
|
59
|
+
| `thin-api-adapters` | `if`, `switch`, ternary branches, top-level private helpers, and direct `ctx.db` writes in `api/`. | `conventions.md`, The adapter file |
|
|
60
|
+
| `no-inline-enums` | Literal arrays passed to imported Zod `enum`. Use named vocabulary tuples. | `conventions.md`, Vocabularies |
|
|
61
|
+
| `no-unbounded-reads` | Direct `ctx.db.query(...).collect()` chains and `ctx.include(...).resolve()` chains in host code. | `llms.md`, Bounded reads; `zod-table.md` |
|
|
62
|
+
| `no-manual-timestamps` | Explicit `createdAt`/`updatedAt` fields in object literals passed to direct `ctx.db` writes. | `triggers.md` |
|
|
63
|
+
| `component-boundaries` | Local portable components importing host/sibling files; host imports bypassing component client facades; private package component subpaths. | `architecture.md`, Injection, not import |
|
|
64
|
+
| `no-component-env` | Direct `process.env` access, process import aliases, environment destructuring, and named env imports inside local portable components. | `conventions.md`, Import rules |
|
|
65
|
+
| `schema-file-boundaries` | Host `defineTable`, misplaced `defineSchema` or kit table constructors, and misplaced `.table.index/searchIndex/vectorIndex` chains. | `conventions.md`, Shapes and topology |
|
|
66
|
+
| `no-internal-reexports` | Re-exports and import-then-export aliases inside `convex/`. | `conventions.md`, Universal file order |
|
|
67
|
+
| `public-api-first` | Private function declarations before the last export. | `conventions.md`, Universal file order |
|
|
68
|
+
| `named-private-helpers` | Top-level private arrow/function-expression variables. | `conventions.md`, Universal file order |
|
|
64
69
|
|
|
65
70
|
## Allowed patterns
|
|
66
71
|
|
|
@@ -69,7 +74,8 @@ Every name below has the `cvx/` prefix in lint configuration.
|
|
|
69
74
|
elsewhere. `httpAction` remains available for HTTP routing.
|
|
70
75
|
- Domain modules may import sibling `rules.ts` and `queries.ts`, as explicitly
|
|
71
76
|
allowed by the module-grain section. Cross-module writes go through the
|
|
72
|
-
optional `application/` layer. `domain/table.ts`
|
|
77
|
+
optional `application/` layer. `domain/table.ts` only imports module table maps
|
|
78
|
+
and combines them with `createModule` from `cvx-kit/zod-table`.
|
|
73
79
|
- API adapters may delegate and perform the documented bounded inline query
|
|
74
80
|
and DTO projection. `system*` constructors may live in domain modules.
|
|
75
81
|
- Authored components under `convex/components/<name>/` may use their own raw
|
|
@@ -84,8 +90,9 @@ Every name below has the `cvx/` prefix in lint configuration.
|
|
|
84
90
|
## What still needs review and tests
|
|
85
91
|
|
|
86
92
|
These are syntax and lexical-binding checks, not whole-program validation.
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
Import-boundary rules resolve relative imports and tsconfig aliases (including
|
|
94
|
+
extended configs) using the package resolver. Symlinked source is rejected by
|
|
95
|
+
the project scan. Computed import paths are not resolved. Named import aliases and namespaces are recognized where documented;
|
|
89
96
|
arbitrary variable reassignment, wrapper functions, destructured database
|
|
90
97
|
methods, and calls split across variables are not traced. Database checks
|
|
91
98
|
recognize the documented `ctx.db` member shape, not its runtime type.
|
|
@@ -93,6 +100,47 @@ recognize the documented `ctx.db` member shape, not its runtime type.
|
|
|
93
100
|
The plugin does not prove DTO redaction, complete command/audit coverage,
|
|
94
101
|
tenant/RLS correctness, unique table ownership across files, required facade
|
|
95
102
|
existence, exactly one component mount, numeric read limits, or vocabulary
|
|
96
|
-
tuple ownership.
|
|
97
|
-
|
|
103
|
+
tuple ownership. Run the behavioral tests described in `maintainability.md`. The package owns
|
|
104
|
+
structural enforcement; applications do not need copied architecture tests. A clean lint run is not
|
|
98
105
|
evidence of those runtime guarantees.
|
|
106
|
+
|
|
107
|
+
## Whole-project validation
|
|
108
|
+
|
|
109
|
+
Enable `cvx/project-structure` and lint the **whole** configured backend. The
|
|
110
|
+
scan reports on `convex.config.ts`, or the first non-test source file if no
|
|
111
|
+
configuration file exists. That anchor must be part of the lint invocation;
|
|
112
|
+
changed-file-only runs and cached lint runs can skip project checks. Use a full,
|
|
113
|
+
uncached lint run in CI. The scan ignores generated code, reads current files on
|
|
114
|
+
every invocation, and does not execute application code.
|
|
115
|
+
|
|
116
|
+
For standalone tooling, including an empty/missing backend, use the same packaged
|
|
117
|
+
checker directly:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { checkArchitecture } from 'cvx-kit/oxlint'
|
|
121
|
+
|
|
122
|
+
const issues = checkArchitecture({ cwd: process.cwd(), convexDir: 'convex' })
|
|
123
|
+
for (const issue of issues) console.error(`${issue.file}: ${issue.message}`)
|
|
124
|
+
if (issues.length) process.exitCode = 1
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
No fixed module scaffold is required. Type-only modules and grouping folders
|
|
128
|
+
need no test directory. Runtime schema/constants files do; root wiring shares
|
|
129
|
+
root tests. Test files must be directly inside the owning `__tests__/` directory.
|
|
130
|
+
Recognized test entrypoints are `test`/`it` globals and imports from Vitest,
|
|
131
|
+
Vite+, Bun, Node, or Jest, including import aliases and namespaces. Empty,
|
|
132
|
+
TODO, skipped, and conditionally skipped/run tests do not satisfy the minimum.
|
|
133
|
+
A nonempty callback is a presence check, not evidence of meaningful assertions,
|
|
134
|
+
execution, or coverage. Run the tests in CI.
|
|
135
|
+
|
|
136
|
+
Cycle detection operates on runtime ESM imports/re-exports and literal dynamic
|
|
137
|
+
imports between `domain/<module>` owners, including nested files. Type-only
|
|
138
|
+
imports do not create runtime cycle edges. Arbitrary CommonJS wrappers, computed
|
|
139
|
+
imports, implicit generated function-reference calls, and value flow are not
|
|
140
|
+
whole-program analyzed. Rules report recognizable violations; clean lint does
|
|
141
|
+
not prove arbitrary business logic is absent.
|
|
142
|
+
|
|
143
|
+
Shared ownership follows static runtime imports through shared helpers. A shared
|
|
144
|
+
file reached from only one domain is reported for relocation. Unused code,
|
|
145
|
+
type-only consumers, and computed dependencies are not treated as proof of shared
|
|
146
|
+
ownership; their ownership still needs review.
|
package/docs/upgrading.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Upgrading to 0.1.0 — the new way of things
|
|
2
2
|
|
|
3
|
+
## 0.1.6 — stricter architecture checks
|
|
4
|
+
|
|
5
|
+
Enable `project-structure`, `root-wiring-only`, `application-orchestration`,
|
|
6
|
+
`internal-function-ownership`, and `domain-file-responsibilities` alongside the
|
|
7
|
+
existing rules. These are opt-in additions; existing enabled boundary rules now
|
|
8
|
+
also resolve tsconfig aliases and reject migration dependencies.
|
|
9
|
+
|
|
10
|
+
Move root implementation folders into their owning domains. Keep `application/`
|
|
11
|
+
limited to multi-domain coordination. Move centralized domain tests beside their
|
|
12
|
+
owners and add real tests for nested executable directories. Do not create empty
|
|
13
|
+
scaffold files. Replace copied structural architecture suites with the packaged
|
|
14
|
+
checks; retain tests that exercise actual application behavior and wiring.
|
|
15
|
+
|
|
3
16
|
## 0.1.5 — consumer Oxlint rules
|
|
4
17
|
|
|
5
18
|
The initial 0.1.4 plugin incorrectly targeted this package's `src/` layout.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cvx-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -30,6 +30,22 @@
|
|
|
30
30
|
"types": "./dist/zod-table.d.mts",
|
|
31
31
|
"import": "./dist/zod-table.mjs"
|
|
32
32
|
},
|
|
33
|
+
"./effect": {
|
|
34
|
+
"types": "./dist/effect.d.mts",
|
|
35
|
+
"import": "./dist/effect.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./contracts": {
|
|
38
|
+
"types": "./dist/contracts.d.mts",
|
|
39
|
+
"import": "./dist/contracts.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./idempotency": {
|
|
42
|
+
"types": "./dist/idempotency.d.mts",
|
|
43
|
+
"import": "./dist/idempotency.mjs"
|
|
44
|
+
},
|
|
45
|
+
"./workflow": {
|
|
46
|
+
"types": "./dist/workflow.d.mts",
|
|
47
|
+
"import": "./dist/workflow.mjs"
|
|
48
|
+
},
|
|
33
49
|
"./auth": {
|
|
34
50
|
"types": "./dist/auth.d.mts",
|
|
35
51
|
"import": "./dist/auth.mjs"
|
|
@@ -109,13 +125,15 @@
|
|
|
109
125
|
"scripts": {
|
|
110
126
|
"lint": "vp lint",
|
|
111
127
|
"check": "vp check",
|
|
112
|
-
"test:lint": "vp test run tools/oxlint/cvx
|
|
128
|
+
"test:lint": "vp test run tools/oxlint/cvx",
|
|
113
129
|
"build": "vp pack && node scripts/convex-config-bridge.mjs && publint",
|
|
114
130
|
"typecheck": "tsc --noEmit",
|
|
115
131
|
"test": "vp test run --exclude test/package-contents.test.mjs",
|
|
116
132
|
"test:package": "bun run build && vp test run test/package-contents.test.mjs",
|
|
117
133
|
"test:package:ids": "bun run build && node scripts/test-packed-ids.mjs",
|
|
118
134
|
"test:package:oxlint": "bun run build && node scripts/test-packed-oxlint.mjs",
|
|
135
|
+
"test:package:effect": "bun run build && node scripts/test-packed-effect.mjs",
|
|
136
|
+
"test:package:smoke:effect": "bun run build && node scripts/smoke-packed-effect.mjs",
|
|
119
137
|
"test:package:smoke:bun": "bun scripts/smoke-packed-components.mjs bun",
|
|
120
138
|
"test:package:smoke:npm": "node scripts/smoke-packed-components.mjs npm",
|
|
121
139
|
"docs:sync": "rm -f docs/*.md && cp src/docs/*.md docs/",
|
|
@@ -127,7 +145,9 @@
|
|
|
127
145
|
"dependencies": {
|
|
128
146
|
"@convex-dev/workflow": "0.4.6",
|
|
129
147
|
"@oxlint/plugins": "1.79.0",
|
|
130
|
-
"convex-audit-log": "^0.2.0"
|
|
148
|
+
"convex-audit-log": "^0.2.0",
|
|
149
|
+
"oxc-parser": "0.150.0",
|
|
150
|
+
"oxc-resolver": "11.24.2"
|
|
131
151
|
},
|
|
132
152
|
"devDependencies": {
|
|
133
153
|
"@edge-runtime/vm": "^5.0.0",
|
|
@@ -135,6 +155,7 @@
|
|
|
135
155
|
"convex": "1.45.0",
|
|
136
156
|
"convex-helpers": "0.1.124",
|
|
137
157
|
"convex-test": "^0.0.56",
|
|
158
|
+
"effect": "4.0.1",
|
|
138
159
|
"oxlint": "1.79.0",
|
|
139
160
|
"publint": "^0.3.24",
|
|
140
161
|
"typescript": "7.0.2",
|
|
@@ -144,6 +165,19 @@
|
|
|
144
165
|
"peerDependencies": {
|
|
145
166
|
"convex": "^1.45.0",
|
|
146
167
|
"convex-helpers": ">=0.1.123 <0.2.0",
|
|
168
|
+
"effect": ">=4.0.1 <5",
|
|
147
169
|
"zod": "^4.5.4"
|
|
170
|
+
},
|
|
171
|
+
"peerDependenciesMeta": {
|
|
172
|
+
"effect": {
|
|
173
|
+
"optional": true
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
"devEngines": {
|
|
177
|
+
"packageManager": {
|
|
178
|
+
"name": "bun",
|
|
179
|
+
"version": "1.4.2",
|
|
180
|
+
"onFail": "download"
|
|
181
|
+
}
|
|
148
182
|
}
|
|
149
183
|
}
|