@flowerforce/flowerbase 1.7.5 → 1.7.6-beta.1

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 (108) hide show
  1. package/README.md +125 -1
  2. package/dist/auth/controller.d.ts.map +1 -1
  3. package/dist/auth/controller.js +11 -10
  4. package/dist/auth/plugins/jwt.js +1 -1
  5. package/dist/auth/providers/anon-user/controller.js +1 -1
  6. package/dist/auth/providers/custom-function/controller.d.ts.map +1 -1
  7. package/dist/auth/providers/custom-function/controller.js +28 -7
  8. package/dist/auth/providers/local-userpass/controller.d.ts.map +1 -1
  9. package/dist/auth/providers/local-userpass/controller.js +15 -14
  10. package/dist/auth/utils.d.ts +1 -0
  11. package/dist/auth/utils.d.ts.map +1 -1
  12. package/dist/constants.d.ts +11 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +14 -3
  15. package/dist/features/encryption/interface.d.ts +36 -0
  16. package/dist/features/encryption/interface.d.ts.map +1 -0
  17. package/dist/features/encryption/interface.js +2 -0
  18. package/dist/features/encryption/utils.d.ts +9 -0
  19. package/dist/features/encryption/utils.d.ts.map +1 -0
  20. package/dist/features/encryption/utils.js +34 -0
  21. package/dist/features/rules/utils.d.ts.map +1 -1
  22. package/dist/features/rules/utils.js +1 -11
  23. package/dist/features/triggers/index.d.ts.map +1 -1
  24. package/dist/features/triggers/index.js +5 -1
  25. package/dist/features/triggers/utils.js +3 -3
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -4
  29. package/dist/monitoring/plugin.d.ts.map +1 -1
  30. package/dist/monitoring/plugin.js +31 -0
  31. package/dist/monitoring/routes/users.d.ts.map +1 -1
  32. package/dist/monitoring/routes/users.js +7 -6
  33. package/dist/monitoring/utils.d.ts.map +1 -1
  34. package/dist/monitoring/utils.js +5 -4
  35. package/dist/services/api/index.d.ts +4 -0
  36. package/dist/services/api/index.d.ts.map +1 -1
  37. package/dist/services/api/utils.d.ts +1 -0
  38. package/dist/services/api/utils.d.ts.map +1 -1
  39. package/dist/services/index.d.ts +4 -0
  40. package/dist/services/index.d.ts.map +1 -1
  41. package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
  42. package/dist/services/mongodb-atlas/index.js +9 -7
  43. package/dist/services/mongodb-atlas/model.d.ts +2 -1
  44. package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
  45. package/dist/shared/handleUserDeletion.js +1 -1
  46. package/dist/shared/handleUserRegistration.js +2 -2
  47. package/dist/utils/context/helpers.d.ts +12 -0
  48. package/dist/utils/context/helpers.d.ts.map +1 -1
  49. package/dist/utils/index.d.ts +1 -0
  50. package/dist/utils/index.d.ts.map +1 -1
  51. package/dist/utils/index.js +14 -3
  52. package/dist/utils/initializer/exposeRoutes.js +1 -1
  53. package/dist/utils/initializer/mongodbCSFLE.d.ts +69 -0
  54. package/dist/utils/initializer/mongodbCSFLE.d.ts.map +1 -0
  55. package/dist/utils/initializer/mongodbCSFLE.js +131 -0
  56. package/dist/utils/initializer/registerPlugins.d.ts +5 -1
  57. package/dist/utils/initializer/registerPlugins.d.ts.map +1 -1
  58. package/dist/utils/initializer/registerPlugins.js +27 -5
  59. package/dist/utils/rules-matcher/interface.d.ts +5 -1
  60. package/dist/utils/rules-matcher/interface.d.ts.map +1 -1
  61. package/dist/utils/rules-matcher/interface.js +2 -0
  62. package/dist/utils/rules-matcher/utils.d.ts.map +1 -1
  63. package/dist/utils/rules-matcher/utils.js +51 -16
  64. package/package.json +4 -2
  65. package/src/auth/__tests__/controller.test.ts +1 -0
  66. package/src/auth/controller.ts +12 -11
  67. package/src/auth/plugins/jwt.ts +2 -2
  68. package/src/auth/providers/anon-user/__tests__/controller.test.ts +1 -0
  69. package/src/auth/providers/anon-user/controller.ts +2 -2
  70. package/src/auth/providers/custom-function/controller.ts +29 -8
  71. package/src/auth/providers/local-userpass/controller.ts +16 -15
  72. package/src/auth/utils.ts +1 -0
  73. package/src/constants.ts +14 -4
  74. package/src/features/encryption/interface.ts +46 -0
  75. package/src/features/encryption/utils.ts +22 -0
  76. package/src/features/rules/utils.ts +1 -11
  77. package/src/features/triggers/__tests__/index.test.ts +1 -0
  78. package/src/features/triggers/index.ts +6 -2
  79. package/src/features/triggers/utils.ts +4 -4
  80. package/src/index.ts +10 -2
  81. package/src/monitoring/plugin.ts +33 -0
  82. package/src/monitoring/routes/users.ts +8 -7
  83. package/src/monitoring/ui.collections.js +7 -10
  84. package/src/monitoring/ui.css +383 -1
  85. package/src/monitoring/ui.endpoints.js +5 -10
  86. package/src/monitoring/ui.events.js +4 -6
  87. package/src/monitoring/ui.functions.js +64 -71
  88. package/src/monitoring/ui.html +8 -0
  89. package/src/monitoring/ui.js +189 -0
  90. package/src/monitoring/ui.shared.js +239 -3
  91. package/src/monitoring/ui.triggers.js +2 -3
  92. package/src/monitoring/ui.users.js +5 -9
  93. package/src/monitoring/utils.ts +6 -5
  94. package/src/services/mongodb-atlas/index.ts +10 -13
  95. package/src/services/mongodb-atlas/model.ts +3 -1
  96. package/src/shared/handleUserDeletion.ts +2 -2
  97. package/src/shared/handleUserRegistration.ts +3 -3
  98. package/src/types/fastify-raw-body.d.ts +0 -9
  99. package/src/utils/__tests__/mongodbCSFLE.test.ts +105 -0
  100. package/src/utils/__tests__/operators.test.ts +24 -0
  101. package/src/utils/__tests__/rule.test.ts +39 -0
  102. package/src/utils/__tests__/rulesMatcherInterfaces.test.ts +2 -0
  103. package/src/utils/index.ts +12 -1
  104. package/src/utils/initializer/exposeRoutes.ts +2 -2
  105. package/src/utils/initializer/mongodbCSFLE.ts +224 -0
  106. package/src/utils/initializer/registerPlugins.ts +45 -10
  107. package/src/utils/rules-matcher/interface.ts +5 -1
  108. package/src/utils/rules-matcher/utils.ts +78 -32
