@flowerforce/flowerbase 1.7.2 → 1.7.3-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.
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.GenerateContext = GenerateContext;
16
+ exports.GenerateContextSync = GenerateContextSync;
16
17
  const node_module_1 = require("node:module");
17
18
  const node_path_1 = __importDefault(require("node:path"));
18
19
  const node_url_1 = require("node:url");
@@ -108,6 +109,34 @@ const shouldFallbackFromVmModules = (error) => {
108
109
  const code = error.code;
109
110
  return code === 'ERR_VM_MODULES_DISABLED' || code === 'ERR_VM_MODULES_NOT_SUPPORTED';
110
111
  };
112
+ const isExportedFunction = (value) => typeof value === 'function';
113
+ const getDefaultExport = (value) => {
114
+ if (!value || typeof value !== 'object')
115
+ return undefined;
116
+ if (!('default' in value))
117
+ return undefined;
118
+ const maybeDefault = value.default;
119
+ return isExportedFunction(maybeDefault) ? maybeDefault : undefined;
120
+ };
121
+ const resolveExport = (ctx) => {
122
+ var _a, _b, _c, _d, _e;
123
+ const moduleExports = (_b = (_a = ctx.module) === null || _a === void 0 ? void 0 : _a.exports) !== null && _b !== void 0 ? _b : (_c = ctx.__fb_module) === null || _c === void 0 ? void 0 : _c.exports;
124
+ if (isExportedFunction(moduleExports))
125
+ return moduleExports;
126
+ const contextExports = (_d = ctx.exports) !== null && _d !== void 0 ? _d : ctx.__fb_exports;
127
+ if (isExportedFunction(contextExports))
128
+ return contextExports;
129
+ return (_e = getDefaultExport(moduleExports)) !== null && _e !== void 0 ? _e : getDefaultExport(contextExports);
130
+ };
131
+ const buildVmContext = (contextData) => {
132
+ var _a, _b;
133
+ const sandboxModule = { exports: {} };
134
+ const entryFile = (_b = (_a = require.main) === null || _a === void 0 ? void 0 : _a.filename) !== null && _b !== void 0 ? _b : process.cwd();
135
+ const customRequire = (0, node_module_1.createRequire)(entryFile);
136
+ const vmContext = vm_1.default.createContext(Object.assign(Object.assign({}, contextData), { require: customRequire, exports: sandboxModule.exports, module: sandboxModule, __filename,
137
+ __dirname, __fb_require: customRequire, __fb_filename: __filename, __fb_dirname: __dirname }));
138
+ return { sandboxModule, entryFile, customRequire, vmContext };
139
+ };
111
140
  /**
112
141
  * > Used to generate the current context
113
142
  * @testable
@@ -126,7 +155,7 @@ function GenerateContext(_a) {
126
155
  const functionsQueue = state_1.StateManager.select("functionsQueue");
127
156
  const functionToRun = Object.assign({ run_as_system: runAsSystem }, currentFunction);
128
157
  const run = () => __awaiter(this, void 0, void 0, function* () {
129
- var _a, _b, _c;
158
+ var _a;
130
159
  const contextData = (0, helpers_1.generateContextData)({
131
160
  user,
132
161
  services,
@@ -136,32 +165,10 @@ function GenerateContext(_a) {
136
165
  functionName,
137
166
  functionsList,
138
167
  GenerateContext,
168
+ GenerateContextSync,
139
169
  request
140
170
  });
141
- const isExportedFunction = (value) => typeof value === 'function';
142
- const getDefaultExport = (value) => {
143
- if (!value || typeof value !== 'object')
144
- return undefined;
145
- if (!('default' in value))
146
- return undefined;
147
- const maybeDefault = value.default;
148
- return isExportedFunction(maybeDefault) ? maybeDefault : undefined;
149
- };
150
- const resolveExport = (ctx) => {
151
- var _a, _b, _c, _d, _e;
152
- const moduleExports = (_b = (_a = ctx.module) === null || _a === void 0 ? void 0 : _a.exports) !== null && _b !== void 0 ? _b : (_c = ctx.__fb_module) === null || _c === void 0 ? void 0 : _c.exports;
153
- if (isExportedFunction(moduleExports))
154
- return moduleExports;
155
- const contextExports = (_d = ctx.exports) !== null && _d !== void 0 ? _d : ctx.__fb_exports;
156
- if (isExportedFunction(contextExports))
157
- return contextExports;
158
- return (_e = getDefaultExport(moduleExports)) !== null && _e !== void 0 ? _e : getDefaultExport(contextExports);
159
- };
160
- const sandboxModule = { exports: {} };
161
- const entryFile = (_b = (_a = require.main) === null || _a === void 0 ? void 0 : _a.filename) !== null && _b !== void 0 ? _b : process.cwd();
162
- const customRequire = (0, node_module_1.createRequire)(entryFile);
163
- const vmContext = vm_1.default.createContext(Object.assign(Object.assign({}, contextData), { require: customRequire, exports: sandboxModule.exports, module: sandboxModule, __filename,
164
- __dirname, __fb_require: customRequire, __fb_filename: __filename, __fb_dirname: __dirname }));
171
+ const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(contextData);
165
172
  const vmModules = vm_1.default;
166
173
  const hasStaticImport = /\bimport\s+/.test(functionToRun.code);
167
174
  let usedVmModules = false;
@@ -211,7 +218,7 @@ function GenerateContext(_a) {
211
218
  : functionToRun.code;
212
219
  vm_1.default.runInContext(codeToRun, vmContext);
213
220
  }
214
- sandboxModule.exports = (_c = resolveExport(vmContext)) !== null && _c !== void 0 ? _c : sandboxModule.exports;
221
+ sandboxModule.exports = (_a = resolveExport(vmContext)) !== null && _a !== void 0 ? _a : sandboxModule.exports;
215
222
  if (deserializeArgs) {
216
223
  return yield sandboxModule.exports(...bson_1.EJSON.deserialize(args));
217
224
  }
@@ -221,3 +228,32 @@ function GenerateContext(_a) {
221
228
  return res;
222
229
  });
223
230
  }
231
+ function GenerateContextSync({ args, app, rules, user, currentFunction, functionsList, services, functionName, runAsSystem, deserializeArgs = true, request }) {
232
+ var _a;
233
+ if (!currentFunction)
234
+ return;
235
+ const functionToRun = Object.assign({ run_as_system: runAsSystem }, currentFunction);
236
+ const contextData = (0, helpers_1.generateContextData)({
237
+ user,
238
+ services,
239
+ app,
240
+ rules,
241
+ currentFunction: functionToRun,
242
+ functionName,
243
+ functionsList,
244
+ GenerateContext,
245
+ GenerateContextSync,
246
+ request
247
+ });
248
+ const { sandboxModule, vmContext } = buildVmContext(contextData);
249
+ const codeToRun = functionToRun.code.includes('import ')
250
+ ? transformImportsToRequire(functionToRun.code)
251
+ : functionToRun.code;
252
+ vm_1.default.runInContext(codeToRun, vmContext);
253
+ sandboxModule.exports = (_a = resolveExport(vmContext)) !== null && _a !== void 0 ? _a : sandboxModule.exports;
254
+ const fn = sandboxModule.exports;
255
+ if (deserializeArgs) {
256
+ return fn(...bson_1.EJSON.deserialize(args));
257
+ }
258
+ return fn(...args);
259
+ }
@@ -20,6 +20,7 @@ export interface GenerateContextParams {
20
20
  type ContextRequest = Pick<FastifyRequest, "ips" | "host" | "hostname" | "url" | "method" | "ip" | "id">;
21
21
  export interface GenerateContextDataParams extends Omit<GenerateContextParams, 'args'> {
22
22
  GenerateContext: (params: GenerateContextParams) => Promise<unknown>;
23
+ GenerateContextSync: (params: GenerateContextParams) => unknown;
23
24
  }
24
25
  export {};
25
26
  //# sourceMappingURL=interface.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/utils/context/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAA;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAEnD,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,eAAe,CAAA;IACpB,eAAe,EAAE,QAAQ,CAAA;IACzB,aAAa,EAAE,SAAS,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,KAAK,CAAA;IACZ,IAAI,EAAE,IAAI,CAAA;IACV,QAAQ,EAAE,QAAQ,CAAA;IAClB,IAAI,EAAE,SAAS,CAAA;IACf,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,cAAc,CAAA;CACzB;AAED,KAAK,cAAc,GAAG,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC,CAAA;AACxG,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACpF,eAAe,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACrE"}
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/utils/context/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAA;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,gCAAgC,CAAA;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAEnD,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,eAAe,CAAA;IACpB,eAAe,EAAE,QAAQ,CAAA;IACzB,aAAa,EAAE,SAAS,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,EAAE,KAAK,CAAA;IACZ,IAAI,EAAE,IAAI,CAAA;IACV,QAAQ,EAAE,QAAQ,CAAA;IAClB,IAAI,EAAE,SAAS,CAAA;IACf,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,cAAc,CAAA;CACzB;AAED,KAAK,cAAc,GAAG,IAAI,CAAC,cAAc,EAAE,KAAK,GAAG,MAAM,GAAG,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,IAAI,GAAG,IAAI,CAAC,CAAA;AACxG,MAAM,WAAW,yBAA0B,SAAQ,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACpF,eAAe,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACpE,mBAAmB,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAA;CAChE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.7.2",
3
+ "version": "1.7.3-beta.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +1,4 @@
1
- import { ObjectId } from 'bson'
1
+ import { EJSON, ObjectId } from 'bson'
2
2
  import type { FastifyRequest } from 'fastify'
3
3
  import { ChangeStream, Document } from 'mongodb';
4
4
  import { services } from '../../services'
@@ -141,7 +141,7 @@ export const functionsController: FunctionController = async (
141
141
  return JSON.stringify({ message: result.message, name: result.name })
142
142
  }
143
143
  res.type('application/json')
144
- return JSON.stringify(result)
144
+ return JSON.stringify(EJSON.serialize(result, { relaxed: false }));
145
145
  } catch (error) {
146
146
  res.status(400)
147
147
  res.type('application/json')
@@ -60,7 +60,6 @@ describe('mongodb-atlas findOneAndUpdate', () => {
60
60
  const result = await operators.findOneAndUpdate({ _id: id }, { $set: { title: 'New' } })
61
61
 
62
62
  expect(findOne).toHaveBeenCalled()
63
- expect(aggregate).toHaveBeenCalled()
64
63
  expect(findOneAndUpdate).toHaveBeenCalledWith(
65
64
  { $and: [{ _id: id }] },
66
65
  { $set: { title: 'New' } }
@@ -0,0 +1,274 @@
1
+ import { ObjectId } from 'mongodb'
2
+ import MongoDbAtlas from '..'
3
+ import { Role, Rules } from '../../../features/rules/interface'
4
+
5
+ const createAppWithCollection = (collection: Record<string, unknown>) => ({
6
+ mongo: {
7
+ client: {
8
+ db: jest.fn().mockReturnValue({
9
+ collection: jest.fn().mockReturnValue(collection)
10
+ })
11
+ }
12
+ }
13
+ })
14
+
15
+ const createRules = (roleOverrides: Partial<Role> = {}): Rules => ({
16
+ todos: {
17
+ database: 'db',
18
+ collection: 'todos',
19
+ filters: [],
20
+ roles: [
21
+ {
22
+ name: 'owner',
23
+ apply_when: {},
24
+ insert: true,
25
+ delete: true,
26
+ search: true,
27
+ read: true,
28
+ write: true,
29
+ ...roleOverrides
30
+ }
31
+ ]
32
+ }
33
+ })
34
+
35
+ describe('mongodb-atlas Realm compatibility', () => {
36
+ it('allows updateOne upsert when no document matches', async () => {
37
+ const updateResult = { acknowledged: true, matchedCount: 0, modifiedCount: 0, upsertedCount: 1 }
38
+ const id = new ObjectId()
39
+ const findOne = jest.fn().mockResolvedValue(null)
40
+ const updateOne = jest.fn().mockResolvedValue(updateResult)
41
+ const collection = {
42
+ collectionName: 'todos',
43
+ findOne,
44
+ updateOne
45
+ }
46
+
47
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
48
+ rules: createRules(),
49
+ user: { id: 'user-1' }
50
+ }).db('db').collection('todos')
51
+
52
+ const result = await operators.updateOne(
53
+ { _id: id },
54
+ { $set: { label: 'created-by-upsert' } },
55
+ { upsert: true }
56
+ )
57
+
58
+ expect(result).toEqual(updateResult)
59
+ expect(updateOne).toHaveBeenCalledWith(
60
+ { $and: [{ _id: id }] },
61
+ { $set: { label: 'created-by-upsert' } },
62
+ { upsert: true }
63
+ )
64
+ })
65
+
66
+ it('accepts plain documents in updateOne by normalizing them to $set', async () => {
67
+ const updateResult = { acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
68
+ const id = new ObjectId()
69
+ const findOne = jest.fn().mockResolvedValue({ _id: id, label: 'before' })
70
+ const aggregate = jest.fn().mockReturnValue({
71
+ toArray: jest.fn().mockResolvedValue([{ _id: id, label: 'after', count: 5 }])
72
+ })
73
+ const updateOne = jest.fn().mockResolvedValue(updateResult)
74
+ const collection = {
75
+ collectionName: 'todos',
76
+ findOne,
77
+ aggregate,
78
+ updateOne
79
+ }
80
+
81
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
82
+ rules: createRules(),
83
+ user: { id: 'user-1' }
84
+ }).db('db').collection('todos')
85
+
86
+ const replacementLikePayload = { label: 'after', count: 5 }
87
+ const result = await operators.updateOne({ _id: id }, replacementLikePayload)
88
+
89
+ expect(result).toEqual(updateResult)
90
+ expect(updateOne).toHaveBeenCalledWith(
91
+ { $and: [{ _id: id }] },
92
+ { $set: replacementLikePayload },
93
+ undefined
94
+ )
95
+ })
96
+
97
+ it('supports $inc in updateOne without using invalid aggregate stages', async () => {
98
+ const id = new ObjectId()
99
+ const findOne = jest.fn().mockResolvedValue({ _id: id, count: 1 })
100
+ const aggregate = jest.fn(() => {
101
+ throw new Error('aggregate should not be used for operator simulation')
102
+ })
103
+ const updateOne = jest.fn().mockResolvedValue({
104
+ acknowledged: true,
105
+ matchedCount: 1,
106
+ modifiedCount: 1
107
+ })
108
+ const collection = {
109
+ collectionName: 'todos',
110
+ findOne,
111
+ aggregate,
112
+ updateOne
113
+ }
114
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
115
+ rules: createRules(),
116
+ user: { id: 'user-1' }
117
+ }).db('db').collection('todos')
118
+
119
+ await operators.updateOne({ _id: id }, { $inc: { count: 1 } })
120
+
121
+ expect(updateOne).toHaveBeenCalledWith(
122
+ { $and: [{ _id: id }] },
123
+ { $inc: { count: 1 } },
124
+ undefined
125
+ )
126
+ expect(aggregate).not.toHaveBeenCalled()
127
+ })
128
+
129
+ it('supports $push in findOneAndUpdate without treating it as a pipeline stage', async () => {
130
+ const id = new ObjectId()
131
+ const findOne = jest.fn().mockResolvedValue({ _id: id, tags: ['old'] })
132
+ const aggregate = jest.fn(() => {
133
+ throw new Error('aggregate should not be used for operator simulation')
134
+ })
135
+ const findOneAndUpdate = jest.fn().mockResolvedValue({ _id: id, tags: ['old', 'new'] })
136
+ const collection = {
137
+ collectionName: 'todos',
138
+ findOne,
139
+ aggregate,
140
+ findOneAndUpdate
141
+ }
142
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
143
+ rules: createRules(),
144
+ user: { id: 'user-1' }
145
+ }).db('db').collection('todos')
146
+
147
+ await operators.findOneAndUpdate({ _id: id }, { $push: { tags: 'new' } } as any)
148
+
149
+ expect(findOneAndUpdate).toHaveBeenCalledWith(
150
+ { $and: [{ _id: id }] },
151
+ { $push: { tags: 'new' } }
152
+ )
153
+ expect(aggregate).not.toHaveBeenCalled()
154
+ })
155
+
156
+ it('supports operator updates in updateMany without using invalid aggregate stages', async () => {
157
+ const id = new ObjectId()
158
+ const find = jest.fn().mockReturnValue({
159
+ toArray: jest.fn().mockResolvedValue([{ _id: id, tags: ['old'] }])
160
+ })
161
+ const aggregate = jest.fn(() => {
162
+ throw new Error('aggregate should not be used for operator simulation')
163
+ })
164
+ const updateMany = jest.fn().mockResolvedValue({
165
+ acknowledged: true,
166
+ matchedCount: 1,
167
+ modifiedCount: 1
168
+ })
169
+ const collection = {
170
+ collectionName: 'todos',
171
+ find,
172
+ aggregate,
173
+ updateMany
174
+ }
175
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
176
+ rules: createRules(),
177
+ user: { id: 'user-1' }
178
+ }).db('db').collection('todos')
179
+
180
+ await operators.updateMany({ _id: id }, { $push: { tags: 'new' } } as any)
181
+
182
+ expect(updateMany).toHaveBeenCalledWith(
183
+ { $and: [{ _id: id }] },
184
+ { $push: { tags: 'new' } },
185
+ undefined
186
+ )
187
+ expect(aggregate).not.toHaveBeenCalled()
188
+ })
189
+
190
+ it('treats findOne second argument as options when it matches option keys', async () => {
191
+ const findOne = jest.fn().mockResolvedValue({ _id: new ObjectId() })
192
+ const collection = {
193
+ collectionName: 'todos',
194
+ findOne
195
+ }
196
+ const session = { id: 'tx-1' }
197
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
198
+ run_as_system: true
199
+ }).db('db').collection('todos')
200
+
201
+ await operators.findOne({ key: 'value' }, { session } as any)
202
+
203
+ expect(findOne).toHaveBeenCalledWith({ key: 'value' }, { session })
204
+ })
205
+
206
+ it('treats find second argument as options when it matches option keys', async () => {
207
+ const cursor = {
208
+ toArray: jest.fn().mockResolvedValue([]),
209
+ sort: jest.fn(),
210
+ skip: jest.fn(),
211
+ limit: jest.fn()
212
+ }
213
+ const find = jest.fn().mockReturnValue(cursor)
214
+ const collection = {
215
+ collectionName: 'todos',
216
+ find
217
+ }
218
+ const session = { id: 'tx-2' }
219
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
220
+ run_as_system: true
221
+ }).db('db').collection('todos')
222
+
223
+ operators.find({ active: true }, { session } as any)
224
+
225
+ expect(find).toHaveBeenCalledWith({ active: true }, { session })
226
+ })
227
+
228
+ it('returns insertMany insertedIds as an array', async () => {
229
+ const id0 = new ObjectId()
230
+ const id1 = new ObjectId()
231
+ const insertMany = jest.fn().mockResolvedValue({
232
+ acknowledged: true,
233
+ insertedCount: 2,
234
+ insertedIds: { 0: id0, 1: id1 }
235
+ })
236
+ const collection = {
237
+ collectionName: 'todos',
238
+ insertMany
239
+ }
240
+ const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
241
+ run_as_system: true
242
+ }).db('db').collection('todos')
243
+
244
+ const result = await operators.insertMany([{ a: 1 }, { a: 2 }])
245
+
246
+ expect(result.insertedIds).toEqual([id0, id1])
247
+ })
248
+
249
+ it('exposes startSession and delegates to the underlying MongoClient', async () => {
250
+ const mockSession = { withTransaction: jest.fn() }
251
+ const startSession = jest.fn().mockReturnValue(mockSession)
252
+ const app = {
253
+ mongo: {
254
+ client: {
255
+ startSession,
256
+ db: jest.fn().mockReturnValue({
257
+ collection: jest.fn().mockReturnValue({
258
+ collectionName: 'todos'
259
+ })
260
+ })
261
+ }
262
+ }
263
+ }
264
+
265
+ const service = MongoDbAtlas(app as any, {
266
+ run_as_system: true
267
+ })
268
+ const options = { causalConsistency: true }
269
+ const session = service.startSession(options as any)
270
+
271
+ expect(startSession).toHaveBeenCalledWith(options)
272
+ expect(session).toBe(mockSession)
273
+ })
274
+ })