abmp-npm 1.8.1 → 1.8.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,209 @@
1
+ const geohash = require('ngeohash');
2
+
3
+ const { COLLECTIONS, MEMBERS_FIELDS } = require('../public/consts.js');
4
+ const { findMainAddress } = require('../public/Utils/sharedUtils.js');
5
+ const { calculateDistance, shuffleArray } = require('../public/Utils/sharedUtils.js');
6
+
7
+ const { PRECISION, MAX__MEMBERS_SEARCH_RESULTS, WIX_QUERY_MAX_LIMIT } = require('./consts.js');
8
+ const { wixData } = require('./elevated-modules');
9
+
10
+ function buildMembersSearchQuery(data) {
11
+ console.log('data: ', JSON.stringify(data));
12
+ const { filter, isSearchingNearby, includeStudents = false } = data;
13
+ const isUserLocationEnabled = filter.latitude !== 0 || filter.longitude !== 0;
14
+ filter.searchText = filter.searchText || '';
15
+ filter.stateSearch = filter.stateSearch || '';
16
+ filter.practiceAreasSearch = filter.practiceAreasSearch || '';
17
+ filter.practiceAreas = filter.practiceAreas || [];
18
+ filter.state = filter.state || [];
19
+ filter.citySearch = filter.citySearch || '';
20
+ filter.city = filter.city || [];
21
+ filter.latitude = filter.latitude || 0;
22
+ filter.longitude = filter.longitude || 0;
23
+ filter.postalcode = filter.postalcode || '';
24
+ return {
25
+ get: () => {
26
+ let query = wixData
27
+ .query(COLLECTIONS.MEMBERS_DATA)
28
+ .ne('optOut', true)
29
+ .ne('action', 'drop')
30
+ .eq('isVisible', true);
31
+ let filterConfig = [
32
+ {
33
+ filterKey: 'practiceAreas',
34
+ queryMethod: 'hasSome',
35
+ queryField: 'areasOfPractices',
36
+ condition: value => value && value.length > 0,
37
+ fallback: {
38
+ filterKey: 'practiceAreasSearch',
39
+ queryMethod: 'contains',
40
+ queryField: 'areasOfPractices',
41
+ condition: value => value && value.trim() !== '',
42
+ },
43
+ },
44
+ {
45
+ filterKey: 'postalcode',
46
+ queryMethod: 'contains',
47
+ queryField: 'addresses.postalcode',
48
+ condition: value => value && value.trim() !== '',
49
+ },
50
+ {
51
+ filterKey: 'state',
52
+ queryMethod: 'hasSome',
53
+ queryField: 'addresses.state',
54
+ condition: value => value && value.length > 0,
55
+ fallback: {
56
+ filterKey: 'stateSearch',
57
+ queryMethod: 'contains',
58
+ queryField: 'addresses.state',
59
+ condition: value => value && value.trim() !== '',
60
+ },
61
+ },
62
+ {
63
+ filterKey: 'city',
64
+ queryMethod: 'hasSome',
65
+ queryField: 'addresses.city',
66
+ condition: value => value && value.length > 0,
67
+ fallback: {
68
+ filterKey: 'citySearch',
69
+ queryMethod: 'contains',
70
+ queryField: 'addresses.city',
71
+ condition: value => value && value.trim() !== '',
72
+ },
73
+ },
74
+ ];
75
+ //Ignore state, city and postal code when isSearchingNearby is true
76
+ if (isSearchingNearby) {
77
+ filterConfig = filterConfig.filter(
78
+ config => !['state', 'city', 'postalcode'].includes(config.filterKey)
79
+ );
80
+ }
81
+ const applyFilterToQuery = (query, config, filter) => {
82
+ const filterValue = filter[config.filterKey];
83
+ if (config.condition(filterValue)) {
84
+ return query[config.queryMethod](config.queryField, filterValue);
85
+ } else if (config.fallback) {
86
+ return applyFilterToQuery(query, config.fallback, filter);
87
+ }
88
+ return query;
89
+ };
90
+ // Apply filters using the configuration
91
+ filterConfig.forEach(config => {
92
+ query = applyFilterToQuery(query, config, filter);
93
+ });
94
+ if (isUserLocationEnabled && isSearchingNearby) {
95
+ const userGeohash = geohash.encode(filter.latitude, filter.longitude, PRECISION);
96
+ const neighborGeohashes = geohash.neighbors(userGeohash);
97
+ const geohashList = [userGeohash, ...neighborGeohashes];
98
+ query = query.hasSome('locHash', geohashList);
99
+ }
100
+ if (filter.searchText.trim() !== '') {
101
+ query = query.contains('fullName', filter.searchText);
102
+ }
103
+ if (!includeStudents) {
104
+ query = query.ne('memberships.membertype', 'Student');
105
+ }
106
+ return query;
107
+ },
108
+ run: async query => {
109
+ const baseQuery = query.ascending('firstName').fields(...Object.values(MEMBERS_FIELDS));
110
+ const getRandomSkip = totalCount => {
111
+ let randomSkip = 0;
112
+ if (totalCount > MAX__MEMBERS_SEARCH_RESULTS) {
113
+ const maxSkip = totalCount - MAX__MEMBERS_SEARCH_RESULTS;
114
+ randomSkip = Math.floor(Math.random() * (maxSkip + 1));
115
+ }
116
+ return randomSkip;
117
+ };
118
+ const getResult = async query => {
119
+ if (isSearchingNearby) {
120
+ return fetchAllItemsInParallel(baseQuery);
121
+ }
122
+ const totalCount = await query.count();
123
+ const randomSkip = getRandomSkip(totalCount);
124
+ const result = await query
125
+ .skip(randomSkip)
126
+ .limit(MAX__MEMBERS_SEARCH_RESULTS)
127
+ .find({ omitTotalCount: true });
128
+
129
+ // Shuffle the result items for additional randomization
130
+ return {
131
+ ...result,
132
+ items: shuffleArray(result.items),
133
+ };
134
+ };
135
+
136
+ const result = await getResult(baseQuery);
137
+ if (isUserLocationEnabled) {
138
+ const withDistances = result.items.map(item => ({
139
+ ...item,
140
+ distance: calculateDistance(
141
+ {
142
+ latitude: filter.latitude,
143
+ longitude: filter.longitude,
144
+ },
145
+ findMainAddress(item.addressDisplayOption, item.addresses)
146
+ ),
147
+ }));
148
+ const resultWithDistances = {
149
+ ...result,
150
+ items: withDistances,
151
+ };
152
+ if (isSearchingNearby) {
153
+ return {
154
+ ...resultWithDistances,
155
+ items: withDistances
156
+ .filter(item => item.distance !== null)
157
+ .sort((a, b) => a.distance - b.distance)
158
+ .slice(0, MAX__MEMBERS_SEARCH_RESULTS),
159
+ };
160
+ }
161
+ return resultWithDistances;
162
+ }
163
+ return result;
164
+ },
165
+ };
166
+ }
167
+
168
+ // Generic parallel fetch function for large datasets
169
+ async function fetchAllItemsInParallel(query) {
170
+ const batchSize = WIX_QUERY_MAX_LIMIT;
171
+ const allItems = [];
172
+
173
+ const firstResult = await query.skip(0).limit(batchSize).find();
174
+
175
+ const totalBatches = firstResult.totalPages;
176
+ allItems.push(...firstResult.items);
177
+
178
+ if (totalBatches > 1) {
179
+ // Create parallel promises for all remaining batches
180
+ const batchPromises = [];
181
+ for (let i = 1; i < totalBatches; i++) {
182
+ const skip = i * batchSize;
183
+ const promise = query
184
+ .skip(skip)
185
+ .limit(batchSize)
186
+ .find()
187
+ .then(result => result.items);
188
+ batchPromises.push(promise);
189
+ }
190
+
191
+ // Execute all batches in parallel
192
+ const batchResults = await Promise.all(batchPromises);
193
+ for (const items of batchResults) {
194
+ if (items.length > 0) {
195
+ allItems.push(...items);
196
+ }
197
+ }
198
+ }
199
+
200
+ return {
201
+ ...firstResult,
202
+ items: allItems,
203
+ };
204
+ }
205
+
206
+ module.exports = {
207
+ buildMembersSearchQuery,
208
+ fetchAllItemsInParallel,
209
+ };
package/backend/consts.js CHANGED
@@ -8,6 +8,13 @@ const CONFIG_KEYS = {
8
8
  SITE_ASSOCIATION: 'SITE_ASSOCIATION',
9
9
  };
