@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.
@@ -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: { _id: mockId, avatar: 'avatar.png', name: 'Alice' }
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' ? ({ ...candidate, role: customRole } as MachineContext['user']) : user
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(([key]) =>
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("rules"),
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 (maybeObjectId._bsontype === 'ObjectId' && typeof maybeObjectId.toHexString === 'function') {
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.forceArray(item).some((candidateItem) =>
53
- areSemanticallyEqual(sourceItem, candidateItem)
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
- ? _get(
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
- keys[rulesMatcherUtils.getPath(value.replace('$ref:', ''), prefix)] = true
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.forceArray(b).every((candidate) =>
309
- rulesMatcherUtils
310
- .forceArray(a)
311
- .some((value) =>
312
- rulesMatcherUtils.forceArray(candidate).some((item) => areSemanticallyEqual(value, item))
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
- })