@flowerforce/flowerbase 1.8.4-beta.3 → 1.8.4-beta.5
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/README.md +3 -0
- package/dist/auth/plugins/jwt.d.ts +1 -1
- package/dist/auth/plugins/jwt.d.ts.map +1 -1
- package/dist/features/functions/controller.d.ts.map +1 -1
- package/dist/features/functions/controller.js +29 -2
- package/dist/features/functions/dtos.d.ts +2 -0
- package/dist/features/functions/dtos.d.ts.map +1 -1
- package/dist/features/functions/interface.d.ts +2 -0
- package/dist/features/functions/interface.d.ts.map +1 -1
- package/dist/features/functions/utils.d.ts +3 -1
- package/dist/features/functions/utils.d.ts.map +1 -1
- package/dist/features/functions/utils.js +3 -1
- package/dist/services/index.d.ts +8 -8
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +112 -29
- package/dist/services/mongodb-atlas/model.d.ts +2 -0
- package/dist/services/mongodb-atlas/model.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.d.ts +15 -0
- package/dist/services/mongodb-atlas/utils.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/utils.js +74 -12
- package/dist/utils/context/helpers.d.ts +24 -24
- package/dist/utils/roles/machines/read/D/validators.d.ts +1 -1
- package/dist/utils/roles/machines/read/D/validators.d.ts.map +1 -1
- package/dist/utils/roles/machines/write/C/validators.d.ts +1 -1
- package/dist/utils/roles/machines/write/C/validators.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/features/functions/__tests__/controller.test.ts +42 -2
- package/src/features/functions/__tests__/utils.test.ts +44 -0
- package/src/features/functions/controller.ts +36 -1
- package/src/features/functions/dtos.ts +2 -0
- package/src/features/functions/interface.ts +2 -0
- package/src/features/functions/utils.ts +13 -0
- package/src/services/mongodb-atlas/__tests__/realmCompatibility.test.ts +116 -0
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +86 -1
- package/src/services/mongodb-atlas/index.ts +164 -44
- package/src/services/mongodb-atlas/model.ts +8 -0
- package/src/services/mongodb-atlas/utils.ts +81 -12
- package/tsconfig.json +0 -5
- package/tsconfig.spec.json +2 -1
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getCollectionsFromPipeline = exports.checkDenyOperation = exports.applyAccessControlToPipeline = exports.getFormattedProjection = exports.getFormattedQuery = exports.getValidRule = void 0;
|
|
6
|
+
exports.getCollectionsFromPipeline = exports.checkDenyOperation = exports.applyAccessControlToPipeline = exports.mergeProjections = exports.getFormattedProjection = exports.getFormattedQuery = exports.getValidRule = void 0;
|
|
7
7
|
exports.normalizeQuery = normalizeQuery;
|
|
8
8
|
exports.ensureClientPipelineStages = ensureClientPipelineStages;
|
|
9
9
|
exports.getHiddenFieldsFromRulesConfig = getHiddenFieldsFromRulesConfig;
|
|
@@ -48,21 +48,83 @@ const getFormattedQuery = (filters = [], query, user) => {
|
|
|
48
48
|
};
|
|
49
49
|
exports.getFormattedQuery = getFormattedQuery;
|
|
50
50
|
const getFormattedProjection = (filters = [], user) => {
|
|
51
|
-
const projections = filters
|
|
52
|
-
.filter((
|
|
53
|
-
if (filter.projection) {
|
|
54
|
-
const preFilter = (0, exports.getValidRule)({ filters, user });
|
|
55
|
-
const isValidPreFilter = !!(preFilter === null || preFilter === void 0 ? void 0 : preFilter.length);
|
|
56
|
-
return isValidPreFilter;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
59
|
-
})
|
|
51
|
+
const projections = (0, exports.getValidRule)({ filters, user })
|
|
52
|
+
.filter((f) => !!f.projection)
|
|
60
53
|
.map((f) => f.projection);
|
|
61
54
|
if (!projections.length)
|
|
62
55
|
return null;
|
|
63
56
|
return Object.assign({}, ...projections);
|
|
64
57
|
};
|
|
65
58
|
exports.getFormattedProjection = getFormattedProjection;
|
|
59
|
+
/**
|
|
60
|
+
* Merges a client-provided projection with the one computed from rules filters.
|
|
61
|
+
*
|
|
62
|
+
* Rules have higher priority over the client:
|
|
63
|
+
* - If rules exclude a top-level field (e.g. `{ instock: 0 }`), every client
|
|
64
|
+
* reference to that field — including dotted sub-paths such as
|
|
65
|
+
* `"instock.qty": 1` — is dropped from the final projection.
|
|
66
|
+
* - If rules include a field (value `1`), it is always part of the final
|
|
67
|
+
* projection and overrides any conflicting client value.
|
|
68
|
+
* - The returned projection is always a valid MongoDB projection (no mixing of
|
|
69
|
+
* inclusion and exclusion on non-`_id` keys), so it can be passed as-is to
|
|
70
|
+
* native MongoDB methods.
|
|
71
|
+
* - Returns `undefined` when neither side provided a meaningful projection.
|
|
72
|
+
*/
|
|
73
|
+
const mergeProjections = (clientProjection, rulesProjection) => {
|
|
74
|
+
const hasClient = !!clientProjection && Object.keys(clientProjection).length > 0;
|
|
75
|
+
const hasRules = !!rulesProjection && Object.keys(rulesProjection).length > 0;
|
|
76
|
+
if (!hasClient && !hasRules)
|
|
77
|
+
return undefined;
|
|
78
|
+
const client = (hasClient ? clientProjection : {});
|
|
79
|
+
const rules = (hasRules ? rulesProjection : {});
|
|
80
|
+
const getTopLevel = (key) => key.split('.')[0];
|
|
81
|
+
const rulesEntries = Object.entries(rules);
|
|
82
|
+
const rulesIncludeKeys = rulesEntries
|
|
83
|
+
.filter(([, value]) => value === 1)
|
|
84
|
+
.map(([key]) => key);
|
|
85
|
+
const rulesExcludeKeys = rulesEntries
|
|
86
|
+
.filter(([, value]) => value === 0)
|
|
87
|
+
.map(([key]) => key);
|
|
88
|
+
// Top-level fields excluded by rules (excluding `_id` which has special
|
|
89
|
+
// MongoDB semantics and is allowed alongside inclusion projections).
|
|
90
|
+
const excludedTopLevel = new Set(rulesExcludeKeys.map(getTopLevel).filter((key) => key !== '_id'));
|
|
91
|
+
const filteredClient = {};
|
|
92
|
+
for (const [key, value] of Object.entries(client)) {
|
|
93
|
+
if (excludedTopLevel.has(getTopLevel(key)))
|
|
94
|
+
continue;
|
|
95
|
+
filteredClient[key] = value;
|
|
96
|
+
}
|
|
97
|
+
const hasInclusion = rulesIncludeKeys.some((key) => key !== '_id') ||
|
|
98
|
+
Object.entries(filteredClient).some(([key, value]) => value === 1 && key !== '_id');
|
|
99
|
+
const merged = {};
|
|
100
|
+
if (hasInclusion) {
|
|
101
|
+
// Inclusion mode: keep only client inclusions, then overlay rules inclusions.
|
|
102
|
+
// Client exclusions (other than `_id: 0`) are incompatible with inclusion
|
|
103
|
+
// mode and are dropped; not-included fields are implicitly excluded anyway.
|
|
104
|
+
for (const [key, value] of Object.entries(filteredClient)) {
|
|
105
|
+
if (value === 1 || key === '_id')
|
|
106
|
+
merged[key] = value;
|
|
107
|
+
}
|
|
108
|
+
for (const key of rulesIncludeKeys)
|
|
109
|
+
merged[key] = 1;
|
|
110
|
+
// Allow `_id: 0` to be forced by rules in inclusion mode.
|
|
111
|
+
for (const key of rulesExcludeKeys) {
|
|
112
|
+
if (key === '_id')
|
|
113
|
+
merged[key] = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Pure exclusion mode: combine all exclusions from both sides.
|
|
118
|
+
for (const [key, value] of Object.entries(filteredClient)) {
|
|
119
|
+
if (value === 0)
|
|
120
|
+
merged[key] = 0;
|
|
121
|
+
}
|
|
122
|
+
for (const key of rulesExcludeKeys)
|
|
123
|
+
merged[key] = 0;
|
|
124
|
+
}
|
|
125
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
126
|
+
};
|
|
127
|
+
exports.mergeProjections = mergeProjections;
|
|
66
128
|
const applyAccessControlToPipeline = (pipeline, rules, user, collectionName, options) => {
|
|
67
129
|
const { isClientPipeline = false } = options || {};
|
|
68
130
|
const hiddenFieldsForCollection = isClientPipeline
|
|
@@ -77,7 +139,7 @@ const applyAccessControlToPipeline = (pipeline, rules, user, collectionName, opt
|
|
|
77
139
|
(0, exports.checkDenyOperation)(rules, currentCollection, model_1.CRUD_OPERATIONS.READ);
|
|
78
140
|
const lookupRules = rules[currentCollection] || {};
|
|
79
141
|
const formattedQuery = (0, exports.getFormattedQuery)(lookupRules.filters, {}, user);
|
|
80
|
-
const projection = (0, exports.getFormattedProjection)(lookupRules.filters);
|
|
142
|
+
const projection = (0, exports.getFormattedProjection)(lookupRules.filters, user);
|
|
81
143
|
const nestedPipeline = (0, exports.applyAccessControlToPipeline)(lookUpStage.pipeline || [], rules, user, currentCollection, { isClientPipeline });
|
|
82
144
|
const lookupPipeline = [
|
|
83
145
|
...(formattedQuery.length ? [{ $match: { $and: formattedQuery } }] : []),
|
|
@@ -98,7 +160,7 @@ const applyAccessControlToPipeline = (pipeline, rules, user, collectionName, opt
|
|
|
98
160
|
(0, exports.checkDenyOperation)(rules, currentCollection, model_1.CRUD_OPERATIONS.READ);
|
|
99
161
|
const unionRules = rules[currentCollection] || {};
|
|
100
162
|
const formattedQuery = (0, exports.getFormattedQuery)(unionRules.filters, {}, user);
|
|
101
|
-
const projection = (0, exports.getFormattedProjection)(unionRules.filters);
|
|
163
|
+
const projection = (0, exports.getFormattedProjection)(unionRules.filters, user);
|
|
102
164
|
if (isSimpleStage) {
|
|
103
165
|
return stage;
|
|
104
166
|
}
|
|
@@ -176,23 +176,23 @@ export declare const generateContextData: ({ user, services, app, rules, current
|
|
|
176
176
|
context: object;
|
|
177
177
|
}>;
|
|
178
178
|
} | {
|
|
179
|
-
lambda: (region: string) => import("@aws-sdk/client-lambda
|
|
180
|
-
Invoke: (params: import("@aws-sdk/client-lambda
|
|
179
|
+
lambda: (region: string) => import("@aws-sdk/client-lambda").Lambda & {
|
|
180
|
+
Invoke: (params: import("@aws-sdk/client-lambda").InvokeCommandInput) => Promise<Omit<import("@aws-sdk/client-lambda").InvokeCommandOutput, "Payload"> & {
|
|
181
181
|
Payload: {
|
|
182
182
|
text: () => string | undefined;
|
|
183
183
|
};
|
|
184
184
|
}>;
|
|
185
|
-
InvokeAsync: (params: import("@aws-sdk/client-lambda
|
|
185
|
+
InvokeAsync: (params: import("@aws-sdk/client-lambda").InvokeAsyncCommandInput) => Promise<import("@aws-sdk/client-lambda").InvokeAsyncCommandOutput>;
|
|
186
186
|
};
|
|
187
|
-
s3: (region: string) => import("@aws-sdk/client-s3
|
|
188
|
-
PutObject: (params: import("@aws-sdk/client-s3
|
|
189
|
-
GetObject: (params: import("@aws-sdk/client-s3
|
|
190
|
-
getSignedUrl: (operation: "getObject" | "putObject", params: (import("@aws-sdk/client-s3
|
|
187
|
+
s3: (region: string) => import("@aws-sdk/client-s3").S3Client & {
|
|
188
|
+
PutObject: (params: import("@aws-sdk/client-s3").PutObjectCommandInput) => Promise<import("@aws-sdk/client-s3").PutObjectCommandOutput>;
|
|
189
|
+
GetObject: (params: import("@aws-sdk/client-s3").GetObjectCommandInput) => Promise<import("@aws-sdk/client-s3").GetObjectCommandOutput>;
|
|
190
|
+
getSignedUrl: (operation: "getObject" | "putObject", params: (import("@aws-sdk/client-s3").GetObjectCommandInput | import("@aws-sdk/client-s3").PutObjectCommandInput) & {
|
|
191
191
|
Expires?: number;
|
|
192
192
|
}, options?: {
|
|
193
193
|
expiresIn?: number;
|
|
194
194
|
}) => Promise<string>;
|
|
195
|
-
PresignURL: (params: (import("@aws-sdk/client-s3
|
|
195
|
+
PresignURL: (params: (import("@aws-sdk/client-s3").GetObjectCommandInput | import("@aws-sdk/client-s3").PutObjectCommandInput) & {
|
|
196
196
|
Method?: string;
|
|
197
197
|
ExpirationMS?: number;
|
|
198
198
|
Expires?: number;
|
|
@@ -328,23 +328,23 @@ export declare const generateContextData: ({ user, services, app, rules, current
|
|
|
328
328
|
context: object;
|
|
329
329
|
}>;
|
|
330
330
|
} | {
|
|
331
|
-
lambda: (region: string) => import("@aws-sdk/client-lambda
|
|
332
|
-
Invoke: (params: import("@aws-sdk/client-lambda
|
|
331
|
+
lambda: (region: string) => import("@aws-sdk/client-lambda").Lambda & {
|
|
332
|
+
Invoke: (params: import("@aws-sdk/client-lambda").InvokeCommandInput) => Promise<Omit<import("@aws-sdk/client-lambda").InvokeCommandOutput, "Payload"> & {
|
|
333
333
|
Payload: {
|
|
334
334
|
text: () => string | undefined;
|
|
335
335
|
};
|
|
336
336
|
}>;
|
|
337
|
-
InvokeAsync: (params: import("@aws-sdk/client-lambda
|
|
337
|
+
InvokeAsync: (params: import("@aws-sdk/client-lambda").InvokeAsyncCommandInput) => Promise<import("@aws-sdk/client-lambda").InvokeAsyncCommandOutput>;
|
|
338
338
|
};
|
|
339
|
-
s3: (region: string) => import("@aws-sdk/client-s3
|
|
340
|
-
PutObject: (params: import("@aws-sdk/client-s3
|
|
341
|
-
GetObject: (params: import("@aws-sdk/client-s3
|
|
342
|
-
getSignedUrl: (operation: "getObject" | "putObject", params: (import("@aws-sdk/client-s3
|
|
339
|
+
s3: (region: string) => import("@aws-sdk/client-s3").S3Client & {
|
|
340
|
+
PutObject: (params: import("@aws-sdk/client-s3").PutObjectCommandInput) => Promise<import("@aws-sdk/client-s3").PutObjectCommandOutput>;
|
|
341
|
+
GetObject: (params: import("@aws-sdk/client-s3").GetObjectCommandInput) => Promise<import("@aws-sdk/client-s3").GetObjectCommandOutput>;
|
|
342
|
+
getSignedUrl: (operation: "getObject" | "putObject", params: (import("@aws-sdk/client-s3").GetObjectCommandInput | import("@aws-sdk/client-s3").PutObjectCommandInput) & {
|
|
343
343
|
Expires?: number;
|
|
344
344
|
}, options?: {
|
|
345
345
|
expiresIn?: number;
|
|
346
346
|
}) => Promise<string>;
|
|
347
|
-
PresignURL: (params: (import("@aws-sdk/client-s3
|
|
347
|
+
PresignURL: (params: (import("@aws-sdk/client-s3").GetObjectCommandInput | import("@aws-sdk/client-s3").PutObjectCommandInput) & {
|
|
348
348
|
Method?: string;
|
|
349
349
|
ExpirationMS?: number;
|
|
350
350
|
Expires?: number;
|
|
@@ -479,23 +479,23 @@ export declare const generateContextData: ({ user, services, app, rules, current
|
|
|
479
479
|
context: object;
|
|
480
480
|
}>;
|
|
481
481
|
} | {
|
|
482
|
-
lambda: (region: string) => import("@aws-sdk/client-lambda
|
|
483
|
-
Invoke: (params: import("@aws-sdk/client-lambda
|
|
482
|
+
lambda: (region: string) => import("@aws-sdk/client-lambda").Lambda & {
|
|
483
|
+
Invoke: (params: import("@aws-sdk/client-lambda").InvokeCommandInput) => Promise<Omit<import("@aws-sdk/client-lambda").InvokeCommandOutput, "Payload"> & {
|
|
484
484
|
Payload: {
|
|
485
485
|
text: () => string | undefined;
|
|
486
486
|
};
|
|
487
487
|
}>;
|
|
488
|
-
InvokeAsync: (params: import("@aws-sdk/client-lambda
|
|
488
|
+
InvokeAsync: (params: import("@aws-sdk/client-lambda").InvokeAsyncCommandInput) => Promise<import("@aws-sdk/client-lambda").InvokeAsyncCommandOutput>;
|
|
489
489
|
};
|
|
490
|
-
s3: (region: string) => import("@aws-sdk/client-s3
|
|
491
|
-
PutObject: (params: import("@aws-sdk/client-s3
|
|
492
|
-
GetObject: (params: import("@aws-sdk/client-s3
|
|
493
|
-
getSignedUrl: (operation: "getObject" | "putObject", params: (import("@aws-sdk/client-s3
|
|
490
|
+
s3: (region: string) => import("@aws-sdk/client-s3").S3Client & {
|
|
491
|
+
PutObject: (params: import("@aws-sdk/client-s3").PutObjectCommandInput) => Promise<import("@aws-sdk/client-s3").PutObjectCommandOutput>;
|
|
492
|
+
GetObject: (params: import("@aws-sdk/client-s3").GetObjectCommandInput) => Promise<import("@aws-sdk/client-s3").GetObjectCommandOutput>;
|
|
493
|
+
getSignedUrl: (operation: "getObject" | "putObject", params: (import("@aws-sdk/client-s3").GetObjectCommandInput | import("@aws-sdk/client-s3").PutObjectCommandInput) & {
|
|
494
494
|
Expires?: number;
|
|
495
495
|
}, options?: {
|
|
496
496
|
expiresIn?: number;
|
|
497
497
|
}) => Promise<string>;
|
|
498
|
-
PresignURL: (params: (import("@aws-sdk/client-s3
|
|
498
|
+
PresignURL: (params: (import("@aws-sdk/client-s3").GetObjectCommandInput | import("@aws-sdk/client-s3").PutObjectCommandInput) & {
|
|
499
499
|
Method?: string;
|
|
500
500
|
ExpirationMS?: number;
|
|
501
501
|
Expires?: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { MachineContext } from '../../interface';
|
|
2
2
|
export declare const checkAdditionalFieldsFn: ({ role }: MachineContext) => boolean;
|
|
3
|
-
export declare const checkIsValidFieldNameFn: (context: MachineContext) => Promise<import("bson
|
|
3
|
+
export declare const checkIsValidFieldNameFn: (context: MachineContext) => Promise<import("bson").Document>;
|
|
4
4
|
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/read/D/validators.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,GAAI,UAAU,cAAc,YAE/D,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAU,SAAS,cAAc,
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/read/D/validators.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,GAAI,UAAU,cAAc,YAE/D,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAU,SAAS,cAAc,qCAOpE,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { MachineContext } from '../../interface';
|
|
2
2
|
export declare const checkAdditionalFieldsFn: ({ role }: MachineContext) => boolean;
|
|
3
|
-
export declare const checkIsValidFieldNameFn: (context: MachineContext) => Promise<import("bson
|
|
3
|
+
export declare const checkIsValidFieldNameFn: (context: MachineContext) => Promise<import("bson").Document>;
|
|
4
4
|
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/write/C/validators.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,GAAI,UAAU,cAAc,YAE/D,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAU,SAAS,cAAc,
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../../../../../src/utils/roles/machines/write/C/validators.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,GAAI,UAAU,cAAc,YAE/D,CAAA;AAED,eAAO,MAAM,uBAAuB,GAAU,SAAS,cAAc,qCAIpE,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Fastify, { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
|
|
2
|
+
import { services } from '../../../services'
|
|
2
3
|
import { GenerateContext } from '../../../utils/context'
|
|
3
4
|
import { functionsController } from '../controller'
|
|
4
5
|
|
|
@@ -8,18 +9,19 @@ jest.mock('../../../utils/context', () => ({
|
|
|
8
9
|
|
|
9
10
|
describe('functionsController', () => {
|
|
10
11
|
let app: FastifyInstance
|
|
12
|
+
const originalMongoService = services['mongodb-atlas']
|
|
11
13
|
|
|
12
14
|
beforeEach(async () => {
|
|
13
15
|
app = Fastify()
|
|
14
16
|
|
|
15
17
|
app.decorate('jwtAuthentication', async (request: FastifyRequest, _reply: FastifyReply) => {
|
|
16
|
-
;(request as any).user = {
|
|
18
|
+
; (request as any).user = {
|
|
17
19
|
id: '507f191e810c19729de860ea',
|
|
18
20
|
typ: 'access'
|
|
19
21
|
}
|
|
20
22
|
})
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
; (GenerateContext as jest.Mock).mockResolvedValue({ ok: true })
|
|
23
25
|
|
|
24
26
|
await app.register(functionsController, {
|
|
25
27
|
functionsList: {
|
|
@@ -33,6 +35,7 @@ describe('functionsController', () => {
|
|
|
33
35
|
})
|
|
34
36
|
|
|
35
37
|
afterEach(async () => {
|
|
38
|
+
services['mongodb-atlas'] = originalMongoService
|
|
36
39
|
await app.close()
|
|
37
40
|
jest.clearAllMocks()
|
|
38
41
|
})
|
|
@@ -57,4 +60,41 @@ describe('functionsController', () => {
|
|
|
57
60
|
})
|
|
58
61
|
)
|
|
59
62
|
})
|
|
63
|
+
|
|
64
|
+
it('passes mongodb-atlas distinct service arguments through POST /call', async () => {
|
|
65
|
+
const distinct = jest.fn().mockResolvedValue(['open'])
|
|
66
|
+
services['mongodb-atlas'] = jest.fn(() => ({
|
|
67
|
+
db: jest.fn().mockReturnValue({
|
|
68
|
+
collection: jest.fn().mockReturnValue({
|
|
69
|
+
distinct
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
})) as any
|
|
73
|
+
|
|
74
|
+
const response = await app.inject({
|
|
75
|
+
method: 'POST',
|
|
76
|
+
url: '/call',
|
|
77
|
+
payload: {
|
|
78
|
+
service: 'mongodb-atlas',
|
|
79
|
+
name: 'distinct',
|
|
80
|
+
arguments: [
|
|
81
|
+
{
|
|
82
|
+
database: 'app',
|
|
83
|
+
collection: 'todos',
|
|
84
|
+
key: 'status',
|
|
85
|
+
query: { archived: false },
|
|
86
|
+
options: { maxTimeMS: 250 }
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(response.statusCode).toBe(200)
|
|
93
|
+
expect(JSON.parse(response.body)).toEqual(['open'])
|
|
94
|
+
expect(distinct).toHaveBeenCalledWith(
|
|
95
|
+
'status',
|
|
96
|
+
{ archived: false },
|
|
97
|
+
{ maxTimeMS: 250 }
|
|
98
|
+
)
|
|
99
|
+
})
|
|
60
100
|
})
|
|
@@ -30,4 +30,48 @@ describe('executeQuery', () => {
|
|
|
30
30
|
{ upsert: true }
|
|
31
31
|
)
|
|
32
32
|
})
|
|
33
|
+
|
|
34
|
+
it('passes distinct arguments through to the service method', async () => {
|
|
35
|
+
const currentMethod = jest.fn().mockResolvedValue(['open'])
|
|
36
|
+
|
|
37
|
+
const operators = await executeQuery({
|
|
38
|
+
currentMethod,
|
|
39
|
+
key: 'status',
|
|
40
|
+
query: { archived: false },
|
|
41
|
+
update: {},
|
|
42
|
+
options: { maxTimeMS: 500 }
|
|
43
|
+
} as any)
|
|
44
|
+
|
|
45
|
+
await operators.distinct()
|
|
46
|
+
|
|
47
|
+
expect(currentMethod).toHaveBeenCalledWith(
|
|
48
|
+
'status',
|
|
49
|
+
{ archived: false },
|
|
50
|
+
{ maxTimeMS: 500 }
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('passes bulkWrite operations through to the service method', async () => {
|
|
55
|
+
const currentMethod = jest.fn().mockResolvedValue({ acknowledged: true })
|
|
56
|
+
const operations = [
|
|
57
|
+
{
|
|
58
|
+
updateOne: {
|
|
59
|
+
filter: { archived: false },
|
|
60
|
+
update: { $set: { archived: true } }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const operators = await executeQuery({
|
|
66
|
+
currentMethod,
|
|
67
|
+
query: {},
|
|
68
|
+
update: {},
|
|
69
|
+
operations,
|
|
70
|
+
options: { ordered: false }
|
|
71
|
+
} as any)
|
|
72
|
+
|
|
73
|
+
await operators.bulkWrite()
|
|
74
|
+
|
|
75
|
+
expect(currentMethod).toHaveBeenCalledWith(operations, { ordered: false })
|
|
76
|
+
})
|
|
33
77
|
})
|
|
@@ -9,6 +9,29 @@ import { Base64Function, FunctionCallBase64Dto, FunctionCallDto } from './dtos'
|
|
|
9
9
|
import { FunctionController } from './interface'
|
|
10
10
|
import { executeQuery } from './utils'
|
|
11
11
|
|
|
12
|
+
const SUPPORTED_QUERY_METHODS = [
|
|
13
|
+
'find',
|
|
14
|
+
'findOne',
|
|
15
|
+
'count',
|
|
16
|
+
'countDocuments',
|
|
17
|
+
'distinct',
|
|
18
|
+
'deleteOne',
|
|
19
|
+
'insertOne',
|
|
20
|
+
'updateOne',
|
|
21
|
+
'findOneAndUpdate',
|
|
22
|
+
'aggregate',
|
|
23
|
+
'insertMany',
|
|
24
|
+
'bulkWrite',
|
|
25
|
+
'updateMany',
|
|
26
|
+
'deleteMany'
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
type SupportedQueryMethod = (typeof SUPPORTED_QUERY_METHODS)[number]
|
|
30
|
+
|
|
31
|
+
const isSupportedQueryMethod = (value: unknown): value is SupportedQueryMethod =>
|
|
32
|
+
typeof value === 'string' &&
|
|
33
|
+
(SUPPORTED_QUERY_METHODS as readonly string[]).includes(value)
|
|
34
|
+
|
|
12
35
|
const normalizeUser = (payload: Record<string, any> | undefined) => {
|
|
13
36
|
if (!payload) return undefined
|
|
14
37
|
const nestedUser =
|
|
@@ -352,6 +375,7 @@ export const functionsController: FunctionController = async (
|
|
|
352
375
|
const [{
|
|
353
376
|
database,
|
|
354
377
|
collection,
|
|
378
|
+
key,
|
|
355
379
|
query,
|
|
356
380
|
filter,
|
|
357
381
|
update,
|
|
@@ -360,9 +384,14 @@ export const functionsController: FunctionController = async (
|
|
|
360
384
|
returnNewDocument,
|
|
361
385
|
document,
|
|
362
386
|
documents,
|
|
387
|
+
operations,
|
|
363
388
|
pipeline = []
|
|
364
389
|
}] = args
|
|
365
390
|
|
|
391
|
+
if (!isSupportedQueryMethod(method)) {
|
|
392
|
+
throw new Error(`Unsupported service method "${String(method)}"`)
|
|
393
|
+
}
|
|
394
|
+
|
|
366
395
|
const currentMethod = serviceFn(app, { rules, user })
|
|
367
396
|
.db(database)
|
|
368
397
|
.collection(collection)[method]
|
|
@@ -371,6 +400,7 @@ export const functionsController: FunctionController = async (
|
|
|
371
400
|
const operatorsByType = await executeQuery({
|
|
372
401
|
currentMethod,
|
|
373
402
|
query,
|
|
403
|
+
key,
|
|
374
404
|
filter,
|
|
375
405
|
update,
|
|
376
406
|
projection,
|
|
@@ -378,10 +408,15 @@ export const functionsController: FunctionController = async (
|
|
|
378
408
|
returnNewDocument,
|
|
379
409
|
document,
|
|
380
410
|
documents,
|
|
411
|
+
operations,
|
|
381
412
|
pipeline,
|
|
382
413
|
isClient: true
|
|
383
414
|
})
|
|
384
|
-
const
|
|
415
|
+
const operator = operatorsByType[method]
|
|
416
|
+
if (typeof operator !== 'function') {
|
|
417
|
+
throw new Error(`Unsupported service method "${method}"`)
|
|
418
|
+
}
|
|
419
|
+
const serviceResult = await operator()
|
|
385
420
|
res.type('application/json')
|
|
386
421
|
return serializeEjson(serviceResult)
|
|
387
422
|
}
|
|
@@ -23,6 +23,7 @@ export type FunctionCallBase64Dto = {
|
|
|
23
23
|
type ArgumentsData = Arguments<{
|
|
24
24
|
database: string
|
|
25
25
|
collection: string
|
|
26
|
+
key?: string
|
|
26
27
|
filter?: Document
|
|
27
28
|
query: Parameters<GetOperatorsFunction>
|
|
28
29
|
update: Document
|
|
@@ -31,6 +32,7 @@ type ArgumentsData = Arguments<{
|
|
|
31
32
|
returnNewDocument?: boolean
|
|
32
33
|
document: Document
|
|
33
34
|
documents: Document[]
|
|
35
|
+
operations?: Document[]
|
|
34
36
|
pipeline?: Document[]
|
|
35
37
|
}>
|
|
36
38
|
|
|
@@ -27,12 +27,14 @@ export type ExecuteQueryParams = {
|
|
|
27
27
|
currentMethod: ReturnType<GetOperatorsFunction>[keyof ReturnType<GetOperatorsFunction>]
|
|
28
28
|
query: Parameters<GetOperatorsFunction>
|
|
29
29
|
update: Document
|
|
30
|
+
key?: string
|
|
30
31
|
filter?: Document
|
|
31
32
|
projection?: Document
|
|
32
33
|
options?: Document
|
|
33
34
|
returnNewDocument?: boolean
|
|
34
35
|
document: Document
|
|
35
36
|
documents: Document[]
|
|
37
|
+
operations?: Document[]
|
|
36
38
|
pipeline: Document[]
|
|
37
39
|
isClient?: boolean
|
|
38
40
|
}
|
|
@@ -45,12 +45,14 @@ export const executeQuery = async ({
|
|
|
45
45
|
currentMethod,
|
|
46
46
|
query,
|
|
47
47
|
update,
|
|
48
|
+
key,
|
|
48
49
|
filter,
|
|
49
50
|
projection,
|
|
50
51
|
options,
|
|
51
52
|
returnNewDocument,
|
|
52
53
|
document,
|
|
53
54
|
documents,
|
|
55
|
+
operations,
|
|
54
56
|
pipeline,
|
|
55
57
|
isClient = false
|
|
56
58
|
}: ExecuteQueryParams) => {
|
|
@@ -113,6 +115,12 @@ export const executeQuery = async ({
|
|
|
113
115
|
EJSON.deserialize(resolvedQuery),
|
|
114
116
|
parsedOptions
|
|
115
117
|
),
|
|
118
|
+
distinct: () =>
|
|
119
|
+
(currentMethod as ReturnType<GetOperatorsFunction>['distinct'])(
|
|
120
|
+
key ?? '',
|
|
121
|
+
EJSON.deserialize(resolvedQuery),
|
|
122
|
+
parsedOptions
|
|
123
|
+
),
|
|
116
124
|
deleteOne: () =>
|
|
117
125
|
(currentMethod as ReturnType<GetOperatorsFunction>['deleteOne'])(
|
|
118
126
|
EJSON.deserialize(resolvedQuery),
|
|
@@ -144,6 +152,11 @@ export const executeQuery = async ({
|
|
|
144
152
|
(currentMethod as ReturnType<GetOperatorsFunction>['insertMany'])(
|
|
145
153
|
EJSON.deserialize(documents)
|
|
146
154
|
),
|
|
155
|
+
bulkWrite: () =>
|
|
156
|
+
(currentMethod as ReturnType<GetOperatorsFunction>['bulkWrite'])(
|
|
157
|
+
EJSON.deserialize(operations ?? []),
|
|
158
|
+
parsedOptions
|
|
159
|
+
),
|
|
147
160
|
updateMany: () =>
|
|
148
161
|
(currentMethod as ReturnType<GetOperatorsFunction>['updateMany'])(
|
|
149
162
|
EJSON.deserialize(resolvedQuery),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
|
|
1
3
|
import { ObjectId } from 'mongodb'
|
|
2
4
|
import MongoDbAtlas from '..'
|
|
3
5
|
import { Rules } from '../../../features/rules/interface'
|
|
@@ -178,6 +180,70 @@ describe('mongodb-atlas Realm compatibility', () => {
|
|
|
178
180
|
)
|
|
179
181
|
})
|
|
180
182
|
|
|
183
|
+
it('forwards bulkWrite to the underlying collection in system mode', async () => {
|
|
184
|
+
const operations = [
|
|
185
|
+
{
|
|
186
|
+
updateOne: {
|
|
187
|
+
filter: { done: false },
|
|
188
|
+
update: { $set: { done: true } }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
const bulkWriteResult = {
|
|
193
|
+
acknowledged: true,
|
|
194
|
+
matchedCount: 1,
|
|
195
|
+
modifiedCount: 1,
|
|
196
|
+
insertedCount: 0,
|
|
197
|
+
deletedCount: 0,
|
|
198
|
+
upsertedCount: 0,
|
|
199
|
+
insertedIds: {},
|
|
200
|
+
upsertedIds: {}
|
|
201
|
+
}
|
|
202
|
+
const bulkWrite = jest.fn().mockResolvedValue(bulkWriteResult)
|
|
203
|
+
const collection = {
|
|
204
|
+
collectionName: 'todos',
|
|
205
|
+
bulkWrite
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
|
|
209
|
+
run_as_system: true
|
|
210
|
+
}).db('db').collection('todos')
|
|
211
|
+
|
|
212
|
+
const result = await operators.bulkWrite(operations as any, { ordered: false } as any)
|
|
213
|
+
|
|
214
|
+
expect(result).toEqual(bulkWriteResult)
|
|
215
|
+
expect(bulkWrite).toHaveBeenCalledWith(operations, { ordered: false })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('rejects bulkWrite outside run_as_system mode', async () => {
|
|
219
|
+
const bulkWrite = jest.fn()
|
|
220
|
+
const collection = {
|
|
221
|
+
collectionName: 'todos',
|
|
222
|
+
bulkWrite
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
|
|
226
|
+
rules: createRules(),
|
|
227
|
+
user: { id: 'user-1' }
|
|
228
|
+
}).db('db').collection('todos')
|
|
229
|
+
|
|
230
|
+
await expect(
|
|
231
|
+
operators.bulkWrite(
|
|
232
|
+
[
|
|
233
|
+
{
|
|
234
|
+
updateOne: {
|
|
235
|
+
filter: { done: false },
|
|
236
|
+
update: { $set: { done: true } }
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
] as any,
|
|
240
|
+
{ ordered: false } as any
|
|
241
|
+
)
|
|
242
|
+
).rejects.toThrow('bulkWrite is available only when run_as_system is enabled')
|
|
243
|
+
|
|
244
|
+
expect(bulkWrite).not.toHaveBeenCalled()
|
|
245
|
+
})
|
|
246
|
+
|
|
181
247
|
it('supports operator updates in updateMany without using invalid aggregate stages', async () => {
|
|
182
248
|
const id = new ObjectId()
|
|
183
249
|
const find = jest.fn().mockReturnValue({
|
|
@@ -513,6 +579,56 @@ describe('mongodb-atlas Realm compatibility', () => {
|
|
|
513
579
|
expect(result.insertedIds).toEqual([id0, id1])
|
|
514
580
|
})
|
|
515
581
|
|
|
582
|
+
it('computes distinct values from readable documents only', async () => {
|
|
583
|
+
const find = jest.fn().mockReturnValue({
|
|
584
|
+
toArray: jest.fn().mockResolvedValue([
|
|
585
|
+
{ _id: new ObjectId(), visible: 'A', secret: 'internal-1' },
|
|
586
|
+
{ _id: new ObjectId(), visible: 'A', secret: 'internal-2' },
|
|
587
|
+
{ _id: new ObjectId(), visible: 'B', secret: 'internal-1' }
|
|
588
|
+
])
|
|
589
|
+
})
|
|
590
|
+
const collection = {
|
|
591
|
+
collectionName: 'todos',
|
|
592
|
+
find
|
|
593
|
+
}
|
|
594
|
+
const operators = MongoDbAtlas(createAppWithCollection(collection) as any, {
|
|
595
|
+
rules: createRules({
|
|
596
|
+
roles: [
|
|
597
|
+
{
|
|
598
|
+
name: 'reader',
|
|
599
|
+
apply_when: {},
|
|
600
|
+
insert: true,
|
|
601
|
+
delete: true,
|
|
602
|
+
search: true,
|
|
603
|
+
read: true,
|
|
604
|
+
write: true,
|
|
605
|
+
fields: {
|
|
606
|
+
visible: { read: true },
|
|
607
|
+
secret: { read: false, write: false }
|
|
608
|
+
}
|
|
609
|
+
} as any
|
|
610
|
+
]
|
|
611
|
+
}),
|
|
612
|
+
user: { id: 'user-1' }
|
|
613
|
+
}).db('db').collection('todos')
|
|
614
|
+
|
|
615
|
+
const visibleResult = await operators.distinct('visible', { archived: false })
|
|
616
|
+
const secretResult = await operators.distinct('secret', { archived: false })
|
|
617
|
+
|
|
618
|
+
expect(visibleResult).toEqual(['A', 'B'])
|
|
619
|
+
expect(secretResult).toEqual([])
|
|
620
|
+
expect(find).toHaveBeenNthCalledWith(
|
|
621
|
+
1,
|
|
622
|
+
{ $and: [{ archived: false }] },
|
|
623
|
+
{ projection: { _id: 1, visible: 1 } }
|
|
624
|
+
)
|
|
625
|
+
expect(find).toHaveBeenNthCalledWith(
|
|
626
|
+
2,
|
|
627
|
+
{ $and: [{ archived: false }] },
|
|
628
|
+
{ projection: { _id: 1, secret: 1 } }
|
|
629
|
+
)
|
|
630
|
+
})
|
|
631
|
+
|
|
516
632
|
it('exposes startSession and delegates to the underlying MongoClient', async () => {
|
|
517
633
|
const mockSession = { withTransaction: jest.fn() }
|
|
518
634
|
const startSession = jest.fn().mockReturnValue(mockSession)
|