@danceroutine/tango-resources 1.11.15 → 1.12.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.
@@ -1 +1 @@
1
- {"version":3,"file":"CursorPaginator-B_8MhYZY.js","names":[],"sources":["../src/pagination/CursorPaginationInput.ts","../src/paginators/CursorPaginator.ts"],"sourcesContent":["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, QueryResult, QuerySet } from '@danceroutine/tango-orm';\nimport { BasePaginator } from '../pagination/BasePaginator';\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>>\n extends BasePaginator\n implements Paginator<T, T, CursorPaginatedResponse<T>>\n{\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 super();\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>(\n results: readonly TResult[] | QueryResult<TResult>,\n _context?: { totalCount?: number; params?: TangoQueryParams }\n ): CursorPaginatedResponse<TResult> {\n const response: CursorPaginatedResponse<TResult> = { results: this.resolveQueryResultRows(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>(\n results: readonly TResult[] | QueryResult<TResult>,\n _totalCount?: number\n ): CursorPaginatedResponse<TResult> {\n return this.toResponse(results);\n }\n\n /**\n * Apply cursor constraints and ordering to a queryset.\n */\n apply<TBaseResult extends Record<string, unknown>, TSourceModel, THydrated extends Record<string, unknown>>(\n queryset: QuerySet<T, TBaseResult, TSourceModel, THydrated>\n ): QuerySet<T, TBaseResult, TSourceModel, THydrated> {\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 = this.resolveQueryResultRows(fetched);\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"],"mappings":"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"}
1
+ {"version":3,"file":"CursorPaginator-B_8MhYZY.js","names":[],"sources":["../src/pagination/CursorPaginationInput.ts","../src/paginators/CursorPaginator.ts"],"sourcesContent":["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, QueryResult, QuerySet } from '@danceroutine/tango-orm';\nimport { BasePaginator } from '../pagination/BasePaginator';\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>>\n extends BasePaginator\n implements Paginator<T, T, CursorPaginatedResponse<T>>\n{\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 super();\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>(\n results: readonly TResult[] | QueryResult<TResult>,\n _context?: { totalCount?: number; params?: TangoQueryParams }\n ): CursorPaginatedResponse<TResult> {\n const response: CursorPaginatedResponse<TResult> = { results: this.resolveQueryResultRows(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>(\n results: readonly TResult[] | QueryResult<TResult>,\n _totalCount?: number\n ): CursorPaginatedResponse<TResult> {\n return this.toResponse(results);\n }\n\n /**\n * Apply cursor constraints and ordering to a queryset.\n */\n apply<TBaseResult extends Record<string, unknown>, TSourceModel, THydrated extends Record<string, unknown>>(\n queryset: QuerySet<T, TBaseResult, TSourceModel, THydrated>\n ): QuerySet<T, TBaseResult, TSourceModel, THydrated> {\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 = this.resolveQueryResultRows(fetched);\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"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"OffsetPaginator-CaycvxJU.js","names":[],"sources":["../src/pagination/BasePaginator.ts","../src/pagination/OffsetPaginationInput.ts","../src/paginators/OffsetPaginator.ts"],"sourcesContent":["import { QueryResult } from '@danceroutine/tango-orm';\n\nexport abstract class BasePaginator {\n protected resolveQueryResultRows<T>(rows: readonly T[] | QueryResult<T>): T[] {\n if (QueryResult.isQueryResult<T>(rows)) {\n return rows.toArray();\n }\n return [...rows];\n }\n}\n","import { z } from 'zod';\n\nexport type OffsetPaginationInputValue = {\n limit: number;\n offset: number;\n page?: number;\n};\n\nexport const OffsetPaginationInput: z.ZodType<OffsetPaginationInputValue> = 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 { TangoQueryParams } from '@danceroutine/tango-core';\nimport type { QueryResult, QuerySet } from '@danceroutine/tango-orm';\nimport { BasePaginator } from '../pagination/BasePaginator';\nimport type { Paginator, Page } from '../pagination/Paginator';\nimport type { OffsetPaginatedResponse } from '../pagination/PaginatedResponse';\nimport { OffsetPaginationInput } from '../pagination/OffsetPaginationInput';\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 constructor(\n public readonly results: T[],\n private readonly pageNumber: number,\n private readonly perPage: number,\n private readonly totalCount?: number\n ) {}\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 /** Whether a next page exists based on known total count. */\n hasNext(): boolean {\n if (this.totalCount === undefined) {\n return false;\n }\n return this.endIndex() < this.totalCount;\n }\n\n /** Whether a previous page exists. */\n hasPrevious(): boolean {\n return this.pageNumber > 1;\n }\n\n /** The next page number, if available. */\n nextPageNumber(): number | null {\n return this.hasNext() ? this.pageNumber + 1 : null;\n }\n\n /** The previous page number, if available. */\n previousPageNumber(): number | null {\n return this.hasPrevious() ? this.pageNumber - 1 : null;\n }\n\n /** Zero-based start index of this page in the full result set. */\n startIndex(): number {\n return (this.pageNumber - 1) * this.perPage;\n }\n\n /** Exclusive end index of this page in the full result set. */\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>>\n extends BasePaginator\n implements Paginator<T, T, OffsetPaginatedResponse<T>>\n{\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 super();\n this.limit = perPage;\n }\n\n /**\n * Narrow an unknown value to `OffsetPaginator`.\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 Tango query params.\n * If `page` is provided, it's converted to an offset.\n * Stores parsed values for use by getPaginatedResponse.\n */\n parse(params: TangoQueryParams): 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 = OffsetPaginationInput.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 /**\n * Parse params and return `{ limit, offset }` for compatibility callers.\n */\n parseParams(params: TangoQueryParams): { 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 needsTotalCount(): boolean {\n return true;\n }\n\n toResponse<TResult>(\n results: readonly TResult[] | QueryResult<TResult>,\n context?: { totalCount?: number; params?: TangoQueryParams }\n ): OffsetPaginatedResponse<TResult> {\n const totalCount = context?.totalCount;\n const response: OffsetPaginatedResponse<TResult> = { results: this.resolveQueryResultRows(results) };\n\n if (totalCount !== undefined) {\n response.count = totalCount;\n\n if (this.offset + this.limit < totalCount) {\n response.next = this.buildPageLink(this.offset + this.limit, context?.params);\n }\n\n if (this.offset > 0) {\n const prevOffset = Math.max(0, this.offset - this.limit);\n response.previous = this.buildPageLink(prevOffset, context?.params);\n }\n }\n\n return response;\n }\n\n /**\n * Backward-compatible alias for `toResponse`.\n */\n getPaginatedResponse<TResult>(\n results: readonly TResult[] | QueryResult<TResult>,\n totalCount?: number,\n params?: TangoQueryParams\n ): OffsetPaginatedResponse<TResult> {\n return this.toResponse(results, { totalCount, params });\n }\n\n /**\n * Apply current limit/offset to a queryset.\n */\n apply<TBaseResult extends Record<string, unknown>, TSourceModel, THydrated extends Record<string, unknown>>(\n queryset: QuerySet<T, TBaseResult, TSourceModel, THydrated>\n ): QuerySet<T, TBaseResult, TSourceModel, THydrated> {\n return queryset.limit(this.limit).offset(this.offset);\n }\n\n /**\n * Fetch a 1-based page number from the bound queryset.\n */\n async paginate(page: number): Promise<Page<T>> {\n return this.getPage(page);\n }\n\n /**\n * Fetch a 1-based page and return page metadata.\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(this.resolveQueryResultRows(results), page, this.perPage, totalCount);\n }\n\n /**\n * Count total rows for the current queryset state.\n */\n async count(): Promise<number> {\n return this.queryset.count();\n }\n\n private buildPageLink(offset: number, params?: TangoQueryParams): string {\n if (!params) {\n return `?limit=${this.limit}&offset=${offset}`;\n }\n\n return params\n .withValues({\n limit: this.limit,\n offset,\n page: null,\n })\n .toRelativeURL();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"OffsetPaginator-CaycvxJU.js","names":[],"sources":["../src/pagination/BasePaginator.ts","../src/pagination/OffsetPaginationInput.ts","../src/paginators/OffsetPaginator.ts"],"sourcesContent":["import { QueryResult } from '@danceroutine/tango-orm';\n\nexport abstract class BasePaginator {\n protected resolveQueryResultRows<T>(rows: readonly T[] | QueryResult<T>): T[] {\n if (QueryResult.isQueryResult<T>(rows)) {\n return rows.toArray();\n }\n return [...rows];\n }\n}\n","import { z } from 'zod';\n\nexport type OffsetPaginationInputValue = {\n limit: number;\n offset: number;\n page?: number;\n};\n\nexport const OffsetPaginationInput: z.ZodType<OffsetPaginationInputValue> = 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 { TangoQueryParams } from '@danceroutine/tango-core';\nimport type { QueryResult, QuerySet } from '@danceroutine/tango-orm';\nimport { BasePaginator } from '../pagination/BasePaginator';\nimport type { Paginator, Page } from '../pagination/Paginator';\nimport type { OffsetPaginatedResponse } from '../pagination/PaginatedResponse';\nimport { OffsetPaginationInput } from '../pagination/OffsetPaginationInput';\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 constructor(\n public readonly results: T[],\n private readonly pageNumber: number,\n private readonly perPage: number,\n private readonly totalCount?: number\n ) {}\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 /** Whether a next page exists based on known total count. */\n hasNext(): boolean {\n if (this.totalCount === undefined) {\n return false;\n }\n return this.endIndex() < this.totalCount;\n }\n\n /** Whether a previous page exists. */\n hasPrevious(): boolean {\n return this.pageNumber > 1;\n }\n\n /** The next page number, if available. */\n nextPageNumber(): number | null {\n return this.hasNext() ? this.pageNumber + 1 : null;\n }\n\n /** The previous page number, if available. */\n previousPageNumber(): number | null {\n return this.hasPrevious() ? this.pageNumber - 1 : null;\n }\n\n /** Zero-based start index of this page in the full result set. */\n startIndex(): number {\n return (this.pageNumber - 1) * this.perPage;\n }\n\n /** Exclusive end index of this page in the full result set. */\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>>\n extends BasePaginator\n implements Paginator<T, T, OffsetPaginatedResponse<T>>\n{\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 super();\n this.limit = perPage;\n }\n\n /**\n * Narrow an unknown value to `OffsetPaginator`.\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 Tango query params.\n * If `page` is provided, it's converted to an offset.\n * Stores parsed values for use by getPaginatedResponse.\n */\n parse(params: TangoQueryParams): 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 = OffsetPaginationInput.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 /**\n * Parse params and return `{ limit, offset }` for compatibility callers.\n */\n parseParams(params: TangoQueryParams): { 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 needsTotalCount(): boolean {\n return true;\n }\n\n toResponse<TResult>(\n results: readonly TResult[] | QueryResult<TResult>,\n context?: { totalCount?: number; params?: TangoQueryParams }\n ): OffsetPaginatedResponse<TResult> {\n const totalCount = context?.totalCount;\n const response: OffsetPaginatedResponse<TResult> = { results: this.resolveQueryResultRows(results) };\n\n if (totalCount !== undefined) {\n response.count = totalCount;\n\n if (this.offset + this.limit < totalCount) {\n response.next = this.buildPageLink(this.offset + this.limit, context?.params);\n }\n\n if (this.offset > 0) {\n const prevOffset = Math.max(0, this.offset - this.limit);\n response.previous = this.buildPageLink(prevOffset, context?.params);\n }\n }\n\n return response;\n }\n\n /**\n * Backward-compatible alias for `toResponse`.\n */\n getPaginatedResponse<TResult>(\n results: readonly TResult[] | QueryResult<TResult>,\n totalCount?: number,\n params?: TangoQueryParams\n ): OffsetPaginatedResponse<TResult> {\n return this.toResponse(results, { totalCount, params });\n }\n\n /**\n * Apply current limit/offset to a queryset.\n */\n apply<TBaseResult extends Record<string, unknown>, TSourceModel, THydrated extends Record<string, unknown>>(\n queryset: QuerySet<T, TBaseResult, TSourceModel, THydrated>\n ): QuerySet<T, TBaseResult, TSourceModel, THydrated> {\n return queryset.limit(this.limit).offset(this.offset);\n }\n\n /**\n * Fetch a 1-based page number from the bound queryset.\n */\n async paginate(page: number): Promise<Page<T>> {\n return this.getPage(page);\n }\n\n /**\n * Fetch a 1-based page and return page metadata.\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(this.resolveQueryResultRows(results), page, this.perPage, totalCount);\n }\n\n /**\n * Count total rows for the current queryset state.\n */\n async count(): Promise<number> {\n return this.queryset.count();\n }\n\n private buildPageLink(offset: number, params?: TangoQueryParams): string {\n if (!params) {\n return `?limit=${this.limit}&offset=${offset}`;\n }\n\n return params\n .withValues({\n limit: this.limit,\n offset,\n page: null,\n })\n .toRelativeURL();\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"filters-46d2Nr5C.js","names":[],"sources":["../src/filters/internal/InternalFilterType.ts","../src/filters/internal/InternalFilterLookup.ts","../src/filters/FilterSet.ts","../src/filters/index.ts"],"sourcesContent":["export const InternalFilterType = {\n SCALAR: 'scalar',\n ILIKE: 'ilike',\n RANGE: 'range',\n IN: 'in',\n CUSTOM: 'custom',\n} as const;\n","export const InternalFilterLookup = {\n EXACT: 'exact',\n LT: 'lt',\n LTE: 'lte',\n GT: 'gt',\n GTE: 'gte',\n IN: 'in',\n ISNULL: 'isnull',\n CONTAINS: 'contains',\n ICONTAINS: 'icontains',\n STARTSWITH: 'startswith',\n ISTARTSWITH: 'istartswith',\n ENDSWITH: 'endswith',\n IENDSWITH: 'iendswith',\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 { InternalFilterLookup } from './internal/InternalFilterLookup';\nimport type { RangeOperator } from './RangeOperator';\n\nconst FILTER_LOOKUPS = Object.values(InternalFilterLookup) as readonly LookupType[];\n\ntype FilterFieldRef<T extends Record<string, unknown>> = Extract<keyof T, string> | string;\n\nfunction isFilterLookup(value: string): value is LookupType {\n return (FILTER_LOOKUPS as readonly string[]).includes(value);\n}\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 extends Record<string, unknown>> =\n | { type: typeof InternalFilterType.SCALAR; column: FilterFieldRef<T> }\n | { type: typeof InternalFilterType.ILIKE; columns: FilterFieldRef<T>[] }\n | { type: typeof InternalFilterType.RANGE; column: FilterFieldRef<T>; op: RangeOperator }\n | { type: typeof InternalFilterType.IN; column: FilterFieldRef<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: FilterFieldRef<T>;\n lookup?: FilterLookup;\n parse?: FilterValueParser;\n }\n | {\n fields: readonly FilterFieldRef<T>[];\n lookup?: FilterLookup;\n parse?: FilterValueParser;\n };\n\nexport interface FilterSetDefineConfig<T extends Record<string, unknown>> {\n fields?: Partial<Record<string, FieldFilterDeclaration>>;\n aliases?: Record<string, AliasFilterDeclaration<T>>;\n parsers?: Partial<Record<string, 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<string, FieldFilterDeclaration>> = config.fields ?? {};\n const fieldParsers: Partial<Record<string, FilterValueParser>> = config.parsers ?? {};\n\n for (const rawField of Object.keys(fieldDeclarations) as Array<FilterFieldRef<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: FilterFieldRef<T>,\n declaration: FieldFilterDeclaration,\n parser: FilterValueParser | undefined\n ): void {\n if (declaration === true) {\n spec[String(field)] = FilterSet.createLookupResolver(field, InternalFilterLookup.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 ?? [InternalFilterLookup.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 ?? InternalFilterLookup.ICONTAINS;\n return FilterSet.createMultiFieldResolver(declaration.fields, lookup, declaration.parse);\n }\n\n return FilterSet.createLookupResolver(\n declaration.field,\n declaration.lookup ?? InternalFilterLookup.EXACT,\n declaration.parse\n );\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 FilterFieldRef<T>[],\n lookup: FilterLookup,\n parser?: FilterValueParser\n ): FilterResolver<T> {\n if (lookup === InternalFilterLookup.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: FilterFieldRef<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: FilterFieldRef<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 * Return a new filter set with parser-aware scalar/range/in resolvers for matching fields.\n */\n withFieldParsers(parsers: Partial<Record<string, FilterValueParser>>): FilterSet<T> {\n if (Object.keys(parsers).length === 0) {\n return this;\n }\n\n let changed = false;\n const nextSpec = Object.fromEntries(\n Object.entries(this.spec).map(([param, resolver]) => {\n const nextResolver = this.applyFieldParserOverride(resolver, parsers);\n if (nextResolver !== resolver) {\n changed = true;\n }\n return [param, nextResolver];\n })\n ) as Record<string, FilterResolver<T>>;\n\n return changed ? new FilterSet(nextSpec, this.allowAllParams) : this;\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 segments = param.split('__').filter(Boolean);\n if (segments.length === 0) {\n return undefined;\n }\n\n const lastSegment = segments.at(-1)!;\n const lookup = isFilterLookup(lastSegment) ? lastSegment : 'exact';\n const field = (lookup === 'exact' ? segments : segments.slice(0, -1)).join('__') as FilterFieldRef<T>;\n\n if (!field) {\n return undefined;\n }\n\n if (lookup === 'exact') {\n return { type: InternalFilterType.SCALAR, column: field };\n }\n\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 private applyFieldParserOverride(\n resolver: FilterResolver<T>,\n parsers: Partial<Record<string, FilterValueParser>>\n ): FilterResolver<T> {\n switch (resolver.type) {\n case InternalFilterType.SCALAR: {\n const parser = parsers[resolver.column];\n return parser ? FilterSet.createLookupResolver(resolver.column, 'exact', parser) : resolver;\n }\n\n case InternalFilterType.RANGE: {\n const parser = parsers[resolver.column];\n return parser ? FilterSet.createLookupResolver(resolver.column, resolver.op, parser) : resolver;\n }\n\n case InternalFilterType.IN: {\n const parser = parsers[resolver.column];\n return parser ? FilterSet.createLookupResolver(resolver.column, 'in', parser) : resolver;\n }\n\n default:\n return resolver;\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"],"mappings":"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"}
1
+ {"version":3,"file":"filters-46d2Nr5C.js","names":[],"sources":["../src/filters/internal/InternalFilterType.ts","../src/filters/internal/InternalFilterLookup.ts","../src/filters/FilterSet.ts","../src/filters/index.ts"],"sourcesContent":["export const InternalFilterType = {\n SCALAR: 'scalar',\n ILIKE: 'ilike',\n RANGE: 'range',\n IN: 'in',\n CUSTOM: 'custom',\n} as const;\n","export const InternalFilterLookup = {\n EXACT: 'exact',\n LT: 'lt',\n LTE: 'lte',\n GT: 'gt',\n GTE: 'gte',\n IN: 'in',\n ISNULL: 'isnull',\n CONTAINS: 'contains',\n ICONTAINS: 'icontains',\n STARTSWITH: 'startswith',\n ISTARTSWITH: 'istartswith',\n ENDSWITH: 'endswith',\n IENDSWITH: 'iendswith',\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 { InternalFilterLookup } from './internal/InternalFilterLookup';\nimport type { RangeOperator } from './RangeOperator';\n\nconst FILTER_LOOKUPS = Object.values(InternalFilterLookup) as readonly LookupType[];\n\ntype FilterFieldRef<T extends Record<string, unknown>> = Extract<keyof T, string> | string;\n\nfunction isFilterLookup(value: string): value is LookupType {\n return (FILTER_LOOKUPS as readonly string[]).includes(value);\n}\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 extends Record<string, unknown>> =\n | { type: typeof InternalFilterType.SCALAR; column: FilterFieldRef<T> }\n | { type: typeof InternalFilterType.ILIKE; columns: FilterFieldRef<T>[] }\n | { type: typeof InternalFilterType.RANGE; column: FilterFieldRef<T>; op: RangeOperator }\n | { type: typeof InternalFilterType.IN; column: FilterFieldRef<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: FilterFieldRef<T>;\n lookup?: FilterLookup;\n parse?: FilterValueParser;\n }\n | {\n fields: readonly FilterFieldRef<T>[];\n lookup?: FilterLookup;\n parse?: FilterValueParser;\n };\n\nexport interface FilterSetDefineConfig<T extends Record<string, unknown>> {\n fields?: Partial<Record<string, FieldFilterDeclaration>>;\n aliases?: Record<string, AliasFilterDeclaration<T>>;\n parsers?: Partial<Record<string, 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<string, FieldFilterDeclaration>> = config.fields ?? {};\n const fieldParsers: Partial<Record<string, FilterValueParser>> = config.parsers ?? {};\n\n for (const rawField of Object.keys(fieldDeclarations) as Array<FilterFieldRef<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: FilterFieldRef<T>,\n declaration: FieldFilterDeclaration,\n parser: FilterValueParser | undefined\n ): void {\n if (declaration === true) {\n spec[String(field)] = FilterSet.createLookupResolver(field, InternalFilterLookup.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 ?? [InternalFilterLookup.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 ?? InternalFilterLookup.ICONTAINS;\n return FilterSet.createMultiFieldResolver(declaration.fields, lookup, declaration.parse);\n }\n\n return FilterSet.createLookupResolver(\n declaration.field,\n declaration.lookup ?? InternalFilterLookup.EXACT,\n declaration.parse\n );\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 FilterFieldRef<T>[],\n lookup: FilterLookup,\n parser?: FilterValueParser\n ): FilterResolver<T> {\n if (lookup === InternalFilterLookup.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: FilterFieldRef<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: FilterFieldRef<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 * Return a new filter set with parser-aware scalar/range/in resolvers for matching fields.\n */\n withFieldParsers(parsers: Partial<Record<string, FilterValueParser>>): FilterSet<T> {\n if (Object.keys(parsers).length === 0) {\n return this;\n }\n\n let changed = false;\n const nextSpec = Object.fromEntries(\n Object.entries(this.spec).map(([param, resolver]) => {\n const nextResolver = this.applyFieldParserOverride(resolver, parsers);\n if (nextResolver !== resolver) {\n changed = true;\n }\n return [param, nextResolver];\n })\n ) as Record<string, FilterResolver<T>>;\n\n return changed ? new FilterSet(nextSpec, this.allowAllParams) : this;\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 segments = param.split('__').filter(Boolean);\n if (segments.length === 0) {\n return undefined;\n }\n\n const lastSegment = segments.at(-1)!;\n const lookup = isFilterLookup(lastSegment) ? lastSegment : 'exact';\n const field = (lookup === 'exact' ? segments : segments.slice(0, -1)).join('__') as FilterFieldRef<T>;\n\n if (!field) {\n return undefined;\n }\n\n if (lookup === 'exact') {\n return { type: InternalFilterType.SCALAR, column: field };\n }\n\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 private applyFieldParserOverride(\n resolver: FilterResolver<T>,\n parsers: Partial<Record<string, FilterValueParser>>\n ): FilterResolver<T> {\n switch (resolver.type) {\n case InternalFilterType.SCALAR: {\n const parser = parsers[resolver.column];\n return parser ? FilterSet.createLookupResolver(resolver.column, 'exact', parser) : resolver;\n }\n\n case InternalFilterType.RANGE: {\n const parser = parsers[resolver.column];\n return parser ? FilterSet.createLookupResolver(resolver.column, resolver.op, parser) : resolver;\n }\n\n case InternalFilterType.IN: {\n const parser = parsers[resolver.column];\n return parser ? FilterSet.createLookupResolver(resolver.column, 'in', parser) : resolver;\n }\n\n default:\n return resolver;\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"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"inferModelFieldParsers-2irv7j1T.js","names":[],"sources":["../src/filters/inferModelFieldParsers.ts"],"sourcesContent":["import type { ResourceModelLike } from '../resource/index';\nimport type { FilterValueParser } from './FilterSet';\n\nfunction normalizeParserTokens(raw: string | string[]): string[] {\n const tokens = Array.isArray(raw) ? raw : String(raw).split(',');\n const normalized = tokens.map((value) => value.trim());\n return normalized.every((value) => value.length > 0) ? normalized : [];\n}\n\nfunction createBooleanParser(): FilterValueParser {\n return (raw) => {\n const values = normalizeParserTokens(raw);\n if (values.length === 0) {\n return undefined;\n }\n\n const parsed = values.map((value) => {\n const normalized = value.toLowerCase();\n\n if (normalized === 'true' || normalized === '1') {\n return true;\n }\n\n if (normalized === 'false' || normalized === '0') {\n return false;\n }\n\n return null;\n });\n\n if (parsed.some((value) => value === null)) {\n return undefined;\n }\n\n return parsed.length === 1 ? parsed[0]! : (parsed as boolean[]);\n };\n}\n\nfunction createIntegerParser(): FilterValueParser {\n return (raw) => {\n const values = normalizeParserTokens(raw);\n if (values.length === 0) {\n return undefined;\n }\n\n const parsed = values.map(Number);\n\n if (parsed.some((value) => !Number.isInteger(value))) {\n return undefined;\n }\n\n return parsed.length === 1 ? parsed[0] : parsed;\n };\n}\n\nfunction createTimestampParser(): FilterValueParser {\n return (raw) => {\n const values = normalizeParserTokens(raw);\n if (values.length === 0) {\n return undefined;\n }\n\n const parsed = values.map((value) => {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n });\n\n if (parsed.some((value) => value === null)) {\n return undefined;\n }\n\n return parsed.length === 1 ? parsed[0]! : (parsed as Date[]);\n };\n}\n\n/**\n * Infer resource-level query-value parsers from Tango model metadata.\n *\n * Parsers are inferred conservatively from field metadata so HTTP query filters\n * can be coerced into typed ORM inputs without framework-specific glue.\n */\nexport function inferModelFieldParsers<T extends Record<string, unknown>>(\n model: ResourceModelLike<T>\n): Partial<Record<keyof T, FilterValueParser>> {\n const metadata = model.metadata;\n if (!metadata) {\n return {};\n }\n\n const parsers: Partial<Record<keyof T, FilterValueParser>> = {};\n\n for (const field of metadata.fields) {\n switch (field.type) {\n case 'bool':\n parsers[field.name as keyof T] = createBooleanParser();\n break;\n case 'serial':\n case 'int':\n case 'bigint':\n parsers[field.name as keyof T] = createIntegerParser();\n break;\n case 'timestamptz':\n parsers[field.name as keyof T] = createTimestampParser();\n break;\n default:\n break;\n }\n }\n\n return parsers;\n}\n"],"mappings":";AAGA,SAAS,sBAAsB,KAAkC;CAE7D,MAAM,cADS,MAAM,QAAQ,GAAG,IAAI,MAAM,OAAO,GAAG,EAAE,MAAM,GAAG,GACrC,KAAK,UAAU,MAAM,KAAK,CAAC;CACrD,OAAO,WAAW,OAAO,UAAU,MAAM,SAAS,CAAC,IAAI,aAAa,CAAC;AACzE;AAEA,SAAS,sBAAyC;CAC9C,QAAQ,QAAQ;EACZ,MAAM,SAAS,sBAAsB,GAAG;EACxC,IAAI,OAAO,WAAW,GAClB;EAGJ,MAAM,SAAS,OAAO,KAAK,UAAU;GACjC,MAAM,aAAa,MAAM,YAAY;GAErC,IAAI,eAAe,UAAU,eAAe,KACxC,OAAO;GAGX,IAAI,eAAe,WAAW,eAAe,KACzC,OAAO;GAGX,OAAO;EACX,CAAC;EAED,IAAI,OAAO,MAAM,UAAU,UAAU,IAAI,GACrC;EAGJ,OAAO,OAAO,WAAW,IAAI,OAAO,KAAO;CAC/C;AACJ;AAEA,SAAS,sBAAyC;CAC9C,QAAQ,QAAQ;EACZ,MAAM,SAAS,sBAAsB,GAAG;EACxC,IAAI,OAAO,WAAW,GAClB;EAGJ,MAAM,SAAS,OAAO,IAAI,MAAM;EAEhC,IAAI,OAAO,MAAM,UAAU,CAAC,OAAO,UAAU,KAAK,CAAC,GAC/C;EAGJ,OAAO,OAAO,WAAW,IAAI,OAAO,KAAK;CAC7C;AACJ;AAEA,SAAS,wBAA2C;CAChD,QAAQ,QAAQ;EACZ,MAAM,SAAS,sBAAsB,GAAG;EACxC,IAAI,OAAO,WAAW,GAClB;EAGJ,MAAM,SAAS,OAAO,KAAK,UAAU;GACjC,MAAM,OAAO,IAAI,KAAK,KAAK;GAC3B,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,IAAI,OAAO;EACjD,CAAC;EAED,IAAI,OAAO,MAAM,UAAU,UAAU,IAAI,GACrC;EAGJ,OAAO,OAAO,WAAW,IAAI,OAAO,KAAO;CAC/C;AACJ;;;;;;;AAQA,SAAgB,uBACZ,OAC2C;CAC3C,MAAM,WAAW,MAAM;CACvB,IAAI,CAAC,UACD,OAAO,CAAC;CAGZ,MAAM,UAAuD,CAAC;CAE9D,KAAK,MAAM,SAAS,SAAS,QACzB,QAAQ,MAAM,MAAd;EACI,KAAK;GACD,QAAQ,MAAM,QAAmB,oBAAoB;GACrD;EACJ,KAAK;EACL,KAAK;EACL,KAAK;GACD,QAAQ,MAAM,QAAmB,oBAAoB;GACrD;EACJ,KAAK;GACD,QAAQ,MAAM,QAAmB,sBAAsB;GACvD;EACJ,SACI;CACR;CAGJ,OAAO;AACX"}
1
+ {"version":3,"file":"inferModelFieldParsers-2irv7j1T.js","names":[],"sources":["../src/filters/inferModelFieldParsers.ts"],"sourcesContent":["import type { ResourceModelLike } from '../resource/index';\nimport type { FilterValueParser } from './FilterSet';\n\nfunction normalizeParserTokens(raw: string | string[]): string[] {\n const tokens = Array.isArray(raw) ? raw : String(raw).split(',');\n const normalized = tokens.map((value) => value.trim());\n return normalized.every((value) => value.length > 0) ? normalized : [];\n}\n\nfunction createBooleanParser(): FilterValueParser {\n return (raw) => {\n const values = normalizeParserTokens(raw);\n if (values.length === 0) {\n return undefined;\n }\n\n const parsed = values.map((value) => {\n const normalized = value.toLowerCase();\n\n if (normalized === 'true' || normalized === '1') {\n return true;\n }\n\n if (normalized === 'false' || normalized === '0') {\n return false;\n }\n\n return null;\n });\n\n if (parsed.some((value) => value === null)) {\n return undefined;\n }\n\n return parsed.length === 1 ? parsed[0]! : (parsed as boolean[]);\n };\n}\n\nfunction createIntegerParser(): FilterValueParser {\n return (raw) => {\n const values = normalizeParserTokens(raw);\n if (values.length === 0) {\n return undefined;\n }\n\n const parsed = values.map(Number);\n\n if (parsed.some((value) => !Number.isInteger(value))) {\n return undefined;\n }\n\n return parsed.length === 1 ? parsed[0] : parsed;\n };\n}\n\nfunction createTimestampParser(): FilterValueParser {\n return (raw) => {\n const values = normalizeParserTokens(raw);\n if (values.length === 0) {\n return undefined;\n }\n\n const parsed = values.map((value) => {\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n });\n\n if (parsed.some((value) => value === null)) {\n return undefined;\n }\n\n return parsed.length === 1 ? parsed[0]! : (parsed as Date[]);\n };\n}\n\n/**\n * Infer resource-level query-value parsers from Tango model metadata.\n *\n * Parsers are inferred conservatively from field metadata so HTTP query filters\n * can be coerced into typed ORM inputs without framework-specific glue.\n */\nexport function inferModelFieldParsers<T extends Record<string, unknown>>(\n model: ResourceModelLike<T>\n): Partial<Record<keyof T, FilterValueParser>> {\n const metadata = model.metadata;\n if (!metadata) {\n return {};\n }\n\n const parsers: Partial<Record<keyof T, FilterValueParser>> = {};\n\n for (const field of metadata.fields) {\n switch (field.type) {\n case 'bool':\n parsers[field.name as keyof T] = createBooleanParser();\n break;\n case 'serial':\n case 'int':\n case 'bigint':\n parsers[field.name as keyof T] = createIntegerParser();\n break;\n case 'timestamptz':\n parsers[field.name as keyof T] = createTimestampParser();\n break;\n default:\n break;\n }\n }\n\n return parsers;\n}\n"],"mappings":";AAGA,SAAS,sBAAsB,KAAkC;CAE7D,MAAM,cADS,MAAM,QAAQ,GAAG,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,GAAG,EAAA,CACrC,KAAK,UAAU,MAAM,KAAK,CAAC;CACrD,OAAO,WAAW,OAAO,UAAU,MAAM,SAAS,CAAC,IAAI,aAAa,CAAC;AACzE;AAEA,SAAS,sBAAyC;CAC9C,QAAQ,QAAQ;EACZ,MAAM,SAAS,sBAAsB,GAAG;EACxC,IAAI,OAAO,WAAW,GAClB;EAGJ,MAAM,SAAS,OAAO,KAAK,UAAU;GACjC,MAAM,aAAa,MAAM,YAAY;GAErC,IAAI,eAAe,UAAU,eAAe,KACxC,OAAO;GAGX,IAAI,eAAe,WAAW,eAAe,KACzC,OAAO;GAGX,OAAO;EACX,CAAC;EAED,IAAI,OAAO,MAAM,UAAU,UAAU,IAAI,GACrC;EAGJ,OAAO,OAAO,WAAW,IAAI,OAAO,KAAO;CAC/C;AACJ;AAEA,SAAS,sBAAyC;CAC9C,QAAQ,QAAQ;EACZ,MAAM,SAAS,sBAAsB,GAAG;EACxC,IAAI,OAAO,WAAW,GAClB;EAGJ,MAAM,SAAS,OAAO,IAAI,MAAM;EAEhC,IAAI,OAAO,MAAM,UAAU,CAAC,OAAO,UAAU,KAAK,CAAC,GAC/C;EAGJ,OAAO,OAAO,WAAW,IAAI,OAAO,KAAK;CAC7C;AACJ;AAEA,SAAS,wBAA2C;CAChD,QAAQ,QAAQ;EACZ,MAAM,SAAS,sBAAsB,GAAG;EACxC,IAAI,OAAO,WAAW,GAClB;EAGJ,MAAM,SAAS,OAAO,KAAK,UAAU;GACjC,MAAM,OAAO,IAAI,KAAK,KAAK;GAC3B,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,IAAI,OAAO;EACjD,CAAC;EAED,IAAI,OAAO,MAAM,UAAU,UAAU,IAAI,GACrC;EAGJ,OAAO,OAAO,WAAW,IAAI,OAAO,KAAO;CAC/C;AACJ;;;;;;;AAQA,SAAgB,uBACZ,OAC2C;CAC3C,MAAM,WAAW,MAAM;CACvB,IAAI,CAAC,UACD,OAAO,CAAC;CAGZ,MAAM,UAAuD,CAAC;CAE9D,KAAK,MAAM,SAAS,SAAS,QACzB,QAAQ,MAAM,MAAd;EACI,KAAK;GACD,QAAQ,MAAM,QAAmB,oBAAoB;GACrD;EACJ,KAAK;EACL,KAAK;EACL,KAAK;GACD,QAAQ,MAAM,QAAmB,oBAAoB;GACrD;EACJ,KAAK;GACD,QAAQ,MAAM,QAAmB,sBAAsB;GACvD;EACJ,SACI;CACR;CAGJ,OAAO;AACX"}
@@ -1 +1 @@
1
- {"version":3,"file":"serializer-RSwlXWls.js","names":[],"sources":["../src/serializer/Serializer.ts","../src/serializer/internal/InternalSerializerRelationKind.ts","../src/serializer/ModelSerializer.ts","../src/serializer/relation.ts","../src/serializer/index.ts"],"sourcesContent":["import { getLogger } from '@danceroutine/tango-core';\nimport { 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 TRecord = unknown,\n> = {\n new (): Serializer<TCreateSchema, TUpdateSchema, TOutputSchema, TRecord>;\n readonly createSchema: TCreateSchema;\n readonly updateSchema: TUpdateSchema;\n readonly outputSchema: TOutputSchema;\n readonly outputResolvers?: SerializerOutputResolvers<TRecord>;\n};\n\nexport type AnySerializerClass = SerializerClass<\n SerializerSchema,\n SerializerSchema,\n SerializerSchema,\n // oxlint-disable-next-line typescript/no-explicit-any\n any\n>;\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']>;\nexport type SerializerOutputResolver<TRecord = unknown> = (record: TRecord) => unknown | Promise<unknown>;\nexport type SerializerOutputResolvers<TRecord = unknown> = Record<string, SerializerOutputResolver<TRecord>>;\n\nconst logger = getLogger('tango.resources.serializer');\nlet hasWarnedAboutToRepresentationDeprecation = false;\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 TRecord = unknown,\n> {\n static readonly createSchema: SerializerSchema = z.unknown();\n static readonly updateSchema: SerializerSchema = z.unknown();\n static readonly outputSchema: SerializerSchema = z.unknown();\n static readonly outputResolvers:\n | SerializerOutputResolvers<// oxlint-disable-next-line typescript/no-explicit-any\n any>\n | undefined = undefined;\n\n /**\n * Return the serializer class for the current instance.\n */\n getSerializerClass(): SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema, TRecord> {\n return this.constructor as SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema, TRecord>;\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 * Return the resolver map used to enrich serializer output fields before\n * the outward Zod schema parses the final response shape.\n */\n getOutputResolvers(): SerializerOutputResolvers<TRecord> {\n return this.getSerializerClass().outputResolvers ?? {};\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 * @deprecated Use `serialize(...)` instead so serializer-owned output\n * resolvers run before the outward Zod schema parses the response shape.\n */\n toRepresentation(record: TRecord): z.output<TOutputSchema> {\n if (!hasWarnedAboutToRepresentationDeprecation) {\n hasWarnedAboutToRepresentationDeprecation = true;\n logger.warn(\n '`Serializer.toRepresentation(...)` is deprecated. Use `serialize(...)` instead so output resolvers run before outward parsing.'\n );\n }\n return this.getOutputSchema().parse(record);\n }\n\n /**\n * Resolve serializer-owned output fields and parse the outward response\n * contract.\n */\n async serialize(record: TRecord): Promise<z.output<TOutputSchema>> {\n return this.getOutputSchema().parse(await this.applyOutputResolvers(record));\n }\n\n /**\n * Serialize many records through the same outward response contract.\n */\n async serializeMany(records: readonly TRecord[]): Promise<z.output<TOutputSchema>[]> {\n return Promise.all(records.map((record) => this.serialize(record)));\n }\n\n private async applyOutputResolvers(record: TRecord): Promise<unknown> {\n const resolvers = this.getOutputResolvers();\n const resolverEntries = Object.entries(resolvers);\n\n if (resolverEntries.length === 0 || typeof record !== 'object' || record === null) {\n return record;\n }\n\n const resolved = await Promise.all(\n resolverEntries.map(async ([key, resolver]) => [key, await resolver(record)] as const)\n );\n\n return {\n ...(record as Record<string, unknown>),\n ...Object.fromEntries(resolved),\n };\n }\n}\n","export const InternalSerializerRelationKind = {\n MANY_TO_MANY: 'manyToMany',\n} as const;\n\nexport const InternalManyToManyReadStrategyKind = {\n PK_LIST: 'pkList',\n NESTED: 'nested',\n} as const;\n\nexport const InternalManyToManyWriteStrategyKind = {\n PK_LIST: 'pkList',\n SLUG_LIST: 'slugList',\n} as const;\n","import type { FilterInput, ManagerLike, ManyToManyTargetRef } from '@danceroutine/tango-orm';\nimport { ManyToManyRelatedManager } 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 SerializerOutputResolvers,\n type SerializerSchema,\n type SerializerUpdateInput,\n} from './Serializer';\nimport type { ManyToManyManagerKeys, ManyToManyRelationField, ModelSerializerRelationFields } from './relation';\nimport {\n InternalManyToManyReadStrategyKind,\n InternalManyToManyWriteStrategyKind,\n InternalSerializerRelationKind,\n} from './internal/InternalSerializerRelationKind';\n\n// oxlint-disable-next-line typescript/no-explicit-any\ntype AnyRelationField = ManyToManyRelationField<any, any>;\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 TModelRecord extends Record<string, unknown> = TModel,\n> = SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord> & {\n new (): ModelSerializer<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>;\n readonly model?: ResourceModelLike<TModel, TModelRecord>;\n readonly relationFields?: ModelSerializerRelationFields<TModelRecord>;\n};\n\n//\nexport type AnyModelSerializer<TModel extends Record<string, unknown> = Record<string, unknown>> = ModelSerializerClass<\n TModel,\n SerializerSchema,\n SerializerSchema,\n SerializerSchema,\n // oxlint-disable-next-line typescript/no-explicit-any\n any\n>;\n\nexport type AnyModelSerializerClass = AnyModelSerializer;\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 TModelRecord extends Record<string, unknown> = TModel,\n> extends Serializer<TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord> {\n static readonly model?: unknown;\n static readonly relationFields: ModelSerializerRelationFields<Record<string, unknown>> | undefined = undefined;\n\n /**\n * Return the Tango model backing this serializer.\n */\n getModel(): ResourceModelLike<TModel, TModelRecord> {\n const model = (\n this.constructor as Partial<\n ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>\n >\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<TModelRecord> {\n return this.getModel().objects as ManagerLike<TModelRecord>;\n }\n\n /**\n * Return the declarative relation-field map for this serializer.\n */\n getRelationFields(): ModelSerializerRelationFields<TModelRecord> {\n return (\n (\n this.constructor as Partial<\n ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>\n >\n ).relationFields ?? {}\n );\n }\n\n /**\n * Merge relation-field read resolvers into the serializer output path.\n */\n override getOutputResolvers(): SerializerOutputResolvers<TModelRecord> {\n const baseResolvers = super.getOutputResolvers();\n const relationFields = this.getRelationFields() as Record<string, AnyRelationField>;\n const relationResolvers = Object.fromEntries(\n Object.entries(relationFields).map(([fieldName, field]) => [\n fieldName,\n async (record: TModelRecord) => this.serializeRelationField(record, fieldName, field),\n ])\n ) as SerializerOutputResolvers<TModelRecord>;\n\n return {\n ...relationResolvers,\n ...baseResolvers,\n };\n }\n\n /**\n * Validate, enrich, persist, and serialize a create workflow.\n */\n async create(\n input: unknown\n ): Promise<\n SerializerOutput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>>\n > {\n const validated = this.deserializeCreate(input);\n const relationWrites = this.extractRelationWrites(validated, input);\n const prepared = await this.beforeCreate(validated);\n const created = await this.getManager().create(this.stripRelationFields(prepared));\n await this.applyRelationWrites(created, relationWrites);\n return this.serialize(created);\n }\n\n /**\n * Validate, enrich, persist, and serialize an update workflow.\n */\n async update(\n id: TModelRecord[keyof TModelRecord],\n input: unknown\n ): Promise<\n SerializerOutput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>>\n > {\n const validated = this.deserializeUpdate(input);\n const relationWrites = this.extractRelationWrites(validated, input);\n const prepared = await this.beforeUpdate(id, validated);\n const updated = await this.getManager().update(id, this.stripRelationFields(prepared));\n await this.applyRelationWrites(updated, relationWrites);\n return this.serialize(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<\n ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>\n >\n ): Promise<Partial<TModelRecord>> {\n return data as Partial<TModelRecord>;\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: TModelRecord[keyof TModelRecord],\n data: SerializerUpdateInput<\n ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>\n >\n ): Promise<Partial<TModelRecord>> {\n return data as Partial<TModelRecord>;\n }\n\n private extractRelationWrites(\n data: unknown,\n rawInput: unknown\n ): Partial<Record<ManyToManyManagerKeys<TModelRecord>, unknown>> {\n if (typeof data !== 'object' || data === null || typeof rawInput !== 'object' || rawInput === null) {\n return {};\n }\n\n const relationFields = this.getRelationFields() as Record<string, AnyRelationField>;\n const writes: Partial<Record<ManyToManyManagerKeys<TModelRecord>, unknown>> = {};\n\n for (const fieldName of Object.keys(relationFields) as ManyToManyManagerKeys<TModelRecord>[]) {\n if (fieldName in rawInput) {\n writes[fieldName] = (data as Record<string, unknown>)[fieldName];\n }\n }\n\n return writes;\n }\n\n private stripRelationFields(data: Partial<TModelRecord>): Partial<TModelRecord> {\n const relationFieldNames = new Set(Object.keys(this.getRelationFields()));\n if (relationFieldNames.size === 0) {\n return data;\n }\n\n return Object.fromEntries(\n Object.entries(data as Record<string, unknown>).filter(([fieldName]) => !relationFieldNames.has(fieldName))\n ) as Partial<TModelRecord>;\n }\n\n private async applyRelationWrites(\n record: TModelRecord,\n writes: Partial<Record<ManyToManyManagerKeys<TModelRecord>, unknown>>\n ): Promise<void> {\n const relationFields = this.getRelationFields() as Record<string, AnyRelationField>;\n\n for (const [fieldName, value] of Object.entries(writes) as Array<\n [ManyToManyManagerKeys<TModelRecord>, unknown]\n >) {\n const field = relationFields[fieldName];\n if (!field) {\n continue;\n }\n await this.syncManyToManyRelation(record, fieldName, value);\n }\n }\n\n private async serializeRelationField(\n record: TModelRecord,\n fieldName: string,\n field: AnyRelationField\n ): Promise<unknown> {\n const manager = this.getManyToManyManager(record, fieldName);\n const rows = (await manager.all().fetch()).results;\n const relationMeta = this.getManyToManyRelationMeta(fieldName);\n\n switch (field.read.kind) {\n case InternalManyToManyReadStrategyKind.PK_LIST:\n return rows.map((row) => row[relationMeta.targetPrimaryKey]);\n case InternalManyToManyReadStrategyKind.NESTED:\n return rows.map((row) =>\n (field.read as { schema: { parse: (input: unknown) => unknown } }).schema.parse(row)\n );\n }\n }\n\n private async syncManyToManyRelation(\n record: TModelRecord,\n fieldName: ManyToManyManagerKeys<TModelRecord>,\n value: unknown\n ): Promise<void> {\n if (value === undefined) {\n return;\n }\n\n const manager = this.getManyToManyManager(record, fieldName);\n const field = (this.getRelationFields() as Record<string, AnyRelationField>)[fieldName];\n if (!field) {\n return;\n }\n\n const nextTargets = await this.resolveWriteTargets(fieldName, field.write, value);\n await manager.set(...nextTargets);\n }\n\n private async resolveWriteTargets(\n fieldName: ManyToManyManagerKeys<TModelRecord>,\n strategy: AnyRelationField['write'],\n value: unknown\n ): Promise<readonly ManyToManyTargetRef<Record<string, unknown>>[]> {\n switch (strategy.kind) {\n case InternalManyToManyWriteStrategyKind.PK_LIST:\n if (!Array.isArray(value)) {\n throw new TypeError(\n `Relation field '${String(fieldName)}' expects an array of primary-key values.`\n );\n }\n return value as readonly ManyToManyTargetRef<Record<string, unknown>>[];\n case InternalManyToManyWriteStrategyKind.SLUG_LIST: {\n if (!Array.isArray(value)) {\n throw new TypeError(`Relation field '${String(fieldName)}' expects an array of lookup values.`);\n }\n\n const lookupValues = [...new Set(value.map((entry) => String(entry).trim()).filter(Boolean))];\n if (lookupValues.length === 0) {\n return [];\n }\n\n const filter = {\n [`${strategy.lookupField}__in`]: lookupValues,\n } as FilterInput<Record<string, unknown>>;\n const existing = await strategy.model.objects.query().filter(filter).fetch();\n const byLookup = new Map(\n existing.results.map((row) => [String((row as Record<string, unknown>)[strategy.lookupField]), row])\n );\n const resolved: Record<string, unknown>[] = [];\n\n for (const lookupValue of lookupValues) {\n const found = byLookup.get(lookupValue);\n if (found) {\n resolved.push(found);\n continue;\n }\n\n if (!strategy.createIfMissing) {\n throw new Error(\n `Relation field '${String(fieldName)}' could not resolve '${lookupValue}' via '${strategy.lookupField}'.`\n );\n }\n\n const created = await strategy.model.objects.create(\n strategy.buildCreateInput?.(lookupValue) ??\n ({ [strategy.lookupField]: lookupValue } as Record<string, unknown>)\n );\n resolved.push(created as Record<string, unknown>);\n }\n\n return resolved;\n }\n }\n }\n\n private getManyToManyManager(\n record: TModelRecord,\n fieldName: string\n ): ManyToManyRelatedManager<Record<string, unknown>> {\n const manager = (record as Record<string, unknown>)[fieldName];\n if (!ManyToManyRelatedManager.isManyToManyRelatedManager(manager)) {\n throw new Error(`Relation field '${fieldName}' is not backed by a many-to-many related manager.`);\n }\n return manager as ManyToManyRelatedManager<Record<string, unknown>>;\n }\n\n private getManyToManyRelationMeta(\n fieldName: string\n ): NonNullable<NonNullable<ManagerLike<TModelRecord>['meta']['relations']>[string]> {\n const relation = this.getManager().meta.relations?.[fieldName];\n if (!relation || relation.kind !== InternalSerializerRelationKind.MANY_TO_MANY) {\n throw new Error(`Relation field '${fieldName}' is not a persisted many-to-many edge.`);\n }\n return relation;\n }\n}\n\nexport type { AnySerializerClass };\n","import { z } from 'zod';\nimport type { ManyToManyRelatedManager } from '@danceroutine/tango-orm';\nimport type { ResourceModelLike } from '../resource/ResourceModelLike';\nimport {\n InternalManyToManyReadStrategyKind,\n InternalManyToManyWriteStrategyKind,\n InternalSerializerRelationKind,\n} from './internal/InternalSerializerRelationKind';\n\ntype AnyRecord = Record<string, unknown>;\n\nexport type ManyToManyManagerKeys<TRecord extends AnyRecord> = Extract<\n {\n [K in keyof TRecord]: TRecord[K] extends ManyToManyRelatedManager<AnyRecord> ? K : never;\n }[keyof TRecord],\n string\n>;\n\nexport type ManyToManyTargetRow<TRecord extends AnyRecord, TFieldName extends ManyToManyManagerKeys<TRecord>> =\n TRecord[TFieldName] extends ManyToManyRelatedManager<infer TTarget extends AnyRecord> ? TTarget : never;\n\nexport type ManyToManyPkListStrategy = {\n kind: typeof InternalManyToManyReadStrategyKind.PK_LIST | typeof InternalManyToManyWriteStrategyKind.PK_LIST;\n};\n\nexport type ManyToManyNestedStrategy = {\n kind: typeof InternalManyToManyReadStrategyKind.NESTED;\n schema: z.ZodTypeAny;\n};\n\nexport type ManyToManySlugListStrategy<TTarget extends AnyRecord> = {\n kind: typeof InternalManyToManyWriteStrategyKind.SLUG_LIST;\n // oxlint-disable-next-line typescript/no-explicit-any\n model: ResourceModelLike<any, any>;\n lookupField: Extract<keyof TTarget, string>;\n createIfMissing?: boolean;\n buildCreateInput?: (value: string) => Partial<TTarget>;\n};\n\nexport type ManyToManyReadStrategy = ManyToManyPkListStrategy | ManyToManyNestedStrategy;\nexport type ManyToManyWriteStrategy<TTarget extends AnyRecord> =\n | ManyToManyPkListStrategy\n | ManyToManySlugListStrategy<TTarget>;\n\nexport type ManyToManyRelationField<TRecord extends AnyRecord, TFieldName extends ManyToManyManagerKeys<TRecord>> = {\n kind: typeof InternalSerializerRelationKind.MANY_TO_MANY;\n read: ManyToManyReadStrategy;\n write: ManyToManyWriteStrategy<ManyToManyTargetRow<TRecord, TFieldName>>;\n};\n\nexport type ModelSerializerRelationFields<TRecord extends AnyRecord> = Partial<{\n [K in ManyToManyManagerKeys<TRecord>]: ManyToManyRelationField<TRecord, K>;\n}>;\n\nfunction pkList(): ManyToManyPkListStrategy {\n return { kind: InternalManyToManyReadStrategyKind.PK_LIST };\n}\n\nfunction nested(schema: z.ZodTypeAny): ManyToManyNestedStrategy {\n return { kind: InternalManyToManyReadStrategyKind.NESTED, schema };\n}\n\nfunction slugList<TTarget extends AnyRecord>(\n options: Omit<ManyToManySlugListStrategy<TTarget>, 'kind'>\n): ManyToManySlugListStrategy<TTarget> {\n return {\n kind: InternalManyToManyWriteStrategyKind.SLUG_LIST,\n ...options,\n };\n}\n\nfunction manyToMany(\n // oxlint-disable-next-line typescript/no-explicit-any\n config: Partial<Pick<ManyToManyRelationField<any, any>, 'read' | 'write'>> = {}\n // oxlint-disable-next-line typescript/no-explicit-any\n): ManyToManyRelationField<any, any> {\n return {\n kind: InternalSerializerRelationKind.MANY_TO_MANY,\n read: config.read ?? pkList(),\n write: config.write ?? pkList(),\n };\n}\n\nexport type RelationHelpers = {\n manyToMany: typeof manyToMany;\n pkList: typeof pkList;\n nested: typeof nested;\n slugList: typeof slugList;\n};\n\nexport const relation: RelationHelpers = {\n manyToMany: manyToMany,\n pkList: pkList,\n nested: nested,\n slugList: slugList,\n};\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 SerializerOutputResolver,\n type SerializerOutputResolvers,\n type SerializerUpdateInput,\n type SerializerOutput,\n type SerializerSchema,\n} from './Serializer';\nexport {\n ModelSerializer,\n type ModelSerializerClass,\n type AnyModelSerializer,\n type AnyModelSerializerClass,\n} from './ModelSerializer';\nexport {\n relation,\n type ModelSerializerRelationFields,\n type ManyToManyManagerKeys,\n type ManyToManyRelationField,\n type ManyToManyReadStrategy,\n type ManyToManyWriteStrategy,\n} from './relation';\n"],"mappings":";;;;;AAgCA,MAAM,SAAS,UAAU,4BAA4B;AACrD,IAAI,4CAA4C;;;;;;;;AAShD,IAAsB,aAAtB,MAKE;CACE,OAAgB,eAAiC,EAAE,QAAQ;CAC3D,OAAgB,eAAiC,EAAE,QAAQ;CAC3D,OAAgB,eAAiC,EAAE,QAAQ;CAC3D,OAAgB,kBAGE,KAAA;;;;CAKlB,qBAA4F;EACxF,OAAO,KAAK;CAChB;;;;CAKA,kBAAiC;EAC7B,OAAO,KAAK,mBAAmB,EAAE;CACrC;;;;CAKA,kBAAiC;EAC7B,OAAO,KAAK,mBAAmB,EAAE;CACrC;;;;CAKA,kBAAiC;EAC7B,OAAO,KAAK,mBAAmB,EAAE;CACrC;;;;;CAMA,qBAAyD;EACrD,OAAO,KAAK,mBAAmB,EAAE,mBAAmB,CAAC;CACzD;;;;CAKA,kBAAkB,OAAyC;EACvD,OAAO,KAAK,gBAAgB,EAAE,MAAM,KAAK;CAC7C;;;;CAKA,kBAAkB,OAAyC;EACvD,OAAO,KAAK,gBAAgB,EAAE,MAAM,KAAK;CAC7C;;;;;;;CAQA,iBAAiB,QAA0C;EACvD,IAAI,CAAC,2CAA2C;GAC5C,4CAA4C;GAC5C,OAAO,KACH,gIACJ;EACJ;EACA,OAAO,KAAK,gBAAgB,EAAE,MAAM,MAAM;CAC9C;;;;;CAMA,MAAM,UAAU,QAAmD;EAC/D,OAAO,KAAK,gBAAgB,EAAE,MAAM,MAAM,KAAK,qBAAqB,MAAM,CAAC;CAC/E;;;;CAKA,MAAM,cAAc,SAAiE;EACjF,OAAO,QAAQ,IAAI,QAAQ,KAAK,WAAW,KAAK,UAAU,MAAM,CAAC,CAAC;CACtE;CAEA,MAAc,qBAAqB,QAAmC;EAClE,MAAM,YAAY,KAAK,mBAAmB;EAC1C,MAAM,kBAAkB,OAAO,QAAQ,SAAS;EAEhD,IAAI,gBAAgB,WAAW,KAAK,OAAO,WAAW,YAAY,WAAW,MACzE,OAAO;EAGX,MAAM,WAAW,MAAM,QAAQ,IAC3B,gBAAgB,IAAI,OAAO,CAAC,KAAK,cAAc,CAAC,KAAK,MAAM,SAAS,MAAM,CAAC,CAAU,CACzF;EAEA,OAAO;GACH,GAAI;GACJ,GAAG,OAAO,YAAY,QAAQ;EAClC;CACJ;AACJ;;;AC1JA,MAAa,iCAAiC,EAC1C,cAAc,aAClB;AAEA,MAAa,qCAAqC;CAC9C,SAAS;CACT,QAAQ;AACZ;AAEA,MAAa,sCAAsC;CAC/C,SAAS;CACT,WAAW;AACf;;;;;;ACsCA,IAAsB,kBAAtB,cAMU,WAAsE;CAC5E,OAAgB;CAChB,OAAgB,iBAAqF,KAAA;;;;CAKrG,WAAoD;EAChD,MAAM,QACF,KAAK,YAGP;EAEF,IAAI,CAAC,OACD,MAAM,IAAI,MAAM,GAAG,KAAK,YAAY,KAAK,oDAAoD;EAGjG,OAAO;CACX;;;;CAKA,aAAwC;EACpC,OAAO,KAAK,SAAS,EAAE;CAC3B;;;;CAKA,oBAAiE;EAC7D,OAEQ,KAAK,YAGP,kBAAkB,CAAC;CAE7B;;;;CAKA,qBAAuE;EACnE,MAAM,gBAAgB,MAAM,mBAAmB;EAC/C,MAAM,iBAAiB,KAAK,kBAAkB;EAQ9C,OAAO;GACH,GARsB,OAAO,YAC7B,OAAO,QAAQ,cAAc,EAAE,KAAK,CAAC,WAAW,WAAW,CACvD,WACA,OAAO,WAAyB,KAAK,uBAAuB,QAAQ,WAAW,KAAK,CACxF,CAAC,CAIkB;GACnB,GAAG;EACP;CACJ;;;;CAKA,MAAM,OACF,OAGF;EACE,MAAM,YAAY,KAAK,kBAAkB,KAAK;EAC9C,MAAM,iBAAiB,KAAK,sBAAsB,WAAW,KAAK;EAClE,MAAM,WAAW,MAAM,KAAK,aAAa,SAAS;EAClD,MAAM,UAAU,MAAM,KAAK,WAAW,EAAE,OAAO,KAAK,oBAAoB,QAAQ,CAAC;EACjF,MAAM,KAAK,oBAAoB,SAAS,cAAc;EACtD,OAAO,KAAK,UAAU,OAAO;CACjC;;;;CAKA,MAAM,OACF,IACA,OAGF;EACE,MAAM,YAAY,KAAK,kBAAkB,KAAK;EAC9C,MAAM,iBAAiB,KAAK,sBAAsB,WAAW,KAAK;EAClE,MAAM,WAAW,MAAM,KAAK,aAAa,IAAI,SAAS;EACtD,MAAM,UAAU,MAAM,KAAK,WAAW,EAAE,OAAO,IAAI,KAAK,oBAAoB,QAAQ,CAAC;EACrF,MAAM,KAAK,oBAAoB,SAAS,cAAc;EACtD,OAAO,KAAK,UAAU,OAAO;CACjC;;;;;;;;CASA,MAAgB,aACZ,MAG8B;EAC9B,OAAO;CACX;;;;;;;;CASA,MAAgB,aACZ,KACA,MAG8B;EAC9B,OAAO;CACX;CAEA,sBACI,MACA,UAC6D;EAC7D,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,OAAO,aAAa,YAAY,aAAa,MAC1F,OAAO,CAAC;EAGZ,MAAM,iBAAiB,KAAK,kBAAkB;EAC9C,MAAM,SAAwE,CAAC;EAE/E,KAAK,MAAM,aAAa,OAAO,KAAK,cAAc,GAC9C,IAAI,aAAa,UACb,OAAO,aAAc,KAAiC;EAI9D,OAAO;CACX;CAEA,oBAA4B,MAAoD;EAC5E,MAAM,qBAAqB,IAAI,IAAI,OAAO,KAAK,KAAK,kBAAkB,CAAC,CAAC;EACxE,IAAI,mBAAmB,SAAS,GAC5B,OAAO;EAGX,OAAO,OAAO,YACV,OAAO,QAAQ,IAA+B,EAAE,QAAQ,CAAC,eAAe,CAAC,mBAAmB,IAAI,SAAS,CAAC,CAC9G;CACJ;CAEA,MAAc,oBACV,QACA,QACa;EACb,MAAM,iBAAiB,KAAK,kBAAkB;EAE9C,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,MAAM,GAEnD;GAEC,IAAI,CADU,eAAe,YAEzB;GAEJ,MAAM,KAAK,uBAAuB,QAAQ,WAAW,KAAK;EAC9D;CACJ;CAEA,MAAc,uBACV,QACA,WACA,OACgB;EAEhB,MAAM,QAAQ,MADE,KAAK,qBAAqB,QAAQ,SACxB,EAAE,IAAI,EAAE,MAAM,GAAG;EAC3C,MAAM,eAAe,KAAK,0BAA0B,SAAS;EAE7D,QAAQ,MAAM,KAAK,MAAnB;GACI,KAAK,mCAAmC,SACpC,OAAO,KAAK,KAAK,QAAQ,IAAI,aAAa,iBAAiB;GAC/D,KAAK,mCAAmC,QACpC,OAAO,KAAK,KAAK,QACZ,MAAM,KAA4D,OAAO,MAAM,GAAG,CACvF;EACR;CACJ;CAEA,MAAc,uBACV,QACA,WACA,OACa;EACb,IAAI,UAAU,KAAA,GACV;EAGJ,MAAM,UAAU,KAAK,qBAAqB,QAAQ,SAAS;EAC3D,MAAM,QAAS,KAAK,kBAAkB,EAAuC;EAC7E,IAAI,CAAC,OACD;EAGJ,MAAM,cAAc,MAAM,KAAK,oBAAoB,WAAW,MAAM,OAAO,KAAK;EAChF,MAAM,QAAQ,IAAI,GAAG,WAAW;CACpC;CAEA,MAAc,oBACV,WACA,UACA,OACgE;EAChE,QAAQ,SAAS,MAAjB;GACI,KAAK,oCAAoC;IACrC,IAAI,CAAC,MAAM,QAAQ,KAAK,GACpB,MAAM,IAAI,UACN,mBAAmB,OAAO,SAAS,EAAE,0CACzC;IAEJ,OAAO;GACX,KAAK,oCAAoC,WAAW;IAChD,IAAI,CAAC,MAAM,QAAQ,KAAK,GACpB,MAAM,IAAI,UAAU,mBAAmB,OAAO,SAAS,EAAE,qCAAqC;IAGlG,MAAM,eAAe,CAAC,GAAG,IAAI,IAAI,MAAM,KAAK,UAAU,OAAO,KAAK,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,CAAC,CAAC;IAC5F,IAAI,aAAa,WAAW,GACxB,OAAO,CAAC;IAGZ,MAAM,SAAS,GACV,GAAG,SAAS,YAAY,QAAQ,aACrC;IACA,MAAM,WAAW,MAAM,SAAS,MAAM,QAAQ,MAAM,EAAE,OAAO,MAAM,EAAE,MAAM;IAC3E,MAAM,WAAW,IAAI,IACjB,SAAS,QAAQ,KAAK,QAAQ,CAAC,OAAQ,IAAgC,SAAS,YAAY,GAAG,GAAG,CAAC,CACvG;IACA,MAAM,WAAsC,CAAC;IAE7C,KAAK,MAAM,eAAe,cAAc;KACpC,MAAM,QAAQ,SAAS,IAAI,WAAW;KACtC,IAAI,OAAO;MACP,SAAS,KAAK,KAAK;MACnB;KACJ;KAEA,IAAI,CAAC,SAAS,iBACV,MAAM,IAAI,MACN,mBAAmB,OAAO,SAAS,EAAE,uBAAuB,YAAY,SAAS,SAAS,YAAY,GAC1G;KAGJ,MAAM,UAAU,MAAM,SAAS,MAAM,QAAQ,OACzC,SAAS,mBAAmB,WAAW,KAClC,GAAG,SAAS,cAAc,YAAY,CAC/C;KACA,SAAS,KAAK,OAAkC;IACpD;IAEA,OAAO;GACX;EACJ;CACJ;CAEA,qBACI,QACA,WACiD;EACjD,MAAM,UAAW,OAAmC;EACpD,IAAI,CAAC,yBAAyB,2BAA2B,OAAO,GAC5D,MAAM,IAAI,MAAM,mBAAmB,UAAU,mDAAmD;EAEpG,OAAO;CACX;CAEA,0BACI,WACgF;EAChF,MAAM,WAAW,KAAK,WAAW,EAAE,KAAK,YAAY;EACpD,IAAI,CAAC,YAAY,SAAS,SAAS,+BAA+B,cAC9D,MAAM,IAAI,MAAM,mBAAmB,UAAU,wCAAwC;EAEzF,OAAO;CACX;AACJ;;;ACjSA,SAAS,SAAmC;CACxC,OAAO,EAAE,MAAM,mCAAmC,QAAQ;AAC9D;AAEA,SAAS,OAAO,QAAgD;CAC5D,OAAO;EAAE,MAAM,mCAAmC;EAAQ;CAAO;AACrE;AAEA,SAAS,SACL,SACmC;CACnC,OAAO;EACH,MAAM,oCAAoC;EAC1C,GAAG;CACP;AACJ;AAEA,SAAS,WAEL,SAA6E,CAAC,GAE7C;CACjC,OAAO;EACH,MAAM,+BAA+B;EACrC,MAAM,OAAO,QAAQ,OAAO;EAC5B,OAAO,OAAO,SAAS,OAAO;CAClC;AACJ;AASA,MAAa,WAA4B;CACzB;CACJ;CACA;CACE;AACd"}
1
+ {"version":3,"file":"serializer-RSwlXWls.js","names":[],"sources":["../src/serializer/Serializer.ts","../src/serializer/internal/InternalSerializerRelationKind.ts","../src/serializer/ModelSerializer.ts","../src/serializer/relation.ts","../src/serializer/index.ts"],"sourcesContent":["import { getLogger } from '@danceroutine/tango-core';\nimport { 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 TRecord = unknown,\n> = {\n new (): Serializer<TCreateSchema, TUpdateSchema, TOutputSchema, TRecord>;\n readonly createSchema: TCreateSchema;\n readonly updateSchema: TUpdateSchema;\n readonly outputSchema: TOutputSchema;\n readonly outputResolvers?: SerializerOutputResolvers<TRecord>;\n};\n\nexport type AnySerializerClass = SerializerClass<\n SerializerSchema,\n SerializerSchema,\n SerializerSchema,\n // oxlint-disable-next-line typescript/no-explicit-any\n any\n>;\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']>;\nexport type SerializerOutputResolver<TRecord = unknown> = (record: TRecord) => unknown | Promise<unknown>;\nexport type SerializerOutputResolvers<TRecord = unknown> = Record<string, SerializerOutputResolver<TRecord>>;\n\nconst logger = getLogger('tango.resources.serializer');\nlet hasWarnedAboutToRepresentationDeprecation = false;\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 TRecord = unknown,\n> {\n static readonly createSchema: SerializerSchema = z.unknown();\n static readonly updateSchema: SerializerSchema = z.unknown();\n static readonly outputSchema: SerializerSchema = z.unknown();\n static readonly outputResolvers:\n | SerializerOutputResolvers<// oxlint-disable-next-line typescript/no-explicit-any\n any>\n | undefined = undefined;\n\n /**\n * Return the serializer class for the current instance.\n */\n getSerializerClass(): SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema, TRecord> {\n return this.constructor as SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema, TRecord>;\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 * Return the resolver map used to enrich serializer output fields before\n * the outward Zod schema parses the final response shape.\n */\n getOutputResolvers(): SerializerOutputResolvers<TRecord> {\n return this.getSerializerClass().outputResolvers ?? {};\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 * @deprecated Use `serialize(...)` instead so serializer-owned output\n * resolvers run before the outward Zod schema parses the response shape.\n */\n toRepresentation(record: TRecord): z.output<TOutputSchema> {\n if (!hasWarnedAboutToRepresentationDeprecation) {\n hasWarnedAboutToRepresentationDeprecation = true;\n logger.warn(\n '`Serializer.toRepresentation(...)` is deprecated. Use `serialize(...)` instead so output resolvers run before outward parsing.'\n );\n }\n return this.getOutputSchema().parse(record);\n }\n\n /**\n * Resolve serializer-owned output fields and parse the outward response\n * contract.\n */\n async serialize(record: TRecord): Promise<z.output<TOutputSchema>> {\n return this.getOutputSchema().parse(await this.applyOutputResolvers(record));\n }\n\n /**\n * Serialize many records through the same outward response contract.\n */\n async serializeMany(records: readonly TRecord[]): Promise<z.output<TOutputSchema>[]> {\n return Promise.all(records.map((record) => this.serialize(record)));\n }\n\n private async applyOutputResolvers(record: TRecord): Promise<unknown> {\n const resolvers = this.getOutputResolvers();\n const resolverEntries = Object.entries(resolvers);\n\n if (resolverEntries.length === 0 || typeof record !== 'object' || record === null) {\n return record;\n }\n\n const resolved = await Promise.all(\n resolverEntries.map(async ([key, resolver]) => [key, await resolver(record)] as const)\n );\n\n return {\n ...(record as Record<string, unknown>),\n ...Object.fromEntries(resolved),\n };\n }\n}\n","export const InternalSerializerRelationKind = {\n MANY_TO_MANY: 'manyToMany',\n} as const;\n\nexport const InternalManyToManyReadStrategyKind = {\n PK_LIST: 'pkList',\n NESTED: 'nested',\n} as const;\n\nexport const InternalManyToManyWriteStrategyKind = {\n PK_LIST: 'pkList',\n SLUG_LIST: 'slugList',\n} as const;\n","import type { FilterInput, ManagerLike, ManyToManyTargetRef } from '@danceroutine/tango-orm';\nimport { ManyToManyRelatedManager } 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 SerializerOutputResolvers,\n type SerializerSchema,\n type SerializerUpdateInput,\n} from './Serializer';\nimport type { ManyToManyManagerKeys, ManyToManyRelationField, ModelSerializerRelationFields } from './relation';\nimport {\n InternalManyToManyReadStrategyKind,\n InternalManyToManyWriteStrategyKind,\n InternalSerializerRelationKind,\n} from './internal/InternalSerializerRelationKind';\n\n// oxlint-disable-next-line typescript/no-explicit-any\ntype AnyRelationField = ManyToManyRelationField<any, any>;\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 TModelRecord extends Record<string, unknown> = TModel,\n> = SerializerClass<TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord> & {\n new (): ModelSerializer<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>;\n readonly model?: ResourceModelLike<TModel, TModelRecord>;\n readonly relationFields?: ModelSerializerRelationFields<TModelRecord>;\n};\n\n//\nexport type AnyModelSerializer<TModel extends Record<string, unknown> = Record<string, unknown>> = ModelSerializerClass<\n TModel,\n SerializerSchema,\n SerializerSchema,\n SerializerSchema,\n // oxlint-disable-next-line typescript/no-explicit-any\n any\n>;\n\nexport type AnyModelSerializerClass = AnyModelSerializer;\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 TModelRecord extends Record<string, unknown> = TModel,\n> extends Serializer<TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord> {\n static readonly model?: unknown;\n static readonly relationFields: ModelSerializerRelationFields<Record<string, unknown>> | undefined = undefined;\n\n /**\n * Return the Tango model backing this serializer.\n */\n getModel(): ResourceModelLike<TModel, TModelRecord> {\n const model = (\n this.constructor as Partial<\n ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>\n >\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<TModelRecord> {\n return this.getModel().objects as ManagerLike<TModelRecord>;\n }\n\n /**\n * Return the declarative relation-field map for this serializer.\n */\n getRelationFields(): ModelSerializerRelationFields<TModelRecord> {\n return (\n (\n this.constructor as Partial<\n ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>\n >\n ).relationFields ?? {}\n );\n }\n\n /**\n * Merge relation-field read resolvers into the serializer output path.\n */\n override getOutputResolvers(): SerializerOutputResolvers<TModelRecord> {\n const baseResolvers = super.getOutputResolvers();\n const relationFields = this.getRelationFields() as Record<string, AnyRelationField>;\n const relationResolvers = Object.fromEntries(\n Object.entries(relationFields).map(([fieldName, field]) => [\n fieldName,\n async (record: TModelRecord) => this.serializeRelationField(record, fieldName, field),\n ])\n ) as SerializerOutputResolvers<TModelRecord>;\n\n return {\n ...relationResolvers,\n ...baseResolvers,\n };\n }\n\n /**\n * Validate, enrich, persist, and serialize a create workflow.\n */\n async create(\n input: unknown\n ): Promise<\n SerializerOutput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>>\n > {\n const validated = this.deserializeCreate(input);\n const relationWrites = this.extractRelationWrites(validated, input);\n const prepared = await this.beforeCreate(validated);\n const created = await this.getManager().create(this.stripRelationFields(prepared));\n await this.applyRelationWrites(created, relationWrites);\n return this.serialize(created);\n }\n\n /**\n * Validate, enrich, persist, and serialize an update workflow.\n */\n async update(\n id: TModelRecord[keyof TModelRecord],\n input: unknown\n ): Promise<\n SerializerOutput<ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>>\n > {\n const validated = this.deserializeUpdate(input);\n const relationWrites = this.extractRelationWrites(validated, input);\n const prepared = await this.beforeUpdate(id, validated);\n const updated = await this.getManager().update(id, this.stripRelationFields(prepared));\n await this.applyRelationWrites(updated, relationWrites);\n return this.serialize(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<\n ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>\n >\n ): Promise<Partial<TModelRecord>> {\n return data as Partial<TModelRecord>;\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: TModelRecord[keyof TModelRecord],\n data: SerializerUpdateInput<\n ModelSerializerClass<TModel, TCreateSchema, TUpdateSchema, TOutputSchema, TModelRecord>\n >\n ): Promise<Partial<TModelRecord>> {\n return data as Partial<TModelRecord>;\n }\n\n private extractRelationWrites(\n data: unknown,\n rawInput: unknown\n ): Partial<Record<ManyToManyManagerKeys<TModelRecord>, unknown>> {\n if (typeof data !== 'object' || data === null || typeof rawInput !== 'object' || rawInput === null) {\n return {};\n }\n\n const relationFields = this.getRelationFields() as Record<string, AnyRelationField>;\n const writes: Partial<Record<ManyToManyManagerKeys<TModelRecord>, unknown>> = {};\n\n for (const fieldName of Object.keys(relationFields) as ManyToManyManagerKeys<TModelRecord>[]) {\n if (fieldName in rawInput) {\n writes[fieldName] = (data as Record<string, unknown>)[fieldName];\n }\n }\n\n return writes;\n }\n\n private stripRelationFields(data: Partial<TModelRecord>): Partial<TModelRecord> {\n const relationFieldNames = new Set(Object.keys(this.getRelationFields()));\n if (relationFieldNames.size === 0) {\n return data;\n }\n\n return Object.fromEntries(\n Object.entries(data as Record<string, unknown>).filter(([fieldName]) => !relationFieldNames.has(fieldName))\n ) as Partial<TModelRecord>;\n }\n\n private async applyRelationWrites(\n record: TModelRecord,\n writes: Partial<Record<ManyToManyManagerKeys<TModelRecord>, unknown>>\n ): Promise<void> {\n const relationFields = this.getRelationFields() as Record<string, AnyRelationField>;\n\n for (const [fieldName, value] of Object.entries(writes) as Array<\n [ManyToManyManagerKeys<TModelRecord>, unknown]\n >) {\n const field = relationFields[fieldName];\n if (!field) {\n continue;\n }\n await this.syncManyToManyRelation(record, fieldName, value);\n }\n }\n\n private async serializeRelationField(\n record: TModelRecord,\n fieldName: string,\n field: AnyRelationField\n ): Promise<unknown> {\n const manager = this.getManyToManyManager(record, fieldName);\n const rows = (await manager.all().fetch()).results;\n const relationMeta = this.getManyToManyRelationMeta(fieldName);\n\n switch (field.read.kind) {\n case InternalManyToManyReadStrategyKind.PK_LIST:\n return rows.map((row) => row[relationMeta.targetPrimaryKey]);\n case InternalManyToManyReadStrategyKind.NESTED:\n return rows.map((row) =>\n (field.read as { schema: { parse: (input: unknown) => unknown } }).schema.parse(row)\n );\n }\n }\n\n private async syncManyToManyRelation(\n record: TModelRecord,\n fieldName: ManyToManyManagerKeys<TModelRecord>,\n value: unknown\n ): Promise<void> {\n if (value === undefined) {\n return;\n }\n\n const manager = this.getManyToManyManager(record, fieldName);\n const field = (this.getRelationFields() as Record<string, AnyRelationField>)[fieldName];\n if (!field) {\n return;\n }\n\n const nextTargets = await this.resolveWriteTargets(fieldName, field.write, value);\n await manager.set(...nextTargets);\n }\n\n private async resolveWriteTargets(\n fieldName: ManyToManyManagerKeys<TModelRecord>,\n strategy: AnyRelationField['write'],\n value: unknown\n ): Promise<readonly ManyToManyTargetRef<Record<string, unknown>>[]> {\n switch (strategy.kind) {\n case InternalManyToManyWriteStrategyKind.PK_LIST:\n if (!Array.isArray(value)) {\n throw new TypeError(\n `Relation field '${String(fieldName)}' expects an array of primary-key values.`\n );\n }\n return value as readonly ManyToManyTargetRef<Record<string, unknown>>[];\n case InternalManyToManyWriteStrategyKind.SLUG_LIST: {\n if (!Array.isArray(value)) {\n throw new TypeError(`Relation field '${String(fieldName)}' expects an array of lookup values.`);\n }\n\n const lookupValues = [...new Set(value.map((entry) => String(entry).trim()).filter(Boolean))];\n if (lookupValues.length === 0) {\n return [];\n }\n\n const filter = {\n [`${strategy.lookupField}__in`]: lookupValues,\n } as FilterInput<Record<string, unknown>>;\n const existing = await strategy.model.objects.query().filter(filter).fetch();\n const byLookup = new Map(\n existing.results.map((row) => [String((row as Record<string, unknown>)[strategy.lookupField]), row])\n );\n const resolved: Record<string, unknown>[] = [];\n\n for (const lookupValue of lookupValues) {\n const found = byLookup.get(lookupValue);\n if (found) {\n resolved.push(found);\n continue;\n }\n\n if (!strategy.createIfMissing) {\n throw new Error(\n `Relation field '${String(fieldName)}' could not resolve '${lookupValue}' via '${strategy.lookupField}'.`\n );\n }\n\n const created = await strategy.model.objects.create(\n strategy.buildCreateInput?.(lookupValue) ??\n ({ [strategy.lookupField]: lookupValue } as Record<string, unknown>)\n );\n resolved.push(created as Record<string, unknown>);\n }\n\n return resolved;\n }\n }\n }\n\n private getManyToManyManager(\n record: TModelRecord,\n fieldName: string\n ): ManyToManyRelatedManager<Record<string, unknown>> {\n const manager = (record as Record<string, unknown>)[fieldName];\n if (!ManyToManyRelatedManager.isManyToManyRelatedManager(manager)) {\n throw new Error(`Relation field '${fieldName}' is not backed by a many-to-many related manager.`);\n }\n return manager as ManyToManyRelatedManager<Record<string, unknown>>;\n }\n\n private getManyToManyRelationMeta(\n fieldName: string\n ): NonNullable<NonNullable<ManagerLike<TModelRecord>['meta']['relations']>[string]> {\n const relation = this.getManager().meta.relations?.[fieldName];\n if (!relation || relation.kind !== InternalSerializerRelationKind.MANY_TO_MANY) {\n throw new Error(`Relation field '${fieldName}' is not a persisted many-to-many edge.`);\n }\n return relation;\n }\n}\n\nexport type { AnySerializerClass };\n","import { z } from 'zod';\nimport type { ManyToManyRelatedManager } from '@danceroutine/tango-orm';\nimport type { ResourceModelLike } from '../resource/ResourceModelLike';\nimport {\n InternalManyToManyReadStrategyKind,\n InternalManyToManyWriteStrategyKind,\n InternalSerializerRelationKind,\n} from './internal/InternalSerializerRelationKind';\n\ntype AnyRecord = Record<string, unknown>;\n\nexport type ManyToManyManagerKeys<TRecord extends AnyRecord> = Extract<\n {\n [K in keyof TRecord]: TRecord[K] extends ManyToManyRelatedManager<AnyRecord> ? K : never;\n }[keyof TRecord],\n string\n>;\n\nexport type ManyToManyTargetRow<TRecord extends AnyRecord, TFieldName extends ManyToManyManagerKeys<TRecord>> =\n TRecord[TFieldName] extends ManyToManyRelatedManager<infer TTarget extends AnyRecord> ? TTarget : never;\n\nexport type ManyToManyPkListStrategy = {\n kind: typeof InternalManyToManyReadStrategyKind.PK_LIST | typeof InternalManyToManyWriteStrategyKind.PK_LIST;\n};\n\nexport type ManyToManyNestedStrategy = {\n kind: typeof InternalManyToManyReadStrategyKind.NESTED;\n schema: z.ZodTypeAny;\n};\n\nexport type ManyToManySlugListStrategy<TTarget extends AnyRecord> = {\n kind: typeof InternalManyToManyWriteStrategyKind.SLUG_LIST;\n // oxlint-disable-next-line typescript/no-explicit-any\n model: ResourceModelLike<any, any>;\n lookupField: Extract<keyof TTarget, string>;\n createIfMissing?: boolean;\n buildCreateInput?: (value: string) => Partial<TTarget>;\n};\n\nexport type ManyToManyReadStrategy = ManyToManyPkListStrategy | ManyToManyNestedStrategy;\nexport type ManyToManyWriteStrategy<TTarget extends AnyRecord> =\n | ManyToManyPkListStrategy\n | ManyToManySlugListStrategy<TTarget>;\n\nexport type ManyToManyRelationField<TRecord extends AnyRecord, TFieldName extends ManyToManyManagerKeys<TRecord>> = {\n kind: typeof InternalSerializerRelationKind.MANY_TO_MANY;\n read: ManyToManyReadStrategy;\n write: ManyToManyWriteStrategy<ManyToManyTargetRow<TRecord, TFieldName>>;\n};\n\nexport type ModelSerializerRelationFields<TRecord extends AnyRecord> = Partial<{\n [K in ManyToManyManagerKeys<TRecord>]: ManyToManyRelationField<TRecord, K>;\n}>;\n\nfunction pkList(): ManyToManyPkListStrategy {\n return { kind: InternalManyToManyReadStrategyKind.PK_LIST };\n}\n\nfunction nested(schema: z.ZodTypeAny): ManyToManyNestedStrategy {\n return { kind: InternalManyToManyReadStrategyKind.NESTED, schema };\n}\n\nfunction slugList<TTarget extends AnyRecord>(\n options: Omit<ManyToManySlugListStrategy<TTarget>, 'kind'>\n): ManyToManySlugListStrategy<TTarget> {\n return {\n kind: InternalManyToManyWriteStrategyKind.SLUG_LIST,\n ...options,\n };\n}\n\nfunction manyToMany(\n // oxlint-disable-next-line typescript/no-explicit-any\n config: Partial<Pick<ManyToManyRelationField<any, any>, 'read' | 'write'>> = {}\n // oxlint-disable-next-line typescript/no-explicit-any\n): ManyToManyRelationField<any, any> {\n return {\n kind: InternalSerializerRelationKind.MANY_TO_MANY,\n read: config.read ?? pkList(),\n write: config.write ?? pkList(),\n };\n}\n\nexport type RelationHelpers = {\n manyToMany: typeof manyToMany;\n pkList: typeof pkList;\n nested: typeof nested;\n slugList: typeof slugList;\n};\n\nexport const relation: RelationHelpers = {\n manyToMany: manyToMany,\n pkList: pkList,\n nested: nested,\n slugList: slugList,\n};\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 SerializerOutputResolver,\n type SerializerOutputResolvers,\n type SerializerUpdateInput,\n type SerializerOutput,\n type SerializerSchema,\n} from './Serializer';\nexport {\n ModelSerializer,\n type ModelSerializerClass,\n type AnyModelSerializer,\n type AnyModelSerializerClass,\n} from './ModelSerializer';\nexport {\n relation,\n type ModelSerializerRelationFields,\n type ManyToManyManagerKeys,\n type ManyToManyRelationField,\n type ManyToManyReadStrategy,\n type ManyToManyWriteStrategy,\n} from './relation';\n"],"mappings":";;;;;AAgCA,MAAM,SAAS,UAAU,4BAA4B;AACrD,IAAI,4CAA4C;;;;;;;;AAShD,IAAsB,aAAtB,MAKE;CACE,OAAgB,eAAiC,EAAE,QAAQ;CAC3D,OAAgB,eAAiC,EAAE,QAAQ;CAC3D,OAAgB,eAAiC,EAAE,QAAQ;CAC3D,OAAgB,kBAGE,KAAA;;;;CAKlB,qBAA4F;EACxF,OAAO,KAAK;CAChB;;;;CAKA,kBAAiC;EAC7B,OAAO,KAAK,mBAAmB,CAAC,CAAC;CACrC;;;;CAKA,kBAAiC;EAC7B,OAAO,KAAK,mBAAmB,CAAC,CAAC;CACrC;;;;CAKA,kBAAiC;EAC7B,OAAO,KAAK,mBAAmB,CAAC,CAAC;CACrC;;;;;CAMA,qBAAyD;EACrD,OAAO,KAAK,mBAAmB,CAAC,CAAC,mBAAmB,CAAC;CACzD;;;;CAKA,kBAAkB,OAAyC;EACvD,OAAO,KAAK,gBAAgB,CAAC,CAAC,MAAM,KAAK;CAC7C;;;;CAKA,kBAAkB,OAAyC;EACvD,OAAO,KAAK,gBAAgB,CAAC,CAAC,MAAM,KAAK;CAC7C;;;;;;;CAQA,iBAAiB,QAA0C;EACvD,IAAI,CAAC,2CAA2C;GAC5C,4CAA4C;GAC5C,OAAO,KACH,gIACJ;EACJ;EACA,OAAO,KAAK,gBAAgB,CAAC,CAAC,MAAM,MAAM;CAC9C;;;;;CAMA,MAAM,UAAU,QAAmD;EAC/D,OAAO,KAAK,gBAAgB,CAAC,CAAC,MAAM,MAAM,KAAK,qBAAqB,MAAM,CAAC;CAC/E;;;;CAKA,MAAM,cAAc,SAAiE;EACjF,OAAO,QAAQ,IAAI,QAAQ,KAAK,WAAW,KAAK,UAAU,MAAM,CAAC,CAAC;CACtE;CAEA,MAAc,qBAAqB,QAAmC;EAClE,MAAM,YAAY,KAAK,mBAAmB;EAC1C,MAAM,kBAAkB,OAAO,QAAQ,SAAS;EAEhD,IAAI,gBAAgB,WAAW,KAAK,OAAO,WAAW,YAAY,WAAW,MACzE,OAAO;EAGX,MAAM,WAAW,MAAM,QAAQ,IAC3B,gBAAgB,IAAI,OAAO,CAAC,KAAK,cAAc,CAAC,KAAK,MAAM,SAAS,MAAM,CAAC,CAAU,CACzF;EAEA,OAAO;GACH,GAAI;GACJ,GAAG,OAAO,YAAY,QAAQ;EAClC;CACJ;AACJ;;;AC1JA,MAAa,iCAAiC,EAC1C,cAAc,aAClB;AAEA,MAAa,qCAAqC;CAC9C,SAAS;CACT,QAAQ;AACZ;AAEA,MAAa,sCAAsC;CAC/C,SAAS;CACT,WAAW;AACf;;;;;;ACsCA,IAAsB,kBAAtB,cAMU,WAAsE;CAC5E,OAAgB;CAChB,OAAgB,iBAAqF,KAAA;;;;CAKrG,WAAoD;EAChD,MAAM,QACF,KAAK,YAGP;EAEF,IAAI,CAAC,OACD,MAAM,IAAI,MAAM,GAAG,KAAK,YAAY,KAAK,oDAAoD;EAGjG,OAAO;CACX;;;;CAKA,aAAwC;EACpC,OAAO,KAAK,SAAS,CAAC,CAAC;CAC3B;;;;CAKA,oBAAiE;EAC7D,OAEQ,KAAK,YAGP,kBAAkB,CAAC;CAE7B;;;;CAKA,qBAAuE;EACnE,MAAM,gBAAgB,MAAM,mBAAmB;EAC/C,MAAM,iBAAiB,KAAK,kBAAkB;EAQ9C,OAAO;GACH,GARsB,OAAO,YAC7B,OAAO,QAAQ,cAAc,CAAC,CAAC,KAAK,CAAC,WAAW,WAAW,CACvD,WACA,OAAO,WAAyB,KAAK,uBAAuB,QAAQ,WAAW,KAAK,CACxF,CAAC,CAIkB;GACnB,GAAG;EACP;CACJ;;;;CAKA,MAAM,OACF,OAGF;EACE,MAAM,YAAY,KAAK,kBAAkB,KAAK;EAC9C,MAAM,iBAAiB,KAAK,sBAAsB,WAAW,KAAK;EAClE,MAAM,WAAW,MAAM,KAAK,aAAa,SAAS;EAClD,MAAM,UAAU,MAAM,KAAK,WAAW,CAAC,CAAC,OAAO,KAAK,oBAAoB,QAAQ,CAAC;EACjF,MAAM,KAAK,oBAAoB,SAAS,cAAc;EACtD,OAAO,KAAK,UAAU,OAAO;CACjC;;;;CAKA,MAAM,OACF,IACA,OAGF;EACE,MAAM,YAAY,KAAK,kBAAkB,KAAK;EAC9C,MAAM,iBAAiB,KAAK,sBAAsB,WAAW,KAAK;EAClE,MAAM,WAAW,MAAM,KAAK,aAAa,IAAI,SAAS;EACtD,MAAM,UAAU,MAAM,KAAK,WAAW,CAAC,CAAC,OAAO,IAAI,KAAK,oBAAoB,QAAQ,CAAC;EACrF,MAAM,KAAK,oBAAoB,SAAS,cAAc;EACtD,OAAO,KAAK,UAAU,OAAO;CACjC;;;;;;;;CASA,MAAgB,aACZ,MAG8B;EAC9B,OAAO;CACX;;;;;;;;CASA,MAAgB,aACZ,KACA,MAG8B;EAC9B,OAAO;CACX;CAEA,sBACI,MACA,UAC6D;EAC7D,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,OAAO,aAAa,YAAY,aAAa,MAC1F,OAAO,CAAC;EAGZ,MAAM,iBAAiB,KAAK,kBAAkB;EAC9C,MAAM,SAAwE,CAAC;EAE/E,KAAK,MAAM,aAAa,OAAO,KAAK,cAAc,GAC9C,IAAI,aAAa,UACb,OAAO,aAAc,KAAiC;EAI9D,OAAO;CACX;CAEA,oBAA4B,MAAoD;EAC5E,MAAM,qBAAqB,IAAI,IAAI,OAAO,KAAK,KAAK,kBAAkB,CAAC,CAAC;EACxE,IAAI,mBAAmB,SAAS,GAC5B,OAAO;EAGX,OAAO,OAAO,YACV,OAAO,QAAQ,IAA+B,CAAC,CAAC,QAAQ,CAAC,eAAe,CAAC,mBAAmB,IAAI,SAAS,CAAC,CAC9G;CACJ;CAEA,MAAc,oBACV,QACA,QACa;EACb,MAAM,iBAAiB,KAAK,kBAAkB;EAE9C,KAAK,MAAM,CAAC,WAAW,UAAU,OAAO,QAAQ,MAAM,GAEnD;GAEC,IAAI,CADU,eAAe,YAEzB;GAEJ,MAAM,KAAK,uBAAuB,QAAQ,WAAW,KAAK;EAC9D;CACJ;CAEA,MAAc,uBACV,QACA,WACA,OACgB;EAEhB,MAAM,QAAQ,MADE,KAAK,qBAAqB,QAAQ,SACxB,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAA,CAAG;EAC3C,MAAM,eAAe,KAAK,0BAA0B,SAAS;EAE7D,QAAQ,MAAM,KAAK,MAAnB;GACI,KAAK,mCAAmC,SACpC,OAAO,KAAK,KAAK,QAAQ,IAAI,aAAa,iBAAiB;GAC/D,KAAK,mCAAmC,QACpC,OAAO,KAAK,KAAK,QACZ,MAAM,KAA4D,OAAO,MAAM,GAAG,CACvF;EACR;CACJ;CAEA,MAAc,uBACV,QACA,WACA,OACa;EACb,IAAI,UAAU,KAAA,GACV;EAGJ,MAAM,UAAU,KAAK,qBAAqB,QAAQ,SAAS;EAC3D,MAAM,QAAS,KAAK,kBAAkB,CAAC,CAAsC;EAC7E,IAAI,CAAC,OACD;EAGJ,MAAM,cAAc,MAAM,KAAK,oBAAoB,WAAW,MAAM,OAAO,KAAK;EAChF,MAAM,QAAQ,IAAI,GAAG,WAAW;CACpC;CAEA,MAAc,oBACV,WACA,UACA,OACgE;EAChE,QAAQ,SAAS,MAAjB;GACI,KAAK,oCAAoC;IACrC,IAAI,CAAC,MAAM,QAAQ,KAAK,GACpB,MAAM,IAAI,UACN,mBAAmB,OAAO,SAAS,EAAE,0CACzC;IAEJ,OAAO;GACX,KAAK,oCAAoC,WAAW;IAChD,IAAI,CAAC,MAAM,QAAQ,KAAK,GACpB,MAAM,IAAI,UAAU,mBAAmB,OAAO,SAAS,EAAE,qCAAqC;IAGlG,MAAM,eAAe,CAAC,GAAG,IAAI,IAAI,MAAM,KAAK,UAAU,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC;IAC5F,IAAI,aAAa,WAAW,GACxB,OAAO,CAAC;IAGZ,MAAM,SAAS,GACV,GAAG,SAAS,YAAY,QAAQ,aACrC;IACA,MAAM,WAAW,MAAM,SAAS,MAAM,QAAQ,MAAM,CAAC,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM;IAC3E,MAAM,WAAW,IAAI,IACjB,SAAS,QAAQ,KAAK,QAAQ,CAAC,OAAQ,IAAgC,SAAS,YAAY,GAAG,GAAG,CAAC,CACvG;IACA,MAAM,WAAsC,CAAC;IAE7C,KAAK,MAAM,eAAe,cAAc;KACpC,MAAM,QAAQ,SAAS,IAAI,WAAW;KACtC,IAAI,OAAO;MACP,SAAS,KAAK,KAAK;MACnB;KACJ;KAEA,IAAI,CAAC,SAAS,iBACV,MAAM,IAAI,MACN,mBAAmB,OAAO,SAAS,EAAE,uBAAuB,YAAY,SAAS,SAAS,YAAY,GAC1G;KAGJ,MAAM,UAAU,MAAM,SAAS,MAAM,QAAQ,OACzC,SAAS,mBAAmB,WAAW,KAClC,GAAG,SAAS,cAAc,YAAY,CAC/C;KACA,SAAS,KAAK,OAAkC;IACpD;IAEA,OAAO;GACX;EACJ;CACJ;CAEA,qBACI,QACA,WACiD;EACjD,MAAM,UAAW,OAAmC;EACpD,IAAI,CAAC,yBAAyB,2BAA2B,OAAO,GAC5D,MAAM,IAAI,MAAM,mBAAmB,UAAU,mDAAmD;EAEpG,OAAO;CACX;CAEA,0BACI,WACgF;EAChF,MAAM,WAAW,KAAK,WAAW,CAAC,CAAC,KAAK,YAAY;EACpD,IAAI,CAAC,YAAY,SAAS,SAAS,+BAA+B,cAC9D,MAAM,IAAI,MAAM,mBAAmB,UAAU,wCAAwC;EAEzF,OAAO;CACX;AACJ;;;ACjSA,SAAS,SAAmC;CACxC,OAAO,EAAE,MAAM,mCAAmC,QAAQ;AAC9D;AAEA,SAAS,OAAO,QAAgD;CAC5D,OAAO;EAAE,MAAM,mCAAmC;EAAQ;CAAO;AACrE;AAEA,SAAS,SACL,SACmC;CACnC,OAAO;EACH,MAAM,oCAAoC;EAC1C,GAAG;CACP;AACJ;AAEA,SAAS,WAEL,SAA6E,CAAC,GAE7C;CACjC,OAAO;EACH,MAAM,+BAA+B;EACrC,MAAM,OAAO,QAAQ,OAAO;EAC5B,OAAO,OAAO,SAAS,OAAO;CAClC;AACJ;AASA,MAAa,WAA4B;CACzB;CACJ;CACA;CACE;AACd"}
@@ -1 +1 @@
1
- {"version":3,"file":"view-CYdJAO4t.js","names":[],"sources":["../src/view/APIView.ts","../src/view/GenericAPIView.ts","../src/view/mixins/ListModelMixin.ts","../src/view/mixins/CreateModelMixin.ts","../src/view/mixins/RetrieveModelMixin.ts","../src/view/mixins/UpdateModelMixin.ts","../src/view/mixins/DestroyModelMixin.ts","../src/view/generics/ListAPIView.ts","../src/view/generics/CreateAPIView.ts","../src/view/generics/RetrieveAPIView.ts","../src/view/generics/ListCreateAPIView.ts","../src/view/generics/RetrieveUpdateAPIView.ts","../src/view/generics/RetrieveDestroyAPIView.ts","../src/view/generics/RetrieveUpdateDestroyAPIView.ts","../src/view/index.ts"],"sourcesContent":["import { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../context/index';\n\nexport type APIViewMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\ntype APIViewMethodHandler = (ctx: RequestContext) => Promise<TangoResponse>;\n\n/**\n * Lightweight class-based request dispatcher for non-model API endpoints.\n */\nexport abstract class APIView {\n static readonly BRAND = 'tango.resources.api_view' as const;\n readonly __tangoBrand: typeof APIView.BRAND = APIView.BRAND;\n\n /**\n * Narrow an unknown value to `APIView`.\n */\n static isAPIView(value: unknown): value is APIView {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === APIView.BRAND\n );\n }\n\n /**\n * Dispatch the request to the handler for the current HTTP method.\n */\n async dispatch(ctx: RequestContext): Promise<TangoResponse> {\n const method = normalizeMethod(ctx.request.method);\n if (!method) {\n return this.httpMethodNotAllowed();\n }\n\n const handler = this.getMethodHandler(method);\n return handler(ctx);\n }\n\n getAllowedMethods(): readonly APIViewMethod[] {\n const allowed: APIViewMethod[] = [];\n if (this.get !== APIView.prototype.get) {\n allowed.push('GET');\n }\n if (this.post !== APIView.prototype.post) {\n allowed.push('POST');\n }\n if (this.put !== APIView.prototype.put) {\n allowed.push('PUT');\n }\n if (this.patch !== APIView.prototype.patch) {\n allowed.push('PATCH');\n }\n if (this.delete !== APIView.prototype.delete) {\n allowed.push('DELETE');\n }\n return allowed;\n }\n\n protected get(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected post(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected put(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected patch(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected delete(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected httpMethodNotAllowed(): TangoResponse {\n return TangoResponse.methodNotAllowed(this.getAllowedMethods());\n }\n\n private getMethodHandler(method: APIViewMethod): APIViewMethodHandler {\n if (method === 'GET') {\n return (ctx) => this.get(ctx);\n }\n if (method === 'POST') {\n return (ctx) => this.post(ctx);\n }\n if (method === 'PUT') {\n return (ctx) => this.put(ctx);\n }\n if (method === 'PATCH') {\n return (ctx) => this.patch(ctx);\n }\n return (ctx) => this.delete(ctx);\n }\n}\n\nfunction normalizeMethod(method: string): APIViewMethod | null {\n const upper = method.toUpperCase();\n if (upper === 'GET') {\n return 'GET';\n }\n if (upper === 'POST') {\n return 'POST';\n }\n if (upper === 'PUT') {\n return 'PUT';\n }\n if (upper === 'PATCH') {\n return 'PATCH';\n }\n if (upper === 'DELETE') {\n return 'DELETE';\n }\n return null;\n}\n","import { HttpErrorFactory, TangoResponse, type JsonValue, NotFoundError } from '@danceroutine/tango-core';\nimport { Q, type FilterInput, type ManagerLike, type QuerySet } from '@danceroutine/tango-orm';\nimport type { OffsetPaginatedResponse, Paginator } from '../pagination/index';\nimport { OffsetPaginator } from '../paginators/OffsetPaginator';\nimport { APIView } from './APIView';\nimport { RequestContext } from '../context/index';\nimport type { FilterSet } from '../filters/index';\nimport { inferModelFieldParsers } from '../filters/inferModelFieldParsers';\nimport type { GenericAPIViewOpenAPIDescription } from '../resource/index';\nimport type { AnyModelSerializer, SerializerOutput } from '../serializer/index';\nimport type { ResourceModelLike } from '../resource/index';\n\ntype SearchFieldRef<TModel extends Record<string, unknown>> = Extract<keyof TModel, string> | string;\n\nexport interface GenericAPIViewConfig<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> {\n serializer: TSerializer;\n filters?: FilterSet<TModel>;\n orderingFields?: (keyof TModel)[];\n searchFields?: SearchFieldRef<TModel>[];\n lookupField?: keyof TModel;\n lookupParam?: string;\n paginatorFactory?: (queryset: QuerySet<TModel>) => Paginator<TModel, SerializerOutput<TSerializer>>;\n}\n\n/**\n * Generic API base class that centralizes query/build/validation helpers.\n */\nexport abstract class GenericAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends APIView {\n protected readonly serializerClass: TSerializer;\n protected readonly filters?: FilterSet<TModel>;\n protected readonly orderingFields: readonly (keyof TModel)[];\n protected readonly searchFields: readonly SearchFieldRef<TModel>[];\n protected readonly lookupField?: keyof TModel;\n protected readonly lookupParam: string;\n protected readonly paginatorFactory?: (\n queryset: QuerySet<TModel>\n ) => Paginator<TModel, SerializerOutput<TSerializer>>;\n private serializer?: InstanceType<TSerializer>;\n\n constructor(config: GenericAPIViewConfig<TModel, TSerializer>) {\n super();\n this.serializerClass = config.serializer;\n this.filters = config.filters;\n this.orderingFields = config.orderingFields ?? [];\n this.searchFields = config.searchFields ?? [];\n this.lookupField = config.lookupField;\n this.lookupParam = config.lookupParam ?? 'id';\n this.paginatorFactory = config.paginatorFactory;\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(): GenericAPIViewOpenAPIDescription<TModel, TSerializer> {\n const model = this.requireModelMetadata();\n return {\n model,\n outputSchema: this.getOutputSchema(),\n createSchema: this.getCreateSchema(),\n updateSchema: this.getUpdateSchema(),\n searchFields: this.searchFields,\n orderingFields: this.orderingFields,\n lookupField: this.lookupField ?? this.getLookupFieldFromMetadata(model),\n lookupParam: this.lookupParam,\n allowedMethods: this.getAllowedMethods(),\n usesDefaultOffsetPagination: !this.paginatorFactory,\n };\n }\n\n protected getManager(): ManagerLike<TModel> {\n return this.getSerializer().getManager();\n }\n\n protected getOutputSchema(): TSerializer['outputSchema'] {\n return this.getSerializer().getOutputSchema() as TSerializer['outputSchema'];\n }\n\n protected getCreateSchema(): TSerializer['createSchema'] {\n return this.getSerializer().getCreateSchema() as TSerializer['createSchema'];\n }\n\n protected getUpdateSchema(): TSerializer['updateSchema'] {\n return this.getSerializer().getUpdateSchema() as TSerializer['updateSchema'];\n }\n\n protected getLookupField(): keyof TModel {\n return this.lookupField ?? (this.getManager().meta.pk as keyof TModel);\n }\n\n protected getLookupValue(ctx: RequestContext): string | null {\n const value = ctx.params[this.lookupParam]?.trim();\n return value || null;\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 async performList(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\n .withFieldParsers(inferModelFieldParsers(this.getSerializer().getModel()))\n .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 const paginatedQueryset = paginator.apply(qs);\n const resultPromise = paginatedQueryset.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 rows = await serializer.serializeMany(result.items);\n const response = paginator.toResponse(rows as SerializerOutput<TSerializer>[], {\n totalCount,\n }) as OffsetPaginatedResponse<SerializerOutput<TSerializer>>;\n\n return TangoResponse.json(response as unknown as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n protected async performCreate(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 protected async performRetrieve(ctx: RequestContext): Promise<TangoResponse> {\n try {\n const value = this.getLookupValue(ctx);\n if (!value) {\n throw new NotFoundError('Lookup parameter was not provided.');\n }\n\n const lookupField = this.getLookupField();\n const filterByLookup = { [lookupField]: value } as FilterInput<TModel>;\n const result = await this.getManager().query().filter(filterByLookup).fetchOne();\n if (!result) {\n throw new NotFoundError(\n `No ${this.getManager().meta.table} record found for ${String(lookupField)}=${value}.`\n );\n }\n\n return TangoResponse.json((await this.getSerializer().serialize(result)) as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n protected async performUpdate(ctx: RequestContext): Promise<TangoResponse> {\n try {\n const value = this.getLookupValue(ctx);\n if (!value) {\n throw new NotFoundError('Lookup parameter was not provided.');\n }\n\n const body = await ctx.request.json();\n const result = await this.getSerializer().update(value as TModel[keyof TModel], body);\n\n return TangoResponse.json(result as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n protected async performDestroy(ctx: RequestContext): Promise<TangoResponse> {\n try {\n const value = this.getLookupValue(ctx);\n if (!value) {\n throw new NotFoundError('Lookup parameter was not provided.');\n }\n\n await this.getManager().delete(value as TModel[keyof TModel]);\n return TangoResponse.noContent();\n } catch (error) {\n return this.handleError(error);\n }\n }\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 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 GenericAPIView models.');\n }\n\n return model as ResourceModelLike<TModel> & {\n metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;\n };\n }\n\n private getLookupFieldFromMetadata(\n model: ResourceModelLike<TModel> & {\n metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;\n }\n ): keyof TModel {\n const primaryKeyField = model.metadata.fields.find((field) => field.primaryKey);\n\n if (!primaryKeyField) {\n throw new Error('OpenAPI generation requires a primary key field in Tango model metadata.');\n }\n\n return primaryKeyField.name as keyof TModel;\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `GET` requests to the generic list implementation.\n */\nexport abstract class ListModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected list(ctx: RequestContext): Promise<TangoResponse> {\n return this.performList(ctx);\n }\n\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.list(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `POST` requests to the generic create implementation.\n */\nexport abstract class CreateModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected create(ctx: RequestContext): Promise<TangoResponse> {\n return this.performCreate(ctx);\n }\n\n protected override post(ctx: RequestContext): Promise<TangoResponse> {\n return this.create(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `GET` requests to the generic retrieve implementation.\n */\nexport abstract class RetrieveModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected retrieve(ctx: RequestContext): Promise<TangoResponse> {\n return this.performRetrieve(ctx);\n }\n\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.retrieve(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `PUT` and `PATCH` requests to the generic update implementation.\n */\nexport abstract class UpdateModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected update(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n\n protected override put(ctx: RequestContext): Promise<TangoResponse> {\n return this.update(ctx);\n }\n\n protected override patch(ctx: RequestContext): Promise<TangoResponse> {\n return this.update(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `DELETE` requests to the generic destroy implementation.\n */\nexport abstract class DestroyModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected destroy(ctx: RequestContext): Promise<TangoResponse> {\n return this.performDestroy(ctx);\n }\n\n protected override delete(ctx: RequestContext): Promise<TangoResponse> {\n return this.destroy(ctx);\n }\n}\n","import { ListModelMixin } from '../mixins/ListModelMixin';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that only expose a list operation.\n */\nexport abstract class ListAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends ListModelMixin<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return super.get(ctx);\n }\n}\n","import { CreateModelMixin } from '../mixins/CreateModelMixin';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that only support resource creation.\n */\nexport abstract class CreateAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends CreateModelMixin<TModel, TSerializer> {\n protected override post(ctx: RequestContext): Promise<TangoResponse> {\n return super.post(ctx);\n }\n}\n","import { RetrieveModelMixin } from '../mixins/RetrieveModelMixin';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that retrieve a single resource by lookup.\n */\nexport abstract class RetrieveAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends RetrieveModelMixin<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return super.get(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for collection endpoints that list and create resources.\n */\nexport abstract class ListCreateAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.performList(ctx);\n }\n\n protected override post(ctx: RequestContext): Promise<TangoResponse> {\n return this.performCreate(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that retrieve and update a single resource.\n */\nexport abstract class RetrieveUpdateAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.performRetrieve(ctx);\n }\n\n protected override put(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n\n protected override patch(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that retrieve and delete a single resource.\n */\nexport abstract class RetrieveDestroyAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.performRetrieve(ctx);\n }\n\n protected override delete(ctx: RequestContext): Promise<TangoResponse> {\n return this.performDestroy(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for full detail endpoints that retrieve, update, and delete.\n */\nexport abstract class RetrieveUpdateDestroyAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.performRetrieve(ctx);\n }\n\n protected override put(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n\n protected override patch(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n\n protected override delete(ctx: RequestContext): Promise<TangoResponse> {\n return this.performDestroy(ctx);\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { APIView, type APIViewMethod } from './APIView';\nexport { GenericAPIView, type GenericAPIViewConfig } from './GenericAPIView';\nexport type { GenericAPIViewOpenAPIDescription } from '../resource/index';\nexport {\n ListModelMixin,\n CreateModelMixin,\n RetrieveModelMixin,\n UpdateModelMixin,\n DestroyModelMixin,\n} from './mixins/index';\nexport {\n ListAPIView,\n CreateAPIView,\n RetrieveAPIView,\n ListCreateAPIView,\n RetrieveUpdateAPIView,\n RetrieveDestroyAPIView,\n RetrieveUpdateDestroyAPIView,\n} from './generics/index';\n"],"mappings":"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"}
1
+ {"version":3,"file":"view-CYdJAO4t.js","names":[],"sources":["../src/view/APIView.ts","../src/view/GenericAPIView.ts","../src/view/mixins/ListModelMixin.ts","../src/view/mixins/CreateModelMixin.ts","../src/view/mixins/RetrieveModelMixin.ts","../src/view/mixins/UpdateModelMixin.ts","../src/view/mixins/DestroyModelMixin.ts","../src/view/generics/ListAPIView.ts","../src/view/generics/CreateAPIView.ts","../src/view/generics/RetrieveAPIView.ts","../src/view/generics/ListCreateAPIView.ts","../src/view/generics/RetrieveUpdateAPIView.ts","../src/view/generics/RetrieveDestroyAPIView.ts","../src/view/generics/RetrieveUpdateDestroyAPIView.ts","../src/view/index.ts"],"sourcesContent":["import { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../context/index';\n\nexport type APIViewMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\ntype APIViewMethodHandler = (ctx: RequestContext) => Promise<TangoResponse>;\n\n/**\n * Lightweight class-based request dispatcher for non-model API endpoints.\n */\nexport abstract class APIView {\n static readonly BRAND = 'tango.resources.api_view' as const;\n readonly __tangoBrand: typeof APIView.BRAND = APIView.BRAND;\n\n /**\n * Narrow an unknown value to `APIView`.\n */\n static isAPIView(value: unknown): value is APIView {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { __tangoBrand?: unknown }).__tangoBrand === APIView.BRAND\n );\n }\n\n /**\n * Dispatch the request to the handler for the current HTTP method.\n */\n async dispatch(ctx: RequestContext): Promise<TangoResponse> {\n const method = normalizeMethod(ctx.request.method);\n if (!method) {\n return this.httpMethodNotAllowed();\n }\n\n const handler = this.getMethodHandler(method);\n return handler(ctx);\n }\n\n getAllowedMethods(): readonly APIViewMethod[] {\n const allowed: APIViewMethod[] = [];\n if (this.get !== APIView.prototype.get) {\n allowed.push('GET');\n }\n if (this.post !== APIView.prototype.post) {\n allowed.push('POST');\n }\n if (this.put !== APIView.prototype.put) {\n allowed.push('PUT');\n }\n if (this.patch !== APIView.prototype.patch) {\n allowed.push('PATCH');\n }\n if (this.delete !== APIView.prototype.delete) {\n allowed.push('DELETE');\n }\n return allowed;\n }\n\n protected get(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected post(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected put(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected patch(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected delete(_ctx: RequestContext): Promise<TangoResponse> {\n return Promise.resolve(this.httpMethodNotAllowed());\n }\n\n protected httpMethodNotAllowed(): TangoResponse {\n return TangoResponse.methodNotAllowed(this.getAllowedMethods());\n }\n\n private getMethodHandler(method: APIViewMethod): APIViewMethodHandler {\n if (method === 'GET') {\n return (ctx) => this.get(ctx);\n }\n if (method === 'POST') {\n return (ctx) => this.post(ctx);\n }\n if (method === 'PUT') {\n return (ctx) => this.put(ctx);\n }\n if (method === 'PATCH') {\n return (ctx) => this.patch(ctx);\n }\n return (ctx) => this.delete(ctx);\n }\n}\n\nfunction normalizeMethod(method: string): APIViewMethod | null {\n const upper = method.toUpperCase();\n if (upper === 'GET') {\n return 'GET';\n }\n if (upper === 'POST') {\n return 'POST';\n }\n if (upper === 'PUT') {\n return 'PUT';\n }\n if (upper === 'PATCH') {\n return 'PATCH';\n }\n if (upper === 'DELETE') {\n return 'DELETE';\n }\n return null;\n}\n","import { HttpErrorFactory, TangoResponse, type JsonValue, NotFoundError } from '@danceroutine/tango-core';\nimport { Q, type FilterInput, type ManagerLike, type QuerySet } from '@danceroutine/tango-orm';\nimport type { OffsetPaginatedResponse, Paginator } from '../pagination/index';\nimport { OffsetPaginator } from '../paginators/OffsetPaginator';\nimport { APIView } from './APIView';\nimport { RequestContext } from '../context/index';\nimport type { FilterSet } from '../filters/index';\nimport { inferModelFieldParsers } from '../filters/inferModelFieldParsers';\nimport type { GenericAPIViewOpenAPIDescription } from '../resource/index';\nimport type { AnyModelSerializer, SerializerOutput } from '../serializer/index';\nimport type { ResourceModelLike } from '../resource/index';\n\ntype SearchFieldRef<TModel extends Record<string, unknown>> = Extract<keyof TModel, string> | string;\n\nexport interface GenericAPIViewConfig<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> {\n serializer: TSerializer;\n filters?: FilterSet<TModel>;\n orderingFields?: (keyof TModel)[];\n searchFields?: SearchFieldRef<TModel>[];\n lookupField?: keyof TModel;\n lookupParam?: string;\n paginatorFactory?: (queryset: QuerySet<TModel>) => Paginator<TModel, SerializerOutput<TSerializer>>;\n}\n\n/**\n * Generic API base class that centralizes query/build/validation helpers.\n */\nexport abstract class GenericAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends APIView {\n protected readonly serializerClass: TSerializer;\n protected readonly filters?: FilterSet<TModel>;\n protected readonly orderingFields: readonly (keyof TModel)[];\n protected readonly searchFields: readonly SearchFieldRef<TModel>[];\n protected readonly lookupField?: keyof TModel;\n protected readonly lookupParam: string;\n protected readonly paginatorFactory?: (\n queryset: QuerySet<TModel>\n ) => Paginator<TModel, SerializerOutput<TSerializer>>;\n private serializer?: InstanceType<TSerializer>;\n\n constructor(config: GenericAPIViewConfig<TModel, TSerializer>) {\n super();\n this.serializerClass = config.serializer;\n this.filters = config.filters;\n this.orderingFields = config.orderingFields ?? [];\n this.searchFields = config.searchFields ?? [];\n this.lookupField = config.lookupField;\n this.lookupParam = config.lookupParam ?? 'id';\n this.paginatorFactory = config.paginatorFactory;\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(): GenericAPIViewOpenAPIDescription<TModel, TSerializer> {\n const model = this.requireModelMetadata();\n return {\n model,\n outputSchema: this.getOutputSchema(),\n createSchema: this.getCreateSchema(),\n updateSchema: this.getUpdateSchema(),\n searchFields: this.searchFields,\n orderingFields: this.orderingFields,\n lookupField: this.lookupField ?? this.getLookupFieldFromMetadata(model),\n lookupParam: this.lookupParam,\n allowedMethods: this.getAllowedMethods(),\n usesDefaultOffsetPagination: !this.paginatorFactory,\n };\n }\n\n protected getManager(): ManagerLike<TModel> {\n return this.getSerializer().getManager();\n }\n\n protected getOutputSchema(): TSerializer['outputSchema'] {\n return this.getSerializer().getOutputSchema() as TSerializer['outputSchema'];\n }\n\n protected getCreateSchema(): TSerializer['createSchema'] {\n return this.getSerializer().getCreateSchema() as TSerializer['createSchema'];\n }\n\n protected getUpdateSchema(): TSerializer['updateSchema'] {\n return this.getSerializer().getUpdateSchema() as TSerializer['updateSchema'];\n }\n\n protected getLookupField(): keyof TModel {\n return this.lookupField ?? (this.getManager().meta.pk as keyof TModel);\n }\n\n protected getLookupValue(ctx: RequestContext): string | null {\n const value = ctx.params[this.lookupParam]?.trim();\n return value || null;\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 async performList(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\n .withFieldParsers(inferModelFieldParsers(this.getSerializer().getModel()))\n .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 const paginatedQueryset = paginator.apply(qs);\n const resultPromise = paginatedQueryset.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 rows = await serializer.serializeMany(result.items);\n const response = paginator.toResponse(rows as SerializerOutput<TSerializer>[], {\n totalCount,\n }) as OffsetPaginatedResponse<SerializerOutput<TSerializer>>;\n\n return TangoResponse.json(response as unknown as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n protected async performCreate(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 protected async performRetrieve(ctx: RequestContext): Promise<TangoResponse> {\n try {\n const value = this.getLookupValue(ctx);\n if (!value) {\n throw new NotFoundError('Lookup parameter was not provided.');\n }\n\n const lookupField = this.getLookupField();\n const filterByLookup = { [lookupField]: value } as FilterInput<TModel>;\n const result = await this.getManager().query().filter(filterByLookup).fetchOne();\n if (!result) {\n throw new NotFoundError(\n `No ${this.getManager().meta.table} record found for ${String(lookupField)}=${value}.`\n );\n }\n\n return TangoResponse.json((await this.getSerializer().serialize(result)) as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n protected async performUpdate(ctx: RequestContext): Promise<TangoResponse> {\n try {\n const value = this.getLookupValue(ctx);\n if (!value) {\n throw new NotFoundError('Lookup parameter was not provided.');\n }\n\n const body = await ctx.request.json();\n const result = await this.getSerializer().update(value as TModel[keyof TModel], body);\n\n return TangoResponse.json(result as JsonValue, { status: 200 });\n } catch (error) {\n return this.handleError(error);\n }\n }\n\n protected async performDestroy(ctx: RequestContext): Promise<TangoResponse> {\n try {\n const value = this.getLookupValue(ctx);\n if (!value) {\n throw new NotFoundError('Lookup parameter was not provided.');\n }\n\n await this.getManager().delete(value as TModel[keyof TModel]);\n return TangoResponse.noContent();\n } catch (error) {\n return this.handleError(error);\n }\n }\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 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 GenericAPIView models.');\n }\n\n return model as ResourceModelLike<TModel> & {\n metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;\n };\n }\n\n private getLookupFieldFromMetadata(\n model: ResourceModelLike<TModel> & {\n metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;\n }\n ): keyof TModel {\n const primaryKeyField = model.metadata.fields.find((field) => field.primaryKey);\n\n if (!primaryKeyField) {\n throw new Error('OpenAPI generation requires a primary key field in Tango model metadata.');\n }\n\n return primaryKeyField.name as keyof TModel;\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `GET` requests to the generic list implementation.\n */\nexport abstract class ListModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected list(ctx: RequestContext): Promise<TangoResponse> {\n return this.performList(ctx);\n }\n\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.list(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `POST` requests to the generic create implementation.\n */\nexport abstract class CreateModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected create(ctx: RequestContext): Promise<TangoResponse> {\n return this.performCreate(ctx);\n }\n\n protected override post(ctx: RequestContext): Promise<TangoResponse> {\n return this.create(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `GET` requests to the generic retrieve implementation.\n */\nexport abstract class RetrieveModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected retrieve(ctx: RequestContext): Promise<TangoResponse> {\n return this.performRetrieve(ctx);\n }\n\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.retrieve(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `PUT` and `PATCH` requests to the generic update implementation.\n */\nexport abstract class UpdateModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected update(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n\n protected override put(ctx: RequestContext): Promise<TangoResponse> {\n return this.update(ctx);\n }\n\n protected override patch(ctx: RequestContext): Promise<TangoResponse> {\n return this.update(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Mixin that wires `DELETE` requests to the generic destroy implementation.\n */\nexport abstract class DestroyModelMixin<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected destroy(ctx: RequestContext): Promise<TangoResponse> {\n return this.performDestroy(ctx);\n }\n\n protected override delete(ctx: RequestContext): Promise<TangoResponse> {\n return this.destroy(ctx);\n }\n}\n","import { ListModelMixin } from '../mixins/ListModelMixin';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that only expose a list operation.\n */\nexport abstract class ListAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends ListModelMixin<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return super.get(ctx);\n }\n}\n","import { CreateModelMixin } from '../mixins/CreateModelMixin';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that only support resource creation.\n */\nexport abstract class CreateAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends CreateModelMixin<TModel, TSerializer> {\n protected override post(ctx: RequestContext): Promise<TangoResponse> {\n return super.post(ctx);\n }\n}\n","import { RetrieveModelMixin } from '../mixins/RetrieveModelMixin';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that retrieve a single resource by lookup.\n */\nexport abstract class RetrieveAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends RetrieveModelMixin<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return super.get(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for collection endpoints that list and create resources.\n */\nexport abstract class ListCreateAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.performList(ctx);\n }\n\n protected override post(ctx: RequestContext): Promise<TangoResponse> {\n return this.performCreate(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that retrieve and update a single resource.\n */\nexport abstract class RetrieveUpdateAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.performRetrieve(ctx);\n }\n\n protected override put(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n\n protected override patch(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for endpoints that retrieve and delete a single resource.\n */\nexport abstract class RetrieveDestroyAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.performRetrieve(ctx);\n }\n\n protected override delete(ctx: RequestContext): Promise<TangoResponse> {\n return this.performDestroy(ctx);\n }\n}\n","import { GenericAPIView } from '../GenericAPIView';\nimport type { TangoResponse } from '@danceroutine/tango-core';\nimport { RequestContext } from '../../context/index';\nimport type { AnyModelSerializer } from '../../serializer/index';\n\n/**\n * Generic API view for full detail endpoints that retrieve, update, and delete.\n */\nexport abstract class RetrieveUpdateDestroyAPIView<\n TModel extends Record<string, unknown>,\n TSerializer extends AnyModelSerializer<TModel>,\n> extends GenericAPIView<TModel, TSerializer> {\n protected override get(ctx: RequestContext): Promise<TangoResponse> {\n return this.performRetrieve(ctx);\n }\n\n protected override put(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n\n protected override patch(ctx: RequestContext): Promise<TangoResponse> {\n return this.performUpdate(ctx);\n }\n\n protected override delete(ctx: RequestContext): Promise<TangoResponse> {\n return this.performDestroy(ctx);\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { APIView, type APIViewMethod } from './APIView';\nexport { GenericAPIView, type GenericAPIViewConfig } from './GenericAPIView';\nexport type { GenericAPIViewOpenAPIDescription } from '../resource/index';\nexport {\n ListModelMixin,\n CreateModelMixin,\n RetrieveModelMixin,\n UpdateModelMixin,\n DestroyModelMixin,\n} from './mixins/index';\nexport {\n ListAPIView,\n CreateAPIView,\n RetrieveAPIView,\n ListCreateAPIView,\n RetrieveUpdateAPIView,\n RetrieveDestroyAPIView,\n RetrieveUpdateDestroyAPIView,\n} from './generics/index';\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"viewset-C9j-2U29.js","names":[],"sources":["../src/viewset/ModelViewSet.ts","../src/viewset/index.ts"],"sourcesContent":["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 { inferModelFieldParsers } from '../filters/inferModelFieldParsers';\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 { AnyModelSerializer, AnyModelSerializerClass, SerializerOutput } 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>;\ntype SearchFieldRef<TModel extends Record<string, unknown>> = Extract<keyof TModel, string> | string;\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 AnyModelSerializer<TModel>,\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?: SearchFieldRef<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 AnyModelSerializer<TModel>,\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: SearchFieldRef<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 const model = this.requireModelMetadata();\n return {\n model,\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.getLookupFieldFromMetadata(model),\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\n .withFieldParsers(inferModelFieldParsers(this.getSerializer().getModel()))\n .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 const paginatedQueryset = paginator.apply(qs);\n const resultPromise = paginatedQueryset.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 rows = await serializer.serializeMany(result.items);\n const response = paginator.toResponse(rows as SerializerOutput<TSerializer>[], { totalCount });\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((await this.getSerializer().serialize(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 private getLookupFieldFromMetadata(\n model: ResourceModelLike<TModel> & {\n metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;\n }\n ): keyof TModel {\n const primaryKeyField = model.metadata.fields.find((field) => field.primaryKey);\n\n if (!primaryKeyField) {\n throw new Error('OpenAPI generation requires a primary key field in Tango model metadata.');\n }\n\n return primaryKeyField.name as keyof TModel;\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
+ {"version":3,"file":"viewset-C9j-2U29.js","names":[],"sources":["../src/viewset/ModelViewSet.ts","../src/viewset/index.ts"],"sourcesContent":["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 { inferModelFieldParsers } from '../filters/inferModelFieldParsers';\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 { AnyModelSerializer, AnyModelSerializerClass, SerializerOutput } 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>;\ntype SearchFieldRef<TModel extends Record<string, unknown>> = Extract<keyof TModel, string> | string;\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 AnyModelSerializer<TModel>,\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?: SearchFieldRef<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 AnyModelSerializer<TModel>,\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: SearchFieldRef<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 const model = this.requireModelMetadata();\n return {\n model,\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.getLookupFieldFromMetadata(model),\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\n .withFieldParsers(inferModelFieldParsers(this.getSerializer().getModel()))\n .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 const paginatedQueryset = paginator.apply(qs);\n const resultPromise = paginatedQueryset.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 rows = await serializer.serializeMany(result.items);\n const response = paginator.toResponse(rows as SerializerOutput<TSerializer>[], { totalCount });\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((await this.getSerializer().serialize(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 private getLookupFieldFromMetadata(\n model: ResourceModelLike<TModel> & {\n metadata: NonNullable<ResourceModelLike<TModel>['metadata']>;\n }\n ): keyof TModel {\n const primaryKeyField = model.metadata.fields.find((field) => field.primaryKey);\n\n if (!primaryKeyField) {\n throw new Error('OpenAPI generation requires a primary key field in Tango model metadata.');\n }\n\n return primaryKeyField.name as keyof TModel;\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danceroutine/tango-resources",
3
- "version": "1.11.15",
3
+ "version": "1.12.0",
4
4
  "description": "ModelViewSet, serializers, filters, and pagination for Tango",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -62,17 +62,17 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "zod": "^4.0.0",
65
- "@danceroutine/tango-core": "1.11.15",
66
- "@danceroutine/tango-orm": "1.11.15"
65
+ "@danceroutine/tango-core": "1.12.0",
66
+ "@danceroutine/tango-orm": "1.12.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^25.9.1",
70
- "tsdown": "^0.22.1",
70
+ "tsdown": "^0.22.2",
71
71
  "typescript": "^6.0.3",
72
72
  "vitest": "^4.1.8",
73
73
  "zod": "^4.0.0",
74
- "@danceroutine/tango-schema": "1.11.15",
75
- "@danceroutine/tango-testing": "1.11.15"
74
+ "@danceroutine/tango-schema": "1.12.0",
75
+ "@danceroutine/tango-testing": "1.12.0"
76
76
  },
77
77
  "scripts": {
78
78
  "build": "tsdown",