@flowerforce/flowerbase 1.7.5-beta.2 → 1.7.5-beta.3

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.
Files changed (61) hide show
  1. package/dist/features/functions/controller.d.ts.map +1 -1
  2. package/dist/features/functions/controller.js +14 -3
  3. package/dist/services/api/index.d.ts +4 -0
  4. package/dist/services/api/index.d.ts.map +1 -1
  5. package/dist/services/api/utils.d.ts +1 -0
  6. package/dist/services/api/utils.d.ts.map +1 -1
  7. package/dist/services/index.d.ts +4 -0
  8. package/dist/services/index.d.ts.map +1 -1
  9. package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
  10. package/dist/services/mongodb-atlas/utils.js +17 -1
  11. package/dist/utils/context/helpers.d.ts +12 -0
  12. package/dist/utils/context/helpers.d.ts.map +1 -1
  13. package/dist/utils/roles/helpers.d.ts.map +1 -1
  14. package/dist/utils/roles/helpers.js +19 -4
  15. package/dist/utils/roles/interface.d.ts +10 -6
  16. package/dist/utils/roles/interface.d.ts.map +1 -1
  17. package/dist/utils/roles/machines/commonValidators.js +2 -2
  18. package/dist/utils/roles/machines/fieldPermissions.d.ts +8 -0
  19. package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -0
  20. package/dist/utils/roles/machines/fieldPermissions.js +67 -0
  21. package/dist/utils/roles/machines/read/A/index.d.ts.map +1 -1
  22. package/dist/utils/roles/machines/read/A/index.js +4 -3
  23. package/dist/utils/roles/machines/read/C/index.d.ts.map +1 -1
  24. package/dist/utils/roles/machines/read/C/index.js +16 -16
  25. package/dist/utils/roles/machines/read/C/validators.js +2 -2
  26. package/dist/utils/roles/machines/read/D/index.js +1 -1
  27. package/dist/utils/roles/machines/read/D/validators.d.ts +1 -1
  28. package/dist/utils/roles/machines/read/D/validators.d.ts.map +1 -1
  29. package/dist/utils/roles/machines/read/D/validators.js +19 -21
  30. package/dist/utils/roles/machines/write/B/index.d.ts.map +1 -1
  31. package/dist/utils/roles/machines/write/B/index.js +12 -9
  32. package/dist/utils/roles/machines/write/C/index.js +1 -1
  33. package/dist/utils/roles/machines/write/C/validators.d.ts +1 -1
  34. package/dist/utils/roles/machines/write/C/validators.d.ts.map +1 -1
  35. package/dist/utils/roles/machines/write/C/validators.js +16 -21
  36. package/package.json +1 -1
  37. package/src/features/functions/controller.ts +16 -3
  38. package/src/features/triggers/__tests__/index.test.ts +2 -2
  39. package/src/services/mongodb-atlas/__tests__/findOneAndUpdate.test.ts +1 -1
  40. package/src/services/mongodb-atlas/utils.ts +19 -4
  41. package/src/utils/__tests__/STEP_A_STATES.test.ts +24 -2
  42. package/src/utils/__tests__/STEP_C_STATES.test.ts +61 -27
  43. package/src/utils/__tests__/STEP_D_STATES.test.ts +9 -9
  44. package/src/utils/__tests__/WRITE_STEP_B_STATES.test.ts +184 -0
  45. package/src/utils/__tests__/checkAdditionalFieldsFn.test.ts +2 -2
  46. package/src/utils/__tests__/checkFieldsPropertyExists.test.ts +13 -0
  47. package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +52 -121
  48. package/src/utils/__tests__/evaluateTopLevelReadFn.test.ts +10 -1
  49. package/src/utils/__tests__/evaluateTopLevelWriteFn.test.ts +21 -5
  50. package/src/utils/roles/helpers.ts +18 -4
  51. package/src/utils/roles/interface.ts +13 -6
  52. package/src/utils/roles/machines/commonValidators.ts +1 -1
  53. package/src/utils/roles/machines/fieldPermissions.ts +86 -0
  54. package/src/utils/roles/machines/read/A/index.ts +4 -3
  55. package/src/utils/roles/machines/read/C/index.ts +18 -18
  56. package/src/utils/roles/machines/read/C/validators.ts +2 -2
  57. package/src/utils/roles/machines/read/D/index.ts +1 -1
  58. package/src/utils/roles/machines/read/D/validators.ts +12 -25
  59. package/src/utils/roles/machines/write/B/index.ts +12 -9
  60. package/src/utils/roles/machines/write/C/index.ts +1 -1
  61. package/src/utils/roles/machines/write/C/validators.ts +9 -26
