The conformance board (tests/afi/test_capability_parity.py) is now fully green: 90 capability cells + 4 meta-locks + 3 codegen byte-parity = 97 passed. The gaps the prose table used to launder as "Django-only" / "out of scope" are wired, against the pinned-spec model (single-authored spec, byte-identical conformance across languages) — never per-language reimplementation. FastAPI — edge_manifest + PSR (logic single-sourced in mizan_core.manifest), WebSocket RPC (/ws/ through the shared dispatch), SSR (the framework-agnostic SSRBridge relocated to mizan_core.ssr; Django rides it from there), Shapes (SQLAlchemy projection, same declaration surface as django-readers), Forms (Pydantic schema/validate/submit). Rust (Axum + Tauri + cores/mizan-rust) — X-Mizan-Invalidate header, auth= enforcement, origin HMAC cache, edge manifest + PSR, WebSocket handler / IPC subscription channel, multipart upload, SSR bridge, Shapes, Forms; JWT/MWT mint+verify and cache-key derivation byte-pinned to the Python reference (cache_keys_pin, token_pin, invalidate_header_pin). TypeScript — a KDL IR emitter byte-identical to the Python build_ir (so a TS backend can feed the codegen — the largest gap), multipart upload, session-init, WebSocket transport, SSR bridge, JWT/MWT mint (pinned to Python), Shapes, Forms. Verified in the merged tree: core 25, fastapi 74, django 353/21-skip, mizan-rust (incl. cross-language pins) green, axum 10, tauri 8, mizan-ts 103/2-skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
168 lines
6.0 KiB
TypeScript
168 lines
6.0 KiB
TypeScript
/**
|
|
* Shapes (typed query projection) + Forms (schema / validate / submit) tests.
|
|
*
|
|
* Shapes prove over-fetch elimination: the projected record carries only the
|
|
* declared fields + nested relations, nothing else. Forms prove the three
|
|
* roles register as dispatchable `@client` functions carrying the IR's
|
|
* `form`/`form-name`/`form-role` meta, and that validate/submit enforce the
|
|
* declared field rules.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach } from 'bun:test'
|
|
import {
|
|
clearRegistry,
|
|
getFunction,
|
|
handleMutationCall,
|
|
Shape,
|
|
project,
|
|
registerForm,
|
|
formSchema,
|
|
validateForm,
|
|
type QueryProjection,
|
|
} from '../src'
|
|
|
|
describe('Shapes — typed query projection', () => {
|
|
test('keeps only declared scalar fields', () => {
|
|
const projection: QueryProjection = { fields: ['id', 'name'] }
|
|
const out = project([{ id: 1, name: 'A', secret: 'x', internal: 42 }], projection)
|
|
expect(out).toEqual([{ id: 1, name: 'A' }])
|
|
expect(out[0]).not.toHaveProperty('secret')
|
|
expect(out[0]).not.toHaveProperty('internal')
|
|
})
|
|
|
|
test('prunes nested relations recursively', () => {
|
|
const projection: QueryProjection = {
|
|
fields: ['id'],
|
|
relations: { orders: { fields: ['total'] } },
|
|
}
|
|
const out = project(
|
|
[{ id: 1, name: 'drop', orders: [{ id: 9, total: 100, hidden: true }] }],
|
|
projection,
|
|
)
|
|
expect(out).toEqual([{ id: 1, orders: [{ total: 100 }] }])
|
|
expect(out[0].orders[0]).not.toHaveProperty('hidden')
|
|
})
|
|
|
|
test('handles single-object relation + null', () => {
|
|
const projection: QueryProjection = {
|
|
fields: ['id'],
|
|
relations: { profile: { fields: ['bio'] } },
|
|
}
|
|
const out = project(
|
|
[
|
|
{ id: 1, profile: { bio: 'hi', age: 30 } },
|
|
{ id: 2, profile: null },
|
|
],
|
|
projection,
|
|
)
|
|
expect(out[0]).toEqual({ id: 1, profile: { bio: 'hi' } })
|
|
expect(out[1]).toEqual({ id: 2, profile: null })
|
|
})
|
|
|
|
test('Shape.query binds a projection to a source', () => {
|
|
const UserShape = new Shape('user', { fields: ['id', 'email'] })
|
|
const out = UserShape.query([{ id: 1, email: 'a@b.c', password: 'nope' }])
|
|
expect(out).toEqual([{ id: 1, email: 'a@b.c' }])
|
|
})
|
|
})
|
|
|
|
describe('Forms — schema / validate / submit', () => {
|
|
beforeEach(() => clearRegistry())
|
|
|
|
const contactForm = {
|
|
fields: [
|
|
{ name: 'email', type: 'email', required: true, label: 'Email' },
|
|
{
|
|
name: 'age',
|
|
type: 'number',
|
|
required: false,
|
|
validate: (v: unknown) => (Number(v) < 0 ? 'must be non-negative' : null),
|
|
},
|
|
],
|
|
}
|
|
|
|
test('formSchema produces field definitions', () => {
|
|
const schema = formSchema(contactForm)
|
|
expect(schema.fields).toHaveLength(2)
|
|
expect(schema.fields[0]).toEqual({
|
|
name: 'email',
|
|
type: 'email',
|
|
required: true,
|
|
label: 'Email',
|
|
helpText: '',
|
|
choices: null,
|
|
initial: null,
|
|
})
|
|
// Default label derived from name when omitted.
|
|
expect(schema.fields[1].label).toBe('Age')
|
|
})
|
|
|
|
test('validateForm: required + custom validator', () => {
|
|
expect(validateForm(contactForm, { email: 'a@b.c' }).valid).toBe(true)
|
|
expect(validateForm(contactForm, {}).errors.email).toEqual(['This field is required.'])
|
|
expect(validateForm(contactForm, { email: 'a@b.c', age: -1 }).errors.age).toEqual([
|
|
'must be non-negative',
|
|
])
|
|
})
|
|
|
|
test('registerForm registers schema + validate + submit with form meta', () => {
|
|
const reg = registerForm(contactForm, 'contact', {
|
|
submit: async (data) => ({ saved: data.email }),
|
|
})
|
|
expect(reg).toEqual({ schema: 'contact-schema', validate: 'contact-validate', submit: 'contact-submit' })
|
|
|
|
for (const [wire, role] of [
|
|
['contact-schema', 'schema'],
|
|
['contact-validate', 'validate'],
|
|
['contact-submit', 'submit'],
|
|
] as const) {
|
|
const entry = getFunction(wire)
|
|
expect(entry).toBeDefined()
|
|
expect(entry!.form).toBe(true)
|
|
expect(entry!.formName).toBe('contact')
|
|
expect(entry!.formRole).toBe(role)
|
|
}
|
|
})
|
|
|
|
test('schema function dispatches to the field defs', async () => {
|
|
registerForm(contactForm, 'contact')
|
|
const r = await handleMutationCall('contact-schema', {})
|
|
expect(r.status).toBe(200)
|
|
expect(r.body.result.fields).toHaveLength(2)
|
|
})
|
|
|
|
test('validate function dispatches and rejects bad data', async () => {
|
|
registerForm(contactForm, 'contact')
|
|
const ok = await handleMutationCall('contact-validate', { data: { email: 'a@b.c' } })
|
|
expect(ok.body.result.valid).toBe(true)
|
|
|
|
const bad = await handleMutationCall('contact-validate', { data: {} })
|
|
expect(bad.body.result.valid).toBe(false)
|
|
expect(bad.body.result.errors.email).toBeDefined()
|
|
})
|
|
|
|
test('submit validates then runs the handler', async () => {
|
|
let handled: any = null
|
|
registerForm(contactForm, 'contact', {
|
|
submit: async (data) => {
|
|
handled = data
|
|
return { id: 7 }
|
|
},
|
|
})
|
|
|
|
const ok = await handleMutationCall('contact-submit', { data: { email: 'a@b.c' } })
|
|
expect(ok.body.result).toEqual({ ok: true, result: { id: 7 } })
|
|
expect(handled).toEqual({ email: 'a@b.c' })
|
|
|
|
const bad = await handleMutationCall('contact-submit', { data: {} })
|
|
expect(bad.body.result.ok).toBe(false)
|
|
expect(bad.body.result.errors.email).toBeDefined()
|
|
})
|
|
|
|
test('submit not registered without a handler', () => {
|
|
const reg = registerForm(contactForm, 'noSubmit')
|
|
expect(reg.submit).toBeUndefined()
|
|
expect(getFunction('noSubmit-submit')).toBeUndefined()
|
|
})
|
|
})
|