cvx-kit 0.1.6 → 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 +23 -0
- package/README.md +10 -1
- 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/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/domain-operations.md +615 -0
- package/docs/effect.md +384 -0
- package/package.json +33 -1
- package/scripts/convex-smoke-env.mjs +33 -0
- package/scripts/smoke-packed-components.mjs +9 -2
- package/scripts/smoke-packed-effect.mjs +439 -0
- package/scripts/test-packed-effect.mjs +457 -0
- package/scripts/test-packed-ids.mjs +1 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { execFileSync, spawn } from 'node:child_process'
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs'
|
|
12
|
+
import { createServer } from 'node:net'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import {
|
|
16
|
+
assertIsolatedConvexFixture,
|
|
17
|
+
assertLocalConvexState,
|
|
18
|
+
localConvexEnvironment,
|
|
19
|
+
} from './convex-smoke-env.mjs'
|
|
20
|
+
|
|
21
|
+
const root = join(import.meta.dirname, '..')
|
|
22
|
+
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
23
|
+
const installer = process.argv[2] ?? 'bun'
|
|
24
|
+
if (!['bun', 'npm'].includes(installer)) throw new Error('Usage: smoke-packed-effect.mjs [bun|npm]')
|
|
25
|
+
const temporaryRoot = mkdtempSync(join(tmpdir(), 'cvx-kit-effect-runtime-'))
|
|
26
|
+
const fixture = join(temporaryRoot, 'fixture')
|
|
27
|
+
function run(command, args, cwd = fixture) {
|
|
28
|
+
try {
|
|
29
|
+
return execFileSync(command, args, {
|
|
30
|
+
cwd,
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
|
+
env: localConvexEnvironment(),
|
|
34
|
+
})
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (args[0] === 'install')
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Isolated ${command} install failed with exit ${error.status ?? 'unknown'}; package-manager output suppressed`,
|
|
39
|
+
)
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function convex(...args) {
|
|
44
|
+
return run('node', [join(fixture, 'node_modules/convex/bin/main.js'), ...args])
|
|
45
|
+
}
|
|
46
|
+
async function verifyNativeFixture(script, label, port, environment = {}) {
|
|
47
|
+
const child = spawn(
|
|
48
|
+
'node',
|
|
49
|
+
[
|
|
50
|
+
join(fixture, 'node_modules/convex/bin/main.js'),
|
|
51
|
+
'dev',
|
|
52
|
+
'--once',
|
|
53
|
+
'--typecheck=disable',
|
|
54
|
+
'--start',
|
|
55
|
+
`node ${script} ${port}`,
|
|
56
|
+
],
|
|
57
|
+
{
|
|
58
|
+
cwd: fixture,
|
|
59
|
+
env: { ...localConvexEnvironment(), ...environment },
|
|
60
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
61
|
+
},
|
|
62
|
+
)
|
|
63
|
+
let output = ''
|
|
64
|
+
let diagnostics = ''
|
|
65
|
+
child.stdout.setEncoding('utf8').on('data', (chunk) => {
|
|
66
|
+
output += chunk
|
|
67
|
+
})
|
|
68
|
+
child.stderr.setEncoding('utf8').on('data', (chunk) => {
|
|
69
|
+
diagnostics += chunk
|
|
70
|
+
})
|
|
71
|
+
const deadline = setTimeout(() => child.kill('SIGINT'), 90000)
|
|
72
|
+
try {
|
|
73
|
+
const code = await new Promise((resolve, reject) => {
|
|
74
|
+
child.once('error', reject)
|
|
75
|
+
child.once('close', resolve)
|
|
76
|
+
})
|
|
77
|
+
process.stdout.write(output)
|
|
78
|
+
process.stderr.write(diagnostics)
|
|
79
|
+
assert.equal(code, 0, `Native ${label} command must complete successfully`)
|
|
80
|
+
const marker = environment.WORKFLOW_NATIVE_PHASE
|
|
81
|
+
? `U6 real native deployment phase passed: ${environment.WORKFLOW_NATIVE_PHASE}`
|
|
82
|
+
: `Native ${label} passed:`
|
|
83
|
+
assert.ok(output.includes(marker))
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(deadline)
|
|
86
|
+
child.stdin.end()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function verifyWorkflowDeployments(port) {
|
|
90
|
+
const sourcePath = join(fixture, 'convex/workflowNative.ts')
|
|
91
|
+
const source = readFileSync(sourcePath, 'utf8')
|
|
92
|
+
assert.ok(source.includes("export const deployedBindingVersion = '1'"))
|
|
93
|
+
assert.ok(source.includes("export const deployedArgumentSuffix = ''"))
|
|
94
|
+
const state = join(temporaryRoot, 'workflow-deployment-state.json')
|
|
95
|
+
const phase = (value) =>
|
|
96
|
+
verifyNativeFixture('workflow-proof.mjs', 'workflow proof', port, {
|
|
97
|
+
WORKFLOW_NATIVE_PHASE: value,
|
|
98
|
+
WORKFLOW_NATIVE_PHASE_STATE: state,
|
|
99
|
+
})
|
|
100
|
+
try {
|
|
101
|
+
await phase('prepare')
|
|
102
|
+
writeFileSync(
|
|
103
|
+
sourcePath,
|
|
104
|
+
source.replace(
|
|
105
|
+
"export const deployedBindingVersion = '1'",
|
|
106
|
+
"export const deployedBindingVersion = '2'",
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
await phase('compatible')
|
|
110
|
+
writeFileSync(
|
|
111
|
+
sourcePath,
|
|
112
|
+
source.replace(
|
|
113
|
+
"export const deployedArgumentSuffix = ''",
|
|
114
|
+
"export const deployedArgumentSuffix = '-changed'",
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
await phase('changed-args')
|
|
118
|
+
} finally {
|
|
119
|
+
writeFileSync(sourcePath, source)
|
|
120
|
+
convex('dev', '--once', '--typecheck=disable')
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function invoke(name, args = {}, identity) {
|
|
124
|
+
const output = convex(
|
|
125
|
+
'run',
|
|
126
|
+
name.includes(':') ? name : `functions:${name}`,
|
|
127
|
+
JSON.stringify(args),
|
|
128
|
+
...(identity ? ['--identity', JSON.stringify(identity)] : []),
|
|
129
|
+
)
|
|
130
|
+
// Convex 1.45's run command emits no output for a successful null result.
|
|
131
|
+
return output.trim() === '' ? null : JSON.parse(output)
|
|
132
|
+
}
|
|
133
|
+
function rejection(name, args, expected, identity) {
|
|
134
|
+
assert.throws(
|
|
135
|
+
() => invoke(name, args, identity),
|
|
136
|
+
(error) => {
|
|
137
|
+
const message = String(error.stderr)
|
|
138
|
+
assert.match(message, expected)
|
|
139
|
+
assert.doesNotMatch(message, /PROJECTION_MUST_NOT_RUN/)
|
|
140
|
+
return true
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
async function freePort() {
|
|
145
|
+
const server = createServer()
|
|
146
|
+
await new Promise((resolve, reject) => {
|
|
147
|
+
server.once('error', reject)
|
|
148
|
+
server.listen(0, '127.0.0.1', resolve)
|
|
149
|
+
})
|
|
150
|
+
const port = server.address().port
|
|
151
|
+
await new Promise((resolve) => server.close(resolve))
|
|
152
|
+
return port
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
console.log('Effect runtime smoke: pack current built artifact (no build)')
|
|
156
|
+
run('bun', ['pm', 'pack', '--destination', temporaryRoot, '--ignore-scripts'], root)
|
|
157
|
+
mkdirSync(fixture)
|
|
158
|
+
cpSync(join(root, 'test/fixture-effect/convex'), join(fixture, 'convex'), { recursive: true })
|
|
159
|
+
// Generate from scratch so removed generated files cannot survive the fixture copy.
|
|
160
|
+
rmSync(join(fixture, 'convex/_generated'), { recursive: true, force: true })
|
|
161
|
+
writeFileSync(
|
|
162
|
+
join(fixture, 'package.json'),
|
|
163
|
+
JSON.stringify(
|
|
164
|
+
{
|
|
165
|
+
private: true,
|
|
166
|
+
type: 'module',
|
|
167
|
+
dependencies: {
|
|
168
|
+
'cvx-kit': `file:${join(temporaryRoot, `cvx-kit-${manifest.version}.tgz`)}`,
|
|
169
|
+
'@convex-dev/workflow': manifest.dependencies['@convex-dev/workflow'],
|
|
170
|
+
convex: manifest.devDependencies.convex,
|
|
171
|
+
effect: manifest.devDependencies.effect,
|
|
172
|
+
zod: manifest.devDependencies.zod,
|
|
173
|
+
'convex-helpers': manifest.devDependencies['convex-helpers'],
|
|
174
|
+
},
|
|
175
|
+
devDependencies: {
|
|
176
|
+
typescript: manifest.devDependencies.typescript,
|
|
177
|
+
'@types/node': manifest.devDependencies['@types/node'],
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
null,
|
|
181
|
+
2,
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
writeFileSync(
|
|
185
|
+
join(fixture, 'tsconfig.json'),
|
|
186
|
+
JSON.stringify(
|
|
187
|
+
{
|
|
188
|
+
compilerOptions: {
|
|
189
|
+
target: 'ES2025',
|
|
190
|
+
lib: ['ES2025', 'DOM'],
|
|
191
|
+
module: 'ESNext',
|
|
192
|
+
moduleResolution: 'Bundler',
|
|
193
|
+
strict: true,
|
|
194
|
+
noEmit: true,
|
|
195
|
+
skipLibCheck: false,
|
|
196
|
+
types: ['node'],
|
|
197
|
+
},
|
|
198
|
+
include: ['convex', 'typecheck.ts'],
|
|
199
|
+
},
|
|
200
|
+
null,
|
|
201
|
+
2,
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
cpSync(join(root, 'test/fixture-effect/typecheck.ts'), join(fixture, 'typecheck.ts'))
|
|
205
|
+
cpSync(join(root, 'test/fixture-effect/pagination.mjs'), join(fixture, 'pagination.mjs'))
|
|
206
|
+
cpSync(join(root, 'test/fixture-effect/idempotency.mjs'), join(fixture, 'idempotency.mjs'))
|
|
207
|
+
cpSync(join(root, 'test/fixture-effect/async-domain.mjs'), join(fixture, 'async-domain.mjs'))
|
|
208
|
+
cpSync(
|
|
209
|
+
join(root, 'test/fixture-effect/action-validation.mjs'),
|
|
210
|
+
join(fixture, 'action-validation.mjs'),
|
|
211
|
+
)
|
|
212
|
+
cpSync(
|
|
213
|
+
join(root, 'test/fixture-effect/integrated-rename.mjs'),
|
|
214
|
+
join(fixture, 'integrated-rename.mjs'),
|
|
215
|
+
)
|
|
216
|
+
cpSync(
|
|
217
|
+
join(root, 'test/fixture-effect/deployment-versions.mjs'),
|
|
218
|
+
join(fixture, 'deployment-versions.mjs'),
|
|
219
|
+
)
|
|
220
|
+
cpSync(join(root, 'scripts/convex-smoke-env.mjs'), join(fixture, 'convex-smoke-env.mjs'))
|
|
221
|
+
cpSync(join(root, 'test/fixture-effect/workflow-proof.mjs'), join(fixture, 'workflow-proof.mjs'))
|
|
222
|
+
cpSync(
|
|
223
|
+
join(root, 'test/fixture-effect/operation-tools.mjs'),
|
|
224
|
+
join(fixture, 'operation-tools.mjs'),
|
|
225
|
+
)
|
|
226
|
+
console.log(`Effect runtime smoke: isolated ${installer} install`)
|
|
227
|
+
run(installer, ['install', '--ignore-scripts'])
|
|
228
|
+
assertIsolatedConvexFixture(fixture)
|
|
229
|
+
const cloudPort = await freePort()
|
|
230
|
+
let sitePort = await freePort()
|
|
231
|
+
while (sitePort === cloudPort) sitePort = await freePort()
|
|
232
|
+
console.log('Effect runtime smoke: deploy isolated local Convex fixture')
|
|
233
|
+
convex(
|
|
234
|
+
'dev',
|
|
235
|
+
'--once',
|
|
236
|
+
'--typecheck=disable',
|
|
237
|
+
'--local-cloud-port',
|
|
238
|
+
String(cloudPort),
|
|
239
|
+
'--local-site-port',
|
|
240
|
+
String(sitePort),
|
|
241
|
+
)
|
|
242
|
+
assertLocalConvexState(fixture)
|
|
243
|
+
convex('codegen', '--typecheck=disable')
|
|
244
|
+
const generated = join(fixture, 'convex/_generated')
|
|
245
|
+
const committed = join(root, 'test/fixture-effect/convex/_generated')
|
|
246
|
+
const regenerateMessage = 'Generated contracts differ; regenerate the Effect fixture contracts'
|
|
247
|
+
assert.deepEqual(readdirSync(generated).sort(), readdirSync(committed).sort(), regenerateMessage)
|
|
248
|
+
for (const file of readdirSync(generated)) {
|
|
249
|
+
assert.equal(
|
|
250
|
+
readFileSync(join(generated, file), 'utf8'),
|
|
251
|
+
readFileSync(join(committed, file), 'utf8'),
|
|
252
|
+
`${regenerateMessage}: ${file}`,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
console.log('Effect runtime smoke: strict generated contracts and packed declarations')
|
|
256
|
+
run('node', [join(fixture, 'node_modules/typescript/bin/tsc'), '--noEmit'])
|
|
257
|
+
const localConfig = JSON.parse(
|
|
258
|
+
readFileSync(join(fixture, '.convex/local/default/config.json'), 'utf8'),
|
|
259
|
+
)
|
|
260
|
+
console.log(
|
|
261
|
+
`Runtime versions: Convex CLI ${manifest.devDependencies.convex}; Effect ${manifest.devDependencies.effect}; backend ${localConfig.backendVersion ?? 'not recorded by CLI'}`,
|
|
262
|
+
)
|
|
263
|
+
// Keep the local backend alive for the subscription; one-off CLI calls stop it on exit.
|
|
264
|
+
await verifyNativeFixture('pagination.mjs', 'pagination', cloudPort)
|
|
265
|
+
await verifyNativeFixture('idempotency.mjs', 'idempotency', cloudPort)
|
|
266
|
+
const versionProof = run('node', ['deployment-versions.mjs', String(cloudPort)])
|
|
267
|
+
assert.ok(versionProof.includes('Native deployment versions passed:'))
|
|
268
|
+
process.stdout.write(versionProof)
|
|
269
|
+
await verifyNativeFixture('workflow-proof.mjs', 'workflow proof', cloudPort)
|
|
270
|
+
await verifyWorkflowDeployments(cloudPort)
|
|
271
|
+
await verifyNativeFixture('operation-tools.mjs', 'operation tools', cloudPort)
|
|
272
|
+
await verifyNativeFixture('async-domain.mjs', 'async domain', cloudPort)
|
|
273
|
+
await verifyNativeFixture('action-validation.mjs', 'action validation', cloudPort)
|
|
274
|
+
await verifyNativeFixture('integrated-rename.mjs', 'integrated rename', cloudPort)
|
|
275
|
+
assert.equal(invoke('save', { key: 'success', mode: 'success' }), 'saved')
|
|
276
|
+
assert.deepEqual(invoke('read', { key: 'success' }), ['domain', 'audit', 'completion'])
|
|
277
|
+
assert.deepEqual(invoke('composed', { key: 'success' }), {
|
|
278
|
+
count: 6,
|
|
279
|
+
events: ['acquire', 'handler', 'release'],
|
|
280
|
+
})
|
|
281
|
+
rejection('save', { key: 'invalid-args', mode: 'unknown' }, /ArgumentValidationError/)
|
|
282
|
+
assert.deepEqual(invoke('read', { key: 'invalid-args' }), [])
|
|
283
|
+
rejection('invalidReturn', { key: 'invalid-return' }, /ReturnsValidationError/)
|
|
284
|
+
assert.deepEqual(invoke('read', { key: 'invalid-return' }), [])
|
|
285
|
+
rejection('invalidCustomReturn', { key: 'invalid-custom-return' }, /FIXTURE_CUSTOM_RETURN/)
|
|
286
|
+
assert.deepEqual(invoke('read', { key: 'invalid-custom-return' }), [])
|
|
287
|
+
for (const mode of ['domain', 'audit', 'completion']) {
|
|
288
|
+
rejection('save', { key: mode, mode }, new RegExp(`FIXTURE_${mode}`))
|
|
289
|
+
assert.deepEqual(invoke('read', { key: mode }), [])
|
|
290
|
+
}
|
|
291
|
+
rejection('cleanupFailure', { composite: false }, /FIXTURE_CLEANUP/)
|
|
292
|
+
assert.deepEqual(invoke('read', { key: 'cleanup' }), [])
|
|
293
|
+
rejection('cleanupFailure', { composite: true }, /Effect API execution failed/)
|
|
294
|
+
assert.deepEqual(invoke('read', { key: 'cleanup' }), [])
|
|
295
|
+
assert.deepEqual(invoke('read', { key: 'provider-initialization' }), [])
|
|
296
|
+
for (let invocation = 0; invocation < 2; invocation++) {
|
|
297
|
+
assert.throws(
|
|
298
|
+
() => invoke('providerInitializationFailure'),
|
|
299
|
+
(error) => {
|
|
300
|
+
const message = String(error.stderr)
|
|
301
|
+
assert.match(message, /FIXTURE_PROVIDER_INITIALIZATION/)
|
|
302
|
+
assert.match(
|
|
303
|
+
message,
|
|
304
|
+
/"events"\s*:\s*\[\s*"acquire"\s*,\s*"initialize"\s*,\s*"release"\s*\]/,
|
|
305
|
+
)
|
|
306
|
+
assert.doesNotMatch(message, /"handler"|FIXTURE_PROVIDER_UNEXPECTED/)
|
|
307
|
+
return true
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
assert.deepEqual(
|
|
311
|
+
invoke('read', { key: 'provider-initialization' }),
|
|
312
|
+
[],
|
|
313
|
+
'Provider acquisition and awaited release writes must roll back after initialization fails',
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
assert.deepEqual(invoke('actions:orchestrate', { key: 'action' }), {
|
|
317
|
+
body: 'fixture-http-ok',
|
|
318
|
+
stages: ['domain', 'audit', 'completion'],
|
|
319
|
+
rejected: true,
|
|
320
|
+
})
|
|
321
|
+
assert.deepEqual(
|
|
322
|
+
invoke('actions:declaredFailureRoundTrip', { key: 'declared-round-trip', composite: false }),
|
|
323
|
+
{
|
|
324
|
+
kind: 'declared',
|
|
325
|
+
code: 'DENIED',
|
|
326
|
+
key: 'declared-round-trip',
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
assert.deepEqual(invoke('read', { key: 'declared-round-trip' }), [])
|
|
330
|
+
assert.deepEqual(
|
|
331
|
+
invoke('actions:declaredFailureRoundTrip', { key: 'composite-round-trip', composite: true }),
|
|
332
|
+
{ kind: 'unknown' },
|
|
333
|
+
)
|
|
334
|
+
assert.deepEqual(invoke('read', { key: 'composite-round-trip' }), [])
|
|
335
|
+
const crudOwner = { subject: 'crud-owner', org_id: 'tenant-a', role: 'owner' }
|
|
336
|
+
const otherId = invoke('crud:createOther', {}, crudOwner)
|
|
337
|
+
const initialOtherState = invoke('crud:otherState', { id: otherId })
|
|
338
|
+
assert.deepEqual(initialOtherState, { title: 'other-table', audits: [] })
|
|
339
|
+
for (const [operation, input] of [
|
|
340
|
+
['crud:getFromString', { id: otherId }],
|
|
341
|
+
['crud:updateFromString', { id: otherId, data: { title: 'wrong-table-write' } }],
|
|
342
|
+
['crud:archiveFromString', { id: otherId }],
|
|
343
|
+
]) {
|
|
344
|
+
rejection(operation, input, /Invalid ID for table "crudNotes"/, crudOwner)
|
|
345
|
+
assert.deepEqual(invoke('crud:otherState', { id: otherId }), initialOtherState)
|
|
346
|
+
}
|
|
347
|
+
const created = invoke('crud:create', { title: 'native-crud' }, crudOwner)
|
|
348
|
+
assert.match(created.id, /^.+$/)
|
|
349
|
+
assert.deepEqual(invoke('crud:get', { id: created.id }, crudOwner), { title: 'native-crud' })
|
|
350
|
+
const initialCrudState = invoke('crud:state', { id: created.id })
|
|
351
|
+
assert.deepEqual(initialCrudState, {
|
|
352
|
+
note: {
|
|
353
|
+
title: 'native-crud',
|
|
354
|
+
tenant: 'tenant-a',
|
|
355
|
+
owner: 'crud-owner',
|
|
356
|
+
secret: 'server-private',
|
|
357
|
+
},
|
|
358
|
+
history: ['insert'],
|
|
359
|
+
audits: [{ operation: 'crudNotes.create', actor: 'crud-owner' }],
|
|
360
|
+
})
|
|
361
|
+
rejection('crud:get', { id: created.id }, /UNAUTHENTICATED|authenticated/i)
|
|
362
|
+
rejection(
|
|
363
|
+
'crud:update',
|
|
364
|
+
{ id: created.id, data: { title: 'viewer-write' } },
|
|
365
|
+
/access|allow|permission/i,
|
|
366
|
+
{ ...crudOwner, subject: 'crud-viewer', role: 'viewer' },
|
|
367
|
+
)
|
|
368
|
+
rejection(
|
|
369
|
+
'crud:update',
|
|
370
|
+
{ id: created.id, data: { tenant: 'tenant-b', secret: 'caller-secret' } },
|
|
371
|
+
/validation|invalid|unrecognized|extra/i,
|
|
372
|
+
crudOwner,
|
|
373
|
+
)
|
|
374
|
+
const foreignOwner = { ...crudOwner, subject: 'foreign-owner', org_id: 'tenant-b' }
|
|
375
|
+
assert.equal(invoke('crud:get', { id: created.id }, foreignOwner), null)
|
|
376
|
+
assert.deepEqual(invoke('crud:list', { numItems: 2, cursor: null }, foreignOwner).page, [])
|
|
377
|
+
assert.deepEqual(invoke('crud:list', { numItems: 2, cursor: null }, crudOwner).page, [
|
|
378
|
+
{ title: 'native-crud' },
|
|
379
|
+
])
|
|
380
|
+
rejection('crud:list', { numItems: 3, cursor: null }, /validation|invalid|too big/i, crudOwner)
|
|
381
|
+
rejection(
|
|
382
|
+
'crud:update',
|
|
383
|
+
{ id: created.id, data: { title: 'foreign' } },
|
|
384
|
+
/access|exist/i,
|
|
385
|
+
foreignOwner,
|
|
386
|
+
)
|
|
387
|
+
rejection(
|
|
388
|
+
'crud:update',
|
|
389
|
+
{ id: created.id, data: { title: 'audit-rollback' }, failAudit: true },
|
|
390
|
+
/CRUD_AUDIT_FAILURE/,
|
|
391
|
+
crudOwner,
|
|
392
|
+
)
|
|
393
|
+
rejection('crud:invalidOutput', { id: created.id }, /invalid|expected|validation/i, crudOwner)
|
|
394
|
+
assert.deepEqual(
|
|
395
|
+
invoke('crud:state', { id: created.id }),
|
|
396
|
+
initialCrudState,
|
|
397
|
+
'RLS, audit and native output failures must leave business, trigger and audit rows unchanged',
|
|
398
|
+
)
|
|
399
|
+
assert.deepEqual(
|
|
400
|
+
invoke('crud:update', { id: created.id, data: { title: 'native-updated' } }, crudOwner),
|
|
401
|
+
{ ok: true },
|
|
402
|
+
)
|
|
403
|
+
assert.deepEqual(invoke('crud:get', { id: created.id }, crudOwner), { title: 'native-updated' })
|
|
404
|
+
const updatedCrudState = invoke('crud:state', { id: created.id })
|
|
405
|
+
assert.deepEqual(updatedCrudState.note, { ...initialCrudState.note, title: 'native-updated' })
|
|
406
|
+
assert.deepEqual(updatedCrudState.history, ['insert', 'update'])
|
|
407
|
+
assert.deepEqual(updatedCrudState.audits, [
|
|
408
|
+
...initialCrudState.audits,
|
|
409
|
+
{ operation: 'crudNotes.update', actor: 'crud-owner' },
|
|
410
|
+
])
|
|
411
|
+
assert.deepEqual(invoke('crud:update', { id: created.id, data: {} }, crudOwner), { ok: true })
|
|
412
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
413
|
+
assert.deepEqual(invoke('crud:archive', { id: created.id }, crudOwner), { ok: true })
|
|
414
|
+
}
|
|
415
|
+
const archivedCrudState = invoke('crud:state', { id: created.id })
|
|
416
|
+
assert.equal(archivedCrudState.note.title, 'native-updated')
|
|
417
|
+
assert.ok(Number.isFinite(archivedCrudState.note.archivedAt))
|
|
418
|
+
assert.deepEqual(archivedCrudState.history, ['insert', 'update', 'update', 'update', 'update'])
|
|
419
|
+
assert.deepEqual(
|
|
420
|
+
archivedCrudState.audits.map((entry) => entry.operation),
|
|
421
|
+
[
|
|
422
|
+
'crudNotes.create',
|
|
423
|
+
'crudNotes.update',
|
|
424
|
+
'crudNotes.update',
|
|
425
|
+
'crudNotes.archive',
|
|
426
|
+
'crudNotes.archive',
|
|
427
|
+
],
|
|
428
|
+
)
|
|
429
|
+
assert.ok(archivedCrudState.audits.every((entry) => entry.actor === 'crud-owner'))
|
|
430
|
+
console.log(
|
|
431
|
+
'Actual local runtime passed: async query ordinary/gen/fn composition, spans, awaited scoped release, domain/audit/completion rollback, declared ConvexError.data round-trip, composite safe unknown round-trip, projected rejection, cleanup/composite rejection, HTTP action and separate transaction commits, secured CRUD tenant projection/pagination, nonempty/empty update persistence, repeated archive, and business/trigger/audit rollback',
|
|
432
|
+
)
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error.stdout) process.stderr.write(String(error.stdout))
|
|
435
|
+
if (error.stderr) process.stderr.write(String(error.stderr))
|
|
436
|
+
throw error
|
|
437
|
+
} finally {
|
|
438
|
+
rmSync(temporaryRoot, { recursive: true, force: true })
|
|
439
|
+
}
|