@flowerforce/flowerbase 1.9.0 → 1.10.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 1.10.0 (2026-05-28)
2
+
3
+
4
+ ### 🚀 Features
5
+
6
+ - enhance rule evaluation and testing for scalar equality with array ([#71](https://github.com/flowerforce/flowerbase/pull/71))
7
+
1
8
  ## 1.9.0 (2026-05-06)
2
9
 
3
10
 
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/utils/rules-matcher/utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAe,MAAM,aAAa,CAAA;AAmEvE;;GAEG;AACH,QAAA,MAAM,iBAAiB,EAAE,iBAkOxB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,SAyDvB,CAAA;AAID,eAAe,iBAAiB,CAAA"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/utils/rules-matcher/utils.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAe,MAAM,aAAa,CAAA;AA2EvE;;GAEG;AACH,QAAA,MAAM,iBAAiB,EAAE,iBAkOxB,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,SAAS,EAAE,SAyDvB,CAAA;AAID,eAAe,iBAAiB,CAAA"}
@@ -45,6 +45,12 @@ const includesWithSemanticEquality = (value, candidate) => rulesMatcherUtils
45
45
  .some((sourceItem) => rulesMatcherUtils
46
46
  .forceArray(item)
47
47
  .some((candidateItem) => areSemanticallyEqual(sourceItem, candidateItem))));
