@danceroutine/tango-resources 1.11.3 → 1.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/CursorPaginator-B_8MhYZY.js +195 -0
- package/dist/CursorPaginator-B_8MhYZY.js.map +1 -0
- package/dist/CursorPaginator-CfeMQCdJ.d.ts +177 -0
- package/dist/OffsetPaginator-CaycvxJU.js +188 -0
- package/dist/OffsetPaginator-CaycvxJU.js.map +1 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context-euBQvNRT.js +65 -0
- package/dist/context-euBQvNRT.js.map +1 -0
- package/dist/filters/index.d.ts +2 -0
- package/dist/filters/index.js +2 -0
- package/dist/filters-46d2Nr5C.js +287 -0
- package/dist/filters-46d2Nr5C.js.map +1 -0
- package/dist/index-Ac94YL5S.d.ts +24 -0
- package/dist/index-BJJalUDB.d.ts +54 -0
- package/dist/index-Bg6TtnmQ.d.ts +175 -0
- package/dist/index-C55GDIOn.d.ts +222 -0
- package/dist/index-CiIB-1Ac.d.ts +123 -0
- package/dist/index-DkJtxvKu.d.ts +164 -0
- package/dist/index.d.ts +10 -12
- package/dist/index.js +10 -1013
- package/dist/inferModelFieldParsers-2irv7j1T.js +70 -0
- package/dist/inferModelFieldParsers-2irv7j1T.js.map +1 -0
- package/dist/pagination/index.d.ts +3 -0
- package/dist/pagination/index.js +15 -0
- package/dist/pagination/index.js.map +1 -0
- package/dist/paginators/index.d.ts +9 -0
- package/dist/paginators/index.js +12 -0
- package/dist/paginators/index.js.map +1 -0
- package/dist/resource/index.d.ts +3 -0
- package/dist/resource/index.js +7 -0
- package/dist/resource/index.js.map +1 -0
- package/dist/serializer/index.d.ts +2 -0
- package/dist/serializer/index.js +2 -0
- package/dist/serializer-RSwlXWls.js +305 -0
- package/dist/serializer-RSwlXWls.js.map +1 -0
- package/dist/view/index.d.ts +2 -1
- package/dist/view/index.js +1 -1
- package/dist/{view-C9B5Lln3.js → view-CYdJAO4t.js} +7 -314
- package/dist/view-CYdJAO4t.js.map +1 -0
- package/dist/viewset/index.d.ts +9 -0
- package/dist/viewset/index.js +2 -0
- package/dist/viewset-C9j-2U29.js +227 -0
- package/dist/viewset-C9j-2U29.js.map +1 -0
- package/package.json +21 -17
- package/dist/index-D6sfTSEj.d.ts +0 -902
- package/dist/index.js.map +0 -1
- package/dist/view-C9B5Lln3.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,1014 +1,11 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|