10
10
 
11
+ const PRECISION = 3;
12
+ const MAX__MEMBERS_SEARCH_RESULTS = 120;
13
+ const WIX_QUERY_MAX_LIMIT = 1000;
14
+
11
15
  module.exports = {
12
16
  CONFIG_KEYS,
17
+ PRECISION,
18
+ MAX__MEMBERS_SEARCH_RESULTS,
19
+ WIX_QUERY_MAX_LIMIT,
13
20
  };
@@ -1,4 +1,4 @@
1
- const { COLLECTIONS } = require('../public');
1
+ const { COLLECTIONS } = require('../public/consts');
2
2
 
3
3
  const { triggerAutomation } = require('./automations-methods');
4
4
  const { CONFIG_KEYS } = require('./consts');
package/backend/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  module.exports = {
2
2
  ...require('./forms-methods'),
3
+ ...require('./search-filters-methods'),
3
4
  };
@@ -1,4 +1,4 @@
1
- const { COLLECTIONS } = require('../public');
1
+ const { COLLECTIONS } = require('../public/consts');
2
2
 
3
3
  const { wixData } = require('./elevated-modules');
4
4
  const { createSiteMember } = require('./members-area-methods');
@@ -0,0 +1,121 @@
1
+ const { COLLECTIONS } = require('../public/consts');
2
+
3
+ const { buildMembersSearchQuery } = require('./cms-data-methods');
4
+ const { wixData } = require('./elevated-modules');
5
+ const { retrieveAllItems } = require('./utils');
6
+
7
+ const getCompiledFiltersOptions = () =>
8
+ wixData.get(COLLECTIONS.COMPILED_STATE_CITY_MAP, 'SINGLE_ITEM_ID');
9
+
10
+ const getNonCompiledFiltersOptions = async () => {
11
+ const [completeStateList, areasOfPracticesList, stateCityMapList] = await Promise.all([
12
+ getCompleteStateList(),
13
+ getAreasOfPracticeList(),
14
+ getStateCityMap(),
15
+ ]);
16
+ return { completeStateList, areasOfPracticesList, stateCityMapList };
17
+ };
18
+ const filterProfiles = async data => {
19
+ const membersSearchQuery = buildMembersSearchQuery({ ...data, includeStudents: false });
20
+ const query = await membersSearchQuery.get();
21
+ return membersSearchQuery.run(query);
22
+ };
23
+
24
+ async function getAreasOfPracticeList() {
25
+ const interestsData = await retrieveAllItems(COLLECTIONS.INTERESTS);
26
+ return interestsData.map(({ title }) => ({ label: title, value: title }));
27
+ }
28
+
29
+ async function getStateCityMap() {
30
+ const getAllCitiesMap = async () => {
31
+ const totalCount = await wixData.query(COLLECTIONS.STATE_CITY_MAP).count();
32
+ const baseQuery = wixData.query(COLLECTIONS.STATE_CITY_MAP).limit(1000);
33
+ const batchedQueries = Array.from({ length: Math.ceil(totalCount / 1000) }, (_, i) =>
34
+ baseQuery.skip(i * 1000)
35
+ );
36
+ const allCities = await Promise.all(
37
+ batchedQueries.map(query => query.find({ omitTotalCount: true }).then(res => res.items))
38
+ );
39
+ return allCities.flat();
40
+ };
41
+ const allCities = await getAllCitiesMap();
42
+ const stateCityMap = new Map();
43
+ allCities.forEach(cityObj => {
44
+ const state = cityObj.stateText;
45
+ const city = cityObj.title;
46
+ const cityArray = stateCityMap.has(state) ? [...stateCityMap.get(state), city] : [city];
47
+ stateCityMap.set(state, cityArray);
48
+ });
49
+ return Object.fromEntries(stateCityMap);
50
+ }
51
+ //Besan comments on moved code: Below code assumes that Velo can cache value, however this isn't valid so it needs adjustment
52
+ // Cache for state list
53
+ let stateListCache = null;
54
+ let lastCacheTime = 0;
55
+ const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
56
+
57
+ async function getCompleteStateList() {
58
+ // Check if we have a valid cache
59
+ const now = Date.now();
60
+ if (stateListCache && now - lastCacheTime < CACHE_DURATION) {
61
+ return stateListCache;
62
+ }
63
+
64
+ // 1. fire both queries at once:
65
+ const [stateRecords, memberStates] = await Promise.all([
66
+ retrieveAllItems(COLLECTIONS.STATE),
67
+ getMembersStateList(), //this method a
68
+ ]);
69
+
70
+ const fullNameByCode = new Map(stateRecords.map(st => [st.title, st.name]));
71
+
72
+ const states = memberStates.map(({ value: code }) => ({
73
+ value: code,
74
+ label: fullNameByCode.get(code) || code,
75
+ }));
76
+
77
+ // Update cache
78
+ stateListCache = states;
79
+ lastCacheTime = now;
80
+
81
+ return states;
82
+ }
83
+ //Besan Comments on moved code: THIS WILL IGNORE SOME STATES VALUES while we should return all states AFAIU
84
+ async function getMembersStateList() {
85
+ try {
86
+ // Use a more efficient query to get only the addresses field
87
+ const results = await wixData
88
+ .query(COLLECTIONS.MEMBERS_DATA)
89
+ .fields('addresses')
90
+ .limit(1000)
91
+ .find();
92
+
93
+ // Extract all state codes from addresses
94
+ const allStates = new Set();
95
+ results.items.forEach(member => {
96
+ if (Array.isArray(member.addresses)) {
97
+ member.addresses.forEach(addr => {
98
+ if (addr.state) allStates.add(addr.state);
99
+ });
100
+ }
101
+ });
102
+
103
+ // Convert to array and sort
104
+ const uniqueStates = Array.from(allStates).sort();
105
+
106
+ // Map to label/value format
107
+ return uniqueStates.map(stateCode => ({
108
+ label: stateCode,
109
+ value: stateCode,
110
+ }));
111
+ } catch (error) {
112
+ console.error('Error in getMembersStateList:', error);
113
+ return [];
114
+ }
115
+ }
116
+
117
+ module.exports = {
118
+ getCompiledFiltersOptions,
119
+ getNonCompiledFiltersOptions,
120
+ filterProfiles,
121
+ };
package/backend/utils.js CHANGED
@@ -29,6 +29,17 @@ const getSiteConfigs = async configKey => {
29
29
  return siteConfigs;
30
30
  };
