@websolutespa/bom-next 3.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 (130) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +11 -0
  3. package/dist/config.client-B8As5DY0.js +2 -0
  4. package/dist/config.edge-B8As5DY0.js +2 -0
  5. package/dist/config.server-B8As5DY0.js +2 -0
  6. package/dist/exports/client-xcFJNcG7.d.ts +979 -0
  7. package/dist/exports/client-xcFJNcG7.d.ts.map +1 -0
  8. package/dist/exports/client.js +5735 -0
  9. package/dist/exports/client.js.map +1 -0
  10. package/dist/exports/instrumentation-DBO6QH4d.d.ts +13 -0
  11. package/dist/exports/instrumentation-DBO6QH4d.d.ts.map +1 -0
  12. package/dist/exports/instrumentation.js +25 -0
  13. package/dist/exports/instrumentation.js.map +1 -0
  14. package/dist/exports/proxy-Cv7QFVci.d.ts +71 -0
  15. package/dist/exports/proxy-Cv7QFVci.d.ts.map +1 -0
  16. package/dist/exports/proxy.js +254 -0
  17. package/dist/exports/proxy.js.map +1 -0
  18. package/dist/exports/server-BHObJHJ6.d.ts +402 -0
  19. package/dist/exports/server-BHObJHJ6.d.ts.map +1 -0
  20. package/dist/exports/server.js +1974 -0
  21. package/dist/exports/server.js.map +1 -0
  22. package/dist/http-_2M-UFRx.js +61 -0
  23. package/dist/http-_2M-UFRx.js.map +1 -0
  24. package/dist/index-CdYcvudT.d.ts +4 -0
  25. package/dist/index-CtfAJ0y7.d.ts +499 -0
  26. package/dist/index-CtfAJ0y7.d.ts.map +1 -0
  27. package/dist/index.js +201 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/route-DzFwERQP.js +65 -0
  30. package/dist/route-DzFwERQP.js.map +1 -0
  31. package/dist/store-CJzqrHsl.js +73 -0
  32. package/dist/store-CJzqrHsl.js.map +1 -0
  33. package/dist/types-BatUTdbZ.d.ts +15 -0
  34. package/dist/types-BatUTdbZ.d.ts.map +1 -0
  35. package/dist/types-D29_snYY.d.ts +191 -0
  36. package/dist/types-D29_snYY.d.ts.map +1 -0
  37. package/dist/utils-Ca7xh5mm.js +78 -0
  38. package/dist/utils-Ca7xh5mm.js.map +1 -0
  39. package/dist/utils-YwteTbk7.js +23 -0
  40. package/dist/utils-YwteTbk7.js.map +1 -0
  41. package/package.json +156 -0
  42. package/src/client/client.tsx +18 -0
  43. package/src/client/extra.tsx +21 -0
  44. package/src/client/label.tsx +24 -0
  45. package/src/client/layout.tsx +22 -0
  46. package/src/client/local.collection.ts +33 -0
  47. package/src/client/metadata.tsx +217 -0
  48. package/src/client/page.tsx +22 -0
  49. package/src/client/useApi.test.tsx +203 -0
  50. package/src/client/useApi.ts +59 -0
  51. package/src/client/useCollectionApi.test.tsx +201 -0
  52. package/src/client/useCollectionApi.ts +66 -0
  53. package/src/client/useExtra.tsx +12 -0
  54. package/src/client/useHttp.ts +54 -0
  55. package/src/client/useLabel.test.tsx +76 -0
  56. package/src/client/useLabel.tsx +12 -0
  57. package/src/client/useLayout.tsx +13 -0
  58. package/src/client/useLink.tsx +13 -0
  59. package/src/client/useMetaData.tsx +12 -0
  60. package/src/client/usePage.tsx +12 -0
  61. package/src/client/useUser.tsx +12 -0
  62. package/src/client/user.tsx +43 -0
  63. package/src/exports/client.ts +20 -0
  64. package/src/exports/instrumentation.ts +1 -0
  65. package/src/exports/proxy.ts +5 -0
  66. package/src/exports/server.ts +19 -0
  67. package/src/global.d.ts +6 -0
  68. package/src/index.ts +17 -0
  69. package/src/instrumentation/client.ts +19 -0
  70. package/src/instrumentation/sentry/capture.requestError.ts +6 -0
  71. package/src/instrumentation/sentry/capture.routerTransitionStart.ts +4 -0
  72. package/src/instrumentation/sentry/config.client.ts +9 -0
  73. package/src/instrumentation/sentry/config.edge.ts +9 -0
  74. package/src/instrumentation/sentry/config.server.ts +9 -0
  75. package/src/instrumentation/server.ts +24 -0
  76. package/src/instrumentation/types.ts +12 -0
  77. package/src/lib/breadcrumb.ts +21 -0
  78. package/src/lib/const.ts +3 -0
  79. package/src/lib/http.test.ts +25 -0
  80. package/src/lib/http.ts +74 -0
  81. package/src/lib/label.ts +43 -0
  82. package/src/lib/layout.defaults.ts +167 -0
  83. package/src/lib/link.ts +19 -0
  84. package/src/lib/meta-data.ts +60 -0
  85. package/src/lib/open-graph.ts +165 -0
  86. package/src/lib/pagination.ts +78 -0
  87. package/src/lib/payload.config.ts +12 -0
  88. package/src/lib/route.ts +83 -0
  89. package/src/lib/store.ts +63 -0
  90. package/src/lib/structured-data.ts +24 -0
  91. package/src/lib/twitter-card.ts +49 -0
  92. package/src/lib/utils.ts +45 -0
  93. package/src/mock.ts +264 -0
  94. package/src/proxy/protected-route.test.ts +75 -0
  95. package/src/proxy/protected-route.ts +57 -0
  96. package/src/proxy/proxy.test.ts +48 -0
  97. package/src/proxy/proxy.ts +64 -0
  98. package/src/proxy/route-auto-detection.ts +126 -0
  99. package/src/proxy/route-proxy.ts +187 -0
  100. package/src/server/context.ts +147 -0
  101. package/src/server/handlers/auth.handler.ts +27 -0
  102. package/src/server/handlers/captions.handler.ts +32 -0
  103. package/src/server/handlers/collection.handler.test.ts +57 -0
  104. package/src/server/handlers/collection.handler.ts +212 -0
  105. package/src/server/handlers/error-response.ts +45 -0
  106. package/src/server/handlers/index.handler.ts +146 -0
  107. package/src/server/handlers/preview.handler.ts +63 -0
  108. package/src/server/handlers/registry.handler.test.ts +65 -0
  109. package/src/server/handlers/registry.handler.ts +259 -0
  110. package/src/server/handlers/route.handler.ts +161 -0
  111. package/src/server/handlers/sitemap.handler.ts +374 -0
  112. package/src/server/handlers/static-params.handler.ts +111 -0
  113. package/src/server/handlers/vars.handler.ts +167 -0
  114. package/src/server/layout.ts +120 -0
  115. package/src/server/lazy.ts +41 -0
  116. package/src/server/local-api.ts +63 -0
  117. package/src/server/metadata.ts +87 -0
  118. package/src/server/mixer-payload.ts +124 -0
  119. package/src/server/mixer-payload.type-test.ts +27 -0
  120. package/src/server/mixer.ts +439 -0
  121. package/src/server/mock/category.ts +34 -0
  122. package/src/server/mock/collection.ts +21 -0
  123. package/src/server/mock/page.ts +62 -0
  124. package/src/server/mock/route.ts +36 -0
  125. package/src/server/mock/store.ts +159 -0
  126. package/src/server/page.ts +86 -0
  127. package/src/server/session/session.ts +101 -0
  128. package/src/server/utils.ts +23 -0
  129. package/src/server/utmz.ts +11 -0
  130. package/src/types.ts +211 -0
