@danceroutine/tango-resources 1.11.3 → 1.11.5

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.
Files changed (49) hide show
  1. package/README.md +2 -2
  2. package/dist/CursorPaginator-B_8MhYZY.js +195 -0
  3. package/dist/CursorPaginator-B_8MhYZY.js.map +1 -0
  4. package/dist/CursorPaginator-CfeMQCdJ.d.ts +177 -0
  5. package/dist/OffsetPaginator-CaycvxJU.js +188 -0
  6. package/dist/OffsetPaginator-CaycvxJU.js.map +1 -0
  7. package/dist/context/index.d.ts +2 -0
  8. package/dist/context/index.js +2 -0
  9. package/dist/context-euBQvNRT.js +65 -0
  10. package/dist/context-euBQvNRT.js.map +1 -0
  11. package/dist/filters/index.d.ts +2 -0
  12. package/dist/filters/index.js +2 -0
  13. package/dist/filters-46d2Nr5C.js +287 -0
  14. package/dist/filters-46d2Nr5C.js.map +1 -0
  15. package/dist/index-Ac94YL5S.d.ts +24 -0
  16. package/dist/index-BJJalUDB.d.ts +54 -0
  17. package/dist/index-Bg6TtnmQ.d.ts +175 -0
  18. package/dist/index-C55GDIOn.d.ts +222 -0
  19. package/dist/index-CiIB-1Ac.d.ts +123 -0
  20. package/dist/index-DkJtxvKu.d.ts +164 -0
  21. package/dist/index.d.ts +10 -12
  22. package/dist/index.js +10 -1013
  23. package/dist/inferModelFieldParsers-2irv7j1T.js +70 -0
  24. package/dist/inferModelFieldParsers-2irv7j1T.js.map +1 -0
  25. package/dist/pagination/index.d.ts +3 -0
  26. package/dist/pagination/index.js +15 -0
  27. package/dist/pagination/index.js.map +1 -0
  28. package/dist/paginators/index.d.ts +9 -0
  29. package/dist/paginators/index.js +12 -0
  30. package/dist/paginators/index.js.map +1 -0
  31. package/dist/resource/index.d.ts +3 -0
  32. package/dist/resource/index.js +7 -0
  33. package/dist/resource/index.js.map +1 -0
  34. package/dist/serializer/index.d.ts +2 -0
  35. package/dist/serializer/index.js +2 -0
  36. package/dist/serializer-RSwlXWls.js +305 -0
  37. package/dist/serializer-RSwlXWls.js.map +1 -0
  38. package/dist/view/index.d.ts +2 -1
  39. package/dist/view/index.js +1 -1
  40. package/dist/{view-C9B5Lln3.js → view-CYdJAO4t.js} +7 -314
  41. package/dist/view-CYdJAO4t.js.map +1 -0
  42. package/dist/viewset/index.d.ts +9 -0
  43. package/dist/viewset/index.js +2 -0
  44. package/dist/viewset-C9j-2U29.js +227 -0
  45. package/dist/viewset-C9j-2U29.js.map +1 -0
  46. package/package.json +21 -17
  47. package/dist/index-D6sfTSEj.d.ts +0 -902
  48. package/dist/index.js.map +0 -1
  49. package/dist/view-C9B5Lln3.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,1014 +1,11 @@
