@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,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
+ }
@@ -0,0 +1,223 @@
1
+ import { IEntity } from '@websolutespa/bom-core';
2
+
3
+ export type IField = {
4
+ name: string,
5
+ type: 'text' | 'number' | 'relationship',
6
+ localized?: boolean,
7
+ relationTo?: string,
8
+ hasMany?: boolean,
9
+ required?: boolean
10
+ };
11
+
12
+ export type MapTo = Record<string, string>;
13
+
14
+ export type StaticServiceSrc<T extends IEntity = IEntity> = string | T[] | ((config: StaticServiceConfig<T>) => T[]) | ((config: StaticServiceConfig<T>) => Promise<T[]>);
15
+
16
+ export type StaticServiceConfig<T extends IEntity = IEntity> = {
17
+ src: StaticServiceSrc<T>;
18
+ slug: string;
19
+ fields: IField[];
20
+ map?: MapTo;
21
+ };
22
+
23
+ export const staticCollections: StaticServiceConfig[] = [{
24
+ src: 'https://actarian.github.io/cms-i18n-react/api/continent.json',
25
+ slug: 'i18n_continent',
26
+ fields: [
27
+ {
28
+ name: 'id',
29
+ type: 'text',
30
+ },
31
+ {
32
+ name: 'name',
33
+ type: 'text',
34
+ },
35
+ ],
36
+ }, {
37
+ src: 'https://actarian.github.io/cms-i18n-react/api/country.json',
38
+ slug: 'i18n_country',
39
+ fields: [
40
+ {
41
+ name: 'id',
42
+ type: 'text',
43
+ },
44
+ {
45
+ name: 'name',
46
+ type: 'text',
47
+ localized: true,
48
+ },
49
+ {
50
+ name: 'englishName',
51
+ type: 'text',
52
+ },
53
+ {
54
+ name: 'nativeName',
55
+ type: 'text',
56
+ },
57
+ {
58
+ name: 'alpha2Code',
59
+ type: 'text',
60
+ },
61
+ {
62
+ name: 'alpha3Code',
63
+ type: 'text',
64
+ },
65
+ {
66
+ name: 'continent',
67
+ type: 'relationship',
68
+ relationTo: 'i18n_continent',
69
+ hasMany: false,
70
+ required: true,
71
+ },
72
+ {
73
+ name: 'subContinent',
74
+ type: 'relationship',
75
+ relationTo: 'i18n_sub_continent',
76
+ hasMany: false,
77
+ required: true,
78
+ },
79
+ ],
80
+ }, {
81
+ src: 'https://actarian.github.io/cms-i18n-react/api/countryZone.json',
82
+ slug: 'i18n_country_zone',
83
+ fields: [
84
+ {
85
+ name: 'id',
86
+ type: 'text',
87
+ },
88
+ {
89
+ name: 'name',
90
+ type: 'text',
91
+ },
92
+ ],
93
+ }, {
94
+ src: 'https://actarian.github.io/cms-i18n-react/api/municipality.json',
95
+ slug: 'i18n_municipality',
96
+ fields: [
97
+ {
98
+ name: 'id',
99
+ type: 'text',
100
+ },
101
+ {
102
+ name: 'name',
103
+ type: 'text',
104
+ },
105
+ {
106
+ name: 'code',
107
+ type: 'text',
108
+ },
109
+ {
110
+ name: 'country',
111
+ type: 'relationship',
112
+ relationTo: 'i18n_country',
113
+ hasMany: false,
114
+ required: true,
115
+ },
116
+ {
117
+ name: 'countryZone',
118
+ type: 'relationship',
119
+ relationTo: 'i18n_country_zone',
120
+ hasMany: false,
121
+ required: true,
122
+ },
123
+ {
124
+ name: 'region',
125
+ type: 'relationship',
126
+ relationTo: 'i18n_region',
127
+ hasMany: false,
128
+ required: true,
129
+ },
130
+ {
131
+ name: 'province',
132
+ type: 'relationship',
133
+ relationTo: 'i18n_province',
134
+ hasMany: false,
135
+ required: true,
136
+ },
137
+ ],
138
+ }, {
139
+ src: 'https://actarian.github.io/cms-i18n-react/api/province.json',
140
+ slug: 'i18n_province',
141
+ fields: [
142
+ {
143
+ name: 'id',
144
+ type: 'text',
145
+ },
146
+ {
147
+ name: 'name',
148
+ type: 'text',
149
+ },
150
+ {
151
+ name: 'code',
152
+ type: 'text',
153
+ },
154
+ {
155
+ name: 'country',
156
+ type: 'relationship',
157
+ relationTo: 'i18n_country',
158
+ hasMany: false,
159
+ required: true,
160
+ },
161
+ {
162
+ name: 'countryZone',
163
+ type: 'relationship',
164
+ relationTo: 'i18n_country_zone',
165
+ hasMany: false,
166
+ required: true,
167
+ },
168
+ {
169
+ name: 'region',
170
+ type: 'relationship',
171
+ relationTo: 'i18n_region',
172
+ hasMany: false,
173
+ required: true,
174
+ },
175
+ ],
176
+ }, {
177
+ src: 'https://actarian.github.io/cms-i18n-react/api/region.json',
178
+ slug: 'i18n_region',
179
+ fields: [
180
+ {
181
+ name: 'id',
182
+ type: 'text',
183
+ },
184
+ {
185
+ name: 'name',
186
+ type: 'text',
187
+ },
188
+ {
189
+ name: 'country',
190
+ type: 'relationship',
191
+ relationTo: 'i18n_country',
192
+ hasMany: false,
193
+ required: true,
194
+ },
195
+ {
196
+ name: 'countryZone',
197
+ type: 'relationship',
198
+ relationTo: 'i18n_country_zone',
199
+ hasMany: false,
200
+ required: true,
201
+ },
202
+ ],
203
+ }, {
204
+ src: 'https://actarian.github.io/cms-i18n-react/api/subContinent.json',
205
+ slug: 'i18n_sub_continent',
206
+ fields: [
207
+ {
208
+ name: 'id',
209
+ type: 'text',
210
+ },
211
+ {
212
+ name: 'name',
213
+ type: 'text',
214
+ },
215
+ {
216
+ name: 'continent',
217
+ type: 'relationship',
218
+ relationTo: 'i18n_continent',
219
+ hasMany: false,
220
+ required: true,
221
+ },
222
+ ],
223
+ }];
@@ -0,0 +1,176 @@
1
+ import { IRoute, IStore, asEquatable, qsDeserialize } from '@websolutespa/bom-core';
2
+ import { apiHandler } from '@websolutespa/bom-mixer-store';
3
+ import { NextApiRequest, NextApiResponse } from 'next';
4
+ import { COLLECTIONS, getStore } from './store.service';
5
+
6
+ export function storeHandler<T extends IStore>(storePromise: () => Promise<T> = async () => await getStore<T>()) {
7
+ return apiHandler({
8
+ // find a specific document by id or
9
+ // find paginated documents
10
+ get: async (request: NextApiRequest, response: NextApiResponse) => {
11
+ try {
12
+ const { collections, ...query } = request.query;
13
+ // console.log('query', query);
14
+ if (!Array.isArray(collections) || collections?.length === 0) {
15
+ console.error('method not allowed', collections);
16
+ return response.status(405).send('method not allowed');
17
+ }
18
+ const collection: string = collections[0];
19
+ const id: string = collections[1];
20
+ const qs = Object.entries(query).map(kv => kv.join('=')).join('&');
21
+ const params = qsDeserialize(qs);
22
+ // console.log('params', params);
23
+ if (!COLLECTIONS.includes(collection)) {
24
+ console.error('method not allowed', collection);
25
+ return response.status(405).send('method not allowed');
26
+ }
27
+ const store = await storePromise();
28
+ const service = store[collection];
29
+ if (!service) {
30
+ console.error('method not allowed', collection);
31
+ return response.status(405).send('method not allowed');
32
+ }
33
+ // find a specific document by id
34
+ if (id) {
35
+ // console.log('getStoreDetail', collection, id, params);
36
+ const item = await service.findOne(asEquatable(id), params);
37
+ if (item) {
38
+ return response.status(200).json(item);
39
+ } else {
40
+ return response.status(404).send('not found');
41
+ }
42
+ // find paginated documents
43
+ } else if (String(params.pagination) === 'true') {
44
+ // console.log('getStoreIndex', collection, params);
45
+ const pagination = await service.findPaged(params);
46
+ return response.status(200).json(pagination);
47
+ } else {
48
+ // console.log('getStoreIndex', collection, params);
49
+ const items = await service.findMany(params);
50
+ return response.status(200).json(items);
51
+ }
52
+ } catch (error: any) {
53
+ console.error('storeHandler.error.500', error);
54
+ throw error;
55
+ }
56
+ },
57
+ // create a new document
58
+ post: async (request: NextApiRequest, response: NextApiResponse) => {
59
+ try {
60
+ const { collections, ...query } = request.query;
61
+ if (!Array.isArray(collections) || collections.length !== 1) {
62
+ console.error('method not allowed', collections);
63
+ return response.status(405).send('method not allowed');
64
+ }
65
+ const collection: string = collections[0];
66
+ const store = await storePromise();
67
+ const service = store[collection];
68
+ if (!service) {
69
+ console.error('method not allowed', collection);
70
+ return response.status(405).send('method not allowed');
71
+ }
72
+ // api post route pathname
73
+ if (collection === 'route' && request.body.pathname) {
74
+ try {
75
+ const { pathname } = request.body;
76
+ // here we use findMany because route id would break api url
77
+ const routes = await store.route.findMany<IRoute>({
78
+ where: {
79
+ id: {
80
+ equals: pathname,
81
+ }
82
+ }
83
+ });
84
+ // let's try if match splat routes
85
+ if (routes.length === 0) {
86
+ const splatRoutes = await store.route.findMany<IRoute>({
87
+ where: {
88
+ useSplat: {
89
+ equals: true,
90
+ }
91
+ }
92
+ });
93
+ // console.log('splatRoutes', splatRoutes);
94
+ // find best match splat route
95
+ const route = splatRoutes.reduce((p: (IRoute & { splat: string }) | undefined, c: IRoute) => {
96
+ const match = pathname.indexOf(c.id) === 0;
97
+ if (match && (!p || p.id.length < c.id.length)) {
98
+ return { ...c, splat: pathname.substring(c.id.length, pathname.length) };
99
+ } else {
100
+ return p;
101
+ }
102
+ }, undefined);
103
+ if (route) {
104
+ return response.status(200).json(route);
105
+ }
106
+ }
107
+ // console.log('pathname', pathname);
108
+ if (routes.length > 0) {
109
+ return response.status(200).json(routes[0]);
110
+ } else {
111
+ // console.error('not found', collection);
112
+ return response.status(404).send('not found');
113
+ }
114
+ } catch (error: any) {
115
+ if (error.status === 404) {
116
+ // console.error('not found', collection);
117
+ return response.status(404).send('not found');
118
+ } else {
119
+ console.error('storeHandler.error.500', error);
120
+ throw error;
121
+ }
122
+ }
123
+ }
124
+ } catch (error: any) {
125
+ console.error('storeHandler.error.500', error);
126
+ throw error;
127
+ }
128
+ },
129
+ // update a document by id
130
+ patch: async (request: NextApiRequest, response: NextApiResponse) => {
131
+ try {
132
+ const params = request.query;
133
+ const { collections, ...query } = params;
134
+ if (!Array.isArray(collections) || collections.length !== 2) {
135
+ console.error('method not allowed', collections);
136
+ return response.status(405).send('method not allowed');
137
+ }
138
+ const collection: string = collections[0];
139
+ const id: string = collections[1];
140
+ const store = await storePromise();
141
+ const service = store[collection];
142
+ if (!service) {
143
+ console.error('method not allowed', collection);
144
+ return response.status(405).send('method not allowed');
145
+ }
146
+ // !!! todo patch
147
+ } catch (error: any) {
148
+ console.error('storeHandler.error.500', error);
149
+ throw error;
150
+ }
151
+ },
152
+ // delete an existing document by id
153
+ delete: async (request: NextApiRequest, response: NextApiResponse) => {
154
+ try {
155
+ const params = request.query;
156
+ const { collections, ...query } = params;
157
+ if (!Array.isArray(collections) || collections.length !== 2) {
158
+ console.error('method not allowed', collections);
159
+ return response.status(405).send('method not allowed');
160
+ }
161
+ const collection: string = collections[0];
162
+ const id: string = collections[1];
163
+ const store = await storePromise();
164
+ const service = store[collection];
165
+ if (!service) {
166
+ console.error('method not allowed', collection);
167
+ return response.status(405).send('method not allowed');
168
+ }
169
+ // !!! todo delete
170
+ } catch (error: any) {
171
+ console.error('storeHandler.error.500', error);
172
+ throw error;
173
+ }
174
+ },
175
+ });
176
+ }
@@ -0,0 +1,34 @@
1
+ import { IStore } from '@websolutespa/bom-core';
2
+ import { getMockStoreServer } from '../mock/mock.store';
3
+ import { staticCollections } from '../static/static';
4
+ import { getApiStore } from '../storeApi/storeApi.store';
5
+ import { StoreStrategy, storeStrategy } from './store';
6
+
7
+ const ENV_COLLECTIONS: string[] = process.env.COLLECTIONS ? process.env.COLLECTIONS.split(',') : [];
8
+ export const MOCK_COLLECTIONS: string[] = process.env.MOCKS ? process.env.MOCKS.split(',') : [];
9
+ export const STATIC_COLLECTIONS: string[] = staticCollections.map(x => x.slug);
10
+ export const KNOWN_COLLECTIONS: string[] = [
11
+ 'category',
12
+ 'label',
13
+ 'locale',
14
+ 'market',
15
+ 'menu',
16
+ 'template',
17
+ 'page',
18
+ 'route',
19
+ ...STATIC_COLLECTIONS,
20
+ ];
21
+ export const COLLECTIONS = [...ENV_COLLECTIONS, ...KNOWN_COLLECTIONS];
22
+
23
+ export async function getStore<T extends IStore>(): Promise<T> {
24
+ switch (storeStrategy) {
25
+ case StoreStrategy.Api:
26
+ if (COLLECTIONS.length === 0) {
27
+ console.error('StoreService.getStore no collections');
28
+ throw ('StoreService.getStore no collections');
29
+ }
30
+ return await getApiStore<T>(COLLECTIONS);
31
+ default:
32
+ return await getMockStoreServer<T>();
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ export enum StoreStrategy {
2
+ Api = 'api',
3
+ Data = 'data',
4
+ Mock = 'mock',
5
+ }
6
+
7
+ function getStoreStrategy(): StoreStrategy {
8
+ let storeStrategy = StoreStrategy.Mock;
9
+ if (process && process.env.STORE_STRATEGY) {
10
+ storeStrategy = process.env.STORE_STRATEGY as StoreStrategy;
11
+ }
12
+ return storeStrategy;
13
+ }
14
+
15
+ export const storeStrategy: StoreStrategy = getStoreStrategy();