@flowerforce/flowerbase 1.7.3 → 1.7.4-beta.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/features/functions/controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AA2ChD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,kBAmKjC,CAAA"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../src/features/functions/controller.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AA8ChD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,EAAE,kBAqKjC,CAAA"}
@@ -46,6 +46,7 @@ const isReturnedError = (value) => {
46
46
  const candidate = value;
47
47
  return typeof candidate.message === 'string' && typeof candidate.name === 'string';
48
48
  };
49
+ const serializeEjson = (value) => JSON.stringify(bson_1.EJSON.serialize(value, { relaxed: false }));
49
50
  /**
50
51
  * > Creates a pre handler for every query
51
52
  * @param app -> the fastify instance
@@ -89,7 +90,9 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
89
90
  pipeline,
90
91
  isClient: true
91
92
  });
92
- return operatorsByType[method]();
93
+ const serviceResult = yield operatorsByType[method]();
94
+ res.type('application/json');
95
+ return serializeEjson(serviceResult);
93
96
  }
94
97
  const currentFunction = functionsList[method];
95
98
  if (!currentFunction) {
@@ -115,7 +118,7 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
115
118
  return JSON.stringify({ message: result.message, name: result.name });
116
119
  }
117
120
  res.type('application/json');
118
- return JSON.stringify(bson_1.EJSON.serialize(result, { relaxed: false }));
121
+ return serializeEjson(result);
119
122
  }
120
123
  catch (error) {
121
124
  res.status(400);
@@ -157,7 +160,7 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
157
160
  const changeStream = streams[requestKey];
158
161
  if (changeStream) {
159
162
  changeStream.on('change', (change) => {
160
- res.raw.write(`data: ${JSON.stringify(change)}\n\n`);
163
+ res.raw.write(`data: ${serializeEjson(change)}\n\n`);
161
164
  });
162
165
  req.raw.on('close', () => {
163
166
  var _a;
@@ -175,7 +178,7 @@ const functionsController = (app_1, _a) => __awaiter(void 0, [app_1, _a], void 0
175
178
  .collection(collection)
176
179
  .watch([], { fullDocument: 'whenAvailable' });
177
180
  streams[requestKey].on('change', (change) => {
178
- res.raw.write(`data: ${JSON.stringify(change)}\n\n`);
181
+ res.raw.write(`data: ${serializeEjson(change)}\n\n`);
179
182
  });
180
183
  }));
181
184
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAuBA,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;AA6tChB,QAAA,MAAM,YAAY,EAAE,oBA6BlB,CAAA;AAEF,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAuBA,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;AA0uChB,QAAA,MAAM,YAAY,EAAE,oBA6BlB,CAAA;AAEF,eAAe,YAAY,CAAA"}
@@ -597,27 +597,40 @@ const getOperators = (collection, { rules, collName, user, run_as_system, monito
597
597
  const safeQuery = Array.isArray(formattedQuery)
598
598
  ? (0, utils_3.normalizeQuery)(formattedQuery)
599
599
  : formattedQuery;
600
- const result = yield collection.findOne(buildAndQuery(safeQuery));
601
- if (!result) {
602
- throw new Error('Update not permitted');
603
- }
604
- const winningRole = (0, utils_2.getWinningRole)(result, user, roles);
605
600
  const normalizedData = Array.isArray(data)
606
601
  ? data
607
602
  : normalizeUpdatePayload(data);
603
+ const currentDoc = yield collection.findOne(buildAndQuery(safeQuery));
608
604
  const updatedPaths = Array.isArray(normalizedData)
609
605
  ? []
610
606
  : getUpdatedPaths(normalizedData);
611
- const [docToCheck] = Array.isArray(normalizedData)
612
- ? yield collection.aggregate([
613
- { $match: buildAndQuery(safeQuery) },
614
- { $limit: 1 },
615
- ...normalizedData
616
- ]).toArray()
617
- : [applyDocumentUpdateOperators(result, normalizedData)];
607
+ let docToCheck;
608
+ let validationType = 'write';
609
+ if (!currentDoc) {
610
+ if (!(normalizedOptions === null || normalizedOptions === void 0 ? void 0 : normalizedOptions.upsert) || Array.isArray(normalizedData)) {
611
+ throw new Error('Update not permitted');
612
+ }
613
+ const updateDocument = normalizedData;
614
+ const setOnInsertSeed = isPlainObject(updateDocument.$setOnInsert)
615
+ ? updateDocument.$setOnInsert
616
+ : {};
617
+ docToCheck = applyDocumentUpdateOperators(setOnInsertSeed, updateDocument);
618
+ validationType = 'insert';
619
+ }
620
+ else {
621
+ const [computedDoc] = Array.isArray(normalizedData)
622
+ ? yield collection.aggregate([
623
+ { $match: buildAndQuery(safeQuery) },
624
+ { $limit: 1 },
625
+ ...normalizedData
626
+ ]).toArray()
627
+ : [applyDocumentUpdateOperators(currentDoc, normalizedData)];
628
+ docToCheck = computedDoc;
629
+ }
630
+ const winningRole = (0, utils_2.getWinningRole)(docToCheck, user, roles);
618
631
  const { status, document } = winningRole
619
632
  ? yield (0, machines_1.checkValidation)(winningRole, {
620
- type: 'write',
633
+ type: validationType,
621
634
  roles,
622
635
  cursor: docToCheck,
623
636
  expansions: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowerforce/flowerbase",
3
- "version": "1.7.3",
3
+ "version": "1.7.4-beta.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,6 +48,9 @@ const isReturnedError = (value: unknown): value is { message: string; name: stri
48
48
  return typeof candidate.message === 'string' && typeof candidate.name === 'string'
49
49
  }
50
50
 
51
+ const serializeEjson = (value: unknown) =>
52
+ JSON.stringify(EJSON.serialize(value, { relaxed: false }))
53
+
51
54
  /**
52
55
  * > Creates a pre handler for every query
53
56
  * @param app -> the fastify instance
@@ -111,7 +114,9 @@ export const functionsController: FunctionController = async (
111
114
  pipeline,
112
115
  isClient: true
113
116
  })
114
- return operatorsByType[method as keyof typeof operatorsByType]()
117
+ const serviceResult = await operatorsByType[method as keyof typeof operatorsByType]()
118
+ res.type('application/json')
119
+ return serializeEjson(serviceResult)
115
120
  }
116
121
 
117
122
  const currentFunction = functionsList[method]
@@ -141,7 +146,7 @@ export const functionsController: FunctionController = async (
141
146
  return JSON.stringify({ message: result.message, name: result.name })
142
147
  }
143
148
  res.type('application/json')
144
- return JSON.stringify(EJSON.serialize(result, { relaxed: false }));
149
+ return serializeEjson(result)
145
150
  } catch (error) {
146
151
  res.status(400)
147
152
  res.type('application/json')
@@ -193,7 +198,7 @@ export const functionsController: FunctionController = async (
193
198
 
194
199
  if (changeStream) {
195
200
  changeStream.on('change', (change) => {
196
- res.raw.write(`data: ${JSON.stringify(change)}\n\n`);
201
+ res.raw.write(`data: ${serializeEjson(change)}\n\n`);
197
202
  });
198
203
 
199
204
  req.raw.on('close', () => {
@@ -214,7 +219,7 @@ export const functionsController: FunctionController = async (
214
219
 
215
220
 
216
221
  streams[requestKey].on('change', (change) => {
217
- res.raw.write(`data: ${JSON.stringify(change)}\n\n`);
222
+ res.raw.write(`data: ${serializeEjson(change)}\n\n`);
218
223
  });
219
224
  })
220
225
  }
@@ -125,4 +125,66 @@ describe('mongodb-atlas findOneAndUpdate', () => {
125
125
  { returnDocument: 'after' }
126
126
  )
127
127
  })
128
+
129
+ it('allows upsert when no document matches', async () => {
130
+ const id = new ObjectId()
131
+ const insertedDoc = { _id: id, title: 'Created', userId: 'user-1' }
132
+ const findOne = jest.fn().mockResolvedValue(null)
133
+ const findOneAndUpdate = jest.fn().mockResolvedValue(insertedDoc)
134
+ const collection = {
135
+ collectionName: 'todos',
136
+ findOne,
137
+ findOneAndUpdate
138
+ }
139
+
140
+ const app = createAppWithCollection(collection)
141
+ const operators = MongoDbAtlas(app as any, {
142
+ rules: createRules(),
143
+ user: { id: 'user-1' }
144
+ })
145
+ .db('db')
146
+ .collection('todos')
147
+
148
+ const result = await operators.findOneAndUpdate(
149
+ { _id: id },
150
+ { $set: { title: 'Created' } },
151
+ { upsert: true }
152
+ )
153
+
154
+ expect(result).toEqual(insertedDoc)
155
+ expect(findOneAndUpdate).toHaveBeenCalledWith(
156
+ { $and: [{ _id: id }] },
157
+ { $set: { title: 'Created' } },
158
+ { upsert: true }
159
+ )
160
+ })
161
+
162
+ it('rejects upsert when insert permission is denied', async () => {
163
+ const id = new ObjectId()
164
+ const findOne = jest.fn().mockResolvedValue(null)
165
+ const findOneAndUpdate = jest.fn()
166
+ const collection = {
167
+ collectionName: 'todos',
168
+ findOne,
169
+ findOneAndUpdate
170
+ }
171
+
172
+ const app = createAppWithCollection(collection)
173
+ const operators = MongoDbAtlas(app as any, {
174
+ rules: createRules({ insert: false }),
175
+ user: { id: 'user-1' }
176
+ })
177
+ .db('db')
178
+ .collection('todos')
179
+
180
+ await expect(
181
+ operators.findOneAndUpdate(
182
+ { _id: id },
183
+ { $set: { title: 'Created' }, $setOnInsert: { createdAt: new Date() } } as any,
184
+ { upsert: true }
185
+ )
186
+ ).rejects.toThrow('Update not permitted')
187
+
188
+ expect(findOneAndUpdate).not.toHaveBeenCalled()
189
+ })
128
190
  })
@@ -687,33 +687,46 @@ const getOperators: GetOperatorsFunction = (
687
687
  const safeQuery = Array.isArray(formattedQuery)
688
688
  ? normalizeQuery(formattedQuery)
689
689
  : formattedQuery
690
-
691
- const result = await collection.findOne(buildAndQuery(safeQuery))
692
-
693
- if (!result) {
694
- throw new Error('Update not permitted')
695
- }
696
-
697
- const winningRole = getWinningRole(result, user, roles)
698
690
  const normalizedData = Array.isArray(data)
699
691
  ? data
700
692
  : normalizeUpdatePayload(data as Document)
693
+ const currentDoc = await collection.findOne(buildAndQuery(safeQuery))
701
694
  const updatedPaths = Array.isArray(normalizedData)
702
695
  ? []
703
696
  : getUpdatedPaths(normalizedData as Document)
704
- const [docToCheck] = Array.isArray(normalizedData)
705
- ? await collection.aggregate([
706
- { $match: buildAndQuery(safeQuery) },
707
- { $limit: 1 },
708
- ...normalizedData
709
- ]).toArray()
710
- : [applyDocumentUpdateOperators(result, normalizedData as Document)]
697
+ let docToCheck: Document
698
+ let validationType: 'write' | 'insert' = 'write'
699
+
700
+ if (!currentDoc) {
701
+ if (!normalizedOptions?.upsert || Array.isArray(normalizedData)) {
702
+ throw new Error('Update not permitted')
703
+ }
704
+
705
+ const updateDocument = normalizedData as Document
706
+ const setOnInsertSeed =
707
+ isPlainObject(updateDocument.$setOnInsert)
708
+ ? (updateDocument.$setOnInsert as Document)
709
+ : {}
710
+ docToCheck = applyDocumentUpdateOperators(setOnInsertSeed, updateDocument)
711
+ validationType = 'insert'
712
+ } else {
713
+ const [computedDoc] = Array.isArray(normalizedData)
714
+ ? await collection.aggregate([
715
+ { $match: buildAndQuery(safeQuery) },
716
+ { $limit: 1 },
717
+ ...normalizedData
718
+ ]).toArray()
719
+ : [applyDocumentUpdateOperators(currentDoc, normalizedData as Document)]
720
+ docToCheck = computedDoc
721
+ }
722
+
723
+ const winningRole = getWinningRole(docToCheck, user, roles)
711
724
 
712
725
  const { status, document } = winningRole
713
726
  ? await checkValidation(
714
727
  winningRole,
715
728
  {
716
- type: 'write',
729
+ type: validationType,
717
730
  roles,
718
731
  cursor: docToCheck,
719
732
  expansions: {}