1
- import { t as __exportAll } from "./chunk-D7D4PA-g.js";
2
- import { _ as OffsetPaginator, a as ListCreateAPIView, b as context_exports, c as ListAPIView, d as RetrieveModelMixin, f as CreateModelMixin, g as inferModelFieldParsers, h as APIView, i as RetrieveUpdateAPIView, l as DestroyModelMixin, m as GenericAPIView, n as RetrieveUpdateDestroyAPIView, o as RetrieveAPIView, p as ListModelMixin, r as RetrieveDestroyAPIView, s as CreateAPIView, t as view_exports, u as UpdateModelMixin, v as OffsetPaginationInput, x as RequestContext, y as BasePaginator } from "./view-C9B5Lln3.js";
3
- import { HttpErrorFactory, NotFoundError, TangoResponse, getLogger } from "@danceroutine/tango-core";
4
- import { ManyToManyRelatedManager, Q } from "@danceroutine/tango-orm";
5
- import { z } from "zod";
6
- //#region src/filters/internal/InternalFilterType.ts
7
- const InternalFilterType = {
8
- SCALAR: "scalar",
9
- ILIKE: "ilike",
10
- RANGE: "range",
11
- IN: "in",
12
- CUSTOM: "custom"
13
- };
14
- //#endregion
15
- //#region src/filters/internal/InternalFilterLookup.ts
16
- const InternalFilterLookup = {
17
- EXACT: "exact",
18
- LT: "lt",
19
- LTE: "lte",
20
- GT: "gt",
21
- GTE: "gte",
22
- IN: "in",
23
- ISNULL: "isnull",
24
- CONTAINS: "contains",
25
- ICONTAINS: "icontains",
26
- STARTSWITH: "startswith",
27
- ISTARTSWITH: "istartswith",
28
- ENDSWITH: "endswith",
29
- IENDSWITH: "iendswith"
30
- };
31
- //#endregion
32
- //#region src/filters/FilterSet.ts
33
- const FILTER_LOOKUPS = Object.values(InternalFilterLookup);
34
- function isFilterLookup(value) {
35
- return FILTER_LOOKUPS.includes(value);
36
- }
37
- /**
38
- * Declarative query-param to filter translation.
39
- *
40
- * A `FilterSet` lets viewsets expose safe, explicit filtering behavior
41
- * without leaking raw ORM filter syntax to request handlers.
42
- */
43
- var FilterSet = class FilterSet {
44
- spec;
45
- allowAllParams;
46
- static BRAND = "tango.resources.filter_set";
47
- __tangoBrand = FilterSet.BRAND;
48
- /**
49
- * Resolve matching query parameters into ORM filter inputs.
50
- */
51
- constructor(spec, allowAllParams = false) {
52
- this.spec = spec;
53
- this.allowAllParams = allowAllParams;
54
- }
55
- /**
56
- * Build a filter set from Django-style field declarations.
57
- */
58
- static define(config) {
59
- return new FilterSet(FilterSet.normalizeDefineConfig(config), config.all === "__all__");
60
- }
61
- /**
62
- * Narrow an unknown value to `FilterSet`.
63
- */
64
- static isFilterSet(value) {
65
- return typeof value === "object" && value !== null && value.__tangoBrand === FilterSet.BRAND;
66
- }
67
- static normalizeDefineConfig(config) {
68
- const spec = {};
69
- const fieldDeclarations = config.fields ?? {};
70
- const fieldParsers = config.parsers ?? {};
71
- for (const rawField of Object.keys(fieldDeclarations)) {
72
- const declaration = fieldDeclarations[rawField];
73
- if (declaration === void 0) continue;
74
- const parser = fieldParsers[rawField];
75
- FilterSet.addFieldDeclaration(spec, rawField, declaration, parser);
76
- }
77
- const aliases = config.aliases ?? {};
78
- for (const [param, declaration] of Object.entries(aliases)) spec[param] = FilterSet.normalizeAliasDeclaration(declaration);
79
- return spec;
80
- }
81
- static addFieldDeclaration(spec, field, declaration, parser) {
82
- if (declaration === true) {
83
- spec[String(field)] = FilterSet.createLookupResolver(field, InternalFilterLookup.EXACT, parser);
84
- return;
85
- }
86
- if (FilterSet.isLookupArray(declaration)) {
87
- for (const lookup of declaration) {
88
- const param = FilterSet.resolveLookupParam(String(field), lookup);
89
- spec[param] = FilterSet.createLookupResolver(field, lookup, parser);
90
- }
91
- return;
92
- }
93
- const lookups = declaration.lookups ?? [InternalFilterLookup.EXACT];
94
- const baseParam = declaration.param ?? String(field);
95
- const effectiveParser = declaration.parse ?? parser;
96
- for (const lookup of lookups) {
97
- const param = FilterSet.resolveLookupParam(baseParam, lookup);
98
- spec[param] = FilterSet.createLookupResolver(field, lookup, effectiveParser);
99
- }
100
- }
101
- static isLookupArray(value) {
102
- return Array.isArray(value);
103
- }
104
- static normalizeAliasDeclaration(declaration) {
105
- if (FilterSet.isFilterResolverDeclaration(declaration)) return declaration;
106
- if ("fields" in declaration) {
107
- const lookup = declaration.lookup ?? InternalFilterLookup.ICONTAINS;
108
- return FilterSet.createMultiFieldResolver(declaration.fields, lookup, declaration.parse);
109
- }
110
- return FilterSet.createLookupResolver(declaration.field, declaration.lookup ?? InternalFilterLookup.EXACT, declaration.parse);
111
- }
112
- static isFilterResolverDeclaration(value) {
113
- if (typeof value !== "object" || value === null || !("type" in value)) return false;
114
- return [
115
- InternalFilterType.SCALAR,
116
- InternalFilterType.ILIKE,
117
- InternalFilterType.RANGE,
118
- InternalFilterType.IN,
119
- InternalFilterType.CUSTOM
120
- ].includes(value.type);
121
- }
122
- static createMultiFieldResolver(fields, lookup, parser) {
123
- if (lookup === InternalFilterLookup.ICONTAINS && parser === void 0) return {
124
- type: InternalFilterType.ILIKE,
125
- columns: [...fields]
126
- };
127
- return {
128
- type: InternalFilterType.CUSTOM,
129
- apply: (raw) => {
130
- const parsed = FilterSet.resolveParserValue(raw, parser);
131
- if (parsed === void 0) return void 0;
132
- const composed = {};
133
- for (const field of fields) {
134
- const segment = FilterSet.resolveLookupFilter(field, lookup, parsed);
135
- if (!segment) continue;
136
- Object.assign(composed, segment);
137
- }
138
- return Object.keys(composed).length > 0 ? composed : void 0;
139
- }
140
- };
141
- }
142
- static createLookupResolver(field, lookup, parser) {
143
- if (parser !== void 0) return {
144
- type: InternalFilterType.CUSTOM,
145
- apply: (raw) => {
146
- const parsed = FilterSet.resolveParserValue(raw, parser);
147
- if (parsed === void 0) return void 0;
148
- return FilterSet.resolveLookupFilter(field, lookup, parsed);
149
- }
150
- };
151
- switch (lookup) {
152
- case "exact": return {
153
- type: InternalFilterType.SCALAR,
154
- column: field
155
- };
156
- case "in": return {
157
- type: InternalFilterType.IN,
158
- column: field
159
- };
160
- case "lt":
161
- case "lte":
162
- case "gt":
163
- case "gte": return {
164
- type: InternalFilterType.RANGE,
165
- column: field,
166
- op: lookup
167
- };
168
- case "icontains": return {
169
- type: InternalFilterType.ILIKE,
170
- columns: [field]
171
- };
172
- default: return {
173
- type: InternalFilterType.CUSTOM,
174
- apply: (raw) => FilterSet.resolveLookupFilter(field, lookup, raw)
175
- };
176
- }
177
- }
178
- static resolveLookupFilter(field, lookup, value) {
179
- if (value === void 0) return void 0;
180
- if (lookup === "exact") return { [field]: value };
181
- if (lookup === "in") {
182
- const arr = Array.isArray(value) ? value : String(value).split(",");
183
- return { [`${String(field)}__in`]: arr };
184
- }
185
- if (lookup === "icontains") return { [`${String(field)}__icontains`]: `%${FilterSet.toScalarString(value)}%` };
186
- return { [`${String(field)}__${lookup}`]: value };
187
- }
188
- static resolveLookupParam(baseParam, lookup) {
189
- if (lookup === "exact") return baseParam;
190
- return `${baseParam}__${lookup}`;
191
- }
192
- static resolveParserValue(value, parser) {
193
- if (value === void 0) return;
194
- if (parser === void 0) return value;
195
- return parser(value);
196
- }
197
- static toScalarString(value) {
198
- if (Array.isArray(value)) return value.join(",");
199
- return String(value);
200
- }
201
- /**
202
- * Return a new filter set with parser-aware scalar/range/in resolvers for matching fields.
203
- */
204
- withFieldParsers(parsers) {
205
- if (Object.keys(parsers).length === 0) return this;
206
- let changed = false;
207
- const nextSpec = Object.fromEntries(Object.entries(this.spec).map(([param, resolver]) => {
208
- const nextResolver = this.applyFieldParserOverride(resolver, parsers);
209
- if (nextResolver !== resolver) changed = true;
210
- return [param, nextResolver];
211
- }));
212
- return changed ? new FilterSet(nextSpec, this.allowAllParams) : this;
213
- }
214
- /**
215
- * Apply all configured resolvers against query params.
216
- */
217
- apply(params) {
218
- const filters = [];
219
- const keys = /* @__PURE__ */ new Set();
220
- for (const [key] of params.entries()) keys.add(key);
221
- for (const key of keys) {
222
- const resolver = this.spec[key] ?? (this.allowAllParams ? this.buildAllResolver(key) : void 0);
223
- if (!resolver) continue;
224
- const rawValue = params.getAll(key);
225
- const value = rawValue.length > 1 ? rawValue : rawValue[0];
226
- if (value === void 0) continue;
227
- const filter = this.resolveFilter(resolver, value);
228
- if (filter) filters.push(filter);
229
- }
230
- return filters;
231
- }
232
- buildAllResolver(param) {
233
- const segments = param.split("__").filter(Boolean);
234
- if (segments.length === 0) return;
235
- const lastSegment = segments.at(-1);
236
- const lookup = isFilterLookup(lastSegment) ? lastSegment : "exact";
237
- const field = (lookup === "exact" ? segments : segments.slice(0, -1)).join("__");
238
- if (!field) return;
239
- if (lookup === "exact") return {
240
- type: InternalFilterType.SCALAR,
241
- column: field
242
- };
243
- return FilterSet.createLookupResolver(field, lookup);
244
- }
245
- resolveFilter(resolver, value) {
246
- if (value === void 0) return void 0;
247
- switch (resolver.type) {
248
- case InternalFilterType.SCALAR: return { [resolver.column]: value };
249
- case InternalFilterType.ILIKE: {
250
- const pattern = `%${FilterSet.toScalarString(value)}%`;
251
- const filter = {};
252
- resolver.columns.forEach((col) => {
253
- filter[`${String(col)}__icontains`] = pattern;
254
- });
255
- return filter;
256
- }
257
- case InternalFilterType.RANGE: return { [`${String(resolver.column)}__${resolver.op}`]: value };
258
- case InternalFilterType.IN: {
259
- const arr = Array.isArray(value) ? value : String(value).split(",");
260
- return { [`${String(resolver.column)}__in`]: arr };
261
- }
262
- case InternalFilterType.CUSTOM: return resolver.apply(value);
263
- default: return;
264
- }
265
- }
266
- applyFieldParserOverride(resolver, parsers) {
267
- switch (resolver.type) {
268
- case InternalFilterType.SCALAR: {
269
- const parser = parsers[resolver.column];
270
- return parser ? FilterSet.createLookupResolver(resolver.column, "exact", parser) : resolver;
271
- }
272
- case InternalFilterType.RANGE: {
273
- const parser = parsers[resolver.column];
274
- return parser ? FilterSet.createLookupResolver(resolver.column, resolver.op, parser) : resolver;
275
- }
276
- case InternalFilterType.IN: {
277
- const parser = parsers[resolver.column];
278
- return parser ? FilterSet.createLookupResolver(resolver.column, "in", parser) : resolver;
279
- }
280
- default: return resolver;
281
- }
282
- }
283
- };
284
- //#endregion
285
- //#region src/filters/index.ts
286
- var filters_exports = /* @__PURE__ */ __exportAll({ FilterSet: () => FilterSet });
287
- //#endregion
288
- //#region src/pagination/CursorPaginationInput.ts
289
- const CursorPaginationInput = z.object({
290
- limit: z.preprocess((value) => {
291
- if (value === void 0 || value === null || value === "") return;
292
- const parsed = Number.parseInt(String(value), 10);
293
- if (!Number.isFinite(parsed) || parsed <= 0) return;
294
- return parsed;
295
- }, z.number().int().min(1).transform((value) => Math.min(value, 100)).optional()),
296
- cursor: z.string().nullable().default(null),
297
- ordering: z.string().optional()
298
- });
299
- //#endregion
300
- //#region src/paginators/CursorPaginator.ts
301
- /**
302
- * Represents a single cursor page of results.
303
- * Cursor pages do not expose numeric page navigation like offset pagination.
304
- */
305
- var CursorPage = class CursorPage {
306
- results;
307
- nextCursor;
308
- previousCursor;
309
- static BRAND = "tango.resources.cursor_page";
310
- __tangoBrand = CursorPage.BRAND;
311
- constructor(results, nextCursor, previousCursor) {
312
- this.results = results;
313
- this.nextCursor = nextCursor;
314
- this.previousCursor = previousCursor;
315
- }
316
- static isCursorPage(value) {
317
- return typeof value === "object" && value !== null && value.__tangoBrand === CursorPage.BRAND;
318
- }
319
- /** Whether a next cursor token exists. */
320
- hasNext() {
321
- return this.nextCursor !== null;
322
- }
323
- /** Whether a previous cursor token exists. */
324
- hasPrevious() {
325
- return this.previousCursor !== null;
326
- }
327
- nextPageNumber() {
328
- return null;
329
- }
330
- previousPageNumber() {
331
- return null;
332
- }
333
- startIndex() {
334
- return 0;
335
- }
336
- endIndex() {
337
- return this.results.length;
338
- }
339
- };
340
- /**
341
- * Cursor-based paginator for stable forward navigation with opaque cursor tokens.
342
- * It supports `limit`, `cursor`, and `ordering` query params and returns DRF-style
343
- * paginated envelopes with cursor links.
344
- */
345
- var CursorPaginator = class CursorPaginator extends BasePaginator {
346
- queryset;
347
- perPage;
348
- cursorField;
349
- static BRAND = "tango.resources.cursor_paginator";
350
- __tangoBrand = CursorPaginator.BRAND;
351
- limit;
352
- cursor = null;
353
- direction = "asc";
354
- nextCursor = null;
355
- previousCursor = null;
356
- constructor(queryset, perPage = 25, cursorField = "id") {
357
- super();
358
- this.queryset = queryset;
359
- this.perPage = perPage;
360
- this.cursorField = cursorField;
361
- this.limit = perPage;
362
- }
363
- /**
364
- * Narrow an unknown value to `CursorPaginator`.
365
- */
366
- static isCursorPaginator(value) {
367
- return typeof value === "object" && value !== null && value.__tangoBrand === CursorPaginator.BRAND;
368
- }
369
- /**
370
- * Parse cursor pagination parameters from Tango query params.
371
- */
372
- parse(params) {
373
- const parsed = CursorPaginationInput.parse({
374
- limit: params.get("limit") ?? void 0,
375
- cursor: params.get("cursor"),
376
- ordering: params.get("ordering") ?? void 0
377
- });
378
- this.limit = parsed.limit ?? this.perPage;
379
- this.cursor = parsed.cursor;
380
- const ordering = parsed.ordering;
381
- if (ordering) {
382
- const parsedDirection = ordering.startsWith("-") ? "desc" : "asc";
383
- const parsedField = ordering.startsWith("-") ? ordering.slice(1) : ordering;
384
- this.direction = parsedField === String(this.cursorField) ? parsedDirection : "asc";
385
- } else this.direction = "asc";
386
- }
387
- /**
388
- * Parse params and return compatibility `{ limit, offset }` shape.
389
- */
390
- parseParams(params) {
391
- this.parse(params);
392
- return {
393
- limit: this.limit,
394
- offset: 0
395
- };
396
- }
397
- /**
398
- * Build a paginated response payload with cursor links.
399
- */
400
- needsTotalCount() {
401
- return false;
402
- }
403
- toResponse(results, _context) {
404
- const response = { results: this.resolveQueryResultRows(results) };
405
- if (this.nextCursor) response.next = this.buildPageLink(this.nextCursor);
406
- if (this.previousCursor) response.previous = this.buildPageLink(this.previousCursor);
407
- return response;
408
- }
409
- /**
410
- * Backward-compatible alias for `toResponse`.
411
- */
412
- getPaginatedResponse(results, _totalCount) {
413
- return this.toResponse(results);
414
- }
415
- /**
416
- * Apply cursor constraints and ordering to a queryset.
417
- */
418
- apply(queryset) {
419
- let qs = queryset.limit(this.limit + 1);
420
- if (this.cursor) {
421
- const decoded = this.decodeCursor(this.cursor);
422
- if (decoded.field !== String(this.cursorField)) throw new Error("Invalid cursor: field mismatch");
423
- const lookup = this.direction === "asc" ? "__gt" : "__lt";
424
- const filterInput = { [`${String(this.cursorField)}${lookup}`]: decoded.value };
425
- qs = qs.filter(filterInput);
426
- }
427
- const orderToken = this.direction === "asc" ? String(this.cursorField) : `-${String(this.cursorField)}`;
428
- return qs.orderBy(orderToken);
429
- }
430
- /**
431
- * Fetch the next cursor page.
432
- */
433
- async paginate(cursor) {
434
- const appliedCursor = cursor ?? this.cursor;
435
- this.cursor = appliedCursor;
436
- const fetched = await this.apply(this.queryset).fetch();
437
- const results = this.resolveQueryResultRows(fetched);
438
- const hasMore = results.length > this.limit;
439
- if (hasMore) results.pop();
440
- this.previousCursor = appliedCursor ?? null;
441
- const last = results.at(-1);
442
- this.nextCursor = hasMore && last ? this.encodeCursor(last) : null;
443
- return new CursorPage(results, this.nextCursor, this.previousCursor);
444
- }
445
- /**
446
- * Cursor paginators only support page `1` as an entry point.
447
- */
448
- async getPage(page) {
449
- if (page !== 1) throw new Error("CursorPaginator only supports getPage(1). Use cursor pagination for subsequent pages.");
450
- return this.paginate();
451
- }
452
- buildPageLink(cursor) {
453
- const orderingToken = this.direction === "asc" ? String(this.cursorField) : `-${String(this.cursorField)}`;
454
- return `?limit=${this.limit}&cursor=${encodeURIComponent(cursor)}&ordering=${encodeURIComponent(orderingToken)}`;
455
- }
456
- encodeCursor(item) {
457
- const payload = {
458
- v: 1,
459
- field: String(this.cursorField),
460
- dir: this.direction,
461
- value: item[this.cursorField]
462
- };
463
- return Buffer.from(JSON.stringify(payload), "utf-8").toString("base64");
464
- }
465
- decodeCursor(cursor) {
466
- let parsed;
467
- try {
468
- parsed = JSON.parse(Buffer.from(cursor, "base64").toString("utf-8"));
469
- } catch {
470
- throw new Error("Invalid cursor: malformed token");
471
- }
472
- if (!parsed || typeof parsed !== "object" || parsed.v !== 1 || typeof parsed.field !== "string" || parsed.dir !== "asc" && parsed.dir !== "desc" || !("value" in parsed)) throw new Error("Invalid cursor: unsupported payload");
473
- return parsed;
474
- }
475
- };
476
- //#endregion
477
- //#region src/pagination/index.ts
478
- var pagination_exports = /* @__PURE__ */ __exportAll({
479
- BasePaginator: () => BasePaginator,
480
- CursorPaginationInput: () => CursorPaginationInput,
481
- CursorPaginator: () => CursorPaginator,
482
- OffsetPaginationInput: () => OffsetPaginationInput,
483
- OffsetPaginator: () => OffsetPaginator
484
- });
485
- //#endregion
486
- //#region src/paginators/index.ts
487
- var paginators_exports = /* @__PURE__ */ __exportAll({
488
- CursorPaginator: () => CursorPaginator,
489
- OffsetPaginator: () => OffsetPaginator
490
- });
491
- //#endregion
492
- //#region src/resource/index.ts
493
- var resource_exports = /* @__PURE__ */ __exportAll({});
494
- //#endregion
495
- //#region src/serializer/Serializer.ts
496
- const logger = getLogger("tango.resources.serializer");
497
- let hasWarnedAboutToRepresentationDeprecation = false;
498
- /**
499
- * DRF-inspired base serializer backed by Zod schemas.
500
- *
501
- * Tango serializers keep Zod as the source of truth for validation and type
502
- * inference while centralizing create, update, and representation workflows in
503
- * one class-owned contract.
504
- */
505
- var Serializer = class {
506
- static createSchema = z.unknown();
507
- static updateSchema = z.unknown();
508
- static outputSchema = z.unknown();
509
- static outputResolvers = void 0;
510
- /**
511
- * Return the serializer class for the current instance.
512
- */
513
- getSerializerClass() {
514
- return this.constructor;
515
- }
516
- /**
517
- * Return the Zod schema used for create payloads.
518
- */
519
- getCreateSchema() {
520
- return this.getSerializerClass().createSchema;
521
- }
522
- /**
523
- * Return the Zod schema used for update payloads.
524
- */
525
- getUpdateSchema() {
526
- return this.getSerializerClass().updateSchema;
527
- }
528
- /**
529
- * Return the Zod schema used for serialized output.
530
- */
531
- getOutputSchema() {
532
- return this.getSerializerClass().outputSchema;
533
- }
534
- /**
535
- * Return the resolver map used to enrich serializer output fields before
536
- * the outward Zod schema parses the final response shape.
537
- */
538
- getOutputResolvers() {
539
- return this.getSerializerClass().outputResolvers ?? {};
540
- }
541
- /**
542
- * Validate unknown input for create workflows.
543
- */
544
- deserializeCreate(input) {
545
- return this.getCreateSchema().parse(input);
546
- }
547
- /**
548
- * Validate unknown input for update workflows.
549
- */
550
- deserializeUpdate(input) {
551
- return this.getUpdateSchema().parse(input);
552
- }
553
- /**
554
- * Convert a persisted record into its outward-facing representation.
555
- *
556
- * @deprecated Use `serialize(...)` instead so serializer-owned output
557
- * resolvers run before the outward Zod schema parses the response shape.
558
- */
559
- toRepresentation(record) {
560
- if (!hasWarnedAboutToRepresentationDeprecation) {
561
- hasWarnedAboutToRepresentationDeprecation = true;
562
- logger.warn("`Serializer.toRepresentation(...)` is deprecated. Use `serialize(...)` instead so output resolvers run before outward parsing.");
563
- }
564
- return this.getOutputSchema().parse(record);
565
- }
566
- /**
567
- * Resolve serializer-owned output fields and parse the outward response
568
- * contract.
569
- */
570
- async serialize(record) {
571
- return this.getOutputSchema().parse(await this.applyOutputResolvers(record));
572
- }
573
- /**
574
- * Serialize many records through the same outward response contract.
575
- */
576
- async serializeMany(records) {
577
- return Promise.all(records.map((record) => this.serialize(record)));
578
- }
579
- async applyOutputResolvers(record) {
580
- const resolvers = this.getOutputResolvers();
581
- const resolverEntries = Object.entries(resolvers);
582
- if (resolverEntries.length === 0 || typeof record !== "object" || record === null) return record;
583
- const resolved = await Promise.all(resolverEntries.map(async ([key, resolver]) => [key, await resolver(record)]));
584
- return {
585
- ...record,
586
- ...Object.fromEntries(resolved)
587
- };
588
- }
589
- };
590
- //#endregion
591
- //#region src/serializer/internal/InternalSerializerRelationKind.ts
592
- const InternalSerializerRelationKind = { MANY_TO_MANY: "manyToMany" };
593
- const InternalManyToManyReadStrategyKind = {
594
- PK_LIST: "pkList",
595
- NESTED: "nested"
596
- };
597
- const InternalManyToManyWriteStrategyKind = {
598
- PK_LIST: "pkList",
599
- SLUG_LIST: "slugList"
600
- };
601
- //#endregion
602
- //#region src/serializer/ModelSerializer.ts
603
- /**
604
- * Zod-backed serializer with default model-manager persistence behavior.
605
- */
606
- var ModelSerializer = class extends Serializer {
607
- static model;
608
- static relationFields = void 0;
609
- /**
610
- * Return the Tango model backing this serializer.
611
- */
612
- getModel() {
613
- const model = this.constructor.model;
614
- if (!model) throw new Error(`${this.constructor.name} must define a static model or override getModel().`);
615
- return model;
616
- }
617
- /**
618
- * Return the manager used for create and update workflows.
619
- */
620
- getManager() {
621
- return this.getModel().objects;
622
- }
623
- /**
624
- * Return the declarative relation-field map for this serializer.
625
- */
626
- getRelationFields() {
627
- return this.constructor.relationFields ?? {};
628
- }
629
- /**
630
- * Merge relation-field read resolvers into the serializer output path.
631
- */
632
- getOutputResolvers() {
633
- const baseResolvers = super.getOutputResolvers();
634
- const relationFields = this.getRelationFields();
635
- return {
636
- ...Object.fromEntries(Object.entries(relationFields).map(([fieldName, field]) => [fieldName, async (record) => this.serializeRelationField(record, fieldName, field)])),
637
- ...baseResolvers
638
- };
639
- }
640
- /**
641
- * Validate, enrich, persist, and serialize a create workflow.
642
- */
643
- async create(input) {
644
- const validated = this.deserializeCreate(input);
645
- const relationWrites = this.extractRelationWrites(validated, input);
646
- const prepared = await this.beforeCreate(validated);
647
- const created = await this.getManager().create(this.stripRelationFields(prepared));
648
- await this.applyRelationWrites(created, relationWrites);
649
- return this.serialize(created);
650
- }
651
- /**
652
- * Validate, enrich, persist, and serialize an update workflow.
653
- */
654
- async update(id, input) {
655
- const validated = this.deserializeUpdate(input);
656
- const relationWrites = this.extractRelationWrites(validated, input);
657
- const prepared = await this.beforeUpdate(id, validated);
658
- const updated = await this.getManager().update(id, this.stripRelationFields(prepared));
659
- await this.applyRelationWrites(updated, relationWrites);
660
- return this.serialize(updated);
661
- }
662
- /**
663
- * Override to normalize create input for this resource workflow before the
664
- * manager call.
665
- *
666
- * Model-owned persistence rules belong in model hooks so they also run for
667
- * scripts and direct manager usage.
668
- */
669
- async beforeCreate(data) {
670
- return data;
671
- }
672
- /**
673
- * Override to normalize update input for this resource workflow before the
674
- * manager call.
675
- *
676
- * Model-owned persistence rules belong in model hooks so they also run for
677
- * scripts and direct manager usage.
678
- */
679
- async beforeUpdate(_id, data) {
680
- return data;
681
- }
682
- extractRelationWrites(data, rawInput) {
683
- if (typeof data !== "object" || data === null || typeof rawInput !== "object" || rawInput === null) return {};
684
- const relationFields = this.getRelationFields();
685
- const writes = {};
686
- for (const fieldName of Object.keys(relationFields)) if (fieldName in rawInput) writes[fieldName] = data[fieldName];
687
- return writes;
688
- }
689
- stripRelationFields(data) {
690
- const relationFieldNames = new Set(Object.keys(this.getRelationFields()));
691
- if (relationFieldNames.size === 0) return data;
692
- return Object.fromEntries(Object.entries(data).filter(([fieldName]) => !relationFieldNames.has(fieldName)));
693
- }
694
- async applyRelationWrites(record, writes) {
695
- const relationFields = this.getRelationFields();
696
- for (const [fieldName, value] of Object.entries(writes)) {
697
- if (!relationFields[fieldName]) continue;
698
- await this.syncManyToManyRelation(record, fieldName, value);
699
- }
700
- }
701
- async serializeRelationField(record, fieldName, field) {
702
- const rows = (await this.getManyToManyManager(record, fieldName).all().fetch()).results;
703
- const relationMeta = this.getManyToManyRelationMeta(fieldName);
704
- switch (field.read.kind) {
705
- case InternalManyToManyReadStrategyKind.PK_LIST: return rows.map((row) => row[relationMeta.targetPrimaryKey]);
706
- case InternalManyToManyReadStrategyKind.NESTED: return rows.map((row) => field.read.schema.parse(row));
707
- }
708
- }
709
- async syncManyToManyRelation(record, fieldName, value) {
710
- if (value === void 0) return;
711
- const manager = this.getManyToManyManager(record, fieldName);
712
- const field = this.getRelationFields()[fieldName];
713
- if (!field) return;
714
- const nextTargets = await this.resolveWriteTargets(fieldName, field.write, value);
715
- await manager.set(...nextTargets);
716
- }
717
- async resolveWriteTargets(fieldName, strategy, value) {
718
- switch (strategy.kind) {
719
- case InternalManyToManyWriteStrategyKind.PK_LIST:
720
- if (!Array.isArray(value)) throw new TypeError(`Relation field '${String(fieldName)}' expects an array of primary-key values.`);
721
- return value;
722
- case InternalManyToManyWriteStrategyKind.SLUG_LIST: {
723
- if (!Array.isArray(value)) throw new TypeError(`Relation field '${String(fieldName)}' expects an array of lookup values.`);
724
- const lookupValues = [...new Set(value.map((entry) => String(entry).trim()).filter(Boolean))];
725
- if (lookupValues.length === 0) return [];
726
- const filter = { [`${strategy.lookupField}__in`]: lookupValues };
727
- const existing = await strategy.model.objects.query().filter(filter).fetch();
728
- const byLookup = new Map(existing.results.map((row) => [String(row[strategy.lookupField]), row]));
729
- const resolved = [];
730
- for (const lookupValue of lookupValues) {
731
- const found = byLookup.get(lookupValue);
732
- if (found) {
733
- resolved.push(found);
734
- continue;
735
- }
736
- if (!strategy.createIfMissing) throw new Error(`Relation field '${String(fieldName)}' could not resolve '${lookupValue}' via '${strategy.lookupField}'.`);
737
- const created = await strategy.model.objects.create(strategy.buildCreateInput?.(lookupValue) ?? { [strategy.lookupField]: lookupValue });
738
- resolved.push(created);
739
- }
740
- return resolved;
741
- }
742
- }
743
- }
744
- getManyToManyManager(record, fieldName) {
745
- const manager = record[fieldName];
746
- if (!ManyToManyRelatedManager.isManyToManyRelatedManager(manager)) throw new Error(`Relation field '${fieldName}' is not backed by a many-to-many related manager.`);
747
- return manager;
748
- }
749
- getManyToManyRelationMeta(fieldName) {
750
- const relation = this.getManager().meta.relations?.[fieldName];
751
- if (!relation || relation.kind !== InternalSerializerRelationKind.MANY_TO_MANY) throw new Error(`Relation field '${fieldName}' is not a persisted many-to-many edge.`);
752
- return relation;
753
- }
754
- };
755
- //#endregion
756
- //#region src/serializer/relation.ts
757
- function pkList() {
758
- return { kind: InternalManyToManyReadStrategyKind.PK_LIST };
759
- }
760
- function nested(schema) {
761
- return {
762
- kind: InternalManyToManyReadStrategyKind.NESTED,
763
- schema
764
- };
765
- }
766
- function slugList(options) {
767
- return {
768
- kind: InternalManyToManyWriteStrategyKind.SLUG_LIST,
769
- ...options
770
- };
771
- }
772
- function manyToMany(config = {}) {
773
- return {
774
- kind: InternalSerializerRelationKind.MANY_TO_MANY,
775
- read: config.read ?? pkList(),
776
- write: config.write ?? pkList()
777
- };
778
- }
779
- const relation = {
780
- manyToMany,
781
- pkList,
782
- nested,
783
- slugList
784
- };
785
- //#endregion
786
- //#region src/serializer/index.ts
787
- var serializer_exports = /* @__PURE__ */ __exportAll({
788
- ModelSerializer: () => ModelSerializer,
789
- Serializer: () => Serializer,
790
- relation: () => relation
791
- });
792
- //#endregion
793
- //#region src/viewset/ModelViewSet.ts
794
- /**
795
- * Base class for creating RESTful API viewsets with built-in CRUD operations.
796
- * Provides list, retrieve, create, update, and delete methods with filtering,
797
- * search, pagination, and ordering support.
798
- */
799
- var ModelViewSet = class ModelViewSet {
800
- static BRAND = "tango.resources.model_view_set";
801
- static actions = [];
802
- __tangoBrand = ModelViewSet.BRAND;
803
- serializerClass;
804
- filters;
805
- orderingFields;
806
- searchFields;
807
- paginatorFactory;
808
- serializer;
809
- constructor(config) {
810
- this.serializerClass = config.serializer;
811
- this.filters = config.filters;
812
- this.orderingFields = config.orderingFields ?? [];
813
- this.searchFields = config.searchFields ?? [];
814
- this.paginatorFactory = config.paginatorFactory;
815
- }
816
- /**
817
- * Return the custom action descriptors declared by a viewset or constructor.
818
- */
819
- static getActions(viewsetOrConstructor) {
820
- const viewset = ModelViewSet.isModelViewSet(viewsetOrConstructor) ? viewsetOrConstructor : null;
821
- const constructorValue = viewset ? viewset.constructor : viewsetOrConstructor;
822
- return (Array.isArray(constructorValue.actions) ? constructorValue.actions : []).map((action) => ({
823
- ...action,
824
- path: viewset ? viewset.resolveActionPath(action) : ModelViewSet.resolvePathFromDescriptor(action.name, action.path)
825
- }));
826
- }
827
- /**
828
- * Narrow an unknown value to `ModelViewSet`.
829
- */
830
- static isModelViewSet(value) {
831
- return typeof value === "object" && value !== null && value.__tangoBrand === ModelViewSet.BRAND;
832
- }
833
- /**
834
- * Preserve literal action inference while validating the descriptor shape.
835
- */
836
- static defineViewSetActions(actions) {
837
- return actions;
838
- }
839
- static resolvePathFromDescriptor(name, explicitPath) {
840
- const normalized = (explicitPath?.trim() || ModelViewSet.toKebabCase(name)).replace(/^\/+|\/+$/g, "");
841
- if (!normalized) throw new Error(`Invalid custom action path for '${name}'.`);
842
- return normalized;
843
- }
844
- static toKebabCase(input) {
845
- return input.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
846
- }
847
- /**
848
- * Return the serializer class that owns this resource contract.
849
- */
850
- getSerializerClass() {
851
- return this.serializerClass;
852
- }
853
- /**
854
- * Return the serializer instance for the current resource.
855
- */
856
- getSerializer() {
857
- if (!this.serializer) this.serializer = new this.serializerClass();
858
- return this.serializer;
859
- }
860
- /**
861
- * Describe the public HTTP contract that this resource contributes to OpenAPI generation.
862
- */
863
- describeOpenAPI() {
864
- const model = this.requireModelMetadata();
865
- return {
866
- model,
867
- outputSchema: this.getSerializer().getOutputSchema(),
868
- createSchema: this.getSerializer().getCreateSchema(),
869
- updateSchema: this.getSerializer().getUpdateSchema(),
870
- searchFields: this.searchFields,
871
- orderingFields: this.orderingFields,
872
- lookupField: this.getLookupFieldFromMetadata(model),
873
- lookupParam: "id",
874
- allowedMethods: [
875
- "GET",
876
- "POST",
877
- "PUT",
878
- "PATCH",
879
- "DELETE"
880
- ],
881
- usesDefaultOffsetPagination: !this.paginatorFactory,
882
- actions: ModelViewSet.getActions(this)
883
- };
884
- }
885
- /**
886
- * List endpoint with filtering, search, ordering, and offset pagination.
887
- */
888
- async list(ctx) {
889
- try {
890
- const params = ctx.request.queryParams;
891
- const baseQueryset = this.getManager().query();
892
- const paginator = this.getPaginator(baseQueryset);
893
- paginator.parse(params);
894
- let qs = baseQueryset;
895
- if (this.filters) {
896
- const filterInputs = this.filters.withFieldParsers(inferModelFieldParsers(this.getSerializer().getModel())).apply(params);
897
- if (filterInputs.length > 0) qs = qs.filter(Q.and(...filterInputs));
898
- }
899
- const search = params.getSearch();
900
- if (search && this.searchFields.length > 0) {
901
- const searchFilters = this.searchFields.map((field) => {
902
- return { [`${String(field)}__icontains`]: search };
903
- });
904
- qs = qs.filter(Q.or(...searchFilters));
905
- }
906
- const ordering = params.getOrdering();
907
- if (ordering.length > 0) {
908
- const orderTokens = ordering.filter((field) => {
909
- const cleanField = field.startsWith("-") ? field.slice(1) : field;
910
- return this.orderingFields.includes(cleanField);
911
- });
912
- if (orderTokens.length > 0) qs = qs.orderBy(...orderTokens.map((token) => token));
913
- }
914
- const resultPromise = paginator.apply(qs).fetch();
915
- const totalCountPromise = paginator.needsTotalCount() ? qs.count() : Promise.resolve(void 0);
916
- const [result, totalCount] = await Promise.all([resultPromise, totalCountPromise]);
917
- const rows = await this.getSerializer().serializeMany(result.items);
918
- const response = paginator.toResponse(rows, { totalCount });
919
- return TangoResponse.json(response, { status: 200 });
920
- } catch (error) {
921
- return this.handleError(error);
922
- }
923
- }
924
- /**
925
- * Retrieve endpoint for a single resource by id.
926
- */
927
- async retrieve(_ctx, id) {
928
- try {
929
- const manager = this.getManager();
930
- const pk = manager.meta.pk;
931
- const filterById = { [pk]: id };
932
- const result = await manager.query().filter(filterById).fetchOne();
933
- if (!result) throw new NotFoundError(`No ${manager.meta.table} record found for ${String(pk)}=${id}.`);
934
- return TangoResponse.json(await this.getSerializer().serialize(result), { status: 200 });
935
- } catch (error) {
936
- return this.handleError(error);
937
- }
938
- }
939
- /**
940
- * Create endpoint: validate input, persist, and return serialized output.
941
- */
942
- async create(ctx) {
943
- try {
944
- const body = await ctx.request.json();
945
- const result = await this.getSerializer().create(body);
946
- return TangoResponse.created(void 0, result);
947
- } catch (error) {
948
- return this.handleError(error);
949
- }
950
- }
951
- /**
952
- * Update endpoint: validate partial payload and persist by id.
953
- */
954
- async update(ctx, id) {
955
- try {
956
- const body = await ctx.request.json();
957
- const pkValue = id;
958
- const result = await this.getSerializer().update(pkValue, body);
959
- return TangoResponse.json(result, { status: 200 });
960
- } catch (error) {
961
- return this.handleError(error);
962
- }
963
- }
964
- /**
965
- * Destroy endpoint: delete a resource by id.
966
- */
967
- async destroy(_ctx, id) {
968
- try {
969
- const pkValue = id;
970
- await this.getManager().delete(pkValue);
971
- return TangoResponse.noContent();
972
- } catch (error) {
973
- return this.handleError(error);
974
- }
975
- }
976
- getPaginator(queryset) {
977
- if (this.paginatorFactory) return this.paginatorFactory(queryset);
978
- return new OffsetPaginator(queryset);
979
- }
980
- getManager() {
981
- return this.getSerializer().getManager();
982
- }
983
- /**
984
- * Convert thrown errors into normalized HTTP responses.
985
- */
986
- handleError(error) {
987
- const httpError = HttpErrorFactory.toHttpError(error);
988
- return TangoResponse.json(httpError.body, { status: httpError.status });
989
- }
990
- /**
991
- * Resolve route path segment(s) for a custom action.
992
- * Override this in subclasses to customize path derivation globally.
993
- */
994
- resolveActionPath(action) {
995
- return ModelViewSet.resolvePathFromDescriptor(action.name, action.path);
996
- }
997
- requireModelMetadata() {
998
- const model = this.getSerializer().getModel();
999
- if (!model.metadata) throw new Error("OpenAPI generation requires Tango model metadata on ModelViewSet models.");
1000
- return model;
1001
- }
1002
- getLookupFieldFromMetadata(model) {
1003
- const primaryKeyField = model.metadata.fields.find((field) => field.primaryKey);
1004
- if (!primaryKeyField) throw new Error("OpenAPI generation requires a primary key field in Tango model metadata.");
1005
- return primaryKeyField.name;
1006
- }
1007
- };
1008
- //#endregion
1009
- //#region src/viewset/index.ts
1010
- var viewset_exports = /* @__PURE__ */ __exportAll({ ModelViewSet: () => ModelViewSet });
1011
- //#endregion
1
+ import { n as RequestContext, t as context_exports } from "./context-euBQvNRT.js";
2
+ import { n as FilterSet, t as filters_exports } from "./filters-46d2Nr5C.js";
3
+ import { n as OffsetPaginationInput, t as OffsetPaginator } from "./OffsetPaginator-CaycvxJU.js";
4
+ import { n as CursorPaginationInput, t as CursorPaginator } from "./CursorPaginator-B_8MhYZY.js";
5
+ import { t as pagination_exports } from "./pagination/index.js";
6
+ import { t as paginators_exports } from "./paginators/index.js";
7
+ import { t as resource_exports } from "./resource/index.js";
8
+ import { i as Serializer, n as relation, r as ModelSerializer, t as serializer_exports } from "./serializer-RSwlXWls.js";
9
+ import { n as ModelViewSet, t as viewset_exports } from "./viewset-C9j-2U29.js";
10
+ import { a as ListCreateAPIView, c as ListAPIView, d as RetrieveModelMixin, f as CreateModelMixin, h as APIView, i as RetrieveUpdateAPIView, l as DestroyModelMixin, m as GenericAPIView, n as RetrieveUpdateDestroyAPIView, o as RetrieveAPIView, p as ListModelMixin, r as RetrieveDestroyAPIView, s as CreateAPIView, t as view_exports, u as UpdateModelMixin } from "./view-CYdJAO4t.js";
1012
11
  export { APIView, CreateAPIView, CreateModelMixin, CursorPaginationInput, CursorPaginator, DestroyModelMixin, FilterSet, GenericAPIView, ListAPIView, ListCreateAPIView, ListModelMixin, ModelSerializer, ModelViewSet, OffsetPaginationInput, OffsetPaginator, RequestContext, RetrieveAPIView, RetrieveDestroyAPIView, RetrieveModelMixin, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, Serializer, UpdateModelMixin, context_exports as context, filters_exports as filters, pagination_exports as pagination, paginators_exports as paginators, relation, resource_exports as resource, serializer_exports as serializer, view_exports as view, viewset_exports as viewset };
1013
-
1014
- //# sourceMappingURL=index.js.map