48
+ const hasScalarArrayMembershipMatch = (left, right) => {
49
+ if (Array.isArray(left) === Array.isArray(right)) {
50
+ return false;
51
+ }
52
+ return includesWithSemanticEquality(left, right);
53
+ };
48
54
  const resolveRefPath = (data, refPath, prefix) => {
49
55
  const exactMatch = (0, get_1.default)(data, refPath, undefined);
50
56
  if (exactMatch !== undefined) {
@@ -241,7 +247,7 @@ const rulesMatcherUtils = {
241
247
  exports.operators = {
242
248
  $exists: (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
243
249
  '%exists': (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
244
- $eq: (a, b) => areSemanticallyEqual(a, b),
250
+ $eq: (a, b) => areSemanticallyEqual(a, b) || hasScalarArrayMembershipMatch(a, b),
245
251
  $ne: (a, b) => !areSemanticallyEqual(a, b),
246
252
  $gt: (a, b) => rulesMatcherUtils.forceNumber(a) > parseFloat(b),
247
253
  $gte: (a, b) => rulesMatcherUtils.forceNumber(a) >= parseFloat(b),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,7 +1,58 @@
1
- import { ensureClientPipelineStages, getHiddenFieldsFromRulesConfig, prependUnsetStage, applyAccessControlToPipeline, mergeProjections } from '../utils'
1
+ import {
2
+ ensureClientPipelineStages,
3
+ getHiddenFieldsFromRulesConfig,
4
+ prependUnsetStage,
5
+ applyAccessControlToPipeline,
6
+ mergeProjections,
7
+ getValidRule
8
+ } from '../utils'
2
9
  import { Role } from '../../../utils/roles/interface'
3
10
 
4
11
  describe('MongoDB Atlas aggregate helpers', () => {
12
+ describe('getValidRule', () => {
13
+ it('matches legacy scalar apply_when syntax against array-valued custom data', () => {
14
+ const validRules = getValidRule({
15
+ filters: [
16
+ {
17
+ apply_when: {
18
+ '%%user.custom_data.roles': 'Admin'
19
+ },
20
+ query: {}
21
+ }
22
+ ] as any,
23
+ user: {
24
+ custom_data: {
25
+ roles: ['Admin', 'SCM']
26
+ }
27
+ } as any,
28
+ record: null
29
+ })
30
+
31
+ expect(validRules).toHaveLength(1)
32
+ })
33
+
34
+ it('does not match legacy scalar apply_when syntax when the array does not contain the value', () => {
35
+ const validRules = getValidRule({
36
+ filters: [
37
+ {
38
+ apply_when: {
39
+ '%%user.custom_data.roles': 'Admin'
40
+ },
41
+ query: {}
42
+ }
43
+ ] as any,
44
+ user: {
45
+ custom_data: {
46
+ roles: ['Editor']
47
+ }
48
+ } as any,
49
+ record: null
50
+ })
51
+
52
+ expect(validRules).toHaveLength(0)
53
+ })
54
+ })
55
+
5
56
  describe('ensureClientPipelineStages', () => {
6
57
  it('allows safe stages', () => {
7
58
  expect(() =>
@@ -119,4 +119,27 @@ describe('evaluateExpression', () => {
119
119
  })
120
120
  )
121
121
  })
122
+
123
+ it('supports scalar equality against array-valued custom data', async () => {
124
+ const expression = {
125
+ '%%user.custom_data.roles': 'Admin'
126
+ }
127
+
128
+ const params = {
129
+ type: 'read',
130
+ cursor: { _id: 'doc-1' },
131
+ expansions: {
132
+ '%%prevRoot': undefined
133
+ },
134
+ roles: []
135
+ } as Params
136
+
137
+ const user = {
138
+ custom_data: {
139
+ roles: ['Admin', 'SCM']
140
+ }
141
+ }
142
+
143
+ await expect(evaluateExpression(params, expression, user as never)).resolves.toBe(true)
144
+ })
122
145
  })
@@ -86,7 +86,7 @@ describe('rule function', () => {
86
86
  expect(result.name).toBe('user.authId___%oidToString')
87
87
  })
88
88
 
89
- it('does not treat scalar equality as array membership in compact rules', () => {
89
+ it('treats scalar equality as array membership in compact rules', () => {
90
90
  const data = {
91
91
  doc: {
92
92
  owners: ['user-1', 'user-2']
@@ -97,7 +97,7 @@ describe('rule function', () => {
97
97
  prefix: 'doc'
98
98
  })
99
99
 
100
- expect(result.valid).toBe(false)
100
+ expect(result.valid).toBe(true)
101
101
  expect(result.name).toBe('doc.owners___$eq')
102
102
  })
103
103
 
@@ -27,8 +27,8 @@ describe('rulesMatcherUtils', () => {
27
27
  // isFunction
28
28
  expect(isFunction(2)).toBe(false)
29
29
  expect(isFunction('ciao')).toBe(false)
30
- expect(isFunction(() => {})).toBe(true)
31
- expect(isFunction(function test() {})).toBe(true)
30
+ expect(isFunction(() => { })).toBe(true)
31
+ expect(isFunction(function test() { })).toBe(true)
32
32
  // isString
33
33
  expect(isString(2)).toBe(false)
34
34
  expect(isString('2')).toBe(true)
@@ -94,4 +94,13 @@ describe('rulesMatcherUtils', () => {
94
94
  )
95
95
  ).toBe(false)
96
96
  })
97
+
98
+ it('matches scalar equality against array-valued fields', () => {
99
+ const data = {
100
+ '%%user': { custom_data: { roles: ['Admin', 'SCM'] } }
101
+ }
102
+
103
+ expect(checkRule({ '%%user.custom_data.roles': 'Admin' } as any, data, {})).toBe(true)
104
+ expect(checkRule({ '%%user.custom_data.roles': 'Manager' } as any, data, {})).toBe(false)
105
+ })
97
106
  })
@@ -58,6 +58,14 @@ const includesWithSemanticEquality = (value: unknown, candidate: unknown): boole
58
58
  )
59
59
  )
60
60
 
61
+ const hasScalarArrayMembershipMatch = (left: unknown, right: unknown): boolean => {
62
+ if (Array.isArray(left) === Array.isArray(right)) {
63
+ return false
64
+ }
65
+
66
+ return includesWithSemanticEquality(left, right)
67
+ }
68
+
61
69
  const resolveRefPath = (data: unknown, refPath: string, prefix?: string): unknown => {
62
70
  const exactMatch = _get(data, refPath, undefined)
63
71
 
@@ -306,7 +314,7 @@ export const operators: Operators = {
306
314
  $exists: (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
307
315
  '%exists': (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
308
316
 
309
- $eq: (a, b) => areSemanticallyEqual(a, b),
317
+ $eq: (a, b) => areSemanticallyEqual(a, b) || hasScalarArrayMembershipMatch(a, b),
310
318
 
311
319
  $ne: (a, b) => !areSemanticallyEqual(a, b),
312
320