@@ -1,3 +1,4 @@
1
+ import { ObjectId } from 'mongodb'
1
2
  import rulesMatcherUtils from '../rules-matcher/utils'
2
3
 
3
4
  describe('rule function', () => {
@@ -46,4 +47,42 @@ describe('rule function', () => {
46
47
  rulesMatcherUtils.rule(missingOperatorBlock, mockData, mockOptions)
47
48
  }).toThrow('Error missing operator:$notFoundOperator')
48
49
  })
50
+
51
+ it('should support %stringToOid with $ref values', () => {
52
+ const companyId = new ObjectId()
53
+ const data = {
54
+ user: {
55
+ _id: companyId
56
+ },
57
+ auth: {
58
+ company: companyId.toHexString()
59
+ }
60
+ }
61
+
62
+ const result = rulesMatcherUtils.rule({ _id: { '%stringToOid': '$ref:auth.company' } }, data, {
63
+ prefix: 'user'
64
+ })
65
+
66
+ expect(result.valid).toBe(true)
67
+ expect(result.name).toBe('user._id___%stringToOid')
68
+ })
69
+
70
+ it('should support %oidToString with $ref values', () => {
71
+ const authId = new ObjectId()
72
+ const data = {
73
+ user: {
74
+ authId: authId.toHexString()
75
+ },
76
+ auth: {
77
+ id: authId
78
+ }
79
+ }
80
+
81
+ const result = rulesMatcherUtils.rule({ authId: { '%oidToString': '$ref:auth.id' } }, data, {
82
+ prefix: 'user'
83
+ })
84
+
85
+ expect(result.valid).toBe(true)
86
+ expect(result.name).toBe('user.authId___%oidToString')
87
+ })
49
88
  })
