@danceroutine/tango-resources 1.7.0 → 1.8.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/dist/filters/FilterSet.d.ts +12 -10
- package/dist/filters/internal/InternalFilterLookup.d.ts +15 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +229 -21
- package/dist/index.js.map +1 -1
- package/dist/resource/OpenAPIDescription.d.ts +6 -4
- package/dist/resource/ResourceModelLike.d.ts +8 -2
- package/dist/serializer/ModelSerializer.d.ts +32 -12
- package/dist/serializer/Serializer.d.ts +29 -6
- package/dist/serializer/index.d.ts +3 -2
- package/dist/serializer/internal/InternalSerializerRelationKind.d.ts +11 -0
- package/dist/serializer/relation.d.ts +45 -0
- package/dist/view/GenericAPIView.d.ts +7 -5
- package/dist/view/generics/CreateAPIView.d.ts +2 -2
- package/dist/view/generics/ListAPIView.d.ts +2 -2
- package/dist/view/generics/ListCreateAPIView.d.ts +2 -2
- package/dist/view/generics/RetrieveAPIView.d.ts +2 -2
- package/dist/view/generics/RetrieveDestroyAPIView.d.ts +2 -2
- package/dist/view/generics/RetrieveUpdateAPIView.d.ts +2 -2
- package/dist/view/generics/RetrieveUpdateDestroyAPIView.d.ts +2 -2
- package/dist/view/index.js +1 -1
- package/dist/view/mixins/CreateModelMixin.d.ts +2 -2
- package/dist/view/mixins/DestroyModelMixin.d.ts +2 -2
- package/dist/view/mixins/ListModelMixin.d.ts +2 -2
- package/dist/view/mixins/RetrieveModelMixin.d.ts +2 -2
- package/dist/view/mixins/UpdateModelMixin.d.ts +2 -2
- package/dist/{view-Djm3cQ6C.js → view-iXGdHuS-.js} +4 -3
- package/dist/view-iXGdHuS-.js.map +1 -0
- package/dist/viewset/ModelViewSet.d.ts +6 -5
- package/package.json +5 -5
- package/dist/view-Djm3cQ6C.js.map +0 -1
|
@@ -2,23 +2,24 @@ import { TangoQueryParams } from '@danceroutine/tango-core';
|
|
|
2
2
|
import type { FilterInput, FilterValue, LookupType } from '@danceroutine/tango-orm';
|
|
3
3
|
import { InternalFilterType } from './internal/InternalFilterType';
|
|
4
4
|
import type { RangeOperator } from './RangeOperator';
|
|
5
|
+
type FilterFieldRef<T extends Record<string, unknown>> = Extract<keyof T, string> | string;
|
|
5
6
|
/**
|
|
6
7
|
* Configuration for how a query parameter should be resolved into a filter.
|
|
7
8
|
* Supports scalar equality, case-insensitive search, range comparisons, IN queries, and custom logic.
|
|
8
9
|
*/
|
|
9
|
-
export type FilterResolver<T
|
|
10
|
+
export type FilterResolver<T extends Record<string, unknown>> = {
|
|
10
11
|
type: typeof InternalFilterType.SCALAR;
|
|
11
|
-
column:
|
|
12
|
+
column: FilterFieldRef<T>;
|
|
12
13
|
} | {
|
|
13
14
|
type: typeof InternalFilterType.ILIKE;
|
|
14
|
-
columns:
|
|
15
|
+
columns: FilterFieldRef<T>[];
|
|
15
16
|
} | {
|
|
16
17
|
type: typeof InternalFilterType.RANGE;
|
|
17
|
-
column:
|
|
18
|
+
column: FilterFieldRef<T>;
|
|
18
19
|
op: RangeOperator;
|
|
19
20
|
} | {
|
|
20
21
|
type: typeof InternalFilterType.IN;
|
|
21
|
-
column:
|
|
22
|
+
column: FilterFieldRef<T>;
|
|
22
23
|
} | {
|
|
23
24
|
type: typeof InternalFilterType.CUSTOM;
|
|
24
25
|
apply: (value: string | string[] | undefined) => FilterInput<T> | undefined;
|
|
@@ -31,18 +32,18 @@ export type FieldFilterDeclaration = true | readonly FilterLookup[] | {
|
|
|
31
32
|
parse?: FilterValueParser;
|
|
32
33
|
};
|
|
33
34
|
export type AliasFilterDeclaration<T extends Record<string, unknown>> = FilterResolver<T> | {
|
|
34
|
-
field:
|
|
35
|
+
field: FilterFieldRef<T>;
|
|
35
36
|
lookup?: FilterLookup;
|
|
36
37
|
parse?: FilterValueParser;
|
|
37
38
|
} | {
|
|
38
|
-
fields: readonly
|
|
39
|
+
fields: readonly FilterFieldRef<T>[];
|
|
39
40
|
lookup?: FilterLookup;
|
|
40
41
|
parse?: FilterValueParser;
|
|
41
42
|
};
|
|
42
43
|
export interface FilterSetDefineConfig<T extends Record<string, unknown>> {
|
|
43
|
-
fields?: Partial<Record<
|
|
44
|
+
fields?: Partial<Record<string, FieldFilterDeclaration>>;
|
|
44
45
|
aliases?: Record<string, AliasFilterDeclaration<T>>;
|
|
45
|
-
parsers?: Partial<Record<
|
|
46
|
+
parsers?: Partial<Record<string, FilterValueParser>>;
|
|
46
47
|
all?: '__all__';
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
@@ -82,7 +83,7 @@ export declare class FilterSet<T extends Record<string, unknown>> {
|
|
|
82
83
|
/**
|
|
83
84
|
* Return a new filter set with parser-aware scalar/range/in resolvers for matching fields.
|
|
84
85
|
*/
|
|
85
|
-
withFieldParsers(parsers: Partial<Record<
|
|
86
|
+
withFieldParsers(parsers: Partial<Record<string, FilterValueParser>>): FilterSet<T>;
|
|
86
87
|
/**
|
|
87
88
|
* Apply all configured resolvers against query params.
|
|
88
89
|
*/
|
|
@@ -91,3 +92,4 @@ export declare class FilterSet<T extends Record<string, unknown>> {
|
|
|
91
92
|
private resolveFilter;
|
|
92
93
|
private applyFieldParserOverride;
|
|
93
94
|
}
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const InternalFilterLookup: {
|
|
2
|
+
readonly EXACT: "exact";
|
|
3
|
+
readonly LT: "lt";
|
|
4
|
+
readonly LTE: "lte";
|
|
5
|
+
readonly GT: "gt";
|
|
6
|
+
readonly GTE: "gte";
|
|
7
|
+
readonly IN: "in";
|
|
8
|
+
readonly ISNULL: "isnull";
|
|
9
|
+
readonly CONTAINS: "contains";
|
|
10
|
+
readonly ICONTAINS: "icontains";
|
|
11
|
+
readonly STARTSWITH: "startswith";
|
|
12
|
+
readonly ISTARTSWITH: "istartswith";
|
|
13
|
+
readonly ENDSWITH: "endswith";
|
|
14
|
+
readonly IENDSWITH: "iendswith";
|
|
15
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type { BaseUser } from './context/index';
|
|
|
15
15
|
export { FilterSet, type AliasFilterDeclaration, type FieldFilterDeclaration, type FilterLookup, type FilterResolver, type FilterSetDefineConfig, type FilterValueParser, } from './filters/index';
|
|
16
16
|
export type { FilterType, RangeOperator } from './filters/index';
|
|
17
17
|
export { CursorPaginator, OffsetPaginator, CursorPaginationInput, OffsetPaginationInput, type Page, type BasePaginatedResponse, type CursorPaginatedResponse, type OffsetPaginatedResponse, type PaginatedResponse, type Paginator, } from './pagination/index';
|
|
18
|
-
export { Serializer, ModelSerializer, type SerializerClass, type AnySerializerClass, type SerializerCreateInput, type SerializerUpdateInput, type SerializerOutput, type SerializerSchema, type ModelSerializerClass, type AnyModelSerializerClass, } from './serializer/index';
|
|
18
|
+
export { Serializer, ModelSerializer, relation, type SerializerClass, type AnySerializerClass, type SerializerCreateInput, type SerializerUpdateInput, type SerializerOutput, type SerializerSchema, type ModelSerializerClass, type AnyModelSerializer, type AnyModelSerializerClass, type ModelSerializerRelationFields, type ManyToManyManagerKeys, type ManyToManyRelationField, type ManyToManyReadStrategy, type ManyToManyWriteStrategy, } from './serializer/index';
|
|
19
19
|
export { ModelViewSet } from './viewset/index';
|
|
20
20
|
export type { ModelViewSetOpenAPIDescription, ModelViewSetConfig, ViewSetActionDescriptor, ViewSetActionMethod, ViewSetActionScope, ResolvedViewSetActionDescriptor, } from './viewset/index';
|
|
21
21
|
export { APIView, GenericAPIView, ListModelMixin, CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, ListAPIView, CreateAPIView, RetrieveAPIView, ListCreateAPIView, RetrieveUpdateAPIView, RetrieveDestroyAPIView, RetrieveUpdateDestroyAPIView, } from './view/index';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { APIView, BasePaginator, CreateAPIView, CreateModelMixin, DestroyModelMixin, GenericAPIView, ListAPIView, ListCreateAPIView, ListModelMixin, OffsetPaginationInput, OffsetPaginator, RetrieveAPIView, RetrieveDestroyAPIView, RetrieveModelMixin, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, UpdateModelMixin, __export, inferModelFieldParsers, view_exports } from "./view-
|
|
2
|
-
import { HttpErrorFactory, NotFoundError, TangoRequest, TangoResponse } from "@danceroutine/tango-core";
|
|
3
|
-
import { Q } from "@danceroutine/tango-orm";
|
|
1
|
+
import { APIView, BasePaginator, CreateAPIView, CreateModelMixin, DestroyModelMixin, GenericAPIView, ListAPIView, ListCreateAPIView, ListModelMixin, OffsetPaginationInput, OffsetPaginator, RetrieveAPIView, RetrieveDestroyAPIView, RetrieveModelMixin, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, UpdateModelMixin, __export, inferModelFieldParsers, view_exports } from "./view-iXGdHuS-.js";
|
|
2
|
+
import { HttpErrorFactory, NotFoundError, TangoRequest, TangoResponse, getLogger } from "@danceroutine/tango-core";
|
|
3
|
+
import { ManyToManyRelatedManager, Q } from "@danceroutine/tango-orm";
|
|
4
4
|
import { z, z as z$1 } from "zod";
|
|
5
5
|
|
|
6
6
|
//#region src/context/RequestContext.ts
|
|
@@ -68,8 +68,30 @@ const InternalFilterType = {
|
|
|
68
68
|
CUSTOM: "custom"
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/filters/internal/InternalFilterLookup.ts
|
|
73
|
+
const InternalFilterLookup = {
|
|
74
|
+
EXACT: "exact",
|
|
75
|
+
LT: "lt",
|
|
76
|
+
LTE: "lte",
|
|
77
|
+
GT: "gt",
|
|
78
|
+
GTE: "gte",
|
|
79
|
+
IN: "in",
|
|
80
|
+
ISNULL: "isnull",
|
|
81
|
+
CONTAINS: "contains",
|
|
82
|
+
ICONTAINS: "icontains",
|
|
83
|
+
STARTSWITH: "startswith",
|
|
84
|
+
ISTARTSWITH: "istartswith",
|
|
85
|
+
ENDSWITH: "endswith",
|
|
86
|
+
IENDSWITH: "iendswith"
|
|
87
|
+
};
|
|
88
|
+
|
|
71
89
|
//#endregion
|
|
72
90
|
//#region src/filters/FilterSet.ts
|
|
91
|
+
const FILTER_LOOKUPS = Object.values(InternalFilterLookup);
|
|
92
|
+
function isFilterLookup(value) {
|
|
93
|
+
return FILTER_LOOKUPS.includes(value);
|
|
94
|
+
}
|
|
73
95
|
var FilterSet = class FilterSet {
|
|
74
96
|
static BRAND = "tango.resources.filter_set";
|
|
75
97
|
__tangoBrand = FilterSet.BRAND;
|
|
@@ -109,7 +131,7 @@ var FilterSet = class FilterSet {
|
|
|
109
131
|
}
|
|
110
132
|
static addFieldDeclaration(spec, field, declaration, parser) {
|
|
111
133
|
if (declaration === true) {
|
|
112
|
-
spec[String(field)] = FilterSet.createLookupResolver(field,
|
|
134
|
+
spec[String(field)] = FilterSet.createLookupResolver(field, InternalFilterLookup.EXACT, parser);
|
|
113
135
|
return;
|
|
114
136
|
}
|
|
115
137
|
if (FilterSet.isLookupArray(declaration)) {
|
|
@@ -119,7 +141,7 @@ var FilterSet = class FilterSet {
|
|
|
119
141
|
}
|
|
120
142
|
return;
|
|
121
143
|
}
|
|
122
|
-
const lookups = declaration.lookups ?? [
|
|
144
|
+
const lookups = declaration.lookups ?? [InternalFilterLookup.EXACT];
|
|
123
145
|
const baseParam = declaration.param ?? String(field);
|
|
124
146
|
const effectiveParser = declaration.parse ?? parser;
|
|
125
147
|
for (const lookup of lookups) {
|
|
@@ -133,10 +155,10 @@ var FilterSet = class FilterSet {
|
|
|
133
155
|
static normalizeAliasDeclaration(declaration) {
|
|
134
156
|
if (FilterSet.isFilterResolverDeclaration(declaration)) return declaration;
|
|
135
157
|
if ("fields" in declaration) {
|
|
136
|
-
const lookup = declaration.lookup ??
|
|
158
|
+
const lookup = declaration.lookup ?? InternalFilterLookup.ICONTAINS;
|
|
137
159
|
return FilterSet.createMultiFieldResolver(declaration.fields, lookup, declaration.parse);
|
|
138
160
|
}
|
|
139
|
-
return FilterSet.createLookupResolver(declaration.field, declaration.lookup ??
|
|
161
|
+
return FilterSet.createLookupResolver(declaration.field, declaration.lookup ?? InternalFilterLookup.EXACT, declaration.parse);
|
|
140
162
|
}
|
|
141
163
|
static isFilterResolverDeclaration(value) {
|
|
142
164
|
if (typeof value !== "object" || value === null || !("type" in value)) return false;
|
|
@@ -149,7 +171,7 @@ var FilterSet = class FilterSet {
|
|
|
149
171
|
].includes(value.type);
|
|
150
172
|
}
|
|
151
173
|
static createMultiFieldResolver(fields, lookup, parser) {
|
|
152
|
-
if (lookup ===
|
|
174
|
+
if (lookup === InternalFilterLookup.ICONTAINS && parser === undefined) return {
|
|
153
175
|
type: InternalFilterType.ILIKE,
|
|
154
176
|
columns: [...fields]
|
|
155
177
|
};
|
|
@@ -264,14 +286,16 @@ var FilterSet = class FilterSet {
|
|
|
264
286
|
return filters;
|
|
265
287
|
}
|
|
266
288
|
buildAllResolver(param) {
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
const
|
|
270
|
-
|
|
289
|
+
const segments = param.split("__").filter(Boolean);
|
|
290
|
+
if (segments.length === 0) return undefined;
|
|
291
|
+
const lastSegment = segments.at(-1);
|
|
292
|
+
const lookup = isFilterLookup(lastSegment) ? lastSegment : "exact";
|
|
293
|
+
const field = (lookup === "exact" ? segments : segments.slice(0, -1)).join("__");
|
|
294
|
+
if (!field) return undefined;
|
|
295
|
+
if (lookup === "exact") return {
|
|
271
296
|
type: InternalFilterType.SCALAR,
|
|
272
297
|
column: field
|
|
273
298
|
};
|
|
274
|
-
const lookup = rawLookupParts.join("__");
|
|
275
299
|
return FilterSet.createLookupResolver(field, lookup);
|
|
276
300
|
}
|
|
277
301
|
resolveFilter(resolver, value) {
|
|
@@ -525,10 +549,13 @@ var resource_exports = {};
|
|
|
525
549
|
|
|
526
550
|
//#endregion
|
|
527
551
|
//#region src/serializer/Serializer.ts
|
|
552
|
+
const logger = getLogger("tango.resources.serializer");
|
|
553
|
+
let hasWarnedAboutToRepresentationDeprecation = false;
|
|
528
554
|
var Serializer = class {
|
|
529
555
|
static createSchema = z.unknown();
|
|
530
556
|
static updateSchema = z.unknown();
|
|
531
557
|
static outputSchema = z.unknown();
|
|
558
|
+
static outputResolvers = undefined;
|
|
532
559
|
/**
|
|
533
560
|
* Return the serializer class for the current instance.
|
|
534
561
|
*/
|
|
@@ -554,6 +581,13 @@ var Serializer = class {
|
|
|
554
581
|
return this.getSerializerClass().outputSchema;
|
|
555
582
|
}
|
|
556
583
|
/**
|
|
584
|
+
* Return the resolver map used to enrich serializer output fields before
|
|
585
|
+
* the outward Zod schema parses the final response shape.
|
|
586
|
+
*/
|
|
587
|
+
getOutputResolvers() {
|
|
588
|
+
return this.getSerializerClass().outputResolvers ?? {};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
557
591
|
* Validate unknown input for create workflows.
|
|
558
592
|
*/
|
|
559
593
|
deserializeCreate(input) {
|
|
@@ -567,16 +601,59 @@ var Serializer = class {
|
|
|
567
601
|
}
|
|
568
602
|
/**
|
|
569
603
|
* Convert a persisted record into its outward-facing representation.
|
|
604
|
+
*
|
|
605
|
+
* @deprecated Use `serialize(...)` instead so serializer-owned output
|
|
606
|
+
* resolvers run before the outward Zod schema parses the response shape.
|
|
570
607
|
*/
|
|
571
608
|
toRepresentation(record) {
|
|
609
|
+
if (!hasWarnedAboutToRepresentationDeprecation) {
|
|
610
|
+
hasWarnedAboutToRepresentationDeprecation = true;
|
|
611
|
+
logger.warn("`Serializer.toRepresentation(...)` is deprecated. Use `serialize(...)` instead so output resolvers run before outward parsing.");
|
|
612
|
+
}
|
|
572
613
|
return this.getOutputSchema().parse(record);
|
|
573
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Resolve serializer-owned output fields and parse the outward response
|
|
617
|
+
* contract.
|
|
618
|
+
*/
|
|
619
|
+
async serialize(record) {
|
|
620
|
+
return this.getOutputSchema().parse(await this.applyOutputResolvers(record));
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Serialize many records through the same outward response contract.
|
|
624
|
+
*/
|
|
625
|
+
async serializeMany(records) {
|
|
626
|
+
return Promise.all(records.map((record) => this.serialize(record)));
|
|
627
|
+
}
|
|
628
|
+
async applyOutputResolvers(record) {
|
|
629
|
+
const resolvers = this.getOutputResolvers();
|
|
630
|
+
const resolverEntries = Object.entries(resolvers);
|
|
631
|
+
if (resolverEntries.length === 0 || typeof record !== "object" || record === null) return record;
|
|
632
|
+
const resolved = await Promise.all(resolverEntries.map(async ([key, resolver]) => [key, await resolver(record)]));
|
|
633
|
+
return {
|
|
634
|
+
...record,
|
|
635
|
+
...Object.fromEntries(resolved)
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/serializer/internal/InternalSerializerRelationKind.ts
|
|
642
|
+
const InternalSerializerRelationKind = { MANY_TO_MANY: "manyToMany" };
|
|
643
|
+
const InternalManyToManyReadStrategyKind = {
|
|
644
|
+
PK_LIST: "pkList",
|
|
645
|
+
NESTED: "nested"
|
|
646
|
+
};
|
|
647
|
+
const InternalManyToManyWriteStrategyKind = {
|
|
648
|
+
PK_LIST: "pkList",
|
|
649
|
+
SLUG_LIST: "slugList"
|
|
574
650
|
};
|
|
575
651
|
|
|
576
652
|
//#endregion
|
|
577
653
|
//#region src/serializer/ModelSerializer.ts
|
|
578
654
|
var ModelSerializer = class extends Serializer {
|
|
579
655
|
static model;
|
|
656
|
+
static relationFields = undefined;
|
|
580
657
|
/**
|
|
581
658
|
* Return the Tango model backing this serializer.
|
|
582
659
|
*/
|
|
@@ -592,22 +669,44 @@ var ModelSerializer = class extends Serializer {
|
|
|
592
669
|
return this.getModel().objects;
|
|
593
670
|
}
|
|
594
671
|
/**
|
|
672
|
+
* Return the declarative relation-field map for this serializer.
|
|
673
|
+
*/
|
|
674
|
+
getRelationFields() {
|
|
675
|
+
return this.constructor.relationFields ?? {};
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Merge relation-field read resolvers into the serializer output path.
|
|
679
|
+
*/
|
|
680
|
+
getOutputResolvers() {
|
|
681
|
+
const baseResolvers = super.getOutputResolvers();
|
|
682
|
+
const relationFields = this.getRelationFields();
|
|
683
|
+
const relationResolvers = Object.fromEntries(Object.entries(relationFields).map(([fieldName, field]) => [fieldName, async (record) => this.serializeRelationField(record, fieldName, field)]));
|
|
684
|
+
return {
|
|
685
|
+
...relationResolvers,
|
|
686
|
+
...baseResolvers
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
595
690
|
* Validate, enrich, persist, and serialize a create workflow.
|
|
596
691
|
*/
|
|
597
692
|
async create(input) {
|
|
598
693
|
const validated = this.deserializeCreate(input);
|
|
694
|
+
const relationWrites = this.extractRelationWrites(validated, input);
|
|
599
695
|
const prepared = await this.beforeCreate(validated);
|
|
600
|
-
const created = await this.getManager().create(prepared);
|
|
601
|
-
|
|
696
|
+
const created = await this.getManager().create(this.stripRelationFields(prepared));
|
|
697
|
+
await this.applyRelationWrites(created, relationWrites);
|
|
698
|
+
return this.serialize(created);
|
|
602
699
|
}
|
|
603
700
|
/**
|
|
604
701
|
* Validate, enrich, persist, and serialize an update workflow.
|
|
605
702
|
*/
|
|
606
703
|
async update(id, input) {
|
|
607
704
|
const validated = this.deserializeUpdate(input);
|
|
705
|
+
const relationWrites = this.extractRelationWrites(validated, input);
|
|
608
706
|
const prepared = await this.beforeUpdate(id, validated);
|
|
609
|
-
const updated = await this.getManager().update(id, prepared);
|
|
610
|
-
|
|
707
|
+
const updated = await this.getManager().update(id, this.stripRelationFields(prepared));
|
|
708
|
+
await this.applyRelationWrites(updated, relationWrites);
|
|
709
|
+
return this.serialize(updated);
|
|
611
710
|
}
|
|
612
711
|
/**
|
|
613
712
|
* Override to normalize create input for this resource workflow before the
|
|
@@ -629,6 +728,113 @@ var ModelSerializer = class extends Serializer {
|
|
|
629
728
|
async beforeUpdate(_id, data) {
|
|
630
729
|
return data;
|
|
631
730
|
}
|
|
731
|
+
extractRelationWrites(data, rawInput) {
|
|
732
|
+
if (typeof data !== "object" || data === null || typeof rawInput !== "object" || rawInput === null) return {};
|
|
733
|
+
const relationFields = this.getRelationFields();
|
|
734
|
+
const writes = {};
|
|
735
|
+
for (const fieldName of Object.keys(relationFields)) if (fieldName in rawInput) writes[fieldName] = data[fieldName];
|
|
736
|
+
return writes;
|
|
737
|
+
}
|
|
738
|
+
stripRelationFields(data) {
|
|
739
|
+
const relationFieldNames = new Set(Object.keys(this.getRelationFields()));
|
|
740
|
+
if (relationFieldNames.size === 0) return data;
|
|
741
|
+
return Object.fromEntries(Object.entries(data).filter(([fieldName]) => !relationFieldNames.has(fieldName)));
|
|
742
|
+
}
|
|
743
|
+
async applyRelationWrites(record, writes) {
|
|
744
|
+
const relationFields = this.getRelationFields();
|
|
745
|
+
for (const [fieldName, value] of Object.entries(writes)) {
|
|
746
|
+
const field = relationFields[fieldName];
|
|
747
|
+
if (!field) continue;
|
|
748
|
+
await this.syncManyToManyRelation(record, fieldName, value);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async serializeRelationField(record, fieldName, field) {
|
|
752
|
+
const manager = this.getManyToManyManager(record, fieldName);
|
|
753
|
+
const rows = (await manager.all().fetch()).results;
|
|
754
|
+
const relationMeta = this.getManyToManyRelationMeta(fieldName);
|
|
755
|
+
switch (field.read.kind) {
|
|
756
|
+
case InternalManyToManyReadStrategyKind.PK_LIST: return rows.map((row) => row[relationMeta.targetPrimaryKey]);
|
|
757
|
+
case InternalManyToManyReadStrategyKind.NESTED: return rows.map((row) => field.read.schema.parse(row));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async syncManyToManyRelation(record, fieldName, value) {
|
|
761
|
+
if (value === undefined) return;
|
|
762
|
+
const manager = this.getManyToManyManager(record, fieldName);
|
|
763
|
+
const field = this.getRelationFields()[fieldName];
|
|
764
|
+
if (!field) return;
|
|
765
|
+
const nextTargets = await this.resolveWriteTargets(fieldName, field.write, value);
|
|
766
|
+
const currentTargets = (await manager.all().fetch()).results;
|
|
767
|
+
if (currentTargets.length > 0) await manager.remove(...currentTargets);
|
|
768
|
+
if (nextTargets.length > 0) await manager.add(...nextTargets);
|
|
769
|
+
}
|
|
770
|
+
async resolveWriteTargets(fieldName, strategy, value) {
|
|
771
|
+
switch (strategy.kind) {
|
|
772
|
+
case InternalManyToManyWriteStrategyKind.PK_LIST:
|
|
773
|
+
if (!Array.isArray(value)) throw new TypeError(`Relation field '${String(fieldName)}' expects an array of primary-key values.`);
|
|
774
|
+
return value;
|
|
775
|
+
case InternalManyToManyWriteStrategyKind.SLUG_LIST: {
|
|
776
|
+
if (!Array.isArray(value)) throw new TypeError(`Relation field '${String(fieldName)}' expects an array of lookup values.`);
|
|
777
|
+
const lookupValues = [...new Set(value.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
778
|
+
if (lookupValues.length === 0) return [];
|
|
779
|
+
const filter = { [`${strategy.lookupField}__in`]: lookupValues };
|
|
780
|
+
const existing = await strategy.model.objects.query().filter(filter).fetch();
|
|
781
|
+
const byLookup = new Map(existing.results.map((row) => [String(row[strategy.lookupField]), row]));
|
|
782
|
+
const resolved = [];
|
|
783
|
+
for (const lookupValue of lookupValues) {
|
|
784
|
+
const found = byLookup.get(lookupValue);
|
|
785
|
+
if (found) {
|
|
786
|
+
resolved.push(found);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (!strategy.createIfMissing) throw new Error(`Relation field '${String(fieldName)}' could not resolve '${lookupValue}' via '${strategy.lookupField}'.`);
|
|
790
|
+
const created = await strategy.model.objects.create(strategy.buildCreateInput?.(lookupValue) ?? { [strategy.lookupField]: lookupValue });
|
|
791
|
+
resolved.push(created);
|
|
792
|
+
}
|
|
793
|
+
return resolved;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
getManyToManyManager(record, fieldName) {
|
|
798
|
+
const manager = record[fieldName];
|
|
799
|
+
if (!ManyToManyRelatedManager.isManyToManyRelatedManager(manager)) throw new Error(`Relation field '${fieldName}' is not backed by a many-to-many related manager.`);
|
|
800
|
+
return manager;
|
|
801
|
+
}
|
|
802
|
+
getManyToManyRelationMeta(fieldName) {
|
|
803
|
+
const relation$1 = this.getManager().meta.relations?.[fieldName];
|
|
804
|
+
if (!relation$1 || relation$1.kind !== InternalSerializerRelationKind.MANY_TO_MANY) throw new Error(`Relation field '${fieldName}' is not a persisted many-to-many edge.`);
|
|
805
|
+
return relation$1;
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/serializer/relation.ts
|
|
811
|
+
function pkList() {
|
|
812
|
+
return { kind: InternalManyToManyReadStrategyKind.PK_LIST };
|
|
813
|
+
}
|
|
814
|
+
function nested(schema) {
|
|
815
|
+
return {
|
|
816
|
+
kind: InternalManyToManyReadStrategyKind.NESTED,
|
|
817
|
+
schema
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function slugList(options) {
|
|
821
|
+
return {
|
|
822
|
+
kind: InternalManyToManyWriteStrategyKind.SLUG_LIST,
|
|
823
|
+
...options
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function manyToMany(config = {}) {
|
|
827
|
+
return {
|
|
828
|
+
kind: InternalSerializerRelationKind.MANY_TO_MANY,
|
|
829
|
+
read: config.read ?? pkList(),
|
|
830
|
+
write: config.write ?? pkList()
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const relation = {
|
|
834
|
+
manyToMany,
|
|
835
|
+
pkList,
|
|
836
|
+
nested,
|
|
837
|
+
slugList
|
|
632
838
|
};
|
|
633
839
|
|
|
634
840
|
//#endregion
|
|
@@ -636,7 +842,8 @@ var ModelSerializer = class extends Serializer {
|
|
|
636
842
|
var serializer_exports = {};
|
|
637
843
|
__export(serializer_exports, {
|
|
638
844
|
ModelSerializer: () => ModelSerializer,
|
|
639
|
-
Serializer: () => Serializer
|
|
845
|
+
Serializer: () => Serializer,
|
|
846
|
+
relation: () => relation
|
|
640
847
|
});
|
|
641
848
|
|
|
642
849
|
//#endregion
|
|
@@ -763,7 +970,8 @@ var ModelViewSet = class ModelViewSet {
|
|
|
763
970
|
const totalCountPromise = paginator.needsTotalCount() ? qs.count() : Promise.resolve(undefined);
|
|
764
971
|
const [result, totalCount] = await Promise.all([resultPromise, totalCountPromise]);
|
|
765
972
|
const serializer = this.getSerializer();
|
|
766
|
-
const
|
|
973
|
+
const rows = await serializer.serializeMany(result.items);
|
|
974
|
+
const response = paginator.toResponse(rows, { totalCount });
|
|
767
975
|
return TangoResponse.json(response, { status: 200 });
|
|
768
976
|
} catch (error) {
|
|
769
977
|
return this.handleError(error);
|
|
@@ -779,7 +987,7 @@ var ModelViewSet = class ModelViewSet {
|
|
|
779
987
|
const filterById = { [pk]: id };
|
|
780
988
|
const result = await manager.query().filter(filterById).fetchOne();
|
|
781
989
|
if (!result) throw new NotFoundError(`No ${manager.meta.table} record found for ${String(pk)}=${id}.`);
|
|
782
|
-
return TangoResponse.json(this.getSerializer().
|
|
990
|
+
return TangoResponse.json(await this.getSerializer().serialize(result), { status: 200 });
|
|
783
991
|
} catch (error) {
|
|
784
992
|
return this.handleError(error);
|
|
785
993
|
}
|
|
@@ -860,5 +1068,5 @@ var viewset_exports = {};
|
|
|
860
1068
|
__export(viewset_exports, { ModelViewSet: () => ModelViewSet });
|
|
861
1069
|
|
|
862
1070
|
//#endregion
|
|
863
|
-
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 };
|
|
1071
|
+
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 };
|
|
864
1072
|
//# sourceMappingURL=index.js.map
|