@@ -0,0 +1,184 @@
1
+ import { MachineContext } from '../roles/machines/interface'
2
+ import { STEP_B_STATES } from '../roles/machines/write/B'
3
+ import {
4
+ checkFieldsPropertyExists,
5
+ evaluateTopLevelPermissionsFn
6
+ } from '../roles/machines/commonValidators'
7
+
8
+ const {
9
+ checkDeleteRequest,
10
+ evaluateTopLevelDelete,
11
+ evaluateTopLevelWrite,
12
+ evaluateTopLevelInsert,
13
+ checkFieldsProperty
14
+ } = STEP_B_STATES
15
+
16
+ jest.mock('../roles/machines/commonValidators', () => ({
17
+ checkFieldsPropertyExists: jest.fn(),
18
+ evaluateTopLevelPermissionsFn: jest.fn()
19
+ }))
20
+
21
+ const endValidation = jest.fn()
22
+ const goToNextValidationStage = jest.fn()
23
+ const next = jest.fn()
24
+
25
+ describe('WRITE STEP_B_STATES', () => {
26
+ beforeEach(() => {
27
+ jest.clearAllMocks()
28
+ })
29
+
30
+ it('routes delete requests to evaluateTopLevelDelete', async () => {
31
+ const context = { params: { type: 'delete' } } as MachineContext
32
+ await checkDeleteRequest({
33
+ context,
34
+ endValidation,
35
+ goToNextValidationStage,
36
+ next,
37
+ initialStep: null
38
+ })
39
+ expect(next).toHaveBeenCalledWith('evaluateTopLevelDelete')
40
+ })
41
+
42
+ it('routes non-delete requests to evaluateTopLevelWrite', async () => {
43
+ const context = { params: { type: 'write' } } as MachineContext
44
+ await checkDeleteRequest({
45
+ context,
46
+ endValidation,
47
+ goToNextValidationStage,
48
+ next,
49
+ initialStep: null
50
+ })
51
+ expect(next).toHaveBeenCalledWith('evaluateTopLevelWrite')
52
+ })
53
+
54
+ it('allows delete only when top-level delete is true', async () => {
55
+ ;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
56
+ const context = {} as MachineContext
57
+ await evaluateTopLevelDelete({
58
+ context,
59
+ endValidation,
60
+ goToNextValidationStage,
61
+ next,
62
+ initialStep: null
63
+ })
64
+ expect(endValidation).toHaveBeenCalledWith({ success: true })
65
+ })
66
+
67
+ it('denies delete when top-level delete is false/undefined', async () => {
68
+ ;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(undefined)
69
+ const context = {} as MachineContext
70
+ await evaluateTopLevelDelete({
71
+ context,
72
+ endValidation,
73
+ goToNextValidationStage,
74
+ next,
75
+ initialStep: null
76
+ })
77
+ expect(endValidation).toHaveBeenCalledWith({ success: false })
78
+ })
79
+
80
+ it('routes to insert check when write=true', async () => {
81
+ ;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
82
+ const context = {} as MachineContext
83
+ await evaluateTopLevelWrite({
84
+ context,
85
+ endValidation,
86
+ goToNextValidationStage,
87
+ next,
88
+ initialStep: null
89
+ })
90
+ expect(next).toHaveBeenCalledWith('evaluateTopLevelInsert')
91
+ })
92
+
93
+ it('routes to insert check when write=true and field-level rules exist', async () => {
94
+ ;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
95
+ ;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
96
+ const context = {} as MachineContext
97
+ await evaluateTopLevelWrite({
98
+ context,
99
+ endValidation,
100
+ goToNextValidationStage,
101
+ next,
102
+ initialStep: null
103
+ })
104
+ expect(next).toHaveBeenCalledWith('evaluateTopLevelInsert')
105
+ })
106
+
107
+ it('denies when write=false', async () => {
108
+ ;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(false)
109
+ const context = {} as MachineContext
110
+ await evaluateTopLevelWrite({
111
+ context,
112
+ endValidation,
113
+ goToNextValidationStage,
114
+ next,
115
+ initialStep: null
116
+ })
117
+ expect(endValidation).toHaveBeenCalledWith({ success: false })
118
+ })
119
+
120
+ it('routes to field-level checks when write is undefined', async () => {
121
+ ;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(undefined)
122
+ const context = {} as MachineContext
123
+ await evaluateTopLevelWrite({
124
+ context,
125
+ endValidation,
126
+ goToNextValidationStage,
127
+ next,
128
+ initialStep: null
129
+ })
130
+ expect(next).toHaveBeenCalledWith('checkFieldsProperty')
131
+ })
132
+
133
+ it('denies insert when insert is false/undefined', async () => {
134
+ ;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(false)
135
+ const context = {} as MachineContext
136
+ await evaluateTopLevelInsert({
137
+ context,
138
+ endValidation,
139
+ goToNextValidationStage,
140
+ next,
141
+ initialStep: null
142
+ })
143
+ expect(endValidation).toHaveBeenCalledWith({ success: false })
144
+ })
145
+
146
+ it('allows when insert=true', async () => {
147
+ ;(evaluateTopLevelPermissionsFn as jest.Mock).mockResolvedValueOnce(true)
148
+ const context = {} as MachineContext
149
+ await evaluateTopLevelInsert({
150
+ context,
151
+ endValidation,
152
+ goToNextValidationStage,
153
+ next,
154
+ initialStep: null
155
+ })
156
+ expect(endValidation).toHaveBeenCalledWith({ success: true })
157
+ })
158
+
159
+ it('routes checkFieldsProperty to checkIsValidFieldName when field rules exist', async () => {
160
+ ;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(true)
161
+ const context = {} as MachineContext
162
+ await checkFieldsProperty({
163
+ context,
164
+ endValidation,
165
+ goToNextValidationStage,
166
+ next,
167
+ initialStep: null
168
+ })
169
+ expect(goToNextValidationStage).toHaveBeenCalledWith('checkIsValidFieldName')
170
+ })
171
+
172
+ it('routes checkFieldsProperty to checkAdditionalFields when no field rules exist', async () => {
173
+ ;(checkFieldsPropertyExists as jest.Mock).mockReturnValue(false)
174
+ const context = {} as MachineContext
175
+ await checkFieldsProperty({
176
+ context,
177
+ endValidation,
178
+ goToNextValidationStage,
179
+ next,
180
+ initialStep: null
181
+ })
182
+ expect(goToNextValidationStage).toHaveBeenCalledWith('checkAdditionalFields')
183
+ })
184
+ })
@@ -34,12 +34,12 @@ describe('comparePassword', () => {
34
34
  })
