@websolutespa/bom-mixer-store 0.3.0 → 0.3.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 -31
- package/package.json +5 -3
- package/src/__tests/example.test.ts +8 -0
- package/src/api/api.handler.ts +43 -0
- package/src/api/api.service.ts +52 -0
- package/src/fs/fs.service.ts +127 -0
- package/src/index.ts +15 -0
- package/src/middleware/middleware.service.ts +79 -0
- package/src/mock/mock-menu.service.ts +276 -0
- package/src/mock/mock-page.service.ts +79 -0
- package/src/mock/mock.handler.ts +6 -0
- package/src/mock/mock.service.ts +73 -0
- package/src/mock/mock.store.ts +80 -0
- package/src/revalidate/revalidate.handler.ts +0 -0
- package/src/session/session.handler.ts +82 -0
- package/src/static/static.service.ts +102 -0
- package/src/static/static.ts +223 -0
- package/src/store/store.handler.ts +176 -0
- package/src/store/store.service.ts +34 -0
- package/src/store/store.ts +15 -0
- package/src/storeApi/storeApi.service.ts +191 -0
- package/src/storeApi/storeApi.store.ts +66 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
# @websolutespa/bom-mixer-store
|
|
2
|
-
|
|
3
|
-
## 0.3.
|
|
4
|
-
|
|
5
|
-
###
|
|
6
|
-
|
|
7
|
-
- Added:
|
|
8
|
-
|
|
9
|
-
## 0.
|
|
10
|
-
|
|
11
|
-
### Minor Changes
|
|
12
|
-
|
|
13
|
-
- Added:
|
|
14
|
-
|
|
15
|
-
## 0.
|
|
16
|
-
|
|
17
|
-
###
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
## 0.1.
|
|
22
|
-
|
|
23
|
-
###
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
## 0.0
|
|
28
|
-
|
|
29
|
-
###
|
|
30
|
-
|
|
31
|
-
-
|
|
1
|
+
# @websolutespa/bom-mixer-store
|
|
2
|
+
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Added: source files.
|
|
8
|
+
|
|
9
|
+
## 0.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Added: splat routes.
|
|
14
|
+
|
|
15
|
+
## 0.2.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- Added: @websolutespa/bom-core.
|
|
20
|
+
|
|
21
|
+
## 0.1.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- Modified: store service.
|
|
26
|
+
|
|
27
|
+
## 0.1.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- mock refactor.
|
|
32
|
+
|
|
33
|
+
## 0.0.2
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Added: first release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@websolutespa/bom-mixer-store",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Mixer Store module of the BOM Repository",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bom",
|
|
@@ -45,12 +45,14 @@
|
|
|
45
45
|
"sideEffects": false,
|
|
46
46
|
"prepack": {
|
|
47
47
|
"types": "./dist/index.d.ts",
|
|
48
|
-
"main": "./dist/index.js"
|
|
48
|
+
"main": "./dist/index.js",
|
|
49
|
+
"source": "./src/index.ts"
|
|
49
50
|
},
|
|
50
51
|
"postpack": {
|
|
51
52
|
"types": "./src/index.ts",
|
|
52
53
|
"main": "./src/index.ts"
|
|
53
54
|
},
|
|
54
55
|
"types": "./dist/index.d.ts",
|
|
55
|
-
"main": "./dist/index.js"
|
|
56
|
+
"main": "./dist/index.js",
|
|
57
|
+
"source": "./src/index.ts"
|
|
56
58
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
2
|
+
|
|
3
|
+
export type ICrudMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | string;
|
|
4
|
+
|
|
5
|
+
export type IHandler = (request: NextApiRequest, response: NextApiResponse) => Promise<any>;
|
|
6
|
+
|
|
7
|
+
export type IApiHandler = Record<ICrudMethod, IHandler>;
|
|
8
|
+
|
|
9
|
+
export function apiHandler(handler: IApiHandler): any {
|
|
10
|
+
return async (request: NextApiRequest, response: NextApiResponse): Promise<any> => {
|
|
11
|
+
const method: ICrudMethod = (request.method || 'GET').toLowerCase() as ICrudMethod;
|
|
12
|
+
// check handler supports HTTP method
|
|
13
|
+
if (!handler[method]) {
|
|
14
|
+
return response.status(405).end(`Method ${method} Not Allowed`);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
// global middleware
|
|
18
|
+
// await jwtMiddleware(request, response);
|
|
19
|
+
// route handler
|
|
20
|
+
await handler[method](request, response);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// console.log('error', error);
|
|
23
|
+
// global error handler
|
|
24
|
+
return errorHandler(error, response);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function errorHandler(error: any, response: NextApiResponse): Promise<any> | void {
|
|
30
|
+
if (typeof (error) === 'string') {
|
|
31
|
+
// custom application error
|
|
32
|
+
const is404 = error.toLowerCase().endsWith('not found');
|
|
33
|
+
const statusCode = is404 ? 404 : 400;
|
|
34
|
+
return response.status(statusCode).json({ message: error });
|
|
35
|
+
}
|
|
36
|
+
if (error.name === 'UnauthorizedError') {
|
|
37
|
+
// jwt authentication error
|
|
38
|
+
return response.status(401).json({ message: 'Invalid Token' });
|
|
39
|
+
}
|
|
40
|
+
// default to 500 server error
|
|
41
|
+
console.error(error);
|
|
42
|
+
return response.status(500).json({ message: error.message });
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { FetchRequestOptions, httpFetch, merge } from '@websolutespa/bom-core';
|
|
2
|
+
|
|
3
|
+
const defaultApiOptions: FetchRequestOptions = {
|
|
4
|
+
// mode: 'cors',
|
|
5
|
+
headers: {
|
|
6
|
+
// Authorization: `Bearer ${BEARER}`,
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// typeof eval === 'function' ? defaultApiOptions.mode = 'cors' : null;
|
|
11
|
+
// const BEARER = process.env.STORE_BEARER || '7584f15d12972fb1e7695e998dd5e4c754f46c74d06a08e8d76f556adcd045e48cd52bed6faac098784471ac273ee40243194b9cff7eba3c4a768f8f41d2d51959a7f767b943c7f0170f6e5f632db523803c357083bf7a7bf03ee8e0df2d8ce5cd52e0211283b34d7781313da775018a9e950433d2b6faf711c20e5a63b25243';
|
|
12
|
+
|
|
13
|
+
export function getApiUrl(pathname: string): string {
|
|
14
|
+
const localUrl = process.env.NEXT_PUBLIC_URL === 'https://' ?
|
|
15
|
+
process.env.NEXT_PUBLIC_MOCK_FALLBACK_URL :
|
|
16
|
+
process.env.NEXT_PUBLIC_URL;
|
|
17
|
+
const apiBase = process.env.NEXT_PUBLIC_API || '/api';
|
|
18
|
+
const url = `${localUrl}${apiBase}${pathname}`;
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function apiFetch<T = any>(pathname: string, options: FetchRequestOptions = {}, serviceOptions?: { locale: string, market: string }): Promise<T> {
|
|
23
|
+
const url = new URL(getApiUrl(pathname));
|
|
24
|
+
if (serviceOptions) {
|
|
25
|
+
url.searchParams.append('locale', serviceOptions.locale);
|
|
26
|
+
url.searchParams.append('market', serviceOptions.market);
|
|
27
|
+
}
|
|
28
|
+
const apiOptions = merge({ ...defaultApiOptions }, options);
|
|
29
|
+
// console.log('apiFetch', url.href, apiOptions);
|
|
30
|
+
const apiResponse = await httpFetch(url.href, apiOptions);
|
|
31
|
+
return apiResponse;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function apiGet<T = any>(url: string, options: FetchRequestOptions = {}, serviceOptions?: { locale: string, market: string }): Promise<T> {
|
|
35
|
+
return await apiFetch<T>(url, { ...options, method: 'GET' }, serviceOptions);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function apiPost<T = any>(url: string, payload?: unknown, options: FetchRequestOptions = {}, serviceOptions?: { locale: string, market: string }): Promise<T> {
|
|
39
|
+
return await apiFetch<T>(url, { ...options, method: 'POST', body: payload ? JSON.stringify(payload) : undefined }, serviceOptions);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function apiPut<T = any>(url: string, payload?: unknown, options: FetchRequestOptions = {}, serviceOptions?: { locale: string, market: string }): Promise<T> {
|
|
43
|
+
return await apiFetch<T>(url, { ...options, method: 'PUT', body: payload ? JSON.stringify(payload) : undefined }, serviceOptions);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function apiPatch<T = any>(url: string, payload?: unknown, options: FetchRequestOptions = {}, serviceOptions?: { locale: string, market: string }): Promise<T> {
|
|
47
|
+
return await apiFetch<T>(url, { ...options, method: 'PATCH', body: payload ? JSON.stringify(payload) : undefined }, serviceOptions);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function apiDelete<T = any>(url: string, options: FetchRequestOptions = {}, serviceOptions?: { locale: string, market: string }): Promise<T> {
|
|
51
|
+
return await apiFetch<T>(url, { ...options, method: 'DELETE' }, serviceOptions);
|
|
52
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
export async function fsExists(pathname: string): Promise<boolean> {
|
|
6
|
+
try {
|
|
7
|
+
await fs.promises.access(pathname);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function fsRead(pathname: string, encoding = 'utf8'): Promise<any> {
|
|
15
|
+
try {
|
|
16
|
+
const data = await fs.promises.readFile(pathname, encoding);
|
|
17
|
+
return data || null;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.log('fsRead', error, pathname);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function fsReadJson(pathname: string): Promise<any> {
|
|
25
|
+
try {
|
|
26
|
+
const data = await fsRead(pathname);
|
|
27
|
+
if (data) {
|
|
28
|
+
return JSON.parse(data);
|
|
29
|
+
} else {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.log('fsReadJson', error, pathname);
|
|
34
|
+
// throw (error);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function fsWrite(pathname: string, data: string, encoding = 'utf8'): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
await fs.promises.mkdir(path.dirname(pathname), { recursive: true });
|
|
42
|
+
await fs.promises.writeFile(pathname, data, encoding);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.log('fsWrite', error, pathname);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function fsWriteJson(pathname: string, data: any): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
await fsWrite(pathname, JSON.stringify(data, null, 2));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.log('fsWriteJson', error, pathname);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function fsExistOrCreateFolder(pathname: string): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
const exists = fs.existsSync(pathname);
|
|
59
|
+
if (!exists) {
|
|
60
|
+
await fs.promises.mkdir(pathname);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.log('fsExistOrCreateFolder', error, pathname);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fsReadDirectory(pathname: string, extension?: string): Promise<{ name: string, data: string }[]> {
|
|
68
|
+
const files = await fs.promises.readdir(pathname);
|
|
69
|
+
const items = files.flatMap(async (file: string) => {
|
|
70
|
+
const filePath = path.join(pathname, file);
|
|
71
|
+
const stat = await fs.promises.stat(filePath);
|
|
72
|
+
if (stat.isDirectory()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (extension && path.parse(file).ext !== extension) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const data = await fs.promises.readFile(filePath);
|
|
79
|
+
if (!data) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
name: path.parse(file).name,
|
|
84
|
+
data: data.toString(),
|
|
85
|
+
};
|
|
86
|
+
}) as Promise<{ name: string, data: string }>[];
|
|
87
|
+
const items2 = (await Promise.all(items)).filter(Boolean);
|
|
88
|
+
return items2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function fsReadJsonDirectory(pathname: string): Promise<{ name: string, data: any }[]> {
|
|
92
|
+
try {
|
|
93
|
+
const datas = await fsReadDirectory(pathname, '.json');
|
|
94
|
+
return datas.filter(Boolean).map(x => {
|
|
95
|
+
x.data = JSON.parse(x.data);
|
|
96
|
+
return x;
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.log('fsReadJsonDirectory', error, pathname);
|
|
100
|
+
// throw (error);
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function fsWatch(pathname: string, callback: (eventType: any, fileName: string) => void, options?: { interval: 2000 }): void {
|
|
106
|
+
const debounced = debounce(callback);
|
|
107
|
+
return fs.watch(pathname, options, debounced);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function fsWatchFile(pathname: string, callback: (eventType: any, fileName: string) => void, options?: { interval: 2000 }): void {
|
|
111
|
+
const debounced = debounce(callback);
|
|
112
|
+
return fs.watchFile(pathname, options, debounced);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function pathJoin(...paths: string[]): string {
|
|
116
|
+
return path.join(process.cwd(), ...paths);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function debounce(callback: (...args: any[]) => void, timeout: number = 300) {
|
|
120
|
+
let timer: NodeJS.Timeout;
|
|
121
|
+
return (...args: any[]) => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
timer = setTimeout(() => {
|
|
124
|
+
callback(...args);
|
|
125
|
+
}, timeout);
|
|
126
|
+
};
|
|
127
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './api/api.handler';
|
|
2
|
+
export * from './api/api.service';
|
|
3
|
+
export * from './middleware/middleware.service';
|
|
4
|
+
export * from './mock/mock.handler';
|
|
5
|
+
export * from './mock/mock.service';
|
|
6
|
+
export * from './mock/mock.store';
|
|
7
|
+
// export * from './session/session.handler';
|
|
8
|
+
export * from './static/static';
|
|
9
|
+
export * from './static/static.service';
|
|
10
|
+
export * from './store/store';
|
|
11
|
+
export * from './store/store.handler';
|
|
12
|
+
export * from './store/store.service';
|
|
13
|
+
export * from './storeApi/storeApi.service';
|
|
14
|
+
export * from './storeApi/storeApi.store';
|
|
15
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const API_BASE = process.env.NEXT_PUBLIC_API || '/api';
|
|
4
|
+
|
|
5
|
+
export function isStaticRequest(request: NextRequest) {
|
|
6
|
+
const url = request.nextUrl;
|
|
7
|
+
const isStatic = url.pathname.indexOf('.') !== -1;
|
|
8
|
+
return isStatic;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isNextRequest(request: NextRequest) {
|
|
12
|
+
const url = request.nextUrl;
|
|
13
|
+
const isNext = url.pathname.indexOf('/_next/') === 0;
|
|
14
|
+
return isNext;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isApiRequest(request: NextRequest) {
|
|
18
|
+
const url = request.nextUrl;
|
|
19
|
+
const isApi = url.pathname.indexOf(API_BASE) === 0;
|
|
20
|
+
return isApi;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
import { NextApiRequest, NextApiResponse } from 'next';
|
|
25
|
+
|
|
26
|
+
export type RouteMiddleware = (request: NextRequest, event: NextFetchEvent) => Promise<Response | undefined> | Response | undefined;
|
|
27
|
+
|
|
28
|
+
export type ApiMiddleware = (request: NextApiRequest, response: NextApiResponse) => unknown;
|
|
29
|
+
|
|
30
|
+
// Helper method to wait for a middleware to execute before continuing
|
|
31
|
+
// And to throw an error when an error happens in a middleware
|
|
32
|
+
export default function useApiMiddleware(middleware) {
|
|
33
|
+
return (request, response) => new Promise((resolve, reject) => {
|
|
34
|
+
middleware(request, response, (result) => {
|
|
35
|
+
if (result instanceof Error) {
|
|
36
|
+
return reject(result);
|
|
37
|
+
}
|
|
38
|
+
return resolve(result);
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
* @name withMiddleware
|
|
44
|
+
* @description combine multiple middleware before handling your API endpoint
|
|
45
|
+
* @param middlewares
|
|
46
|
+
export function withApiMiddleware(...middlewares: ApiMiddleware[]) {
|
|
47
|
+
|
|
48
|
+
return async function withMiddlewareHandler(request: NextApiRequest, response: NextApiResponse) {
|
|
49
|
+
|
|
50
|
+
async function evaluateHandler(middleware: ApiMiddleware, innerMiddleware?: ApiMiddleware | undefined) {
|
|
51
|
+
// return early when the request has
|
|
52
|
+
// been ended by a previous middleware
|
|
53
|
+
if (response.headersSent) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (typeof middleware === 'function') {
|
|
57
|
+
const handler = await middleware(request, response);
|
|
58
|
+
if (typeof handler === 'function') {
|
|
59
|
+
if (innerMiddleware) {
|
|
60
|
+
await handler(innerMiddleware);
|
|
61
|
+
const index = middlewares.indexOf(innerMiddleware);
|
|
62
|
+
// remove inner middleware
|
|
63
|
+
if (index >= 0) {
|
|
64
|
+
middlewares.splice(index, 1);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
await handler();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (let index = 0; index < middlewares.length; index++) {
|
|
73
|
+
const middleware = middlewares[index];
|
|
74
|
+
const nextMiddleware = middlewares[index + 1];
|
|
75
|
+
await evaluateHandler(middleware, nextMiddleware);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
*/
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ICategorized, ICategory, IEntity, IEquatable, IMenu, IMenuCategory, IMenuGroup, IMenuItem,
|
|
3
|
+
IMenuLink, IMenuPage, IMenuRoute, IRoute, IStore,
|
|
4
|
+
IUndecoratedMenu, IUndecoratedMenuCategory, IUndecoratedMenuGroup, IUndecoratedMenuItem,
|
|
5
|
+
IUndecoratedMenuLink, IUndecoratedMenuPage, Pagination, QueryParams, localizeItem
|
|
6
|
+
} from '@websolutespa/bom-core';
|
|
7
|
+
import { MockService } from './mock.service';
|
|
8
|
+
|
|
9
|
+
export const MAX_INT = 1000000000000; // 9007199254740991
|
|
10
|
+
|
|
11
|
+
let UID = 0;
|
|
12
|
+
|
|
13
|
+
export class MockMenuService<T extends IEntity = IEntity> extends MockService<T> {
|
|
14
|
+
|
|
15
|
+
async findOne<B extends T = T>(id: IEquatable, params: QueryParams = {}): Promise<B | undefined> {
|
|
16
|
+
// console.log('MockMenuService.findOne', id, params);
|
|
17
|
+
let item = await super.findOne<B>(id, params);
|
|
18
|
+
if (item && params.market && params.locale) {
|
|
19
|
+
item = await decorateMenu(this.store, (item as any), params.market, params.locale) as any;
|
|
20
|
+
}
|
|
21
|
+
return item;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async findMany<B extends T = T>(params: QueryParams = {}): Promise<B[]> {
|
|
25
|
+
// console.log('MockMenuService.findMany', params);
|
|
26
|
+
let items = await super.findMany<B>(params);
|
|
27
|
+
if (params.market && params.locale) {
|
|
28
|
+
items = await decorateMenus(this.store, (items as any[]), params.market, params.locale) as any[];
|
|
29
|
+
}
|
|
30
|
+
return items;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async findPaged<B extends T = T>(params: QueryParams = {}): Promise<{ docs: B[]; } & Pagination> {
|
|
34
|
+
// console.log('MockMenuService.findPaged', params);
|
|
35
|
+
const pagination = await super.findPaged<B>(params);
|
|
36
|
+
if (params.market && params.locale) {
|
|
37
|
+
pagination.docs = await decorateMenus(this.store, (pagination.docs as any[]), params.market, params.locale) as any[];
|
|
38
|
+
}
|
|
39
|
+
return pagination;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
protected override decorator_<B extends T = T>(item: B, params: QueryParams = {}): B {
|
|
43
|
+
if (params.locale) {
|
|
44
|
+
return localizeItem(item, params.locale);
|
|
45
|
+
} else {
|
|
46
|
+
return item;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function decorateMenu(store: IStore, item: IUndecoratedMenu, market: string = 'ww', locale: string = 'en'): Promise<IMenu> {
|
|
52
|
+
const items = item.items || [];
|
|
53
|
+
const decoratedItem: IMenu = {
|
|
54
|
+
id: item.id,
|
|
55
|
+
items: [],
|
|
56
|
+
};
|
|
57
|
+
const categories = await store.category.findMany<ICategory>({ market, locale });
|
|
58
|
+
const routes = await store.route.findMany<IRoute>({
|
|
59
|
+
where: {
|
|
60
|
+
market: {
|
|
61
|
+
equals: market,
|
|
62
|
+
},
|
|
63
|
+
locale: {
|
|
64
|
+
equals: locale,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
async function collectPages(items?: IUndecoratedMenuItem[]) {
|
|
69
|
+
if (!items) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
for (const item of items) {
|
|
73
|
+
if (
|
|
74
|
+
item.type === 'page' &&
|
|
75
|
+
typeof item.page.value === 'number' &&
|
|
76
|
+
store[item.page.relationTo] !== undefined
|
|
77
|
+
) {
|
|
78
|
+
const value = await store[item.page.relationTo].findOne<ICategorized>(item.page.value);
|
|
79
|
+
if (value) {
|
|
80
|
+
item.page.value = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
switch (item.type) {
|
|
84
|
+
case 'group':
|
|
85
|
+
case 'link':
|
|
86
|
+
case 'page':
|
|
87
|
+
await collectPages(item.items);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await collectPages(items);
|
|
93
|
+
decoratedItem.items = items.filter(x => hasMarket(x, market)).map(x => decorateMenuItem(x, categories, routes, market));
|
|
94
|
+
return decoratedItem;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function decorateMenus(store: IStore, items: IUndecoratedMenu[], market: string = 'ww', locale: string = 'en'): Promise<IMenu[]> {
|
|
98
|
+
return await Promise.all(items.map(x => decorateMenu(store, x, market, locale)));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function decorateMenuCategory(
|
|
102
|
+
item: IUndecoratedMenuCategory,
|
|
103
|
+
categories: ICategory[],
|
|
104
|
+
routes: IRoute[],
|
|
105
|
+
market: string,
|
|
106
|
+
depth: number = 0,
|
|
107
|
+
): IMenuItem {
|
|
108
|
+
const decoratedItem: IMenuCategory = {
|
|
109
|
+
id: item.id || '',
|
|
110
|
+
type: item.type,
|
|
111
|
+
title: '',
|
|
112
|
+
category: '',
|
|
113
|
+
href: '',
|
|
114
|
+
items: [],
|
|
115
|
+
};
|
|
116
|
+
const category = (typeof item.category === 'string' ? categories.find(x => x.id === item.category) : item.category) as ICategory;
|
|
117
|
+
if (category) {
|
|
118
|
+
// assigning category id
|
|
119
|
+
decoratedItem.category = category.id;
|
|
120
|
+
// assigning category title
|
|
121
|
+
decoratedItem.title = item.title || category.title || '';
|
|
122
|
+
// assigning current or category media
|
|
123
|
+
decoratedItem.media = decoratedItem.media || category.media;
|
|
124
|
+
// find category's index page route
|
|
125
|
+
const route = routes.find(x => x.isDefault && x.category === category.id);
|
|
126
|
+
if (route) {
|
|
127
|
+
// hasLink
|
|
128
|
+
decoratedItem.href = route.id;
|
|
129
|
+
} else {
|
|
130
|
+
// isGroup
|
|
131
|
+
}
|
|
132
|
+
// checking for max depth recursion
|
|
133
|
+
let maxDepth = item.maxDepth;
|
|
134
|
+
if (typeof maxDepth === 'number') {
|
|
135
|
+
if (depth > maxDepth) {
|
|
136
|
+
return decoratedItem;
|
|
137
|
+
}
|
|
138
|
+
maxDepth--;
|
|
139
|
+
}
|
|
140
|
+
// search child categories
|
|
141
|
+
const childCategories = categories.filter(x => (
|
|
142
|
+
x.category && (typeof x.category === 'object' ?
|
|
143
|
+
x.category.id === category.id :
|
|
144
|
+
x.category === category.id)
|
|
145
|
+
));
|
|
146
|
+
if (childCategories.length) {
|
|
147
|
+
// sort child categories
|
|
148
|
+
childCategories.sort((a, b) => {
|
|
149
|
+
return (a.order || MAX_INT) - (b.order || MAX_INT);
|
|
150
|
+
});
|
|
151
|
+
decoratedItem.items = childCategories.map(c => decorateMenuCategory({
|
|
152
|
+
id: c.id,
|
|
153
|
+
type: 'category',
|
|
154
|
+
category: c.id,
|
|
155
|
+
maxDepth,
|
|
156
|
+
items: [],
|
|
157
|
+
} as IUndecoratedMenuCategory, categories, routes, market, depth + 1));
|
|
158
|
+
} else {
|
|
159
|
+
// find child routes
|
|
160
|
+
const childRoutes = routes.filter(x => !x.isDefault && x.category === category.id);
|
|
161
|
+
// sort child routes
|
|
162
|
+
childRoutes.sort((a, b) => {
|
|
163
|
+
return (a.order || MAX_INT) - (b.order || MAX_INT);
|
|
164
|
+
});
|
|
165
|
+
decoratedItem.items = childRoutes.map(x => decorateMenuRoute(x));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return decoratedItem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function decorateMenuRoute(
|
|
172
|
+
item: IRoute,
|
|
173
|
+
): IMenuItem {
|
|
174
|
+
const decoratedItem: IMenuRoute = {
|
|
175
|
+
id: item.page,
|
|
176
|
+
type: 'route',
|
|
177
|
+
title: item.title,
|
|
178
|
+
href: item.id,
|
|
179
|
+
page: item.page,
|
|
180
|
+
schema: item.schema,
|
|
181
|
+
items: [],
|
|
182
|
+
};
|
|
183
|
+
return decoratedItem;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function decorateMenuGroup(
|
|
187
|
+
item: IUndecoratedMenuGroup,
|
|
188
|
+
categories: ICategory[],
|
|
189
|
+
routes: IRoute[],
|
|
190
|
+
market: string,
|
|
191
|
+
): IMenuItem {
|
|
192
|
+
const items = item.items || [];
|
|
193
|
+
const decoratedItem: IMenuGroup = {
|
|
194
|
+
id: item.id || '',
|
|
195
|
+
type: item.type,
|
|
196
|
+
title: item.title,
|
|
197
|
+
items: items.filter(x => hasMarket(x, market)).map(x => decorateMenuItem(x, categories, routes, market)),
|
|
198
|
+
};
|
|
199
|
+
return decoratedItem;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function decorateMenuLink(
|
|
203
|
+
item: IUndecoratedMenuLink,
|
|
204
|
+
categories: ICategory[],
|
|
205
|
+
routes: IRoute[],
|
|
206
|
+
market: string,
|
|
207
|
+
): IMenuItem {
|
|
208
|
+
const items = item.items || [];
|
|
209
|
+
const decoratedItem: IMenuLink = {
|
|
210
|
+
id: item.id || '',
|
|
211
|
+
type: item.type,
|
|
212
|
+
title: item.title,
|
|
213
|
+
href: item.href,
|
|
214
|
+
target: item.target || '',
|
|
215
|
+
items: items.filter(x => hasMarket(x, market)).map(x => decorateMenuItem(x, categories, routes, market)),
|
|
216
|
+
};
|
|
217
|
+
return decoratedItem;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function decorateMenuPage(
|
|
221
|
+
item: IUndecoratedMenuPage,
|
|
222
|
+
categories: ICategory[],
|
|
223
|
+
routes: IRoute[],
|
|
224
|
+
market: string,
|
|
225
|
+
): IMenuItem {
|
|
226
|
+
const items = item.items || [];
|
|
227
|
+
const route = routes.find(x => x.schema === item.page.relationTo && x.page === item.page.value.id);
|
|
228
|
+
const decoratedItem: IMenuPage = {
|
|
229
|
+
id: item.id || '',
|
|
230
|
+
type: item.type,
|
|
231
|
+
title: item.title || item.page.value.title || '',
|
|
232
|
+
href: route?.id || '',
|
|
233
|
+
media: item.page.value.media,
|
|
234
|
+
page: route?.page || '',
|
|
235
|
+
schema: route?.schema || '',
|
|
236
|
+
items: items.filter(x => hasMarket(x, market)).map(x => decorateMenuItem(x, categories, routes, market)),
|
|
237
|
+
};
|
|
238
|
+
return decoratedItem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function decorateMenuItem(
|
|
242
|
+
item: IUndecoratedMenuItem,
|
|
243
|
+
categories: ICategory[],
|
|
244
|
+
routes: IRoute[],
|
|
245
|
+
market: string,
|
|
246
|
+
depth: number = 0,
|
|
247
|
+
): IMenuItem {
|
|
248
|
+
let decoratedItem: IMenuItem;
|
|
249
|
+
switch (item.type) {
|
|
250
|
+
case 'category':
|
|
251
|
+
decoratedItem = decorateMenuCategory(item, categories, routes, market, depth);
|
|
252
|
+
break;
|
|
253
|
+
case 'group':
|
|
254
|
+
decoratedItem = decorateMenuGroup(item, categories, routes, market);
|
|
255
|
+
break;
|
|
256
|
+
case 'link':
|
|
257
|
+
decoratedItem = decorateMenuLink(item, categories, routes, market);
|
|
258
|
+
break;
|
|
259
|
+
case 'page':
|
|
260
|
+
decoratedItem = decorateMenuPage(item, categories, routes, market);
|
|
261
|
+
break;
|
|
262
|
+
default:
|
|
263
|
+
// eslint-disable-next-line no-case-declarations
|
|
264
|
+
const { blockType, ...rest } = item;
|
|
265
|
+
decoratedItem = {
|
|
266
|
+
...rest,
|
|
267
|
+
type: blockType,
|
|
268
|
+
} as any;
|
|
269
|
+
}
|
|
270
|
+
decoratedItem.id = ++UID;
|
|
271
|
+
return decoratedItem;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function hasMarket(item: IUndecoratedMenuItem, market: string): boolean {
|
|
275
|
+
return Array.isArray(item.markets) ? item.markets.includes(market) : true;
|
|
276
|
+
}
|