@danceroutine/tango-adapters-next 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 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,88 @@
1
+ # @danceroutine/tango-adapters-next
2
+
3
+ `@danceroutine/tango-adapters-next` runs Tango views and viewsets inside Next.js App Router route handlers.
4
+
5
+ Tango supplies the application-layer concepts such as serializers, API views, and model-backed viewsets. Next.js still owns routing, deployment model, and the route-handler contract. This package exists to connect those layers cleanly so that a Tango API can live inside a Next.js codebase without forcing the resource layer to become Next-specific.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @danceroutine/tango-adapters-next next react react-dom
11
+ ```
12
+
13
+ In practice, you will pair this package with Tango schema, ORM, and resources packages.
14
+
15
+ ## How it fits into a Next.js application
16
+
17
+ A typical workflow looks like this:
18
+
19
+ 1. define models with `@danceroutine/tango-schema`
20
+ 2. query and mutate data through `Model.objects` from `@danceroutine/tango-orm`
21
+ 3. expose that model-backed behavior through Tango views or viewsets
22
+ 4. adapt those handlers to App Router route files with `NextAdapter`
23
+
24
+ This adapter is concerned solely with step 4 of the above workflow. It receives Next's request and route context, invokes the Tango handler, and returns a response in the shape Next expects.
25
+
26
+ ## Quick start
27
+
28
+ ```ts
29
+ import { NextAdapter } from '@danceroutine/tango-adapters-next';
30
+ import { TodoViewSet } from '@/viewsets/TodoViewSet';
31
+
32
+ const adapter = new NextAdapter();
33
+ const viewset = new TodoViewSet();
34
+
35
+ export const { GET, POST, PATCH, PUT, DELETE } = adapter.adaptViewSet(viewset);
36
+ ```
37
+
38
+ Place that code in `app/api/todos/[[...tango]]/route.ts`. The adapter will produce handlers for the collection route at `/api/todos` and the detail route at `/api/todos/:id`.
39
+
40
+ Use `adaptViewSet(...)` when you already have a ready viewset instance in hand. Use `adaptViewSetFactory(...)` when constructing the viewset requires asynchronous setup. The factory result is memoized inside the adapter, and initialization failures clear that memoized promise so a later request can retry cleanly.
41
+
42
+ For detail requests, the adapter resolves the identifier, passes it as the second argument to `retrieve`, `update`, and `destroy`, and also populates `ctx.params.id`.
43
+
44
+ `NextAdapter` also exposes `toQueryParams(searchParams)` for route modules and server components that want the same normalized query contract resources use internally. That helper returns `TangoQueryParams` from `@danceroutine/tango-core`.
45
+
46
+ ## Public API
47
+
48
+ The root export includes:
49
+
50
+ - `NextAdapter`, the main integration class
51
+ - `AdaptNextOptions` and `AdaptNextViewSetOptions`
52
+ - route-facing helper types such as `NextAPIView`, `NextCrudViewSet`, `NextRouteHandler`, and `NextViewSetRouteHandlers`
53
+
54
+ The main adapter entry points are:
55
+
56
+ - `adaptViewSet(...)` for an already constructed viewset
57
+ - `adaptViewSetFactory(...)` for lazy async viewset construction in App Router route modules
58
+ - `adaptAPIView(...)` for `APIView`
59
+ - `adaptGenericAPIView(...)` for `GenericAPIView`-style collection/detail dispatch
60
+
61
+ You can import from the package root or from the `adapter` subpath:
62
+
63
+ ```ts
64
+ import { NextAdapter } from '@danceroutine/tango-adapters-next';
65
+ import { adapter } from '@danceroutine/tango-adapters-next';
66
+ ```
67
+
68
+ ## Documentation
69
+
70
+ - Official documentation: <https://tangowebframework.dev>
71
+ - Next.js blog tutorial: <https://tangowebframework.dev/tutorials/nextjs-blog>
72
+ - Resources topic: <https://tangowebframework.dev/topics/resources-and-viewsets>
73
+
74
+ ## Development
75
+
76
+ ```bash
77
+ pnpm --filter @danceroutine/tango-adapters-next build
78
+ pnpm --filter @danceroutine/tango-adapters-next typecheck
79
+ pnpm --filter @danceroutine/tango-adapters-next test
80
+ ```
81
+
82
+ For the wider contributor workflow, use:
83
+
84
+ - <https://tangowebframework.dev/contributing>
85
+
86
+ ## License
87
+
88
+ MIT
@@ -1,13 +1,100 @@
1
1
  import type { NextRequest } from 'next/server';
2
- import { NextResponse } from 'next/server';
3
2
  import { RequestContext } from '@danceroutine/tango-resources';
4
- import type { FrameworkAdapter, FrameworkAdapterOptions } from '@danceroutine/tango-adapters-core/adapter';
5
- export type NextRouteHandler = (request: NextRequest, context: {
6
- params?: Promise<Record<string, string>>;
7
- }) => Promise<NextResponse>;
8
- export interface AdaptNextOptions extends FrameworkAdapterOptions<NextRequest> {
9
- }
10
- export declare class NextAdapter implements FrameworkAdapter<NextResponse, NextRouteHandler, NextRequest> {
11
- adapt(handler: (ctx: RequestContext, ...args: unknown[]) => Promise<NextResponse>, options?: AdaptNextOptions): NextRouteHandler;
3
+ import { TangoQueryParams, TangoResponse } from '@danceroutine/tango-core';
4
+ import { FRAMEWORK_ADAPTER_BRAND, type FrameworkAdapter, type FrameworkAdapterOptions } from '@danceroutine/tango-adapters-core/adapter';
5
+ /**
6
+ * Next.js route handler signature produced by the adapter.
7
+ */
8
+ export type NextRouteHandler = (request: NextRequest, context?: {
9
+ params?: Promise<Record<string, string | string[]>>;
10
+ }) => Promise<Response>;
11
+ export type NextDynamicRouteContext = {
12
+ params: Promise<Record<string, string | string[]>>;
13
+ };
14
+ export type NextDynamicRouteHandler = (request: NextRequest, context: NextDynamicRouteContext) => Promise<Response>;
15
+ /**
16
+ * Adapter options for Next.js integration.
17
+ */
18
+ export type AdaptNextOptions = FrameworkAdapterOptions<NextRequest>;
19
+ /**
20
+ * Minimal CRUD viewset contract used by the Next adapter route helpers.
21
+ */
22
+ export interface NextCrudViewSet {
23
+ list(ctx: RequestContext): Promise<TangoResponse>;
24
+ create(ctx: RequestContext): Promise<TangoResponse>;
25
+ retrieve(ctx: RequestContext, id: string): Promise<TangoResponse>;
26
+ update(ctx: RequestContext, id: string): Promise<TangoResponse>;
27
+ destroy(ctx: RequestContext, id: string): Promise<TangoResponse>;
28
+ }
29
+ export interface NextAPIView {
30
+ dispatch(ctx: RequestContext): Promise<TangoResponse>;
31
+ }
32
+ export type NextViewSetFactory = () => NextCrudViewSet | Promise<NextCrudViewSet>;
33
+ export type NextAPIViewFactory = () => NextAPIView | Promise<NextAPIView>;
34
+ /**
35
+ * Options for auto-generated viewset route handlers.
36
+ */
37
+ export type AdaptNextViewSetOptions = AdaptNextOptions & {
38
+ paramKey?: string;
39
+ };
40
+ /**
41
+ * HTTP method handlers generated from a CRUD viewset.
42
+ */
43
+ export interface NextViewSetRouteHandlers {
44
+ GET: NextDynamicRouteHandler;
45
+ POST: NextDynamicRouteHandler;
46
+ PATCH: NextDynamicRouteHandler;
47
+ PUT: NextDynamicRouteHandler;
48
+ DELETE: NextDynamicRouteHandler;
49
+ }
50
+ /**
51
+ * Next.js adapter that translates route handlers to Tango `RequestContext`.
52
+ */
53
+ export declare class NextAdapter implements FrameworkAdapter<Response, NextRouteHandler, NextRequest> {
54
+ readonly __tangoBrand: typeof FRAMEWORK_ADAPTER_BRAND;
55
+ private readonly logger;
56
+ /**
57
+ * Normalize Next.js-style route search params into Tango query params.
58
+ */
59
+ toQueryParams(searchParams: Record<string, string | string[] | undefined>): TangoQueryParams;
60
+ /**
61
+ * Adapt a Tango-style handler into a Next.js route handler.
62
+ */
63
+ adapt(handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>, options?: AdaptNextOptions): NextRouteHandler;
64
+ /**
65
+ * Build Next route handlers that map HTTP verbs to standard CRUD viewset actions.
66
+ */
67
+ adaptViewSet(viewset: NextCrudViewSet, options?: AdaptNextViewSetOptions): NextViewSetRouteHandlers;
68
+ /**
69
+ * Build Next route handlers from a lazy viewset factory and memoize initialization.
70
+ * Initialization failures clear the memoized promise so subsequent requests can retry.
71
+ */
72
+ adaptViewSetFactory(factory: NextViewSetFactory, options?: AdaptNextViewSetOptions): NextViewSetRouteHandlers;
73
+ /**
74
+ * Build Next route handlers from a lazy GenericAPIView factory and memoize initialization.
75
+ */
76
+ adaptGenericAPIViewFactory(factory: NextAPIViewFactory, options?: AdaptNextViewSetOptions): NextViewSetRouteHandlers;
77
+ /**
78
+ * Build Next route handlers that dispatch an APIView by HTTP method.
79
+ */
80
+ adaptAPIView(apiView: NextAPIView, options?: AdaptNextOptions): NextViewSetRouteHandlers;
81
+ /**
82
+ * Build handlers for GenericAPIView-style collection/detail splits in catch-all routes.
83
+ */
84
+ adaptGenericAPIView(apiView: NextAPIView, options?: AdaptNextViewSetOptions): NextViewSetRouteHandlers;
85
+ private toTangoRequest;
86
+ private dispatchViewSetAction;
87
+ private invokeDetailAction;
88
+ private invokeCollectionAction;
12
89
  private createHandler;
90
+ private internalServerError;
91
+ private adaptRouteHandlersFactory;
92
+ private resolveDetailId;
93
+ private normalizeRouteParams;
94
+ private extractDirectId;
95
+ private extractCatchAllSegments;
96
+ private methodNotAllowedResponse;
97
+ private notFoundResponse;
98
+ private resolveActionMatch;
99
+ private getViewSetActions;
13
100
  }
@@ -1,4 +1,4 @@
1
1
  /**
2
2
  * Domain boundary barrel: centralizes this subdomain's public contract.
3
3
  */
4
- export { NextAdapter, type AdaptNextOptions, type NextRouteHandler } from './NextAdapter';
4
+ export { NextAdapter, type AdaptNextOptions, type AdaptNextViewSetOptions, type NextAPIView, type NextAPIViewFactory, type NextCrudViewSet, type NextDynamicRouteContext, type NextDynamicRouteHandler, type NextRouteHandler, type NextViewSetFactory, type NextViewSetRouteHandlers, } from './NextAdapter';
@@ -1,4 +1,3 @@
1
- /**
2
- * Domain boundary barrel: centralizes this subdomain's public contract.
3
- */
4
- export { NextAdapter } from './NextAdapter';
1
+ import { NextAdapter } from "../adapter-_WsZ0kE-.js";
2
+
3
+ export { NextAdapter };
@@ -0,0 +1,268 @@
1
+ import { RequestContext } from "@danceroutine/tango-resources";
2
+ import { HttpErrorFactory, TangoQueryParams, TangoRequest, TangoResponse, getLogger } from "@danceroutine/tango-core";
3
+ import { FRAMEWORK_ADAPTER_BRAND } from "@danceroutine/tango-adapters-core/adapter";
4
+ import { InternalActionMatchKind, InternalActionScope, InternalHttpMethod } from "@danceroutine/tango-adapters-core";
5
+
6
+ //#region rolldown:runtime
7
+ var __defProp = Object.defineProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all) __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true
12
+ });
13
+ };
14
+
15
+ //#endregion
16
+ //#region src/adapter/NextAdapter.ts
17
+ var NextAdapter = class {
18
+ __tangoBrand = FRAMEWORK_ADAPTER_BRAND;
19
+ logger = getLogger("tango.adapter.next");
20
+ /**
21
+ * Normalize Next.js-style route search params into Tango query params.
22
+ */
23
+ toQueryParams(searchParams) {
24
+ return TangoQueryParams.fromRecord(searchParams);
25
+ }
26
+ /**
27
+ * Adapt a Tango-style handler into a Next.js route handler.
28
+ */
29
+ adapt(handler, options = {}) {
30
+ return this.createHandler(handler, options);
31
+ }
32
+ /**
33
+ * Build Next route handlers that map HTTP verbs to standard CRUD viewset actions.
34
+ */
35
+ adaptViewSet(viewset, options = {}) {
36
+ const paramKey = options.paramKey ?? "tango";
37
+ return {
38
+ GET: this.adapt((ctx) => this.dispatchViewSetAction(viewset, InternalHttpMethod.GET, ctx, paramKey, (segments, directId) => {
39
+ if (directId) return viewset.retrieve(ctx, directId);
40
+ if (segments.length === 0) return viewset.list(ctx);
41
+ if (segments.length === 1) return viewset.retrieve(ctx, segments[0]);
42
+ return this.notFoundResponse();
43
+ }), options),
44
+ POST: this.adapt((ctx) => this.dispatchViewSetAction(viewset, InternalHttpMethod.POST, ctx, paramKey, (segments, directId) => {
45
+ if (segments.length === 0 && !directId) return viewset.create(ctx);
46
+ return this.methodNotAllowedResponse();
47
+ }), options),
48
+ PATCH: this.adapt((ctx) => this.dispatchViewSetAction(viewset, InternalHttpMethod.PATCH, ctx, paramKey, (segments, directId) => {
49
+ const id = directId ?? segments[0];
50
+ return id ? viewset.update(ctx, id) : this.methodNotAllowedResponse();
51
+ }), options),
52
+ PUT: this.adapt((ctx) => this.dispatchViewSetAction(viewset, InternalHttpMethod.PUT, ctx, paramKey, (segments, directId) => {
53
+ const id = directId ?? segments[0];
54
+ return id ? viewset.update(ctx, id) : this.methodNotAllowedResponse();
55
+ }), options),
56
+ DELETE: this.adapt((ctx) => this.dispatchViewSetAction(viewset, InternalHttpMethod.DELETE, ctx, paramKey, (segments, directId) => {
57
+ const id = directId ?? segments[0];
58
+ return id ? viewset.destroy(ctx, id) : this.methodNotAllowedResponse();
59
+ }), options)
60
+ };
61
+ }
62
+ /**
63
+ * Build Next route handlers from a lazy viewset factory and memoize initialization.
64
+ * Initialization failures clear the memoized promise so subsequent requests can retry.
65
+ */
66
+ adaptViewSetFactory(factory, options = {}) {
67
+ return this.adaptRouteHandlersFactory(async () => {
68
+ const viewset = await factory();
69
+ return this.adaptViewSet(viewset, options);
70
+ });
71
+ }
72
+ /**
73
+ * Build Next route handlers from a lazy GenericAPIView factory and memoize initialization.
74
+ */
75
+ adaptGenericAPIViewFactory(factory, options = {}) {
76
+ return this.adaptRouteHandlersFactory(async () => {
77
+ const apiView = await factory();
78
+ return this.adaptGenericAPIView(apiView, options);
79
+ });
80
+ }
81
+ /**
82
+ * Build Next route handlers that dispatch an APIView by HTTP method.
83
+ */
84
+ adaptAPIView(apiView, options = {}) {
85
+ return {
86
+ GET: this.adapt((ctx) => apiView.dispatch(ctx), options),
87
+ POST: this.adapt((ctx) => apiView.dispatch(ctx), options),
88
+ PATCH: this.adapt((ctx) => apiView.dispatch(ctx), options),
89
+ PUT: this.adapt((ctx) => apiView.dispatch(ctx), options),
90
+ DELETE: this.adapt((ctx) => apiView.dispatch(ctx), options)
91
+ };
92
+ }
93
+ /**
94
+ * Build handlers for GenericAPIView-style collection/detail splits in catch-all routes.
95
+ */
96
+ adaptGenericAPIView(apiView, options = {}) {
97
+ const paramKey = options.paramKey ?? "tango";
98
+ return {
99
+ GET: this.adapt(async (ctx) => {
100
+ const detailId = this.resolveDetailId(ctx.params, paramKey);
101
+ if (!detailId) return apiView.dispatch(ctx);
102
+ ctx.params.id = detailId;
103
+ return apiView.dispatch(ctx);
104
+ }, options),
105
+ POST: this.adapt(async (ctx) => {
106
+ const detailId = this.resolveDetailId(ctx.params, paramKey);
107
+ if (detailId) return this.methodNotAllowedResponse();
108
+ return apiView.dispatch(ctx);
109
+ }, options),
110
+ PATCH: this.adapt(async (ctx) => {
111
+ const detailId = this.resolveDetailId(ctx.params, paramKey);
112
+ if (!detailId) return this.methodNotAllowedResponse();
113
+ ctx.params.id = detailId;
114
+ return apiView.dispatch(ctx);
115
+ }, options),
116
+ PUT: this.adapt(async (ctx) => {
117
+ const detailId = this.resolveDetailId(ctx.params, paramKey);
118
+ if (!detailId) return this.methodNotAllowedResponse();
119
+ ctx.params.id = detailId;
120
+ return apiView.dispatch(ctx);
121
+ }, options),
122
+ DELETE: this.adapt(async (ctx) => {
123
+ const detailId = this.resolveDetailId(ctx.params, paramKey);
124
+ if (!detailId) return this.methodNotAllowedResponse();
125
+ ctx.params.id = detailId;
126
+ return apiView.dispatch(ctx);
127
+ }, options)
128
+ };
129
+ }
130
+ toTangoRequest(request) {
131
+ if (TangoRequest.isTangoRequest(request)) return request;
132
+ if (request instanceof Request) return new TangoRequest(request);
133
+ return new TangoRequest(String(request.url ?? "http://localhost"));
134
+ }
135
+ dispatchViewSetAction(viewset, method, ctx, paramKey, fallback) {
136
+ const segments = this.extractCatchAllSegments(ctx.params, paramKey);
137
+ const directId = this.extractDirectId(ctx.params);
138
+ const actionMatch = this.resolveActionMatch(viewset, method, segments);
139
+ if (actionMatch?.kind === InternalActionMatchKind.METHOD_NOT_ALLOWED) return Promise.resolve(this.methodNotAllowedResponse());
140
+ if (actionMatch?.kind === InternalActionMatchKind.DETAIL) return this.invokeDetailAction(viewset, actionMatch.action, ctx, actionMatch.id);
141
+ if (actionMatch?.kind === InternalActionMatchKind.COLLECTION) return this.invokeCollectionAction(viewset, actionMatch.action, ctx);
142
+ return Promise.resolve(fallback(segments, directId));
143
+ }
144
+ invokeDetailAction(viewset, action, ctx, id) {
145
+ const candidate = viewset[action.name];
146
+ if (typeof candidate !== "function") return Promise.resolve(this.notFoundResponse());
147
+ return candidate.call(viewset, ctx, id);
148
+ }
149
+ invokeCollectionAction(viewset, action, ctx) {
150
+ const candidate = viewset[action.name];
151
+ if (typeof candidate !== "function") return Promise.resolve(this.notFoundResponse());
152
+ return candidate.call(viewset, ctx);
153
+ }
154
+ createHandler(handler, options) {
155
+ return async (request, routeContext) => {
156
+ try {
157
+ const user = options.getUser ? await options.getUser(request) : null;
158
+ const rawParams = routeContext?.params ? await routeContext.params : {};
159
+ const params = this.normalizeRouteParams(rawParams);
160
+ const ctx = RequestContext.create(this.toTangoRequest(request), user);
161
+ if (Object.keys(params).length > 0) ctx.params = params;
162
+ const id = params?.id;
163
+ if (id && handler.length > 1) return (await handler(ctx, id)).toWebResponse();
164
+ return (await handler(ctx)).toWebResponse();
165
+ } catch (error) {
166
+ return this.internalServerError(error);
167
+ }
168
+ };
169
+ }
170
+ internalServerError(error) {
171
+ this.logger.error("Adapter error:", error);
172
+ const httpError = HttpErrorFactory.toHttpError(error);
173
+ return TangoResponse.json(httpError.body, { status: httpError.status }).toWebResponse();
174
+ }
175
+ adaptRouteHandlersFactory(factory) {
176
+ let handlersPromise = null;
177
+ const getHandlers = async () => {
178
+ if (!handlersPromise) {
179
+ const initializing = factory();
180
+ handlersPromise = initializing.catch((error) => {
181
+ handlersPromise = null;
182
+ throw error;
183
+ });
184
+ }
185
+ return handlersPromise;
186
+ };
187
+ const createLazyHandler = (method) => {
188
+ return async (request, context) => {
189
+ try {
190
+ const handlers = await getHandlers();
191
+ return handlers[method](request, context);
192
+ } catch (error) {
193
+ return this.internalServerError(error);
194
+ }
195
+ };
196
+ };
197
+ return {
198
+ GET: createLazyHandler("GET"),
199
+ POST: createLazyHandler("POST"),
200
+ PATCH: createLazyHandler("PATCH"),
201
+ PUT: createLazyHandler("PUT"),
202
+ DELETE: createLazyHandler("DELETE")
203
+ };
204
+ }
205
+ resolveDetailId(params, paramKey) {
206
+ const directId = this.extractDirectId(params);
207
+ if (directId) return directId;
208
+ const segments = this.extractCatchAllSegments(params, paramKey);
209
+ if (segments.length !== 1) return null;
210
+ return segments[0];
211
+ }
212
+ normalizeRouteParams(raw) {
213
+ const entries = Object.entries(raw).map(([key, value]) => {
214
+ return [key, Array.isArray(value) ? value.join("/") : value];
215
+ });
216
+ return Object.fromEntries(entries);
217
+ }
218
+ extractDirectId(params) {
219
+ const directId = params.id?.trim();
220
+ return directId || null;
221
+ }
222
+ extractCatchAllSegments(params, paramKey) {
223
+ const catchAll = params[paramKey]?.trim() ?? "";
224
+ if (!catchAll) return [];
225
+ return catchAll.split("/").filter(Boolean);
226
+ }
227
+ methodNotAllowedResponse() {
228
+ return TangoResponse.json({ error: "Method not allowed for this route." }, { status: 405 });
229
+ }
230
+ notFoundResponse() {
231
+ return TangoResponse.json({ error: "Not found." }, { status: 404 });
232
+ }
233
+ resolveActionMatch(viewset, method, segments) {
234
+ if (segments.length === 0) return null;
235
+ const actions = this.getViewSetActions(viewset);
236
+ if (segments.length >= 2) {
237
+ const detailPath = segments.slice(1).join("/");
238
+ const detailMatch = actions.find((action) => action.scope === InternalActionScope.DETAIL && action.path === detailPath);
239
+ if (detailMatch) return detailMatch.methods.includes(method) ? {
240
+ kind: InternalActionMatchKind.DETAIL,
241
+ action: detailMatch,
242
+ id: segments[0]
243
+ } : { kind: InternalActionMatchKind.METHOD_NOT_ALLOWED };
244
+ }
245
+ if (method === InternalHttpMethod.GET && segments.length === 1) return null;
246
+ const collectionPath = segments.join("/");
247
+ const collectionMatch = actions.find((action) => action.scope === InternalActionScope.COLLECTION && action.path === collectionPath);
248
+ if (!collectionMatch) return null;
249
+ return collectionMatch.methods.includes(method) ? {
250
+ kind: InternalActionMatchKind.COLLECTION,
251
+ action: collectionMatch
252
+ } : { kind: InternalActionMatchKind.METHOD_NOT_ALLOWED };
253
+ }
254
+ getViewSetActions(viewset) {
255
+ const constructorValue = viewset.constructor;
256
+ if (typeof constructorValue.getActions !== "function") return [];
257
+ return constructorValue.getActions(viewset);
258
+ }
259
+ };
260
+
261
+ //#endregion
262
+ //#region src/adapter/index.ts
263
+ var adapter_exports = {};
264
+ __export(adapter_exports, { NextAdapter: () => NextAdapter });
265
+
266
+ //#endregion
267
+ export { NextAdapter, adapter_exports };
268
+ //# sourceMappingURL=adapter-_WsZ0kE-.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter-_WsZ0kE-.js","names":["searchParams: Record<string, string | string[] | undefined>","handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>","options: AdaptNextOptions","viewset: NextCrudViewSet","options: AdaptNextViewSetOptions","factory: NextViewSetFactory","factory: NextAPIViewFactory","apiView: NextAPIView","request: NextRequest","method: HttpMethod","ctx: RequestContext","paramKey: string","fallback: (segments: string[], directId: string | null) => TangoResponse | Promise<TangoResponse>","action: ResolvedViewSetActionDescriptor","id: string","error: unknown","factory: () => Promise<NextViewSetRouteHandlers>","handlersPromise: Promise<NextViewSetRouteHandlers> | null","method: keyof NextViewSetRouteHandlers","params: Record<string, string>","raw: Record<string, string | string[]>","segments: string[]"],"sources":["../src/adapter/NextAdapter.ts","../src/adapter/index.ts"],"sourcesContent":["import type { NextRequest } from 'next/server';\nimport { RequestContext } from '@danceroutine/tango-resources';\nimport {\n HttpErrorFactory,\n TangoQueryParams,\n TangoRequest,\n TangoResponse,\n getLogger,\n type JsonValue,\n} from '@danceroutine/tango-core';\nimport {\n FRAMEWORK_ADAPTER_BRAND,\n type FrameworkAdapter,\n type FrameworkAdapterOptions,\n} from '@danceroutine/tango-adapters-core/adapter';\nimport { InternalHttpMethod, InternalActionScope, InternalActionMatchKind } from '@danceroutine/tango-adapters-core';\n\ntype HttpMethod = (typeof InternalHttpMethod)[keyof typeof InternalHttpMethod];\ntype ActionScope = (typeof InternalActionScope)[keyof typeof InternalActionScope];\n\ntype ResolvedViewSetActionDescriptor = {\n name: string;\n scope: ActionScope;\n methods: readonly HttpMethod[];\n path: string;\n};\n\n/**\n * Next.js route handler signature produced by the adapter.\n */\nexport type NextRouteHandler = (\n request: NextRequest,\n context?: { params?: Promise<Record<string, string | string[]>> }\n) => Promise<Response>;\n\nexport type NextDynamicRouteContext = {\n params: Promise<Record<string, string | string[]>>;\n};\n\nexport type NextDynamicRouteHandler = (request: NextRequest, context: NextDynamicRouteContext) => Promise<Response>;\n\n/**\n * Adapter options for Next.js integration.\n */\nexport type AdaptNextOptions = FrameworkAdapterOptions<NextRequest>;\n\n/**\n * Minimal CRUD viewset contract used by the Next adapter route helpers.\n */\nexport interface NextCrudViewSet {\n list(ctx: RequestContext): Promise<TangoResponse>;\n create(ctx: RequestContext): Promise<TangoResponse>;\n retrieve(ctx: RequestContext, id: string): Promise<TangoResponse>;\n update(ctx: RequestContext, id: string): Promise<TangoResponse>;\n destroy(ctx: RequestContext, id: string): Promise<TangoResponse>;\n}\n\nexport interface NextAPIView {\n dispatch(ctx: RequestContext): Promise<TangoResponse>;\n}\n\nexport type NextViewSetFactory = () => NextCrudViewSet | Promise<NextCrudViewSet>;\nexport type NextAPIViewFactory = () => NextAPIView | Promise<NextAPIView>;\n\n/**\n * Options for auto-generated viewset route handlers.\n */\nexport type AdaptNextViewSetOptions = AdaptNextOptions & {\n paramKey?: string;\n};\n\n/**\n * HTTP method handlers generated from a CRUD viewset.\n */\nexport interface NextViewSetRouteHandlers {\n GET: NextDynamicRouteHandler;\n POST: NextDynamicRouteHandler;\n PATCH: NextDynamicRouteHandler;\n PUT: NextDynamicRouteHandler;\n DELETE: NextDynamicRouteHandler;\n}\n\ntype ActionMatch =\n | { kind: typeof InternalActionMatchKind.DETAIL; action: ResolvedViewSetActionDescriptor; id: string }\n | { kind: typeof InternalActionMatchKind.COLLECTION; action: ResolvedViewSetActionDescriptor }\n | { kind: typeof InternalActionMatchKind.METHOD_NOT_ALLOWED };\n\n/**\n * Next.js adapter that translates route handlers to Tango `RequestContext`.\n */\nexport class NextAdapter implements FrameworkAdapter<Response, NextRouteHandler, NextRequest> {\n readonly __tangoBrand: typeof FRAMEWORK_ADAPTER_BRAND = FRAMEWORK_ADAPTER_BRAND;\n private readonly logger = getLogger('tango.adapter.next');\n /**\n * Normalize Next.js-style route search params into Tango query params.\n */\n toQueryParams(searchParams: Record<string, string | string[] | undefined>): TangoQueryParams {\n return TangoQueryParams.fromRecord(searchParams);\n }\n\n /**\n * Adapt a Tango-style handler into a Next.js route handler.\n */\n adapt(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>,\n options: AdaptNextOptions = {}\n ): NextRouteHandler {\n return this.createHandler(handler, options);\n }\n\n /**\n * Build Next route handlers that map HTTP verbs to standard CRUD viewset actions.\n */\n adaptViewSet(viewset: NextCrudViewSet, options: AdaptNextViewSetOptions = {}): NextViewSetRouteHandlers {\n const paramKey = options.paramKey ?? 'tango';\n return {\n GET: this.adapt(\n (ctx) =>\n this.dispatchViewSetAction(viewset, InternalHttpMethod.GET, ctx, paramKey, (segments, directId) => {\n if (directId) return viewset.retrieve(ctx, directId);\n if (segments.length === 0) return viewset.list(ctx);\n if (segments.length === 1) return viewset.retrieve(ctx, segments[0] as string);\n return this.notFoundResponse();\n }),\n options\n ),\n POST: this.adapt(\n (ctx) =>\n this.dispatchViewSetAction(\n viewset,\n InternalHttpMethod.POST,\n ctx,\n paramKey,\n (segments, directId) => {\n if (segments.length === 0 && !directId) return viewset.create(ctx);\n return this.methodNotAllowedResponse();\n }\n ),\n options\n ),\n PATCH: this.adapt(\n (ctx) =>\n this.dispatchViewSetAction(\n viewset,\n InternalHttpMethod.PATCH,\n ctx,\n paramKey,\n (segments, directId) => {\n const id = directId ?? segments[0];\n return id ? viewset.update(ctx, id) : this.methodNotAllowedResponse();\n }\n ),\n options\n ),\n PUT: this.adapt(\n (ctx) =>\n this.dispatchViewSetAction(viewset, InternalHttpMethod.PUT, ctx, paramKey, (segments, directId) => {\n const id = directId ?? segments[0];\n return id ? viewset.update(ctx, id) : this.methodNotAllowedResponse();\n }),\n options\n ),\n DELETE: this.adapt(\n (ctx) =>\n this.dispatchViewSetAction(\n viewset,\n InternalHttpMethod.DELETE,\n ctx,\n paramKey,\n (segments, directId) => {\n const id = directId ?? segments[0];\n return id ? viewset.destroy(ctx, id) : this.methodNotAllowedResponse();\n }\n ),\n options\n ),\n };\n }\n\n /**\n * Build Next route handlers from a lazy viewset factory and memoize initialization.\n * Initialization failures clear the memoized promise so subsequent requests can retry.\n */\n adaptViewSetFactory(factory: NextViewSetFactory, options: AdaptNextViewSetOptions = {}): NextViewSetRouteHandlers {\n return this.adaptRouteHandlersFactory(async () => {\n const viewset = await factory();\n return this.adaptViewSet(viewset, options);\n });\n }\n\n /**\n * Build Next route handlers from a lazy GenericAPIView factory and memoize initialization.\n */\n adaptGenericAPIViewFactory(\n factory: NextAPIViewFactory,\n options: AdaptNextViewSetOptions = {}\n ): NextViewSetRouteHandlers {\n return this.adaptRouteHandlersFactory(async () => {\n const apiView = await factory();\n return this.adaptGenericAPIView(apiView, options);\n });\n }\n\n /**\n * Build Next route handlers that dispatch an APIView by HTTP method.\n */\n adaptAPIView(apiView: NextAPIView, options: AdaptNextOptions = {}): NextViewSetRouteHandlers {\n return {\n GET: this.adapt((ctx) => apiView.dispatch(ctx), options),\n POST: this.adapt((ctx) => apiView.dispatch(ctx), options),\n PATCH: this.adapt((ctx) => apiView.dispatch(ctx), options),\n PUT: this.adapt((ctx) => apiView.dispatch(ctx), options),\n DELETE: this.adapt((ctx) => apiView.dispatch(ctx), options),\n };\n }\n\n /**\n * Build handlers for GenericAPIView-style collection/detail splits in catch-all routes.\n */\n adaptGenericAPIView(apiView: NextAPIView, options: AdaptNextViewSetOptions = {}): NextViewSetRouteHandlers {\n // Default catch-all param matches the Next.js [[...tango]] route convention.\n const paramKey = options.paramKey ?? 'tango';\n return {\n GET: this.adapt(async (ctx) => {\n const detailId = this.resolveDetailId(ctx.params, paramKey);\n if (!detailId) {\n return apiView.dispatch(ctx);\n }\n ctx.params.id = detailId;\n return apiView.dispatch(ctx);\n }, options),\n POST: this.adapt(async (ctx) => {\n const detailId = this.resolveDetailId(ctx.params, paramKey);\n if (detailId) {\n return this.methodNotAllowedResponse();\n }\n return apiView.dispatch(ctx);\n }, options),\n PATCH: this.adapt(async (ctx) => {\n const detailId = this.resolveDetailId(ctx.params, paramKey);\n if (!detailId) {\n return this.methodNotAllowedResponse();\n }\n ctx.params.id = detailId;\n return apiView.dispatch(ctx);\n }, options),\n PUT: this.adapt(async (ctx) => {\n const detailId = this.resolveDetailId(ctx.params, paramKey);\n if (!detailId) {\n return this.methodNotAllowedResponse();\n }\n ctx.params.id = detailId;\n return apiView.dispatch(ctx);\n }, options),\n DELETE: this.adapt(async (ctx) => {\n const detailId = this.resolveDetailId(ctx.params, paramKey);\n if (!detailId) {\n return this.methodNotAllowedResponse();\n }\n ctx.params.id = detailId;\n return apiView.dispatch(ctx);\n }, options),\n };\n }\n\n private toTangoRequest(request: NextRequest): TangoRequest {\n if (TangoRequest.isTangoRequest(request)) {\n return request;\n }\n\n // oxlint-disable-next-line eslint-js/no-restricted-syntax\n if (request instanceof Request) {\n return new TangoRequest(request);\n }\n\n return new TangoRequest(String((request as { url?: unknown }).url ?? 'http://localhost'));\n }\n\n private dispatchViewSetAction(\n viewset: NextCrudViewSet,\n method: HttpMethod,\n ctx: RequestContext,\n paramKey: string,\n fallback: (segments: string[], directId: string | null) => TangoResponse | Promise<TangoResponse>\n ): Promise<TangoResponse> {\n const segments = this.extractCatchAllSegments(ctx.params, paramKey);\n const directId = this.extractDirectId(ctx.params);\n const actionMatch = this.resolveActionMatch(viewset, method, segments);\n\n if (actionMatch?.kind === InternalActionMatchKind.METHOD_NOT_ALLOWED) {\n return Promise.resolve(this.methodNotAllowedResponse());\n }\n if (actionMatch?.kind === InternalActionMatchKind.DETAIL) {\n return this.invokeDetailAction(viewset, actionMatch.action, ctx, actionMatch.id);\n }\n if (actionMatch?.kind === InternalActionMatchKind.COLLECTION) {\n return this.invokeCollectionAction(viewset, actionMatch.action, ctx);\n }\n\n return Promise.resolve(fallback(segments, directId));\n }\n\n private invokeDetailAction(\n viewset: NextCrudViewSet,\n action: ResolvedViewSetActionDescriptor,\n ctx: RequestContext,\n id: string\n ): Promise<TangoResponse> {\n const candidate = (viewset as unknown as Record<string, unknown>)[action.name];\n if (typeof candidate !== 'function') {\n return Promise.resolve(this.notFoundResponse());\n }\n return (candidate as (this: NextCrudViewSet, ctx: RequestContext, id: string) => Promise<TangoResponse>).call(\n viewset,\n ctx,\n id\n );\n }\n\n private invokeCollectionAction(\n viewset: NextCrudViewSet,\n action: ResolvedViewSetActionDescriptor,\n ctx: RequestContext\n ): Promise<TangoResponse> {\n const candidate = (viewset as unknown as Record<string, unknown>)[action.name];\n if (typeof candidate !== 'function') {\n return Promise.resolve(this.notFoundResponse());\n }\n return (candidate as (this: NextCrudViewSet, ctx: RequestContext) => Promise<TangoResponse>).call(viewset, ctx);\n }\n\n private createHandler(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<TangoResponse>,\n options: AdaptNextOptions\n ): NextRouteHandler {\n return async (request: NextRequest, routeContext) => {\n try {\n const user = options.getUser ? await options.getUser(request) : null;\n const rawParams = routeContext?.params ? await routeContext.params : {};\n const params = this.normalizeRouteParams(rawParams);\n\n const ctx = RequestContext.create(this.toTangoRequest(request), user);\n if (Object.keys(params).length > 0) {\n ctx.params = params;\n }\n\n const id = params?.id;\n if (id && handler.length > 1) {\n return (await handler(ctx, id)).toWebResponse();\n }\n\n return (await handler(ctx)).toWebResponse();\n } catch (error) {\n return this.internalServerError(error);\n }\n };\n }\n\n private internalServerError(error: unknown): Response {\n this.logger.error('Adapter error:', error);\n const httpError = HttpErrorFactory.toHttpError(error);\n return TangoResponse.json(httpError.body as JsonValue, { status: httpError.status }).toWebResponse();\n }\n\n private adaptRouteHandlersFactory(factory: () => Promise<NextViewSetRouteHandlers>): NextViewSetRouteHandlers {\n let handlersPromise: Promise<NextViewSetRouteHandlers> | null = null;\n\n const getHandlers = async (): Promise<NextViewSetRouteHandlers> => {\n if (!handlersPromise) {\n const initializing = factory();\n handlersPromise = initializing.catch((error) => {\n handlersPromise = null;\n throw error;\n });\n }\n return handlersPromise;\n };\n\n const createLazyHandler = (method: keyof NextViewSetRouteHandlers): NextDynamicRouteHandler => {\n return async (request, context) => {\n try {\n const handlers = await getHandlers();\n return handlers[method](request, context);\n } catch (error) {\n return this.internalServerError(error);\n }\n };\n };\n\n return {\n GET: createLazyHandler('GET'),\n POST: createLazyHandler('POST'),\n PATCH: createLazyHandler('PATCH'),\n PUT: createLazyHandler('PUT'),\n DELETE: createLazyHandler('DELETE'),\n };\n }\n\n private resolveDetailId(params: Record<string, string>, paramKey: string): string | null {\n const directId = this.extractDirectId(params);\n if (directId) {\n return directId;\n }\n const segments = this.extractCatchAllSegments(params, paramKey);\n if (segments.length !== 1) {\n return null;\n }\n return segments[0] as string;\n }\n\n private normalizeRouteParams(raw: Record<string, string | string[]>): Record<string, string> {\n const entries = Object.entries(raw).map(([key, value]) => {\n return [key, Array.isArray(value) ? value.join('/') : value];\n });\n\n return Object.fromEntries(entries);\n }\n\n private extractDirectId(params: Record<string, string>): string | null {\n const directId = params.id?.trim();\n return directId || null;\n }\n\n private extractCatchAllSegments(params: Record<string, string>, paramKey: string): string[] {\n const catchAll = params[paramKey]?.trim() ?? '';\n if (!catchAll) {\n return [];\n }\n return catchAll.split('/').filter(Boolean);\n }\n\n private methodNotAllowedResponse(): TangoResponse {\n return TangoResponse.json(\n {\n error: 'Method not allowed for this route.',\n },\n { status: 405 }\n );\n }\n\n private notFoundResponse(): TangoResponse {\n return TangoResponse.json(\n {\n error: 'Not found.',\n },\n { status: 404 }\n );\n }\n\n private resolveActionMatch(viewset: NextCrudViewSet, method: HttpMethod, segments: string[]): ActionMatch | null {\n if (segments.length === 0) {\n return null;\n }\n\n const actions = this.getViewSetActions(viewset);\n\n if (segments.length >= 2) {\n const detailPath = segments.slice(1).join('/');\n const detailMatch = actions.find(\n (action) => action.scope === InternalActionScope.DETAIL && action.path === detailPath\n );\n if (detailMatch) {\n return detailMatch.methods.includes(method)\n ? { kind: InternalActionMatchKind.DETAIL, action: detailMatch, id: segments[0] as string }\n : { kind: InternalActionMatchKind.METHOD_NOT_ALLOWED };\n }\n }\n\n if (method === InternalHttpMethod.GET && segments.length === 1) {\n return null;\n }\n\n const collectionPath = segments.join('/');\n const collectionMatch = actions.find(\n (action) => action.scope === InternalActionScope.COLLECTION && action.path === collectionPath\n );\n if (!collectionMatch) {\n return null;\n }\n return collectionMatch.methods.includes(method)\n ? { kind: InternalActionMatchKind.COLLECTION, action: collectionMatch }\n : { kind: InternalActionMatchKind.METHOD_NOT_ALLOWED };\n }\n\n private getViewSetActions(viewset: NextCrudViewSet): readonly ResolvedViewSetActionDescriptor[] {\n const constructorValue = viewset.constructor as {\n getActions?: (input: NextCrudViewSet) => readonly ResolvedViewSetActionDescriptor[];\n };\n\n if (typeof constructorValue.getActions !== 'function') {\n return [];\n }\n\n return constructorValue.getActions(viewset);\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport {\n NextAdapter,\n type AdaptNextOptions,\n type AdaptNextViewSetOptions,\n type NextAPIView,\n type NextAPIViewFactory,\n type NextCrudViewSet,\n type NextDynamicRouteContext,\n type NextDynamicRouteHandler,\n type NextRouteHandler,\n type NextViewSetFactory,\n type NextViewSetRouteHandlers,\n} from './NextAdapter';\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  * top-level symbols for TS-native ergonomic imports.
4
4
  */
5
5
  export * as adapter from './adapter/index';
6
- export { NextAdapter, type AdaptNextOptions, type NextRouteHandler } from './adapter/index';
6
+ export { NextAdapter, type AdaptNextOptions, type AdaptNextViewSetOptions, type NextAPIView, type NextAPIViewFactory, type NextCrudViewSet, type NextDynamicRouteContext, type NextDynamicRouteHandler, type NextRouteHandler, type NextViewSetFactory, type NextViewSetRouteHandlers, } from './adapter/index';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { NextAdapter, adapter_exports } from "./adapter-BtnGpM5G.js";
1
+ import { NextAdapter, adapter_exports } from "./adapter-_WsZ0kE-.js";
2
2
 
3
3
  export { NextAdapter, adapter_exports as adapter };
package/package.json CHANGED
@@ -1,54 +1,57 @@
1
1
  {
2
- "name": "@danceroutine/tango-adapters-next",
3
- "version": "0.1.0",
4
- "description": "Next.js App Router adapter for Tango viewsets",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.js"
12
- },
13
- "./adapter": {
14
- "types": "./dist/adapter/index.d.ts",
15
- "import": "./dist/adapter/index.js"
16
- }
2
+ "name": "@danceroutine/tango-adapters-next",
3
+ "version": "1.0.0",
4
+ "description": "Next.js App Router adapter for Tango viewsets",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
17
12
  },
18
- "files": [
19
- "dist"
20
- ],
21
- "scripts": {
22
- "build": "tsdown",
23
- "test": "vitest run --coverage",
24
- "test:watch": "vitest",
25
- "typecheck": "tsc --noEmit"
26
- },
27
- "keywords": [
28
- "tango",
29
- "next",
30
- "nextjs",
31
- "adapter"
32
- ],
33
- "author": "Pedro Del Moral Lopez",
34
- "license": "MIT",
35
- "repository": {
36
- "type": "git",
37
- "url": "https://github.com/danceroutine/tango.git",
38
- "directory": "packages/adapters/next"
39
- },
40
- "dependencies": {
41
- "@danceroutine/tango-resources": "workspace:*",
42
- "@danceroutine/tango-adapters-core": "workspace:*"
43
- },
44
- "peerDependencies": {
45
- "next": "^15.0.0"
46
- },
47
- "devDependencies": {
48
- "@types/node": "^22.9.0",
49
- "next": "^15.0.3",
50
- "tsdown": "^0.4.0",
51
- "typescript": "^5.6.3",
52
- "vitest": "^4.0.6"
13
+ "./adapter": {
14
+ "types": "./dist/adapter/index.d.ts",
15
+ "import": "./dist/adapter/index.js"
53
16
  }
54
- }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "keywords": [
22
+ "tango",
23
+ "next",
24
+ "nextjs",
25
+ "adapter"
26
+ ],
27
+ "author": "Pedro Del Moral Lopez",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/danceroutine/tango.git",
32
+ "directory": "packages/adapters/next"
33
+ },
34
+ "dependencies": {
35
+ "@danceroutine/tango-core": "1.0.0",
36
+ "@danceroutine/tango-resources": "1.0.0",
37
+ "@danceroutine/tango-adapters-core": "1.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "next": "^15.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.9.0",
44
+ "next": "^15.0.3",
45
+ "tsdown": "^0.4.0",
46
+ "typescript": "^5.6.3",
47
+ "vitest": "^4.0.6"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "test": "vitest run --coverage",
52
+ "test:watch": "vitest",
53
+ "typecheck": "pnpm run typecheck:prod && pnpm run typecheck:test",
54
+ "typecheck:prod": "tsc --noEmit -p tsconfig.json",
55
+ "typecheck:test": "tsc --noEmit -p tsconfig.tests.json"
56
+ }
57
+ }
@@ -1,31 +0,0 @@
1
- import { NextResponse } from 'next/server';
2
- import { RequestContext } from '@danceroutine/tango-resources';
3
- export class NextAdapter {
4
- adapt(handler, options = {}) {
5
- return this.createHandler(handler, options);
6
- }
7
- createHandler(handler, options) {
8
- return async (request, routeContext) => {
9
- try {
10
- const user = options.getUser ? await options.getUser(request) : null;
11
- const params = routeContext?.params ? await routeContext.params : undefined;
12
- const ctx = RequestContext.create(request, user);
13
- if (params) {
14
- ctx.params = params;
15
- }
16
- const id = params?.id;
17
- if (id && handler.length > 1) {
18
- return await handler(ctx, id);
19
- }
20
- return await handler(ctx);
21
- }
22
- catch (error) {
23
- console.error('Adapter error:', error);
24
- return new NextResponse(JSON.stringify({ error: 'Internal Server Error' }), {
25
- status: 500,
26
- headers: { 'Content-Type': 'application/json' },
27
- });
28
- }
29
- };
30
- }
31
- }
@@ -1,47 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { RequestContext } from "@danceroutine/tango-resources";
3
-
4
- //#region rolldown:runtime
5
- var __defProp = Object.defineProperty;
6
- var __export = (target, all) => {
7
- for (var name in all) __defProp(target, name, {
8
- get: all[name],
9
- enumerable: true
10
- });
11
- };
12
-
13
- //#endregion
14
- //#region src/adapter/NextAdapter.ts
15
- var NextAdapter = class {
16
- adapt(handler, options = {}) {
17
- return this.createHandler(handler, options);
18
- }
19
- createHandler(handler, options) {
20
- return async (request, routeContext) => {
21
- try {
22
- const user = options.getUser ? await options.getUser(request) : null;
23
- const params = routeContext?.params ? await routeContext.params : undefined;
24
- const ctx = RequestContext.create(request, user);
25
- if (params) ctx.params = params;
26
- const id = params?.id;
27
- if (id && handler.length > 1) return await handler(ctx, id);
28
- return await handler(ctx);
29
- } catch (error) {
30
- console.error("Adapter error:", error);
31
- return new NextResponse(JSON.stringify({ error: "Internal Server Error" }), {
32
- status: 500,
33
- headers: { "Content-Type": "application/json" }
34
- });
35
- }
36
- };
37
- }
38
- };
39
-
40
- //#endregion
41
- //#region src/adapter/index.ts
42
- var adapter_exports = {};
43
- __export(adapter_exports, { NextAdapter: () => NextAdapter });
44
-
45
- //#endregion
46
- export { NextAdapter, adapter_exports };
47
- //# sourceMappingURL=adapter-BtnGpM5G.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter-BtnGpM5G.js","names":["handler: (ctx: RequestContext, ...args: unknown[]) => Promise<NextResponse>","options: AdaptNextOptions","request: NextRequest"],"sources":["../src/adapter/NextAdapter.ts","../src/adapter/index.ts"],"sourcesContent":["import type { NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport { RequestContext } from '@danceroutine/tango-resources';\nimport type { FrameworkAdapter, FrameworkAdapterOptions } from '@danceroutine/tango-adapters-core/adapter';\n\nexport type NextRouteHandler = (\n request: NextRequest,\n context: { params?: Promise<Record<string, string>> }\n) => Promise<NextResponse>;\n\nexport interface AdaptNextOptions extends FrameworkAdapterOptions<NextRequest> {}\n\nexport class NextAdapter implements FrameworkAdapter<NextResponse, NextRouteHandler, NextRequest> {\n adapt(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<NextResponse>,\n options: AdaptNextOptions = {}\n ): NextRouteHandler {\n return this.createHandler(handler, options);\n }\n\n private createHandler(\n handler: (ctx: RequestContext, ...args: unknown[]) => Promise<NextResponse>,\n options: AdaptNextOptions\n ): NextRouteHandler {\n return async (request: NextRequest, routeContext) => {\n try {\n const user = options.getUser ? await options.getUser(request) : null;\n\n const params = routeContext?.params ? await routeContext.params : undefined;\n\n const ctx = RequestContext.create(request, user);\n if (params) {\n ctx.params = params;\n }\n\n const id = params?.id;\n if (id && handler.length > 1) {\n return await handler(ctx, id);\n }\n\n return await handler(ctx);\n } catch (error) {\n console.error('Adapter error:', error);\n return new NextResponse(JSON.stringify({ error: 'Internal Server Error' }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n };\n }\n}\n","/**\n * Domain boundary barrel: centralizes this subdomain's public contract.\n */\n\nexport { NextAdapter, type AdaptNextOptions, type NextRouteHandler } from './NextAdapter';\n"],"mappings":";;;;;;;;;;;;;;IAYa,cAAN,MAA2F;CAC9F,MACIA,SACAC,UAA4B,CAAE,GACd;AAChB,SAAO,KAAK,cAAc,SAAS,QAAQ;CAC9C;CAED,cACID,SACAC,SACgB;AAChB,SAAO,OAAOC,SAAsB,iBAAiB;AACjD,OAAI;IACA,MAAM,OAAO,QAAQ,UAAU,MAAM,QAAQ,QAAQ,QAAQ,GAAG;IAEhE,MAAM,SAAS,cAAc,SAAS,MAAM,aAAa,SAAS;IAElE,MAAM,MAAM,eAAe,OAAO,SAAS,KAAK;AAChD,QAAI,OACA,KAAI,SAAS;IAGjB,MAAM,KAAK,QAAQ;AACnB,QAAI,MAAM,QAAQ,SAAS,EACvB,QAAO,MAAM,QAAQ,KAAK,GAAG;AAGjC,WAAO,MAAM,QAAQ,IAAI;GAC5B,SAAQ,OAAO;AACZ,YAAQ,MAAM,kBAAkB,MAAM;AACtC,WAAO,IAAI,aAAa,KAAK,UAAU,EAAE,OAAO,wBAAyB,EAAC,EAAE;KACxE,QAAQ;KACR,SAAS,EAAE,gBAAgB,mBAAoB;IAClD;GACJ;EACJ;CACJ;AACJ"}