Motivation
Real-world evidence from an app using this package alongside tuyau's generated client registry: tuyau's types are wrong or vacuous for every JSON:API endpoint, through no fault of its own — it lacks the information.
- Request bodies are actively wrong. Tuyau infers from vine validators, but the JSON:API pattern validates post-deserialization attributes (
request.validateUsing(v, { data: input.attributes })). Tuyau therefore emits the flat internal shape with internal attribute names (e.g. { name, type }) while the endpoint requires the envelope with wire names ({ data: { type: 'libraries', attributes: { name, libraryType } } }).
- Responses are vacuous. Controller return types are the wide
Document union; per-resource attribute shapes only exist at runtime in the registry, so every endpoint types identically and clients narrow by hand.
- Error shapes are wrong. Tuyau declares vine's
{ errors: SimpleError[] } for 422s, but renderJsonApiError emits JSON:API error documents.
This package is the source of truth for everything tuyau is missing: the resource registry (model ↔ resource ↔ type name), Lucid column metadata including serializeAs wire names, relationship definitions with cardinality, declared filters, route registration per resource, the deserializer (request contract), the document builder (response contract), and the error renderer (error contract). It can generate what tuyau structurally cannot.
Proposed artifacts (phased)
Phase 1: resource typings + document types
- Per-resource interfaces generated from the registry: attributes (wire names, TS types reflected type-level from the model via
ModelAttributes-style indirection so generated code cannot drift), relationships with cardinality and related type names.
- Per-endpoint document types:
SingleDocument<T> / CollectionDocument<T>, request documents for store/update (envelope-aware, wire names, to-one → linkage not FK), and the real error document type.
- Include-aware narrowing: valid include paths derived from exposed relationships as template-literal types (
'plugins' | 'plugins.library' | ...), with included unions computed from the requested paths.
- Enabler: tighten
JsonApiResource.pick to pick<K extends keyof Serialized>(names: K[]): Pick<Serialized, K> so ReturnType<resource['attributes']> is exact for customized resources; the default derivation is already deterministic from Lucid metadata. Conditional attributes (e.g. a create-only plaintext token) surface as optional fields; document annotating attributes() return types for these.
Phase 2: typed fetch client
- Tuyau-style generated client keyed by registered resources/actions:
api.libraries.index({ include, filter, sort, page }) with filters typed from declared FilterHandlers, include paths compile-checked, responses narrowed by the include argument, and errorResponse as JSON:API error documents.
- Generation as an assembler hook (like tuyau's
generateRegistry) or an ace command; needs the booted registry, not static analysis.
Phase 3: WarpDrive schema emission
- Registry → WarpDrive
ResourceSchema modules: { identity, fields: [{ kind: 'attribute' | 'relationship', name, type, options }] }, cardinality mapped to relationship kinds, wire names respected.
- Paired Type-branded TS interfaces for
SchemaRecord, so an Ember app registers generated schemas instead of hand-maintaining models that drift from the server.
Design notes
- Heterogeneous/mixed collections (multiple resource types in one document) need union document types.
- Non-JSON:API endpoints (auth token exchanges etc.) are explicitly out of scope; they are the one place tuyau-style validator inference is actually correct.
- Route-group awareness (e.g.
/api/v1 vs /api/v2 mounting) should follow the same route-name namespacing the LinkBuilder already uses.
- Emission target: a configurable output directory the host app can re-export (the consuming monorepo currently exports
.adonisjs/client artifacts to sibling apps; this would replace that role for JSON:API surfaces).
Motivation
Real-world evidence from an app using this package alongside tuyau's generated client registry: tuyau's types are wrong or vacuous for every JSON:API endpoint, through no fault of its own — it lacks the information.
request.validateUsing(v, { data: input.attributes })). Tuyau therefore emits the flat internal shape with internal attribute names (e.g.{ name, type }) while the endpoint requires the envelope with wire names ({ data: { type: 'libraries', attributes: { name, libraryType } } }).Documentunion; per-resource attribute shapes only exist at runtime in the registry, so every endpoint types identically and clients narrow by hand.{ errors: SimpleError[] }for 422s, butrenderJsonApiErroremits JSON:API error documents.This package is the source of truth for everything tuyau is missing: the resource registry (model ↔ resource ↔ type name), Lucid column metadata including
serializeAswire names, relationship definitions with cardinality, declaredfilters, route registration per resource, the deserializer (request contract), the document builder (response contract), and the error renderer (error contract). It can generate what tuyau structurally cannot.Proposed artifacts (phased)
Phase 1: resource typings + document types
ModelAttributes-style indirection so generated code cannot drift), relationships with cardinality and related type names.SingleDocument<T>/CollectionDocument<T>, request documents for store/update (envelope-aware, wire names, to-one → linkage not FK), and the real error document type.'plugins' | 'plugins.library' | ...), withincludedunions computed from the requested paths.JsonApiResource.picktopick<K extends keyof Serialized>(names: K[]): Pick<Serialized, K>soReturnType<resource['attributes']>is exact for customized resources; the default derivation is already deterministic from Lucid metadata. Conditional attributes (e.g. a create-only plaintext token) surface as optional fields; document annotatingattributes()return types for these.Phase 2: typed fetch client
api.libraries.index({ include, filter, sort, page })with filters typed from declaredFilterHandlers, include paths compile-checked, responses narrowed by the include argument, anderrorResponseas JSON:API error documents.generateRegistry) or an ace command; needs the booted registry, not static analysis.Phase 3: WarpDrive schema emission
ResourceSchemamodules:{ identity, fields: [{ kind: 'attribute' | 'relationship', name, type, options }] }, cardinality mapped to relationship kinds, wire names respected.SchemaRecord, so an Ember app registers generated schemas instead of hand-maintaining models that drift from the server.Design notes
/api/v1vs/api/v2mounting) should follow the same route-name namespacing the LinkBuilder already uses..adonisjs/clientartifacts to sibling apps; this would replace that role for JSON:API surfaces).