@danceroutine/tango-resources 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/context/RequestContext.d.ts +23 -4
- package/dist/filters/FilterSet.d.ts +59 -4
- package/dist/filters/index.d.ts +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +515 -205
- package/dist/index.js.map +1 -1
- package/dist/pagination/CursorPaginationInput.d.ts +7 -0
- package/dist/pagination/OffsetPaginationInput.d.ts +7 -0
- package/dist/pagination/PaginatedResponse.d.ts +8 -2
- package/dist/pagination/Paginator.d.ts +5 -3
- package/dist/pagination/index.d.ts +5 -3
- package/dist/paginators/CursorPaginator.d.ts +32 -6
- package/dist/paginators/OffsetPaginator.d.ts +30 -7
- package/dist/resource/OpenAPIDescription.d.ts +21 -0
- package/dist/resource/ResourceModelLike.d.ts +16 -0
- package/dist/resource/index.d.ts +5 -0
- package/dist/serializer/ModelSerializer.d.ts +47 -0
- package/dist/serializer/Serializer.d.ts +52 -0
- package/dist/serializer/index.d.ts +5 -0
- package/dist/view/APIView.d.ts +26 -0
- package/dist/view/GenericAPIView.d.ts +57 -0
- package/dist/view/generics/CreateAPIView.d.ts +10 -0
- package/dist/view/generics/ListAPIView.d.ts +10 -0
- package/dist/view/generics/ListCreateAPIView.d.ts +11 -0
- package/dist/view/generics/RetrieveAPIView.d.ts +10 -0
- package/dist/view/generics/RetrieveDestroyAPIView.d.ts +11 -0
- package/dist/view/generics/RetrieveUpdateAPIView.d.ts +12 -0
- package/dist/view/generics/RetrieveUpdateDestroyAPIView.d.ts +13 -0
- package/dist/view/generics/index.d.ts +10 -0
- package/dist/view/index.d.ts +8 -0
- package/dist/view/index.js +3 -0
- package/dist/view/mixins/CreateModelMixin.d.ts +11 -0
- package/dist/view/mixins/DestroyModelMixin.d.ts +11 -0
- package/dist/view/mixins/ListModelMixin.d.ts +11 -0
- package/dist/view/mixins/RetrieveModelMixin.d.ts +11 -0
- package/dist/view/mixins/UpdateModelMixin.d.ts +12 -0
- package/dist/view/mixins/index.d.ts +8 -0
- package/dist/view-BNGEURL_.js +547 -0
- package/dist/view-BNGEURL_.js.map +1 -0
- package/dist/viewset/ModelViewSet.d.ts +91 -45
- package/dist/viewset/index.d.ts +2 -1
- package/package.json +75 -69
- package/dist/domain/index.d.ts +0 -8
- package/dist/pagination/PaginationInput.d.ts +0 -7
- package/dist/viewset/ModelViewSet.js +0 -143
package/dist/index.js
CHANGED
|
@@ -1,42 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { APIView, CreateAPIView, CreateModelMixin, DestroyModelMixin, GenericAPIView, ListAPIView, ListCreateAPIView, ListModelMixin, OffsetPaginationInput, OffsetPaginator, RetrieveAPIView, RetrieveDestroyAPIView, RetrieveModelMixin, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, UpdateModelMixin, __export, view_exports } from "./view-BNGEURL_.js";
|
|
2
|
+
import { HttpErrorFactory, NotFoundError, TangoRequest, TangoResponse } from "@danceroutine/tango-core";
|
|
3
|
+
import { z, z as z$1 } from "zod";
|
|
3
4
|
import { Q } from "@danceroutine/tango-orm";
|
|
4
5
|
|
|
5
|
-
//#region rolldown:runtime
|
|
6
|
-
var __defProp = Object.defineProperty;
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all) __defProp(target, name, {
|
|
9
|
-
get: all[name],
|
|
10
|
-
enumerable: true
|
|
11
|
-
});
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
//#endregion
|
|
15
6
|
//#region src/context/RequestContext.ts
|
|
16
7
|
var RequestContext = class RequestContext {
|
|
17
8
|
static BRAND = "tango.resources.request_context";
|
|
18
|
-
state = new Map();
|
|
19
9
|
__tangoBrand = RequestContext.BRAND;
|
|
10
|
+
state = new Map();
|
|
20
11
|
constructor(request, user = null, params = {}) {
|
|
21
12
|
this.request = request;
|
|
22
13
|
this.user = user;
|
|
23
14
|
this.params = params;
|
|
24
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Narrow an unknown value to `RequestContext`.
|
|
18
|
+
*/
|
|
25
19
|
static isRequestContext(value) {
|
|
26
20
|
return typeof value === "object" && value !== null && value.__tangoBrand === RequestContext.BRAND;
|
|
27
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Construct a context with optional user payload.
|
|
24
|
+
*/
|
|
25
|
+
static create(request, user) {
|
|
26
|
+
return new RequestContext(TangoRequest.isTangoRequest(request) ? request : new TangoRequest(request), user ?? null);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Store arbitrary per-request state for downstream middleware/handlers.
|
|
30
|
+
*/
|
|
28
31
|
setState(key, value) {
|
|
29
32
|
this.state.set(key, value);
|
|
30
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Retrieve previously stored request state.
|
|
36
|
+
*/
|
|
31
37
|
getState(key) {
|
|
32
38
|
return this.state.get(key);
|
|
33
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Check whether a state key has been set.
|
|
42
|
+
*/
|
|
34
43
|
hasState(key) {
|
|
35
44
|
return this.state.has(key);
|
|
36
45
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Clone the context, including route params and request-local state.
|
|
48
|
+
*/
|
|
40
49
|
clone() {
|
|
41
50
|
const cloned = new RequestContext(this.request, this.user, { ...this.params });
|
|
42
51
|
cloned.state = new Map(this.state);
|
|
@@ -64,29 +73,200 @@ const InternalFilterType = {
|
|
|
64
73
|
var FilterSet = class FilterSet {
|
|
65
74
|
static BRAND = "tango.resources.filter_set";
|
|
66
75
|
__tangoBrand = FilterSet.BRAND;
|
|
76
|
+
/**
|
|
77
|
+
* Resolve matching query parameters into ORM filter inputs.
|
|
78
|
+
*/
|
|
79
|
+
constructor(spec, allowAllParams = false) {
|
|
80
|
+
this.spec = spec;
|
|
81
|
+
this.allowAllParams = allowAllParams;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Build a filter set from Django-style field declarations.
|
|
85
|
+
*/
|
|
86
|
+
static define(config) {
|
|
87
|
+
const spec = FilterSet.normalizeDefineConfig(config);
|
|
88
|
+
return new FilterSet(spec, config.all === "__all__");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Narrow an unknown value to `FilterSet`.
|
|
92
|
+
*/
|
|
67
93
|
static isFilterSet(value) {
|
|
68
94
|
return typeof value === "object" && value !== null && value.__tangoBrand === FilterSet.BRAND;
|
|
69
95
|
}
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
static normalizeDefineConfig(config) {
|
|
97
|
+
const spec = {};
|
|
98
|
+
const fieldDeclarations = config.fields ?? {};
|
|
99
|
+
const fieldParsers = config.parsers ?? {};
|
|
100
|
+
for (const rawField of Object.keys(fieldDeclarations)) {
|
|
101
|
+
const declaration = fieldDeclarations[rawField];
|
|
102
|
+
if (declaration === undefined) continue;
|
|
103
|
+
const parser = fieldParsers[rawField];
|
|
104
|
+
FilterSet.addFieldDeclaration(spec, rawField, declaration, parser);
|
|
105
|
+
}
|
|
106
|
+
const aliases = config.aliases ?? {};
|
|
107
|
+
for (const [param, declaration] of Object.entries(aliases)) spec[param] = FilterSet.normalizeAliasDeclaration(declaration);
|
|
108
|
+
return spec;
|
|
109
|
+
}
|
|
110
|
+
static addFieldDeclaration(spec, field, declaration, parser) {
|
|
111
|
+
if (declaration === true) {
|
|
112
|
+
spec[String(field)] = FilterSet.createLookupResolver(field, "exact", parser);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (FilterSet.isLookupArray(declaration)) {
|
|
116
|
+
for (const lookup of declaration) {
|
|
117
|
+
const param = FilterSet.resolveLookupParam(String(field), lookup);
|
|
118
|
+
spec[param] = FilterSet.createLookupResolver(field, lookup, parser);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const lookups = declaration.lookups ?? ["exact"];
|
|
123
|
+
const baseParam = declaration.param ?? String(field);
|
|
124
|
+
const effectiveParser = declaration.parse ?? parser;
|
|
125
|
+
for (const lookup of lookups) {
|
|
126
|
+
const param = FilterSet.resolveLookupParam(baseParam, lookup);
|
|
127
|
+
spec[param] = FilterSet.createLookupResolver(field, lookup, effectiveParser);
|
|
128
|
+
}
|
|
72
129
|
}
|
|
130
|
+
static isLookupArray(value) {
|
|
131
|
+
return Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
static normalizeAliasDeclaration(declaration) {
|
|
134
|
+
if (FilterSet.isFilterResolverDeclaration(declaration)) return declaration;
|
|
135
|
+
if ("fields" in declaration) {
|
|
136
|
+
const lookup = declaration.lookup ?? "icontains";
|
|
137
|
+
return FilterSet.createMultiFieldResolver(declaration.fields, lookup, declaration.parse);
|
|
138
|
+
}
|
|
139
|
+
return FilterSet.createLookupResolver(declaration.field, declaration.lookup ?? "exact", declaration.parse);
|
|
140
|
+
}
|
|
141
|
+
static isFilterResolverDeclaration(value) {
|
|
142
|
+
if (typeof value !== "object" || value === null || !("type" in value)) return false;
|
|
143
|
+
return [
|
|
144
|
+
InternalFilterType.SCALAR,
|
|
145
|
+
InternalFilterType.ILIKE,
|
|
146
|
+
InternalFilterType.RANGE,
|
|
147
|
+
InternalFilterType.IN,
|
|
148
|
+
InternalFilterType.CUSTOM
|
|
149
|
+
].includes(value.type);
|
|
150
|
+
}
|
|
151
|
+
static createMultiFieldResolver(fields, lookup, parser) {
|
|
152
|
+
if (lookup === "icontains" && parser === undefined) return {
|
|
153
|
+
type: InternalFilterType.ILIKE,
|
|
154
|
+
columns: [...fields]
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
type: InternalFilterType.CUSTOM,
|
|
158
|
+
apply: (raw) => {
|
|
159
|
+
const parsed = FilterSet.resolveParserValue(raw, parser);
|
|
160
|
+
if (parsed === undefined) return undefined;
|
|
161
|
+
const composed = {};
|
|
162
|
+
for (const field of fields) {
|
|
163
|
+
const segment = FilterSet.resolveLookupFilter(field, lookup, parsed);
|
|
164
|
+
if (!segment) continue;
|
|
165
|
+
Object.assign(composed, segment);
|
|
166
|
+
}
|
|
167
|
+
return Object.keys(composed).length > 0 ? composed : undefined;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
static createLookupResolver(field, lookup, parser) {
|
|
172
|
+
if (parser !== undefined) return {
|
|
173
|
+
type: InternalFilterType.CUSTOM,
|
|
174
|
+
apply: (raw) => {
|
|
175
|
+
const parsed = FilterSet.resolveParserValue(raw, parser);
|
|
176
|
+
if (parsed === undefined) return undefined;
|
|
177
|
+
return FilterSet.resolveLookupFilter(field, lookup, parsed);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
switch (lookup) {
|
|
181
|
+
case "exact": return {
|
|
182
|
+
type: InternalFilterType.SCALAR,
|
|
183
|
+
column: field
|
|
184
|
+
};
|
|
185
|
+
case "in": return {
|
|
186
|
+
type: InternalFilterType.IN,
|
|
187
|
+
column: field
|
|
188
|
+
};
|
|
189
|
+
case "lt":
|
|
190
|
+
case "lte":
|
|
191
|
+
case "gt":
|
|
192
|
+
case "gte": return {
|
|
193
|
+
type: InternalFilterType.RANGE,
|
|
194
|
+
column: field,
|
|
195
|
+
op: lookup
|
|
196
|
+
};
|
|
197
|
+
case "icontains": return {
|
|
198
|
+
type: InternalFilterType.ILIKE,
|
|
199
|
+
columns: [field]
|
|
200
|
+
};
|
|
201
|
+
default: return {
|
|
202
|
+
type: InternalFilterType.CUSTOM,
|
|
203
|
+
apply: (raw) => FilterSet.resolveLookupFilter(field, lookup, raw)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
static resolveLookupFilter(field, lookup, value) {
|
|
208
|
+
if (value === undefined) return undefined;
|
|
209
|
+
if (lookup === "exact") return { [field]: value };
|
|
210
|
+
if (lookup === "in") {
|
|
211
|
+
const arr = Array.isArray(value) ? value : String(value).split(",");
|
|
212
|
+
const lookupKey$1 = `${String(field)}__in`;
|
|
213
|
+
return { [lookupKey$1]: arr };
|
|
214
|
+
}
|
|
215
|
+
if (lookup === "icontains") {
|
|
216
|
+
const lookupKey$1 = `${String(field)}__icontains`;
|
|
217
|
+
return { [lookupKey$1]: `%${FilterSet.toScalarString(value)}%` };
|
|
218
|
+
}
|
|
219
|
+
const lookupKey = `${String(field)}__${lookup}`;
|
|
220
|
+
return { [lookupKey]: value };
|
|
221
|
+
}
|
|
222
|
+
static resolveLookupParam(baseParam, lookup) {
|
|
223
|
+
if (lookup === "exact") return baseParam;
|
|
224
|
+
return `${baseParam}__${lookup}`;
|
|
225
|
+
}
|
|
226
|
+
static resolveParserValue(value, parser) {
|
|
227
|
+
if (value === undefined) return undefined;
|
|
228
|
+
if (parser === undefined) return value;
|
|
229
|
+
return parser(value);
|
|
230
|
+
}
|
|
231
|
+
static toScalarString(value) {
|
|
232
|
+
if (Array.isArray(value)) return value.join(",");
|
|
233
|
+
return String(value);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Apply all configured resolvers against query params.
|
|
237
|
+
*/
|
|
73
238
|
apply(params) {
|
|
74
239
|
const filters = [];
|
|
75
|
-
|
|
240
|
+
const keys = new Set();
|
|
241
|
+
for (const [key] of params.entries()) keys.add(key);
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
const resolver = this.spec[key] ?? (this.allowAllParams ? this.buildAllResolver(key) : undefined);
|
|
244
|
+
if (!resolver) continue;
|
|
76
245
|
const rawValue = params.getAll(key);
|
|
77
|
-
const value = rawValue.length > 1 ? rawValue :
|
|
246
|
+
const value = rawValue.length > 1 ? rawValue : rawValue[0];
|
|
78
247
|
if (value === undefined) continue;
|
|
79
248
|
const filter = this.resolveFilter(resolver, value);
|
|
80
249
|
if (filter) filters.push(filter);
|
|
81
250
|
}
|
|
82
251
|
return filters;
|
|
83
252
|
}
|
|
253
|
+
buildAllResolver(param) {
|
|
254
|
+
const [rawField, ...rawLookupParts] = param.split("__");
|
|
255
|
+
if (!rawField) return undefined;
|
|
256
|
+
const field = rawField;
|
|
257
|
+
if (rawLookupParts.length === 0) return {
|
|
258
|
+
type: InternalFilterType.SCALAR,
|
|
259
|
+
column: field
|
|
260
|
+
};
|
|
261
|
+
const lookup = rawLookupParts.join("__");
|
|
262
|
+
return FilterSet.createLookupResolver(field, lookup);
|
|
263
|
+
}
|
|
84
264
|
resolveFilter(resolver, value) {
|
|
85
265
|
if (value === undefined) return undefined;
|
|
86
266
|
switch (resolver.type) {
|
|
87
267
|
case InternalFilterType.SCALAR: return { [resolver.column]: value };
|
|
88
268
|
case InternalFilterType.ILIKE: {
|
|
89
|
-
const pattern = `%${value}%`;
|
|
269
|
+
const pattern = `%${FilterSet.toScalarString(value)}%`;
|
|
90
270
|
const filter = {};
|
|
91
271
|
resolver.columns.forEach((col) => {
|
|
92
272
|
filter[`${String(col)}__icontains`] = pattern;
|
|
@@ -114,136 +294,36 @@ var filters_exports = {};
|
|
|
114
294
|
__export(filters_exports, { FilterSet: () => FilterSet });
|
|
115
295
|
|
|
116
296
|
//#endregion
|
|
117
|
-
//#region src/pagination/
|
|
118
|
-
const
|
|
119
|
-
limit: z
|
|
120
|
-
|
|
121
|
-
|
|
297
|
+
//#region src/pagination/CursorPaginationInput.ts
|
|
298
|
+
const CursorPaginationInput = z$1.object({
|
|
299
|
+
limit: z$1.preprocess((value) => {
|
|
300
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
301
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
302
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
303
|
+
return parsed;
|
|
304
|
+
}, z$1.number().int().min(1).transform((value) => Math.min(value, 100)).optional()),
|
|
305
|
+
cursor: z$1.string().nullable().default(null),
|
|
306
|
+
ordering: z$1.string().optional()
|
|
122
307
|
});
|
|
123
308
|
|
|
124
|
-
//#endregion
|
|
125
|
-
//#region src/paginators/OffsetPaginator.ts
|
|
126
|
-
var OffsetPage = class OffsetPage {
|
|
127
|
-
static BRAND = "tango.resources.offset_page";
|
|
128
|
-
__tangoBrand = OffsetPage.BRAND;
|
|
129
|
-
static isOffsetPage(value) {
|
|
130
|
-
return typeof value === "object" && value !== null && value.__tangoBrand === OffsetPage.BRAND;
|
|
131
|
-
}
|
|
132
|
-
constructor(results, pageNumber, perPage, totalCount) {
|
|
133
|
-
this.results = results;
|
|
134
|
-
this.pageNumber = pageNumber;
|
|
135
|
-
this.perPage = perPage;
|
|
136
|
-
this.totalCount = totalCount;
|
|
137
|
-
}
|
|
138
|
-
hasNext() {
|
|
139
|
-
if (this.totalCount === undefined) return false;
|
|
140
|
-
return this.endIndex() < this.totalCount;
|
|
141
|
-
}
|
|
142
|
-
hasPrevious() {
|
|
143
|
-
return this.pageNumber > 1;
|
|
144
|
-
}
|
|
145
|
-
nextPageNumber() {
|
|
146
|
-
return this.hasNext() ? this.pageNumber + 1 : null;
|
|
147
|
-
}
|
|
148
|
-
previousPageNumber() {
|
|
149
|
-
return this.hasPrevious() ? this.pageNumber - 1 : null;
|
|
150
|
-
}
|
|
151
|
-
startIndex() {
|
|
152
|
-
return (this.pageNumber - 1) * this.perPage;
|
|
153
|
-
}
|
|
154
|
-
endIndex() {
|
|
155
|
-
return this.startIndex() + this.results.length;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
var OffsetPaginator = class OffsetPaginator {
|
|
159
|
-
static BRAND = "tango.resources.offset_paginator";
|
|
160
|
-
__tangoBrand = OffsetPaginator.BRAND;
|
|
161
|
-
limit = 25;
|
|
162
|
-
offset = 0;
|
|
163
|
-
constructor(queryset, perPage = 25) {
|
|
164
|
-
this.queryset = queryset;
|
|
165
|
-
this.perPage = perPage;
|
|
166
|
-
this.limit = perPage;
|
|
167
|
-
}
|
|
168
|
-
static isOffsetPaginator(value) {
|
|
169
|
-
return typeof value === "object" && value !== null && value.__tangoBrand === OffsetPaginator.BRAND;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Parse limit, offset, and page from URL search params.
|
|
173
|
-
* If `page` is provided, it's converted to an offset.
|
|
174
|
-
* Stores parsed values for use by getPaginatedResponse.
|
|
175
|
-
*/
|
|
176
|
-
parse(params) {
|
|
177
|
-
const input = {
|
|
178
|
-
limit: params.get("limit") ?? undefined,
|
|
179
|
-
offset: params.get("offset") ?? undefined,
|
|
180
|
-
page: params.get("page") ?? undefined
|
|
181
|
-
};
|
|
182
|
-
const parsed = PaginationInput.parse(input);
|
|
183
|
-
if (parsed.page) parsed.offset = (parsed.page - 1) * parsed.limit;
|
|
184
|
-
this.limit = parsed.limit;
|
|
185
|
-
this.offset = parsed.offset;
|
|
186
|
-
}
|
|
187
|
-
parseParams(params) {
|
|
188
|
-
this.parse(params);
|
|
189
|
-
return {
|
|
190
|
-
limit: this.limit,
|
|
191
|
-
offset: this.offset
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Build a DRF-style paginated response with count, next, and previous links.
|
|
196
|
-
* Uses the limit/offset stored from the most recent parseParams call.
|
|
197
|
-
*/
|
|
198
|
-
toResponse(results, context) {
|
|
199
|
-
const totalCount = context?.totalCount;
|
|
200
|
-
const response = { results };
|
|
201
|
-
if (totalCount !== undefined) {
|
|
202
|
-
response.count = totalCount;
|
|
203
|
-
if (this.offset + this.limit < totalCount) response.next = `?limit=${this.limit}&offset=${this.offset + this.limit}`;
|
|
204
|
-
if (this.offset > 0) {
|
|
205
|
-
const prevOffset = Math.max(0, this.offset - this.limit);
|
|
206
|
-
response.previous = `?limit=${this.limit}&offset=${prevOffset}`;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return response;
|
|
210
|
-
}
|
|
211
|
-
getPaginatedResponse(results, totalCount) {
|
|
212
|
-
return this.toResponse(results, { totalCount });
|
|
213
|
-
}
|
|
214
|
-
apply(queryset) {
|
|
215
|
-
return queryset.limit(this.limit).offset(this.offset);
|
|
216
|
-
}
|
|
217
|
-
async paginate(page) {
|
|
218
|
-
return this.getPage(page);
|
|
219
|
-
}
|
|
220
|
-
async getPage(page) {
|
|
221
|
-
const offset = (page - 1) * this.perPage;
|
|
222
|
-
const results = await this.queryset.offset(offset).limit(this.perPage).fetch();
|
|
223
|
-
const totalCount = await this.count();
|
|
224
|
-
return new OffsetPage(results.results, page, this.perPage, totalCount);
|
|
225
|
-
}
|
|
226
|
-
async count() {
|
|
227
|
-
return this.queryset.count();
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
|
|
231
309
|
//#endregion
|
|
232
310
|
//#region src/paginators/CursorPaginator.ts
|
|
233
311
|
var CursorPage = class CursorPage {
|
|
234
312
|
static BRAND = "tango.resources.cursor_page";
|
|
235
313
|
__tangoBrand = CursorPage.BRAND;
|
|
236
|
-
static isCursorPage(value) {
|
|
237
|
-
return typeof value === "object" && value !== null && value.__tangoBrand === CursorPage.BRAND;
|
|
238
|
-
}
|
|
239
314
|
constructor(results, nextCursor, previousCursor) {
|
|
240
315
|
this.results = results;
|
|
241
316
|
this.nextCursor = nextCursor;
|
|
242
317
|
this.previousCursor = previousCursor;
|
|
243
318
|
}
|
|
319
|
+
static isCursorPage(value) {
|
|
320
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === CursorPage.BRAND;
|
|
321
|
+
}
|
|
322
|
+
/** Whether a next cursor token exists. */
|
|
244
323
|
hasNext() {
|
|
245
324
|
return this.nextCursor !== null;
|
|
246
325
|
}
|
|
326
|
+
/** Whether a previous cursor token exists. */
|
|
247
327
|
hasPrevious() {
|
|
248
328
|
return this.previousCursor !== null;
|
|
249
329
|
}
|
|
@@ -274,21 +354,33 @@ var CursorPaginator = class CursorPaginator {
|
|
|
274
354
|
this.cursorField = cursorField;
|
|
275
355
|
this.limit = perPage;
|
|
276
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Narrow an unknown value to `CursorPaginator`.
|
|
359
|
+
*/
|
|
277
360
|
static isCursorPaginator(value) {
|
|
278
361
|
return typeof value === "object" && value !== null && value.__tangoBrand === CursorPaginator.BRAND;
|
|
279
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Parse cursor pagination parameters from Tango query params.
|
|
365
|
+
*/
|
|
280
366
|
parse(params) {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
367
|
+
const parsed = CursorPaginationInput.parse({
|
|
368
|
+
limit: params.get("limit") ?? undefined,
|
|
369
|
+
cursor: params.get("cursor"),
|
|
370
|
+
ordering: params.get("ordering") ?? undefined
|
|
371
|
+
});
|
|
372
|
+
this.limit = parsed.limit ?? this.perPage;
|
|
373
|
+
this.cursor = parsed.cursor;
|
|
374
|
+
const ordering = parsed.ordering;
|
|
286
375
|
if (ordering) {
|
|
287
376
|
const parsedDirection = ordering.startsWith("-") ? "desc" : "asc";
|
|
288
377
|
const parsedField = ordering.startsWith("-") ? ordering.slice(1) : ordering;
|
|
289
378
|
this.direction = parsedField === String(this.cursorField) ? parsedDirection : "asc";
|
|
290
379
|
} else this.direction = "asc";
|
|
291
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Parse params and return compatibility `{ limit, offset }` shape.
|
|
383
|
+
*/
|
|
292
384
|
parseParams(params) {
|
|
293
385
|
this.parse(params);
|
|
294
386
|
return {
|
|
@@ -296,15 +388,27 @@ else this.limit = this.perPage;
|
|
|
296
388
|
offset: 0
|
|
297
389
|
};
|
|
298
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Build a paginated response payload with cursor links.
|
|
393
|
+
*/
|
|
394
|
+
needsTotalCount() {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
299
397
|
toResponse(results) {
|
|
300
398
|
const response = { results };
|
|
301
399
|
if (this.nextCursor) response.next = this.buildPageLink(this.nextCursor);
|
|
302
400
|
if (this.previousCursor) response.previous = this.buildPageLink(this.previousCursor);
|
|
303
401
|
return response;
|
|
304
402
|
}
|
|
403
|
+
/**
|
|
404
|
+
* Backward-compatible alias for `toResponse`.
|
|
405
|
+
*/
|
|
305
406
|
getPaginatedResponse(results, _totalCount) {
|
|
306
407
|
return this.toResponse(results);
|
|
307
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Apply cursor constraints and ordering to a queryset.
|
|
411
|
+
*/
|
|
308
412
|
apply(queryset) {
|
|
309
413
|
let qs = queryset.limit(this.limit + 1);
|
|
310
414
|
if (this.cursor) {
|
|
@@ -318,6 +422,9 @@ else this.limit = this.perPage;
|
|
|
318
422
|
const orderToken = this.direction === "asc" ? String(this.cursorField) : `-${String(this.cursorField)}`;
|
|
319
423
|
return qs.orderBy(orderToken);
|
|
320
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Fetch the next cursor page.
|
|
427
|
+
*/
|
|
321
428
|
async paginate(cursor) {
|
|
322
429
|
const appliedCursor = cursor ?? this.cursor;
|
|
323
430
|
this.cursor = appliedCursor;
|
|
@@ -330,6 +437,9 @@ else this.limit = this.perPage;
|
|
|
330
437
|
this.nextCursor = hasMore && last ? this.encodeCursor(last) : null;
|
|
331
438
|
return new CursorPage(results, this.nextCursor, this.previousCursor);
|
|
332
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* Cursor paginators only support page `1` as an entry point.
|
|
442
|
+
*/
|
|
333
443
|
async getPage(page) {
|
|
334
444
|
if (page !== 1) throw new Error("CursorPaginator only supports getPage(1). Use cursor pagination for subsequent pages.");
|
|
335
445
|
return this.paginate();
|
|
@@ -363,9 +473,10 @@ else this.limit = this.perPage;
|
|
|
363
473
|
//#region src/pagination/index.ts
|
|
364
474
|
var pagination_exports = {};
|
|
365
475
|
__export(pagination_exports, {
|
|
476
|
+
CursorPaginationInput: () => CursorPaginationInput,
|
|
366
477
|
CursorPaginator: () => CursorPaginator,
|
|
367
|
-
|
|
368
|
-
|
|
478
|
+
OffsetPaginationInput: () => OffsetPaginationInput,
|
|
479
|
+
OffsetPaginator: () => OffsetPaginator
|
|
369
480
|
});
|
|
370
481
|
|
|
371
482
|
//#endregion
|
|
@@ -376,41 +487,229 @@ __export(paginators_exports, {
|
|
|
376
487
|
OffsetPaginator: () => OffsetPaginator
|
|
377
488
|
});
|
|
378
489
|
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/resource/index.ts
|
|
492
|
+
var resource_exports = {};
|
|
493
|
+
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/serializer/Serializer.ts
|
|
496
|
+
var Serializer = class {
|
|
497
|
+
static createSchema = z.unknown();
|
|
498
|
+
static updateSchema = z.unknown();
|
|
499
|
+
static outputSchema = z.unknown();
|
|
500
|
+
/**
|
|
501
|
+
* Return the serializer class for the current instance.
|
|
502
|
+
*/
|
|
503
|
+
getSerializerClass() {
|
|
504
|
+
return this.constructor;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Return the Zod schema used for create payloads.
|
|
508
|
+
*/
|
|
509
|
+
getCreateSchema() {
|
|
510
|
+
return this.getSerializerClass().createSchema;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Return the Zod schema used for update payloads.
|
|
514
|
+
*/
|
|
515
|
+
getUpdateSchema() {
|
|
516
|
+
return this.getSerializerClass().updateSchema;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Return the Zod schema used for serialized output.
|
|
520
|
+
*/
|
|
521
|
+
getOutputSchema() {
|
|
522
|
+
return this.getSerializerClass().outputSchema;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Validate unknown input for create workflows.
|
|
526
|
+
*/
|
|
527
|
+
deserializeCreate(input) {
|
|
528
|
+
return this.getCreateSchema().parse(input);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Validate unknown input for update workflows.
|
|
532
|
+
*/
|
|
533
|
+
deserializeUpdate(input) {
|
|
534
|
+
return this.getUpdateSchema().parse(input);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Convert a persisted record into its outward-facing representation.
|
|
538
|
+
*/
|
|
539
|
+
toRepresentation(record) {
|
|
540
|
+
return this.getOutputSchema().parse(record);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
//#endregion
|
|
545
|
+
//#region src/serializer/ModelSerializer.ts
|
|
546
|
+
var ModelSerializer = class extends Serializer {
|
|
547
|
+
static model;
|
|
548
|
+
/**
|
|
549
|
+
* Return the Tango model backing this serializer.
|
|
550
|
+
*/
|
|
551
|
+
getModel() {
|
|
552
|
+
const model = this.constructor.model;
|
|
553
|
+
if (!model) throw new Error(`${this.constructor.name} must define a static model or override getModel().`);
|
|
554
|
+
return model;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Return the manager used for create and update workflows.
|
|
558
|
+
*/
|
|
559
|
+
getManager() {
|
|
560
|
+
return this.getModel().objects;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Validate, enrich, persist, and serialize a create workflow.
|
|
564
|
+
*/
|
|
565
|
+
async create(input) {
|
|
566
|
+
const validated = this.deserializeCreate(input);
|
|
567
|
+
const prepared = await this.beforeCreate(validated);
|
|
568
|
+
const created = await this.getManager().create(prepared);
|
|
569
|
+
return this.toRepresentation(created);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Validate, enrich, persist, and serialize an update workflow.
|
|
573
|
+
*/
|
|
574
|
+
async update(id, input) {
|
|
575
|
+
const validated = this.deserializeUpdate(input);
|
|
576
|
+
const prepared = await this.beforeUpdate(id, validated);
|
|
577
|
+
const updated = await this.getManager().update(id, prepared);
|
|
578
|
+
return this.toRepresentation(updated);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Override to normalize create input for this resource workflow before the
|
|
582
|
+
* manager call.
|
|
583
|
+
*
|
|
584
|
+
* Model-owned persistence rules belong in model hooks so they also run for
|
|
585
|
+
* scripts and direct manager usage.
|
|
586
|
+
*/
|
|
587
|
+
async beforeCreate(data) {
|
|
588
|
+
return data;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Override to normalize update input for this resource workflow before the
|
|
592
|
+
* manager call.
|
|
593
|
+
*
|
|
594
|
+
* Model-owned persistence rules belong in model hooks so they also run for
|
|
595
|
+
* scripts and direct manager usage.
|
|
596
|
+
*/
|
|
597
|
+
async beforeUpdate(_id, data) {
|
|
598
|
+
return data;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/serializer/index.ts
|
|
604
|
+
var serializer_exports = {};
|
|
605
|
+
__export(serializer_exports, {
|
|
606
|
+
ModelSerializer: () => ModelSerializer,
|
|
607
|
+
Serializer: () => Serializer
|
|
608
|
+
});
|
|
609
|
+
|
|
379
610
|
//#endregion
|
|
380
611
|
//#region src/viewset/ModelViewSet.ts
|
|
381
612
|
var ModelViewSet = class ModelViewSet {
|
|
382
613
|
static BRAND = "tango.resources.model_view_set";
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
updateSchema;
|
|
614
|
+
static actions = [];
|
|
615
|
+
__tangoBrand = ModelViewSet.BRAND;
|
|
616
|
+
serializerClass;
|
|
387
617
|
filters;
|
|
388
618
|
orderingFields;
|
|
389
619
|
searchFields;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return typeof value === "object" && value !== null && value.__tangoBrand === ModelViewSet.BRAND;
|
|
393
|
-
}
|
|
620
|
+
paginatorFactory;
|
|
621
|
+
serializer;
|
|
394
622
|
constructor(config) {
|
|
395
|
-
this.
|
|
396
|
-
this.readSchema = config.readSchema;
|
|
397
|
-
this.writeSchema = config.writeSchema;
|
|
398
|
-
this.updateSchema = config.updateSchema ?? config.writeSchema.partial();
|
|
623
|
+
this.serializerClass = config.serializer;
|
|
399
624
|
this.filters = config.filters;
|
|
400
625
|
this.orderingFields = config.orderingFields ?? [];
|
|
401
626
|
this.searchFields = config.searchFields ?? [];
|
|
627
|
+
this.paginatorFactory = config.paginatorFactory;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Return the custom action descriptors declared by a viewset or constructor.
|
|
631
|
+
*/
|
|
632
|
+
static getActions(viewsetOrConstructor) {
|
|
633
|
+
const viewset = ModelViewSet.isModelViewSet(viewsetOrConstructor) ? viewsetOrConstructor : null;
|
|
634
|
+
const constructorValue = viewset ? viewset.constructor : viewsetOrConstructor;
|
|
635
|
+
const actions = Array.isArray(constructorValue.actions) ? constructorValue.actions : [];
|
|
636
|
+
return actions.map((action) => ({
|
|
637
|
+
...action,
|
|
638
|
+
path: viewset ? viewset.resolveActionPath(action) : ModelViewSet.resolvePathFromDescriptor(action.name, action.path)
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Narrow an unknown value to `ModelViewSet`.
|
|
643
|
+
*/
|
|
644
|
+
static isModelViewSet(value) {
|
|
645
|
+
return typeof value === "object" && value !== null && value.__tangoBrand === ModelViewSet.BRAND;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Preserve literal action inference while validating the descriptor shape.
|
|
649
|
+
*/
|
|
650
|
+
static defineViewSetActions(actions) {
|
|
651
|
+
return actions;
|
|
652
|
+
}
|
|
653
|
+
static resolvePathFromDescriptor(name, explicitPath) {
|
|
654
|
+
const normalized = (explicitPath?.trim() || ModelViewSet.toKebabCase(name)).replace(/^\/+|\/+$/g, "");
|
|
655
|
+
if (!normalized) throw new Error(`Invalid custom action path for '${name}'.`);
|
|
656
|
+
return normalized;
|
|
657
|
+
}
|
|
658
|
+
static toKebabCase(input) {
|
|
659
|
+
return input.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Return the serializer class that owns this resource contract.
|
|
663
|
+
*/
|
|
664
|
+
getSerializerClass() {
|
|
665
|
+
return this.serializerClass;
|
|
402
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* Return the serializer instance for the current resource.
|
|
669
|
+
*/
|
|
670
|
+
getSerializer() {
|
|
671
|
+
if (!this.serializer) this.serializer = new this.serializerClass();
|
|
672
|
+
return this.serializer;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Describe the public HTTP contract that this resource contributes to OpenAPI generation.
|
|
676
|
+
*/
|
|
677
|
+
describeOpenAPI() {
|
|
678
|
+
return {
|
|
679
|
+
model: this.requireModelMetadata(),
|
|
680
|
+
outputSchema: this.getSerializer().getOutputSchema(),
|
|
681
|
+
createSchema: this.getSerializer().getCreateSchema(),
|
|
682
|
+
updateSchema: this.getSerializer().getUpdateSchema(),
|
|
683
|
+
searchFields: this.searchFields,
|
|
684
|
+
orderingFields: this.orderingFields,
|
|
685
|
+
lookupField: this.getManager().meta.pk,
|
|
686
|
+
lookupParam: "id",
|
|
687
|
+
allowedMethods: [
|
|
688
|
+
"GET",
|
|
689
|
+
"POST",
|
|
690
|
+
"PUT",
|
|
691
|
+
"PATCH",
|
|
692
|
+
"DELETE"
|
|
693
|
+
],
|
|
694
|
+
usesDefaultOffsetPagination: !this.paginatorFactory,
|
|
695
|
+
actions: ModelViewSet.getActions(this)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* List endpoint with filtering, search, ordering, and offset pagination.
|
|
700
|
+
*/
|
|
403
701
|
async list(ctx) {
|
|
404
702
|
try {
|
|
405
|
-
const params =
|
|
406
|
-
const
|
|
703
|
+
const params = ctx.request.queryParams;
|
|
704
|
+
const baseQueryset = this.getManager().query();
|
|
705
|
+
const paginator = this.getPaginator(baseQueryset);
|
|
407
706
|
paginator.parse(params);
|
|
408
|
-
let qs =
|
|
707
|
+
let qs = baseQueryset;
|
|
409
708
|
if (this.filters) {
|
|
410
709
|
const filterInputs = this.filters.apply(params);
|
|
411
710
|
if (filterInputs.length > 0) qs = qs.filter(Q.and(...filterInputs));
|
|
412
711
|
}
|
|
413
|
-
const search = params.
|
|
712
|
+
const search = params.getSearch();
|
|
414
713
|
if (search && this.searchFields.length > 0) {
|
|
415
714
|
const searchFilters = this.searchFields.map((field) => {
|
|
416
715
|
const lookup = `${String(field)}__icontains`;
|
|
@@ -418,86 +717,102 @@ var ModelViewSet = class ModelViewSet {
|
|
|
418
717
|
});
|
|
419
718
|
qs = qs.filter(Q.or(...searchFilters));
|
|
420
719
|
}
|
|
421
|
-
const ordering = params.
|
|
422
|
-
if (ordering) {
|
|
423
|
-
const orderTokens = ordering.
|
|
720
|
+
const ordering = params.getOrdering();
|
|
721
|
+
if (ordering.length > 0) {
|
|
722
|
+
const orderTokens = ordering.filter((field) => {
|
|
424
723
|
const cleanField = field.startsWith("-") ? field.slice(1) : field;
|
|
425
724
|
return this.orderingFields.includes(cleanField);
|
|
426
725
|
});
|
|
427
726
|
if (orderTokens.length > 0) qs = qs.orderBy(...orderTokens.map((token) => token));
|
|
428
727
|
}
|
|
429
728
|
qs = paginator.apply(qs);
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
});
|
|
729
|
+
const resultPromise = qs.fetch();
|
|
730
|
+
const totalCountPromise = paginator.needsTotalCount() ? qs.count() : Promise.resolve(undefined);
|
|
731
|
+
const [result, totalCount] = await Promise.all([resultPromise, totalCountPromise]);
|
|
732
|
+
const serializer = this.getSerializer();
|
|
733
|
+
const response = paginator.toResponse(result.results.map((row) => serializer.toRepresentation(row)), { totalCount });
|
|
734
|
+
return TangoResponse.json(response, { status: 200 });
|
|
436
735
|
} catch (error) {
|
|
437
736
|
return this.handleError(error);
|
|
438
737
|
}
|
|
439
738
|
}
|
|
739
|
+
/**
|
|
740
|
+
* Retrieve endpoint for a single resource by id.
|
|
741
|
+
*/
|
|
440
742
|
async retrieve(_ctx, id) {
|
|
441
743
|
try {
|
|
442
|
-
const
|
|
744
|
+
const manager = this.getManager();
|
|
745
|
+
const pk = manager.meta.pk;
|
|
443
746
|
const filterById = { [pk]: id };
|
|
444
|
-
const result = await
|
|
445
|
-
if (!result)
|
|
446
|
-
|
|
447
|
-
headers: { "Content-Type": "application/json" }
|
|
448
|
-
});
|
|
449
|
-
return new Response(JSON.stringify(result), {
|
|
450
|
-
status: 200,
|
|
451
|
-
headers: { "Content-Type": "application/json" }
|
|
452
|
-
});
|
|
747
|
+
const result = await manager.query().filter(filterById).fetchOne();
|
|
748
|
+
if (!result) throw new NotFoundError(`No ${manager.meta.table} record found for ${String(pk)}=${id}.`);
|
|
749
|
+
return TangoResponse.json(this.getSerializer().toRepresentation(result), { status: 200 });
|
|
453
750
|
} catch (error) {
|
|
454
751
|
return this.handleError(error);
|
|
455
752
|
}
|
|
456
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* Create endpoint: validate input, persist, and return serialized output.
|
|
756
|
+
*/
|
|
457
757
|
async create(ctx) {
|
|
458
758
|
try {
|
|
459
759
|
const body = await ctx.request.json();
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
const result = this.readSchema.parse(created);
|
|
463
|
-
return new Response(JSON.stringify(result), {
|
|
464
|
-
status: 201,
|
|
465
|
-
headers: { "Content-Type": "application/json" }
|
|
466
|
-
});
|
|
760
|
+
const result = await this.getSerializer().create(body);
|
|
761
|
+
return TangoResponse.created(undefined, result);
|
|
467
762
|
} catch (error) {
|
|
468
763
|
return this.handleError(error);
|
|
469
764
|
}
|
|
470
765
|
}
|
|
766
|
+
/**
|
|
767
|
+
* Update endpoint: validate partial payload and persist by id.
|
|
768
|
+
*/
|
|
471
769
|
async update(ctx, id) {
|
|
472
770
|
try {
|
|
473
771
|
const body = await ctx.request.json();
|
|
474
|
-
const validated = this.updateSchema.parse(body);
|
|
475
772
|
const pkValue = id;
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
return new Response(JSON.stringify(result), {
|
|
479
|
-
status: 200,
|
|
480
|
-
headers: { "Content-Type": "application/json" }
|
|
481
|
-
});
|
|
773
|
+
const result = await this.getSerializer().update(pkValue, body);
|
|
774
|
+
return TangoResponse.json(result, { status: 200 });
|
|
482
775
|
} catch (error) {
|
|
483
776
|
return this.handleError(error);
|
|
484
777
|
}
|
|
485
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* Destroy endpoint: delete a resource by id.
|
|
781
|
+
*/
|
|
486
782
|
async destroy(_ctx, id) {
|
|
487
783
|
try {
|
|
488
784
|
const pkValue = id;
|
|
489
|
-
await this.
|
|
490
|
-
return
|
|
785
|
+
await this.getManager().delete(pkValue);
|
|
786
|
+
return TangoResponse.noContent();
|
|
491
787
|
} catch (error) {
|
|
492
788
|
return this.handleError(error);
|
|
493
789
|
}
|
|
494
790
|
}
|
|
791
|
+
getPaginator(queryset) {
|
|
792
|
+
if (this.paginatorFactory) return this.paginatorFactory(queryset);
|
|
793
|
+
return new OffsetPaginator(queryset);
|
|
794
|
+
}
|
|
795
|
+
getManager() {
|
|
796
|
+
return this.getSerializer().getManager();
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Convert thrown errors into normalized HTTP responses.
|
|
800
|
+
*/
|
|
495
801
|
handleError(error) {
|
|
496
|
-
const httpError = toHttpError(error);
|
|
497
|
-
return
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
802
|
+
const httpError = HttpErrorFactory.toHttpError(error);
|
|
803
|
+
return TangoResponse.json(httpError.body, { status: httpError.status });
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Resolve route path segment(s) for a custom action.
|
|
807
|
+
* Override this in subclasses to customize path derivation globally.
|
|
808
|
+
*/
|
|
809
|
+
resolveActionPath(action) {
|
|
810
|
+
return ModelViewSet.resolvePathFromDescriptor(action.name, action.path);
|
|
811
|
+
}
|
|
812
|
+
requireModelMetadata() {
|
|
813
|
+
const model = this.getSerializer().getModel();
|
|
814
|
+
if (!model.metadata) throw new Error("OpenAPI generation requires Tango model metadata on ModelViewSet models.");
|
|
815
|
+
return model;
|
|
501
816
|
}
|
|
502
817
|
};
|
|
503
818
|
|
|
@@ -507,10 +822,5 @@ var viewset_exports = {};
|
|
|
507
822
|
__export(viewset_exports, { ModelViewSet: () => ModelViewSet });
|
|
508
823
|
|
|
509
824
|
//#endregion
|
|
510
|
-
|
|
511
|
-
var domain_exports = {};
|
|
512
|
-
__export(domain_exports, { PaginationInput: () => PaginationInput });
|
|
513
|
-
|
|
514
|
-
//#endregion
|
|
515
|
-
export { CursorPaginator, FilterSet, ModelViewSet, OffsetPaginator, PaginationInput, RequestContext, context_exports as context, domain_exports as domain, filters_exports as filters, pagination_exports as pagination, paginators_exports as paginators, viewset_exports as viewset };
|
|
825
|
+
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, resource_exports as resource, serializer_exports as serializer, view_exports as view, viewset_exports as viewset };
|
|
516
826
|
//# sourceMappingURL=index.js.map
|