cvx-kit 0.1.15 → 0.1.16

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.
Files changed (45) hide show
  1. package/README.md +59 -38
  2. package/dist/relations.mjs +1 -1
  3. package/dist/relations.mjs.map +1 -1
  4. package/dist/zod-table.d.mts +59 -2
  5. package/dist/zod-table.mjs +1 -1
  6. package/dist/zod-table.mjs.map +1 -1
  7. package/package.json +10 -3
  8. package/AGENTS.md +0 -47
  9. package/CHANGELOG.md +0 -337
  10. package/ROADMAP.md +0 -126
  11. package/docs/agent-tools.md +0 -41
  12. package/docs/approvals.md +0 -201
  13. package/docs/architecture.md +0 -124
  14. package/docs/auth.md +0 -443
  15. package/docs/command-context.md +0 -150
  16. package/docs/commands-legacy.md +0 -513
  17. package/docs/commands.md +0 -173
  18. package/docs/conventions.md +0 -345
  19. package/docs/crud.md +0 -66
  20. package/docs/domain-contracts.md +0 -148
  21. package/docs/domain-operations.md +0 -615
  22. package/docs/effect.md +0 -403
  23. package/docs/llms.md +0 -324
  24. package/docs/maintainability.md +0 -124
  25. package/docs/migration.md +0 -351
  26. package/docs/oxlint.md +0 -260
  27. package/docs/querying.md +0 -329
  28. package/docs/tenancy.md +0 -176
  29. package/docs/triggers.md +0 -77
  30. package/docs/upgrading.md +0 -287
  31. package/docs/webhooks.md +0 -58
  32. package/docs/zod-table.md +0 -278
  33. package/scripts/convex-config-bridge.mjs +0 -44
  34. package/scripts/convex-smoke-env.mjs +0 -33
  35. package/scripts/extract-changelog.mjs +0 -29
  36. package/scripts/owned-process.mjs +0 -140
  37. package/scripts/owned-process.test.mjs +0 -107
  38. package/scripts/smoke-packed-components.mjs +0 -341
  39. package/scripts/smoke-packed-effect.mjs +0 -439
  40. package/scripts/smoke-packed-querying.mjs +0 -298
  41. package/scripts/test-packed-auth.mjs +0 -72
  42. package/scripts/test-packed-effect.mjs +0 -482
  43. package/scripts/test-packed-ids.mjs +0 -150
  44. package/scripts/test-packed-oxlint.mjs +0 -167
  45. package/scripts/test-packed-querying.mjs +0 -185
