@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/dist/context/RequestContext.d.ts +23 -4
  4. package/dist/filters/FilterSet.d.ts +59 -4
  5. package/dist/filters/index.d.ts +1 -1
  6. package/dist/index.d.ts +10 -4
  7. package/dist/index.js +515 -205
  8. package/dist/index.js.map +1 -1
  9. package/dist/pagination/CursorPaginationInput.d.ts +7 -0
  10. package/dist/pagination/OffsetPaginationInput.d.ts +7 -0
  11. package/dist/pagination/PaginatedResponse.d.ts +8 -2
  12. package/dist/pagination/Paginator.d.ts +5 -3
  13. package/dist/pagination/index.d.ts +5 -3
  14. package/dist/paginators/CursorPaginator.d.ts +32 -6
  15. package/dist/paginators/OffsetPaginator.d.ts +30 -7
  16. package/dist/resource/OpenAPIDescription.d.ts +21 -0
  17. package/dist/resource/ResourceModelLike.d.ts +16 -0
  18. package/dist/resource/index.d.ts +5 -0
  19. package/dist/serializer/ModelSerializer.d.ts +47 -0
  20. package/dist/serializer/Serializer.d.ts +52 -0
  21. package/dist/serializer/index.d.ts +5 -0
  22. package/dist/view/APIView.d.ts +26 -0
  23. package/dist/view/GenericAPIView.d.ts +57 -0
  24. package/dist/view/generics/CreateAPIView.d.ts +10 -0
  25. package/dist/view/generics/ListAPIView.d.ts +10 -0
  26. package/dist/view/generics/ListCreateAPIView.d.ts +11 -0
  27. package/dist/view/generics/RetrieveAPIView.d.ts +10 -0
  28. package/dist/view/generics/RetrieveDestroyAPIView.d.ts +11 -0
  29. package/dist/view/generics/RetrieveUpdateAPIView.d.ts +12 -0
  30. package/dist/view/generics/RetrieveUpdateDestroyAPIView.d.ts +13 -0
  31. package/dist/view/generics/index.d.ts +10 -0
  32. package/dist/view/index.d.ts +8 -0
  33. package/dist/view/index.js +3 -0
  34. package/dist/view/mixins/CreateModelMixin.d.ts +11 -0
  35. package/dist/view/mixins/DestroyModelMixin.d.ts +11 -0
  36. package/dist/view/mixins/ListModelMixin.d.ts +11 -0
  37. package/dist/view/mixins/RetrieveModelMixin.d.ts +11 -0
  38. package/dist/view/mixins/UpdateModelMixin.d.ts +12 -0
  39. package/dist/view/mixins/index.d.ts +8 -0
  40. package/dist/view-BNGEURL_.js +547 -0
  41. package/dist/view-BNGEURL_.js.map +1 -0
  42. package/dist/viewset/ModelViewSet.d.ts +91 -45
  43. package/dist/viewset/index.d.ts +2 -1
  44. package/package.json +75 -69
  45. package/dist/domain/index.d.ts +0 -8
  46. package/dist/pagination/PaginationInput.d.ts +0 -7
  47. package/dist/viewset/ModelViewSet.js +0 -143
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pedro Del Moral Lopez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # @danceroutine/tango-resources
2
+
3
+ `@danceroutine/tango-resources` provides Tango's API-layer primitives.
4
+
5
+ The resources package turns model-backed data access into HTTP behavior. It gives application code a consistent way to express CRUD endpoints, custom API views, filtering, ordering, search, pagination, and serializer-backed request and response contracts while leaving request lifecycle ownership to adapters such as Express and Next.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @danceroutine/tango-resources
11
+ ```
12
+
13
+ You will usually pair this package with `@danceroutine/tango-schema` and `@danceroutine/tango-orm`.
14
+
15
+ ## What the package does inside Tango
16
+
17
+ The resource layer centers on four roles:
18
+
19
+ - `APIView` and the generic API view classes for endpoints that are not full CRUD resources
20
+ - `Serializer` and `ModelSerializer` for Zod-backed input validation, output representation, and resource-scoped normalization
21
+ - `ModelViewSet` for CRUD APIs backed by a Tango serializer
22
+ - filtering and pagination primitives that keep collection behavior consistent
23
+
24
+ Model lifecycle hooks remain part of the persistence story through `@danceroutine/tango-schema`. A serializer shapes the resource contract. A model hook shapes the record lifecycle.
25
+
26
+ Request query input reaches the resource layer through `TangoRequest.queryParams`, which exposes `TangoQueryParams` from `@danceroutine/tango-core`. That keeps filtering, search, ordering, and pagination behavior framework-agnostic while giving application code a public query helper it can reuse outside resources.
27
+
28
+ ## Quick start
29
+
30
+ ```ts
31
+ import { z } from 'zod';
32
+ import '@danceroutine/tango-orm/runtime';
33
+ import { FilterSet, ModelSerializer, ModelViewSet } from '@danceroutine/tango-resources';
34
+ import { Model, t } from '@danceroutine/tango-schema';
35
+
36
+ const TodoReadSchema = z.object({
37
+ id: z.number(),
38
+ title: z.string(),
39
+ completed: z.coerce.boolean(),
40
+ createdAt: z.string(),
41
+ updatedAt: z.string(),
42
+ });
43
+ const TodoCreateSchema = z.object({
44
+ title: z.string(),
45
+ completed: z.boolean().optional(),
46
+ });
47
+ const TodoUpdateSchema = TodoCreateSchema.partial();
48
+
49
+ type Todo = z.output<typeof TodoReadSchema>;
50
+
51
+ const TodoModel = Model({
52
+ namespace: 'app',
53
+ name: 'Todo',
54
+ schema: TodoReadSchema.extend({
55
+ id: t.primaryKey(z.number().int()),
56
+ title: z.string(),
57
+ completed: t.default(z.coerce.boolean(), 'false'),
58
+ }),
59
+ hooks: {
60
+ async beforeCreate({ data }) {
61
+ const now = new Date().toISOString();
62
+
63
+ return {
64
+ ...data,
65
+ createdAt: now,
66
+ updatedAt: now,
67
+ };
68
+ },
69
+ async beforeUpdate({ patch }) {
70
+ return {
71
+ ...patch,
72
+ updatedAt: new Date().toISOString(),
73
+ };
74
+ },
75
+ },
76
+ });
77
+
78
+ class TodoSerializer extends ModelSerializer<
79
+ Todo,
80
+ typeof TodoCreateSchema,
81
+ typeof TodoUpdateSchema,
82
+ typeof TodoReadSchema
83
+ > {
84
+ static readonly model = TodoModel;
85
+ static readonly createSchema = TodoCreateSchema;
86
+ static readonly updateSchema = TodoUpdateSchema;
87
+ static readonly outputSchema = TodoReadSchema;
88
+ }
89
+
90
+ class TodoViewSet extends ModelViewSet<Todo, typeof TodoSerializer> {
91
+ constructor() {
92
+ super({
93
+ serializer: TodoSerializer,
94
+ filters: FilterSet.define<Todo>({
95
+ fields: { completed: true },
96
+ }),
97
+ orderingFields: ['id', 'title'],
98
+ });
99
+ }
100
+ }
101
+ ```
102
+
103
+ Adapters wire those resource classes to host routes through helpers such as `ExpressAdapter.registerViewSet(...)` and `NextAdapter.adaptViewSet(...)`.
104
+
105
+ ## Where logic belongs
106
+
107
+ Use a serializer for:
108
+
109
+ - create and update input validation
110
+ - output representation
111
+ - request-scoped normalization
112
+ - resource-specific transformation that belongs to the HTTP contract
113
+
114
+ Use model hooks for:
115
+
116
+ - timestamp stamping
117
+ - slug generation that must apply for every write path
118
+ - persistence defaults and normalization that belong to the record itself
119
+ - side effects that should run no matter which caller writes through `Model.objects`
120
+
121
+ Use the resource or viewset for:
122
+
123
+ - routing behavior
124
+ - filtering, search, and pagination policy
125
+ - custom actions and endpoint orchestration
126
+
127
+ ## Public API
128
+
129
+ The root export includes:
130
+
131
+ - `RequestContext`
132
+ - `Serializer` and `ModelSerializer`
133
+ - `FilterSet`
134
+ - `OffsetPaginator`, `CursorPaginator`, and pagination contracts
135
+ - `ModelViewSet`
136
+ - `APIView`, `GenericAPIView`, and the generic CRUD-oriented view classes and mixins
137
+
138
+ Most applications start with `ModelSerializer`, `ModelViewSet`, `FilterSet`, and one paginator. The generic view stack becomes useful when an endpoint is narrower than a full CRUD resource, and `APIView` stays available for fully custom request handling.
139
+
140
+ ## Import style
141
+
142
+ The package supports both root imports and domain-style imports:
143
+
144
+ ```ts
145
+ import { APIView, FilterSet, ModelSerializer, ModelViewSet, OffsetPaginator } from '@danceroutine/tango-resources';
146
+ import { context, filters, pagination, serializer, view, viewset } from '@danceroutine/tango-resources';
147
+ ```
148
+
149
+ Available subpaths include `context`, `filters`, `pagination`, `paginators`, `serializer`, `viewset`, `view`, and `domain`.
150
+
151
+ ## Developer workflow
152
+
153
+ ```bash
154
+ pnpm --filter @danceroutine/tango-resources build
155
+ pnpm --filter @danceroutine/tango-resources typecheck
156
+ pnpm --filter @danceroutine/tango-resources test
157
+ ```
158
+
159
+ ## Bugs and support
160
+
161
+ - Documentation: <https://tangowebframework.dev>
162
+ - Serializers topic: <https://tangowebframework.dev/topics/serializers>
163
+ - Model lifecycle hooks topic: <https://tangowebframework.dev/topics/model-lifecycle-hooks>
164
+ - Resources topic: <https://tangowebframework.dev/topics/resources-and-viewsets>
165
+ - API reference: <https://tangowebframework.dev/reference/resources-api>
166
+ - Issue tracker: <https://github.com/danceroutine/tango/issues>
167
+
168
+ ## License
169
+
170
+ MIT
@@ -1,3 +1,4 @@
1
+ import { TangoRequest } from '@danceroutine/tango-core';
1
2
  /**
2
3
  * Default user shape for RequestContext.
3
4
  * Consumers can provide their own user type via the TUser generic parameter.
@@ -11,17 +12,35 @@ export interface BaseUser {
11
12
  * Generic over the user type so consumers can plug in their own auth infrastructure.
12
13
  */
13
14
  export declare class RequestContext<TUser = BaseUser> {
14
- readonly request: Request;
15
+ readonly request: TangoRequest;
15
16
  user: TUser | null;
16
17
  params: Record<string, string>;
17
18
  static readonly BRAND: "tango.resources.request_context";
18
- private state;
19
19
  readonly __tangoBrand: typeof RequestContext.BRAND;
20
- constructor(request: Request, user?: TUser | null, params?: Record<string, string>);
20
+ private state;
21
+ constructor(request: TangoRequest, user?: TUser | null, params?: Record<string, string>);
22
+ /**
23
+ * Narrow an unknown value to `RequestContext`.
24
+ */
21
25
  static isRequestContext<TUser = BaseUser>(value: unknown): value is RequestContext<TUser>;
26
+ /**
27
+ * Construct a context with optional user payload.
28
+ */
29
+ static create<TUser = BaseUser>(request: Request, user?: TUser | null): RequestContext<TUser>;
30
+ /**
31
+ * Store arbitrary per-request state for downstream middleware/handlers.
32
+ */
22
33
  setState<T>(key: string | symbol, value: T): void;
34
+ /**
35
+ * Retrieve previously stored request state.
36
+ */
23
37
  getState<T>(key: string | symbol): T | undefined;
38
+ /**
39
+ * Check whether a state key has been set.
40
+ */
24
41
  hasState(key: string | symbol): boolean;
25
- static create<TUser = BaseUser>(request: Request, user?: TUser | null): RequestContext<TUser>;
42
+ /**
43
+ * Clone the context, including route params and request-local state.
44
+ */
26
45
  clone(): RequestContext<TUser>;
27
46
  }
@@ -1,4 +1,5 @@
1
- import type { FilterInput } from '@danceroutine/tango-orm';
1
+ import { TangoQueryParams } from '@danceroutine/tango-core';
2
+ import type { FilterInput, FilterValue, LookupType } from '@danceroutine/tango-orm';
2
3
  import { InternalFilterType } from './internal/InternalFilterType';
3
4
  import type { RangeOperator } from './RangeOperator';
4
5
  /**
@@ -22,12 +23,66 @@ export type FilterResolver<T> = {
22
23
  type: typeof InternalFilterType.CUSTOM;
23
24
  apply: (value: string | string[] | undefined) => FilterInput<T> | undefined;
24
25
  };
26
+ export type FilterLookup = LookupType;
27
+ export type FilterValueParser = (raw: string | string[]) => FilterValue | FilterValue[] | undefined;
28
+ export type FieldFilterDeclaration = true | readonly FilterLookup[] | {
29
+ lookups?: readonly FilterLookup[];
30
+ param?: string;
31
+ parse?: FilterValueParser;
32
+ };
33
+ export type AliasFilterDeclaration<T extends Record<string, unknown>> = FilterResolver<T> | {
34
+ field: keyof T;
35
+ lookup?: FilterLookup;
36
+ parse?: FilterValueParser;
37
+ } | {
38
+ fields: readonly (keyof T)[];
39
+ lookup?: FilterLookup;
40
+ parse?: FilterValueParser;
41
+ };
42
+ export interface FilterSetDefineConfig<T extends Record<string, unknown>> {
43
+ fields?: Partial<Record<keyof T, FieldFilterDeclaration>>;
44
+ aliases?: Record<string, AliasFilterDeclaration<T>>;
45
+ parsers?: Partial<Record<keyof T, FilterValueParser>>;
46
+ all?: '__all__';
47
+ }
48
+ /**
49
+ * Declarative query-param to filter translation.
50
+ *
51
+ * A `FilterSet` lets viewsets expose safe, explicit filtering behavior
52
+ * without leaking raw ORM filter syntax to request handlers.
53
+ */
25
54
  export declare class FilterSet<T extends Record<string, unknown>> {
26
- private spec;
55
+ private readonly spec;
56
+ private readonly allowAllParams;
27
57
  static readonly BRAND: "tango.resources.filter_set";
28
58
  readonly __tangoBrand: typeof FilterSet.BRAND;
59
+ /**
60
+ * Resolve matching query parameters into ORM filter inputs.
61
+ */
62
+ constructor(spec: Record<string, FilterResolver<T>>, allowAllParams?: boolean);
63
+ /**
64
+ * Build a filter set from Django-style field declarations.
65
+ */
66
+ static define<T extends Record<string, unknown>>(config: FilterSetDefineConfig<T>): FilterSet<T>;
67
+ /**
68
+ * Narrow an unknown value to `FilterSet`.
69
+ */
29
70
  static isFilterSet<T extends Record<string, unknown>>(value: unknown): value is FilterSet<T>;
30
- constructor(spec: Record<string, FilterResolver<T>>);
31
- apply(params: URLSearchParams): FilterInput<T>[];
71
+ private static normalizeDefineConfig;
72
+ private static addFieldDeclaration;
73
+ private static isLookupArray;
74
+ private static normalizeAliasDeclaration;
75
+ private static isFilterResolverDeclaration;
76
+ private static createMultiFieldResolver;
77
+ private static createLookupResolver;
78
+ private static resolveLookupFilter;
79
+ private static resolveLookupParam;
80
+ private static resolveParserValue;
81
+ private static toScalarString;
82
+ /**
83
+ * Apply all configured resolvers against query params.
84
+ */
85
+ apply(params: TangoQueryParams): FilterInput<T>[];
86
+ private buildAllResolver;
32
87
  private resolveFilter;
33
88
  }
@@ -3,5 +3,5 @@
3
3
  */
4
4
  export type { FilterType } from './FilterType';
5
5
  export type { RangeOperator } from './RangeOperator';
6
- export type { FilterResolver } from './FilterSet';
6
+ export type { AliasFilterDeclaration, FieldFilterDeclaration, FilterLookup, FilterResolver, FilterSetDefineConfig, FilterValueParser, } from './FilterSet';
7
7
  export { FilterSet } from './FilterSet';
package/dist/index.d.ts CHANGED
@@ -6,12 +6,18 @@ export * as context from './context/index';
6
6
  export * as filters from './filters/index';
7
7
  export * as pagination from './pagination/index';
8
8
  export * as paginators from './paginators/index';
9
+ export * as resource from './resource/index';
10
+ export * as serializer from './serializer/index';
9
11
  export * as viewset from './viewset/index';
10
- export * as domain from './domain/index';
12
+ export * as view from './view/index';
11
13
  export { RequestContext } from './context/index';
12
14
  export type { BaseUser } from './context/index';
13
- export { FilterSet, type FilterResolver } from './filters/index';
15
+ export { FilterSet, type AliasFilterDeclaration, type FieldFilterDeclaration, type FilterLookup, type FilterResolver, type FilterSetDefineConfig, type FilterValueParser, } from './filters/index';
14
16
  export type { FilterType, RangeOperator } from './filters/index';
15
- export { CursorPaginator, OffsetPaginator, PaginationInput, type Page, type PaginatedResponse, type Paginator, } from './pagination/index';
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';
16
19
  export { ModelViewSet } from './viewset/index';
17
- export type { ModelViewSetConfig } from './viewset/index';
20
+ export type { ModelViewSetOpenAPIDescription, ModelViewSetConfig, ViewSetActionDescriptor, ViewSetActionMethod, ViewSetActionScope, ResolvedViewSetActionDescriptor, } from './viewset/index';
21
+ export { APIView, GenericAPIView, ListModelMixin, CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, ListAPIView, CreateAPIView, RetrieveAPIView, ListCreateAPIView, RetrieveUpdateAPIView, RetrieveDestroyAPIView, RetrieveUpdateDestroyAPIView, } from './view/index';
22
+ export type { APIViewMethod, GenericAPIViewConfig, GenericAPIViewOpenAPIDescription } from './view/index';
23
+ export type { ResourceModelFieldMetadata, ResourceModelLike, ResourceModelMetadata } from './resource/index';