@@ -0,0 +1,187 @@
1
+ import { defaultLocale, defaultMarket, IRoute } from '@websolutespa/bom-core';
2
+ import { NextMiddlewareResult } from 'next/dist/server/web/types';
3
+ import { NextFetchEvent, NextRequest, NextResponse } from 'next/server';
4
+ import { localApiGet } from '../lib/http';
5
+ import { getHrefBeforeRedirect, getNextUrl } from '../server/utils';
6
+ import { getRequestOrigin, Middleware, MiddlewareOptions } from './proxy';
7
+
8
+ export type Proxy = (request: NextRequest, next: NextFetchEvent) => Promise<NextMiddlewareResult>;
9
+
10
+ export type RouteMiddlewareOptions = {
11
+ request: NextRequest,
12
+ next: NextFetchEvent,
13
+ route: IRoute,
14
+ disabled?: boolean,
15
+ };
16
+ export type RouteMiddleware = (props: RouteMiddlewareOptions) => NextMiddlewareResult | Promise<NextMiddlewareResult>;
17
+
18
+ export type RouteInterceptorProxy = (route: IRoute, request: NextRequest, next: NextFetchEvent) => Promise<NextMiddlewareResult>;
19
+
20
+ export type ResolvedRoute = IRoute & { splat?: string };
21
+ export type RouteResolverResponse = {
22
+ parent?: IRoute;
23
+ route?: ResolvedRoute;
24
+ };
25
+
26
+ /**
27
+ * Resolves a public request to a CMS route, then rewrites it to the internal
28
+ * Next.js page while preserving the original browser URL.
29
+ *
30
+ * The resolver call is deliberately made to this request's public origin so it
31
+ * works behind reverse proxies and deployment hosts. A rewrite keeps routing
32
+ * server-side, avoiding an extra browser round trip that a redirect would add.
33
+ */
34
+ export function createRouteProxy(...middlewares: RouteMiddleware[]): Middleware {
35
+ return async function ({ request, next }: MiddlewareOptions) {
36
+ const url = request.nextUrl;
37
+ const origin = getRequestOrigin(request);
38
+ const hrefBeforeRedirect = getHrefBeforeRedirect(request);
39
+ const { pathname } = url;
40
+ const headers = new Headers(request.headers);
41
+ headers.set('x-pathname', pathname);
42
+ try {
43
+ const draftMode = request.cookies.has('__prerender_bypass');
44
+ console.log('draftMode', draftMode);
45
+ if (draftMode) {
46
+ // Draft requests need Payload's native preview behavior, so skip the
47
+ // route lookup and its database/cache work entirely.
48
+ return;
49
+ }
50
+ // Resolve once before invoking route middleware. This shares the route
51
+ // snapshot result across all middleware rather than letting each one
52
+ // repeat the internal API request.
53
+ const resolution = await localApiGet<RouteResolverResponse>(
54
+ `/-route?pathname=${url.pathname}&href=${url.href}&hrefBeforeRedirect=${hrefBeforeRedirect}`,
55
+ {},
56
+ undefined,
57
+ origin
58
+ );
59
+ const route = resolution?.route;
60
+ // console.log('createRouteProxy', route);
61
+ if (route) {
62
+ let middlewareResponse, i = 0;
63
+ // Run in registration order and stop at the first response. Later
64
+ // middleware cannot affect a committed rewrite, so short-circuiting
65
+ // avoids unnecessary authentication or policy checks.
66
+ while (i < middlewares.length && !middlewareResponse) {
67
+ const middleware = middlewares[i] as RouteMiddleware;
68
+ // console.log('createRouteProxy.middlewares', middlewares.length, typeof middleware === 'function');
69
+ if (typeof middleware === 'function') {
70
+ middlewareResponse = await middleware({ request, next, route });
71
+ if (middlewareResponse) {
72
+ return middlewareResponse;
73
+ }
74
+ }
75
+ i++;
76
+ }
77
+ const nextUrl = getNextUrl(request, route);
78
+ console.log('createRouteProxy.Found', pathname, '>', nextUrl.pathname);
79
+ // Pass the resolved route as request headers so the internal page can
80
+ // render without performing a second route-resolution lookup.
81
+ headers.set('x-market', route.market);
82
+ headers.set('x-locale', route.locale);
83
+ headers.set('x-schema', route.schema);
84
+ headers.set('x-id', String(route.page));
85
+ headers.set('x-forwarded-pathname', nextUrl.pathname);
86
+ const response = NextResponse.rewrite(nextUrl, {
87
+ request: {
88
+ headers,
89
+ },
90
+ });
91
+ // const response = NextResponse.redirect(url);
92
+ return response;
93
+ } else {
94
+ const parentRoute = resolution?.parent;
95
+ // The resolver returns the nearest parent even when no leaf route
96
+ // matches. Reusing its market and locale makes the 404 contextual
97
+ // without another lookup; defaults cover an entirely unknown path.
98
+ const market = parentRoute?.market || defaultMarket;
99
+ const locale = parentRoute?.locale || defaultLocale;
100
+ const nextUrl = request.nextUrl.clone();
101
+ nextUrl.pathname = `/${market}/${locale}/404`;
102
+ headers.set('x-market', market);
103
+ headers.set('x-locale', locale);
104
+ headers.set('x-schema', 'not_found');
105
+ headers.set('x-id', '404');
106
+ headers.set('x-forwarded-pathname', nextUrl.pathname);
107
+ console.log('createRouteProxy.NotFound', pathname);
108
+ const response = NextResponse
109
+ .rewrite(nextUrl, {
110
+ request: {
111
+ headers,
112
+ },
113
+ });
114
+ return response;
115
+ }
116
+ } catch (error: unknown) {
117
+ const routeError = error && typeof error === 'object'
118
+ ? error as { status?: number; statusText?: string; url?: string }
119
+ : {};
120
+
121
+ if (routeError.status && routeError.status >= 300 && routeError.status < 400) {
122
+ console.log('createRouteProxy.3xx', url.pathname, routeError.status, routeError.statusText);
123
+ const nextUrl = request.nextUrl.clone();
124
+ // The handler has already validated the redirect target. Redirect the
125
+ // browser once so its address bar changes, rather than rewriting a
126
+ // route whose canonical URL is different.
127
+ nextUrl.pathname = '/';
128
+ headers.set('x-forwarded-pathname', nextUrl.pathname);
129
+ const response = NextResponse.redirect(nextUrl, 307);
130
+ return response;
131
+ } else if (routeError.status && routeError.status >= 400 && routeError.status < 500) {
132
+ if (routeError.status === 404) {
133
+ /**
134
+ * A resolver miss is not necessarily a CMS 404: it may be a Next.js
135
+ * file-system route such as an API endpoint or static asset. Letting
136
+ * Next continue avoids routing those requests through a fallback
137
+ * render and keeps their fast native handling intact.
138
+ */
139
+ // console.log('createRouteProxy.route.notfound', url.pathname);
140
+ return;
141
+ }
142
+ console.log('createRouteProxy.4xx', url.pathname, routeError.status, routeError.statusText);
143
+ // Other client errors are terminal CMS outcomes, so render the shared
144
+ // 404 page directly instead of issuing a separate client navigation.
145
+ const market = defaultMarket;
146
+ const locale = defaultLocale;
147
+ const nextUrl = request.nextUrl.clone();
148
+ nextUrl.pathname = `/${market}/${locale}/404`;
149
+ headers.set('x-market', market);
150
+ headers.set('x-locale', locale);
151
+ headers.set('x-schema', 'not_found');
152
+ headers.set('x-id', '404');
153
+ headers.set('x-forwarded-pathname', nextUrl.pathname);
154
+ console.log('createRouteProxy.NotFound', pathname);
155
+ const response = NextResponse
156
+ .rewrite(nextUrl, {
157
+ request: {
158
+ headers,
159
+ },
160
+ });
161
+ return response;
162
+ /*
163
+ const response = NextResponse.next({
164
+ status: routeError.status,
165
+ statusText: routeError.statusText,
166
+ headers,
167
+ });
168
+ return response;
169
+ */
170
+ }
171
+ // A resolver failure must not block unrelated Next.js routes. Returning
172
+ // undefined delegates to the normal pipeline and avoids masking a
173
+ // transient upstream failure with a misleading synthetic response.
174
+ console.log('createRouteProxy.error.5xx', url.pathname, routeError.url, routeError.status, routeError.statusText || error);
175
+ return;
176
+ }
177
+ };
178
+ }
179
+
180
+ /*
181
+
182
+ export const routeInterceptor: Proxy = async function (request: NextRequest, next: NextFetchEvent) {
183
+ const middleware = await createRouteProxy();
184
+ return await middleware(request, next);
185
+ };
186
+
187
+ */
@@ -0,0 +1,147 @@
1
+ 'use server';
2
+
3
+ import { headers as getHeaders } from 'next/headers';
4
+ import type { CollectionSlug, SanitizedConfig } from 'payload';
5
+ import { cache } from 'react';
6
+ import { getLabelFromLabels } from '../lib/label';
7
+ import { getLink } from '../lib/link';
8
+ import type { IPageContext, TypedPageContext } from '../types';
9
+ import { getLayout } from './layout';
10
+ import { getPayload } from './mixer';
11
+ import { getPage } from './page';
12
+
13
+ /**
14
+ * Per-request context store (React.cache ensures a fresh Map per RSC render).
15
+ * Safe to use inside Next.js server components — do NOT call this at build time.
16
+ */
17
+ const PageContext = cache(() => {
18
+ return new Map<string, any>();
19
+ });
20
+
21
+ export async function getPageContext<TSlug extends CollectionSlug = CollectionSlug>(): Promise<TypedPageContext<TSlug>> {
22
+ return {
23
+ layout: PageContext().get('layout'),
24
+ page: PageContext().get('page'),
25
+ user: PageContext().get('user') || null,
26
+ label: PageContext().get('label'),
27
+ link: PageContext().get('link'),
28
+ } as TypedPageContext<TSlug>;
29
+ }
30
+
31
+ export async function setPageContext<TSlug extends CollectionSlug>(data: TypedPageContext<TSlug>) {
32
+ const context = PageContext();
33
+ for (const key in data) {
34
+ context.set(key, data[key as keyof IPageContext]);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Same as createPageContext but without draft mode — used for error/not-found pages
40
+ * that should always show published content regardless of draft mode state.
41
+ */
42
+ export async function createNotFoundContext<TSlug extends CollectionSlug>(
43
+ market: string,
44
+ locale: string,
45
+ slug: TSlug,
46
+ id: string,
47
+ config: Promise<SanitizedConfig>,
48
+ headers?: Awaited<ReturnType<typeof getHeaders>>
49
+ ): Promise<TypedPageContext<TSlug>> {
50
+ const { payload, user, getContext } = await getPayload({ config, headers });
51
+ const context = await getContext(market, locale);
52
+
53
+ const layout = await getLayout(market, locale, payload, user, context);
54
+ const page = await getPage(slug, id, market, locale, payload, user, context);
55
+
56
+ const label = getLabelFromLabels(layout.labels);
57
+ const link = (category: string) => getLink(category, layout);
58
+
59
+ const pageContext = {
60
+ layout,
61
+ page,
62
+ user,
63
+ label,
64
+ link,
65
+ } as TypedPageContext<TSlug>;
66
+
67
+ setPageContext(pageContext);
68
+
69
+ return pageContext;
70
+ }
71
+
72
+ /**
73
+ * Initialises the full page context using Payload local API.
74
+ *
75
+ * Replaces the @websolutespa/bom-next/server createPageContext that goes through the store
76
+ * abstraction. All data is fetched directly from Payload; the mixer context
77
+ * (routes + categories) is passed to every payload.find call so that
78
+ * payload-plugin-bowl can enrich responses.
79
+ */
80
+ export async function createPageContext<TSlug extends CollectionSlug>(
81
+ market: string,
82
+ locale: string,
83
+ slug: TSlug,
84
+ id: string,
85
+ config: Promise<SanitizedConfig>,
86
+ headers?: Awaited<ReturnType<typeof getHeaders>>
87
+ ): Promise<TypedPageContext<TSlug>> {
88
+
89
+ // const draft = await draftMode();
90
+ const draft = { isEnabled: false };
91
+
92
+ const { payload, user, getContext } = await getPayload({ config, headers });
93
+ const context = await getContext(market, locale);
94
+
95
+ const layout = await getLayout(market, locale, payload, user, context, draft.isEnabled);
96
+ const page = await getPage(slug, id, market, locale, payload, user, context, draft.isEnabled);
97
+
98
+ const label = getLabelFromLabels(layout.labels);
99
+ const link = (category: string) => getLink(category, layout);
100
+
101
+ const pageContext = {
102
+ layout,
103
+ page,
104
+ user,
105
+ label,
106
+ link,
107
+ } as TypedPageContext<TSlug>;
108
+
109
+ setPageContext(pageContext);
110
+
111
+ /*
112
+ if (draft.isEnabled) {
113
+ draft.disable();
114
+ }
115
+ */
116
+
117
+ return pageContext;
118
+ }
119
+
120
+ /**
121
+ * Layout-only context — no page fetch. Used by RootLayout server components.
122
+ */
123
+ export async function createLayoutContext(
124
+ market: string,
125
+ locale: string,
126
+ config: Promise<SanitizedConfig>,
127
+ headers?: Awaited<ReturnType<typeof getHeaders>>
128
+ ) {
129
+ const { payload, user, getContext } = await getPayload({ config, headers });
130
+ const context = await getContext(market, locale);
131
+
132
+ const layout = await getLayout(market, locale, payload, user, context);
133
+
134
+ const label = getLabelFromLabels(layout.labels);
135
+ const link = (category: string) => getLink(category, layout);
136
+
137
+ const pageContext = {
138
+ layout,
139
+ user,
140
+ label,
141
+ link,
142
+ } as unknown as IPageContext;
143
+
144
+ setPageContext(pageContext);
145
+
146
+ return pageContext;
147
+ }
@@ -0,0 +1,27 @@
1
+ import { headers as getHeaders } from 'next/headers';
2
+ import { NextRequest } from 'next/server';
3
+ import { SanitizedConfig } from 'payload';
4
+ import { getPayload } from '../mixer';
5
+ import { getHandlerErrorResponse } from './error-response';
6
+
7
+ export function getAuthMeHandler(config: Promise<SanitizedConfig>) {
8
+ return async function (request: NextRequest) {
9
+ try {
10
+ const headers = await getHeaders();
11
+ const { user } = await getPayload({ config, headers });
12
+ const roles = (user?.roles || []);
13
+ if (roles.includes('user') && 'firstName' in user) {
14
+ return Response.json({
15
+ id: user.id,
16
+ email: user.email,
17
+ firstName: user.firstName,
18
+ lastName: user.lastName,
19
+ }, { status: 200 });
20
+ }
21
+ return Response.json(null, { status: 200 });
22
+ } catch (error) {
23
+ console.log('getAuthMeHandler.error', error);
24
+ return getHandlerErrorResponse(error);
25
+ }
26
+ };
27
+ }
@@ -0,0 +1,32 @@
1
+ import { NextRequest } from 'next/server';
2
+ import { SanitizedConfig } from 'payload';
3
+ import { getHandlerErrorResponse } from './error-response';
4
+
5
+ export function getCaptionsVTT(config: Promise<SanitizedConfig>) {
6
+ return async function (request: NextRequest) {
7
+ try {
8
+ const searchParams = request.nextUrl.searchParams;
9
+ const title = searchParams.get('title') || 'Untitled';
10
+ const body = /* html */`WEBVTT
11
+
12
+ 00:00:01.000 --> 00:00:03.000
13
+ - This video has no audio.
14
+
15
+ ${title ? `
16
+ 00:00:04.000 --> 00:00:06.000
17
+ - The title of the video is "${title}".
18
+ ` : ''}`;
19
+ return new Response(body, {
20
+ status: 200,
21
+ headers: {
22
+ 'Content-Type': 'text/plain; charset=UTF-8',
23
+ 'X-Frame-Options': 'SAMEORIGIN',
24
+ 'X-Robots-Tag': 'noindex, follow',
25
+ },
26
+ });
27
+ } catch (error: unknown) {
28
+ console.error('getCaptionsVTT.error', error);
29
+ return getHandlerErrorResponse(error);
30
+ }
31
+ };
32
+ }
@@ -0,0 +1,57 @@
1
+ // @vitest-environment node
2
+
3
+ import { describe, expect, test } from 'vitest';
4
+ import { getHandlerErrorResponse } from './error-response';
5
+
6
+ describe('getHandlerErrorResponse', () => {
7
+ test('preserves a Payload authorization error as a 403 response', async () => {
8
+ const response = getHandlerErrorResponse({
9
+ isPublic: true,
10
+ message: 'You are not allowed to perform this action.',
11
+ status: 403,
12
+ });
13
+
14
+ expect(response.status).toBe(403);
15
+ await expect(response.json()).resolves.toEqual({
16
+ errors: [{
17
+ message: 'You are not allowed to perform this action.',
18
+ }],
19
+ });
20
+ });
21
+
22
+ test('preserves public Payload validation data', async () => {
23
+ const response = getHandlerErrorResponse({
24
+ data: {
25
+ collection: 'page',
26
+ errors: [{ message: 'Title is required', path: 'title' }],
27
+ },
28
+ isPublic: true,
29
+ message: 'The following field is invalid: Title',
30
+ name: 'ValidationError',
31
+ status: 400,
32
+ });
33
+
34
+ expect(response.status).toBe(400);
35
+ await expect(response.json()).resolves.toEqual({
36
+ errors: [{
37
+ data: {
38
+ collection: 'page',
39
+ errors: [{ message: 'Title is required', path: 'title' }],
40
+ },
41
+ message: 'The following field is invalid: Title',
42
+ name: 'ValidationError',
43
+ }],
44
+ });
45
+ });
46
+
47
+ test('hides non-public errors behind a 500 response', async () => {
48
+ const response = getHandlerErrorResponse(new Error('database unavailable'));
49
+
50
+ expect(response.status).toBe(500);
51
+ await expect(response.json()).resolves.toEqual({
52
+ errors: [{
53
+ message: 'Something went wrong.',
54
+ }],
55
+ });
56
+ });
57
+ });
@@ -0,0 +1,212 @@
1
+
2
+ import { qsDeserialize } from '@websolutespa/bom-core';
3
+ import { headers as getHeaders } from 'next/headers';
4
+ import { CollectionSlug, GlobalSlug, SanitizedConfig } from 'payload';
5
+ import { getLocalApi } from '../local-api';
6
+ import { getPayload } from '../mixer';
7
+ import { getHandlerErrorResponse } from './error-response';
8
+
9
+ // find a specific document by id or
10
+ // find paginated documents
11
+ export function collectionGetHandler(config: Promise<SanitizedConfig>) {
12
+ return async function (request: Request, {
13
+ params: getParams,
14
+ }: {
15
+ params: Promise<{ collections: string[]; }>;
16
+ }) {
17
+ try {
18
+ const headers = await getHeaders();
19
+ const { payload, user, getContext } = await getPayload({ config, headers });
20
+ const collectionSlugs = payload.config.collections.map(x => x.slug);
21
+ const globalSlugs = payload.config.globals.map(x => x.slug);
22
+ const keys = [
23
+ ...collectionSlugs,
24
+ ...globalSlugs,
25
+ ];
26
+ keys.sort();
27
+ const { collections } = await getParams;
28
+ if (!Array.isArray(collections) || collections?.length === 0) {
29
+ console.error('storeHandler.collectionGet', 'method not allowed', collections);
30
+ return new Response('method not allowed', {
31
+ status: 405,
32
+ statusText: 'method not allowed',
33
+ });
34
+ }
35
+ const slug = collections[0] as CollectionSlug | GlobalSlug;
36
+ const id: string = collections[1];
37
+ const search = new URL(request.url).search;
38
+ const params = qsDeserialize(search);
39
+ const context = params.market && params.locale ? await getContext(params.market, params.locale) : undefined;
40
+ if (!keys.includes(slug)) {
41
+ console.error('storeHandler.collectionGet', 'keys', slug);
42
+ console.error('storeHandler.collectionGet', 'method not allowed', slug);
43
+ return new Response('method not allowed', {
44
+ status: 405,
45
+ statusText: 'method not allowed',
46
+ });
47
+ }
48
+ const localApi = await getLocalApi(config, slug);
49
+ if (id) {
50
+ // find by id
51
+ const item = await localApi.findOne({
52
+ ...params,
53
+ id,
54
+ context,
55
+ user,
56
+ });
57
+ if (item) {
58
+ return Response.json(item);
59
+ } else {
60
+ return new Response('not found', {
61
+ status: 404,
62
+ statusText: 'not found',
63
+ });
64
+ }
65
+ } else if (String(params.pagination) === 'true') {
66
+ // find paginated documents
67
+ const items = await localApi.findPaged({
68
+ ...params,
69
+ context,
70
+ user,
71
+ });
72
+ return Response.json(items);
73
+ } else {
74
+ // find many
75
+ const items = await localApi.findMany({
76
+ ...params,
77
+ context,
78
+ user,
79
+ });
80
+ return Response.json(items);
81
+ }
82
+ } catch (error: unknown) {
83
+ const response = getHandlerErrorResponse(error);
84
+ console.error('storeHandler.collectionGet.error', response.status, error);
85
+ return response;
86
+ }
87
+ };
88
+ }
89
+
90
+ // create a new document + post route
91
+ export function collectionPostHandler(config: Promise<SanitizedConfig>) {
92
+ return async function (request: Request, {
93
+ params: getParams,
94
+ }: {
95
+ params: Promise<{ collections: string[]; }>;
96
+ }) {
97
+ try {
98
+ const { payload } = await getPayload({ config });
99
+ const keys = [
100
+ ...payload.config.collections.map(x => x.slug),
101
+ ...payload.config.globals.map(x => x.slug),
102
+ ];
103
+ keys.sort();
104
+ const { collections } = await getParams;
105
+ if (!Array.isArray(collections) || collections.length !== 1) {
106
+ console.error('method not allowed', collections);
107
+ return new Response('method not allowed', {
108
+ status: 405,
109
+ statusText: 'method not allowed',
110
+ });
111
+ }
112
+ const slug = collections[0] as CollectionSlug | GlobalSlug;
113
+ if (!keys.includes(slug)) {
114
+ console.error('storeHandler.collectionGet', 'method not allowed', slug);
115
+ return new Response('method not allowed', {
116
+ status: 405,
117
+ statusText: 'method not allowed',
118
+ });
119
+ }
120
+ // api post route pathname
121
+ /*
122
+ if (slug === 'route') {
123
+ return routePost(store, request);
124
+ }
125
+ */
126
+ // !!! todo post create
127
+ } catch (error: unknown) {
128
+ const response = getHandlerErrorResponse(error);
129
+ console.error('storeHandler.collectionPost.error', response.status, error);
130
+ return response;
131
+ }
132
+ };
133
+ }
134
+
135
+ // update a document by id
136
+ export function collectionPatchHandler(config: Promise<SanitizedConfig>) {
137
+ return async function (request: Request, {
138
+ params: getParams,
139
+ }: {
140
+ params: Promise<{ collections: string[]; }>;
141
+ }) {
142
+ try {
143
+ const { payload } = await getPayload({ config });
144
+ const keys = [
145
+ ...payload.config.collections.map(x => x.slug),
146
+ ...payload.config.globals.map(x => x.slug),
147
+ ];
148
+ keys.sort();
149
+ const { collections } = await getParams;
150
+ if (!Array.isArray(collections) || collections.length !== 2) {
151
+ console.error('method not allowed', collections);
152
+ return new Response('method not allowed', {
153
+ status: 405,
154
+ statusText: 'method not allowed',
155
+ });
156
+ }
157
+ const slug = collections[0] as CollectionSlug | GlobalSlug;
158
+ if (!keys.includes(slug)) {
159
+ console.error('storeHandler.collectionGet', 'method not allowed', slug);
160
+ return new Response('method not allowed', {
161
+ status: 405,
162
+ statusText: 'method not allowed',
163
+ });
164
+ }
165
+ // !!! todo patch update
166
+ } catch (error: unknown) {
167
+ const response = getHandlerErrorResponse(error);
168
+ console.error('storeHandler.collectionPatch.error', response.status, error);
169
+ return response;
170
+ }
171
+ };
172
+ }
173
+
174
+ // delete an existing document by id
175
+ export function collectionDeleteHandler(config: Promise<SanitizedConfig>) {
176
+ return async function (request: Request, {
177
+ params: getParams,
178
+ }: {
179
+ params: Promise<{ collections: string[]; }>;
180
+ }) {
181
+ try {
182
+ const { payload } = await getPayload({ config });
183
+ const keys = [
184
+ ...payload.config.collections.map(x => x.slug),
185
+ ...payload.config.globals.map(x => x.slug),
186
+ ];
187
+ keys.sort();
188
+ const { collections } = await getParams;
189
+ if (!Array.isArray(collections) || collections.length !== 2) {
190
+ console.error('method not allowed', collections);
191
+ return new Response('method not allowed', {
192
+ status: 405,
193
+ statusText: 'method not allowed',
194
+ });
195
+ }
196
+ const slug = collections[0] as CollectionSlug | GlobalSlug;
197
+ if (!keys.includes(slug)) {
198
+ console.error('storeHandler.collectionGet', 'method not allowed', slug);
199
+ return new Response('method not allowed', {
200
+ status: 405,
201
+ statusText: 'method not allowed',
202
+ });
203
+ }
204
+ const id: string = collections[1];
205
+ // !!! todo delete
206
+ } catch (error: unknown) {
207
+ const response = getHandlerErrorResponse(error);
208
+ console.error('storeHandler.collectionDelete.error', response.status, error);
209
+ return response;
210
+ }
211
+ };
212
+ }