@websolutespa/bom-mixer-store 0.3.0 → 0.3.2

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.
@@ -0,0 +1,286 @@
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
+
52
+ async function collectPages(store: IStore, items?: IUndecoratedMenuItem[]) {
53
+ if (!items) {
54
+ return;
55
+ }
56
+ for (const item of items) {
57
+ if (
58
+ item.type === 'page' &&
59
+ typeof item.page.value === 'number' &&
60
+ store[item.page.relationTo] !== undefined
61
+ ) {
62
+ const value = await store[item.page.relationTo].findOne<ICategorized>(item.page.value);
63
+ if (value) {
64
+ item.page.value = value;
65
+ }
66
+ }
67
+ switch (item.type) {
68
+ case 'group':
69
+ case 'link':
70
+ case 'page':
71
+ await collectPages(store, item.items);
72
+ break;
73
+ }
74
+ }
75
+ }
76
+
77
+ export async function decorateMenu(store: IStore, item: IUndecoratedMenu, market: string = 'ww', locale: string = 'en'): Promise<IMenu> {
78
+ const items = item.items || [];
79
+ const decoratedItem: IMenu = {
80
+ id: item.id,
81
+ items: [],
82
+ };
83
+ const categories = await store.category.findMany<ICategory>({ market, locale });
84
+ const routes = await store.route.findMany<IRoute>({
85
+ where: {
86
+ market: {
87
+ equals: market,
88
+ },
89
+ locale: {
90
+ equals: locale,
91
+ }
92
+ }
93
+ });
94
+ await collectPages(store, items);
95
+ decoratedItem.items = items.filter(x => hasMarket(x, market)).map(x => decorateMenuItem(x, categories, routes, market));
96
+ return decoratedItem;
97
+ }
98
+
99
+ export async function decorateMenus(store: IStore, items: IUndecoratedMenu[], market: string = 'ww', locale: string = 'en'): Promise<IMenu[]> {
100
+ return await Promise.all(items.map(x => decorateMenu(store, x, market, locale)));
101
+ }
102
+
103
+ export function decorateMenuCategory(
104
+ item: IUndecoratedMenuCategory,
105
+ categories: ICategory[],
106
+ routes: IRoute[],
107
+ market: string,
108
+ depth: number = 0,
109
+ ): IMenuItem {
110
+ const decoratedItem: IMenuCategory = {
111
+ abstract: item.abstract,
112
+ category: '',
113
+ customClass: item.customClass,
114
+ href: '',
115
+ id: item.id || '',
116
+ items: [],
117
+ title: '',
118
+ type: item.type,
119
+ };
120
+ const category = (typeof item.category === 'string' ? categories.find(x => x.id === item.category) : item.category) as ICategory;
121
+ if (category) {
122
+ // assigning category id
123
+ decoratedItem.category = category.id;
124
+ // assigning category title
125
+ decoratedItem.title = item.title || category.title || '';
126
+ // assigning current or category media
127
+ decoratedItem.media = decoratedItem.media || category.media;
128
+ // find category's index page route
129
+ const route = routes.find(x => x.isDefault && x.category === category.id);
130
+ if (route) {
131
+ // hasLink
132
+ decoratedItem.href = route.id;
133
+ } else {
134
+ // isGroup
135
+ }
136
+ // checking for max depth recursion
137
+ let maxDepth = item.maxDepth;
138
+ if (typeof maxDepth === 'number') {
139
+ if (depth > maxDepth) {
140
+ return decoratedItem;
141
+ }
142
+ maxDepth--;
143
+ }
144
+ // search child categories
145
+ const childCategories = categories.filter(x => (
146
+ x.category && (typeof x.category === 'object' ?
147
+ x.category.id === category.id :
148
+ x.category === category.id)
149
+ ));
150
+ if (childCategories.length) {
151
+ // sort child categories
152
+ childCategories.sort((a, b) => {
153
+ return (a.order || MAX_INT) - (b.order || MAX_INT);
154
+ });
155
+ decoratedItem.items = childCategories.map(c => decorateMenuCategory({
156
+ id: c.id,
157
+ type: 'category',
158
+ category: c.id,
159
+ maxDepth,
160
+ items: [],
161
+ } as IUndecoratedMenuCategory, categories, routes, market, depth + 1));
162
+ } else {
163
+ // find child routes
164
+ const childRoutes = routes.filter(x => !x.isDefault && x.category === category.id);
165
+ // sort child routes
166
+ childRoutes.sort((a, b) => {
167
+ return (a.order || MAX_INT) - (b.order || MAX_INT);
168
+ });
169
+ decoratedItem.items = childRoutes.map(x => decorateMenuRoute(x));
170
+ }
171
+ }
172
+ return decoratedItem;
173
+ }
174
+
175
+ export function decorateMenuRoute(
176
+ item: IRoute,
177
+ ): IMenuItem {
178
+ const decoratedItem: IMenuRoute = {
179
+ id: item.page,
180
+ type: 'route',
181
+ title: item.title,
182
+ href: item.id,
183
+ page: item.page,
184
+ schema: item.schema,
185
+ items: [],
186
+ };
187
+ return decoratedItem;
188
+ }
189
+
190
+ export function decorateMenuGroup(
191
+ item: IUndecoratedMenuGroup,
192
+ categories: ICategory[],
193
+ routes: IRoute[],
194
+ market: string,
195
+ ): IMenuItem {
196
+ const items = item.items || [];
197
+ const decoratedItem: IMenuGroup = {
198
+ abstract: item.abstract,
199
+ customClass: item.customClass,
200
+ id: item.id || '',
201
+ title: item.title,
202
+ type: item.type,
203
+ items: items.filter(x => hasMarket(x, market)).map(x => decorateMenuItem(x, categories, routes, market)),
204
+ };
205
+ return decoratedItem;
206
+ }
207
+
208
+ export function decorateMenuLink(
209
+ item: IUndecoratedMenuLink,
210
+ categories: ICategory[],
211
+ routes: IRoute[],
212
+ market: string,
213
+ ): IMenuItem {
214
+ const items = item.items || [];
215
+ const decoratedItem: IMenuLink = {
216
+ abstract: item.abstract,
217
+ customClass: item.customClass,
218
+ href: item.href,
219
+ id: item.id || '',
220
+ target: item.target || '',
221
+ title: item.title,
222
+ type: item.type,
223
+ items: items.filter(x => hasMarket(x, market)).map(x => decorateMenuItem(x, categories, routes, market)),
224
+ };
225
+ return decoratedItem;
226
+ }
227
+
228
+ export function decorateMenuPage(
229
+ item: IUndecoratedMenuPage,
230
+ categories: ICategory[],
231
+ routes: IRoute[],
232
+ market: string,
233
+ ): IMenuItem {
234
+ const items = item.items || [];
235
+ const route = routes.find(x => x.schema === item.page.relationTo && x.page === item.page.value.id);
236
+ const decoratedItem: IMenuPage = {
237
+ id: item.id || '',
238
+ type: item.type,
239
+ title: item.title || item.page.value.title || '',
240
+ abstract: item.abstract || item.page.value.abstract || '',
241
+ customClass: item.customClass,
242
+ href: route?.id || '',
243
+ media: item.page.value.media,
244
+ page: route?.page || '',
245
+ schema: route?.schema || '',
246
+ items: items.filter(x => hasMarket(x, market)).map(x => decorateMenuItem(x, categories, routes, market)),
247
+ };
248
+ return decoratedItem;
249
+ }
250
+
251
+ export function decorateMenuItem(
252
+ item: IUndecoratedMenuItem,
253
+ categories: ICategory[],
254
+ routes: IRoute[],
255
+ market: string,
256
+ depth: number = 0,
257
+ ): IMenuItem {
258
+ let decoratedItem: IMenuItem;
259
+ switch (item.type) {
260
+ case 'category':
261
+ decoratedItem = decorateMenuCategory(item, categories, routes, market, depth);
262
+ break;
263
+ case 'group':
264
+ decoratedItem = decorateMenuGroup(item, categories, routes, market);
265
+ break;
266
+ case 'link':
267
+ decoratedItem = decorateMenuLink(item, categories, routes, market);
268
+ break;
269
+ case 'page':
270
+ decoratedItem = decorateMenuPage(item, categories, routes, market);
271
+ break;
272
+ default:
273
+ // eslint-disable-next-line no-case-declarations
274
+ const { blockType, ...rest } = item;
275
+ decoratedItem = {
276
+ ...rest,
277
+ type: blockType,
278
+ } as any;
279
+ }
280
+ decoratedItem.id = ++UID;
281
+ return decoratedItem;
282
+ }
283
+
284
+ export function hasMarket(item: IUndecoratedMenuItem, market: string): boolean {
285
+ return Array.isArray(item.markets) ? item.markets.includes(market) : true;
286
+ }
@@ -0,0 +1,79 @@
1
+ import {
2
+ IEntity, IEquatable,
3
+ IRoute, ISchema, IStore, Pagination,
4
+ QueryParams, localizeItem
5
+ } from '@websolutespa/bom-core';
6
+ import { MockService } from './mock.service';
7
+
8
+ export class MockPageService<T extends IEntity = IEntity> extends MockService<T> {
9
+
10
+ async findOne<B extends T = T>(id: IEquatable, params: QueryParams = {}): Promise<B | undefined> {
11
+ // console.log('MockPageService.findOne', id, params);
12
+ let item = await super.findOne<B>(id, params);
13
+ if (item && params.market && params.locale) {
14
+ item = await decorateHref<B>(this.store, item, params.market, params.locale);
15
+ }
16
+ return item;
17
+ }
18
+
19
+ async findMany<B extends T = T>(params: QueryParams = {}): Promise<B[]> {
20
+ // console.log('MockPageService.findMany', params);
21
+ let items = await super.findMany<B>(params);
22
+ if (params.market && params.locale) {
23
+ items = await decorateHrefs<B>(this.store, items, params.market, params.locale);
24
+ }
25
+ return items;
26
+ }
27
+
28
+ async findPaged<B extends T = T>(params: QueryParams = {}): Promise<{ docs: B[]; } & Pagination> {
29
+ // console.log('MockPageService.findPaged', params);
30
+ const pagination = await super.findPaged<B>(params);
31
+ if (params.market && params.locale) {
32
+ pagination.docs = await decorateHrefs<B>(this.store, pagination.docs, params.market, params.locale);
33
+ }
34
+ return pagination;
35
+ }
36
+
37
+ protected override decorator_<B extends T = T>(item: B, params: QueryParams = {}): B {
38
+ if (params.locale) {
39
+ return localizeItem(item, params.locale);
40
+ } else {
41
+ return item;
42
+ }
43
+ }
44
+ }
45
+
46
+ export async function decorateHref<T extends ISchema = ISchema>(store: IStore, item: T, market: string = 'ww', locale: string = 'en'): Promise<T & { href: string | null }> {
47
+ const routes = await store.route.findMany<IRoute>({
48
+ where: {
49
+ schema: {
50
+ equals: item.schema
51
+ },
52
+ page: {
53
+ equals: item.id
54
+ },
55
+ market: {
56
+ equals: market
57
+ },
58
+ locale: {
59
+ equals: locale
60
+ },
61
+ }
62
+ });
63
+ const href = routes.length ? routes[0].id : null;
64
+ return { ...item, href };
65
+ }
66
+
67
+ export async function decorateHrefs<T extends ISchema = ISchema>(store: IStore, items: T[], market: string = 'ww', locale: string = 'en'): Promise<(T & { href: string | null })[]> {
68
+ const routes = await store.route.findMany<IRoute>();
69
+ return items.map(item => {
70
+ const route = routes.find(x =>
71
+ x.schema === item.schema &&
72
+ x.page === item.id &&
73
+ x.market === market &&
74
+ x.locale === locale
75
+ );
76
+ const href = route ? route.id : null;
77
+ return { ...item, href };
78
+ });
79
+ }
@@ -0,0 +1,6 @@
1
+ import { storeHandler } from '../store/store.handler';
2
+ import { getMockStore } from './mock.store';
3
+
4
+ export function mockHandler(getJson: () => Promise<string>) {
5
+ return storeHandler(async () => await getMockStore(getJson));
6
+ }
@@ -0,0 +1,73 @@
1
+ import { IEntity, IPageRelation, IStore, JsonService, localizeItem, QueryParams } from '@websolutespa/bom-core';
2
+
3
+ export class MockService<T extends IEntity = IEntity> extends JsonService<T> {
4
+ store: IStore;
5
+
6
+ constructor(items: T[], store: IStore) {
7
+ super(items);
8
+ this.store = store;
9
+ }
10
+
11
+ // !!! todo handle relations
12
+
13
+ /*
14
+ override async findOne<B extends T = T>(id: IEquatable, params: QueryParams = {}): Promise<B | undefined> {
15
+ console.log('MockService.findOne', id, params);
16
+ let item = await super.findOne<B>(id, params);
17
+ // relations
18
+ if (item) {
19
+ if (params.depth !== 0) {
20
+ item = await this.relatedItems_(item);
21
+ }
22
+ }
23
+ return item;
24
+ }
25
+
26
+ override async findMany<B extends T = T>(params: QueryParams = {}): Promise<B[]> {
27
+ // console.log('MockService.findMany', params);
28
+ let items = await super.findMany<B>(params);
29
+ // relations
30
+ if (params.depth !== 0) {
31
+ items = await Promise.all(items.map(x => this.relatedItems_(x)));
32
+ }
33
+ return items;
34
+ }
35
+
36
+ override async findPaged<B extends T = T>(params: QueryParams = {}): Promise<{ docs: B[]; } & Pagination> {
37
+ // console.log('MockService.findPaged', params);
38
+ const pagination = await super.findPaged<B>(params);
39
+ // relations
40
+ if (params.depth !== 0) {
41
+ pagination.docs = await Promise.all(pagination.docs.map(x => this.relatedItems_(x)));
42
+ }
43
+ return pagination;
44
+ }
45
+
46
+ protected async relatedItems_<B extends T = T>(item: B): Promise<B> {
47
+ const x = { ...item };
48
+ Object.entries(item).forEach(([k, v]) => {
49
+ if (isRelation(v)) {
50
+ console.log(k, v);
51
+ }
52
+ });
53
+ return x;
54
+ }
55
+ */
56
+
57
+ protected override decorator_<B extends T = T>(item: B, params: QueryParams = {}): B {
58
+ if (params.locale) {
59
+ return localizeItem(item, params.locale);
60
+ } else {
61
+ return item;
62
+ }
63
+ }
64
+
65
+ }
66
+
67
+ export function isObject(item: unknown): item is Record<string, unknown> {
68
+ return item !== null && (typeof item === 'object');
69
+ }
70
+
71
+ export function isRelation(item: unknown): item is IPageRelation {
72
+ return isObject(item) && typeof item.relationTo === 'string' && item.value !== undefined;
73
+ }
@@ -0,0 +1,80 @@
1
+ import { IMemoryStore, isDevelopment, IStore } from '@websolutespa/bom-core';
2
+ import { fsReadJson } from '../fs/fs.service';
3
+ import { staticCollections } from '../static/static';
4
+ import { StaticService } from '../static/static.service';
5
+ import { MockMenuService } from './mock-menu.service';
6
+ import { MockPageService } from './mock-page.service';
7
+ import { MockService } from './mock.service';
8
+
9
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
10
+ const path = require('path');
11
+
12
+ const PAGES: string[] = process.env.PAGES ? process.env.PAGES.split(',') : [];
13
+
14
+ let STORE_: IStore;
15
+
16
+ export async function getMockStoreServer<T extends IStore>(): Promise<T> {
17
+ // console.log('getMockStore', STORE_ ? Object.keys(STORE_) : null);
18
+ // console.log('getMockStore.process.cwd', process.cwd());
19
+ if (STORE_) {
20
+ return STORE_ as T;
21
+ }
22
+ const pathname = path.join(process.cwd(), '.mixer', 'store.json');
23
+ // console.log('getMockStore.pathname', pathname);
24
+ const data: IMemoryStore = await fsReadJson(pathname);
25
+ const store = getMockStoreFromData(data);
26
+ if (!isDevelopment) {
27
+ STORE_ = store;
28
+ }
29
+ return store as T;
30
+ }
31
+
32
+ export async function getMockStore<T extends IStore>(getJson: () => Promise<string>): Promise<T> {
33
+ // console.log('getMockStore', STORE_ ? Object.keys(STORE_) : null);
34
+ // console.log('getMockStore.process.cwd', process.cwd());
35
+ if (STORE_) {
36
+ return STORE_ as T;
37
+ }
38
+ const json = await getJson();
39
+ const data: IMemoryStore = JSON.parse(json);
40
+ const store = getMockStoreFromData(data);
41
+ if (!isDevelopment) {
42
+ STORE_ = store;
43
+ }
44
+ return store as T;
45
+ }
46
+
47
+ export function getMockStoreFromData<T extends IStore>(data: IMemoryStore): T {
48
+ const store: IStore = {};
49
+ for (const key in data) {
50
+ if (!PAGES.includes(key)) {
51
+ // new collection
52
+ const service = new MockService(data[key], store);
53
+ store[key] = service;
54
+ }
55
+ }
56
+ const category = store.category;
57
+ const route = store.route;
58
+ const menu = data.menu;
59
+ if (category && route && menu) {
60
+ store.menu = new MockMenuService(menu, store);
61
+ }
62
+ for (const key in data) {
63
+ if (PAGES.includes(key)) {
64
+ let service;
65
+ if (route) {
66
+ // new page
67
+ service = new MockPageService(data[key], store);
68
+ } else {
69
+ // !!! new collection should throw
70
+ service = new MockService(data[key], store);
71
+ }
72
+ store[key] = service;
73
+ }
74
+ }
75
+ staticCollections.forEach(config => {
76
+ store[config.slug] = new StaticService(config, store);
77
+ });
78
+ return store as T;
79
+ }
80
+
File without changes
@@ -0,0 +1,82 @@
1
+ import { httpDelete, httpGet, httpPost } from '@websolutespa/bom-core';
2
+ import { apiHandler } from '@websolutespa/bom-mixer-store';
3
+ import { IronSessionOptions } from 'iron-session/edge';
4
+ import { withIronSessionApiRoute } from 'iron-session/next';
5
+ import { NextApiRequest, NextApiResponse } from 'next';
6
+ import { StateStorage } from 'zustand/middleware';
7
+
8
+ function sessionGet(request: NextApiRequest, response: NextApiResponse<string | null>) {
9
+ const { key } = request.query;
10
+ const session: { [key: string]: string | null } = request.session as any;
11
+ if (session[key as string]) {
12
+ response.status(200).json(session[key as string]);
13
+ } else {
14
+ response.status(200).json(null);
15
+ }
16
+ }
17
+
18
+ async function sessionPost(request: NextApiRequest, response: NextApiResponse<null>) {
19
+ const { key } = request.query;
20
+ const body = request.body;
21
+ const session: { [key: string]: string | null } = request.session as any;
22
+ // console.log('body', body);
23
+ session[key as string] = body.value;
24
+ // console.log('session', session);
25
+ await request.session.save();
26
+ response.status(200).json(null);
27
+ }
28
+
29
+ async function sessionDelete(request: NextApiRequest, response: NextApiResponse<null>) {
30
+ const { key } = request.query;
31
+ const session: { [key: string]: string | null } = request.session as any;
32
+ session[key as string] = null;
33
+ await request.session.save();
34
+ response.status(200).json(null);
35
+ }
36
+
37
+ export type ISessionHandler = (request: NextApiRequest, response: NextApiResponse) => Promise<void>;
38
+
39
+ export function sessionHandler<T extends {}>(sessionOptions: IronSessionOptions) {
40
+ return apiHandler({
41
+ get: withIronSessionApiRoute(sessionGet, sessionOptions) as ISessionHandler,
42
+ post: withIronSessionApiRoute(sessionPost, sessionOptions) as ISessionHandler,
43
+ delete: withIronSessionApiRoute(sessionDelete, sessionOptions) as ISessionHandler,
44
+ });
45
+ }
46
+
47
+ export const IronSessionStorage: StateStorage = {
48
+ getItem: async (name: string) => {
49
+ const value = await httpGet(`/api/__session/${name}`);
50
+ // console.log('IronSessionStorage.getItem', name, value);
51
+ return value;
52
+ },
53
+ setItem: async (name: string, value: string) => {
54
+ // console.log('IronSessionStorage.setItem', name, { cart: value });
55
+ await httpPost(`/api/__session/${name}`, { value });
56
+ },
57
+ removeItem: async (name: string) => {
58
+ // console.log('IronSessionStorage.removeItem', name);
59
+ await httpDelete(`/api/__session/${name}`);
60
+ },
61
+ };
62
+
63
+ /*
64
+ import { withIronSessionSsr } from 'iron-session/next';
65
+
66
+ type IronSessionCallback<P> = (context: GetServerSidePropsContext) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
67
+ type IronSessionHandlerResult<P> = (context: GetServerSidePropsContext) => Promise<GetServerSidePropsResult<P>>;
68
+
69
+ export function withIronSession<P extends { [key: string]: unknown; } = { [key: string]: unknown; }>(
70
+ callback: IronSessionCallback<P>,
71
+ options: IronSessionOptions,
72
+ ): IronSessionHandlerResult<P> {
73
+ return withIronSessionSsr(async function (context) {
74
+ // !!! hack for vercel lambdas
75
+ if (storeStrategy === StoreStrategy.Mock) {
76
+ const pathname = path.join(process.cwd(), '.mixer', 'store.json');
77
+ const data = await fs.readFile(pathname, 'utf8');
78
+ }
79
+ return callback(context);
80
+ }, options);
81
+ }
82
+ */