@@ -22,6 +22,8 @@ describe('Enums and Types', () => {
22
22
  expect(RulesOperators.$nin).toBe('$nin')
23
23
  expect(RulesOperators.$all).toBe('$all')
24
24
  expect(RulesOperators.$regex).toBe('$regex')
25
+ expect(RulesOperators['%stringToOid']).toBe('%stringToOid')
26
+ expect(RulesOperators['%oidToString']).toBe('%oidToString')
25
27
  })
26
28
 
27
29
  it('should validate RulesOperatorsInArray type', () => {
@@ -1,5 +1,16 @@
1
- import fs from 'fs'
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
2
3
 
3
4
  export const readFileContent = (filePath: string) => fs.readFileSync(filePath, 'utf-8')
4
5
  export const readJsonContent = (filePath: string) =>
5
6
  JSON.parse(readFileContent(filePath)) as unknown
7
+
8
+ export const recursivelyCollectFiles = (dir: string): string[] => {
9
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
10
+ const fullPath = path.join(dir, entry.name)
11
+ if (entry.isDirectory()) {
12
+ return recursivelyCollectFiles(fullPath)
13
+ }
14
+ return entry.isFile() ? [fullPath] : []
15
+ })
16
+ }
@@ -2,7 +2,7 @@ import { uptime } from 'node:process'
2
2
  import { FastifyInstance } from 'fastify'
3
3
  import { RegistrationDto } from '../../auth/providers/local-userpass/dtos'
4
4
  import { AUTH_ENDPOINTS, REGISTRATION_SCHEMA } from '../../auth/utils'
5
- import { API_VERSION, AUTH_CONFIG, DB_NAME, DEFAULT_CONFIG } from '../../constants'
5
+ import { API_VERSION, AUTH_CONFIG, AUTH_DB_NAME, DEFAULT_CONFIG } from '../../constants'
6
6
  import { PROVIDER } from '../../shared/models/handleUserRegistration.model'
7
7
  import { hashPassword } from '../crypto'
8
8
 