@@ -1,482 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
- import {
3
- cpSync,
4
- mkdtempSync,
5
- mkdirSync,
6
- readFileSync,
7
- realpathSync,
8
- rmSync,
9
- writeFileSync,
10
- } from 'node:fs'
11
- import { tmpdir } from 'node:os'
12
- import { basename, join, resolve } from 'node:path'
13
-
14
- const root = resolve(import.meta.dirname, '..')
15
- const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
16
- const temporary = mkdtempSync(join(tmpdir(), 'cvx-kit-effect-'))
17
- const effectVersion = '4.0.1'
18
- const helpersVersion = '0.1.127'
19
- const nodeTypesVersion = '26.6.1'
20
- const domainGuidePath = join(root, 'src/docs/domain-operations.md')
21
- const domainGuideImports = new Set([
22
- './fixture-effect/convex/_generated/api',
23
- './fixture-effect/convex/_generated/server',
24
- './fixture-effect/convex/idempotency',
25
- './fixture-effect/convex/integratedRename',
26
- './fixture-effect/convex/_generated/dataModel',
27
- ])
28
- const extraTypeFixtures = [
29
- join(root, 'README.md'),
30
- join(root, 'src/docs/effect.md'),
31
- join(root, 'src/docs/commands.md'),
32
- join(root, 'src/docs/domain-contracts.md'),
33
- join(root, 'src/docs/command-context.md'),
34
- join(root, 'src/docs/commands-legacy.md'),
35
- join(root, 'src/docs/auth.md'),
36
- domainGuidePath,
37
- ...process.argv.slice(2).map((path) => resolve(path)),
38
- ]
39
-
40
- function run(command, args, cwd) {
41
- try {
42
- return execFileSync(command, args, { cwd, encoding: 'utf8', stdio: 'pipe' })
43
- } catch (error) {
44
- // Install output can contain registry credentials. Report compiler/runtime diagnostics only.
45
- if (!args.includes('install')) {
46
- process.stderr.write(error.stdout ?? '')
47
- process.stderr.write(error.stderr ?? '')
48
- }
49
- throw new Error(`${command} ${args.join(' ')} failed in ${basename(cwd)}`, {
50
- cause: error.code,
51
- })
52
- }
53
- }
54
-
55
- function copyPublicTypeFixture(fixture, filename, publicImport) {
56
- const source = readFileSync(join(root, 'test', filename), 'utf8')
57
- .replaceAll(publicImport, 'cvx-kit/effect')
58
- .replaceAll('../src/index', 'cvx-kit')
59
- .replaceAll('../src/errors', 'cvx-kit/errors')
60
- .replaceAll('../src/contracts', 'cvx-kit/contracts')
61
- .replaceAll('../src/idempotency', 'cvx-kit/idempotency')
62
- .replaceAll('../src/modules/contracts/errors', 'cvx-kit/errors')
63
- .replaceAll('../src/modules/contracts/idempotency', 'cvx-kit/idempotency')
64
- .replaceAll('../src/modules/contracts/workflow', 'cvx-kit/workflow')
65
- .replaceAll('../src/modules/contracts/exposure', 'cvx-kit/agent-tools')
66
- .replaceAll('../src/modules/effect/workflow', 'cvx-kit/effect')
67
- .replaceAll('../src/modules/effect/idempotency', 'cvx-kit/effect')
68
- .replaceAll('../src/modules/effect/foundation', 'cvx-kit/effect')
69
- .replaceAll('../src/modules/effect/schema', 'cvx-kit/effect')
70
- .replaceAll('../src/modules/effect/operation', 'cvx-kit/effect')
71
- .replaceAll('../src/modules/effect/crud', 'cvx-kit/effect')
72
- .replaceAll('../src/modules/effect/errors', 'cvx-kit/effect')
73
- .replaceAll('../src/components/foundation/client', 'cvx-kit/components/foundation')
74
- .replaceAll('../src/crud', 'cvx-kit/crud')
75
- .replaceAll('../src/zod-table', 'cvx-kit/zod-table')
76
- if (/from ['"]\.\.\/src\//.test(source))
77
- throw new Error(`${filename} still uses private source imports`)
78
- writeFileSync(join(fixture, filename), source)
79
- return filename
80
- }
81
-
82
- // Compile the published guide itself so prose edits cannot leave stale companion fixtures passing.
83
- function publishedTypeSources(path, contents) {
84
- if (!path.endsWith('.md')) return [contents]
85
- const marked = [
86
- ...contents.matchAll(/<!-- packed-effect-example -->\s*```ts\s*\n([\s\S]*?)^```/gm),
87
- ]
88
- if (marked.length) return marked.map((match) => match[1])
89
- return [[...contents.matchAll(/^```ts\s*\n([\s\S]*?)^```/gm)].map((match) => match[1]).join('\n')]
90
- }
91
-
92
- function copyExtraTypeFixtures(fixture, paths) {
93
- return paths.flatMap((path, index) => {
94
- const contents = readFileSync(path, 'utf8')
95
- const sources = publishedTypeSources(path, contents)
96
- const isDomainGuide = path === domainGuidePath
97
- if (isDomainGuide) {
98
- const companion = readFileSync(join(root, 'test/domain-operation-guide-types.ts'), 'utf8')
99
- const blocks = [...contents.matchAll(/^```ts\s*\n([\s\S]*?)^```/gm)]
100
- if (blocks.length !== 1 || sources.length !== 1 || sources[0].trim() !== companion.trim())
101
- throw new Error(
102
- 'Domain operation guide must contain its complete, current companion fixture',
103
- )
104
- cpSync(join(root, 'test/fixture-effect/convex'), join(fixture, 'fixture-effect/convex'), {
105
- recursive: true,
106
- })
107
- }
108
- return sources.map((source, snippet) => {
109
- if (!source.trim())
110
- throw new Error(`Documentation fixture ${basename(path)} has no TypeScript`)
111
- const imports = [...source.matchAll(/from ['"]([^'"]+)['"]/g)].map((match) => match[1])
112
- if (
113
- imports.some(
114
- (specifier) =>
115
- /^cvx-kit\/(?:src|dist)\//.test(specifier) ||
116
- (/^\.\.?\//.test(specifier) && !(isDomainGuide && domainGuideImports.has(specifier))),
117
- )
118
- ) {
119
- throw new Error(`Documentation fixture ${basename(path)} must use public package imports`)
120
- }
121
- const filename = isDomainGuide
122
- ? `domain-operation-guide-${snippet}.ts`
123
- : `doc-example-${index}-${snippet}.ts`
124
- writeFileSync(join(fixture, filename), source)
125
- return filename
126
- })
127
- })
128
- }
129
-
130
- const legacyRuntime = `
131
- import { createFoundation } from 'cvx-kit'
132
- import { defineDomainContract } from 'cvx-kit/contracts'
133
- import { strict as assert } from 'node:assert'
134
- import { createRequire } from 'node:module'
135
- import { Foundation } from 'cvx-kit/components/foundation'
136
- import { Foundation as RootFoundation } from 'cvx-kit'
137
- import { createAuthFunctions, defaultRoleMap } from 'cvx-kit/auth'
138
- import { createCrudCommands } from 'cvx-kit/crud'
139
- import { zodTable } from 'cvx-kit/zod-table'
140
- import { decodeContract, standardContract, zodContract } from 'cvx-kit/contracts'
141
- import { captureIdempotencyInvocation, transactionalIdempotency, canonicalConvexBytes } from 'cvx-kit/idempotency'
142
- import { defineErrorContract } from 'cvx-kit/errors'
143
- import { workflowTransition } from 'cvx-kit/workflow'
144
- import { selectOperation, bindOperationExecutor, createOperationTools, operationToolDialect } from 'cvx-kit/agent-tools'
145
- import { actionGeneric, queryGeneric, mutationGeneric, internalActionGeneric, internalQueryGeneric, internalMutationGeneric } from 'convex/server'
146
- import { z } from 'zod'
147
- import { convexTest } from 'convex-test'
148
- import { defineSchema, anyApi } from 'convex/server'
149
- const require = createRequire(import.meta.url)
150
- if (process.argv.includes('--without-effect')) {
151
- assert.throws(() => require.resolve('effect'), { code: 'MODULE_NOT_FOUND' }, 'optional peer was unexpectedly auto-installed')
152
- }
153
- assert.equal(RootFoundation, Foundation)
154
- assert.equal(typeof workflowTransition, 'function')
155
- const toolOwner = Symbol('packed-tools')
156
- const toolSelection = selectOperation(toolOwner, 'query', 'measure', { input: z.object({ title: z.string() }), result: z.number() })
157
- const toolExecutor = bindOperationExecutor(toolSelection, { owner: toolOwner, key: 'measure', execute: async raw => raw.title.length })
158
- const operationTools = createOperationTools({ measure: { operation: toolSelection, executor: toolExecutor, description: 'Measure a title', converter: { dialect: operationToolDialect, convert: () => ({ type: 'object', properties: { title: { type: 'string' } }, required: ['title'], additionalProperties: false }) } } })
159
- assert.deepEqual(await operationTools.measure.invoke({ title: 'packed' }), { _tag: 'Success', value: 6 })
160
- let normalizations = 0
161
- const normalized = zodContract(z.string().transform(async value => { normalizations++; return value.trim() }))
162
- assert.deepEqual(await decodeContract(normalized, ' packed '), { value: 'packed' })
163
- assert.equal(normalizations, 1)
164
- const receiverSchema = { '~standard': {
165
- version: 1, vendor: 'packed-receiver',
166
- validate(value) { return this.vendor === 'packed-receiver' ? { value } : { issues: [{ message: 'lost receiver' }] } },
167
- } }
168
- assert.deepEqual(await decodeContract(receiverSchema, 'packed'), { value: 'packed' })
169
- assert.deepEqual(await decodeContract(standardContract(receiverSchema), 'packed'), { value: 'packed' })
170
- const errors = defineErrorContract({ DENIED: { message: 'Denied', details: { key: z.string() } } })
171
- const envelope = errors.project(errors.create('DENIED', { key: 'packed' }))
172
- assert.deepEqual(errors.decode(JSON.parse(JSON.stringify(envelope))), envelope)
173
- const audits = []
174
- const { Command } = new Foundation({ functions: { status: null } }, {
175
- observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }), writeAudit: (_ctx, entry) => audits.push(entry) },
176
- })
177
- const operations = { measure: Command.operation({
178
- command: z.object({ title: z.string().transform(value => value.length) }), result: z.number(),
179
- classification: 'business', audit: () => null,
180
- }) }
181
- const execute = new Command(operations).exec({ operation: 'measure', handler: async (ctx, input) => ctx.offset + input.title })
182
- const pending = execute({ offset: 2 }, { title: 'abc' })
183
- assert.ok(pending instanceof Promise)
184
- assert.equal(await pending, 5)
185
- assert.equal(defaultRoleMap('member'), 'writer')
186
- const auth = createAuthFunctions({
187
- query: queryGeneric, mutation: mutationGeneric, action: actionGeneric,
188
- internalQuery: internalQueryGeneric, internalMutation: internalMutationGeneric, internalAction: internalActionGeneric,
189
- getAuthUser: async () => ({ id: 'actor' }), mapRole: defaultRoleMap, adminRoles: ['admin'],
190
- })
191
- const authenticated = auth.authQuery({ args: {}, returns: z.string(), handler: ctx => ctx.actor.userId + ':' + ctx.tenant })
192
- const authTest = convexTest(defineSchema({}), {
193
- './_generated/server.js': async () => ({ query: queryGeneric }),
194
- './auth.js': async () => ({ authenticated }),
195
- })
196
- assert.equal(await authTest.withIdentity({ subject: 'actor', org_id: 'org', role: 'member' }).query(anyApi.auth.authenticated, {}), 'actor:org')
197
- const table = zodTable('notes', () => ({ text: z.string() }), { commandFields: ['text'] })
198
- const crud = createCrudCommands({ Command, table, aggregateType: 'note', actor: () => 'actor' })
199
- const changes = []
200
- const ctx = { db: { insert: async (name, row) => { changes.push([name, row]); return 'note-id' }, patch: async (id, row) => changes.push([id, row]) } }
201
- assert.deepEqual(await crud.executeCreate(ctx, { text: 'packed' }), { id: 'note-id' })
202
- assert.deepEqual(await crud.executeUpdate(ctx, { id: 'note-id', data: { text: 'updated' } }), { ok: true })
203
- assert.deepEqual(changes, [['notes', { text: 'packed' }], ['note-id', { text: 'updated' }]])
204
- assert.equal(audits.length, 2)
205
- const bounds = { maxDepth: 8, maxNodes: 100, maxBytes: 4096, maxArrayLength: 20, maxObjectFields: 20 }
206
- assert.equal(canonicalConvexBytes({ b: { d: 2, c: 1 }, a: 0 }, bounds), canonicalConvexBytes({ a: 0, b: { c: 1, d: 2 } }, bounds))
207
- const invocation = captureIdempotencyInvocation({ binding: { kind: 'mutation', atomicity: 'same-mutation' }, identity: { operation: 'packed', scope: 'host', key: 'one' }, versions: { operation: '1', contract: '1', binding: '1', fingerprintPolicy: '1' }, rawInput: { title: 'raw' }, canonical: { bounds } })
208
- const ledger = []
209
- let authorizations = 0
210
- const idempotency = transactionalIdempotency({
211
- final: { replayResult: zodContract(z.object({ version: z.literal(1), value: z.string() }).transform(wire => wire.value)), encode: value => ({ version: 1, value }) },
212
- authorize: () => { authorizations++ }, lookup: () => ledger, fingerprint: bytes => bytes,
213
- claim: (_identity, receipt) => { ledger.push(receipt); return 0 }, complete: (index, receipt) => { ledger[index] = receipt },
214
- })
215
- const preparation = await idempotency.prepare(invocation)
216
- assert.equal(preparation.kind, 'execute')
217
- await preparation.complete('final!')
218
- const replay = await idempotency.prepare(invocation)
219
- assert.equal(replay.kind, 'replay')
220
- assert.deepEqual(await decodeContract(idempotency.replayResult, replay.result), { value: 'final!' })
221
- assert.equal(authorizations, 2)
222
- assert.equal(ledger.length, 1)
223
- const domain = defineDomainContract({ commands: { save: { input: z.string().transform(Number), result: z.number().transform(count => ({ count })), classification: 'business' } } })
224
- const regular = createFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: () => undefined })
225
- const domainCommands = regular.Command({ contract: domain, context: host => host, audit: () => null, operations: ({ command }) => ({ save: command.save({ handler: value => value + 1 }) }) })
226
- assert.deepEqual(await domainCommands.exec('save', '2', {}), { count: 3 })
227
- `
228
-
229
- const legacyTypes = `
230
- import { Foundation } from 'cvx-kit'
231
- import { createAuthFunctions, defaultRoleMap } from 'cvx-kit/auth'
232
- import { createCrudCommands } from 'cvx-kit/crud'
233
- import { zodTable } from 'cvx-kit/zod-table'
234
- import { actionGeneric, queryGeneric, mutationGeneric, internalActionGeneric, internalQueryGeneric, internalMutationGeneric, type GenericMutationCtx, type GenericDataModel } from 'convex/server'
235
- import { z } from 'zod'
236
- const { Command } = new Foundation({ functions: { status: null } }, {
237
- observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) },
238
- })
239
- const operation = Command.withContext<{ offset: number }>().operation({
240
- command: z.object({ title: z.string().transform(value => value.length) }), result: z.number(), classification: 'business', audit: () => null,
241
- })
242
- const commands = new Command<{ offset: number }, { measure: typeof operation }>({ measure: operation })
243
- const execute = commands.exec({ operation: 'measure', handler: async (ctx, input) => ctx.offset + input.title })
244
- const result: Promise<number> = execute({ offset: 1 }, { title: 'raw' })
245
- void result
246
- const auth = createAuthFunctions({ query: queryGeneric, mutation: mutationGeneric, action: actionGeneric,
247
- internalQuery: internalQueryGeneric, internalMutation: internalMutationGeneric, internalAction: internalActionGeneric,
248
- getAuthUser: async () => ({ id: 'actor' }), mapRole: defaultRoleMap, adminRoles: ['admin'],
249
- })
250
- const authenticated = auth.authQuery({ args: {}, returns: z.string(), handler: ctx => ctx.actor.userId })
251
- void authenticated
252
- const table = zodTable('notes', () => ({ text: z.string() }), { commandFields: ['text'] })
253
- const crud = createCrudCommands<GenericMutationCtx<GenericDataModel>, typeof table>({ Command, table, aggregateType: 'note', actor: () => 'actor' })
254
- declare const ctx: GenericMutationCtx<GenericDataModel>
255
- void crud.executeCreate(ctx, { text: 'packed' })
256
- `
257
-
258
- const domainTypes = `
259
- import { Context, Effect } from 'effect'
260
- import { createEffectFoundation } from 'cvx-kit/effect'
261
- import { z } from 'zod'
262
- type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false
263
- type Assert<T extends true> = T
264
- class Reader extends Context.Service<Reader, { count: number }>()('PackedReader') {}
265
- class Failure { readonly _tag = 'PackedFailure' }
266
- const foundation = createEffectFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: () => undefined })
267
- const queries = foundation.Query({ context: (host: { actor: string }) => host, operations: ({ query }) => ({
268
- read: query({ input: z.string().transform(value => value.length), result: z.number().transform(String),
269
- handler: (input, ctx) => Effect.gen(function* () { ctx.actor.toUpperCase(); if (input < 1) return yield* Effect.fail(new Failure()); return input + (yield* Reader).count }),
270
- }),
271
- recovered: query({ input: z.number(), result: z.number(), handler: input => Effect.gen(function* () { if (input < 0) return yield* Effect.fail(new Failure()); return input + (yield* Reader).count }),
272
- middleware: ({ next }) => next().pipe(Effect.provideService(Reader, { count: 1 }), Effect.catchTag('PackedFailure', () => Effect.succeed(0))),
273
- }),
274
- ordinary: query({ input: z.number(), result: z.number(), handler: input => input }),
275
- promise: query({ input: z.number(), result: z.number(), handler: async input => input }),
276
- named: query({ input: z.number(), result: z.number(), handler: Effect.fn('packed.named')(function* (input: number) { return yield* Effect.succeed(input) }) }),
277
- }) })
278
- const read = queries.exec('read', 'raw', { actor: 'trusted' })
279
- const recovered = queries.exec('recovered', 1, { actor: 'trusted' })
280
- const checks: [Assert<Equal<Effect.Success<typeof read>, string>>, Assert<Equal<Effect.Error<typeof read>, Failure>>, Assert<Equal<Effect.Services<typeof read>, Reader>>, Assert<Equal<Effect.Error<typeof recovered>, never>>, Assert<Equal<Effect.Services<typeof recovered>, never>>] = [true, true, true, true, true]
281
- void checks
282
- // @ts-expect-error an unprovided domain requirement cannot reach the runner
283
- void Effect.runPromise(read)
284
- // @ts-expect-error transport input is the raw schema input
285
- queries.exec('read', 1, { actor: 'trusted' })
286
- for (const kind of ['ordinary', 'promise', 'named'] as const) { const execution = queries.exec(kind, 1, { actor: 'trusted' }); const checks: [Assert<Equal<Effect.Error<typeof execution>, never>>, Assert<Equal<Effect.Services<typeof execution>, never>>] = [true, true]; void checks }
287
- `
288
-
289
- const effectRuntime = `
290
- import { strict as assert } from 'node:assert'
291
- import { createRequire } from 'node:module'
292
- import { realpathSync } from 'node:fs'
293
- import { Context, Effect } from 'effect'
294
- import { createEffectFoundation, createEffectCrud, effectApiBuilder, effectZodApiBuilder, createOperationTools } from 'cvx-kit/effect'
295
- import { createOperationTools as neutralOperationTools } from 'cvx-kit/agent-tools'
296
- import { zodTable } from 'cvx-kit/zod-table'
297
- import { queryGeneric, internalMutationGeneric, actionGeneric } from 'convex/server'
298
- import { zCustomMutation } from 'convex-helpers/server/zod4'
299
- import { v } from 'convex/values'
300
- import { z } from 'zod'
301
- const require = createRequire(import.meta.url)
302
- const packageRequire = createRequire(import.meta.resolve('cvx-kit/effect'))
303
- assert.equal(createOperationTools, neutralOperationTools)
304
- assert.equal(realpathSync(require.resolve('effect')), realpathSync(packageRequire.resolve('effect')), 'package resolved a second Effect identity')
305
- class Request extends Context.Service()('PackedRequest') {}
306
- const events = []
307
- const foundation = createEffectFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: () => events.push('audit') })
308
- const queries = foundation.Query({ context: host => host, operations: ({ query }) => ({
309
- read: query({ input: z.string().transform(value => value.length), result: z.number(), handler: input => Effect.gen(function* () { events.push('read'); return input + (yield* Request).offset }) }),
310
- }) })
311
- const commands = foundation.Command({ context: host => host, operations: ({ command }) => ({
312
- save: command({ input: z.string(), result: z.number(), classification: 'business', audit: () => ({ operation: 'save', actorId: 'actor', aggregate: { type: 'note', id: 'note-id' } }), handler: (input, host) => Effect.gen(function* () { events.push('save'); return yield* queries.exec('read', input, host) }) }),
313
- }) })
314
- const execution = commands.exec('save', 'abc', {})
315
- assert.ok(Effect.isEffect(execution), 'public facade must return the host Effect value')
316
- assert.deepEqual(events, [], 'domain composition must stay lazy')
317
- const adapted = effectApiBuilder(queryGeneric, { services: () => { events.push('provide'); return Context.make(Request, { offset: 2 }) } })({ args: {}, returns: v.number(), handler: () => execution })
318
- assert.equal(await adapted._handler({}, {}), 5)
319
- assert.deepEqual(events, ['provide', 'save', 'read', 'audit'], 'one adapter run must execute nested composition once')
320
- const plain = effectApiBuilder(queryGeneric, { services: () => Context.empty() })
321
- for (const handler of [() => 3, () => Promise.resolve(3), () => Effect.succeed(3), Effect.fn('packed.fn')(function* () { return yield* Effect.succeed(3) })]) {
322
- assert.equal(await plain({ handler })._handler({}, {}), 3)
323
- }
324
- const customization = []
325
- const custom = effectZodApiBuilder(zCustomMutation(internalMutationGeneric, { args: { session: v.string() }, input: (_ctx, args) => { customization.push('custom'); return { ctx: { actor: args.session }, args: { made: 4 } } } }), {
326
- services: ctx => { customization.push('provide:' + ctx.actor); return Context.make(Request, { offset: 2 }) },
327
- })({ args: { amount: z.string().transform(value => value.length) }, returns: z.number().transform(String), skipConvexValidation: true,
328
- handler: (ctx, args) => Effect.gen(function* () { customization.push('handle:' + ctx.actor); return args.amount + args.made + (yield* Request).offset }),
329
- })
330
- assert.equal(await custom._handler({}, { session: 'trusted', amount: 'abc' }), '9')
331
- assert.deepEqual(customization, ['custom', 'provide:trusted', 'handle:trusted'])
332
- assert.ok(custom.isInternal && custom.isMutation)
333
- assert.ok(effectApiBuilder(actionGeneric, { services: () => Context.empty() })(() => 1).isAction)
334
- for (const fail of [false, true]) {
335
- const lifecycle = []
336
- const adapter = effectApiBuilder(queryGeneric, { services: () => Effect.acquireRelease(Effect.sync(() => { lifecycle.push('acquire'); return Context.empty() }), () => Effect.promise(async () => { await Promise.resolve(); lifecycle.push('release') })) })
337
- const registered = adapter({ handler: () => Effect.gen(function* () { lifecycle.push('handle'); if (fail) return yield* Effect.fail('packed-failure'); return 1 }) })
338
- if (fail) await assert.rejects(registered._handler({}, {}), error => error === 'packed-failure')
339
- else assert.equal(await registered._handler({}, {}), 1)
340
- assert.deepEqual(lifecycle, ['acquire', 'handle', 'release'])
341
- }
342
- await assert.rejects(plain({ handler: () => Effect.die(new Error('packed-defect')) })._handler({}, {}), /packed-defect/)
343
- await assert.rejects(plain({ handler: () => Effect.interrupt })._handler({}, {}))
344
- const crudAudits = []
345
- const crudFoundation = createEffectFoundation({ observability: { enabled: false, classifyError: () => ({ outcome: 'failed', errorCode: 'FAILED' }) }, writeAudit: (_host, entry) => crudAudits.push(entry) })
346
- const crudTable = zodTable('packedNotes', () => ({ title: z.string(), secret: z.string() }), { commandFields: ['title'], serverFields: ['secret'], publicFields: ['title'] })
347
- let crudRow
348
- const crudHost = { actor: 'packed-actor', db: { insert: async (_table, values) => { crudRow = values; return 'packed-id' }, patch: async (_id, values) => { crudRow = { ...crudRow, ...values } } } }
349
- const crud = createEffectCrud({ foundation: crudFoundation, table: crudTable, context: host => host, aggregateType: 'note', actor: host => host.actor, checkId: (_host, id) => id === 'packed-id', enrich: () => ({ secret: 'private' }), read: { maxPageSize: 2, project: row => ({ title: row.title }), get: () => crudRow ?? null, list: () => ({ page: crudRow ? [crudRow] : [], isDone: true, continueCursor: '' }) } })
350
- const pendingCreate = crud.commands.exec('packedNotes.create', { title: 'packed-crud' }, crudHost)
351
- assert.equal(crudRow, undefined, 'CRUD remains lazy before the API runner')
352
- assert.deepEqual(await Effect.runPromise(pendingCreate), { id: 'packed-id' })
353
- assert.deepEqual(await Effect.runPromise(crud.queries.exec('packedNotes.get', { id: 'packed-id' }, crudHost)), { title: 'packed-crud' })
354
- assert.deepEqual(await Effect.runPromise(crud.commands.exec('packedNotes.update', { id: 'packed-id', data: {} }, crudHost)), { ok: true })
355
- assert.equal(crudAudits.length, 2)
356
- assert.deepEqual(crudRow, { title: 'packed-crud', secret: 'private' })
357
- assert.deepEqual(await Effect.runPromise(crud.commands.exec('packedNotes.update', { id: 'packed-id', data: { title: 'packed-updated' } }, crudHost)), { ok: true })
358
- assert.deepEqual(crudRow, { title: 'packed-updated', secret: 'private' })
359
- assert.deepEqual(await Effect.runPromise(crud.queries.exec('packedNotes.get', { id: 'packed-id' }, crudHost)), { title: 'packed-updated' })
360
- assert.equal(crudAudits.length, 3)
361
- assert.deepEqual(crudAudits.map(entry => entry.operation), ['packedNotes.create', 'packedNotes.update', 'packedNotes.update'])
362
- for (const [registry, operation, input] of [
363
- [crud.queries, 'packedNotes.get', { id: 'other-table-id' }],
364
- [crud.commands, 'packedNotes.update', { id: 'other-table-id', data: { title: 'wrong-table' } }],
365
- [crud.commands, 'packedNotes.archive', { id: 'other-table-id' }],
366
- ]) {
367
- await assert.rejects(Effect.runPromise(registry.exec(operation, input, crudHost)), error => error.code === 'CRUD_INVALID_ID')
368
- assert.deepEqual(crudRow, { title: 'packed-updated', secret: 'private' })
369
- assert.equal(crudAudits.length, 3)
370
- }
371
- `
372
-
373
- try {
374
- run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)
375
- for (const installer of ['npm', 'bun']) {
376
- for (const withEffect of [false, true]) {
377
- const name = `${installer}-${withEffect ? 'effect' : 'legacy'}`
378
- const fixture = join(temporary, name)
379
- mkdirSync(fixture)
380
- const dependencies = {
381
- 'cvx-kit': `file:${join(temporary, `cvx-kit-${manifest.version}.tgz`)}`,
382
- convex: manifest.devDependencies.convex,
383
- 'convex-helpers': helpersVersion,
384
- 'convex-test': '0.0.61',
385
- '@convex-dev/workflow': manifest.dependencies['@convex-dev/workflow'],
386
- zod: manifest.devDependencies.zod,
387
- }
388
- if (withEffect) dependencies.effect = effectVersion
389
- const devDependencies = { typescript: manifest.devDependencies.typescript }
390
- if (withEffect) devDependencies['@types/node'] = nodeTypesVersion
391
- writeFileSync(
392
- join(fixture, 'package.json'),
393
- JSON.stringify({
394
- private: true,
395
- type: 'module',
396
- dependencies,
397
- devDependencies,
398
- }),
399
- )
400
- run(installer, ['install', '--ignore-scripts'], fixture)
401
- writeFileSync(join(fixture, 'legacy.mjs'), legacyRuntime)
402
- writeFileSync(join(fixture, 'legacy-types.ts'), legacyTypes)
403
- const typeFiles = [
404
- 'legacy-types.ts',
405
- copyPublicTypeFixture(fixture, 'domain-contract-types.ts', '../src/effect'),
406
- copyPublicTypeFixture(fixture, 'domain-foundation-types.ts', '../src/effect'),
407
- copyPublicTypeFixture(fixture, 'domain-contract-guide-types.ts', '../src/effect'),
408
- copyPublicTypeFixture(fixture, 'operation-neutral-types.ts', '../src/effect'),
409
- copyPublicTypeFixture(fixture, 'operation-agent-tools-types.ts', '../src/effect'),
410
- ]
411
- const guideTypeFiles = []
412
- if (withEffect) {
413
- for (const filename of [
414
- 'effect-foundation-types.ts',
415
- 'domain-effect-types.ts',
416
- 'domain-handler-types.ts',
417
- 'domain-exactness-types.ts',
418
- 'effect-api-types.ts',
419
- 'effect-api-zod-types.ts',
420
- 'effect-auth-types.ts',
421
- 'effect-registry-review-types.ts',
422
- 'operation-contract-types.ts',
423
- 'effect-schema-types.ts',
424
- 'effect-crud-types.ts',
425
- 'idempotency-types.ts',
426
- 'operation-workflow-types.ts',
427
- 'error-contract-types.ts',
428
- ]) {
429
- typeFiles.push(copyPublicTypeFixture(fixture, filename, '../src/effect'))
430
- }
431
- writeFileSync(join(fixture, 'domain-types.ts'), domainTypes)
432
- writeFileSync(join(fixture, 'effect.mjs'), effectRuntime)
433
- typeFiles.push('domain-types.ts')
434
- for (const filename of copyExtraTypeFixtures(fixture, extraTypeFixtures)) {
435
- if (filename.startsWith('domain-operation-guide-')) guideTypeFiles.push(filename)
436
- else typeFiles.push(filename)
437
- }
438
- }
439
- writeFileSync(
440
- join(fixture, 'tsconfig.json'),
441
- JSON.stringify({
442
- compilerOptions: {
443
- noEmit: true,
444
- strict: true,
445
- skipLibCheck: false,
446
- target: 'ES2025',
447
- lib: ['ES2025', 'DOM', 'ESNext.Disposable'],
448
- module: 'ESNext',
449
- moduleResolution: 'Bundler',
450
- types: [],
451
- },
452
- files: typeFiles,
453
- }),
454
- )
455
- const compiler = realpathSync(join(fixture, 'node_modules/typescript/bin/tsc'))
456
- run('node', [compiler, '-p', 'tsconfig.json'], fixture)
457
- if (withEffect) {
458
- writeFileSync(
459
- join(fixture, 'tsconfig.domain.json'),
460
- JSON.stringify({
461
- extends: './tsconfig.json',
462
- compilerOptions: { types: ['node'] },
463
- files: guideTypeFiles,
464
- }),
465
- )
466
- run('node', [compiler, '-p', 'tsconfig.domain.json'], fixture)
467
- console.log(`${name}: complete domain guide and actual native references pass separately`)
468
- }
469
- console.log(
470
- `${name}: strict public declaration/type fixtures pass (TypeScript ${manifest.devDependencies.typescript}, helpers ${helpersVersion})`,
471
- )
472
- const runtime = installer === 'bun' ? 'bun' : 'node'
473
- run(runtime, ['legacy.mjs', ...(withEffect ? [] : ['--without-effect'])], fixture)
474
- if (withEffect) run(runtime, ['effect.mjs'], fixture)
475
- console.log(
476
- `${name}: public consumer runtime pass (Convex ${manifest.devDependencies.convex}, Zod ${manifest.devDependencies.zod}${withEffect ? `, Effect ${effectVersion}` : ', Effect absent'})`,
477
- )
478
- }
479
- }
480
- } finally {
481
- rmSync(temporary, { recursive: true, force: true })
482
- }
@@ -1,150 +0,0 @@
1
- import { execFileSync } from 'node:child_process'
2
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
-
6
- const root = join(import.meta.dirname, '..')
7
- const temporary = mkdtempSync(join(tmpdir(), 'cvx-kit-ids-'))
8
- const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
9
- function run(command, args, cwd) {
10
- return execFileSync(command, args, {
11
- cwd,
12
- encoding: 'utf8',
13
- stdio: 'pipe',
14
- })
15
- }
16
- try {
17
- run('bun', ['pm', 'pack', '--destination', temporary, '--ignore-scripts'], root)
18
- for (const installer of ['npm', 'bun']) {
19
- const fixture = join(temporary, installer)
20
- mkdirSync(fixture)
21
- writeFileSync(
22
- join(fixture, 'package.json'),
23
- JSON.stringify({
24
- private: true,
25
- type: 'module',
26
- dependencies: {
27
- 'cvx-kit': `file:${join(temporary, `cvx-kit-${manifest.version}.tgz`)}`,
28
- 'convex-helpers': '0.1.127',
29
- convex: manifest.devDependencies.convex,
30
- zod: manifest.devDependencies.zod,
31
- },
32
- devDependencies: {
33
- 'convex-test': manifest.devDependencies['convex-test'],
34
- 'vite-plus': manifest.devDependencies['vite-plus'],
35
- '@edge-runtime/vm': manifest.devDependencies['@edge-runtime/vm'],
36
- },
37
- }),
38
- )
39
- run(installer, ['install', '--ignore-scripts'], fixture)
40
- writeFileSync(
41
- join(fixture, 'probe.mjs'),
42
- `
43
- import { strict as assert } from 'node:assert'
44
- import { tenantTable, zodToConvex as kitConverter, zid as kitId } from 'cvx-kit/zod-table'
45
- import { zid, zodToConvex } from 'convex-helpers/server/zod4'
46
- import { z } from 'zod'
47
- const table = tenantTable('children', id => ({
48
- parentId: id('parents'), nested: z.object({ parentId: id('parents') }),
49
- }), { commandFields: ['parentId', 'nested'], publicFields: ['parentId', 'nested'] })
50
- for (const validator of [
51
- zodToConvex(zid('parents')), table.table.validator.fields.parentId,
52
- zodToConvex(table.storage).fields.parentId,
53
- zodToConvex(table.commandInput).fields.parentId,
54
- zodToConvex(table.publicDto).fields.parentId,
55
- zodToConvex(table.storage).fields.nested.fields.parentId,
56
- kitConverter(kitId('parents')),
57
- kitConverter(table.commandInput).fields.nested.fields.parentId,
58
- kitConverter(table.publicDto).fields.nested.fields.parentId,
59
- ]) {
60
- assert.equal(validator.kind, 'id', 'exported schema lost ID validation')
61
- assert.equal(validator.tableName, 'parents')
62
- }
63
- `,
64
- )
65
- run('node', ['probe.mjs'], fixture)
66
- for (const testFile of ['command.test.ts', 'command-lifecycle.test.ts']) {
67
- const source = readFileSync(join(root, 'test', testFile), 'utf8')
68
- .replace('../src/components/foundation/client', 'cvx-kit/components/foundation')
69
- .replaceAll('../src/contracts', 'cvx-kit/contracts')
70
- .replace(
71
- "const modules = import.meta.glob('./fixture/**/*.ts')",
72
- "const modules = { './_generated/server.ts': async () => ({}) }",
73
- )
74
- writeFileSync(join(fixture, testFile), source)
75
- }
76
- for (const testFile of ['zod-table.test.ts', 'agent-tools.test.ts']) {
77
- const source = readFileSync(join(root, 'test', testFile), 'utf8')
78
- .replaceAll('../src/zod-table', 'cvx-kit/zod-table')
79
- .replaceAll('../src/agent-tools', 'cvx-kit/agent-tools')
80
- writeFileSync(join(fixture, testFile), source)
81
- }
82
- writeFileSync(
83
- join(fixture, 'command-types.ts'),
84
- readFileSync(join(root, 'test/command-types.ts'), 'utf8').replace(
85
- '../src/components/foundation/client',
86
- 'cvx-kit/components/foundation',
87
- ),
88
- )
89
- writeFileSync(
90
- join(fixture, 'registration.test.ts'),
91
- `
92
- import { convexTest } from 'convex-test'
93
- import { defineSchema, defineTable } from 'convex/server'
94
- import { v } from 'convex/values'
95
- import { registerFoundation, registerApprovals } from 'cvx-kit/test'
96
- import { it } from 'vite-plus/test'
97
- it('registers concrete and generic backends from the packed package', () => {
98
- const modules = { './_generated/server.ts': async () => ({}) }
99
- const concrete = convexTest(defineSchema({ items: defineTable({ name: v.string() }) }), modules)
100
- const generic = convexTest(undefined, modules)
101
- for (const backend of [concrete, generic]) {
102
- registerFoundation(backend)
103
- registerApprovals(backend)
104
- registerFoundation(backend, 'customFoundation')
105
- registerApprovals(backend, 'customApprovals')
106
- }
107
- })
108
- `,
109
- )
110
- run(
111
- join(root, 'node_modules/.bin/tsc'),
112
- [
113
- '--noEmit',
114
- '--strict',
115
- '--skipLibCheck',
116
- '--target',
117
- 'es2025',
118
- '--module',
119
- 'esnext',
120
- '--moduleResolution',
121
- 'bundler',
122
- '--types',
123
- 'vite/client',
124
- 'command-types.ts',
125
- 'registration.test.ts',
126
- 'zod-table.test.ts',
127
- 'agent-tools.test.ts',
128
- ],
129
- fixture,
130
- )
131
- run(
132
- join(fixture, 'node_modules/.bin/vp'),
133
- [
134
- 'test',
135
- 'run',
136
- 'registration.test.ts',
137
- 'command.test.ts',
138
- 'command-lifecycle.test.ts',
139
- 'zod-table.test.ts',
140
- 'agent-tools.test.ts',
141
- ],
142
- fixture,
143
- )
144
- console.log(
145
- `${installer}: packed IDs, table/tool types, registration, and command lifecycle pass with host convex-helpers 0.1.127`,
146
- )
147
- }
148
- } finally {
149
- rmSync(temporary, { recursive: true, force: true })
150
- }