@danceroutine/tango-resources 1.6.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.
Files changed (31) hide show
  1. package/dist/filters/FilterSet.d.ts +12 -10
  2. package/dist/filters/internal/InternalFilterLookup.d.ts +15 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +229 -21
  5. package/dist/index.js.map +1 -1
  6. package/dist/resource/OpenAPIDescription.d.ts +6 -4
  7. package/dist/resource/ResourceModelLike.d.ts +8 -2
  8. package/dist/serializer/ModelSerializer.d.ts +32 -12
  9. package/dist/serializer/Serializer.d.ts +29 -6
  10. package/dist/serializer/index.d.ts +3 -2
  11. package/dist/serializer/internal/InternalSerializerRelationKind.d.ts +11 -0
  12. package/dist/serializer/relation.d.ts +45 -0
  13. package/dist/view/GenericAPIView.d.ts +7 -5
  14. package/dist/view/generics/CreateAPIView.d.ts +2 -2
  15. package/dist/view/generics/ListAPIView.d.ts +2 -2
  16. package/dist/view/generics/ListCreateAPIView.d.ts +2 -2
  17. package/dist/view/generics/RetrieveAPIView.d.ts +2 -2
  18. package/dist/view/generics/RetrieveDestroyAPIView.d.ts +2 -2
  19. package/dist/view/generics/RetrieveUpdateAPIView.d.ts +2 -2
  20. package/dist/view/generics/RetrieveUpdateDestroyAPIView.d.ts +2 -2
  21. package/dist/view/index.js +1 -1
  22. package/dist/view/mixins/CreateModelMixin.d.ts +2 -2
  23. package/dist/view/mixins/DestroyModelMixin.d.ts +2 -2
  24. package/dist/view/mixins/ListModelMixin.d.ts +2 -2
  25. package/dist/view/mixins/RetrieveModelMixin.d.ts +2 -2
  26. package/dist/view/mixins/UpdateModelMixin.d.ts +2 -2
  27. package/dist/{view-Djm3cQ6C.js → view-iXGdHuS-.js} +4 -3
  28. package/dist/view-iXGdHuS-.js.map +1 -0
  29. package/dist/viewset/ModelViewSet.d.ts +6 -5
  30. package/package.json +5 -5
  31. 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: keyof T;
12
+ column: FilterFieldRef<T>;
12
13
  } | {
13
14
  type: typeof InternalFilterType.ILIKE;
14
- columns: (keyof T)[];
15
+ columns: FilterFieldRef<T>[];
15
16
  } | {
16
17
  type: typeof InternalFilterType.RANGE;
17
- column: keyof T;
18
+ column: FilterFieldRef<T>;
18
19
  op: RangeOperator;
19
20
  } | {
20
21
  type: typeof InternalFilterType.IN;
21
- column: keyof T;
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: keyof T;
35
+ field: FilterFieldRef<T>;
35
36
  lookup?: FilterLookup;
36
37
  parse?: FilterValueParser;
37
38
  } | {
38
- fields: readonly (keyof T)[];
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<keyof T, FieldFilterDeclaration>>;
44
+ fields?: Partial<Record<string, FieldFilterDeclaration>>;
44
45
  aliases?: Record<string, AliasFilterDeclaration<T>>;
45
- parsers?: Partial<Record<keyof T, FilterValueParser>>;
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<keyof T, FilterValueParser>>): FilterSet<T>;
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-Djm3cQ6C.js";
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, "exact", parser);
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 ?? ["exact"];
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 ?? "icontains";
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 ?? "exact", declaration.parse);
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 === "icontains" && parser === undefined) return {
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 [rawField, ...rawLookupParts] = param.split("__");
268
- if (!rawField) return undefined;
269
- const field = rawField;
270
- if (rawLookupParts.length === 0) return {
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
- return this.toRepresentation(created);
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
- return this.toRepresentation(updated);
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 response = paginator.toResponse(result.map((row) => serializer.toRepresentation(row)), { totalCount });
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().toRepresentation(result), { status: 200 });
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