@websolutespa/bom-next 3.0.2 → 4.0.0-next.1
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/CHANGELOG.md +37 -7
- package/README.md +28 -0
- package/dist/_virtual/{_rolldown/runtime.js → rolldown_runtime.js} +7 -4
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +17 -6
- package/dist/client/client.js.map +1 -1
- package/dist/client/extra.d.ts +3 -3
- package/dist/client/extra.d.ts.map +1 -1
- package/dist/client/extra.js +10 -5
- package/dist/client/extra.js.map +1 -1
- package/dist/client/label.d.ts +3 -3
- package/dist/client/label.d.ts.map +1 -1
- package/dist/client/label.js +10 -5
- package/dist/client/label.js.map +1 -1
- package/dist/client/layout.d.ts +3 -3
- package/dist/client/layout.d.ts.map +1 -1
- package/dist/client/layout.js +10 -5
- package/dist/client/layout.js.map +1 -1
- package/dist/client/local.collection.d.ts.map +1 -1
- package/dist/client/local.collection.js +4 -2
- package/dist/client/local.collection.js.map +1 -1
- package/dist/client/metadata.d.ts +3 -3
- package/dist/client/metadata.d.ts.map +1 -1
- package/dist/client/metadata.js +48 -40
- package/dist/client/metadata.js.map +1 -1
- package/dist/client/page.d.ts +3 -3
- package/dist/client/page.d.ts.map +1 -1
- package/dist/client/page.js +10 -5
- package/dist/client/page.js.map +1 -1
- package/dist/client/useApi.d.ts +7 -7
- package/dist/client/useApi.d.ts.map +1 -1
- package/dist/client/useApi.js +2 -1
- package/dist/client/useApi.js.map +1 -1
- package/dist/client/useCollectionApi.d.ts +6 -6
- package/dist/client/useCollectionApi.d.ts.map +1 -1
- package/dist/client/useCollectionApi.js +4 -2
- package/dist/client/useCollectionApi.js.map +1 -1
- package/dist/client/useExtra.d.ts.map +1 -1
- package/dist/client/useExtra.js +5 -2
- package/dist/client/useExtra.js.map +1 -1
- package/dist/client/useHttp.d.ts.map +1 -1
- package/dist/client/useHttp.js +9 -6
- package/dist/client/useHttp.js.map +1 -1
- package/dist/client/useLabel.d.ts.map +1 -1
- package/dist/client/useLabel.js +5 -2
- package/dist/client/useLabel.js.map +1 -1
- package/dist/client/useLayout.d.ts.map +1 -1
- package/dist/client/useLayout.js +5 -2
- package/dist/client/useLayout.js.map +1 -1
- package/dist/client/useLink.d.ts.map +1 -1
- package/dist/client/useLink.js +7 -3
- package/dist/client/useLink.js.map +1 -1
- package/dist/client/useMetaData.d.ts.map +1 -1
- package/dist/client/useMetaData.js +5 -2
- package/dist/client/useMetaData.js.map +1 -1
- package/dist/client/usePage.d.ts.map +1 -1
- package/dist/client/usePage.js +5 -2
- package/dist/client/usePage.js.map +1 -1
- package/dist/client/useUser.d.ts.map +1 -1
- package/dist/client/useUser.js +5 -2
- package/dist/client/useUser.js.map +1 -1
- package/dist/client/user.d.ts.map +1 -1
- package/dist/client/user.js +17 -11
- package/dist/client/user.js.map +1 -1
- package/dist/exports/client.js +2 -1
- package/dist/exports/instrumentation.js +2 -1
- package/dist/exports/proxy.js +2 -1
- package/dist/exports/server.d.ts +5 -4
- package/dist/exports/server.js +7 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -2
- package/dist/instrumentation/client.d.ts.map +1 -1
- package/dist/instrumentation/client.js +2 -1
- package/dist/instrumentation/client.js.map +1 -1
- package/dist/instrumentation/sentry/capture.requestError.js +4 -1
- package/dist/instrumentation/sentry/capture.requestError.js.map +1 -1
- package/dist/instrumentation/sentry/capture.routerTransitionStart.js +2 -1
- package/dist/instrumentation/sentry/capture.routerTransitionStart.js.map +1 -1
- package/dist/instrumentation/sentry/config.client.js +10 -2
- package/dist/instrumentation/sentry/config.client.js.map +1 -0
- package/dist/instrumentation/sentry/config.edge.js +10 -2
- package/dist/instrumentation/sentry/config.edge.js.map +1 -0
- package/dist/instrumentation/sentry/config.server.js +10 -2
- package/dist/instrumentation/sentry/config.server.js.map +1 -0
- package/dist/instrumentation/server.d.ts.map +1 -1
- package/dist/instrumentation/server.js +8 -5
- package/dist/instrumentation/server.js.map +1 -1
- package/dist/instrumentation/types.d.ts.map +1 -1
- package/dist/lib/breadcrumb.d.ts.map +1 -1
- package/dist/lib/breadcrumb.js +6 -1
- package/dist/lib/breadcrumb.js.map +1 -1
- package/dist/lib/const.d.ts +14 -2
- package/dist/lib/const.d.ts.map +1 -1
- package/dist/lib/const.js +24 -2
- package/dist/lib/const.js.map +1 -1
- package/dist/lib/http.d.ts +9 -0
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +27 -4
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/label.d.ts.map +1 -1
- package/dist/lib/label.js +4 -3
- package/dist/lib/label.js.map +1 -1
- package/dist/lib/layout.defaults.d.ts.map +1 -1
- package/dist/lib/layout.defaults.js +1 -1
- package/dist/lib/layout.defaults.js.map +1 -1
- package/dist/lib/link.d.ts.map +1 -1
- package/dist/lib/link.js +2 -1
- package/dist/lib/link.js.map +1 -1
- package/dist/lib/meta-data.d.ts.map +1 -1
- package/dist/lib/open-graph.d.ts.map +1 -1
- package/dist/lib/pagination.d.ts.map +1 -1
- package/dist/lib/pagination.js +4 -2
- package/dist/lib/pagination.js.map +1 -1
- package/dist/lib/route.d.ts.map +1 -1
- package/dist/lib/route.js +9 -3
- package/dist/lib/route.js.map +1 -1
- package/dist/lib/store.d.ts.map +1 -1
- package/dist/lib/store.js +31 -31
- package/dist/lib/store.js.map +1 -1
- package/dist/lib/structured-data.d.ts.map +1 -1
- package/dist/lib/twitter-card.d.ts.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +3 -2
- package/dist/lib/utils.js.map +1 -1
- package/dist/mock.d.ts.map +1 -1
- package/dist/node_modules/@selderee/plugin-htmlparser2/lib/hp2-builder.js +3 -2
- package/dist/node_modules/@selderee/plugin-htmlparser2/lib/hp2-builder.js.map +1 -1
- package/dist/node_modules/deepmerge/dist/cjs.js +17 -9
- package/dist/node_modules/deepmerge/dist/cjs.js.map +1 -1
- package/dist/node_modules/dom-serializer/lib/esm/foreignNames.js +2 -2
- package/dist/node_modules/dom-serializer/lib/esm/foreignNames.js.map +1 -1
- package/dist/node_modules/dom-serializer/lib/esm/index.js +10 -15
- package/dist/node_modules/dom-serializer/lib/esm/index.js.map +1 -1
- package/dist/node_modules/domelementtype/lib/esm/index.js +12 -12
- package/dist/node_modules/domelementtype/lib/esm/index.js.map +1 -1
- package/dist/node_modules/domhandler/lib/esm/index.js +4 -3
- package/dist/node_modules/domhandler/lib/esm/index.js.map +1 -1
- package/dist/node_modules/domhandler/lib/esm/node.js +8 -7
- package/dist/node_modules/domhandler/lib/esm/node.js.map +1 -1
- package/dist/node_modules/entities/lib/esm/decode.js +33 -32
- package/dist/node_modules/entities/lib/esm/decode.js.map +1 -1
- package/dist/node_modules/entities/lib/esm/decode_codepoint.js +5 -5
- package/dist/node_modules/entities/lib/esm/decode_codepoint.js.map +1 -1
- package/dist/node_modules/entities/lib/esm/escape.js +14 -6
- package/dist/node_modules/entities/lib/esm/escape.js.map +1 -1
- package/dist/node_modules/entities/lib/esm/generated/decode-data-html.js +2 -2
- package/dist/node_modules/entities/lib/esm/generated/decode-data-html.js.map +1 -1
- package/dist/node_modules/entities/lib/esm/generated/decode-data-xml.js +2 -2
- package/dist/node_modules/entities/lib/esm/generated/decode-data-xml.js.map +1 -1
- package/dist/node_modules/html-to-text/lib/html-to-text.js +45 -33
- package/dist/node_modules/html-to-text/lib/html-to-text.js.map +1 -1
- package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/Parser.js +25 -25
- package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/Parser.js.map +1 -1
- package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/Tokenizer.js +76 -73
- package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/Tokenizer.js.map +1 -1
- package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/index.js +4 -3
- package/dist/node_modules/html-to-text/node_modules/htmlparser2/lib/esm/index.js.map +1 -1
- package/dist/node_modules/leac/lib/leac.js +72 -72
- package/dist/node_modules/leac/lib/leac.js.map +1 -1
- package/dist/node_modules/parseley/lib/parseley.js +37 -33
- package/dist/node_modules/parseley/lib/parseley.js.map +1 -1
- package/dist/node_modules/peberminta/lib/core.js +4 -4
- package/dist/node_modules/peberminta/lib/core.js.map +1 -1
- package/dist/node_modules/selderee/lib/selderee.js +16 -10
- package/dist/node_modules/selderee/lib/selderee.js.map +1 -1
- package/dist/proxy/protected-route.d.ts.map +1 -1
- package/dist/proxy/protected-route.js +11 -6
- package/dist/proxy/protected-route.js.map +1 -1
- package/dist/proxy/proxy.d.ts +5 -4
- package/dist/proxy/proxy.d.ts.map +1 -1
- package/dist/proxy/proxy.js +26 -6
- package/dist/proxy/proxy.js.map +1 -1
- package/dist/proxy/route-auto-detection.d.ts.map +1 -1
- package/dist/proxy/route-auto-detection.js +26 -14
- package/dist/proxy/route-auto-detection.js.map +1 -1
- package/dist/proxy/route-proxy.d.ts.map +1 -1
- package/dist/proxy/route-proxy.js +22 -5
- package/dist/proxy/route-proxy.js.map +1 -1
- package/dist/server/context.d.ts +31 -15
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +156 -69
- package/dist/server/context.js.map +1 -1
- package/dist/server/handlers/auth.handler.d.ts.map +1 -1
- package/dist/server/handlers/auth.handler.js +4 -2
- package/dist/server/handlers/auth.handler.js.map +1 -1
- package/dist/server/handlers/captions.handler.d.ts.map +1 -1
- package/dist/server/handlers/captions.handler.js +4 -2
- package/dist/server/handlers/captions.handler.js.map +1 -1
- package/dist/server/handlers/collection.handler.d.ts.map +1 -1
- package/dist/server/handlers/collection.handler.js +2 -1
- package/dist/server/handlers/collection.handler.js.map +1 -1
- package/dist/server/handlers/error-response.js +6 -2
- package/dist/server/handlers/error-response.js.map +1 -1
- package/dist/server/handlers/index.handler.d.ts.map +1 -1
- package/dist/server/handlers/index.handler.js +2 -1
- package/dist/server/handlers/index.handler.js.map +1 -1
- package/dist/server/handlers/preview.handler.d.ts.map +1 -1
- package/dist/server/handlers/preview.handler.js +6 -3
- package/dist/server/handlers/preview.handler.js.map +1 -1
- package/dist/server/handlers/registry.handler.d.ts.map +1 -1
- package/dist/server/handlers/registry.handler.js +14 -9
- package/dist/server/handlers/registry.handler.js.map +1 -1
- package/dist/server/handlers/route.handler.d.ts.map +1 -1
- package/dist/server/handlers/route.handler.js +12 -3
- package/dist/server/handlers/route.handler.js.map +1 -1
- package/dist/server/handlers/sitemap.handler.d.ts.map +1 -1
- package/dist/server/handlers/sitemap.handler.js +12 -9
- package/dist/server/handlers/sitemap.handler.js.map +1 -1
- package/dist/server/handlers/static-params.handler.d.ts.map +1 -1
- package/dist/server/handlers/static-params.handler.js +13 -8
- package/dist/server/handlers/static-params.handler.js.map +1 -1
- package/dist/server/handlers/vars.handler.d.ts.map +1 -1
- package/dist/server/handlers/vars.handler.js +5 -4
- package/dist/server/handlers/vars.handler.js.map +1 -1
- package/dist/server/helpers.d.ts +50 -0
- package/dist/server/helpers.d.ts.map +1 -0
- package/dist/server/helpers.js +60 -0
- package/dist/server/helpers.js.map +1 -0
- package/dist/server/layout.js +69 -34
- package/dist/server/layout.js.map +1 -1
- package/dist/server/lazy.d.ts +1 -0
- package/dist/server/lazy.d.ts.map +1 -1
- package/dist/server/lazy.js +3 -2
- package/dist/server/lazy.js.map +1 -1
- package/dist/server/local-api.d.ts.map +1 -1
- package/dist/server/local-api.js +12 -6
- package/dist/server/local-api.js.map +1 -1
- package/dist/server/metadata.d.ts +8 -5
- package/dist/server/metadata.d.ts.map +1 -1
- package/dist/server/metadata.js +21 -7
- package/dist/server/metadata.js.map +1 -1
- package/dist/server/mixer-payload.d.ts +1 -1
- package/dist/server/mixer-payload.d.ts.map +1 -1
- package/dist/server/mixer.d.ts +48 -6
- package/dist/server/mixer.d.ts.map +1 -1
- package/dist/server/mixer.js +235 -68
- package/dist/server/mixer.js.map +1 -1
- package/dist/server/page.d.ts +21 -5
- package/dist/server/page.d.ts.map +1 -1
- package/dist/server/page.js +74 -15
- package/dist/server/page.js.map +1 -1
- package/dist/server/utils.d.ts.map +1 -1
- package/dist/server/utils.js +4 -2
- package/dist/server/utils.js.map +1 -1
- package/dist/server/utmz.d.ts.map +1 -1
- package/dist/server/utmz.js +1 -1
- package/dist/server/utmz.js.map +1 -1
- package/dist/types.d.ts +79 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +16 -15
- package/src/exports/server.ts +1 -0
- package/src/lib/const.ts +15 -0
- package/src/lib/http.ts +15 -4
- package/src/proxy/proxy.ts +11 -4
- package/src/server/context.test.ts +467 -0
- package/src/server/context.ts +237 -78
- package/src/server/context.type-test.ts +106 -0
- package/src/server/helpers.ts +58 -0
- package/src/server/layout.ts +45 -39
- package/src/server/lazy.ts +1 -1
- package/src/server/metadata.ts +15 -7
- package/src/server/mixer.ts +57 -9
- package/src/server/page.ts +62 -17
- package/src/types.ts +109 -7
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import type { SanitizedConfig } from 'payload';
|
|
4
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Emulates one Next.js request: React `cache` memoizes by argument identity
|
|
8
|
+
* until `newRequest()` starts the next request.
|
|
9
|
+
*/
|
|
10
|
+
const request = vi.hoisted(() => ({
|
|
11
|
+
memo: new Map<Function, { args: unknown[]; value: unknown }[]>(),
|
|
12
|
+
draft: false,
|
|
13
|
+
headers: new Headers(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('server-only', () => ({}));
|
|
17
|
+
|
|
18
|
+
vi.mock('react', async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal<typeof import('react')>();
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
cache: <T extends (...args: any[]) => any>(fn: T) => ((...args: unknown[]) => {
|
|
23
|
+
const entries = request.memo.get(fn) ?? [];
|
|
24
|
+
request.memo.set(fn, entries);
|
|
25
|
+
const hit = entries.find(entry => entry.args.length === args.length && entry.args.every((arg, i) => Object.is(arg, args[i])));
|
|
26
|
+
if (hit) {
|
|
27
|
+
return hit.value;
|
|
28
|
+
}
|
|
29
|
+
const value = fn(...args);
|
|
30
|
+
entries.push({ args, value });
|
|
31
|
+
return value;
|
|
32
|
+
}) as T,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
vi.mock('next/headers', () => ({
|
|
37
|
+
draftMode: vi.fn(async () => ({ isEnabled: request.draft })),
|
|
38
|
+
headers: vi.fn(async () => request.headers),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
vi.mock('@websolutespa/payload-plugin-bowl', () => ({
|
|
42
|
+
InMemoryCache: class<T> {
|
|
43
|
+
private map = new Map<string, T>();
|
|
44
|
+
has(key: string) { return this.map.has(key); }
|
|
45
|
+
get(key: string) { return this.map.get(key); }
|
|
46
|
+
set(key: string, value: T) { this.map.set(key, value); }
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const fake = vi.hoisted(() => {
|
|
51
|
+
type Doc = Record<string, any>;
|
|
52
|
+
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value));
|
|
53
|
+
|
|
54
|
+
const db: Record<string, Record<string, Doc>> = {
|
|
55
|
+
page: {
|
|
56
|
+
p1: {
|
|
57
|
+
id: 'p1',
|
|
58
|
+
title: 'Post',
|
|
59
|
+
slug: 'post',
|
|
60
|
+
abstract: 'Abstract',
|
|
61
|
+
order: 1,
|
|
62
|
+
category: 'blog',
|
|
63
|
+
template: 'tpl',
|
|
64
|
+
markets: ['ww'],
|
|
65
|
+
meta: { title: 'Meta title', description: 'Meta description' },
|
|
66
|
+
media: 'm1',
|
|
67
|
+
components: [{ blockType: 'hero', media: 'm1', related: 'p2' }],
|
|
68
|
+
},
|
|
69
|
+
p2: { id: 'p2', title: 'Related', slug: 'related', category: 'blog', markets: ['ww'], media: 'm2' },
|
|
70
|
+
},
|
|
71
|
+
media: {
|
|
72
|
+
m1: { id: 'm1', url: '/m1.jpg', alt: 'one', width: 10 },
|
|
73
|
+
m2: { id: 'm2', url: '/m2.jpg', alt: 'two', width: 20 },
|
|
74
|
+
},
|
|
75
|
+
category: {
|
|
76
|
+
root: { id: 'root', title: 'Home', slug: 'home' },
|
|
77
|
+
blog: { id: 'blog', title: 'Blog', slug: 'blog', category: 'root' },
|
|
78
|
+
},
|
|
79
|
+
market: { ww: { id: 'ww', title: 'World', isActive: true, isDefault: true } },
|
|
80
|
+
label: { hello: { id: 'hello', text: 'Hello' } },
|
|
81
|
+
menu: {},
|
|
82
|
+
};
|
|
83
|
+
const relations: Record<string, string> = { media: 'media', related: 'page' };
|
|
84
|
+
|
|
85
|
+
const applySelect = (doc: Doc, select?: Record<string, unknown>): Doc => {
|
|
86
|
+
if (!select) {
|
|
87
|
+
return doc;
|
|
88
|
+
}
|
|
89
|
+
const exclude = Object.values(select).some(value => value === false);
|
|
90
|
+
return Object.fromEntries(Object.entries(doc).filter(([key]) =>
|
|
91
|
+
key === 'id' || (exclude ? select[key] !== false : select[key] !== undefined)
|
|
92
|
+
));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Bowl decorates every document read with a mixer context, populated ones included.
|
|
96
|
+
const decorate = (doc: Doc, collection: string, context?: Doc) =>
|
|
97
|
+
context?.market ? { ...doc, schema: collection } : doc;
|
|
98
|
+
|
|
99
|
+
const populate = (value: any, depth: number, select: Doc | undefined, context?: Doc): any => {
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return value.map(item => populate(item, depth, select, context));
|
|
102
|
+
}
|
|
103
|
+
if (value && typeof value === 'object') {
|
|
104
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
105
|
+
const collection = relations[key];
|
|
106
|
+
if (collection && typeof item === 'string' && depth > 0 && db[collection][item]) {
|
|
107
|
+
const related = applySelect(clone(db[collection][item]), select?.[collection]);
|
|
108
|
+
return [key, decorate(populate(related, depth - 1, select, context), collection, context)];
|
|
109
|
+
}
|
|
110
|
+
return [key, populate(item, depth, select, context)];
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const payload = {
|
|
117
|
+
config: {
|
|
118
|
+
collections: [{ slug: 'page', fields: [{ name: 'slug' }] }],
|
|
119
|
+
},
|
|
120
|
+
auth: vi.fn(async ({ headers }: { headers: Headers }) => ({
|
|
121
|
+
user: headers.get('Authorization') ? { id: 'u1', collection: 'users', roles: ['admin'] } : null,
|
|
122
|
+
})),
|
|
123
|
+
findByID: vi.fn(async ({ collection, id, depth = 0, select, populate: populateSelect, context }: Doc) => {
|
|
124
|
+
if (id === 'boom') {
|
|
125
|
+
throw new Error('database unavailable');
|
|
126
|
+
}
|
|
127
|
+
const doc = db[collection]?.[id];
|
|
128
|
+
if (!doc) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return decorate(populate(applySelect(clone(doc), select), depth, populateSelect, context), collection, context);
|
|
132
|
+
}),
|
|
133
|
+
find: vi.fn(async ({ collection }: Doc) => ({
|
|
134
|
+
docs: Object.values(db[collection] ?? {}).map(doc => clone(doc)),
|
|
135
|
+
})),
|
|
136
|
+
findGlobal: vi.fn(async ({ slug }: Doc) => slug === 'locale'
|
|
137
|
+
? { items: [{ id: 'it', title: 'Italiano', isActive: true, isDefault: true }] }
|
|
138
|
+
: { id: slug, name: 'App' }),
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return { payload };
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
vi.mock('payload', () => ({
|
|
145
|
+
getPayload: vi.fn(async () => fake.payload),
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
const { createLayoutContext, createNotFoundContext, createPageContext } = await import('./context');
|
|
149
|
+
const { getMetadata } = await import('./metadata');
|
|
150
|
+
const { buildReadOptions } = await import('./mixer');
|
|
151
|
+
const { withEnvelopeSelect } = await import('./page');
|
|
152
|
+
const { DEFAULT_LAYOUT_DEPTH, DEFAULT_PAGE_DEPTH } = await import('../lib/const');
|
|
153
|
+
const { draftMode, headers: getHeaders } = await import('next/headers');
|
|
154
|
+
|
|
155
|
+
const config = Promise.resolve({} as SanitizedConfig);
|
|
156
|
+
|
|
157
|
+
const pageReads = () => fake.payload.findByID.mock.calls.map(([options]) => options as Record<string, any>);
|
|
158
|
+
|
|
159
|
+
function newRequest() {
|
|
160
|
+
request.memo.clear();
|
|
161
|
+
request.draft = false;
|
|
162
|
+
request.headers = new Headers();
|
|
163
|
+
(globalThis as any)._mixer = undefined;
|
|
164
|
+
(globalThis as any)._route = undefined;
|
|
165
|
+
vi.clearAllMocks();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
describe('createPageContext', () => {
|
|
169
|
+
beforeEach(newRequest);
|
|
170
|
+
|
|
171
|
+
test('reads the page at the default depth, populating uploads inside blocks', async () => {
|
|
172
|
+
const { page } = await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
173
|
+
|
|
174
|
+
expect(DEFAULT_PAGE_DEPTH).toBe(2);
|
|
175
|
+
expect(pageReads()[0]).toMatchObject({ collection: 'page', id: 'p1', depth: DEFAULT_PAGE_DEPTH, draft: false });
|
|
176
|
+
const [hero] = page!.components as any[];
|
|
177
|
+
expect(hero.media).toMatchObject({ id: 'm1', url: '/m1.jpg' });
|
|
178
|
+
// depth 2: relations of relations
|
|
179
|
+
expect(hero.related.media).toMatchObject({ id: 'm2', url: '/m2.jpg' });
|
|
180
|
+
expect(page!.media).toMatchObject({ id: 'm1', url: '/m1.jpg' });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('depth 0 returns ids', async () => {
|
|
184
|
+
const { page } = await createPageContext('ww', 'it', 'page', 'p1', { config, depth: 0 });
|
|
185
|
+
|
|
186
|
+
expect(pageReads()[0]).toMatchObject({ depth: 0 });
|
|
187
|
+
expect(page!.media).toBe('m1');
|
|
188
|
+
expect((page!.components as any[])[0].media).toBe('m1');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('builds the envelope (href, alternates, breadcrumb) and the layout', async () => {
|
|
192
|
+
const { page, layout, label, link } = await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
193
|
+
|
|
194
|
+
expect(page!.href).toBeTruthy();
|
|
195
|
+
expect(page!.alternates).toHaveLength(1);
|
|
196
|
+
expect(page!.breadcrumb.length).toBeGreaterThan(0);
|
|
197
|
+
expect(page!.breadcrumb[page!.breadcrumb.length - 1].href).toBe(page!.href);
|
|
198
|
+
expect(layout.market).toBe('ww');
|
|
199
|
+
expect(layout.labels).toHaveLength(1);
|
|
200
|
+
expect(typeof label).toBe('function');
|
|
201
|
+
expect(typeof link).toBe('function');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('select keeps the envelope fields, so breadcrumb and href still work', async () => {
|
|
205
|
+
const full = await createPageContext('ww', 'it', 'page', 'p1', { config, depth: 0 });
|
|
206
|
+
newRequest();
|
|
207
|
+
const { page } = await createPageContext('ww', 'it', 'page', 'p1', {
|
|
208
|
+
config,
|
|
209
|
+
depth: 0,
|
|
210
|
+
select: { title: true, media: true },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(pageReads()[0].select).toEqual({
|
|
214
|
+
title: true,
|
|
215
|
+
media: true,
|
|
216
|
+
id: true,
|
|
217
|
+
slug: true,
|
|
218
|
+
category: true,
|
|
219
|
+
markets: true,
|
|
220
|
+
meta: true,
|
|
221
|
+
template: true,
|
|
222
|
+
});
|
|
223
|
+
expect(page!.title).toBe('Post');
|
|
224
|
+
expect((page as any).order).toBeUndefined();
|
|
225
|
+
expect((page as any).abstract).toBeUndefined();
|
|
226
|
+
expect(page!.href).toBe(full.page!.href);
|
|
227
|
+
expect(page!.breadcrumb).toEqual(full.page!.breadcrumb);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('passes populate to the read and trims populated documents', async () => {
|
|
231
|
+
const { page } = await createPageContext('ww', 'it', 'page', 'p1', {
|
|
232
|
+
config,
|
|
233
|
+
depth: 1,
|
|
234
|
+
populate: { media: { url: true } },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(pageReads()[0]).toMatchObject({ depth: 1, populate: { media: { url: true } } });
|
|
238
|
+
expect(page!.media).toEqual({ id: 'm1', url: '/m1.jpg', schema: 'media' });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('populated documents keep Bowl decoration: the mixer context is passed to the read', async () => {
|
|
242
|
+
const { page } = await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
243
|
+
|
|
244
|
+
expect(pageReads()[0].context).toMatchObject({ market: 'ww', locale: 'it' });
|
|
245
|
+
expect(page!.schema).toBe('page');
|
|
246
|
+
expect(page!.media).toMatchObject({ schema: 'media' });
|
|
247
|
+
expect((page!.components as any[])[0].related).toMatchObject({ schema: 'page' });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('legacy positional signature still works, with the new default depth', async () => {
|
|
251
|
+
const legacy = await createPageContext('ww', 'it', 'page', 'p1', config);
|
|
252
|
+
|
|
253
|
+
expect(pageReads()[0]).toMatchObject({ depth: DEFAULT_PAGE_DEPTH });
|
|
254
|
+
expect(legacy.page!.id).toBe('p1');
|
|
255
|
+
expect(legacy.layout.market).toBe('ww');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('legacy signature warns once in development', async () => {
|
|
259
|
+
vi.stubEnv('NODE_ENV', 'development');
|
|
260
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
261
|
+
try {
|
|
262
|
+
await createPageContext('ww', 'it', 'page', 'p1', config);
|
|
263
|
+
request.memo.clear();
|
|
264
|
+
await createPageContext('ww', 'it', 'page', 'p1', config);
|
|
265
|
+
expect(warn.mock.calls.filter(([message]) => String(message).includes('createPageContext'))).toHaveLength(1);
|
|
266
|
+
} finally {
|
|
267
|
+
warn.mockRestore();
|
|
268
|
+
vi.unstubAllEnvs();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('legacy headers authenticate the user', async () => {
|
|
273
|
+
const headers = new Headers({ Authorization: 'JWT token' });
|
|
274
|
+
const { user } = await createPageContext('ww', 'it', 'page', 'p1', config, headers as any);
|
|
275
|
+
|
|
276
|
+
expect(user).toMatchObject({ id: 'u1' });
|
|
277
|
+
expect(pageReads()[0].user).toMatchObject({ id: 'u1' });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('metadata plus page trigger a single findByID per request', async () => {
|
|
281
|
+
const generateMetadata = getMetadata('page', config);
|
|
282
|
+
const metadata = await generateMetadata({ params: Promise.resolve({ market: 'ww', locale: 'it', id: 'p1' }) }, {} as any);
|
|
283
|
+
const { page } = await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
284
|
+
|
|
285
|
+
expect(metadata).toEqual({ title: 'Meta title', description: 'Meta description' });
|
|
286
|
+
expect(page!.id).toBe('p1');
|
|
287
|
+
expect(fake.payload.findByID).toHaveBeenCalledTimes(1);
|
|
288
|
+
// layout reads are shared too
|
|
289
|
+
expect(fake.payload.find.mock.calls.filter(([options]) => options.collection === 'label')).toHaveLength(1);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('each caller gets its own page object', async () => {
|
|
293
|
+
const first = await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
294
|
+
const second = await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
295
|
+
|
|
296
|
+
expect(fake.payload.findByID).toHaveBeenCalledTimes(1);
|
|
297
|
+
expect(first.page).not.toBe(second.page);
|
|
298
|
+
expect(first.page).toEqual(second.page);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('different options are different reads', async () => {
|
|
302
|
+
await createPageContext('ww', 'it', 'page', 'p1', { config, depth: 1 });
|
|
303
|
+
await createPageContext('ww', 'it', 'page', 'p1', { config, depth: 2 });
|
|
304
|
+
|
|
305
|
+
expect(fake.payload.findByID).toHaveBeenCalledTimes(2);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('a new request reads again', async () => {
|
|
309
|
+
await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
310
|
+
newRequest();
|
|
311
|
+
await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
312
|
+
|
|
313
|
+
expect(fake.payload.findByID).toHaveBeenCalledTimes(1);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('returns undefined when the page is not found', async () => {
|
|
317
|
+
const { page } = await createPageContext('ww', 'it', 'page', 'missing', { config });
|
|
318
|
+
|
|
319
|
+
expect(page).toBeUndefined();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('rethrows query errors instead of returning a 404', async () => {
|
|
323
|
+
const error = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
324
|
+
try {
|
|
325
|
+
await expect(createPageContext('ww', 'it', 'page', 'boom', { config })).rejects.toThrow('database unavailable');
|
|
326
|
+
} finally {
|
|
327
|
+
error.mockRestore();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('draft auto reads Next draftMode() and reads as the request user', async () => {
|
|
332
|
+
request.draft = true;
|
|
333
|
+
request.headers = new Headers({ Authorization: 'JWT token' });
|
|
334
|
+
|
|
335
|
+
const { user } = await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
336
|
+
|
|
337
|
+
expect(draftMode).toHaveBeenCalled();
|
|
338
|
+
expect(getHeaders).toHaveBeenCalled();
|
|
339
|
+
expect(user).toMatchObject({ id: 'u1' });
|
|
340
|
+
expect(pageReads()[0]).toMatchObject({ draft: true, user: { id: 'u1' } });
|
|
341
|
+
// the mixer context and routes are read with draft too
|
|
342
|
+
expect(fake.payload.find.mock.calls.find(([options]) => options.collection === 'category')![0]).toMatchObject({ draft: true });
|
|
343
|
+
expect(fake.payload.find.mock.calls.find(([options]) => options.collection === 'label')![0]).toMatchObject({ draft: true });
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test('draft: false ignores draftMode()', async () => {
|
|
347
|
+
request.draft = true;
|
|
348
|
+
|
|
349
|
+
await createPageContext('ww', 'it', 'page', 'p1', { config, draft: false });
|
|
350
|
+
|
|
351
|
+
expect(pageReads()[0]).toMatchObject({ draft: false });
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('layout options: own depth, or skipped', async () => {
|
|
355
|
+
await createPageContext('ww', 'it', 'page', 'p1', { config, layout: { depth: 0 } });
|
|
356
|
+
expect(fake.payload.find.mock.calls.find(([options]) => options.collection === 'label')![0]).toMatchObject({ depth: 0 });
|
|
357
|
+
|
|
358
|
+
newRequest();
|
|
359
|
+
const { layout, page, label } = await createPageContext('ww', 'it', 'page', 'p1', { config, layout: false });
|
|
360
|
+
expect(layout).toBeUndefined();
|
|
361
|
+
expect(page!.id).toBe('p1');
|
|
362
|
+
expect(label('missing')).toBeTypeOf('string');
|
|
363
|
+
expect(fake.payload.find.mock.calls.filter(([options]) => options.collection === 'label')).toHaveLength(0);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('reads page and layout in parallel', async () => {
|
|
367
|
+
let releasePage!: () => void;
|
|
368
|
+
const pageGate = new Promise<void>(resolve => releasePage = resolve);
|
|
369
|
+
const findByID = fake.payload.findByID.getMockImplementation()!;
|
|
370
|
+
fake.payload.findByID.mockImplementationOnce(async (options) => {
|
|
371
|
+
await pageGate;
|
|
372
|
+
return findByID(options);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
const pending = createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
376
|
+
await vi.waitFor(() => {
|
|
377
|
+
expect(fake.payload.find.mock.calls.some(([options]) => options.collection === 'label')).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
releasePage();
|
|
380
|
+
|
|
381
|
+
expect((await pending).page!.id).toBe('p1');
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('createNotFoundContext', () => {
|
|
386
|
+
beforeEach(newRequest);
|
|
387
|
+
|
|
388
|
+
test('always reads published content', async () => {
|
|
389
|
+
request.draft = true;
|
|
390
|
+
|
|
391
|
+
await createNotFoundContext('ww', 'it', 'page', 'p1', { config, draft: true });
|
|
392
|
+
|
|
393
|
+
expect(pageReads()[0]).toMatchObject({ draft: false });
|
|
394
|
+
expect(fake.payload.find.mock.calls.find(([options]) => options.collection === 'label')![0]).toMatchObject({ draft: false });
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('supports the legacy signature', async () => {
|
|
398
|
+
const { page } = await createNotFoundContext('ww', 'it', 'page', 'p1', config);
|
|
399
|
+
|
|
400
|
+
expect(page!.id).toBe('p1');
|
|
401
|
+
expect(pageReads()[0]).toMatchObject({ draft: false, depth: DEFAULT_PAGE_DEPTH });
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('createLayoutContext', () => {
|
|
406
|
+
beforeEach(newRequest);
|
|
407
|
+
|
|
408
|
+
test('options form reads the layout at the default layout depth', async () => {
|
|
409
|
+
const { layout } = await createLayoutContext('ww', 'it', { config });
|
|
410
|
+
|
|
411
|
+
expect(DEFAULT_LAYOUT_DEPTH).toBe(1);
|
|
412
|
+
expect(layout.market).toBe('ww');
|
|
413
|
+
expect(fake.payload.findByID).not.toHaveBeenCalled();
|
|
414
|
+
expect(fake.payload.find.mock.calls.find(([options]) => options.collection === 'label')![0]).toMatchObject({ depth: 1 });
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('legacy signature still works', async () => {
|
|
418
|
+
const { layout, user } = await createLayoutContext('ww', 'it', config, new Headers({ Authorization: 'JWT token' }) as any);
|
|
419
|
+
|
|
420
|
+
expect(layout.locale).toBe('it');
|
|
421
|
+
expect(user).toMatchObject({ id: 'u1' });
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test('shares the layout reads with createPageContext', async () => {
|
|
425
|
+
await createLayoutContext('ww', 'it', { config });
|
|
426
|
+
await createPageContext('ww', 'it', 'page', 'p1', { config });
|
|
427
|
+
|
|
428
|
+
expect(fake.payload.find.mock.calls.filter(([options]) => options.collection === 'label')).toHaveLength(1);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
describe('withEnvelopeSelect', () => {
|
|
433
|
+
test('leaves an empty select untouched', () => {
|
|
434
|
+
expect(withEnvelopeSelect(undefined)).toBeUndefined();
|
|
435
|
+
expect(withEnvelopeSelect({})).toEqual({});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('adds the envelope fields in include mode, keeping nested selects', () => {
|
|
439
|
+
expect(withEnvelopeSelect({ abstract: true, meta: { title: true } })).toEqual({
|
|
440
|
+
abstract: true,
|
|
441
|
+
meta: { title: true },
|
|
442
|
+
id: true,
|
|
443
|
+
slug: true,
|
|
444
|
+
title: true,
|
|
445
|
+
category: true,
|
|
446
|
+
markets: true,
|
|
447
|
+
template: true,
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('removes the envelope fields from the exclusion list in exclude mode', () => {
|
|
452
|
+
expect(withEnvelopeSelect({ components: false, category: false, meta: false })).toEqual({ components: false });
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe('buildReadOptions', () => {
|
|
457
|
+
test('has no hardcoded depth and only sets select / populate / joins when given', () => {
|
|
458
|
+
const options = buildReadOptions({ user: { id: 'u1' }, locale: 'it', depth: 3 });
|
|
459
|
+
|
|
460
|
+
expect(options).toMatchObject({ locale: 'it', depth: 3, draft: false, overrideAccess: false });
|
|
461
|
+
expect(options).not.toHaveProperty('select');
|
|
462
|
+
expect(options).not.toHaveProperty('populate');
|
|
463
|
+
expect(options).not.toHaveProperty('joins');
|
|
464
|
+
expect(buildReadOptions({ user: null, locale: 'it', select: { title: true }, populate: {}, joins: false }))
|
|
465
|
+
.toMatchObject({ select: { title: true }, populate: {}, joins: false });
|
|
466
|
+
});
|
|
467
|
+
});
|