@flowerforce/flowerbase 1.7.6-beta.7 → 1.7.6-beta.8
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/dist/features/rules/interface.d.ts +6 -5
- package/dist/features/rules/interface.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +41 -28
- package/dist/utils/roles/helpers.d.ts.map +1 -1
- package/dist/utils/roles/helpers.js +6 -3
- package/dist/utils/roles/machines/fieldPermissions.d.ts.map +1 -1
- package/dist/utils/roles/machines/fieldPermissions.js +3 -0
- package/dist/utils/rules-matcher/interface.d.ts +2 -0
- package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
- package/dist/utils/rules-matcher/interface.js +1 -0
- package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
- package/dist/utils/rules-matcher/utils.js +23 -6
- package/package.json +1 -1
- package/src/features/rules/interface.ts +18 -17
- package/src/features/triggers/__tests__/index.test.ts +6 -4
- package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +205 -7
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +27 -0
- package/src/services/mongodb-atlas/index.ts +294 -180
- package/src/utils/__tests__/checkIsValidFieldNameFn.test.ts +21 -4
- package/src/utils/__tests__/evaluateExpression.test.ts +33 -0
- package/src/utils/__tests__/rule.test.ts +38 -0
- package/src/utils/roles/helpers.ts +10 -5
- package/src/utils/roles/machines/fieldPermissions.ts +2 -0
- package/src/utils/rules-matcher/interface.ts +2 -0
- package/src/utils/rules-matcher/utils.ts +33 -17
- package/src/utils/__tests__/readFileContent.test.ts +0 -35
|
@@ -68,23 +68,40 @@ describe('checkIsValidFieldNameFn', () => {
|
|
|
68
68
|
read: true,
|
|
69
69
|
fields: {
|
|
70
70
|
avatar: { write: false },
|
|
71
|
-
name: { write: true }
|
|
72
|
-
|
|
71
|
+
name: { write: true },
|
|
72
|
+
tags: { write: false },
|
|
73
|
+
updatedAt: { write: true }
|
|
74
|
+
},
|
|
75
|
+
additional_fields: {}
|
|
73
76
|
} as Role
|
|
74
77
|
const context = {
|
|
75
78
|
user: mockUser,
|
|
76
79
|
role: mockedRole,
|
|
77
80
|
params: {
|
|
78
81
|
type: 'read',
|
|
79
|
-
cursor: {
|
|
82
|
+
cursor: {
|
|
83
|
+
_id: mockId,
|
|
84
|
+
userId: 'user-1',
|
|
85
|
+
email: 'alice@example.com',
|
|
86
|
+
workspaces: ['workspace-1'],
|
|
87
|
+
avatar: 'avatar.png',
|
|
88
|
+
name: 'Alice',
|
|
89
|
+
tags: ['owner'],
|
|
90
|
+
updatedAt: new Date('2026-03-17T10:00:00.000Z')
|
|
91
|
+
}
|
|
80
92
|
}
|
|
81
93
|
} as MachineContext
|
|
82
94
|
|
|
83
95
|
const result = await checkIsValidFieldNameFn(context)
|
|
84
96
|
expect(result).toEqual({
|
|
85
97
|
_id: mockId,
|
|
98
|
+
userId: 'user-1',
|
|
99
|
+
email: 'alice@example.com',
|
|
100
|
+
workspaces: ['workspace-1'],
|
|
86
101
|
avatar: 'avatar.png',
|
|
87
|
-
name: 'Alice'
|
|
102
|
+
name: 'Alice',
|
|
103
|
+
tags: ['owner'],
|
|
104
|
+
updatedAt: new Date('2026-03-17T10:00:00.000Z')
|
|
88
105
|
})
|
|
89
106
|
})
|
|
90
107
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { evaluateExpression } from '../roles/helpers'
|
|
2
|
+
import { Params } from '../roles/interface'
|
|
3
|
+
|
|
4
|
+
describe('evaluateExpression', () => {
|
|
5
|
+
it('supports insert-only write expressions that rely on %%prevRoot', async () => {
|
|
6
|
+
const expression = {
|
|
7
|
+
'%%prevRoot': {
|
|
8
|
+
'%exists': false
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const insertParams = {
|
|
13
|
+
type: 'insert',
|
|
14
|
+
cursor: { title: 'new doc' },
|
|
15
|
+
expansions: {
|
|
16
|
+
'%%prevRoot': undefined
|
|
17
|
+
},
|
|
18
|
+
roles: []
|
|
19
|
+
} as Params
|
|
20
|
+
|
|
21
|
+
const readParams = {
|
|
22
|
+
type: 'read',
|
|
23
|
+
cursor: { _id: 'doc-1', title: 'existing doc' },
|
|
24
|
+
expansions: {
|
|
25
|
+
'%%prevRoot': { _id: 'doc-1', title: 'existing doc' }
|
|
26
|
+
},
|
|
27
|
+
roles: []
|
|
28
|
+
} as Params
|
|
29
|
+
|
|
30
|
+
await expect(evaluateExpression(insertParams, expression)).resolves.toBe(true)
|
|
31
|
+
await expect(evaluateExpression(readParams, expression)).resolves.toBe(false)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -85,4 +85,42 @@ describe('rule function', () => {
|
|
|
85
85
|
expect(result.valid).toBe(true)
|
|
86
86
|
expect(result.name).toBe('user.authId___%oidToString')
|
|
87
87
|
})
|
|
88
|
+
|
|
89
|
+
it('does not treat scalar equality as array membership in compact rules', () => {
|
|
90
|
+
const data = {
|
|
91
|
+
doc: {
|
|
92
|
+
owners: ['user-1', 'user-2']
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = rulesMatcherUtils.rule({ owners: 'user-1' }, data, {
|
|
97
|
+
prefix: 'doc'
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
expect(result.valid).toBe(false)
|
|
101
|
+
expect(result.name).toBe('doc.owners___$eq')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('supports explicit array membership with $in rules', () => {
|
|
105
|
+
const data = {
|
|
106
|
+
doc: {
|
|
107
|
+
owners: ['user-1', 'user-2']
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = rulesMatcherUtils.rule(
|
|
112
|
+
{
|
|
113
|
+
owners: {
|
|
114
|
+
$in: ['user-1']
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
data,
|
|
118
|
+
{
|
|
119
|
+
prefix: 'doc'
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
expect(result.valid).toBe(true)
|
|
124
|
+
expect(result.name).toBe('doc.owners___$in')
|
|
125
|
+
})
|
|
88
126
|
})
|
|
@@ -17,7 +17,9 @@ const normalizeUserRole = (user?: MachineContext['user']) => {
|
|
|
17
17
|
typeof candidate.custom_data === 'object' && candidate.custom_data !== null
|
|
18
18
|
? (candidate.custom_data as Record<string, unknown>).role
|
|
19
19
|
: undefined
|
|
20
|
-
return typeof customRole === 'string'
|
|
20
|
+
return typeof customRole === 'string'
|
|
21
|
+
? ({ ...candidate, role: customRole } as MachineContext['user'])
|
|
22
|
+
: user
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export const evaluateExpression = async (
|
|
@@ -31,12 +33,15 @@ export const evaluateExpression = async (
|
|
|
31
33
|
const value = {
|
|
32
34
|
...params.expansions,
|
|
33
35
|
...params.cursor,
|
|
36
|
+
'%%root': params.cursor,
|
|
37
|
+
'%%prevRoot': params.expansions?.['%%prevRoot'],
|
|
34
38
|
'%%user': normalizedUser,
|
|
35
|
-
'%%true': true
|
|
39
|
+
'%%true': true,
|
|
40
|
+
'%%false': false
|
|
36
41
|
}
|
|
37
42
|
const conditions = expandQuery(expression, value)
|
|
38
|
-
const complexCondition = Object.entries(conditions as Record<string, any>).find(
|
|
39
|
-
functionsConditions.includes(key)
|
|
43
|
+
const complexCondition = Object.entries(conditions as Record<string, any>).find(
|
|
44
|
+
([key]) => functionsConditions.includes(key)
|
|
40
45
|
)
|
|
41
46
|
return complexCondition
|
|
42
47
|
? await evaluateComplexExpression(complexCondition, params, normalizedUser)
|
|
@@ -75,7 +80,7 @@ const evaluateComplexExpression = async (
|
|
|
75
80
|
const response = await GenerateContext({
|
|
76
81
|
args: expandedArguments,
|
|
77
82
|
app,
|
|
78
|
-
rules: StateManager.select(
|
|
83
|
+
rules: StateManager.select('rules'),
|
|
79
84
|
user: normalizedUser,
|
|
80
85
|
currentFunction,
|
|
81
86
|
functionName: name,
|
|
@@ -78,6 +78,8 @@ export const filterDocumentByFieldPermissions = async (
|
|
|
78
78
|
const readAllowed = await canReadField(context, permission)
|
|
79
79
|
if (typeof readAllowed !== 'undefined') {
|
|
80
80
|
allowed = readAllowed
|
|
81
|
+
} else if (!allowed && typeof permission.write !== 'undefined') {
|
|
82
|
+
allowed = await canWriteField(context, permission)
|
|
81
83
|
}
|
|
82
84
|
} else {
|
|
83
85
|
allowed = await canWriteField(context, permission)
|
|
@@ -331,6 +331,7 @@ export type Operators = {
|
|
|
331
331
|
* @returns
|
|
332
332
|
*/
|
|
333
333
|
$regex: OperatorsFunction
|
|
334
|
+
'%exists': OperatorsFunction
|
|
334
335
|
'%stringToOid': OperatorsFunction
|
|
335
336
|
'%oidToString': OperatorsFunction
|
|
336
337
|
}
|
|
@@ -352,6 +353,7 @@ export enum RulesOperators {
|
|
|
352
353
|
$all = '$all',
|
|
353
354
|
$size = '$size',
|
|
354
355
|
$regex = '$regex',
|
|
356
|
+
'%exists' = '%exists',
|
|
355
357
|
'%stringToOid' = '%stringToOid',
|
|
356
358
|
'%oidToString' = '%oidToString'
|
|
357
359
|
}
|
|
@@ -23,7 +23,10 @@ const toObjectIdHex = (value: unknown): string | null => {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const maybeObjectId = value as { _bsontype?: string; toHexString?: () => string }
|
|
26
|
-
if (
|
|
26
|
+
if (
|
|
27
|
+
maybeObjectId._bsontype === 'ObjectId' &&
|
|
28
|
+
typeof maybeObjectId.toHexString === 'function'
|
|
29
|
+
) {
|
|
27
30
|
const hex = maybeObjectId.toHexString()
|
|
28
31
|
return HEX_24_REGEXP.test(hex) ? hex.toLowerCase() : null
|
|
29
32
|
}
|
|
@@ -49,12 +52,22 @@ const includesWithSemanticEquality = (value: unknown, candidate: unknown): boole
|
|
|
49
52
|
rulesMatcherUtils
|
|
50
53
|
.forceArray(value)
|
|
51
54
|
.some((sourceItem) =>
|
|
52
|
-
rulesMatcherUtils
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
rulesMatcherUtils
|
|
56
|
+
.forceArray(item)
|
|
57
|
+
.some((candidateItem) => areSemanticallyEqual(sourceItem, candidateItem))
|
|
55
58
|
)
|
|
56
59
|
)
|
|
57
60
|
|
|
61
|
+
const resolveRefPath = (data: unknown, refPath: string, prefix?: string): unknown => {
|
|
62
|
+
const exactMatch = _get(data, refPath, undefined)
|
|
63
|
+
|
|
64
|
+
if (exactMatch !== undefined) {
|
|
65
|
+
return exactMatch
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return _get(data, rulesMatcherUtils.getPath(refPath, prefix), undefined)
|
|
69
|
+
}
|
|
70
|
+
|
|
58
71
|
/**
|
|
59
72
|
* Defines a utility object named rulesMatcherUtils, which contains various helper functions used for processing rules and data in a rule-matching context.
|
|
60
73
|
*/
|
|
@@ -73,11 +86,7 @@ const rulesMatcherUtils: RulesMatcherUtils = {
|
|
|
73
86
|
const { op, value, opt } = rulesMatcherUtils.getDefaultRule(valueBlock[path])
|
|
74
87
|
const valueRef =
|
|
75
88
|
value && String(value).indexOf('$ref:') === 0
|
|
76
|
-
?
|
|
77
|
-
data,
|
|
78
|
-
rulesMatcherUtils.getPath(value.replace('$ref:', ''), prefix),
|
|
79
|
-
undefined
|
|
80
|
-
)
|
|
89
|
+
? resolveRefPath(data, value.replace('$ref:', ''), prefix)
|
|
81
90
|
: value
|
|
82
91
|
|
|
83
92
|
if (!operators[op]) {
|
|
@@ -107,7 +116,9 @@ const rulesMatcherUtils: RulesMatcherUtils = {
|
|
|
107
116
|
const res = rulesMatcherUtils.getPath(path, prefix)
|
|
108
117
|
const { value } = rulesMatcherUtils.getDefaultRule(valueBlock[path])
|
|
109
118
|
if (value && String(value).indexOf('$ref:') === 0) {
|
|
110
|
-
|
|
119
|
+
const refPath = value.replace('$ref:', '')
|
|
120
|
+
keys[refPath] = true
|
|
121
|
+
keys[rulesMatcherUtils.getPath(refPath, prefix)] = true
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
return (keys[res] = true)
|
|
@@ -279,6 +290,7 @@ const rulesMatcherUtils: RulesMatcherUtils = {
|
|
|
279
290
|
*/
|
|
280
291
|
export const operators: Operators = {
|
|
281
292
|
$exists: (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
|
|
293
|
+
'%exists': (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
|
|
282
294
|
|
|
283
295
|
$eq: (a, b) => areSemanticallyEqual(a, b),
|
|
284
296
|
|
|
@@ -305,13 +317,17 @@ export const operators: Operators = {
|
|
|
305
317
|
$nin: (a, b) => !includesWithSemanticEquality(a, b),
|
|
306
318
|
|
|
307
319
|
$all: (a, b) =>
|
|
308
|
-
rulesMatcherUtils
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
rulesMatcherUtils
|
|
321
|
+
.forceArray(b)
|
|
322
|
+
.every((candidate) =>
|
|
323
|
+
rulesMatcherUtils
|
|
324
|
+
.forceArray(a)
|
|
325
|
+
.some((value) =>
|
|
326
|
+
rulesMatcherUtils
|
|
327
|
+
.forceArray(candidate)
|
|
328
|
+
.some((item) => areSemanticallyEqual(value, item))
|
|
329
|
+
)
|
|
330
|
+
),
|
|
315
331
|
|
|
316
332
|
$size: (a, b) => Array.isArray(a) && a.length === parseFloat(b),
|
|
317
333
|
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import { readFileContent, readJsonContent } from '..'
|
|
3
|
-
|
|
4
|
-
jest.mock('fs')
|
|
5
|
-
|
|
6
|
-
describe('File Reading Functions', () => {
|
|
7
|
-
const mockFilePath = '/mock/path/file.txt'
|
|
8
|
-
const mockJsonPath = '/mock/path/data.json'
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
jest.clearAllMocks()
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('should read file content correctly', () => {
|
|
15
|
-
const mockContent = 'Hello, world!'
|
|
16
|
-
;(fs.readFileSync as jest.Mock).mockReturnValue(mockContent)
|
|
17
|
-
const result = readFileContent(mockFilePath)
|
|
18
|
-
expect(fs.readFileSync).toHaveBeenCalledWith(mockFilePath, 'utf-8')
|
|
19
|
-
expect(result).toBe(mockContent)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('should read JSON content correctly', () => {
|
|
23
|
-
const mockJsonContent = '{"name": "Alice", "age": 30}'
|
|
24
|
-
;(fs.readFileSync as jest.Mock).mockReturnValue(mockJsonContent)
|
|
25
|
-
const result = readJsonContent(mockJsonPath)
|
|
26
|
-
expect(fs.readFileSync).toHaveBeenCalledWith(mockJsonPath, 'utf-8')
|
|
27
|
-
expect(result).toEqual(JSON.parse(mockJsonContent))
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should throw an error when JSON is invalid', () => {
|
|
31
|
-
const invalidJson = '{name: Alice, age: 30}'
|
|
32
|
-
;(fs.readFileSync as jest.Mock).mockReturnValue(invalidJson)
|
|
33
|
-
expect(() => readJsonContent(mockJsonPath)).toThrow(SyntaxError)
|
|
34
|
-
})
|
|
35
|
-
})
|