feed-common 1.10.5 → 1.10.6
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/.prettierrc +10 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +7 -0
- package/dist/types/profile.types.d.ts +6 -6
- package/dist/utils/profile.d.ts +18 -2
- package/dist/utils/profile.d.ts.map +1 -1
- package/dist/utils/profile.js +55 -45
- package/dist/utils/profile.js.map +1 -1
- package/package.json +11 -4
- package/src/types/profile.types.ts +6 -6
- package/src/utils/profile.ts +258 -288
- package/tests/company.spec.ts +49 -0
- package/tests/profile.spec.ts +1458 -0
package/src/utils/profile.ts
CHANGED
@@ -1,180 +1,172 @@
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
1
2
|
import {
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
OptionType,
|
4
|
+
OptionTypeItem,
|
5
|
+
OptionTypeGroup,
|
6
|
+
ProductUploadProfile,
|
7
|
+
ProductUploadMapping,
|
8
|
+
ProductUploadRuleFilterType,
|
9
|
+
RuleOperatorsType,
|
10
|
+
ProductUploadRuleItem,
|
11
|
+
ProductUploadRuleSection,
|
12
|
+
ProductUploadRules,
|
12
13
|
} from '../types/profile.types.js';
|
13
14
|
import { ProductUploadMappings } from '../constants/profile.constants.js';
|
14
15
|
import { code } from './utils.js';
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
17
|
+
/**
|
18
|
+
* Check for rules duplication
|
19
|
+
* @param rules
|
20
|
+
* @returns
|
21
|
+
*/
|
22
|
+
export function checkRuleDuplication(rules: ProductUploadRuleItem[]): string[] {
|
23
|
+
return rules
|
24
|
+
.map(r => {
|
25
|
+
for (const rule of rules) {
|
26
|
+
if (
|
27
|
+
r.id !== rule.id &&
|
28
|
+
r.attribute === rule.attribute &&
|
29
|
+
r.operator === rule.operator &&
|
30
|
+
r.value === rule.value
|
31
|
+
) {
|
32
|
+
return r.id;
|
33
|
+
}
|
34
|
+
}
|
35
|
+
return null;
|
36
|
+
})
|
37
|
+
.filter(Boolean) as string[];
|
32
38
|
}
|
33
39
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
/**
|
41
|
+
* Check if rules mutually exclusive
|
42
|
+
* @param rules
|
43
|
+
* @returns
|
44
|
+
*/
|
45
|
+
export function checkRuleAlwaysness(rules: ProductUploadRuleItem[]): string[] {
|
46
|
+
return rules
|
47
|
+
.map(r => {
|
48
|
+
for (const rule of rules) {
|
49
|
+
if (isExclusive(r, rule)) {
|
50
|
+
return r.id;
|
51
|
+
}
|
52
|
+
}
|
53
|
+
return null;
|
54
|
+
})
|
55
|
+
.filter(Boolean) as string[];
|
45
56
|
}
|
46
57
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
const
|
54
|
-
const
|
55
|
-
|
56
|
-
for (let
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
58
|
+
/**
|
59
|
+
* Checks for contradictions in rules, e.g A == 1 and A != 1
|
60
|
+
* @param sections
|
61
|
+
* @returns
|
62
|
+
*/
|
63
|
+
export function checkRuleDeadlocks(input: ProductUploadRuleSection[]): string[] {
|
64
|
+
const totalList = [] as string[][];
|
65
|
+
const sections = structuredClone(input);
|
66
|
+
|
67
|
+
for (let i = 0; i < sections.length - 1; i++) {
|
68
|
+
const currentSection = sections[i];
|
69
|
+
|
70
|
+
for (let y = i + 1; y < sections.length; y++) {
|
71
|
+
from_start: for (let x = 0; x < currentSection.ruleItems.length; x++) {
|
72
|
+
const ruleA = currentSection.ruleItems[x];
|
73
|
+
for (let z = 0; z < sections[y].ruleItems.length; z++) {
|
74
|
+
const ruleB = sections[y].ruleItems[z];
|
75
|
+
if (isExclusive(ruleA, ruleB)) {
|
76
|
+
totalList.push([ruleA.id, ruleB.id]);
|
77
|
+
currentSection.ruleItems.splice(x, 1);
|
78
|
+
sections[y].ruleItems.splice(z, 1);
|
79
|
+
continue from_start;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
}
|
67
83
|
}
|
68
|
-
}
|
69
84
|
}
|
70
85
|
|
71
|
-
|
72
|
-
totalList = [...totalList, ...currentList];
|
73
|
-
}
|
74
|
-
}
|
75
|
-
|
76
|
-
return totalList;
|
86
|
+
return sections.some(s => s.ruleItems.length === 0) ? totalList.flat() : [];
|
77
87
|
}
|
78
88
|
|
79
|
-
function
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
rule1.operator ===
|
87
|
-
getOperatorCounterpart(rule2.operator as keyof RuleOperatorsType) &&
|
88
|
-
rule1.value === rule2.value
|
89
|
-
);
|
89
|
+
function isExclusive(rule1: ProductUploadRuleItem, rule2: ProductUploadRuleItem): boolean {
|
90
|
+
return (
|
91
|
+
rule1.id !== rule2.id &&
|
92
|
+
rule1.attribute === rule2.attribute &&
|
93
|
+
rule1.operator === getOperatorCounterpart(rule2.operator as keyof RuleOperatorsType) &&
|
94
|
+
rule1.value === rule2.value
|
95
|
+
);
|
90
96
|
}
|
91
97
|
|
92
|
-
function getOperatorCounterpart
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
throw new Error(`Unknown operator ${String(operator)}`);
|
126
|
-
}
|
98
|
+
export function getOperatorCounterpart(operator: keyof RuleOperatorsType): keyof RuleOperatorsType {
|
99
|
+
switch (operator) {
|
100
|
+
case 'equals':
|
101
|
+
return 'notEquals';
|
102
|
+
case 'notEquals':
|
103
|
+
return 'equals';
|
104
|
+
case 'contains':
|
105
|
+
return 'notContains';
|
106
|
+
case 'notContains':
|
107
|
+
return 'contains';
|
108
|
+
case 'startsWith':
|
109
|
+
return 'notStartsWith';
|
110
|
+
case 'notStartsWith':
|
111
|
+
return 'startsWith';
|
112
|
+
case 'endsWith':
|
113
|
+
return 'notEndsWith';
|
114
|
+
case 'notEndsWith':
|
115
|
+
return 'endsWith';
|
116
|
+
case 'in':
|
117
|
+
return 'notIn';
|
118
|
+
case 'notIn':
|
119
|
+
return 'in';
|
120
|
+
case 'greater':
|
121
|
+
return 'less';
|
122
|
+
case 'less':
|
123
|
+
return 'greater';
|
124
|
+
case 'greaterOrEqual':
|
125
|
+
return 'lessOrEqual';
|
126
|
+
case 'lessOrEqual':
|
127
|
+
return 'greaterOrEqual';
|
128
|
+
default:
|
129
|
+
throw new Error(`Unknown operator ${String(operator)}`);
|
130
|
+
}
|
127
131
|
}
|
128
132
|
|
129
|
-
export function getEmptyRuleItem
|
130
|
-
|
131
|
-
): ProductUploadRuleItem {
|
132
|
-
return { attribute, operator: '', value: '', id: code() };
|
133
|
+
export function getEmptyRuleItem(attribute: ProductUploadRuleFilterType['value'] = ''): ProductUploadRuleItem {
|
134
|
+
return { attribute, operator: '', value: '', id: code() };
|
133
135
|
}
|
134
136
|
|
135
|
-
export function sortMappings
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
(
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
return
|
164
|
-
}
|
165
|
-
|
166
|
-
if (a.attribute > b.attribute) {
|
167
|
-
return 1;
|
168
|
-
}
|
169
|
-
|
170
|
-
return 0;
|
137
|
+
export function sortMappings(a: ProductUploadMapping, b: ProductUploadMapping): number {
|
138
|
+
if (!a.attribute) {
|
139
|
+
return 1;
|
140
|
+
}
|
141
|
+
|
142
|
+
if (!b.attribute) {
|
143
|
+
return -1;
|
144
|
+
}
|
145
|
+
|
146
|
+
const sourceA = ProductUploadMappings.find(m => m.attribute === a.attribute);
|
147
|
+
const sourceB = ProductUploadMappings.find(m => m.attribute === b.attribute);
|
148
|
+
|
149
|
+
if (sourceA?.required && !sourceB?.required) {
|
150
|
+
return -1;
|
151
|
+
}
|
152
|
+
|
153
|
+
if (sourceB?.required && !sourceA?.required) {
|
154
|
+
return 1;
|
155
|
+
}
|
156
|
+
|
157
|
+
if (a.attribute < b.attribute) {
|
158
|
+
return -1;
|
159
|
+
}
|
160
|
+
|
161
|
+
if (a.attribute > b.attribute) {
|
162
|
+
return 1;
|
163
|
+
}
|
164
|
+
|
165
|
+
return 0;
|
171
166
|
}
|
172
167
|
|
173
|
-
export function hasRules
|
174
|
-
|
175
|
-
Array.isArray(rules?.sections) &&
|
176
|
-
rules.sections.some((section) => section.ruleItems.length > 0)
|
177
|
-
);
|
168
|
+
export function hasRules(rules: ProductUploadRules): boolean {
|
169
|
+
return Array.isArray(rules?.sections) && rules.sections.some(section => section.ruleItems.length > 0);
|
178
170
|
}
|
179
171
|
|
180
172
|
/**
|
@@ -183,170 +175,148 @@ export function hasRules (rules: ProductUploadRules): boolean {
|
|
183
175
|
* @param b
|
184
176
|
* @returns
|
185
177
|
*/
|
186
|
-
export function compareRules
|
187
|
-
|
188
|
-
|
178
|
+
export function compareRules(
|
179
|
+
a: ProductUploadRules = { sections: [] },
|
180
|
+
b: ProductUploadRules = { sections: [] }
|
189
181
|
): boolean {
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
): boolean => {
|
195
|
-
return (
|
196
|
-
rA.attribute === rB.attribute &&
|
197
|
-
rA.operator === rB.operator &&
|
198
|
-
compareValues(rA.value, rB.value)
|
199
|
-
);
|
200
|
-
};
|
182
|
+
const matchedSections = [] as number[];
|
183
|
+
const sameRule = (rA: ProductUploadRuleItem, rB: ProductUploadRuleItem): boolean => {
|
184
|
+
return rA.attribute === rB.attribute && rA.operator === rB.operator && compareValues(rA.value, rB.value);
|
185
|
+
};
|
201
186
|
|
202
|
-
|
203
|
-
|
187
|
+
a.sections = a.sections.filter(s => s.ruleItems.length > 0);
|
188
|
+
b.sections = b.sections.filter(s => s.ruleItems.length > 0);
|
204
189
|
|
205
|
-
|
206
|
-
|
207
|
-
|
190
|
+
if (a.sections.length !== b.sections.length) {
|
191
|
+
return false;
|
192
|
+
}
|
208
193
|
|
209
|
-
|
210
|
-
|
194
|
+
for (let i = 0; i < a.sections.length; i++) {
|
195
|
+
const rulesA = a.sections[i].ruleItems;
|
211
196
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
197
|
+
for (let j = 0; j < b.sections.length; j++) {
|
198
|
+
if (matchedSections.includes(j)) {
|
199
|
+
continue;
|
200
|
+
}
|
216
201
|
|
217
|
-
|
202
|
+
const rulesB = b.sections[j].ruleItems;
|
218
203
|
|
219
|
-
|
220
|
-
|
221
|
-
|
204
|
+
if (rulesA.length !== rulesB.length) {
|
205
|
+
continue;
|
206
|
+
}
|
222
207
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
}
|
208
|
+
if (rulesA.every(ruleA => rulesB.some(ruleB => sameRule(ruleA, ruleB)))) {
|
209
|
+
matchedSections.push(j);
|
210
|
+
break;
|
211
|
+
}
|
212
|
+
}
|
229
213
|
}
|
230
|
-
}
|
231
214
|
|
232
|
-
|
215
|
+
return matchedSections.length === a.sections.length;
|
233
216
|
}
|
234
217
|
|
235
|
-
function compareValues
|
236
|
-
|
237
|
-
|
238
|
-
|
218
|
+
function compareValues(a: any, b: any): boolean {
|
219
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
220
|
+
return a.length === b.length && a.every(i => b.includes(i));
|
221
|
+
}
|
239
222
|
|
240
|
-
|
223
|
+
return a === b;
|
241
224
|
}
|
242
225
|
|
243
|
-
export function compareMappings
|
244
|
-
|
245
|
-
|
246
|
-
)
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
break;
|
270
|
-
}
|
226
|
+
export function compareMappings(a: ProductUploadMapping[], b: ProductUploadMapping[]): boolean {
|
227
|
+
const matchedMappings = [] as number[];
|
228
|
+
|
229
|
+
if (a.length !== b.length) {
|
230
|
+
return false;
|
231
|
+
}
|
232
|
+
|
233
|
+
for (let i = 0; i < a.length; i++) {
|
234
|
+
const mappingA = a[i];
|
235
|
+
|
236
|
+
for (let j = 0; j < b.length; j++) {
|
237
|
+
if (matchedMappings.includes(j)) {
|
238
|
+
continue;
|
239
|
+
}
|
240
|
+
|
241
|
+
const mappingB = b[j];
|
242
|
+
|
243
|
+
if (
|
244
|
+
mappingA.attribute === mappingB.attribute &&
|
245
|
+
compareValues(mappingA.value, mappingB.value) &&
|
246
|
+
compareRules(mappingA.rules, mappingB.rules)
|
247
|
+
) {
|
248
|
+
matchedMappings.push(j);
|
249
|
+
break;
|
250
|
+
}
|
251
|
+
}
|
271
252
|
}
|
272
|
-
}
|
273
253
|
|
274
|
-
|
254
|
+
return matchedMappings.length === a.length;
|
275
255
|
}
|
276
|
-
|
277
|
-
|
256
|
+
|
257
|
+
export function optionIsString(option: OptionType): option is string {
|
258
|
+
return typeof option === 'string';
|
278
259
|
}
|
279
260
|
|
280
|
-
export function optionIsItem
|
281
|
-
|
261
|
+
export function optionIsItem(option: OptionType): option is OptionTypeItem {
|
262
|
+
return typeof option !== 'string' && 'value' in option;
|
282
263
|
}
|
283
264
|
|
284
|
-
export function optionIsGroup
|
285
|
-
|
265
|
+
export function optionIsGroup(option: OptionType): option is OptionTypeGroup {
|
266
|
+
return typeof option !== 'string' && 'options' in option;
|
286
267
|
}
|
287
|
-
|
288
|
-
|
289
|
-
|
268
|
+
|
269
|
+
export function detectProfileChange(
|
270
|
+
a: ProductUploadProfile,
|
271
|
+
b: ProductUploadProfile
|
290
272
|
): { name: boolean; rules: boolean; mappings: boolean; id: boolean } {
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
if (a.mappings !== b.mappings) {
|
303
|
-
const mappingsA = a.mappings?.filter(
|
304
|
-
(m) =>
|
305
|
-
m.attribute !== 'targetCountry' && m.attribute !== 'contentLanguage'
|
306
|
-
);
|
307
|
-
const mappingsB = b.mappings?.filter(
|
308
|
-
(m) =>
|
309
|
-
m.attribute !== 'targetCountry' && m.attribute !== 'contentLanguage'
|
310
|
-
);
|
311
|
-
changes.mappings = !compareMappings(mappingsA, mappingsB);
|
312
|
-
}
|
273
|
+
const changes = {
|
274
|
+
name: false,
|
275
|
+
rules: false,
|
276
|
+
mappings: false,
|
277
|
+
id: false,
|
278
|
+
};
|
279
|
+
|
280
|
+
if (a.name !== b.name) {
|
281
|
+
changes.name = true;
|
282
|
+
}
|
313
283
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
284
|
+
if (
|
285
|
+
ProductUploadMappings.filter(m => m.required)
|
286
|
+
.map(m => m.attribute)
|
287
|
+
.some(key => {
|
288
|
+
const mappingA = a.mappings?.filter(m => m.attribute === key) ?? [];
|
289
|
+
const mappingB = b.mappings?.filter(m => m.attribute === key) ?? [];
|
290
|
+
return !compareMappings(mappingA, mappingB);
|
291
|
+
})
|
292
|
+
) {
|
293
|
+
changes.id = true;
|
294
|
+
}
|
318
295
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
changes.
|
323
|
-
}
|
296
|
+
const requiredAttrs = ProductUploadMappings.filter(m => m.required).map(m => m.attribute);
|
297
|
+
const mappingsA = a.mappings?.filter(m => !requiredAttrs.includes(m.attribute));
|
298
|
+
const mappingsB = b.mappings?.filter(m => !requiredAttrs.includes(m.attribute));
|
299
|
+
changes.mappings = !compareMappings(mappingsA, mappingsB);
|
324
300
|
|
325
|
-
|
326
|
-
|
327
|
-
|
301
|
+
if (!compareRules(a.rules, b.rules)) {
|
302
|
+
changes.rules = true;
|
303
|
+
}
|
328
304
|
|
329
|
-
|
305
|
+
return changes;
|
330
306
|
}
|
331
307
|
|
332
|
-
export function sanitizeUploadProfile
|
333
|
-
|
334
|
-
|
335
|
-
if (profile.rules?.sections) {
|
336
|
-
profile.rules.sections = profile.rules.sections.filter(
|
337
|
-
(section) => section.ruleItems.length > 0
|
338
|
-
);
|
339
|
-
}
|
340
|
-
|
341
|
-
profile.mappings = profile.mappings?.map((mapping) => {
|
342
|
-
if (mapping.rules?.sections) {
|
343
|
-
mapping.rules.sections = mapping.rules.sections.filter(
|
344
|
-
(section) => section.ruleItems.length > 0
|
345
|
-
);
|
308
|
+
export function sanitizeUploadProfile(profile: ProductUploadProfile): ProductUploadProfile {
|
309
|
+
if (profile.rules?.sections) {
|
310
|
+
profile.rules.sections = profile.rules.sections.filter(section => section.ruleItems.length > 0);
|
346
311
|
}
|
347
312
|
|
348
|
-
|
349
|
-
|
313
|
+
profile.mappings = profile.mappings?.map(mapping => {
|
314
|
+
if (mapping.rules?.sections) {
|
315
|
+
mapping.rules.sections = mapping.rules.sections.filter(section => section.ruleItems.length > 0);
|
316
|
+
}
|
317
|
+
|
318
|
+
return mapping;
|
319
|
+
});
|
350
320
|
|
351
|
-
|
321
|
+
return profile;
|
352
322
|
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
2
|
+
import {
|
3
|
+
extractSubscriptionCode,
|
4
|
+
makeSubscriptionCode,
|
5
|
+
makeSubscriptionName,
|
6
|
+
parseSubscriptionName,
|
7
|
+
} from '../src/utils/company.js';
|
8
|
+
import { ShopifyRecurringCharge } from '../src/types/shopify.types.js';
|
9
|
+
|
10
|
+
describe('Company', () => {
|
11
|
+
test('Make subscription code', () => {
|
12
|
+
expect(
|
13
|
+
makeSubscriptionCode({
|
14
|
+
code: 'G',
|
15
|
+
level: 20,
|
16
|
+
products: 100,
|
17
|
+
profiles: 100,
|
18
|
+
price: 100,
|
19
|
+
})
|
20
|
+
).toBe('G20');
|
21
|
+
});
|
22
|
+
|
23
|
+
test('Make subscription name', () => {
|
24
|
+
expect(
|
25
|
+
makeSubscriptionName({
|
26
|
+
code: 'G',
|
27
|
+
level: 10,
|
28
|
+
products: 100,
|
29
|
+
profiles: 100,
|
30
|
+
price: 100,
|
31
|
+
})
|
32
|
+
).toBe('G10');
|
33
|
+
});
|
34
|
+
|
35
|
+
test('Extract subscription code', () => {
|
36
|
+
expect(
|
37
|
+
extractSubscriptionCode({
|
38
|
+
name: 'G50 subscription',
|
39
|
+
price: 100,
|
40
|
+
status: 'active',
|
41
|
+
trial_ends_at: '2022-01-01',
|
42
|
+
} as unknown as ShopifyRecurringCharge)
|
43
|
+
).toBe('G50');
|
44
|
+
});
|
45
|
+
|
46
|
+
test('Parse subscription name', () => {
|
47
|
+
expect(parseSubscriptionName('G50')).toEqual({ code: 'G', level: '50' });
|
48
|
+
});
|
49
|
+
});
|