@websolutespa/bom-mixer-store 0.2.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.
@@ -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
+ }
@@ -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
+ */
@@ -0,0 +1,102 @@
1
+ import {
2
+ IEntity, IEquatable, IStore, JsonService, QueryParams,
3
+ httpGet, localizeItem
4
+ } from '@websolutespa/bom-core';
5
+ import { IField, MapTo, StaticServiceConfig } from './static';
6
+
7
+ export class StaticService<T extends IEntity = IEntity> extends JsonService<T> {
8
+ loaded: boolean = false;
9
+ config: StaticServiceConfig<T>;
10
+ store: IStore;
11
+
12
+ constructor(config: StaticServiceConfig<T>, store: IStore) {
13
+ super([]);
14
+ this.config = config;
15
+ this.store = store;
16
+ }
17
+
18
+ async getItems<B extends T = T>(): Promise<B[]> {
19
+ let items = this.items;
20
+ if (this.loaded && items) {
21
+ return items as B[];
22
+ } else {
23
+ const config = this.config;
24
+ const src = config.src;
25
+ if (typeof src === 'string') {
26
+ const response = await httpGet(src, {});
27
+ items = response.items;
28
+ } else if (typeof src === 'function') {
29
+ if (src instanceof Promise) {
30
+ items = await src(config);
31
+ } else {
32
+ items = src(config) as T[];
33
+ }
34
+ } else {
35
+ items = src;
36
+ }
37
+ items = this.mapItems_(items, config.map);
38
+ this.items = items;
39
+ this.loaded = true;
40
+ return items as B[];
41
+ }
42
+ }
43
+
44
+ override async findOne<B extends T = T>(id: IEquatable, params: QueryParams = {}): Promise<B | undefined> {
45
+ let items = await this.getItems<B>();
46
+ params.where = params.where || {};
47
+ params.where.id = params.where.id || { equals: id };
48
+ items = this.whereItems_<B>(items, params.where);
49
+ if (items.length > 0) {
50
+ let item = items[0];
51
+ // relations
52
+ if (params.depth !== 0) {
53
+ item = await this.relatedItems_(item, this.config.fields);
54
+ }
55
+ return this.decorator_<B>(item, params);
56
+ } else {
57
+ console.log('StaticService.findOne.notfound', id, params);
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ protected mapItems_<B extends T = T>(items: B[], map?: MapTo): B[] {
63
+ if (typeof map === 'object') {
64
+ items = items.map(x => {
65
+ const item: Partial<B> = {};
66
+ Object.entries(x).forEach(([k, v]) => {
67
+ const key = map[k] || k;
68
+ (item as any)[key] = v;
69
+ });
70
+ return item as B;
71
+ });
72
+ }
73
+ return items;
74
+ }
75
+
76
+ // !!! todo make recursive by depth with traverse
77
+ protected async relatedItems_<B extends T = T>(item: B, fields: IField[], locale: string = 'en'): Promise<B> {
78
+ const x = { ...item };
79
+ for (const field of fields) {
80
+ if (field.type === 'relationship' && typeof field.relationTo === 'string') {
81
+ const name = field.name as keyof typeof item;
82
+ const items = await this.store[field.relationTo].findMany();
83
+ if (field.hasMany) {
84
+ const ids = x[name] as IEquatable[];
85
+ x[name] = items.filter(x => ids.indexOf(x.id) !== -1) as any;
86
+ } else {
87
+ const id = x[name];
88
+ x[name] = (items.find(x => x.id === id) || id) as any;
89
+ }
90
+ }
91
+ }
92
+ return x;
93
+ }
94
+
95
+ protected override decorator_<B extends T = T>(item: B, params: QueryParams = {}): B {
96
+ if (params.locale) {
97
+ item = localizeItem(item, params.locale);
98
+ }
99
+ return item;
100
+ }
101
+
102
+ }