31
31
 
32
+ const retrieveAllItems = async collectionName => {
33
+ let results = await wixData.query(collectionName).limit(1000).find();
34
+ let allItems = results.items;
35
+ while (results.hasNext()) {
36
+ results = await results.next();
37
+ allItems = allItems.concat(results.items);
38
+ }
39
+ return allItems;
40
+ };
41
+
32
42
  module.exports = {
33
43
  getSiteConfigs,
44
+ retrieveAllItems,
34
45
  };
package/eslint.config.js CHANGED
@@ -54,7 +54,13 @@ module.exports = [
54
54
  alphabetize: { order: 'asc', caseInsensitive: true },
55
55
  },
56
56
  ],
57
- 'import/no-unresolved': 'error',
57
+ 'import/no-unresolved': [
58
+ 'error',
59
+ {
60
+ commonjs: true,
61
+ caseSensitive: true,
62
+ },
63
+ ],
58
64
  'import/no-duplicates': 'error',
59
65
  'import/no-commonjs': 'off',
60
66
  'import/no-dynamic-require': 'off',
@@ -88,6 +94,7 @@ module.exports = [
88
94
  'import/resolver': {
89
95
  node: {
90
96
  extensions: ['.js', '.jsx', '.ts', '.tsx'],
97
+ moduleDirectory: ['node_modules', 'public', 'backend', 'pages'],
91
98
  },
92
99
  },
93
100
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abmp-npm",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -17,6 +17,7 @@
17
17
  "@eslint/js": "^9.12.0",
18
18
  "eslint": "^9.12.0",
19
19
  "eslint-config-prettier": "^9.1.0",
20
+ "eslint-import-resolver-node": "^0.3.9",
20
21
  "eslint-plugin-import": "^2.30.0",
21
22
  "eslint-plugin-prettier": "^5.2.1",
22
23
  "eslint-plugin-promise": "^7.1.0",
@@ -30,7 +31,10 @@
30
31
  "@wix/data": "^1.0.303",
31
32
  "@wix/essentials": "^0.1.28",
32
33
  "@wix/members": "^1.0.330",
34
+ "@wix/site-location": "^1.31.0",
33
35
  "@wix/site-window": "^1.44.0",
34
- "phone": "^3.1.67"
36
+ "ngeohash": "^0.6.3",
37
+ "phone": "^3.1.67",
38
+ "psdev-utils": "1.1.1"
35
39
  }
36
40
  }