@@ -46,7 +46,7 @@ export const exposeRoutes = async (fastify: FastifyInstance) => {
46
46
  schema: REGISTRATION_SCHEMA
47
47
  }, async function (req, res) {
48
48
  const { authCollection } = AUTH_CONFIG
49
- const db = fastify.mongo.client.db(DB_NAME)
49
+ const db = fastify.mongo.client.db(AUTH_DB_NAME)
50
50
  const { email, password } = req.body
51
51
  const hashedPassword = await hashPassword(password)
52
52
  const now = new Date()
@@ -0,0 +1,224 @@
1
+ import {
2
+ ClientEncryption,
3
+ MongoClient,
4
+ UUID,
5
+ Binary,
6
+ type Document,
7
+ type AWSEncryptionKeyOptions,
8
+ type AWSKMSProviderConfiguration,
9
+ type AzureEncryptionKeyOptions,
10
+ type AzureKMSProviderConfiguration,
11
+ type GCPKMSProviderConfiguration,
12
+ type GCPEncryptionKeyOptions,
13
+ type KMIPKMSProviderConfiguration,
14
+ type KMIPEncryptionKeyOptions,
15
+ type LocalKMSProviderConfiguration,
16
+ type AutoEncryptionExtraOptions,
17
+ type KMSProviders,
18
+ type AutoEncryptionOptions,
19
+ } from "mongodb";
20
+ import { EncryptionSchemaProperty, MappedEncryptionSchema, MappedEncryptionSchemaProperty, type EncryptionSchemas } from "../../features/encryption/interface";
21
+ import { DEFAULT_CONFIG } from "../../constants";
22
+
23
+ type KMSProviderConfig =
24
+ | {
25
+ /**
26
+ * The alias of the key. It must be referenced in the schema map
27
+ * to select which key to use for encryption.
28
+ */
29
+ keyAlias: string
30
+ /**
31
+ * KMS Provider name.
32
+ */
33
+ provider: "aws"
34
+ /**
35
+ * KMS Provider specific authorization configuration.
36
+ */
37
+ config: AWSKMSProviderConfiguration
38
+ /**
39
+ * Configuration of the master key.
40
+ */
41
+ masterKey: AWSEncryptionKeyOptions
42
+ } | {
43
+ keyAlias: string
44
+ provider: "azure"
45
+ config: AzureKMSProviderConfiguration,
46
+ masterKey: AzureEncryptionKeyOptions
47
+ } | {
48
+ keyAlias: string
49
+ provider: "gcp"
50
+ config: GCPKMSProviderConfiguration,
51
+ masterKey: GCPEncryptionKeyOptions
52
+ } | {
53
+ keyAlias: string
54
+ provider: "kmip"
55
+ config: KMIPKMSProviderConfiguration,
56
+ masterKey: KMIPEncryptionKeyOptions
57
+ } | {
58
+ keyAlias: string
59
+ provider: "local"
60
+ config: LocalKMSProviderConfiguration,
61
+ }
62
+
63
+ export type MongoDbEncryptionConfig = {
64
+ kmsProviders: KMSProviderConfig[],
65
+ /**
66
+ * The Key Vault database name
67
+ * @default encryption
68
+ */
69
+ keyVaultDb?: string
70
+ /**
71
+ * The Key Vault database collection
72
+ * @default __keyVault
73
+ */
74
+ keyVaultCollection?: string
75
+ extraOptions?: AutoEncryptionExtraOptions
76
+ }
77
+
78
+ /**
79
+ * @internal
80
+ */
81
+ type RequiredConfig = Required<Omit<MongoDbEncryptionConfig, "extraOptions">>
82
+
83
+ type DataKey = { dataKeyId: UUID, dataKeyAlias: string }
84
+
85
+ async function ensureUniqueKeyAltNameIndex(db: ReturnType<MongoClient['db']>, config: RequiredConfig): Promise<void> {
86
+ await db.collection(config.keyVaultCollection).createIndex(
87
+ { keyAltNames: 1 },
88
+ {
89
+ unique: true,
90
+ partialFilterExpression: { keyAltNames: { $exists: true } },
91
+ }
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Ensure provided KMS Providers DEK keys exist in the key vault. If not, they are created.
97
+ */
98
+ async function ensureDataEncryptionKeys(
99
+ clientEncryption: ClientEncryption,
100
+ keyVaultDb: ReturnType<MongoClient['db']>,
101
+ config: RequiredConfig
102
+ ): Promise<DataKey[]> {
103
+ const keys: DataKey[] = []
104
+
105
+ for (const kmsProvider of config.kmsProviders) {
106
+ const existingKey = await keyVaultDb.collection(config.keyVaultCollection).findOne({
107
+ keyAltNames: kmsProvider.keyAlias,
108
+ });
109
+
110
+ if (existingKey?._id instanceof Binary) {
111
+ keys.push({ dataKeyId: existingKey._id, dataKeyAlias: kmsProvider.keyAlias })
112
+ continue
113
+ }
114
+
115
+ const dataKeyId = await clientEncryption.createDataKey(kmsProvider.provider, {
116
+ masterKey: "masterKey" in kmsProvider ? kmsProvider.masterKey : undefined,
117
+ keyAltNames: [kmsProvider.keyAlias],
118
+ });
119
+ console.log(`[MongoDB Encryption] Created new key with alias ${kmsProvider.keyAlias}`)
120
+ keys.push({ dataKeyId, dataKeyAlias: kmsProvider.keyAlias })
121
+ }
122
+
123
+ return keys
124
+ }
125
+
126
+ /**
127
+ * Recursively resolve key aliases in an encryption schema to their corresponding key IDs.
128
+ */
129
+ const resolveKeyAliases = (schema: EncryptionSchemaProperty, dataKeys: DataKey[]): MappedEncryptionSchemaProperty => {
130
+ if ("encrypt" in schema) {
131
+ if (!schema.encrypt.keyAlias) {
132
+ return schema
133
+ }
134
+ const keyId = dataKeys.find(k => k.dataKeyAlias === schema.encrypt.keyAlias)?.dataKeyId
135
+ if (!keyId) {
136
+ throw new Error(`Key with alias ${schema.encrypt.keyAlias} could not be found in the Key Vault.`)
137
+ }
138
+ return {
139
+ encrypt: {
140
+ bsonType: schema.encrypt.bsonType,
141
+ algorithm: schema.encrypt.algorithm,
142
+ keyId: [keyId]
143
+ }
144
+ }
145
+ }
146
+ const mappedSchema: MappedEncryptionSchema = {
147
+ bsonType: "object",
148
+ properties: Object.entries(schema.properties).reduce((acc, [property, config]) => {
149
+ acc[property] = resolveKeyAliases(config, dataKeys)
150
+ return acc
151
+ }, {} as Record<string, MappedEncryptionSchemaProperty>)
152
+ }
153
+
154
+ if (schema.encryptMetadata) {
155
+ const keyId = dataKeys.find(k => k.dataKeyAlias === schema.encryptMetadata!.keyAlias)?.dataKeyId
156
+ if (!keyId) {
157
+ throw new Error(`Key with alias ${schema.encryptMetadata.keyAlias} could not be found in the Key Vault.`)
158
+ }
159
+ mappedSchema.encryptMetadata = { keyId: [keyId] }
160
+ }
161
+
162
+ return mappedSchema
163
+ }
164
+
165
+ export const buildSchemaMap = (schemas: EncryptionSchemas, dataKeys: DataKey[]) => {
166
+ return Object.entries(schemas).reduce((acc, [key, schema]) => {
167
+ acc[key] = resolveKeyAliases(schema, dataKeys)
168
+ return acc
169
+ }, {} as Record<string, Document>)
170
+ }
171
+
172
+ /**
173
+ * Setup MongoDB Client-Side Field Level Encryption (CSFLE).
174
+ * @see https://www.mongodb.com/docs/manual/core/csfle
175
+ */
176
+ export const setupMongoDbCSFLE = async (
177
+ config: MongoDbEncryptionConfig & { mongodbUrl: string; schemas?: EncryptionSchemas }
178
+ ): Promise<AutoEncryptionOptions> => {
179
+ if (config.kmsProviders.length === 0) {
180
+ throw new Error('At least one KMS Provider is required when using MongoDB encryption')
181
+ }
182
+
183
+ const requiredConfig: RequiredConfig = {
184
+ kmsProviders: config.kmsProviders,
185
+ keyVaultDb: config.keyVaultDb ?? DEFAULT_CONFIG.MONGODB_ENCRYPTION_CONFIG.keyVaultDb,
186
+ keyVaultCollection: config.keyVaultDb ?? DEFAULT_CONFIG.MONGODB_ENCRYPTION_CONFIG.keyVaultCollection,
187
+ }
188
+
189
+ const kmsProviders = requiredConfig.kmsProviders.reduce(
190
+ (acc, { provider, config }) => ({ ...acc, [provider]: config }),
191
+ {} as KMSProviders
192
+ )
193
+
194
+ const keyVaultNamespace = `${requiredConfig.keyVaultDb}.${requiredConfig.keyVaultCollection}`
195
+ const keyVaultClient = new MongoClient(config.mongodbUrl, {
196
+ maxPoolSize: 1,
197
+ autoEncryption: {
198
+ keyVaultNamespace,
199
+ kmsProviders,
200
+ extraOptions: config.extraOptions
201
+ }
202
+ });
203
+
204
+ await keyVaultClient.connect();
205
+
206
+ const keyVaultDb = keyVaultClient.db(requiredConfig.keyVaultDb);
207
+ await ensureUniqueKeyAltNameIndex(keyVaultDb, requiredConfig)
208
+
209
+ const clientEncryption = new ClientEncryption(keyVaultClient, {
210
+ keyVaultNamespace,
211
+ kmsProviders,
212
+ });
213
+
214
+ const dataKeys = await ensureDataEncryptionKeys(clientEncryption, keyVaultDb, requiredConfig)
215
+
216
+ await keyVaultClient.close()
217
+
218
+ return {
219
+ keyVaultNamespace,
220
+ kmsProviders,
221
+ schemaMap: config.schemas ? buildSchemaMap(config.schemas, dataKeys) : undefined,
222
+ extraOptions: config.extraOptions
223
+ }
224
+ }
@@ -1,5 +1,5 @@
1
1
  import cors from '@fastify/cors'
2
- import fastifyMongodb from '@fastify/mongodb'
2
+ import fastifyMongodb, { FastifyMongodbOptions } from '@fastify/mongodb'
3
3
  import { FastifyInstance } from 'fastify'
4
4
  import fastifyRawBody from 'fastify-raw-body'
5
5
  import { CorsConfig } from '../../'
@@ -10,7 +10,9 @@ import { customFunctionController } from '../../auth/providers/custom-function/c
10
10
  import { localUserPassController } from '../../auth/providers/local-userpass/controller'
11
11
  import { API_VERSION, DEFAULT_CONFIG } from '../../constants'
12
12
  import { Functions } from '../../features/functions/interface'
13
+ import { EncryptionSchemas } from '../../features/encryption/interface'
13
14
  import monitoringPlugin from '../../monitoring/plugin'
15
+ import { setupMongoDbCSFLE, MongoDbEncryptionConfig } from './mongodbCSFLE'
14
16
 
15
17
  type RegisterFunction = FastifyInstance['register']
16
18
  type RegisterParameters = Parameters<RegisterFunction>
@@ -21,6 +23,8 @@ type RegisterPluginsParams = {
21
23
  jwtSecret: string
22
24
  functionsList: Functions
23
25
  corsConfig?: CorsConfig
26
+ encryptionSchemas?: EncryptionSchemas
27
+ mongodbEncryptionConfig?: MongoDbEncryptionConfig
24
28
  }
25
29
 
26
30
  type RegisterConfig = {
@@ -41,14 +45,18 @@ export const registerPlugins = async ({
41
45
  mongodbUrl,
42
46
  jwtSecret,
43
47
  functionsList,
44
- corsConfig
48
+ corsConfig,
49
+ mongodbEncryptionConfig,
50
+ encryptionSchemas
45
51
  }: RegisterPluginsParams) => {
46
52
  try {
47
53
  const registersConfig = await getRegisterConfig({
48
54
  mongodbUrl,
49
55
  jwtSecret,
50
56
  corsConfig,
51
- functionsList
57
+ functionsList,
58
+ mongodbEncryptionConfig,
59
+ encryptionSchemas
52
60
  })
53
61
 
54
62
  registersConfig.forEach(({ plugin, options, pluginName }) => {
@@ -75,15 +83,23 @@ export const registerPlugins = async ({
75
83
  const getRegisterConfig = async ({
76
84
  mongodbUrl,
77
85
  jwtSecret,
78
- corsConfig
79
- }: Pick<RegisterPluginsParams, 'jwtSecret' | 'mongodbUrl' | 'functionsList' | 'corsConfig'>): Promise<
80
- RegisterConfig[]
81
- > => {
86
+ corsConfig,
87
+ encryptionSchemas,
88
+ mongodbEncryptionConfig,
89
+ }: Omit<RegisterPluginsParams, "register">): Promise<RegisterConfig[]> => {
82
90
  const corsOptions = corsConfig ?? {
83
91
  origin: '*',
84
92
  methods: ['POST', 'GET']
85
93
  }
86
94
 
95
+ const autoEncryption = mongodbEncryptionConfig
96
+ ? await setupMongoDbCSFLE({
97
+ mongodbUrl,
98
+ schemas: encryptionSchemas,
99
+ ...mongodbEncryptionConfig
100
+ })
101
+ : undefined
102
+
87
103
  const baseConfig = [
88
104
  {
89
105
  pluginName: 'cors',
@@ -94,9 +110,28 @@ const getRegisterConfig = async ({
94
110
  pluginName: 'fastifyMongodb',
95
111
  plugin: fastifyMongodb,
96
112
  options: {
113
+ url: mongodbUrl,
97
114
  forceClose: true,
98
- url: mongodbUrl
99
- }
115
+ autoEncryption
116
+ } satisfies FastifyMongodbOptions
117
+ },
118
+ /**
119
+ * When auto-encryption is active, add another MongoDB client with bypass for change streams.
120
+ * The $changeStream operator does not support automatic encryption, only decryption.
121
+ * @see https://www.mongodb.com/docs/manual/core/csfle/reference/supported-operations
122
+ */
123
+ autoEncryption && {
124
+ pluginName: 'fastifyMongodb',
125
+ plugin: fastifyMongodb,
126
+ options: {
127
+ name: "changestream",
128
+ url: mongodbUrl,
129
+ forceClose: true,
130
+ autoEncryption: {
131
+ ...autoEncryption,
132
+ bypassAutoEncryption: true
133
+ }
134
+ } satisfies FastifyMongodbOptions
100
135
  },
101
136
  {
102
137
  pluginName: 'jwtAuthPlugin',
@@ -153,5 +188,5 @@ const getRegisterConfig = async ({
153
188
  } as RegisterConfig)
154
189
  }
155
190
 
156
- return baseConfig
191
+ return baseConfig.filter(Boolean)
157
192
  }
@@ -331,6 +331,8 @@ export type Operators = {
331
331
  * @returns
332
332
  */
333
333
  $regex: OperatorsFunction
334
+ '%stringToOid': OperatorsFunction
335
+ '%oidToString': OperatorsFunction
334
336
  }
335
337
 
336
338
  export enum RulesOperators {
@@ -349,7 +351,9 @@ export enum RulesOperators {
349
351
  $nin = '$nin',
350
352
  $all = '$all',
351
353
  $size = '$size',
352
- $regex = '$regex'
354
+ $regex = '$regex',
355
+ '%stringToOid' = '%stringToOid',
356
+ '%oidToString' = '%oidToString'
353
357
  }
354
358
 
355
359
  export type RulesOperatorsInArray<T> = Partial<{
@@ -1,9 +1,59 @@
1
+ import { ObjectId } from 'bson'
1
2
  import _get from 'lodash/get'
2
- import _intersection from 'lodash/intersection'
3
3
  import _trimStart from 'lodash/trimStart'
4
4
  import { Operators, RulesMatcherUtils, RulesObject } from './interface'
5
5
 
6
6
  const EMPTY_STRING_REGEXP = /^\s*$/
7
+ const HEX_24_REGEXP = /^[a-fA-F0-9]{24}$/
8
+
9
+ const toObjectIdHex = (value: unknown): string | null => {
10
+ if (value instanceof ObjectId) {
11
+ return value.toHexString()
12
+ }
13
+
14
+ if (typeof value === 'string') {
15
+ if (!HEX_24_REGEXP.test(value) || !ObjectId.isValid(value)) {
16
+ return null
17
+ }
18
+ return new ObjectId(value).toHexString()
19
+ }
20
+
21
+ if (!value || typeof value !== 'object') {
22
+ return null
23
+ }
24
+
25
+ const maybeObjectId = value as { _bsontype?: string; toHexString?: () => string }
26
+ if (maybeObjectId._bsontype === 'ObjectId' && typeof maybeObjectId.toHexString === 'function') {
27
+ const hex = maybeObjectId.toHexString()
28
+ return HEX_24_REGEXP.test(hex) ? hex.toLowerCase() : null
29
+ }
30
+
31
+ return null
32
+ }
33
+
34
+ const areSemanticallyEqual = (left: unknown, right: unknown): boolean => {
35
+ const leftOid = toObjectIdHex(left)
36
+ const rightOid = toObjectIdHex(right)
37
+
38
+ if (leftOid || rightOid) {
39
+ return leftOid !== null && rightOid !== null && leftOid === rightOid
40
+ }
41
+
42
+ return left === right
43
+ }
44
+
45
+ const includesWithSemanticEquality = (value: unknown, candidate: unknown): boolean =>
46
+ rulesMatcherUtils
47
+ .forceArray(candidate)
48
+ .some((item) =>
49
+ rulesMatcherUtils
50
+ .forceArray(value)
51
+ .some((sourceItem) =>
52
+ rulesMatcherUtils.forceArray(item).some((candidateItem) =>
53
+ areSemanticallyEqual(sourceItem, candidateItem)
54
+ )
55
+ )
56
+ )
7
57
 
8
58
  /**
9
59
  * Defines a utility object named rulesMatcherUtils, which contains various helper functions used for processing rules and data in a rule-matching context.
@@ -24,10 +74,10 @@ const rulesMatcherUtils: RulesMatcherUtils = {
24
74
  const valueRef =
25
75
  value && String(value).indexOf('$ref:') === 0
26
76
  ? _get(
27
- data,
28
- rulesMatcherUtils.getPath(value.replace('$ref:', ''), prefix),
29
- undefined
30
- )
77
+ data,
78
+ rulesMatcherUtils.getPath(value.replace('$ref:', ''), prefix),
79
+ undefined
80
+ )
31
81
  : value
32
82
 
33
83
  if (!operators[op]) {
@@ -230,9 +280,9 @@ const rulesMatcherUtils: RulesMatcherUtils = {
230
280
  export const operators: Operators = {
231
281
  $exists: (a, b) => !rulesMatcherUtils.isEmpty(a) === b,
232
282
 
233
- $eq: (a, b) => a === b,
283
+ $eq: (a, b) => areSemanticallyEqual(a, b),
234
284
 
235
- $ne: (a, b) => a !== b,
285
+ $ne: (a, b) => !areSemanticallyEqual(a, b),
236
286
 
237
287
  $gt: (a, b) => rulesMatcherUtils.forceNumber(a) > parseFloat(b),
238
288
 
@@ -250,39 +300,35 @@ export const operators: Operators = {
250
300
 
251
301
  $strLte: (a, b) => String(a || '').length <= parseFloat(b),
252
302
 
253
- $in: (a, b) =>
254
- rulesMatcherUtils
255
- .forceArray(b)
256
- .some(
257
- (c) =>
258
- _intersection(rulesMatcherUtils.forceArray(a), rulesMatcherUtils.forceArray(c))
259
- .length
260
- ),
261
-
262
- $nin: (a, b) =>
263
- !rulesMatcherUtils
264
- .forceArray(b)
265
- .some(
266
- (c) =>
267
- _intersection(rulesMatcherUtils.forceArray(a), rulesMatcherUtils.forceArray(c))
268
- .length
269
- ),
303
+ $in: (a, b) => includesWithSemanticEquality(a, b),
304
+
305
+ $nin: (a, b) => !includesWithSemanticEquality(a, b),
270
306
 
271
307
  $all: (a, b) =>
272
- rulesMatcherUtils
273
- .forceArray(b)
274
- .every(
275
- (c) =>
276
- _intersection(rulesMatcherUtils.forceArray(a), rulesMatcherUtils.forceArray(c))
277
- .length
278
- ),
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
+ ),
279
315
 
280
316
  $size: (a, b) => Array.isArray(a) && a.length === parseFloat(b),
281
317
 
282
318
  $regex: (a, b, opt) =>
283
319
  rulesMatcherUtils
284
320
  .forceArray(b)
285
- .some((c) => (c instanceof RegExp ? c.test(a) : new RegExp(c, opt).test(a)))
321
+ .some((c) => (c instanceof RegExp ? c.test(a) : new RegExp(c, opt).test(a))),
322
+
323
+ '%stringToOid': (a, b) => {
324
+ const converted = toObjectIdHex(b)
325
+ return converted !== null && areSemanticallyEqual(a, converted)
326
+ },
327
+
328
+ '%oidToString': (a, b) => {
329
+ const converted = toObjectIdHex(b)
330
+ return converted !== null && areSemanticallyEqual(a, converted)
331
+ }
286
332
  }
287
333
 
288
334
  // export default operators