@danceroutine/tango-resources 0.1.0 → 1.0.0
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/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/context/RequestContext.d.ts +23 -4
- package/dist/filters/FilterSet.d.ts +59 -4
- package/dist/filters/index.d.ts +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +515 -205
- package/dist/index.js.map +1 -1
- package/dist/pagination/CursorPaginationInput.d.ts +7 -0
- package/dist/pagination/OffsetPaginationInput.d.ts +7 -0
- package/dist/pagination/PaginatedResponse.d.ts +8 -2
- package/dist/pagination/Paginator.d.ts +5 -3
- package/dist/pagination/index.d.ts +5 -3
- package/dist/paginators/CursorPaginator.d.ts +32 -6
- package/dist/paginators/OffsetPaginator.d.ts +30 -7
- package/dist/resource/OpenAPIDescription.d.ts +21 -0
- package/dist/resource/ResourceModelLike.d.ts +16 -0
- package/dist/resource/index.d.ts +5 -0
- package/dist/serializer/ModelSerializer.d.ts +47 -0
- package/dist/serializer/Serializer.d.ts +52 -0
- package/dist/serializer/index.d.ts +5 -0
- package/dist/view/APIView.d.ts +26 -0
- package/dist/view/GenericAPIView.d.ts +57 -0
- package/dist/view/generics/CreateAPIView.d.ts +10 -0
- package/dist/view/generics/ListAPIView.d.ts +10 -0
- package/dist/view/generics/ListCreateAPIView.d.ts +11 -0
- package/dist/view/generics/RetrieveAPIView.d.ts +10 -0
- package/dist/view/generics/RetrieveDestroyAPIView.d.ts +11 -0
- package/dist/view/generics/RetrieveUpdateAPIView.d.ts +12 -0
- package/dist/view/generics/RetrieveUpdateDestroyAPIView.d.ts +13 -0
- package/dist/view/generics/index.d.ts +10 -0
- package/dist/view/index.d.ts +8 -0
- package/dist/view/index.js +3 -0
- package/dist/view/mixins/CreateModelMixin.d.ts +11 -0
- package/dist/view/mixins/DestroyModelMixin.d.ts +11 -0
- package/dist/view/mixins/ListModelMixin.d.ts +11 -0
- package/dist/view/mixins/RetrieveModelMixin.d.ts +11 -0
- package/dist/view/mixins/UpdateModelMixin.d.ts +12 -0
- package/dist/view/mixins/index.d.ts +8 -0
- package/dist/view-BNGEURL_.js +547 -0
- package/dist/view-BNGEURL_.js.map +1 -0
- package/dist/viewset/ModelViewSet.d.ts +91 -45
- package/dist/viewset/index.d.ts +2 -1
- package/package.json +75 -69
- package/dist/domain/index.d.ts +0 -8
- package/dist/pagination/PaginationInput.d.ts +0 -7
- package/dist/viewset/ModelViewSet.js +0 -143
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["request: Request","user: TUser | null","params: Record<string, string>","value: unknown","key: string | symbol","value: T","user?: TUser | null","value: unknown","spec: Record<string, FilterResolver<T>>","params: URLSearchParams","filters: FilterInput<T>[]","resolver: FilterResolver<T>","value: string | string[] | undefined","filter: Partial<Record<FilterKey<T>, FilterValue>>","PaginationInput: z.ZodType<PaginationInputValue>","value: unknown","results: T[]","pageNumber: number","perPage: number","totalCount?: number","queryset: QuerySet<T>","params: URLSearchParams","results: TResult[]","context?: { totalCount?: number }","response: PaginatedResponse<TResult>","page: number","value: unknown","results: T[]","nextCursor: string | null","previousCursor: string | null","queryset: QuerySet<T>","perPage: number","cursorField: keyof T","params: URLSearchParams","parsedDirection: CursorDirection","results: TResult[]","response: PaginatedResponse<TResult>","_totalCount?: number","cursor?: string","page: number","cursor: string","item: T","payload: CursorPayload","parsed: unknown","value: unknown","config: ModelViewSetConfig<TModel, TRead, TWrite>","ctx: RequestContext","searchFilters: FilterInput<TModel>[]","_ctx: RequestContext","id: string","error: unknown"],"sources":["../src/context/RequestContext.ts","../src/context/index.ts","../src/filters/internal/InternalFilterType.ts","../src/filters/FilterSet.ts","../src/filters/index.ts","../src/pagination/PaginationInput.ts","../src/paginators/OffsetPaginator.ts","../src/paginators/CursorPaginator.ts","../src/pagination/index.ts","../src/paginators/index.ts","../src/viewset/ModelViewSet.ts","../src/viewset/index.ts","../src/domain/index.ts"],"sourcesContent":["/**\n * Default user shape for RequestContext.\n * Consumers can provide their own user type via the TUser generic parameter.\n */\nexport interface BaseUser {\n id: string | number;\n roles?: string[];\n}\n\n/**\n * Normalized request context passed through the framework adapter into viewset methods.\n * Generic over the user type so consumers can plug in their own auth infrastructure.\n */\nexport class RequestContext<TUser = BaseUser> {\n static readonly BRAND = 'tango.resources.request_context' as const;\n private state: Map<string | symbol, unknown> = new Map();\n readonly __tangoBrand: typeof RequestContext.BRAND = RequestContext.BRAND;\n\n constructor(\n public readonly request: Request,\n public user: TUser | null = null,\n public params: Record<string, string> = {}\n ) {}\n\n static isRequestContext<TUser = BaseUser>(value: unknown): value is RequestContext<TUser> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === RequestContext.BRAND\n );\n }\n\n setState<T>(key: string | symbol, value: T): void {\n this.state.set(key, value);\n }\n\n getState<T>(key: string | symbol): T | undefined {\n return this.state.get(key) as T | undefined;\n }\n\n hasState(key: string | symbol): boolean {\n return this.state.has(key);\n }\n\n static create<TUser = BaseUser>(request: Request, user?: TUser | null): RequestContext<TUser> {\n return new RequestContext<TUser>(request, user ?? null);\n }\n\n clone(): RequestContext<TUser> {\n const cloned = new RequestContext<TUser>(this.request, this.user, { ...this.params });\n cloned.state = new Map(this.state);\n return cloned;\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport type { BaseUser } from './RequestContext';\nexport { RequestContext } from './RequestContext';\n","export const InternalFilterType = {\n SCALAR: 'scalar',\n ILIKE: 'ilike',\n RANGE: 'range',\n IN: 'in',\n CUSTOM: 'custom',\n} as const;\n","import type { FilterInput, FilterKey, FilterValue } from '@danceroutine/tango-orm';\nimport { InternalFilterType } from './internal/InternalFilterType';\nimport type { RangeOperator } from './RangeOperator';\n\n/**\n * Configuration for how a query parameter should be resolved into a filter.\n * Supports scalar equality, case-insensitive search, range comparisons, IN queries, and custom logic.\n */\nexport type FilterResolver<T> =\n | { type: typeof InternalFilterType.SCALAR; column: keyof T }\n | { type: typeof InternalFilterType.ILIKE; columns: (keyof T)[] }\n | { type: typeof InternalFilterType.RANGE; column: keyof T; op: RangeOperator }\n | { type: typeof InternalFilterType.IN; column: keyof T }\n | {\n type: typeof InternalFilterType.CUSTOM;\n apply: (value: string | string[] | undefined) => FilterInput<T> | undefined;\n };\n\nexport class FilterSet<T extends Record<string, unknown>> {\n static readonly BRAND = 'tango.resources.filter_set' as const;\n readonly __tangoBrand: typeof FilterSet.BRAND = FilterSet.BRAND;\n\n static isFilterSet<T extends Record<string, unknown>>(value: unknown): value is FilterSet<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === FilterSet.BRAND\n );\n }\n\n constructor(private spec: Record<string, FilterResolver<T>>) {}\n\n apply(params: URLSearchParams): FilterInput<T>[] {\n const filters: FilterInput<T>[] = [];\n\n for (const [key, resolver] of Object.entries(this.spec)) {\n const rawValue = params.getAll(key);\n const value = rawValue.length > 1 ? rawValue : (params.get(key) ?? undefined);\n\n if (value === undefined) continue;\n\n const filter = this.resolveFilter(resolver, value);\n if (filter) {\n filters.push(filter);\n }\n }\n\n return filters;\n }\n\n private resolveFilter(\n resolver: FilterResolver<T>,\n value: string | string[] | undefined\n ): FilterInput<T> | undefined {\n if (value === undefined) return undefined;\n\n switch (resolver.type) {\n case InternalFilterType.SCALAR:\n return { [resolver.column]: value } as FilterInput<T>;\n\n case InternalFilterType.ILIKE: {\n const pattern = `%${value}%`;\n const filter: Partial<Record<FilterKey<T>, FilterValue>> = {};\n resolver.columns.forEach((col) => {\n filter[`${String(col)}__icontains` as FilterKey<T>] = pattern;\n });\n return filter;\n }\n\n case InternalFilterType.RANGE: {\n const lookupKey = `${String(resolver.column)}__${resolver.op}` as keyof FilterInput<T>;\n return { [lookupKey]: value } as FilterInput<T>;\n }\n\n case InternalFilterType.IN: {\n const arr = Array.isArray(value) ? value : String(value).split(',');\n const lookupKey = `${String(resolver.column)}__in` as keyof FilterInput<T>;\n return { [lookupKey]: arr } as FilterInput<T>;\n }\n\n case InternalFilterType.CUSTOM:\n return resolver.apply(value);\n\n default:\n return undefined;\n }\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport type { FilterType } from './FilterType';\nexport type { RangeOperator } from './RangeOperator';\nexport type { FilterResolver } from './FilterSet';\nexport { FilterSet } from './FilterSet';\n","import { z } from 'zod';\n\nexport type PaginationInputValue = {\n limit: number;\n offset: number;\n page?: number;\n};\n\nexport const PaginationInput: z.ZodType<PaginationInputValue> = z.object({\n limit: z.coerce\n .number()\n .int()\n .min(1)\n .default(25)\n .transform((value) => Math.min(value, 100)),\n offset: z.coerce.number().int().min(0).default(0),\n page: z.coerce.number().int().min(1).optional(),\n});\n","import type { QuerySet } from '@danceroutine/tango-orm';\nimport type { Paginator, Page } from '../pagination/Paginator';\nimport type { PaginatedResponse } from '../pagination/PaginatedResponse';\nimport { PaginationInput } from '../pagination/PaginationInput';\n\nclass OffsetPage<T> implements Page<T> {\n static readonly BRAND = 'tango.resources.offset_page' as const;\n readonly __tangoBrand: typeof OffsetPage.BRAND = OffsetPage.BRAND;\n\n static isOffsetPage<T>(value: unknown): value is OffsetPage<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === OffsetPage.BRAND\n );\n }\n\n constructor(\n public readonly results: T[],\n private readonly pageNumber: number,\n private readonly perPage: number,\n private readonly totalCount?: number\n ) {}\n\n hasNext(): boolean {\n if (this.totalCount === undefined) {\n return false;\n }\n return this.endIndex() < this.totalCount;\n }\n\n hasPrevious(): boolean {\n return this.pageNumber > 1;\n }\n\n nextPageNumber(): number | null {\n return this.hasNext() ? this.pageNumber + 1 : null;\n }\n\n previousPageNumber(): number | null {\n return this.hasPrevious() ? this.pageNumber - 1 : null;\n }\n\n startIndex(): number {\n return (this.pageNumber - 1) * this.perPage;\n }\n\n endIndex(): number {\n return this.startIndex() + this.results.length;\n }\n}\n\n/**\n * Offset/limit paginator modelled after DRF's LimitOffsetPagination.\n * Handles parsing limit/offset/page from URL query params and building\n * the paginated response envelope with next/previous links.\n *\n * @example\n * ```typescript\n * const paginator = new OffsetPaginator(queryset);\n * const { limit, offset } = paginator.parseParams(searchParams);\n * const results = await queryset.limit(limit).offset(offset).fetchAll();\n * const response = paginator.getPaginatedResponse(results, totalCount);\n * ```\n */\nexport class OffsetPaginator<T extends Record<string, unknown>> implements Paginator<T> {\n static readonly BRAND = 'tango.resources.offset_paginator' as const;\n readonly __tangoBrand: typeof OffsetPaginator.BRAND = OffsetPaginator.BRAND;\n private limit = 25;\n private offset = 0;\n\n constructor(\n private queryset: QuerySet<T>,\n private perPage: number = 25\n ) {\n this.limit = perPage;\n }\n\n static isOffsetPaginator<T extends Record<string, unknown>>(value: unknown): value is OffsetPaginator<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === OffsetPaginator.BRAND\n );\n }\n\n /**\n * Parse limit, offset, and page from URL search params.\n * If `page` is provided, it's converted to an offset.\n * Stores parsed values for use by getPaginatedResponse.\n */\n parse(params: URLSearchParams): void {\n const input = {\n limit: params.get('limit') ?? undefined,\n offset: params.get('offset') ?? undefined,\n page: params.get('page') ?? undefined,\n };\n\n const parsed = PaginationInput.parse(input);\n\n if (parsed.page) {\n parsed.offset = (parsed.page - 1) * parsed.limit;\n }\n\n this.limit = parsed.limit;\n this.offset = parsed.offset;\n }\n\n parseParams(params: URLSearchParams): { limit: number; offset: number } {\n this.parse(params);\n return { limit: this.limit, offset: this.offset };\n }\n\n /**\n * Build a DRF-style paginated response with count, next, and previous links.\n * Uses the limit/offset stored from the most recent parseParams call.\n */\n toResponse<TResult>(results: TResult[], context?: { totalCount?: number }): PaginatedResponse<TResult> {\n const totalCount = context?.totalCount;\n const response: PaginatedResponse<TResult> = { results };\n\n if (totalCount !== undefined) {\n response.count = totalCount;\n\n if (this.offset + this.limit < totalCount) {\n response.next = `?limit=${this.limit}&offset=${this.offset + this.limit}`;\n }\n\n if (this.offset > 0) {\n const prevOffset = Math.max(0, this.offset - this.limit);\n response.previous = `?limit=${this.limit}&offset=${prevOffset}`;\n }\n }\n\n return response;\n }\n\n getPaginatedResponse<TResult>(results: TResult[], totalCount?: number): PaginatedResponse<TResult> {\n return this.toResponse(results, { totalCount });\n }\n\n apply(queryset: QuerySet<T>): QuerySet<T> {\n return queryset.limit(this.limit).offset(this.offset);\n }\n\n async paginate(page: number): Promise<Page<T>> {\n return this.getPage(page);\n }\n\n async getPage(page: number): Promise<Page<T>> {\n const offset = (page - 1) * this.perPage;\n const results = await this.queryset.offset(offset).limit(this.perPage).fetch();\n\n const totalCount = await this.count();\n\n return new OffsetPage(results.results, page, this.perPage, totalCount);\n }\n\n async count(): Promise<number> {\n return this.queryset.count();\n }\n}\n","import type { FilterInput, QuerySet } from '@danceroutine/tango-orm';\nimport type { Paginator, Page } from '../pagination/Paginator';\nimport type { PaginatedResponse } from '../pagination/PaginatedResponse';\n\ntype CursorDirection = 'asc' | 'desc';\n\ntype CursorPayload = {\n v: 1;\n field: string;\n dir: CursorDirection;\n value: unknown;\n};\n\n/**\n * Represents a single cursor page of results.\n * Cursor pages do not expose numeric page navigation like offset pagination.\n */\nclass CursorPage<T> implements Page<T> {\n static readonly BRAND = 'tango.resources.cursor_page' as const;\n readonly __tangoBrand: typeof CursorPage.BRAND = CursorPage.BRAND;\n\n static isCursorPage<T>(value: unknown): value is CursorPage<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === CursorPage.BRAND\n );\n }\n\n constructor(\n public readonly results: T[],\n public readonly nextCursor: string | null,\n public readonly previousCursor: string | null\n ) {}\n\n hasNext(): boolean {\n return this.nextCursor !== null;\n }\n\n hasPrevious(): boolean {\n return this.previousCursor !== null;\n }\n\n nextPageNumber(): number | null {\n return null;\n }\n\n previousPageNumber(): number | null {\n return null;\n }\n\n startIndex(): number {\n return 0;\n }\n\n endIndex(): number {\n return this.results.length;\n }\n}\n\n/**\n * Cursor-based paginator for stable forward navigation with opaque cursor tokens.\n * It supports `limit`, `cursor`, and `ordering` query params and returns DRF-style\n * paginated envelopes with cursor links.\n */\nexport class CursorPaginator<T extends Record<string, unknown>> implements Paginator<T> {\n static readonly BRAND = 'tango.resources.cursor_paginator' as const;\n readonly __tangoBrand: typeof CursorPaginator.BRAND = CursorPaginator.BRAND;\n private limit: number;\n private cursor: string | null = null;\n private direction: CursorDirection = 'asc';\n private nextCursor: string | null = null;\n private previousCursor: string | null = null;\n\n constructor(\n private queryset: QuerySet<T>,\n private perPage: number = 25,\n private cursorField: keyof T = 'id' as keyof T\n ) {\n this.limit = perPage;\n }\n\n static isCursorPaginator<T extends Record<string, unknown>>(value: unknown): value is CursorPaginator<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === CursorPaginator.BRAND\n );\n }\n\n parse(params: URLSearchParams): void {\n const rawLimit = Number.parseInt(params.get('limit') ?? '', 10);\n if (Number.isFinite(rawLimit) && rawLimit > 0) {\n this.limit = Math.min(rawLimit, 100);\n } else {\n this.limit = this.perPage;\n }\n\n this.cursor = params.get('cursor');\n\n const ordering = params.get('ordering');\n if (ordering) {\n const parsedDirection: CursorDirection = ordering.startsWith('-') ? 'desc' : 'asc';\n const parsedField = ordering.startsWith('-') ? ordering.slice(1) : ordering;\n this.direction = parsedField === String(this.cursorField) ? parsedDirection : 'asc';\n } else {\n this.direction = 'asc';\n }\n }\n\n parseParams(params: URLSearchParams): { limit: number; offset: number } {\n this.parse(params);\n return { limit: this.limit, offset: 0 };\n }\n\n toResponse<TResult>(results: TResult[]): PaginatedResponse<TResult> {\n const response: PaginatedResponse<TResult> = { results };\n if (this.nextCursor) {\n response.next = this.buildPageLink(this.nextCursor);\n }\n if (this.previousCursor) {\n response.previous = this.buildPageLink(this.previousCursor);\n }\n return response;\n }\n\n getPaginatedResponse<TResult>(results: TResult[], _totalCount?: number): PaginatedResponse<TResult> {\n return this.toResponse(results);\n }\n\n apply(queryset: QuerySet<T>): QuerySet<T> {\n let qs = queryset.limit(this.limit + 1);\n if (this.cursor) {\n const decoded = this.decodeCursor(this.cursor);\n if (decoded.field !== String(this.cursorField)) {\n throw new Error('Invalid cursor: field mismatch');\n }\n const lookup = this.direction === 'asc' ? '__gt' : '__lt';\n const fieldLookup = `${String(this.cursorField)}${lookup}`;\n const filterInput = { [fieldLookup]: decoded.value } as FilterInput<T>;\n qs = qs.filter(filterInput);\n }\n const orderToken = this.direction === 'asc' ? String(this.cursorField) : `-${String(this.cursorField)}`;\n return qs.orderBy(orderToken as keyof T | `-${string}`);\n }\n\n async paginate(cursor?: string): Promise<Page<T>> {\n const appliedCursor = cursor ?? this.cursor;\n this.cursor = appliedCursor;\n const fetched = await this.apply(this.queryset).fetch();\n const results = [...fetched.results];\n const hasMore = results.length > this.limit;\n\n if (hasMore) {\n results.pop();\n }\n\n this.previousCursor = appliedCursor ?? null;\n const last = results.at(-1);\n this.nextCursor = hasMore && last ? this.encodeCursor(last) : null;\n\n return new CursorPage(results, this.nextCursor, this.previousCursor);\n }\n\n async getPage(page: number): Promise<Page<T>> {\n if (page !== 1) {\n throw new Error('CursorPaginator only supports getPage(1). Use cursor pagination for subsequent pages.');\n }\n return this.paginate();\n }\n\n private buildPageLink(cursor: string): string {\n const orderingToken = this.direction === 'asc' ? String(this.cursorField) : `-${String(this.cursorField)}`;\n return `?limit=${this.limit}&cursor=${encodeURIComponent(cursor)}&ordering=${encodeURIComponent(orderingToken)}`;\n }\n\n private encodeCursor(item: T): string {\n const payload: CursorPayload = {\n v: 1,\n field: String(this.cursorField),\n dir: this.direction,\n value: item[this.cursorField],\n };\n return Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64');\n }\n\n private decodeCursor(cursor: string): CursorPayload {\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.from(cursor, 'base64').toString('utf-8'));\n } catch {\n throw new Error('Invalid cursor: malformed token');\n }\n\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n (parsed as { v?: unknown }).v !== 1 ||\n typeof (parsed as { field?: unknown }).field !== 'string' ||\n ((parsed as { dir?: unknown }).dir !== 'asc' && (parsed as { dir?: unknown }).dir !== 'desc') ||\n !('value' in parsed)\n ) {\n throw new Error('Invalid cursor: unsupported payload');\n }\n\n return parsed as CursorPayload;\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { OffsetPaginator } from '../paginators/OffsetPaginator';\nexport { CursorPaginator } from '../paginators/CursorPaginator';\nexport { PaginationInput } from './PaginationInput';\nexport type { PaginationInputValue } from './PaginationInput';\nexport type { Paginator, Page } from './Paginator';\nexport type { PaginatedResponse } from './PaginatedResponse';\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { CursorPaginator } from './CursorPaginator';\nexport { OffsetPaginator } from './OffsetPaginator';\n","import type { FilterInput, Repository } from '@danceroutine/tango-orm';\nimport type { z } from 'zod';\nimport type { RequestContext } from '../context/index';\nimport type { FilterSet } from '../filters/index';\nimport { OffsetPaginator } from '../paginators/OffsetPaginator';\nimport { toHttpError } from '@danceroutine/tango-core';\nimport { Q } from '@danceroutine/tango-orm';\n\n/**\n * Configuration for a ModelViewSet, defining how a model is exposed as an API resource.\n *\n * @template TModel - The record type of the underlying database model\n * @template TRead - Zod schema used to validate and shape read (GET) responses\n * @template TWrite - Zod schema used to validate incoming write (POST/PUT) request bodies\n *\n * @example\n * ```typescript\n * const config: ModelViewSetConfig<Post, typeof PostReadSchema, typeof PostWriteSchema> = {\n * repository: postRepository,\n * readSchema: PostReadSchema,\n * writeSchema: PostWriteSchema,\n * updateSchema: PostPatchSchema,\n * filters: postFilterSet,\n * orderingFields: ['createdAt', 'title'],\n * searchFields: ['title', 'body'],\n * };\n * ```\n */\nexport interface ModelViewSetConfig<\n TModel extends Record<string, unknown>,\n TRead extends z.ZodType,\n TWrite extends z.ZodObject<z.ZodRawShape>,\n> {\n /** The repository instance used to query and persist model data */\n repository: Repository<TModel>;\n\n /** Zod schema applied to outgoing data (list and detail responses) */\n readSchema: TRead;\n\n /** Zod schema applied to incoming data for create operations */\n writeSchema: TWrite;\n\n /** Optional Zod schema for partial updates (PATCH). Falls back to writeSchema if omitted */\n updateSchema?: z.ZodType<Partial<TModel>>;\n\n /** Optional filter set defining which query parameters can filter the list endpoint */\n filters?: FilterSet<TModel>;\n\n /** Fields that clients are allowed to sort by via query parameters */\n orderingFields?: (keyof TModel)[];\n\n /** Fields that are searched when a free-text search query parameter is provided */\n searchFields?: (keyof TModel)[];\n}\n\n/**\n * Base class for creating RESTful API viewsets with built-in CRUD operations.\n * Provides list, retrieve, create, update, and delete methods with filtering,\n * search, pagination, and ordering support.\n */\nexport abstract class ModelViewSet<\n TModel extends Record<string, unknown>,\n TRead extends z.ZodType,\n TWrite extends z.ZodObject<z.ZodRawShape>,\n> {\n static readonly BRAND = 'tango.resources.model_view_set' as const;\n protected repository: Repository<TModel>;\n protected readSchema: TRead;\n protected writeSchema: TWrite;\n protected updateSchema: z.ZodType<Partial<TModel>>;\n protected filters?: FilterSet<TModel>;\n protected orderingFields: (keyof TModel)[];\n protected searchFields: (keyof TModel)[];\n readonly __tangoBrand: typeof ModelViewSet.BRAND = ModelViewSet.BRAND;\n\n static isModelViewSet(\n value: unknown\n ): value is ModelViewSet<Record<string, unknown>, z.ZodType, z.ZodObject<z.ZodRawShape>> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === ModelViewSet.BRAND\n );\n }\n\n constructor(config: ModelViewSetConfig<TModel, TRead, TWrite>) {\n this.repository = config.repository;\n this.readSchema = config.readSchema;\n this.writeSchema = config.writeSchema;\n this.updateSchema = config.updateSchema ?? (config.writeSchema.partial() as z.ZodType<Partial<TModel>>);\n this.filters = config.filters;\n this.orderingFields = config.orderingFields ?? [];\n this.searchFields = config.searchFields ?? [];\n }\n\n async list(ctx: RequestContext): Promise<Response> {\n try {\n const params = new URL(ctx.request.url).searchParams;\n const paginator = new OffsetPaginator<TModel>(this.repository.query());\n paginator.parse(params);\n\n let qs = this.repository.query();\n\n if (this.filters) {\n const filterInputs = this.filters.apply(params);\n if (filterInputs.length > 0) {\n qs = qs.filter(Q.and(...filterInputs));\n }\n }\n\n const search = params.get('search');\n if (search && this.searchFields.length > 0) {\n const searchFilters: FilterInput<TModel>[] = this.searchFields.map((field) => {\n const lookup = `${String(field)}__icontains`;\n return { [lookup]: search } as FilterInput<TModel>;\n });\n qs = qs.filter(Q.or(...searchFilters));\n }\n\n const ordering = params.get('ordering');\n if (ordering) {\n const orderTokens = ordering.split(',').filter((field) => {\n const cleanField = field.startsWith('-') ? field.slice(1) : field;\n return this.orderingFields.includes(cleanField as keyof TModel);\n });\n if (orderTokens.length > 0) {\n qs = qs.orderBy(...orderTokens.map((token) => token as keyof TModel | `-${string & keyof TModel}`));\n }\n }\n\n qs = paginator.apply(qs);\n\n const [result, totalCount] = await Promise.all([qs.fetch(this.readSchema), qs.count()]);\n const response = paginator.toResponse<z.output<TRead>>(result.results, { totalCount });\n\n return new Response(JSON.stringify(response), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n async retrieve(_ctx: RequestContext, id: string): Promise<Response> {\n try {\n const pk = this.repository.meta.pk;\n const filterById = { [pk]: id } as FilterInput<TModel>;\n const result = await this.repository\n .query()\n .filter(filterById)\n .fetchOne(this.readSchema);\n\n if (!result) {\n return new Response(JSON.stringify({ error: 'Not found' }), {\n status: 404,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n return new Response(JSON.stringify(result), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n async create(ctx: RequestContext): Promise<Response> {\n try {\n const body = await ctx.request.json();\n const validated = this.writeSchema.parse(body);\n\n const created = await this.repository.create(validated as Partial<TModel>);\n const result = this.readSchema.parse(created);\n\n return new Response(JSON.stringify(result), {\n status: 201,\n headers: { 'Content-Type': 'application/json' },\n });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n async update(ctx: RequestContext, id: string): Promise<Response> {\n try {\n const body = await ctx.request.json();\n const validated = this.updateSchema.parse(body);\n\n const pkValue = id as TModel[keyof TModel];\n\n const updated = await this.repository.update(pkValue, validated as Partial<TModel>);\n const result = this.readSchema.parse(updated);\n\n return new Response(JSON.stringify(result), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n async destroy(_ctx: RequestContext, id: string): Promise<Response> {\n try {\n const pkValue = id as TModel[keyof TModel];\n\n await this.repository.delete(pkValue);\n\n return new Response(null, { status: 204 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n protected handleError(error: unknown): Response {\n const httpError = toHttpError(error);\n return new Response(JSON.stringify(httpError.body), {\n status: httpError.status,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { ModelViewSet, type ModelViewSetConfig } from './ModelViewSet';\n","/**\n * Compatibility aggregate barrel. Canonical source-of-truth types now live in\n * `filters`, `pagination`, and `viewset` subdomains.\n */\n\nexport type { FilterType, RangeOperator } from '../filters/index';\nexport type { ModelViewSetConfig } from '../viewset/index';\nexport type { PaginatedResponse, Paginator, Page } from '../pagination/index';\nexport { PaginationInput } from '../pagination/index';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["request: TangoRequest","user: TUser | null","params: Record<string, string>","value: unknown","request: Request","user?: TUser | null","key: string | symbol","value: T","spec: Record<string, FilterResolver<T>>","allowAllParams: boolean","config: FilterSetDefineConfig<T>","value: unknown","fieldDeclarations: Partial<Record<keyof T, FieldFilterDeclaration>>","fieldParsers: Partial<Record<keyof T, FilterValueParser>>","field: keyof T","declaration: FieldFilterDeclaration","parser: FilterValueParser | undefined","value: FieldFilterDeclaration","declaration: AliasFilterDeclaration<T>","value: AliasFilterDeclaration<T>","fields: readonly (keyof T)[]","lookup: FilterLookup","parser?: FilterValueParser","composed: Partial<Record<FilterKey<T>, FilterValue>>","value: ResolvedFilterValue | undefined","baseParam: string","value: string | string[] | undefined","value: ResolvedFilterValue","params: TangoQueryParams","filters: FilterInput<T>[]","param: string","resolver: FilterResolver<T>","filter: Partial<Record<FilterKey<T>, FilterValue>>","CursorPaginationInput: z.ZodType<CursorPaginationInputValue>","results: T[]","nextCursor: string | null","previousCursor: string | null","value: unknown","queryset: QuerySet<T>","perPage: number","cursorField: keyof T","params: TangoQueryParams","parsedDirection: CursorDirection","results: TResult[]","response: CursorPaginatedResponse<TResult>","_totalCount?: number","cursor?: string","page: number","cursor: string","item: T","payload: CursorPayload","parsed: unknown","input: unknown","record: unknown","input: unknown","id: TModel[keyof TModel]","data: SerializerCreateInput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>","_id: TModel[keyof TModel]","data: SerializerUpdateInput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>","config: ModelViewSetConfig<TModel, TSerializer>","viewsetOrConstructor: AnyModelViewSet | (new (...args: never[]) => AnyModelViewSet)","value: unknown","actions: T","name: string","explicitPath?: string","input: string","ctx: RequestContext","searchFilters: FilterInput<TModel>[]","_ctx: RequestContext","id: string","queryset: QuerySet<TModel>","error: unknown","action: ViewSetActionDescriptor"],"sources":["../src/context/RequestContext.ts","../src/context/index.ts","../src/filters/internal/InternalFilterType.ts","../src/filters/FilterSet.ts","../src/filters/index.ts","../src/pagination/CursorPaginationInput.ts","../src/paginators/CursorPaginator.ts","../src/pagination/index.ts","../src/paginators/index.ts","../src/resource/index.ts","../src/serializer/Serializer.ts","../src/serializer/ModelSerializer.ts","../src/serializer/index.ts","../src/viewset/ModelViewSet.ts","../src/viewset/index.ts"],"sourcesContent":["import { TangoRequest } from '@danceroutine/tango-core';\n\n/**\n * Default user shape for RequestContext.\n * Consumers can provide their own user type via the TUser generic parameter.\n */\nexport interface BaseUser {\n id: string | number;\n roles?: string[];\n}\n\n/**\n * Normalized request context passed through the framework adapter into viewset methods.\n * Generic over the user type so consumers can plug in their own auth infrastructure.\n */\nexport class RequestContext<TUser = BaseUser> {\n static readonly BRAND = 'tango.resources.request_context' as const;\n readonly __tangoBrand: typeof RequestContext.BRAND = RequestContext.BRAND;\n private state: Map<string | symbol, unknown> = new Map();\n\n constructor(\n public readonly request: TangoRequest,\n public user: TUser | null = null,\n public params: Record<string, string> = {}\n ) {}\n\n /**\n * Narrow an unknown value to `RequestContext`.\n */\n static isRequestContext<TUser = BaseUser>(value: unknown): value is RequestContext<TUser> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === RequestContext.BRAND\n );\n }\n\n /**\n * Construct a context with optional user payload.\n */\n static create<TUser = BaseUser>(request: Request, user?: TUser | null): RequestContext<TUser> {\n return new RequestContext<TUser>(\n TangoRequest.isTangoRequest(request) ? request : new TangoRequest(request),\n user ?? null\n );\n }\n\n /**\n * Store arbitrary per-request state for downstream middleware/handlers.\n */\n setState<T>(key: string | symbol, value: T): void {\n this.state.set(key, value);\n }\n\n /**\n * Retrieve previously stored request state.\n */\n getState<T>(key: string | symbol): T | undefined {\n return this.state.get(key) as T | undefined;\n }\n\n /**\n * Check whether a state key has been set.\n */\n hasState(key: string | symbol): boolean {\n return this.state.has(key);\n }\n\n /**\n * Clone the context, including route params and request-local state.\n */\n clone(): RequestContext<TUser> {\n const cloned = new RequestContext<TUser>(this.request, this.user, { ...this.params });\n cloned.state = new Map(this.state);\n return cloned;\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport type { BaseUser } from './RequestContext';\nexport { RequestContext } from './RequestContext';\n","export const InternalFilterType = {\n SCALAR: 'scalar',\n ILIKE: 'ilike',\n RANGE: 'range',\n IN: 'in',\n CUSTOM: 'custom',\n} as const;\n","import { TangoQueryParams } from '@danceroutine/tango-core';\nimport type { FilterInput, FilterKey, FilterValue, LookupType } from '@danceroutine/tango-orm';\nimport { InternalFilterType } from './internal/InternalFilterType';\nimport type { RangeOperator } from './RangeOperator';\n\n/**\n * Configuration for how a query parameter should be resolved into a filter.\n * Supports scalar equality, case-insensitive search, range comparisons, IN queries, and custom logic.\n */\nexport type FilterResolver<T> =\n | { type: typeof InternalFilterType.SCALAR; column: keyof T }\n | { type: typeof InternalFilterType.ILIKE; columns: (keyof T)[] }\n | { type: typeof InternalFilterType.RANGE; column: keyof T; op: RangeOperator }\n | { type: typeof InternalFilterType.IN; column: keyof T }\n | {\n type: typeof InternalFilterType.CUSTOM;\n apply: (value: string | string[] | undefined) => FilterInput<T> | undefined;\n };\n\nexport type FilterLookup = LookupType;\n\nexport type FilterValueParser = (raw: string | string[]) => FilterValue | FilterValue[] | undefined;\n\nexport type FieldFilterDeclaration =\n | true\n | readonly FilterLookup[]\n | {\n lookups?: readonly FilterLookup[];\n param?: string;\n parse?: FilterValueParser;\n };\n\nexport type AliasFilterDeclaration<T extends Record<string, unknown>> =\n | FilterResolver<T>\n | {\n field: keyof T;\n lookup?: FilterLookup;\n parse?: FilterValueParser;\n }\n | {\n fields: readonly (keyof T)[];\n lookup?: FilterLookup;\n parse?: FilterValueParser;\n };\n\nexport interface FilterSetDefineConfig<T extends Record<string, unknown>> {\n fields?: Partial<Record<keyof T, FieldFilterDeclaration>>;\n aliases?: Record<string, AliasFilterDeclaration<T>>;\n parsers?: Partial<Record<keyof T, FilterValueParser>>;\n all?: '__all__';\n}\n\ntype ResolvedFilterValue = string | string[] | FilterValue | FilterValue[];\n\n/**\n * Declarative query-param to filter translation.\n *\n * A `FilterSet` lets viewsets expose safe, explicit filtering behavior\n * without leaking raw ORM filter syntax to request handlers.\n */\nexport class FilterSet<T extends Record<string, unknown>> {\n static readonly BRAND = 'tango.resources.filter_set' as const;\n readonly __tangoBrand: typeof FilterSet.BRAND = FilterSet.BRAND;\n\n /**\n * Resolve matching query parameters into ORM filter inputs.\n */\n constructor(\n private readonly spec: Record<string, FilterResolver<T>>,\n private readonly allowAllParams: boolean = false\n ) {}\n\n /**\n * Build a filter set from Django-style field declarations.\n */\n static define<T extends Record<string, unknown>>(config: FilterSetDefineConfig<T>): FilterSet<T> {\n const spec = FilterSet.normalizeDefineConfig(config);\n return new FilterSet(spec, config.all === '__all__');\n }\n\n /**\n * Narrow an unknown value to `FilterSet`.\n */\n static isFilterSet<T extends Record<string, unknown>>(value: unknown): value is FilterSet<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === FilterSet.BRAND\n );\n }\n\n private static normalizeDefineConfig<T extends Record<string, unknown>>(\n config: FilterSetDefineConfig<T>\n ): Record<string, FilterResolver<T>> {\n const spec: Record<string, FilterResolver<T>> = {};\n const fieldDeclarations: Partial<Record<keyof T, FieldFilterDeclaration>> = config.fields ?? {};\n const fieldParsers: Partial<Record<keyof T, FilterValueParser>> = config.parsers ?? {};\n\n for (const rawField of Object.keys(fieldDeclarations) as Array<keyof T>) {\n const declaration = fieldDeclarations[rawField];\n if (declaration === undefined) continue;\n const parser = fieldParsers[rawField];\n FilterSet.addFieldDeclaration(spec, rawField, declaration, parser);\n }\n\n const aliases = config.aliases ?? {};\n for (const [param, declaration] of Object.entries(aliases)) {\n spec[param] = FilterSet.normalizeAliasDeclaration(declaration);\n }\n\n return spec;\n }\n\n private static addFieldDeclaration<T extends Record<string, unknown>>(\n spec: Record<string, FilterResolver<T>>,\n field: keyof T,\n declaration: FieldFilterDeclaration,\n parser: FilterValueParser | undefined\n ): void {\n if (declaration === true) {\n spec[String(field)] = FilterSet.createLookupResolver(field, 'exact', parser);\n return;\n }\n\n if (FilterSet.isLookupArray(declaration)) {\n for (const lookup of declaration) {\n const param = FilterSet.resolveLookupParam(String(field), lookup);\n spec[param] = FilterSet.createLookupResolver(field, lookup, parser);\n }\n return;\n }\n\n const lookups = declaration.lookups ?? ['exact'];\n const baseParam = declaration.param ?? String(field);\n const effectiveParser = declaration.parse ?? parser;\n\n for (const lookup of lookups) {\n const param = FilterSet.resolveLookupParam(baseParam, lookup);\n spec[param] = FilterSet.createLookupResolver(field, lookup, effectiveParser);\n }\n }\n\n private static isLookupArray(value: FieldFilterDeclaration): value is readonly FilterLookup[] {\n return Array.isArray(value);\n }\n\n private static normalizeAliasDeclaration<T extends Record<string, unknown>>(\n declaration: AliasFilterDeclaration<T>\n ): FilterResolver<T> {\n if (FilterSet.isFilterResolverDeclaration(declaration)) {\n return declaration;\n }\n\n if ('fields' in declaration) {\n const lookup = declaration.lookup ?? 'icontains';\n return FilterSet.createMultiFieldResolver(declaration.fields, lookup, declaration.parse);\n }\n\n return FilterSet.createLookupResolver(declaration.field, declaration.lookup ?? 'exact', declaration.parse);\n }\n\n private static isFilterResolverDeclaration<T extends Record<string, unknown>>(\n value: AliasFilterDeclaration<T>\n ): value is FilterResolver<T> {\n if (typeof value !== 'object' || value === null || !('type' in value)) {\n return false;\n }\n\n return [\n InternalFilterType.SCALAR,\n InternalFilterType.ILIKE,\n InternalFilterType.RANGE,\n InternalFilterType.IN,\n InternalFilterType.CUSTOM,\n ].includes(value.type);\n }\n\n private static createMultiFieldResolver<T extends Record<string, unknown>>(\n fields: readonly (keyof T)[],\n lookup: FilterLookup,\n parser?: FilterValueParser\n ): FilterResolver<T> {\n if (lookup === 'icontains' && parser === undefined) {\n return { type: InternalFilterType.ILIKE, columns: [...fields] };\n }\n\n return {\n type: InternalFilterType.CUSTOM,\n apply: (raw) => {\n const parsed = FilterSet.resolveParserValue(raw, parser);\n if (parsed === undefined) return undefined;\n\n const composed: Partial<Record<FilterKey<T>, FilterValue>> = {};\n for (const field of fields) {\n const segment = FilterSet.resolveLookupFilter(field, lookup, parsed);\n if (!segment) continue;\n Object.assign(composed, segment);\n }\n\n return Object.keys(composed).length > 0 ? (composed as FilterInput<T>) : undefined;\n },\n };\n }\n\n private static createLookupResolver<T extends Record<string, unknown>>(\n field: keyof T,\n lookup: FilterLookup,\n parser?: FilterValueParser\n ): FilterResolver<T> {\n if (parser !== undefined) {\n return {\n type: InternalFilterType.CUSTOM,\n apply: (raw) => {\n const parsed = FilterSet.resolveParserValue(raw, parser);\n if (parsed === undefined) return undefined;\n return FilterSet.resolveLookupFilter(field, lookup, parsed);\n },\n };\n }\n\n switch (lookup) {\n case 'exact':\n return { type: InternalFilterType.SCALAR, column: field };\n case 'in':\n return { type: InternalFilterType.IN, column: field };\n case 'lt':\n case 'lte':\n case 'gt':\n case 'gte':\n return { type: InternalFilterType.RANGE, column: field, op: lookup };\n case 'icontains':\n return { type: InternalFilterType.ILIKE, columns: [field] };\n default:\n return {\n type: InternalFilterType.CUSTOM,\n apply: (raw) => FilterSet.resolveLookupFilter(field, lookup, raw),\n };\n }\n }\n\n private static resolveLookupFilter<T extends Record<string, unknown>>(\n field: keyof T,\n lookup: FilterLookup,\n value: ResolvedFilterValue | undefined\n ): FilterInput<T> | undefined {\n if (value === undefined) return undefined;\n\n if (lookup === 'exact') {\n return { [field]: value } as FilterInput<T>;\n }\n\n if (lookup === 'in') {\n const arr = Array.isArray(value) ? value : String(value).split(',');\n const lookupKey = `${String(field)}__in` as FilterKey<T>;\n return { [lookupKey]: arr } as FilterInput<T>;\n }\n\n if (lookup === 'icontains') {\n const lookupKey = `${String(field)}__icontains` as FilterKey<T>;\n return { [lookupKey]: `%${FilterSet.toScalarString(value)}%` } as FilterInput<T>;\n }\n\n const lookupKey = `${String(field)}__${lookup}` as FilterKey<T>;\n return { [lookupKey]: value as FilterValue } as FilterInput<T>;\n }\n\n private static resolveLookupParam(baseParam: string, lookup: FilterLookup): string {\n if (lookup === 'exact') {\n return baseParam;\n }\n return `${baseParam}__${lookup}`;\n }\n\n private static resolveParserValue(\n value: string | string[] | undefined,\n parser?: FilterValueParser\n ): ResolvedFilterValue | undefined {\n if (value === undefined) {\n return undefined;\n }\n\n if (parser === undefined) {\n return value;\n }\n\n return parser(value);\n }\n\n private static toScalarString(value: ResolvedFilterValue): string {\n if (Array.isArray(value)) {\n return value.join(',');\n }\n return String(value);\n }\n\n /**\n * Apply all configured resolvers against query params.\n */\n apply(params: TangoQueryParams): FilterInput<T>[] {\n const filters: FilterInput<T>[] = [];\n const keys = new Set<string>();\n\n for (const [key] of params.entries()) {\n keys.add(key);\n }\n\n for (const key of keys) {\n const resolver = this.spec[key] ?? (this.allowAllParams ? this.buildAllResolver(key) : undefined);\n if (!resolver) continue;\n\n const rawValue = params.getAll(key);\n const value = rawValue.length > 1 ? rawValue : rawValue[0];\n\n if (value === undefined) continue;\n\n const filter = this.resolveFilter(resolver, value);\n if (filter) {\n filters.push(filter);\n }\n }\n\n return filters;\n }\n\n private buildAllResolver(param: string): FilterResolver<T> | undefined {\n const [rawField, ...rawLookupParts] = param.split('__');\n if (!rawField) {\n return undefined;\n }\n\n const field = rawField as keyof T;\n if (rawLookupParts.length === 0) {\n return { type: InternalFilterType.SCALAR, column: field };\n }\n\n const lookup = rawLookupParts.join('__') as FilterLookup;\n return FilterSet.createLookupResolver(field, lookup);\n }\n\n private resolveFilter(\n resolver: FilterResolver<T>,\n value: string | string[] | undefined\n ): FilterInput<T> | undefined {\n if (value === undefined) return undefined;\n\n switch (resolver.type) {\n case InternalFilterType.SCALAR:\n return { [resolver.column]: value } as FilterInput<T>;\n\n case InternalFilterType.ILIKE: {\n const pattern = `%${FilterSet.toScalarString(value)}%`;\n const filter: Partial<Record<FilterKey<T>, FilterValue>> = {};\n resolver.columns.forEach((col) => {\n filter[`${String(col)}__icontains` as FilterKey<T>] = pattern;\n });\n return filter;\n }\n\n case InternalFilterType.RANGE: {\n const lookupKey = `${String(resolver.column)}__${resolver.op}` as keyof FilterInput<T>;\n return { [lookupKey]: value } as FilterInput<T>;\n }\n\n case InternalFilterType.IN: {\n const arr = Array.isArray(value) ? value : String(value).split(',');\n const lookupKey = `${String(resolver.column)}__in` as keyof FilterInput<T>;\n return { [lookupKey]: arr } as FilterInput<T>;\n }\n\n case InternalFilterType.CUSTOM:\n return resolver.apply(value);\n\n default:\n return undefined;\n }\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport type { FilterType } from './FilterType';\nexport type { RangeOperator } from './RangeOperator';\nexport type {\n AliasFilterDeclaration,\n FieldFilterDeclaration,\n FilterLookup,\n FilterResolver,\n FilterSetDefineConfig,\n FilterValueParser,\n} from './FilterSet';\nexport { FilterSet } from './FilterSet';\n","import { z } from 'zod';\n\nexport type CursorPaginationInputValue = {\n limit?: number;\n cursor: string | null;\n ordering?: string;\n};\n\nexport const CursorPaginationInput: z.ZodType<CursorPaginationInputValue> = z.object({\n limit: z.preprocess(\n (value) => {\n if (value === undefined || value === null || value === '') {\n return undefined;\n }\n const parsed = Number.parseInt(String(value), 10);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return undefined;\n }\n return parsed;\n },\n z\n .number()\n .int()\n .min(1)\n .transform((value) => Math.min(value, 100))\n .optional()\n ),\n cursor: z.string().nullable().default(null),\n ordering: z.string().optional(),\n});\n","import { TangoQueryParams } from '@danceroutine/tango-core';\nimport type { FilterInput, QuerySet } from '@danceroutine/tango-orm';\nimport type { Paginator, Page } from '../pagination/Paginator';\nimport type { CursorPaginatedResponse } from '../pagination/PaginatedResponse';\nimport { CursorPaginationInput } from '../pagination/CursorPaginationInput';\n\ntype CursorDirection = 'asc' | 'desc';\n\ntype CursorPayload = {\n v: 1;\n field: string;\n dir: CursorDirection;\n value: unknown;\n};\n\n/**\n * Represents a single cursor page of results.\n * Cursor pages do not expose numeric page navigation like offset pagination.\n */\nclass CursorPage<T> implements Page<T> {\n static readonly BRAND = 'tango.resources.cursor_page' as const;\n readonly __tangoBrand: typeof CursorPage.BRAND = CursorPage.BRAND;\n\n constructor(\n public readonly results: T[],\n public readonly nextCursor: string | null,\n public readonly previousCursor: string | null\n ) {}\n\n static isCursorPage<T>(value: unknown): value is CursorPage<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === CursorPage.BRAND\n );\n }\n\n /** Whether a next cursor token exists. */\n hasNext(): boolean {\n return this.nextCursor !== null;\n }\n\n /** Whether a previous cursor token exists. */\n hasPrevious(): boolean {\n return this.previousCursor !== null;\n }\n\n nextPageNumber(): number | null {\n return null;\n }\n\n previousPageNumber(): number | null {\n return null;\n }\n\n startIndex(): number {\n return 0;\n }\n\n endIndex(): number {\n return this.results.length;\n }\n}\n\n/**\n * Cursor-based paginator for stable forward navigation with opaque cursor tokens.\n * It supports `limit`, `cursor`, and `ordering` query params and returns DRF-style\n * paginated envelopes with cursor links.\n */\nexport class CursorPaginator<T extends Record<string, unknown>> implements Paginator<T, T, CursorPaginatedResponse<T>> {\n static readonly BRAND = 'tango.resources.cursor_paginator' as const;\n readonly __tangoBrand: typeof CursorPaginator.BRAND = CursorPaginator.BRAND;\n private limit: number;\n private cursor: string | null = null;\n private direction: CursorDirection = 'asc';\n private nextCursor: string | null = null;\n private previousCursor: string | null = null;\n\n constructor(\n private queryset: QuerySet<T>,\n private perPage: number = 25,\n private cursorField: keyof T = 'id' as keyof T\n ) {\n this.limit = perPage;\n }\n\n /**\n * Narrow an unknown value to `CursorPaginator`.\n */\n static isCursorPaginator<T extends Record<string, unknown>>(value: unknown): value is CursorPaginator<T> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === CursorPaginator.BRAND\n );\n }\n\n /**\n * Parse cursor pagination parameters from Tango query params.\n */\n parse(params: TangoQueryParams): void {\n const parsed = CursorPaginationInput.parse({\n limit: params.get('limit') ?? undefined,\n cursor: params.get('cursor'),\n ordering: params.get('ordering') ?? undefined,\n });\n\n this.limit = parsed.limit ?? this.perPage;\n this.cursor = parsed.cursor;\n\n const ordering = parsed.ordering;\n if (ordering) {\n const parsedDirection: CursorDirection = ordering.startsWith('-') ? 'desc' : 'asc';\n const parsedField = ordering.startsWith('-') ? ordering.slice(1) : ordering;\n this.direction = parsedField === String(this.cursorField) ? parsedDirection : 'asc';\n } else {\n this.direction = 'asc';\n }\n }\n\n /**\n * Parse params and return compatibility `{ limit, offset }` shape.\n */\n parseParams(params: TangoQueryParams): { limit: number; offset: number } {\n this.parse(params);\n return { limit: this.limit, offset: 0 };\n }\n\n /**\n * Build a paginated response payload with cursor links.\n */\n needsTotalCount(): boolean {\n return false;\n }\n\n toResponse<TResult>(results: TResult[]): CursorPaginatedResponse<TResult> {\n const response: CursorPaginatedResponse<TResult> = { results };\n if (this.nextCursor) {\n response.next = this.buildPageLink(this.nextCursor);\n }\n if (this.previousCursor) {\n response.previous = this.buildPageLink(this.previousCursor);\n }\n return response;\n }\n\n /**\n * Backward-compatible alias for `toResponse`.\n */\n getPaginatedResponse<TResult>(results: TResult[], _totalCount?: number): CursorPaginatedResponse<TResult> {\n return this.toResponse(results);\n }\n\n /**\n * Apply cursor constraints and ordering to a queryset.\n */\n apply(queryset: QuerySet<T>): QuerySet<T> {\n let qs = queryset.limit(this.limit + 1);\n if (this.cursor) {\n const decoded = this.decodeCursor(this.cursor);\n if (decoded.field !== String(this.cursorField)) {\n throw new Error('Invalid cursor: field mismatch');\n }\n const lookup = this.direction === 'asc' ? '__gt' : '__lt';\n const fieldLookup = `${String(this.cursorField)}${lookup}`;\n const filterInput = { [fieldLookup]: decoded.value } as FilterInput<T>;\n qs = qs.filter(filterInput);\n }\n const orderToken = this.direction === 'asc' ? String(this.cursorField) : `-${String(this.cursorField)}`;\n return qs.orderBy(orderToken as keyof T | `-${string}`);\n }\n\n /**\n * Fetch the next cursor page.\n */\n async paginate(cursor?: string): Promise<Page<T>> {\n const appliedCursor = cursor ?? this.cursor;\n this.cursor = appliedCursor;\n const fetched = await this.apply(this.queryset).fetch();\n const results = [...fetched.results];\n const hasMore = results.length > this.limit;\n\n if (hasMore) {\n results.pop();\n }\n\n this.previousCursor = appliedCursor ?? null;\n const last = results.at(-1);\n this.nextCursor = hasMore && last ? this.encodeCursor(last) : null;\n\n return new CursorPage(results, this.nextCursor, this.previousCursor);\n }\n\n /**\n * Cursor paginators only support page `1` as an entry point.\n */\n async getPage(page: number): Promise<Page<T>> {\n if (page !== 1) {\n throw new Error('CursorPaginator only supports getPage(1). Use cursor pagination for subsequent pages.');\n }\n return this.paginate();\n }\n\n private buildPageLink(cursor: string): string {\n const orderingToken = this.direction === 'asc' ? String(this.cursorField) : `-${String(this.cursorField)}`;\n return `?limit=${this.limit}&cursor=${encodeURIComponent(cursor)}&ordering=${encodeURIComponent(orderingToken)}`;\n }\n\n private encodeCursor(item: T): string {\n const payload: CursorPayload = {\n v: 1,\n field: String(this.cursorField),\n dir: this.direction,\n value: item[this.cursorField],\n };\n return Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64');\n }\n\n private decodeCursor(cursor: string): CursorPayload {\n let parsed: unknown;\n try {\n parsed = JSON.parse(Buffer.from(cursor, 'base64').toString('utf-8'));\n } catch {\n throw new Error('Invalid cursor: malformed token');\n }\n\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n (parsed as { v?: unknown }).v !== 1 ||\n typeof (parsed as { field?: unknown }).field !== 'string' ||\n ((parsed as { dir?: unknown }).dir !== 'asc' && (parsed as { dir?: unknown }).dir !== 'desc') ||\n !('value' in parsed)\n ) {\n throw new Error('Invalid cursor: unsupported payload');\n }\n\n return parsed as CursorPayload;\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { OffsetPaginator } from '../paginators/OffsetPaginator';\nexport { CursorPaginator } from '../paginators/CursorPaginator';\nexport { OffsetPaginationInput } from './OffsetPaginationInput';\nexport type { OffsetPaginationInputValue } from './OffsetPaginationInput';\nexport { CursorPaginationInput } from './CursorPaginationInput';\nexport type { CursorPaginationInputValue } from './CursorPaginationInput';\nexport type { Paginator, Page } from './Paginator';\nexport type {\n BasePaginatedResponse,\n CursorPaginatedResponse,\n OffsetPaginatedResponse,\n PaginatedResponse,\n} from './PaginatedResponse';\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { CursorPaginator } from './CursorPaginator';\nexport { OffsetPaginator } from './OffsetPaginator';\n","/**\n * Domain boundary barrel: centralizes shared resource metadata contracts.\n */\n\nexport type { ResourceModelFieldMetadata, ResourceModelLike, ResourceModelMetadata } from './ResourceModelLike';\nexport type { GenericAPIViewOpenAPIDescription, ModelViewSetOpenAPIDescription } from './OpenAPIDescription';\n","import { z } from 'zod';\n\nexport type SerializerSchema = z.ZodTypeAny;\n\nexport type SerializerClass<\n TCreateSchema extends SerializerSchema = SerializerSchema,\n TUpdateSchema extends SerializerSchema = SerializerSchema,\n TOutputSchema extends SerializerSchema = SerializerSchema,\n> = {\n new (): Serializer<TCreateSchema, TUpdateSchema, TOutputSchema>;\n readonly createSchema: TCreateSchema;\n readonly updateSchema: TUpdateSchema;\n readonly outputSchema: TOutputSchema;\n};\n\nexport type AnySerializerClass = SerializerClass<SerializerSchema, SerializerSchema, SerializerSchema>;\n\nexport type SerializerCreateInput<TSerializer extends AnySerializerClass> = z.output<TSerializer['createSchema']>;\nexport type SerializerUpdateInput<TSerializer extends AnySerializerClass> = z.output<TSerializer['updateSchema']>;\nexport type SerializerOutput<TSerializer extends AnySerializerClass> = z.output<TSerializer['outputSchema']>;\n\n/**\n * DRF-inspired base serializer backed by Zod schemas.\n *\n * Tango serializers keep Zod as the source of truth for validation and type\n * inference while centralizing create, update, and representation workflows in\n * one class-owned contract.\n */\nexport abstract class Serializer<\n TCreateSchema extends SerializerSchema,\n TUpdateSchema extends SerializerSchema,\n TOutputSchema extends SerializerSchema,\n> {\n static readonly createSchema: SerializerSchema = z.unknown();\n static readonly updateSchema: SerializerSchema = z.unknown();\n static readonly outputSchema: SerializerSchema = z.unknown();\n\n /**\n * Return the serializer class for the current instance.\n */\n getSerializerClass(): SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema> {\n return this.constructor as SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema>;\n }\n\n /**\n * Return the Zod schema used for create payloads.\n */\n getCreateSchema(): TCreateSchema {\n return this.getSerializerClass().createSchema;\n }\n\n /**\n * Return the Zod schema used for update payloads.\n */\n getUpdateSchema(): TUpdateSchema {\n return this.getSerializerClass().updateSchema;\n }\n\n /**\n * Return the Zod schema used for serialized output.\n */\n getOutputSchema(): TOutputSchema {\n return this.getSerializerClass().outputSchema;\n }\n\n /**\n * Validate unknown input for create workflows.\n */\n deserializeCreate(input: unknown): z.output<TCreateSchema> {\n return this.getCreateSchema().parse(input);\n }\n\n /**\n * Validate unknown input for update workflows.\n */\n deserializeUpdate(input: unknown): z.output<TUpdateSchema> {\n return this.getUpdateSchema().parse(input);\n }\n\n /**\n * Convert a persisted record into its outward-facing representation.\n */\n toRepresentation(record: unknown): z.output<TOutputSchema> {\n return this.getOutputSchema().parse(record);\n }\n}\n","import type { ManagerLike } from '@danceroutine/tango-orm';\nimport type { ResourceModelLike } from '../resource/ResourceModelLike';\nimport {\n Serializer,\n type AnySerializerClass,\n type SerializerClass,\n type SerializerCreateInput,\n type SerializerOutput,\n type SerializerSchema,\n type SerializerUpdateInput,\n} from './Serializer';\n\nexport type ModelSerializerClass<\n TModel extends Record<string, unknown> = Record<string, unknown>,\n TCreateSchema extends SerializerSchema = SerializerSchema,\n TUpdateSchema extends SerializerSchema = SerializerSchema,\n TOutputSchema extends SerializerSchema = SerializerSchema,\n> = SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema> & {\n new (): ModelSerializer<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>;\n readonly model?: ResourceModelLike<TModel>;\n};\n\nexport type AnyModelSerializerClass = ModelSerializerClass<\n Record<string, unknown>,\n SerializerSchema,\n SerializerSchema,\n SerializerSchema\n>;\n\n/**\n * Zod-backed serializer with default model-manager persistence behavior.\n */\nexport abstract class ModelSerializer<\n TModel extends Record<string, unknown>,\n TCreateSchema extends SerializerSchema,\n TUpdateSchema extends SerializerSchema,\n TOutputSchema extends SerializerSchema,\n> extends Serializer<TCreateSchema, TUpdateSchema, TOutputSchema> {\n static readonly model?: unknown;\n\n /**\n * Return the Tango model backing this serializer.\n */\n getModel(): ResourceModelLike<TModel> {\n const model = (\n this.constructor as Partial<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>\n ).model;\n\n if (!model) {\n throw new Error(`${this.constructor.name} must define a static model or override getModel().`);\n }\n\n return model;\n }\n\n /**\n * Return the manager used for create and update workflows.\n */\n getManager(): ManagerLike<TModel> {\n return this.getModel().objects;\n }\n\n /**\n * Validate, enrich, persist, and serialize a create workflow.\n */\n async create(\n input: unknown\n ): Promise<SerializerOutput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>> {\n const validated = this.deserializeCreate(input);\n const prepared = await this.beforeCreate(validated);\n const created = await this.getManager().create(prepared);\n return this.toRepresentation(created);\n }\n\n /**\n * Validate, enrich, persist, and serialize an update workflow.\n */\n async update(\n id: TModel[keyof TModel],\n input: unknown\n ): Promise<SerializerOutput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>> {\n const validated = this.deserializeUpdate(input);\n const prepared = await this.beforeUpdate(id, validated);\n const updated = await this.getManager().update(id, prepared);\n return this.toRepresentation(updated);\n }\n\n /**\n * Override to normalize create input for this resource workflow before the\n * manager call.\n *\n * Model-owned persistence rules belong in model hooks so they also run for\n * scripts and direct manager usage.\n */\n protected async beforeCreate(\n data: SerializerCreateInput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>\n ): Promise<Partial<TModel>> {\n return data as Partial<TModel>;\n }\n\n /**\n * Override to normalize update input for this resource workflow before the\n * manager call.\n *\n * Model-owned persistence rules belong in model hooks so they also run for\n * scripts and direct manager usage.\n */\n protected async beforeUpdate(\n _id: TModel[keyof TModel],\n data: SerializerUpdateInput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>\n ): Promise<Partial<TModel>> {\n return data as Partial<TModel>;\n }\n}\n\nexport type { AnySerializerClass };\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport {\n Serializer,\n type SerializerClass,\n type AnySerializerClass,\n type SerializerCreateInput,\n type SerializerUpdateInput,\n type SerializerOutput,\n type SerializerSchema,\n} from './Serializer';\nexport { ModelSerializer, type ModelSerializerClass, type AnyModelSerializerClass } from './ModelSerializer';\n","import type { FilterInput, ManagerLike, QuerySet } from '@danceroutine/tango-orm';\nimport { HttpErrorFactory, TangoResponse, type JsonValue, NotFoundError } from '@danceroutine/tango-core';\nimport type { RequestContext } from '../context/index';\nimport type { FilterSet } from '../filters/index';\nimport { OffsetPaginator } from '../paginators/OffsetPaginator';\nimport { Q } from '@danceroutine/tango-orm';\nimport type { Paginator } from '../pagination/index';\nimport type { ModelViewSetOpenAPIDescription } from '../resource/index';\nimport type { ResourceModelLike } from '../resource/index';\nimport type {\n AnyModelSerializerClass,\n ModelSerializerClass,\n SerializerOutput,\n SerializerSchema,\n} from '../serializer/index';\n\nexport type ViewSetActionScope = 'detail' | 'collection';\nexport type ViewSetActionMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\nexport interface ViewSetActionDescriptor {\n name: string;\n scope: ViewSetActionScope;\n methods: readonly ViewSetActionMethod[];\n path?: string;\n}\n\nexport interface ResolvedViewSetActionDescriptor extends ViewSetActionDescriptor {\n path: string;\n}\n\ntype AnyModelViewSet = ModelViewSet<Record<string, unknown>, AnyModelSerializerClass>;\n\n/**\n * Configuration for a ModelViewSet, defining how a serializer-backed model is exposed as an API resource.\n */\nexport interface ModelViewSetConfig<\n TModel extends Record<string, unknown>,\n TSerializer extends ModelSerializerClass<TModel, SerializerSchema, SerializerSchema, SerializerSchema>,\n> {\n /** Serializer class that owns validation, representation, and persistence hooks */\n serializer: TSerializer;\n\n /** Optional filter set defining which query parameters can filter the list endpoint */\n filters?: FilterSet<TModel>;\n\n /** Fields that clients are allowed to sort by via query parameters */\n orderingFields?: (keyof TModel)[];\n\n /** Fields that are searched when a free-text search query parameter is provided */\n searchFields?: (keyof TModel)[];\n\n /** Optional paginator factory used by list endpoints. */\n paginatorFactory?: (queryset: QuerySet<TModel>) => Paginator<TModel, SerializerOutput<TSerializer>>;\n}\n\n/**\n * Base class for creating RESTful API viewsets with built-in CRUD operations.\n * Provides list, retrieve, create, update, and delete methods with filtering,\n * search, pagination, and ordering support.\n */\nexport abstract class ModelViewSet<\n TModel extends Record<string, unknown>,\n TSerializer extends ModelSerializerClass<TModel, SerializerSchema, SerializerSchema, SerializerSchema>,\n> {\n static readonly BRAND = 'tango.resources.model_view_set' as const;\n static readonly actions: readonly ViewSetActionDescriptor[] = [];\n readonly __tangoBrand: typeof ModelViewSet.BRAND = ModelViewSet.BRAND;\n protected readonly serializerClass: TSerializer;\n protected readonly filters?: FilterSet<TModel>;\n protected readonly orderingFields: (keyof TModel)[];\n protected readonly searchFields: (keyof TModel)[];\n protected readonly paginatorFactory?: (\n queryset: QuerySet<TModel>\n ) => Paginator<TModel, SerializerOutput<TSerializer>>;\n private serializer?: InstanceType<TSerializer>;\n\n constructor(config: ModelViewSetConfig<TModel, TSerializer>) {\n this.serializerClass = config.serializer;\n this.filters = config.filters;\n this.orderingFields = config.orderingFields ?? [];\n this.searchFields = config.searchFields ?? [];\n this.paginatorFactory = config.paginatorFactory;\n }\n\n /**\n * Return the custom action descriptors declared by a viewset or constructor.\n */\n static getActions(\n viewsetOrConstructor: AnyModelViewSet | (new (...args: never[]) => AnyModelViewSet)\n ): readonly ResolvedViewSetActionDescriptor[] {\n const viewset = ModelViewSet.isModelViewSet(viewsetOrConstructor) ? viewsetOrConstructor : null;\n\n const constructorValue = viewset\n ? (viewset.constructor as { actions?: readonly ViewSetActionDescriptor[] })\n : (viewsetOrConstructor as { actions?: readonly ViewSetActionDescriptor[] });\n const actions = Array.isArray(constructorValue.actions) ? constructorValue.actions : [];\n\n return actions.map((action) => ({\n ...action,\n path: viewset\n ? viewset.resolveActionPath(action)\n : ModelViewSet.resolvePathFromDescriptor(action.name, action.path),\n }));\n }\n\n /**\n * Narrow an unknown value to `ModelViewSet`.\n */\n static isModelViewSet(value: unknown): value is ModelViewSet<Record<string, unknown>, AnyModelSerializerClass> {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === ModelViewSet.BRAND\n );\n }\n\n /**\n * Preserve literal action inference while validating the descriptor shape.\n */\n static defineViewSetActions<const T extends readonly ViewSetActionDescriptor[]>(actions: T): T {\n return actions;\n }\n\n private static resolvePathFromDescriptor(name: string, explicitPath?: string): string {\n const normalized = (explicitPath?.trim() || ModelViewSet.toKebabCase(name)).replace(/^\\/+|\\/+$/g, '');\n if (!normalized) {\n throw new Error(`Invalid custom action path for '${name}'.`);\n }\n return normalized;\n }\n\n private static toKebabCase(input: string): string {\n return input\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/[_\\s]+/g, '-')\n .toLowerCase();\n }\n\n /**\n * Return the serializer class that owns this resource contract.\n */\n getSerializerClass(): TSerializer {\n return this.serializerClass;\n }\n\n /**\n * Return the serializer instance for the current resource.\n */\n getSerializer(): InstanceType<TSerializer> {\n if (!this.serializer) {\n this.serializer = new this.serializerClass() as InstanceType<TSerializer>;\n }\n\n return this.serializer;\n }\n\n /**\n * Describe the public HTTP contract that this resource contributes to OpenAPI generation.\n */\n describeOpenAPI(): ModelViewSetOpenAPIDescription<TModel, TSerializer> {\n return {\n model: this.requireModelMetadata(),\n outputSchema: this.getSerializer().getOutputSchema() as TSerializer['outputSchema'],\n createSchema: this.getSerializer().getCreateSchema() as TSerializer['createSchema'],\n updateSchema: this.getSerializer().getUpdateSchema() as TSerializer['updateSchema'],\n searchFields: this.searchFields,\n orderingFields: this.orderingFields,\n lookupField: this.getManager().meta.pk as keyof TModel,\n lookupParam: 'id',\n allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],\n usesDefaultOffsetPagination: !this.paginatorFactory,\n actions: ModelViewSet.getActions(this as unknown as AnyModelViewSet),\n };\n }\n\n /**\n * List endpoint with filtering, search, ordering, and offset pagination.\n */\n async list(ctx: RequestContext): Promise<TangoResponse> {\n try {\n const params = ctx.request.queryParams;\n const baseQueryset = this.getManager().query();\n const paginator = this.getPaginator(baseQueryset);\n paginator.parse(params);\n\n let qs = baseQueryset;\n\n if (this.filters) {\n const filterInputs = this.filters.apply(params);\n if (filterInputs.length > 0) {\n qs = qs.filter(Q.and(...filterInputs));\n }\n }\n\n const search = params.getSearch();\n if (search && this.searchFields.length > 0) {\n const searchFilters: FilterInput<TModel>[] = this.searchFields.map((field) => {\n const lookup = `${String(field)}__icontains`;\n return { [lookup]: search } as FilterInput<TModel>;\n });\n qs = qs.filter(Q.or(...searchFilters));\n }\n\n const ordering = params.getOrdering();\n if (ordering.length > 0) {\n const orderTokens = ordering.filter((field) => {\n const cleanField = field.startsWith('-') ? field.slice(1) : field;\n return this.orderingFields.includes(cleanField as keyof TModel);\n });\n if (orderTokens.length > 0) {\n qs = qs.orderBy(...orderTokens.map((token) => token as keyof TModel | `-${string & keyof TModel}`));\n }\n }\n\n qs = paginator.apply(qs);\n const resultPromise = qs.fetch();\n const totalCountPromise = paginator.needsTotalCount()\n ? qs.count()\n : Promise.resolve<number | undefined>(undefined);\n const [result, totalCount] = await Promise.all([resultPromise, totalCountPromise]);\n const serializer = this.getSerializer();\n const response = paginator.toResponse(\n result.results.map((row) => serializer.toRepresentation(row)) as SerializerOutput<TSerializer>[],\n { totalCount }\n );\n\n return TangoResponse.json(response as unknown as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n /**\n * Retrieve endpoint for a single resource by id.\n */\n async retrieve(_ctx: RequestContext, id: string): Promise<TangoResponse> {\n try {\n const manager = this.getManager();\n const pk = manager.meta.pk;\n const filterById = { [pk]: id } as FilterInput<TModel>;\n const result = await manager.query().filter(filterById).fetchOne();\n\n if (!result) {\n throw new NotFoundError(`No ${manager.meta.table} record found for ${String(pk)}=${id}.`);\n }\n\n return TangoResponse.json(this.getSerializer().toRepresentation(result) as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n /**\n * Create endpoint: validate input, persist, and return serialized output.\n */\n async create(ctx: RequestContext): Promise<TangoResponse> {\n try {\n const body = await ctx.request.json();\n const result = await this.getSerializer().create(body);\n\n return TangoResponse.created(undefined, result as JsonValue);\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n /**\n * Update endpoint: validate partial payload and persist by id.\n */\n async update(ctx: RequestContext, id: string): Promise<TangoResponse> {\n try {\n const body = await ctx.request.json();\n const pkValue = id as TModel[keyof TModel];\n const result = await this.getSerializer().update(pkValue, body);\n\n return TangoResponse.json(result as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n /**\n * Destroy endpoint: delete a resource by id.\n */\n async destroy(_ctx: RequestContext, id: string): Promise<TangoResponse> {\n try {\n const pkValue = id as TModel[keyof TModel];\n\n await this.getManager().delete(pkValue);\n\n return TangoResponse.noContent();\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n protected getPaginator(queryset: QuerySet<TModel>): Paginator<TModel, SerializerOutput<TSerializer>> {\n if (this.paginatorFactory) {\n return this.paginatorFactory(queryset);\n }\n return new OffsetPaginator<TModel>(queryset) as Paginator<TModel, SerializerOutput<TSerializer>>;\n }\n\n protected getManager(): ManagerLike<TModel> {\n return this.getSerializer().getManager();\n }\n\n /**\n * Convert thrown errors into normalized HTTP responses.\n */\n protected handleError(error: unknown): TangoResponse {\n const httpError = HttpErrorFactory.toHttpError(error);\n return TangoResponse.json(httpError.body as JsonValue, { status: httpError.status });\n }\n\n /**\n * Resolve route path segment(s) for a custom action.\n * Override this in subclasses to customize path derivation globally.\n */\n protected resolveActionPath(action: ViewSetActionDescriptor): string {\n return ModelViewSet.resolvePathFromDescriptor(action.name, action.path);\n }\n\n private requireModelMetadata(): ResourceModelLike<TModel> & {\n metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;\n } {\n const model = this.getSerializer().getModel();\n\n if (!model.metadata) {\n throw new Error('OpenAPI generation requires Tango model metadata on ModelViewSet models.');\n }\n\n return model as ResourceModelLike<TModel> & {\n metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;\n };\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport {\n ModelViewSet,\n type ModelViewSetConfig,\n type ViewSetActionDescriptor,\n type ViewSetActionMethod,\n type ViewSetActionScope,\n type ResolvedViewSetActionDescriptor,\n} from './ModelViewSet';\nexport type { ModelViewSetOpenAPIDescription } from '../resource/index';\n"],"mappings":"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"}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface BasePaginatedResponse<T> {
|
|
2
2
|
results: T[];
|
|
3
|
-
count?: number;
|
|
4
3
|
next?: string | null;
|
|
5
4
|
previous?: string | null;
|
|
6
5
|
}
|
|
6
|
+
export interface OffsetPaginatedResponse<T> extends BasePaginatedResponse<T> {
|
|
7
|
+
count?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface CursorPaginatedResponse<T> extends BasePaginatedResponse<T> {
|
|
10
|
+
count?: never;
|
|
11
|
+
}
|
|
12
|
+
export type PaginatedResponse<T> = OffsetPaginatedResponse<T> | CursorPaginatedResponse<T>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TangoQueryParams } from '@danceroutine/tango-core';
|
|
1
2
|
import type { PaginatedResponse } from './PaginatedResponse';
|
|
2
3
|
import type { QuerySet } from '@danceroutine/tango-orm';
|
|
3
4
|
export interface Page<T> {
|
|
@@ -9,10 +10,11 @@ export interface Page<T> {
|
|
|
9
10
|
startIndex(): number;
|
|
10
11
|
endIndex(): number;
|
|
11
12
|
}
|
|
12
|
-
export interface Paginator<TModel extends Record<string, unknown>, TResult = TModel> {
|
|
13
|
-
parse(params:
|
|
13
|
+
export interface Paginator<TModel extends Record<string, unknown>, TResult = TModel, TResponse extends PaginatedResponse<TResult> = PaginatedResponse<TResult>> {
|
|
14
|
+
parse(params: TangoQueryParams): void;
|
|
14
15
|
apply(queryset: QuerySet<TModel>): QuerySet<TModel>;
|
|
16
|
+
needsTotalCount(): boolean;
|
|
15
17
|
toResponse(results: TResult[], context?: {
|
|
16
18
|
totalCount?: number;
|
|
17
|
-
}):
|
|
19
|
+
}): TResponse;
|
|
18
20
|
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { OffsetPaginator } from '../paginators/OffsetPaginator';
|
|
5
5
|
export { CursorPaginator } from '../paginators/CursorPaginator';
|
|
6
|
-
export {
|
|
7
|
-
export type {
|
|
6
|
+
export { OffsetPaginationInput } from './OffsetPaginationInput';
|
|
7
|
+
export type { OffsetPaginationInputValue } from './OffsetPaginationInput';
|
|
8
|
+
export { CursorPaginationInput } from './CursorPaginationInput';
|
|
9
|
+
export type { CursorPaginationInputValue } from './CursorPaginationInput';
|
|
8
10
|
export type { Paginator, Page } from './Paginator';
|
|
9
|
-
export type { PaginatedResponse } from './PaginatedResponse';
|
|
11
|
+
export type { BasePaginatedResponse, CursorPaginatedResponse, OffsetPaginatedResponse, PaginatedResponse, } from './PaginatedResponse';
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { TangoQueryParams } from '@danceroutine/tango-core';
|
|
1
2
|
import type { QuerySet } from '@danceroutine/tango-orm';
|
|
2
3
|
import type { Paginator, Page } from '../pagination/Paginator';
|
|
3
|
-
import type {
|
|
4
|
+
import type { CursorPaginatedResponse } from '../pagination/PaginatedResponse';
|
|
4
5
|
/**
|
|
5
6
|
* Cursor-based paginator for stable forward navigation with opaque cursor tokens.
|
|
6
7
|
* It supports `limit`, `cursor`, and `ordering` query params and returns DRF-style
|
|
7
8
|
* paginated envelopes with cursor links.
|
|
8
9
|
*/
|
|
9
|
-
export declare class CursorPaginator<T extends Record<string, unknown>> implements Paginator<T
|
|
10
|
+
export declare class CursorPaginator<T extends Record<string, unknown>> implements Paginator<T, T, CursorPaginatedResponse<T>> {
|
|
10
11
|
private queryset;
|
|
11
12
|
private perPage;
|
|
12
13
|
private cursorField;
|
|
@@ -18,16 +19,41 @@ export declare class CursorPaginator<T extends Record<string, unknown>> implemen
|
|
|
18
19
|
private nextCursor;
|
|
19
20
|
private previousCursor;
|
|
20
21
|
constructor(queryset: QuerySet<T>, perPage?: number, cursorField?: keyof T);
|
|
22
|
+
/**
|
|
23
|
+
* Narrow an unknown value to `CursorPaginator`.
|
|
24
|
+
*/
|
|
21
25
|
static isCursorPaginator<T extends Record<string, unknown>>(value: unknown): value is CursorPaginator<T>;
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Parse cursor pagination parameters from Tango query params.
|
|
28
|
+
*/
|
|
29
|
+
parse(params: TangoQueryParams): void;
|
|
30
|
+
/**
|
|
31
|
+
* Parse params and return compatibility `{ limit, offset }` shape.
|
|
32
|
+
*/
|
|
33
|
+
parseParams(params: TangoQueryParams): {
|
|
24
34
|
limit: number;
|
|
25
35
|
offset: number;
|
|
26
36
|
};
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Build a paginated response payload with cursor links.
|
|
39
|
+
*/
|
|
40
|
+
needsTotalCount(): boolean;
|
|
41
|
+
toResponse<TResult>(results: TResult[]): CursorPaginatedResponse<TResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Backward-compatible alias for `toResponse`.
|
|
44
|
+
*/
|
|
45
|
+
getPaginatedResponse<TResult>(results: TResult[], _totalCount?: number): CursorPaginatedResponse<TResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Apply cursor constraints and ordering to a queryset.
|
|
48
|
+
*/
|
|
29
49
|
apply(queryset: QuerySet<T>): QuerySet<T>;
|
|
50
|
+
/**
|
|
51
|
+
* Fetch the next cursor page.
|
|
52
|
+
*/
|
|
30
53
|
paginate(cursor?: string): Promise<Page<T>>;
|
|
54
|
+
/**
|
|
55
|
+
* Cursor paginators only support page `1` as an entry point.
|
|
56
|
+
*/
|
|
31
57
|
getPage(page: number): Promise<Page<T>>;
|
|
32
58
|
private buildPageLink;
|
|
33
59
|
private encodeCursor;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { TangoQueryParams } from '@danceroutine/tango-core';
|
|
1
2
|
import type { QuerySet } from '@danceroutine/tango-orm';
|
|
2
3
|
import type { Paginator, Page } from '../pagination/Paginator';
|
|
3
|
-
import type {
|
|
4
|
+
import type { OffsetPaginatedResponse } from '../pagination/PaginatedResponse';
|
|
4
5
|
/**
|
|
5
6
|
* Offset/limit paginator modelled after DRF's LimitOffsetPagination.
|
|
6
7
|
* Handles parsing limit/offset/page from URL query params and building
|
|
@@ -14,7 +15,7 @@ import type { PaginatedResponse } from '../pagination/PaginatedResponse';
|
|
|
14
15
|
* const response = paginator.getPaginatedResponse(results, totalCount);
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
|
-
export declare class OffsetPaginator<T extends Record<string, unknown>> implements Paginator<T
|
|
18
|
+
export declare class OffsetPaginator<T extends Record<string, unknown>> implements Paginator<T, T, OffsetPaginatedResponse<T>> {
|
|
18
19
|
private queryset;
|
|
19
20
|
private perPage;
|
|
20
21
|
static readonly BRAND: "tango.resources.offset_paginator";
|
|
@@ -22,14 +23,20 @@ export declare class OffsetPaginator<T extends Record<string, unknown>> implemen
|
|
|
22
23
|
private limit;
|
|
23
24
|
private offset;
|
|
24
25
|
constructor(queryset: QuerySet<T>, perPage?: number);
|
|
26
|
+
/**
|
|
27
|
+
* Narrow an unknown value to `OffsetPaginator`.
|
|
28
|
+
*/
|
|
25
29
|
static isOffsetPaginator<T extends Record<string, unknown>>(value: unknown): value is OffsetPaginator<T>;
|
|
26
30
|
/**
|
|
27
|
-
* Parse limit, offset, and page from
|
|
31
|
+
* Parse limit, offset, and page from Tango query params.
|
|
28
32
|
* If `page` is provided, it's converted to an offset.
|
|
29
33
|
* Stores parsed values for use by getPaginatedResponse.
|
|
30
34
|
*/
|
|
31
|
-
parse(params:
|
|
32
|
-
|
|
35
|
+
parse(params: TangoQueryParams): void;
|
|
36
|
+
/**
|
|
37
|
+
* Parse params and return `{ limit, offset }` for compatibility callers.
|
|
38
|
+
*/
|
|
39
|
+
parseParams(params: TangoQueryParams): {
|
|
33
40
|
limit: number;
|
|
34
41
|
offset: number;
|
|
35
42
|
};
|
|
@@ -37,12 +44,28 @@ export declare class OffsetPaginator<T extends Record<string, unknown>> implemen
|
|
|
37
44
|
* Build a DRF-style paginated response with count, next, and previous links.
|
|
38
45
|
* Uses the limit/offset stored from the most recent parseParams call.
|
|
39
46
|
*/
|
|
47
|
+
needsTotalCount(): boolean;
|
|
40
48
|
toResponse<TResult>(results: TResult[], context?: {
|
|
41
49
|
totalCount?: number;
|
|
42
|
-
}):
|
|
43
|
-
|
|
50
|
+
}): OffsetPaginatedResponse<TResult>;
|
|
51
|
+
/**
|
|
52
|
+
* Backward-compatible alias for `toResponse`.
|
|
53
|
+
*/
|
|
54
|
+
getPaginatedResponse<TResult>(results: TResult[], totalCount?: number): OffsetPaginatedResponse<TResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Apply current limit/offset to a queryset.
|
|
57
|
+
*/
|
|
44
58
|
apply(queryset: QuerySet<T>): QuerySet<T>;
|
|
59
|
+
/**
|
|
60
|
+
* Fetch a 1-based page number from the bound queryset.
|
|
61
|
+
*/
|
|
45
62
|
paginate(page: number): Promise<Page<T>>;
|
|
63
|
+
/**
|
|
64
|
+
* Fetch a 1-based page and return page metadata.
|
|
65
|
+
*/
|
|
46
66
|
getPage(page: number): Promise<Page<T>>;
|
|
67
|
+
/**
|
|
68
|
+
* Count total rows for the current queryset state.
|
|
69
|
+
*/
|
|
47
70
|
count(): Promise<number>;
|
|
48
71
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { APIViewMethod } from '../view/APIView';
|
|
2
|
+
import type { ResolvedViewSetActionDescriptor } from '../viewset/ModelViewSet';
|
|
3
|
+
import type { ResourceModelLike } from './ResourceModelLike';
|
|
4
|
+
import type { ModelSerializerClass, SerializerSchema } from '../serializer/index';
|
|
5
|
+
export type GenericAPIViewOpenAPIDescription<TModel extends Record<string, unknown>, TSerializer extends ModelSerializerClass<TModel, SerializerSchema, SerializerSchema, SerializerSchema>> = {
|
|
6
|
+
model: ResourceModelLike<TModel> & {
|
|
7
|
+
metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;
|
|
8
|
+
};
|
|
9
|
+
outputSchema: TSerializer['outputSchema'];
|
|
10
|
+
createSchema: TSerializer['createSchema'];
|
|
11
|
+
updateSchema: TSerializer['updateSchema'];
|
|
12
|
+
searchFields: readonly (keyof TModel)[];
|
|
13
|
+
orderingFields: readonly (keyof TModel)[];
|
|
14
|
+
lookupField: keyof TModel;
|
|
15
|
+
lookupParam: string;
|
|
16
|
+
allowedMethods: readonly APIViewMethod[];
|
|
17
|
+
usesDefaultOffsetPagination: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type ModelViewSetOpenAPIDescription<TModel extends Record<string, unknown>, TSerializer extends ModelSerializerClass<TModel, SerializerSchema, SerializerSchema, SerializerSchema>> = GenericAPIViewOpenAPIDescription<TModel, TSerializer> & {
|
|
20
|
+
actions: readonly ResolvedViewSetActionDescriptor[];
|
|
21
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ManagerLike } from '@danceroutine/tango-orm';
|
|
2
|
+
export type ResourceModelFieldMetadata = {
|
|
3
|
+
name: string;
|
|
4
|
+
type: string;
|
|
5
|
+
notNull?: boolean;
|
|
6
|
+
default?: unknown;
|
|
7
|
+
primaryKey?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type ResourceModelMetadata = {
|
|
10
|
+
name: string;
|
|
11
|
+
fields: ResourceModelFieldMetadata[];
|
|
12
|
+
};
|
|
13
|
+
export type ResourceModelLike<TModel extends Record<string, unknown>> = {
|
|
14
|
+
readonly objects: ManagerLike<TModel>;
|
|
15
|
+
readonly metadata?: ResourceModelMetadata;
|
|
16
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain boundary barrel: centralizes shared resource metadata contracts.
|
|
3
|
+
*/
|
|
4
|
+
export type { ResourceModelFieldMetadata, ResourceModelLike, ResourceModelMetadata } from './ResourceModelLike';
|
|
5
|
+
export type { GenericAPIViewOpenAPIDescription, ModelViewSetOpenAPIDescription } from './OpenAPIDescription';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ManagerLike } from '@danceroutine/tango-orm';
|
|
2
|
+
import type { ResourceModelLike } from '../resource/ResourceModelLike';
|
|
3
|
+
import { Serializer, type AnySerializerClass, type SerializerClass, type SerializerCreateInput, type SerializerOutput, type SerializerSchema, type SerializerUpdateInput } from './Serializer';
|
|
4
|
+
export type ModelSerializerClass<TModel extends Record<string, unknown> = Record<string, unknown>, TCreateSchema extends SerializerSchema = SerializerSchema, TUpdateSchema extends SerializerSchema = SerializerSchema, TOutputSchema extends SerializerSchema = SerializerSchema> = SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema> & {
|
|
5
|
+
new (): ModelSerializer<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>;
|
|
6
|
+
readonly model?: ResourceModelLike<TModel>;
|
|
7
|
+
};
|
|
8
|
+
export type AnyModelSerializerClass = ModelSerializerClass<Record<string, unknown>, SerializerSchema, SerializerSchema, SerializerSchema>;
|
|
9
|
+
/**
|
|
10
|
+
* Zod-backed serializer with default model-manager persistence behavior.
|
|
11
|
+
*/
|
|
12
|
+
export declare abstract class ModelSerializer<TModel extends Record<string, unknown>, TCreateSchema extends SerializerSchema, TUpdateSchema extends SerializerSchema, TOutputSchema extends SerializerSchema> extends Serializer<TCreateSchema, TUpdateSchema, TOutputSchema> {
|
|
13
|
+
static readonly model?: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Return the Tango model backing this serializer.
|
|
16
|
+
*/
|
|
17
|
+
getModel(): ResourceModelLike<TModel>;
|
|
18
|
+
/**
|
|
19
|
+
* Return the manager used for create and update workflows.
|
|
20
|
+
*/
|
|
21
|
+
getManager(): ManagerLike<TModel>;
|
|
22
|
+
/**
|
|
23
|
+
* Validate, enrich, persist, and serialize a create workflow.
|
|
24
|
+
*/
|
|
25
|
+
create(input: unknown): Promise<SerializerOutput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>>;
|
|
26
|
+
/**
|
|
27
|
+
* Validate, enrich, persist, and serialize an update workflow.
|
|
28
|
+
*/
|
|
29
|
+
update(id: TModel[keyof TModel], input: unknown): Promise<SerializerOutput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>>;
|
|
30
|
+
/**
|
|
31
|
+
* Override to normalize create input for this resource workflow before the
|
|
32
|
+
* manager call.
|
|
33
|
+
*
|
|
34
|
+
* Model-owned persistence rules belong in model hooks so they also run for
|
|
35
|
+
* scripts and direct manager usage.
|
|
36
|
+
*/
|
|
37
|
+
protected beforeCreate(data: SerializerCreateInput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>): Promise<Partial<TModel>>;
|
|
38
|
+
/**
|
|
39
|
+
* Override to normalize update input for this resource workflow before the
|
|
40
|
+
* manager call.
|
|
41
|
+
*
|
|
42
|
+
* Model-owned persistence rules belong in model hooks so they also run for
|
|
43
|
+
* scripts and direct manager usage.
|
|
44
|
+
*/
|
|
45
|
+
protected beforeUpdate(_id: TModel[keyof TModel], data: SerializerUpdateInput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema>>): Promise<Partial<TModel>>;
|
|
46
|
+
}
|
|
47
|
+
export type { AnySerializerClass };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export type SerializerSchema = z.ZodTypeAny;
|
|
3
|
+
export type SerializerClass<TCreateSchema extends SerializerSchema = SerializerSchema, TUpdateSchema extends SerializerSchema = SerializerSchema, TOutputSchema extends SerializerSchema = SerializerSchema> = {
|
|
4
|
+
new (): Serializer<TCreateSchema, TUpdateSchema, TOutputSchema>;
|
|
5
|
+
readonly createSchema: TCreateSchema;
|
|
6
|
+
readonly updateSchema: TUpdateSchema;
|
|
7
|
+
readonly outputSchema: TOutputSchema;
|
|
8
|
+
};
|
|
9
|
+
export type AnySerializerClass = SerializerClass<SerializerSchema, SerializerSchema, SerializerSchema>;
|
|
10
|
+
export type SerializerCreateInput<TSerializer extends AnySerializerClass> = z.output<TSerializer['createSchema']>;
|
|
11
|
+
export type SerializerUpdateInput<TSerializer extends AnySerializerClass> = z.output<TSerializer['updateSchema']>;
|
|
12
|
+
export type SerializerOutput<TSerializer extends AnySerializerClass> = z.output<TSerializer['outputSchema']>;
|
|
13
|
+
/**
|
|
14
|
+
* DRF-inspired base serializer backed by Zod schemas.
|
|
15
|
+
*
|
|
16
|
+
* Tango serializers keep Zod as the source of truth for validation and type
|
|
17
|
+
* inference while centralizing create, update, and representation workflows in
|
|
18
|
+
* one class-owned contract.
|
|
19
|
+
*/
|
|
20
|
+
export declare abstract class Serializer<TCreateSchema extends SerializerSchema, TUpdateSchema extends SerializerSchema, TOutputSchema extends SerializerSchema> {
|
|
21
|
+
static readonly createSchema: SerializerSchema;
|
|
22
|
+
static readonly updateSchema: SerializerSchema;
|
|
23
|
+
static readonly outputSchema: SerializerSchema;
|
|
24
|
+
/**
|
|
25
|
+
* Return the serializer class for the current instance.
|
|
26
|
+
*/
|
|
27
|
+
getSerializerClass(): SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema>;
|
|
28
|
+
/**
|
|
29
|
+
* Return the Zod schema used for create payloads.
|
|
30
|
+
*/
|
|
31
|
+
getCreateSchema(): TCreateSchema;
|
|
32
|
+
/**
|
|
33
|
+
* Return the Zod schema used for update payloads.
|
|
34
|
+
*/
|
|
35
|
+
getUpdateSchema(): TUpdateSchema;
|
|
36
|
+
/**
|
|
37
|
+
* Return the Zod schema used for serialized output.
|
|
38
|
+
*/
|
|
39
|
+
getOutputSchema(): TOutputSchema;
|
|
40
|
+
/**
|
|
41
|
+
* Validate unknown input for create workflows.
|
|
42
|
+
*/
|
|
43
|
+
deserializeCreate(input: unknown): z.output<TCreateSchema>;
|
|
44
|
+
/**
|
|
45
|
+
* Validate unknown input for update workflows.
|
|
46
|
+
*/
|
|
47
|
+
deserializeUpdate(input: unknown): z.output<TUpdateSchema>;
|
|
48
|
+
/**
|
|
49
|
+
* Convert a persisted record into its outward-facing representation.
|
|
50
|
+
*/
|
|
51
|
+
toRepresentation(record: unknown): z.output<TOutputSchema>;
|
|
52
|
+
}
|