35
35
  expect(isDefined).toBe(false)
36
36
  })
37
- it('should return false for empty additional fields', () => {
37
+ it('should return true for empty additional fields (defined)', () => {
38
38
  const isDefined = checkAdditionalFieldsFn({
39
39
  role: { ...mockedRole, additional_fields: {} },
40
40
  user: mockUser,
41
41
  params: mockParams
42
42
  })
43
- expect(isDefined).toBe(false)
43
+ expect(isDefined).toBe(true)
44
44
  })
45
45
  })
@@ -44,4 +44,17 @@ describe('checkFieldsPropertyExists', () => {
44
44
  })
45
45
  expect(isValid).toBe(true)
46
46
  })
47
+ it('should return true if additional_fields is defined (even empty)', () => {
48
+ const isValid = checkFieldsPropertyExists({
49
+ role: {
50
+ fields: {},
51
+ additional_fields: {},
52
+ apply_when: {},
53
+ name: 'test'
54
+ } as Role,
55
+ user: mockUser,
56
+ params: mockParams
57
+ })
58
+ expect(isValid).toBe(true)
59
+ })
47
60
  })
@@ -11,17 +11,16 @@ describe('checkIsValidFieldNameFn', () => {
11
11
  beforeEach(() => {
12
12
  jest.clearAllMocks()
13
13
  })
14
- it('should return filtered fields based on role permissions, without excluding _id', () => {
14
+
15
+ it('returns only explicitly allowed fields when no fallback is configured', async () => {
15
16
  const mockedRole = {
16
17
  name: 'test',
17
- apply_when: {
18
- '%%true': true
19
- },
18
+ apply_when: { '%%true': true },
20
19
  fields: {
21
20
  name: { read: true, write: false },
22
- email: { read: false, write: true }
23
- },
24
- additional_fields: {}
21
+ email: { read: false, write: true },
22
+ age: { read: false, write: false }
23
+ }
25
24
  } as Role
26
25
  const context = {
27
26
  user: mockUser,
@@ -29,168 +28,100 @@ describe('checkIsValidFieldNameFn', () => {
29
28
  params: {
30
29
  cursor: { _id: mockId, name: 'Alice', email: 'alice@example.com', age: 25 }
31
30
  }
32
- }
31
+ } as MachineContext
33
32
 
34
- const result = checkIsValidFieldNameFn(context as MachineContext)
33
+ const result = await checkIsValidFieldNameFn(context)
35
34
  expect(result).toEqual({
36
- _id: mockId,
37
35
  name: 'Alice',
38
- email: 'alice@example.com',
39
- age: 25
36
+ email: 'alice@example.com'
40
37
  })
41
38
  })
42
- it("should exclude _id if role doesn't allows it", () => {
43
- const mockedRole = {
44
- name: 'test',
45
- apply_when: {
46
- '%%true': true
47
- },
48
- fields: {
49
- _id: { read: false, write: false },
50
- name: { read: true, write: false }
51
- },
52
- additional_fields: {}
53
- } as Role
54
- const context = {
55
- user: mockUser,
56
- role: mockedRole,
57
- params: {
58
- cursor: { _id: mockId, name: 'Alice' }
59
- }
60
- }
61
39
 
62
- const result = checkIsValidFieldNameFn(context as MachineContext)
63
-
64
- expect(result).toEqual({ name: 'Alice' })
65
- })
66
- it('should include _id if write role allows it', () => {
40
+ it('uses per-field additional_fields as fallback for unknown fields', async () => {
67
41
  const mockedRole = {
68
42
  name: 'test',
69
- apply_when: {
70
- '%%true': true
71
- },
72
- fields: {
73
- _id: { read: false, write: true },
74
- name: { read: true, write: false }
75
- },
76
- additional_fields: {}
77
- } as Role
78
- const context = {
79
- user: mockUser,
80
- role: mockedRole,
81
- params: {
82
- cursor: { _id: mockId, name: 'Alice' }
43
+ apply_when: { '%%true': true },
44
+ fields: {},
45
+ additional_fields: {
46
+ phone: { read: true, write: false },
47
+ address: { read: false, write: true }
83
48
  }
84
- }
85
-
86
- const result = checkIsValidFieldNameFn(context as MachineContext)
87
-
88
- expect(result).toEqual({ _id: mockId, name: 'Alice' })
89
- })
90
- it('should include _id if read role allows it', () => {
91
- const mockedRole = {
92
- name: 'test',
93
- apply_when: {
94
- '%%true': true
95
- },
96
- fields: {
97
- _id: { read: true, write: false },
98
- name: { read: true, write: false }
99
- },
100
- additional_fields: {}
101
49
  } as Role
102
50
  const context = {
103
- user: mockUser,
104
51
  role: mockedRole,
105
52
  params: {
106
- cursor: { _id: mockId, name: 'Alice' }
53
+ cursor: { _id: mockId, phone: '123456789', address: 'Unknown', city: 'Rome' }
107
54
  }
108
- }
55
+ } as MachineContext
109
56
 
110
- const result = checkIsValidFieldNameFn(context as MachineContext)
111
-
112
- expect(result).toEqual({ _id: mockId, name: 'Alice' })
57
+ const result = await checkIsValidFieldNameFn(context)
58
+ expect(result).toEqual({
59
+ phone: '123456789',
60
+ address: 'Unknown'
61
+ })
113
62
  })
114
63
 
115
- it('should return an empty object if no fields are readable/writable', () => {
64
+ it('supports realm-style global additional_fields fallback', async () => {
116
65
  const mockedRole = {
117
- name: 'test',
118
- apply_when: {
119
- '%%true': true
120
- },
66
+ name: 'collaborator',
67
+ apply_when: { '%%true': true },
121
68
  fields: {
122
- name: { read: false, write: false }
69
+ roles: { read: true, write: false }
123
70
  },
124
- additional_fields: {}
125
- } as Role
126
- const context = {
127
- role: mockedRole,
128
- params: {
129
- cursor: { name: 'Charlie', email: 'charlie@example.com' }
71
+ additional_fields: {
72
+ read: false,
73
+ write: false
130
74
  }
131
- }
132
-
133
- const result = checkIsValidFieldNameFn(context as MachineContext)
134
-
135
- expect(result).toEqual({ email: 'charlie@example.com' })
136
- })
137
-
138
- it('should handle additional_fields correctly for read permission', () => {
139
- const mockedRole = {
140
- name: 'test',
141
- apply_when: {
142
- '%%true': true
143
- },
144
- fields: {},
145
- additional_fields: { phone: { read: true, write: false } }
146
75
  } as Role
147
76
  const context = {
148
77
  role: mockedRole,
149
78
  params: {
150
- cursor: { _id: mockId, phone: '123456789', address: 'Unknown' }
79
+ cursor: { roles: ['editor'], email: 'user@example.com' }
151
80
  }
152
- }
81
+ } as MachineContext
153
82
 
154
- const result = checkIsValidFieldNameFn(context as MachineContext)
155
- expect(result).toEqual({ _id: mockId, phone: '123456789', address: 'Unknown' })
83
+ const result = await checkIsValidFieldNameFn(context)
84
+ expect(result).toEqual({
85
+ roles: ['editor']
86
+ })
156
87
  })
157
- it('should handle additional_fields correctly for write permission', () => {
88
+
89
+ it('denies unknown fields when additional_fields global fallback is false', async () => {
158
90
  const mockedRole = {
159
91
  name: 'test',
160
- apply_when: {
161
- '%%true': true
92
+ apply_when: { '%%true': true },
93
+ fields: {
94
+ roles: { read: true }
162
95
  },
163
- fields: {},
164
96
  additional_fields: {
165
- phone: { read: false, write: true },
166
- address: { read: false, write: true }
97
+ read: false,
98
+ write: false
167
99
  }
168
100
  } as Role
169
101
  const context = {
170
102
  role: mockedRole,
171
103
  params: {
172
- cursor: { _id: mockId, phone: '123456789', address: 'Unknown' }
104
+ cursor: { email: 'user@example.com' }
173
105
  }
174
- }
106
+ } as MachineContext
175
107
 
176
- const result = checkIsValidFieldNameFn(context as MachineContext)
177
- expect(result).toEqual({ _id: mockId, phone: '123456789', address: 'Unknown' })
108
+ const result = await checkIsValidFieldNameFn(context)
109
+ expect(result).toEqual({})
178
110
  })
179
- it('should return only the _id if fields and additional fields are not defined', () => {
111
+
112
+ it('returns empty object when no field permissions are available', async () => {
180
113
  const mockedRole = {
181
114
  name: 'test',
182
- apply_when: {
183
- '%%true': true
184
- }
115
+ apply_when: { '%%true': true }
185
116
  } as Role
186
117
  const context = {
187
118
  role: mockedRole,
188
119
  params: {
189
120
  cursor: { _id: mockId, phone: '123456789', address: 'Unknown' }
190
121
  }
191
- }
122
+ } as MachineContext
192
123
 
193
- const result = checkIsValidFieldNameFn(context as MachineContext)
194
- expect(result).toEqual({ _id: mockId, phone: '123456789', address: 'Unknown' })
124
+ const result = await checkIsValidFieldNameFn(context)
125
+ expect(result).toEqual({})
195
126
  })
196
127
  })
@@ -17,7 +17,7 @@ describe('evaluateTopLevelReadFn', () => {
17
17
  beforeEach(() => {
18
18
  jest.clearAllMocks()
19
19
  })
20
- it('should return false if type is different from read', async () => {
20
+ it('should return false if type is neither read nor search', async () => {
21
21
  const isValid = await evaluateTopLevelReadFn({
22
22
  role: mockedRole,
23
23
  user: mockUser,
@@ -35,6 +35,15 @@ describe('evaluateTopLevelReadFn', () => {
35
35
  expect(isValid).toBe(undefined)
36
36
  expect(evaluateExpression).not.toHaveBeenCalled()
37
37
  })
38
+ it('should return undefined if type is search and role read is not defined', async () => {
39
+ const isValid = await evaluateTopLevelReadFn({
40
+ role: mockedRole,
41
+ user: mockUser,
42
+ params: { type: 'search' } as Params
43
+ })
44
+ expect(isValid).toBe(undefined)
45
+ expect(evaluateExpression).not.toHaveBeenCalled()
46
+ })
38
47
  it('should return false if type is read and role read defined but evaluate expression returns false', async () => {
39
48
  (evaluateExpression as jest.Mock).mockResolvedValueOnce(false)
40
49
  const isValid = await evaluateTopLevelReadFn({
@@ -14,7 +14,10 @@ const mockParams = {
14
14
  } as Params
15
15
 
16
16
  describe('evaluateTopLevelWriteFn', () => {
17
- it('should return undefined if type is different from read and write', async () => {
17
+ beforeEach(() => {
18
+ jest.clearAllMocks()
19
+ })
20
+ it('should return undefined if type is different from read, search and write', async () => {
18
21
  const isValid = await evaluateTopLevelWriteFn({
19
22
  role: mockedRole,
20
23
  user: mockUser,
@@ -23,6 +26,17 @@ describe('evaluateTopLevelWriteFn', () => {
23
26
  expect(isValid).toBe(undefined)
24
27
  expect(evaluateExpression).not.toHaveBeenCalled()
25
28
  })
29
+ it('should evaluate write for search requests', async () => {
30
+ (evaluateExpression as jest.Mock).mockResolvedValueOnce(true)
31
+ const searchParams = { type: 'search' } as Params
32
+ const isValid = await evaluateTopLevelWriteFn({
33
+ role: { ...mockedRole, write: true },
34
+ user: mockUser,
35
+ params: searchParams
36
+ })
37
+ expect(isValid).toBe(true)
38
+ expect(evaluateExpression).toHaveBeenCalledWith(searchParams, true, mockUser)
39
+ })
26
40
  it('should return false if type is read but evaluate expression returns false', async () => {
27
41
  (evaluateExpression as jest.Mock).mockResolvedValueOnce(false)
28
42
  const isValid = await evaluateTopLevelWriteFn({
@@ -45,22 +59,24 @@ describe('evaluateTopLevelWriteFn', () => {
45
59
  })
46
60
  it('should return false if type is write but evaluate expression returns false', async () => {
47
61
  (evaluateExpression as jest.Mock).mockResolvedValueOnce(false)
62
+ const writeParams = { type: 'write' } as Params
48
63
  const isValid = await evaluateTopLevelWriteFn({
49
64
  role: { ...mockedRole, write: false },
50
65
  user: mockUser,
51
- params: { type: 'write' } as Params
66
+ params: writeParams
52
67
  })
53
68
  expect(isValid).toBe(false)
54
- expect(evaluateExpression).toHaveBeenCalledWith(mockParams, false, mockUser)
69
+ expect(evaluateExpression).toHaveBeenCalledWith(writeParams, false, mockUser)
55
70
  })
56
71
  it('should return true if type is write and evaluate expression returns true', async () => {
57
72
  (evaluateExpression as jest.Mock).mockResolvedValueOnce(true)
73
+ const writeParams = { type: 'write' } as Params
58
74
  const isValid = await evaluateTopLevelWriteFn({
59
75
  role: { ...mockedRole, write: false },
60
76
  user: mockUser,
61
- params: { type: 'write' } as Params
77
+ params: writeParams
62
78
  })
63
79
  expect(isValid).toBe(true)
64
- expect(evaluateExpression).toHaveBeenCalledWith(mockParams, false, mockUser)
80
+ expect(evaluateExpression).toHaveBeenCalledWith(writeParams, false, mockUser)
65
81
  })
66
82
  })
@@ -8,17 +8,30 @@ import { MachineContext } from './machines/interface'
8
8
 
9
9
  const functionsConditions = ['%%true', '%%false']
10
10
 
11
+ const normalizeUserRole = (user?: MachineContext['user']) => {
12
+ if (!user) return user
13
+ if (typeof user !== 'object') return user
14
+ const candidate = user as Record<string, unknown>
15
+ if (typeof candidate.role === 'string') return user
16
+ const customRole =
17
+ typeof candidate.custom_data === 'object' && candidate.custom_data !== null
18
+ ? (candidate.custom_data as Record<string, unknown>).role
19
+ : undefined
20
+ return typeof customRole === 'string' ? ({ ...candidate, role: customRole } as MachineContext['user']) : user
21
+ }
22
+
11
23
  export const evaluateExpression = async (
12
24
  params: MachineContext['params'],
13
25
  expression?: PermissionExpression,
14
26
  user?: MachineContext['user']
15
27
  ): Promise<boolean> => {
16
28
  if (!expression || typeof expression === 'boolean') return !!expression
29
+ const normalizedUser = normalizeUserRole(user)
17
30
 
18
31
  const value = {
19
32
  ...params.expansions,
20
33
  ...params.cursor,
21
- '%%user': user,
34
+ '%%user': normalizedUser,
22
35
  '%%true': true
23
36
  }
24
37
  const conditions = expandQuery(expression, value)
@@ -26,7 +39,7 @@ export const evaluateExpression = async (
26
39
  functionsConditions.includes(key)
27
40
  )
28
41
  return complexCondition
29
- ? await evaluateComplexExpression(complexCondition, params, user)
42
+ ? await evaluateComplexExpression(complexCondition, params, normalizedUser)
30
43
  : rulesMatcherUtils.checkRule(conditions, value, {})
31
44
  }
32
45
 
@@ -36,6 +49,7 @@ const evaluateComplexExpression = async (
36
49
  user: MachineContext['user']
37
50
  ): Promise<boolean> => {
38
51
  const [key, config] = condition
52
+ const normalizedUser = normalizeUserRole(user)
39
53
 
40
54
  const functionConfig = config['%function']
41
55
  const { name, arguments: fnArguments } = functionConfig
@@ -47,7 +61,7 @@ const evaluateComplexExpression = async (
47
61
  ...params.expansions,
48
62
  ...params.cursor,
49
63
  '%%root': params.cursor,
50
- '%%user': user,
64
+ '%%user': normalizedUser,
51
65
  '%%true': true,
52
66
  '%%false': false
53
67
  }
@@ -62,7 +76,7 @@ const evaluateComplexExpression = async (
62
76
  args: expandedArguments,
63
77
  app,
64
78
  rules: StateManager.select("rules"),
65
- user,
79
+ user: normalizedUser,
66
80
  currentFunction,
67
81
  functionName: name,
68
82
  functionsList,
@@ -1,10 +1,19 @@
1
- export type PermissionExpression = boolean // TODO: add complex condition (%%true: %function)
1
+ export type PermissionExpression = boolean | Record<string, unknown>
2
2
 
3
3
  export type FieldPermissionExpression = {
4
- read?: boolean
5
- write?: boolean
4
+ read?: PermissionExpression
5
+ write?: PermissionExpression
6
+ fields?: {
7
+ [K: string]: FieldPermissionExpression
8
+ }
6
9
  }
7
10
 
11
+ export type AdditionalFieldsPermissionExpression =
12
+ | FieldPermissionExpression
13
+ | {
14
+ [K: string]: FieldPermissionExpression
15
+ }
16
+
8
17
  export interface DocumentFiltersPermissions {
9
18
  read?: PermissionExpression
10
19
  write?: PermissionExpression
@@ -23,9 +32,7 @@ export interface Role {
23
32
  fields?: {
24
33
  [K: string]: FieldPermissionExpression
25
34
  }
26
- additional_fields?: {
27
- [K: string]: FieldPermissionExpression
28
- }
35
+ additional_fields?: AdditionalFieldsPermissionExpression
29
36
  }
30
37
 
31
38
  export interface Params {
@@ -33,6 +33,6 @@ export const evaluateTopLevelPermissionsFn = async (
33
33
 
34
34
  export const checkFieldsPropertyExists = ({ role }: MachineContext) => {
35
35
  const hasFields = !!Object.keys(role?.fields ?? {}).length
36
- const hasAdditional = !!Object.keys(role?.additional_fields ?? {}).length
36
+ const hasAdditional = typeof role?.additional_fields !== 'undefined'
37
37
  return hasFields || hasAdditional
38
38
  }