@nage-api/contracts
v1.0.0-beta.4
Published
Pure TypeScript contracts for the @nage-api framework — types only, no runtime, no Nest
Readme
@nage-api/contracts
Pure TypeScript contracts for the @nage-api framework. Types only — no runtime
code, no NestJS, no validation libraries (PLAN.md §8).
It sits at the bottom of the dependency graph, so every package and every
generated app may import it, while it imports nothing itself. The boundary lint
rules in @nage-api/eslint-config enforce that.
// A query is typed against the entity, and a policy states what a client may reach.
import type { BaseEntity, Query, QueryPolicy } from '@nage-api/contracts';
interface Invoice extends BaseEntity {
readonly id: number;
reference: string;
total: number;
cost_price: number;
}
const query: Query<Invoice> = {
where: { total: { gte: 100 }, reference: { like: 'INV-2026%' } },
sort: [['total', 'desc']],
select: ['id', 'reference', 'total'],
limit: 50,
};
const invoicePolicy: QueryPolicy<Invoice> = {
filterable: ['reference', 'total'],
sortable: ['total'],
selectable: ['id', 'reference', 'total'],
searchable: ['reference'],
populatable: [],
scopes: [],
operators: ['eq', 'gte', 'lte', 'like'],
maxLimit: 100,
defaultLimit: 25,
maxPopulateDepth: 1,
};cost_price is a field of the entity and is absent from every list in the
policy, so no spelling of a request can filter, sort or select by it. The legacy
DSL forwarded whatever where the client sent to the ORM.
// The envelope is a discriminated union, and the error catalog is closed.
import type { ApiResponse } from '@nage-api/contracts';
export function referenceOf(response: ApiResponse<{ readonly reference: string }>): string {
if (!response.success) {
switch (response.error.code) {
case 'RESOURCE_NOT_FOUND':
return '(deleted)';
// `case 'SOMETHING_WENT_WRONG':` does not compile — clients program
// against codes, and a code that is not in the catalog is a typo.
default:
throw new Error(response.error.code);
}
}
return response.data.reference;
}Modules
| File | Contracts |
| --------------------- | -------------------------------------------------------------------------------------- |
| common.types.ts | Id, Nullable, Maybe, DeepPartial, DeepReadonly, FieldName, Brand |
| error.types.ts | ErrorCode catalog, ErrorDetail, ErrorPayload, NageErrorLike (§17) |
| pagination.types.ts | Paginated<T>, PaginationMeta, cursor variants (§16.1) |
| query.types.ts | Query<T>, Where<T>, Sort<T>, ComparisonOperator, QueryPolicy<T> (§12, §16.2) |
| entity.types.ts | BaseEntity, audit/soft-delete/version fields, Writable<T>, DeleteMode (§14.2) |
| auth.types.ts | AuthUser, JwtClaims, SessionRecord, TokenPair, RoleMatrix (§15) |
| context.types.ts | RequestContext, ContextStore — the correlation-id carrier (§18) |
| response.types.ts | SuccessResponse, ErrorResponse, ApiResponse, Result<T> (§16.1) |
| job.types.ts | Job<TEntity, TBody, TParams>, QueueJob, QueueJobHandler (§13) |
| repository.types.ts | RepositoryPort<T>, UnitOfWork, TxContext, KeyValueStore (§14.1) |
| config.types.ts | NageConfig, NageCoreConfig and one block per feature, SecretProviderPort (§11) |
| logger.types.ts | LoggerPort, LogLevel, LogFields (§18) |
| security.types.ts | RateLimitStore, RateLimitResult, SecurityFinding, SecuritySeverity (§12) |
Design notes
- Roles are project-defined.
AuthUser<TRole extends string>keeps the union open so an application constrains it to its own roles. - No unbounded queries.
QueryPolicy.maxLimitis required rather than optional, so a model cannot be given a policy that forgot the ceiling.limititself stays a plainnumber— the legacylimit: -1is rejected by the parser in@nage-api/data, not by the type, because a branded positive integer would make every caller construct one. - Error codes are a closed union. Clients program against
code, never againstmessage. - Everything is
readonlyby default. The fourJobmembers that lifecycle hooks exist to change (query,body,record,records, pluscount) are deliberately writable; nothing else is. - Config types live here, config loading does not. A feature package must
be able to read the shape of its own config block without importing
@nage-api/config, which sits above it (§7.2).
The deeper guide is docs/packages/contracts.md.
