@flowerforce/flowerbase 1.8.4-beta.2 → 1.8.4-beta.4
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/dist/features/functions/interface.d.ts +1 -0
- package/dist/features/functions/interface.d.ts.map +1 -1
- package/dist/features/functions/utils.js +1 -1
- package/dist/services/mongodb-atlas/index.d.ts.map +1 -1
- package/dist/services/mongodb-atlas/index.js +21 -9
- 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/index.d.ts.map +1 -1
- package/dist/utils/context/index.js +91 -14
- package/package.json +1 -1
- package/src/features/functions/interface.ts +4 -1
- package/src/features/functions/utils.ts +3 -3
- package/src/services/mongodb-atlas/__tests__/utils.test.ts +86 -1
- package/src/services/mongodb-atlas/index.ts +42 -19
- package/src/services/mongodb-atlas/utils.ts +81 -12
- package/src/utils/__tests__/contextExecuteCompatibility.test.ts +40 -0
- package/src/utils/context/index.ts +142 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/features/functions/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAA;AACzE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG;
|
|
1
|
+
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../../src/features/functions/interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAClC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAA;AACzE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,MAAM,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,GAAG;IACpD,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;AAEhD,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,eAAe,CAAA;IACpB,aAAa,EAAE,SAAS,CAAA;IACxB,SAAS,EAAE,KAAK,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,aAAa,EAAE,UAAU,CAAC,oBAAoB,CAAC,CAAC,MAAM,UAAU,CAAC,oBAAoB,CAAC,CAAC,CAAA;IACvF,KAAK,EAAE,UAAU,CAAC,oBAAoB,CAAC,CAAA;IACvC,MAAM,EAAE,QAAQ,CAAA;IAChB,MAAM,CAAC,EAAE,QAAQ,CAAA;IACjB,UAAU,CAAC,EAAE,QAAQ,CAAA;IACrB,OAAO,CAAC,EAAE,QAAQ,CAAA;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,QAAQ,EAAE,QAAQ,CAAA;IAClB,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,KAAK,0BAA0B,GAAG;IAChC,aAAa,EAAE,SAAS,CAAA;IACxB,KAAK,EAAE,KAAK,CAAA;CACb,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG,CAC/B,GAAG,EAAE,eAAe,EACpB,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,0BAA0B,KACjD,OAAO,CAAC,IAAI,CAAC,CAAA"}
|
|
@@ -46,7 +46,7 @@ const loadFunctions = (...args_1) => __awaiter(void 0, [...args_1], void 0, func
|
|
|
46
46
|
throw new Error(`File ${name}.js or ${name}.ts not found`);
|
|
47
47
|
}
|
|
48
48
|
code = fs_1.default.readFileSync(fnPath, 'utf-8');
|
|
49
|
-
acc[name] = Object.assign({ code }, opts);
|
|
49
|
+
acc[name] = Object.assign({ code, sourcePath: fnPath }, opts);
|
|
50
50
|
return acc;
|
|
51
51
|
}, {});
|
|
52
52
|
return functions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,QAAQ,EAQT,MAAM,SAAS,CAAA;AAOhB,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAIL,QAAQ,EAQT,MAAM,SAAS,CAAA;AAOhB,OAAO,EAGL,oBAAoB,EAErB,MAAM,SAAS,CAAA;AA8JhB,eAAO,MAAM,kBAAkB,GAAI,OAAO,OAAO,KAAG,OA0BnD,CAAA;AA8BD,eAAO,MAAM,2BAA2B,GAAI,UAAU,QAAQ,EAAE,YAK5D,CAAA;AA80CJ,QAAA,MAAM,YAAY,EAAE,oBAwBlB,CAAA;AAEF,eAAe,YAAY,CAAA"}
|
|
@@ -459,13 +459,17 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
459
459
|
var _a;
|
|
460
460
|
try {
|
|
461
461
|
const { projection, options: normalizedOptions } = resolveFindArgs(projectionOrOptions, options);
|
|
462
|
-
const resolvedOptions = projection || normalizedOptions
|
|
463
|
-
? Object.assign(Object.assign({}, (normalizedOptions !== null && normalizedOptions !== void 0 ? normalizedOptions : {})), (projection ? { projection } : {})) : undefined;
|
|
464
462
|
const resolvedQuery = query !== null && query !== void 0 ? query : {};
|
|
465
463
|
if (!run_as_system) {
|
|
466
464
|
(0, utils_3.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
467
465
|
// Apply access control filters to the query
|
|
468
466
|
const formattedQuery = (0, utils_3.getFormattedQuery)(filters, resolvedQuery, user);
|
|
467
|
+
// Rules-level projection has priority over client-provided projection.
|
|
468
|
+
// The merged projection is passed natively to MongoDB.
|
|
469
|
+
const rulesProjection = (0, utils_3.getFormattedProjection)(filters, user);
|
|
470
|
+
const finalProjection = (0, utils_3.mergeProjections)(projection, rulesProjection);
|
|
471
|
+
const resolvedOptions = finalProjection || normalizedOptions
|
|
472
|
+
? Object.assign(Object.assign({}, (normalizedOptions !== null && normalizedOptions !== void 0 ? normalizedOptions : {})), (finalProjection ? { projection: finalProjection } : {})) : undefined;
|
|
469
473
|
logDebug('update formattedQuery', {
|
|
470
474
|
collection: collName,
|
|
471
475
|
query,
|
|
@@ -509,8 +513,10 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
509
513
|
emitMongoEvent('findOne');
|
|
510
514
|
return Promise.resolve(response);
|
|
511
515
|
}
|
|
512
|
-
// System mode: no validation applied
|
|
513
|
-
const
|
|
516
|
+
// System mode: no validation applied, only client-provided projection/options.
|
|
517
|
+
const systemOptions = projection || normalizedOptions
|
|
518
|
+
? Object.assign(Object.assign({}, (normalizedOptions !== null && normalizedOptions !== void 0 ? normalizedOptions : {})), (projection ? { projection } : {})) : undefined;
|
|
519
|
+
const response = yield collection.findOne(resolvedQuery, systemOptions);
|
|
514
520
|
emitMongoEvent('findOne');
|
|
515
521
|
return response;
|
|
516
522
|
}
|
|
@@ -825,13 +831,17 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
825
831
|
find: (query = {}, projectionOrOptions, options) => {
|
|
826
832
|
try {
|
|
827
833
|
const { projection, options: normalizedOptions } = resolveFindArgs(projectionOrOptions, options);
|
|
828
|
-
const resolvedOptions = projection || normalizedOptions
|
|
829
|
-
? Object.assign(Object.assign({}, (normalizedOptions !== null && normalizedOptions !== void 0 ? normalizedOptions : {})), (projection ? { projection } : {})) : undefined;
|
|
830
834
|
if (!run_as_system) {
|
|
831
835
|
(0, utils_3.checkDenyOperation)(normalizedRules, collection.collectionName, model_1.CRUD_OPERATIONS.READ);
|
|
832
836
|
// Pre-query filtering based on access control rules
|
|
833
837
|
const formattedQuery = (0, utils_3.getFormattedQuery)(filters, query, user);
|
|
834
838
|
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {};
|
|
839
|
+
// Rules-level projection has priority over client-provided projection.
|
|
840
|
+
// The merged projection is passed natively to MongoDB.
|
|
841
|
+
const rulesProjection = (0, utils_3.getFormattedProjection)(filters, user);
|
|
842
|
+
const finalProjection = (0, utils_3.mergeProjections)(projection, rulesProjection);
|
|
843
|
+
const resolvedOptions = finalProjection || normalizedOptions
|
|
844
|
+
? Object.assign(Object.assign({}, (normalizedOptions !== null && normalizedOptions !== void 0 ? normalizedOptions : {})), (finalProjection ? { projection: finalProjection } : {})) : undefined;
|
|
835
845
|
// aggiunto filter per evitare questo errore: $and argument's entries must be objects
|
|
836
846
|
const cursor = collection.find(currentQuery, resolvedOptions);
|
|
837
847
|
const originalToArray = cursor.toArray.bind(cursor);
|
|
@@ -866,8 +876,10 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
866
876
|
emitMongoEvent('find');
|
|
867
877
|
return cursor;
|
|
868
878
|
}
|
|
869
|
-
// System mode: return original unfiltered cursor
|
|
870
|
-
const
|
|
879
|
+
// System mode: return original unfiltered cursor (only client projection/options).
|
|
880
|
+
const systemOptions = projection || normalizedOptions
|
|
881
|
+
? Object.assign(Object.assign({}, (normalizedOptions !== null && normalizedOptions !== void 0 ? normalizedOptions : {})), (projection ? { projection } : {})) : undefined;
|
|
882
|
+
const cursor = collection.find(query, systemOptions);
|
|
871
883
|
emitMongoEvent('find');
|
|
872
884
|
return cursor;
|
|
873
885
|
}
|
|
@@ -1041,7 +1053,7 @@ const getOperators = (mongo, { rules, dbName, collName, user, run_as_system, mon
|
|
|
1041
1053
|
formattedQuery,
|
|
1042
1054
|
pipeline
|
|
1043
1055
|
});
|
|
1044
|
-
const projection = (0, utils_3.getFormattedProjection)(filters);
|
|
1056
|
+
const projection = (0, utils_3.getFormattedProjection)(filters, user);
|
|
1045
1057
|
const hiddenFields = (0, utils_3.getHiddenFieldsFromRulesConfig)(rulesConfig);
|
|
1046
1058
|
const sanitizedPipeline = (0, utils_3.applyAccessControlToPipeline)(pipeline, normalizedRules, user, collName, { isClientPipeline: true });
|
|
1047
1059
|
logDebug('aggregate sanitizedPipeline', {
|
|
@@ -7,6 +7,21 @@ import { CRUD_OPERATIONS, GetValidRuleParams } from './model';
|
|
|
7
7
|
export declare const getValidRule: <T extends Role | Filter>({ filters, user, record }: GetValidRuleParams<T>) => T[];
|
|
8
8
|
export declare const getFormattedQuery: (filters?: Filter[], query?: Parameters<Collection<Document>["findOne"]>[0], user?: User) => FilterMongoDB<Document>[];
|
|
9
9
|
export declare const getFormattedProjection: (filters?: Filter[], user?: User) => Projection | null;
|
|
10
|
+
/**
|
|
11
|
+
* Merges a client-provided projection with the one computed from rules filters.
|
|
12
|
+
*
|
|
13
|
+
* Rules have higher priority over the client:
|
|
14
|
+
* - If rules exclude a top-level field (e.g. `{ instock: 0 }`), every client
|
|
15
|
+
* reference to that field — including dotted sub-paths such as
|
|
16
|
+
* `"instock.qty": 1` — is dropped from the final projection.
|
|
17
|
+
* - If rules include a field (value `1`), it is always part of the final
|
|
18
|
+
* projection and overrides any conflicting client value.
|
|
19
|
+
* - The returned projection is always a valid MongoDB projection (no mixing of
|
|
20
|
+
* inclusion and exclusion on non-`_id` keys), so it can be passed as-is to
|
|
21
|
+
* native MongoDB methods.
|
|
22
|
+
* - Returns `undefined` when neither side provided a meaningful projection.
|
|
23
|
+
*/
|
|
24
|
+
export declare const mergeProjections: (clientProjection: Projection | Document | undefined, rulesProjection: Projection | null | undefined) => Projection | Document | undefined;
|
|
10
25
|
export declare const applyAccessControlToPipeline: (pipeline: AggregationPipeline, rules: Record<string, {
|
|
11
26
|
filters?: Filter[];
|
|
12
27
|
roles?: Role[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,SAAS,CAAA;AACvE,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,OAAO,EACL,mBAAmB,EAEnB,MAAM,EAEN,UAAU,EACV,KAAK,EAGN,MAAM,gCAAgC,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AAGlD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAE7D,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,IAAI,GAAG,MAAM,EAAE,2BAInD,kBAAkB,CAAC,CAAC,CAAC,QA8BvB,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,UAAS,MAAM,EAAO,EACtB,QAAQ,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EACtD,OAAO,IAAI,8BAcZ,CAAA;AAED,eAAO,MAAM,sBAAsB,GACjC,UAAS,MAAM,EAAO,EACtB,OAAO,IAAI,KACV,UAAU,GAAG,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/services/mongodb-atlas/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAElC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,SAAS,CAAA;AACvE,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AACtC,OAAO,EACL,mBAAmB,EAEnB,MAAM,EAEN,UAAU,EACV,KAAK,EAGN,MAAM,gCAAgC,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AAGlD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAE7D,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,IAAI,GAAG,MAAM,EAAE,2BAInD,kBAAkB,CAAC,CAAC,CAAC,QA8BvB,CAAA;AAED,eAAO,MAAM,iBAAiB,GAC5B,UAAS,MAAM,EAAO,EACtB,QAAQ,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,EACtD,OAAO,IAAI,8BAcZ,CAAA;AAED,eAAO,MAAM,sBAAsB,GACjC,UAAS,MAAM,EAAO,EACtB,OAAO,IAAI,KACV,UAAU,GAAG,IAMf,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,GAC3B,kBAAkB,UAAU,GAAG,QAAQ,GAAG,SAAS,EACnD,iBAAiB,UAAU,GAAG,IAAI,GAAG,SAAS,KAC7C,UAAU,GAAG,QAAQ,GAAG,SAyD1B,CAAA;AAED,eAAO,MAAM,4BAA4B,GACvC,UAAU,mBAAmB,EAC7B,OAAO,MAAM,CACX,MAAM,EACN;IACE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,CAAC,EAAE,IAAI,EAAE,CAAA;CACf,CACF,EACD,MAAM,IAAI,EACV,gBAAgB,MAAM,EACtB,UAAU;IACR,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B,KACA,mBA6GF,CAAA;AAED,eAAO,MAAM,kBAAkB,GAC7B,OAAO,KAAK,EACZ,gBAAgB,MAAM,EACtB,WAAW,eAAe,SAM3B,CAAA;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,CAAC,QAAQ,CAAC,EAAE;;;;;;;;iBA0I2jkS,CAAC;sBAAgC,CAAC;2BAAsC,CAAC;;;;IAlIlskS;AAED,eAAO,MAAM,0BAA0B,GAAI,UAAU,QAAQ,EAAE,aAgC9D,CAAA;AAYD,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,mBAAmB,QA+BvE;AAED,wBAAgB,8BAA8B,CAAC,WAAW,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,IAAI,EAAE,CAAA;CAAE,YAK9E;AAwCD,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,EAAE,uBAKtF"}
|
|
@@ -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
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/context/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/context/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AA8RnD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,EACpC,IAAI,EACJ,GAAG,EACH,KAAK,EACL,IAAI,EACJ,eAAe,EACf,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,eAAsB,EACtB,OAAO,EACP,OAAO,EACR,EAAE,qBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC,CA2G1C;AAED,wBAAgB,mBAAmB,CAAC,EAClC,IAAI,EACJ,GAAG,EACH,KAAK,EACL,IAAI,EACJ,eAAe,EACf,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,eAAsB,EACtB,OAAO,EACR,EAAE,qBAAqB,GAAG,OAAO,CA0BjC"}
|
|
@@ -14,6 +14,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.GenerateContext = GenerateContext;
|
|
16
16
|
exports.GenerateContextSync = GenerateContextSync;
|
|
17
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
17
18
|
const node_module_1 = require("node:module");
|
|
18
19
|
const node_path_1 = __importDefault(require("node:path"));
|
|
19
20
|
const node_url_1 = require("node:url");
|
|
@@ -89,6 +90,27 @@ const wrapEsmModule = (code) => {
|
|
|
89
90
|
].join('\n');
|
|
90
91
|
return `${prelude}\n${code}\n${trailer}`;
|
|
91
92
|
};
|
|
93
|
+
const transpileSandboxModule = (code) => {
|
|
94
|
+
const exportedNames = [];
|
|
95
|
+
let transformed = code.includes('import ')
|
|
96
|
+
? transformImportsToRequire(code)
|
|
97
|
+
: code;
|
|
98
|
+
transformed = transformed.replace(/^\s*export\s+function\s+([A-Za-z_$][\w$]*)\s*\(/gm, (_match, name) => {
|
|
99
|
+
exportedNames.push(name);
|
|
100
|
+
return `function ${name}(`;
|
|
101
|
+
});
|
|
102
|
+
transformed = transformed.replace(/^\s*export\s+(const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm, (_match, kind, name) => {
|
|
103
|
+
exportedNames.push(name);
|
|
104
|
+
return `${kind} ${name}`;
|
|
105
|
+
});
|
|
106
|
+
transformed = transformed.replace(/^\s*export\s+default\s+/gm, 'module.exports = ');
|
|
107
|
+
if (exportedNames.length === 0) {
|
|
108
|
+
return transformed;
|
|
109
|
+
}
|
|
110
|
+
return `${transformed}\n${[...new Set(exportedNames)]
|
|
111
|
+
.map((name) => `exports.${name} = ${name}`)
|
|
112
|
+
.join('\n')}`;
|
|
113
|
+
};
|
|
92
114
|
const resolveImportTarget = (specifier, customRequire) => {
|
|
93
115
|
try {
|
|
94
116
|
const resolved = customRequire.resolve(specifier);
|
|
@@ -109,6 +131,53 @@ const shouldFallbackFromVmModules = (error) => {
|
|
|
109
131
|
const code = error.code;
|
|
110
132
|
return code === 'ERR_VM_MODULES_DISABLED' || code === 'ERR_VM_MODULES_NOT_SUPPORTED';
|
|
111
133
|
};
|
|
134
|
+
const resolveModulePath = (specifier, parentFile) => {
|
|
135
|
+
const parentDir = node_path_1.default.dirname(parentFile);
|
|
136
|
+
const basePath = node_path_1.default.resolve(parentDir, specifier);
|
|
137
|
+
const candidates = [
|
|
138
|
+
basePath,
|
|
139
|
+
`${basePath}.js`,
|
|
140
|
+
`${basePath}.ts`,
|
|
141
|
+
node_path_1.default.join(basePath, 'index.js'),
|
|
142
|
+
node_path_1.default.join(basePath, 'index.ts')
|
|
143
|
+
];
|
|
144
|
+
return candidates.find((candidate) => {
|
|
145
|
+
try {
|
|
146
|
+
return node_fs_1.default.statSync(candidate).isFile();
|
|
147
|
+
}
|
|
148
|
+
catch (_a) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const executeSandboxModule = ({ code, contextData, filePath, moduleCache }) => {
|
|
154
|
+
var _a;
|
|
155
|
+
if (moduleCache.has(filePath)) {
|
|
156
|
+
return moduleCache.get(filePath);
|
|
157
|
+
}
|
|
158
|
+
const sandboxModule = { exports: {} };
|
|
159
|
+
moduleCache.set(filePath, sandboxModule.exports);
|
|
160
|
+
const baseRequire = (0, node_module_1.createRequire)(filePath);
|
|
161
|
+
const localRequire = ((specifier) => {
|
|
162
|
+
if (specifier.startsWith('.') || specifier.startsWith('/')) {
|
|
163
|
+
const resolvedPath = resolveModulePath(specifier, filePath);
|
|
164
|
+
if (resolvedPath) {
|
|
165
|
+
return executeSandboxModule({
|
|
166
|
+
code: node_fs_1.default.readFileSync(resolvedPath, 'utf-8'),
|
|
167
|
+
contextData,
|
|
168
|
+
filePath: resolvedPath,
|
|
169
|
+
moduleCache
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return baseRequire(specifier);
|
|
174
|
+
});
|
|
175
|
+
const vmContext = vm_1.default.createContext(Object.assign(Object.assign({}, contextData), { require: localRequire, exports: sandboxModule.exports, module: sandboxModule, __filename: filePath, __dirname: node_path_1.default.dirname(filePath), __fb_require: localRequire, __fb_filename: filePath, __fb_dirname: node_path_1.default.dirname(filePath) }));
|
|
176
|
+
vm_1.default.runInContext(transpileSandboxModule(code), vmContext, { filename: filePath });
|
|
177
|
+
sandboxModule.exports = (_a = resolveExport(vmContext)) !== null && _a !== void 0 ? _a : sandboxModule.exports;
|
|
178
|
+
moduleCache.set(filePath, sandboxModule.exports);
|
|
179
|
+
return sandboxModule.exports;
|
|
180
|
+
};
|
|
112
181
|
const isExportedFunction = (value) => typeof value === 'function';
|
|
113
182
|
const getDefaultExport = (value) => {
|
|
114
183
|
if (!value || typeof value !== 'object')
|
|
@@ -128,11 +197,25 @@ const resolveExport = (ctx) => {
|
|
|
128
197
|
return contextExports;
|
|
129
198
|
return (_e = getDefaultExport(moduleExports)) !== null && _e !== void 0 ? _e : getDefaultExport(contextExports);
|
|
130
199
|
};
|
|
131
|
-
const buildVmContext = (contextData) => {
|
|
132
|
-
var _a, _b;
|
|
200
|
+
const buildVmContext = (contextData, currentFunction) => {
|
|
201
|
+
var _a, _b, _c;
|
|
133
202
|
const sandboxModule = { exports: {} };
|
|
134
|
-
const entryFile = (
|
|
135
|
-
const
|
|
203
|
+
const entryFile = (_c = (_a = currentFunction === null || currentFunction === void 0 ? void 0 : currentFunction.sourcePath) !== null && _a !== void 0 ? _a : (_b = require.main) === null || _b === void 0 ? void 0 : _b.filename) !== null && _c !== void 0 ? _c : process.cwd();
|
|
204
|
+
const moduleCache = new Map();
|
|
205
|
+
const customRequire = ((specifier) => {
|
|
206
|
+
if ((specifier.startsWith('.') || specifier.startsWith('/')) && (currentFunction === null || currentFunction === void 0 ? void 0 : currentFunction.sourcePath)) {
|
|
207
|
+
const resolvedPath = resolveModulePath(specifier, currentFunction.sourcePath);
|
|
208
|
+
if (resolvedPath) {
|
|
209
|
+
return executeSandboxModule({
|
|
210
|
+
code: node_fs_1.default.readFileSync(resolvedPath, 'utf-8'),
|
|
211
|
+
contextData,
|
|
212
|
+
filePath: resolvedPath,
|
|
213
|
+
moduleCache
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return (0, node_module_1.createRequire)(entryFile)(specifier);
|
|
218
|
+
});
|
|
136
219
|
const vmContext = vm_1.default.createContext(Object.assign(Object.assign({}, contextData), { require: customRequire, exports: sandboxModule.exports, module: sandboxModule, __filename,
|
|
137
220
|
__dirname, __fb_require: customRequire, __fb_filename: __filename, __fb_dirname: __dirname }));
|
|
138
221
|
return { sandboxModule, entryFile, customRequire, vmContext };
|
|
@@ -169,7 +252,7 @@ function GenerateContext(_a) {
|
|
|
169
252
|
GenerateContextSync,
|
|
170
253
|
request
|
|
171
254
|
});
|
|
172
|
-
const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(contextData);
|
|
255
|
+
const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(contextData, functionToRun);
|
|
173
256
|
const vmModules = vm_1.default;
|
|
174
257
|
const hasStaticImport = /\bimport\s+/.test(functionToRun.code);
|
|
175
258
|
let usedVmModules = false;
|
|
@@ -214,10 +297,7 @@ function GenerateContext(_a) {
|
|
|
214
297
|
}
|
|
215
298
|
}
|
|
216
299
|
if (!usedVmModules) {
|
|
217
|
-
|
|
218
|
-
? transformImportsToRequire(functionToRun.code)
|
|
219
|
-
: functionToRun.code;
|
|
220
|
-
vm_1.default.runInContext(codeToRun, vmContext);
|
|
300
|
+
vm_1.default.runInContext(transpileSandboxModule(functionToRun.code), vmContext, { filename: entryFile });
|
|
221
301
|
}
|
|
222
302
|
sandboxModule.exports = (_a = resolveExport(vmContext)) !== null && _a !== void 0 ? _a : sandboxModule.exports;
|
|
223
303
|
if (deserializeArgs) {
|
|
@@ -247,11 +327,8 @@ function GenerateContextSync({ args, app, rules, user, currentFunction, function
|
|
|
247
327
|
GenerateContextSync,
|
|
248
328
|
request
|
|
249
329
|
});
|
|
250
|
-
const { sandboxModule, vmContext } = buildVmContext(contextData);
|
|
251
|
-
|
|
252
|
-
? transformImportsToRequire(functionToRun.code)
|
|
253
|
-
: functionToRun.code;
|
|
254
|
-
vm_1.default.runInContext(codeToRun, vmContext);
|
|
330
|
+
const { sandboxModule, entryFile, vmContext } = buildVmContext(contextData, functionToRun);
|
|
331
|
+
vm_1.default.runInContext(transpileSandboxModule(functionToRun.code), vmContext, { filename: entryFile });
|
|
255
332
|
sandboxModule.exports = (_a = resolveExport(vmContext)) !== null && _a !== void 0 ? _a : sandboxModule.exports;
|
|
256
333
|
const fn = sandboxModule.exports;
|
|
257
334
|
if (deserializeArgs) {
|
package/package.json
CHANGED
|
@@ -10,7 +10,10 @@ export interface FunctionConfig {
|
|
|
10
10
|
disable_arg_logs?: boolean
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export type Function = Omit<FunctionConfig, 'name'> & {
|
|
13
|
+
export type Function = Omit<FunctionConfig, 'name'> & {
|
|
14
|
+
code: string
|
|
15
|
+
sourcePath?: string
|
|
16
|
+
}
|
|
14
17
|
|
|
15
18
|
export type Functions = Record<string, Function>
|
|
16
19
|
|
|
@@ -27,7 +27,7 @@ export const loadFunctions = async (rootDir = process.cwd()): Promise<Functions>
|
|
|
27
27
|
throw new Error(`File ${name}.js or ${name}.ts not found`)
|
|
28
28
|
}
|
|
29
29
|
code = fs.readFileSync(fnPath, 'utf-8')
|
|
30
|
-
acc[name] = { code, ...opts }
|
|
30
|
+
acc[name] = { code, sourcePath: fnPath, ...opts }
|
|
31
31
|
|
|
32
32
|
return acc
|
|
33
33
|
}, {} as Functions)
|
|
@@ -74,8 +74,8 @@ export const executeQuery = async ({
|
|
|
74
74
|
typeof projection !== 'undefined'
|
|
75
75
|
? parsedProjection
|
|
76
76
|
: parsedOptions &&
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
typeof parsedOptions === 'object' &&
|
|
78
|
+
'projection' in parsedOptions
|
|
79
79
|
? (parsedOptions as Document).projection
|
|
80
80
|
: undefined
|
|
81
81
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ensureClientPipelineStages, getHiddenFieldsFromRulesConfig, prependUnsetStage, applyAccessControlToPipeline } from '../utils'
|
|
1
|
+
import { ensureClientPipelineStages, getHiddenFieldsFromRulesConfig, prependUnsetStage, applyAccessControlToPipeline, mergeProjections } from '../utils'
|
|
2
2
|
import { Role } from '../../../utils/roles/interface'
|
|
3
3
|
|
|
4
4
|
describe('MongoDB Atlas aggregate helpers', () => {
|
|
@@ -165,4 +165,89 @@ describe('MongoDB Atlas aggregate helpers', () => {
|
|
|
165
165
|
})
|
|
166
166
|
})
|
|
167
167
|
})
|
|
168
|
+
|
|
169
|
+
describe('mergeProjections', () => {
|
|
170
|
+
it('returns undefined when both sides are empty', () => {
|
|
171
|
+
expect(mergeProjections(undefined, undefined)).toBeUndefined()
|
|
172
|
+
expect(mergeProjections({}, null)).toBeUndefined()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('returns the client projection when rules have none', () => {
|
|
176
|
+
expect(mergeProjections({ a: 1, b: 1 }, null)).toEqual({ a: 1, b: 1 })
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('normalizes the rules projection when client has none', () => {
|
|
180
|
+
// Mixed inclusion/exclusion rules are normalized to pure inclusion mode.
|
|
181
|
+
expect(
|
|
182
|
+
mergeProjections(undefined, { item: 1, status: 1, instock: 0 })
|
|
183
|
+
).toEqual({ item: 1, status: 1 })
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('merges plain inclusion projections (rules wins on conflict)', () => {
|
|
187
|
+
expect(
|
|
188
|
+
mergeProjections(
|
|
189
|
+
{ item: 1, price: 1 },
|
|
190
|
+
{ item: 1, status: 1 }
|
|
191
|
+
)
|
|
192
|
+
).toEqual({ item: 1, status: 1, price: 1 })
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('supports dotted client keys alongside plain rules keys', () => {
|
|
196
|
+
expect(
|
|
197
|
+
mergeProjections(
|
|
198
|
+
{ price: 1 },
|
|
199
|
+
{ item: 1, status: 1, 'instock.qty': 1 }
|
|
200
|
+
)
|
|
201
|
+
).toEqual({ item: 1, status: 1, 'instock.qty': 1, price: 1 })
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('drops client dotted keys when rules exclude the top-level field', () => {
|
|
205
|
+
// Rules: include item/status, exclude the whole `instock` subtree.
|
|
206
|
+
// Client tries to read `instock.qty` — it must be stripped.
|
|
207
|
+
expect(
|
|
208
|
+
mergeProjections(
|
|
209
|
+
{ item: 1, status: 1, 'instock.qty': 1 },
|
|
210
|
+
{ item: 1, status: 1, instock: 0 }
|
|
211
|
+
)
|
|
212
|
+
).toEqual({ item: 1, status: 1 })
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('drops every client inclusion whose top-level is excluded by rules', () => {
|
|
216
|
+
expect(
|
|
217
|
+
mergeProjections(
|
|
218
|
+
{
|
|
219
|
+
item: 1,
|
|
220
|
+
'instock.qty': 1,
|
|
221
|
+
'instock.warehouse': 1,
|
|
222
|
+
price: 1
|
|
223
|
+
},
|
|
224
|
+
{ item: 1, instock: 0 }
|
|
225
|
+
)
|
|
226
|
+
).toEqual({ item: 1, price: 1 })
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('produces pure exclusion output when neither side has inclusions', () => {
|
|
230
|
+
expect(
|
|
231
|
+
mergeProjections({ secretA: 0 }, { secretB: 0 })
|
|
232
|
+
).toEqual({ secretA: 0, secretB: 0 })
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('drops non-_id client exclusions when switching to inclusion mode', () => {
|
|
236
|
+
// Can't mix `{ price: 0, item: 1 }` in MongoDB — rules force inclusion
|
|
237
|
+
// mode so the client exclusion is silently dropped (price is implicitly
|
|
238
|
+
// excluded because it is not included).
|
|
239
|
+
expect(
|
|
240
|
+
mergeProjections({ price: 0 }, { item: 1 })
|
|
241
|
+
).toEqual({ item: 1 })
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('keeps _id: 0 alongside inclusion mode', () => {
|
|
245
|
+
expect(
|
|
246
|
+
mergeProjections({ _id: 0 }, { item: 1 })
|
|
247
|
+
).toEqual({ _id: 0, item: 1 })
|
|
248
|
+
expect(
|
|
249
|
+
mergeProjections({ item: 1 }, { _id: 0 })
|
|
250
|
+
).toEqual({ _id: 0, item: 1 })
|
|
251
|
+
})
|
|
252
|
+
})
|
|
168
253
|
})
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
getFormattedProjection,
|
|
36
36
|
getFormattedQuery,
|
|
37
37
|
getHiddenFieldsFromRulesConfig,
|
|
38
|
+
mergeProjections,
|
|
38
39
|
normalizeQuery
|
|
39
40
|
} from './utils'
|
|
40
41
|
|
|
@@ -558,13 +559,6 @@ const getOperators: GetOperatorsFunction = (
|
|
|
558
559
|
projectionOrOptions,
|
|
559
560
|
options
|
|
560
561
|
)
|
|
561
|
-
const resolvedOptions =
|
|
562
|
-
projection || normalizedOptions
|
|
563
|
-
? {
|
|
564
|
-
...(normalizedOptions ?? {}),
|
|
565
|
-
...(projection ? { projection } : {})
|
|
566
|
-
}
|
|
567
|
-
: undefined
|
|
568
562
|
const resolvedQuery = query ?? {}
|
|
569
563
|
if (!run_as_system) {
|
|
570
564
|
checkDenyOperation(
|
|
@@ -574,6 +568,17 @@ const getOperators: GetOperatorsFunction = (
|
|
|
574
568
|
)
|
|
575
569
|
// Apply access control filters to the query
|
|
576
570
|
const formattedQuery = getFormattedQuery(filters, resolvedQuery, user)
|
|
571
|
+
// Rules-level projection has priority over client-provided projection.
|
|
572
|
+
// The merged projection is passed natively to MongoDB.
|
|
573
|
+
const rulesProjection = getFormattedProjection(filters, user)
|
|
574
|
+
const finalProjection = mergeProjections(projection, rulesProjection)
|
|
575
|
+
const resolvedOptions =
|
|
576
|
+
finalProjection || normalizedOptions
|
|
577
|
+
? {
|
|
578
|
+
...(normalizedOptions ?? {}),
|
|
579
|
+
...(finalProjection ? { projection: finalProjection } : {})
|
|
580
|
+
}
|
|
581
|
+
: undefined
|
|
577
582
|
logDebug('update formattedQuery', {
|
|
578
583
|
collection: collName,
|
|
579
584
|
query,
|
|
@@ -629,8 +634,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
629
634
|
emitMongoEvent('findOne')
|
|
630
635
|
return Promise.resolve(response)
|
|
631
636
|
}
|
|
632
|
-
// System mode: no validation applied
|
|
633
|
-
const
|
|
637
|
+
// System mode: no validation applied, only client-provided projection/options.
|
|
638
|
+
const systemOptions =
|
|
639
|
+
projection || normalizedOptions
|
|
640
|
+
? {
|
|
641
|
+
...(normalizedOptions ?? {}),
|
|
642
|
+
...(projection ? { projection } : {})
|
|
643
|
+
}
|
|
644
|
+
: undefined
|
|
645
|
+
const response = await collection.findOne(resolvedQuery, systemOptions)
|
|
634
646
|
emitMongoEvent('findOne')
|
|
635
647
|
return response
|
|
636
648
|
} catch (error) {
|
|
@@ -1023,13 +1035,6 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1023
1035
|
projectionOrOptions,
|
|
1024
1036
|
options
|
|
1025
1037
|
)
|
|
1026
|
-
const resolvedOptions =
|
|
1027
|
-
projection || normalizedOptions
|
|
1028
|
-
? {
|
|
1029
|
-
...(normalizedOptions ?? {}),
|
|
1030
|
-
...(projection ? { projection } : {})
|
|
1031
|
-
}
|
|
1032
|
-
: undefined
|
|
1033
1038
|
if (!run_as_system) {
|
|
1034
1039
|
checkDenyOperation(
|
|
1035
1040
|
normalizedRules,
|
|
@@ -1039,6 +1044,17 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1039
1044
|
// Pre-query filtering based on access control rules
|
|
1040
1045
|
const formattedQuery = getFormattedQuery(filters, query, user)
|
|
1041
1046
|
const currentQuery = formattedQuery.length ? { $and: formattedQuery } : {}
|
|
1047
|
+
// Rules-level projection has priority over client-provided projection.
|
|
1048
|
+
// The merged projection is passed natively to MongoDB.
|
|
1049
|
+
const rulesProjection = getFormattedProjection(filters, user)
|
|
1050
|
+
const finalProjection = mergeProjections(projection, rulesProjection)
|
|
1051
|
+
const resolvedOptions =
|
|
1052
|
+
finalProjection || normalizedOptions
|
|
1053
|
+
? {
|
|
1054
|
+
...(normalizedOptions ?? {}),
|
|
1055
|
+
...(finalProjection ? { projection: finalProjection } : {})
|
|
1056
|
+
}
|
|
1057
|
+
: undefined
|
|
1042
1058
|
// aggiunto filter per evitare questo errore: $and argument's entries must be objects
|
|
1043
1059
|
const cursor = collection.find(currentQuery, resolvedOptions)
|
|
1044
1060
|
const originalToArray = cursor.toArray.bind(cursor)
|
|
@@ -1084,8 +1100,15 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1084
1100
|
emitMongoEvent('find')
|
|
1085
1101
|
return cursor
|
|
1086
1102
|
}
|
|
1087
|
-
// System mode: return original unfiltered cursor
|
|
1088
|
-
const
|
|
1103
|
+
// System mode: return original unfiltered cursor (only client projection/options).
|
|
1104
|
+
const systemOptions =
|
|
1105
|
+
projection || normalizedOptions
|
|
1106
|
+
? {
|
|
1107
|
+
...(normalizedOptions ?? {}),
|
|
1108
|
+
...(projection ? { projection } : {})
|
|
1109
|
+
}
|
|
1110
|
+
: undefined
|
|
1111
|
+
const cursor = collection.find(query, systemOptions)
|
|
1089
1112
|
emitMongoEvent('find')
|
|
1090
1113
|
return cursor
|
|
1091
1114
|
} catch (error) {
|
|
@@ -1327,7 +1350,7 @@ const getOperators: GetOperatorsFunction = (
|
|
|
1327
1350
|
formattedQuery,
|
|
1328
1351
|
pipeline
|
|
1329
1352
|
})
|
|
1330
|
-
const projection = getFormattedProjection(filters)
|
|
1353
|
+
const projection = getFormattedProjection(filters, user)
|
|
1331
1354
|
const hiddenFields = getHiddenFieldsFromRulesConfig(rulesConfig)
|
|
1332
1355
|
|
|
1333
1356
|
const sanitizedPipeline = applyAccessControlToPipeline(
|
|
@@ -76,20 +76,89 @@ export const getFormattedProjection = (
|
|
|
76
76
|
filters: Filter[] = [],
|
|
77
77
|
user?: User
|
|
78
78
|
): Projection | null => {
|
|
79
|
-
const projections = filters
|
|
80
|
-
.filter((
|
|
81
|
-
|
|
82
|
-
const preFilter = getValidRule({ filters, user })
|
|
83
|
-
const isValidPreFilter = !!preFilter?.length
|
|
84
|
-
return isValidPreFilter
|
|
85
|
-
}
|
|
86
|
-
return false
|
|
87
|
-
})
|
|
88
|
-
.map((f) => f.projection)
|
|
79
|
+
const projections = getValidRule({ filters, user })
|
|
80
|
+
.filter((f) => !!f.projection)
|
|
81
|
+
.map((f) => f.projection as Projection)
|
|
89
82
|
if (!projections.length) return null
|
|
90
83
|
return Object.assign({}, ...projections)
|
|
91
84
|
}
|
|
92
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Merges a client-provided projection with the one computed from rules filters.
|
|
88
|
+
*
|
|
89
|
+
* Rules have higher priority over the client:
|
|
90
|
+
* - If rules exclude a top-level field (e.g. `{ instock: 0 }`), every client
|
|
91
|
+
* reference to that field — including dotted sub-paths such as
|
|
92
|
+
* `"instock.qty": 1` — is dropped from the final projection.
|
|
93
|
+
* - If rules include a field (value `1`), it is always part of the final
|
|
94
|
+
* projection and overrides any conflicting client value.
|
|
95
|
+
* - The returned projection is always a valid MongoDB projection (no mixing of
|
|
96
|
+
* inclusion and exclusion on non-`_id` keys), so it can be passed as-is to
|
|
97
|
+
* native MongoDB methods.
|
|
98
|
+
* - Returns `undefined` when neither side provided a meaningful projection.
|
|
99
|
+
*/
|
|
100
|
+
export const mergeProjections = (
|
|
101
|
+
clientProjection: Projection | Document | undefined,
|
|
102
|
+
rulesProjection: Projection | null | undefined
|
|
103
|
+
): Projection | Document | undefined => {
|
|
104
|
+
const hasClient = !!clientProjection && Object.keys(clientProjection).length > 0
|
|
105
|
+
const hasRules = !!rulesProjection && Object.keys(rulesProjection).length > 0
|
|
106
|
+
if (!hasClient && !hasRules) return undefined
|
|
107
|
+
|
|
108
|
+
const client = (hasClient ? (clientProjection as Projection) : {}) as Projection
|
|
109
|
+
const rules = (hasRules ? (rulesProjection as Projection) : {}) as Projection
|
|
110
|
+
|
|
111
|
+
const getTopLevel = (key: string) => key.split('.')[0]
|
|
112
|
+
|
|
113
|
+
const rulesEntries = Object.entries(rules)
|
|
114
|
+
const rulesIncludeKeys = rulesEntries
|
|
115
|
+
.filter(([, value]) => value === 1)
|
|
116
|
+
.map(([key]) => key)
|
|
117
|
+
const rulesExcludeKeys = rulesEntries
|
|
118
|
+
.filter(([, value]) => value === 0)
|
|
119
|
+
.map(([key]) => key)
|
|
120
|
+
|
|
121
|
+
// Top-level fields excluded by rules (excluding `_id` which has special
|
|
122
|
+
// MongoDB semantics and is allowed alongside inclusion projections).
|
|
123
|
+
const excludedTopLevel = new Set(
|
|
124
|
+
rulesExcludeKeys.map(getTopLevel).filter((key) => key !== '_id')
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const filteredClient: Record<string, 0 | 1> = {}
|
|
128
|
+
for (const [key, value] of Object.entries(client)) {
|
|
129
|
+
if (excludedTopLevel.has(getTopLevel(key))) continue
|
|
130
|
+
filteredClient[key] = value as 0 | 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasInclusion =
|
|
134
|
+
rulesIncludeKeys.some((key) => key !== '_id') ||
|
|
135
|
+
Object.entries(filteredClient).some(([key, value]) => value === 1 && key !== '_id')
|
|
136
|
+
|
|
137
|
+
const merged: Record<string, 0 | 1> = {}
|
|
138
|
+
|
|
139
|
+
if (hasInclusion) {
|
|
140
|
+
// Inclusion mode: keep only client inclusions, then overlay rules inclusions.
|
|
141
|
+
// Client exclusions (other than `_id: 0`) are incompatible with inclusion
|
|
142
|
+
// mode and are dropped; not-included fields are implicitly excluded anyway.
|
|
143
|
+
for (const [key, value] of Object.entries(filteredClient)) {
|
|
144
|
+
if (value === 1 || key === '_id') merged[key] = value
|
|
145
|
+
}
|
|
146
|
+
for (const key of rulesIncludeKeys) merged[key] = 1
|
|
147
|
+
// Allow `_id: 0` to be forced by rules in inclusion mode.
|
|
148
|
+
for (const key of rulesExcludeKeys) {
|
|
149
|
+
if (key === '_id') merged[key] = 0
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// Pure exclusion mode: combine all exclusions from both sides.
|
|
153
|
+
for (const [key, value] of Object.entries(filteredClient)) {
|
|
154
|
+
if (value === 0) merged[key] = 0
|
|
155
|
+
}
|
|
156
|
+
for (const key of rulesExcludeKeys) merged[key] = 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return Object.keys(merged).length > 0 ? merged : undefined
|
|
160
|
+
}
|
|
161
|
+
|
|
93
162
|
export const applyAccessControlToPipeline = (
|
|
94
163
|
pipeline: AggregationPipeline,
|
|
95
164
|
rules: Record<
|
|
@@ -120,7 +189,7 @@ export const applyAccessControlToPipeline = (
|
|
|
120
189
|
checkDenyOperation(rules as Rules, currentCollection, CRUD_OPERATIONS.READ)
|
|
121
190
|
const lookupRules = rules[currentCollection] || {}
|
|
122
191
|
const formattedQuery = getFormattedQuery(lookupRules.filters, {}, user)
|
|
123
|
-
const projection = getFormattedProjection(lookupRules.filters)
|
|
192
|
+
const projection = getFormattedProjection(lookupRules.filters, user)
|
|
124
193
|
|
|
125
194
|
const nestedPipeline = applyAccessControlToPipeline(
|
|
126
195
|
lookUpStage.pipeline || [],
|
|
@@ -155,7 +224,7 @@ export const applyAccessControlToPipeline = (
|
|
|
155
224
|
checkDenyOperation(rules as Rules, currentCollection, CRUD_OPERATIONS.READ)
|
|
156
225
|
const unionRules = rules[currentCollection] || {}
|
|
157
226
|
const formattedQuery = getFormattedQuery(unionRules.filters, {}, user)
|
|
158
|
-
const projection = getFormattedProjection(unionRules.filters)
|
|
227
|
+
const projection = getFormattedProjection(unionRules.filters, user)
|
|
159
228
|
|
|
160
229
|
if (isSimpleStage) {
|
|
161
230
|
return stage
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { GenerateContextSync } from '../context'
|
|
2
2
|
import { Functions } from '../../features/functions/interface'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
3
6
|
|
|
4
7
|
const mockServices = {
|
|
5
8
|
api: jest.fn().mockReturnValue({}),
|
|
@@ -117,4 +120,41 @@ describe('context.functions.execute compatibility', () => {
|
|
|
117
120
|
|
|
118
121
|
expect(result).toBe(true)
|
|
119
122
|
})
|
|
123
|
+
|
|
124
|
+
it('loads same-directory helper modules for sandboxed functions', () => {
|
|
125
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flowerbase-context-'))
|
|
126
|
+
const helperPath = path.join(tempDir, 'getFreightRate.ts')
|
|
127
|
+
|
|
128
|
+
fs.writeFileSync(
|
|
129
|
+
helperPath,
|
|
130
|
+
'export function getFreightRate([address, amount, freightRateValues]) { return { address, total: amount * freightRateValues.multiplier } }'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const functionsList = {
|
|
134
|
+
caller: {
|
|
135
|
+
sourcePath: path.join(tempDir, 'caller.ts'),
|
|
136
|
+
code: `
|
|
137
|
+
import { getFreightRate } from './getFreightRate'
|
|
138
|
+
|
|
139
|
+
module.exports = function() {
|
|
140
|
+
return getFreightRate(['rome', 4, { multiplier: 2.5 }])
|
|
141
|
+
}
|
|
142
|
+
`
|
|
143
|
+
}
|
|
144
|
+
} as Functions
|
|
145
|
+
|
|
146
|
+
const result = GenerateContextSync({
|
|
147
|
+
args: [],
|
|
148
|
+
app: {} as any,
|
|
149
|
+
rules: {} as any,
|
|
150
|
+
user: {} as any,
|
|
151
|
+
currentFunction: functionsList.caller,
|
|
152
|
+
functionsList,
|
|
153
|
+
services: mockServices,
|
|
154
|
+
functionName: 'caller'
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
expect(result).toEqual({ address: 'rome', total: 10 })
|
|
158
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
159
|
+
})
|
|
120
160
|
})
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
1
2
|
import { createRequire } from 'node:module'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import { pathToFileURL } from 'node:url'
|
|
4
5
|
import vm from 'vm'
|
|
5
6
|
import { EJSON } from 'bson'
|
|
6
7
|
import { StateManager } from '../../state'
|
|
8
|
+
import { Function as AppFunction } from '../../features/functions/interface'
|
|
7
9
|
import { generateContextData } from './helpers'
|
|
8
10
|
import { GenerateContextParams } from './interface'
|
|
9
11
|
|
|
@@ -92,6 +94,42 @@ const wrapEsmModule = (code: string): string => {
|
|
|
92
94
|
return `${prelude}\n${code}\n${trailer}`
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
const transpileSandboxModule = (code: string): string => {
|
|
98
|
+
const exportedNames: string[] = []
|
|
99
|
+
let transformed = code.includes('import ')
|
|
100
|
+
? transformImportsToRequire(code)
|
|
101
|
+
: code
|
|
102
|
+
|
|
103
|
+
transformed = transformed.replace(
|
|
104
|
+
/^\s*export\s+function\s+([A-Za-z_$][\w$]*)\s*\(/gm,
|
|
105
|
+
(_match, name: string) => {
|
|
106
|
+
exportedNames.push(name)
|
|
107
|
+
return `function ${name}(`
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
transformed = transformed.replace(
|
|
112
|
+
/^\s*export\s+(const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm,
|
|
113
|
+
(_match, kind: string, name: string) => {
|
|
114
|
+
exportedNames.push(name)
|
|
115
|
+
return `${kind} ${name}`
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
transformed = transformed.replace(
|
|
120
|
+
/^\s*export\s+default\s+/gm,
|
|
121
|
+
'module.exports = '
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (exportedNames.length === 0) {
|
|
125
|
+
return transformed
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `${transformed}\n${[...new Set(exportedNames)]
|
|
129
|
+
.map((name) => `exports.${name} = ${name}`)
|
|
130
|
+
.join('\n')}`
|
|
131
|
+
}
|
|
132
|
+
|
|
95
133
|
const resolveImportTarget = (specifier: string, customRequire: NodeRequire): string => {
|
|
96
134
|
try {
|
|
97
135
|
const resolved = customRequire.resolve(specifier)
|
|
@@ -123,6 +161,82 @@ type SandboxContext = vm.Context & {
|
|
|
123
161
|
__fb_dirname?: string
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
type SandboxExecutionContext = ReturnType<typeof generateContextData>
|
|
165
|
+
|
|
166
|
+
const resolveModulePath = (specifier: string, parentFile: string): string | undefined => {
|
|
167
|
+
const parentDir = path.dirname(parentFile)
|
|
168
|
+
const basePath = path.resolve(parentDir, specifier)
|
|
169
|
+
const candidates = [
|
|
170
|
+
basePath,
|
|
171
|
+
`${basePath}.js`,
|
|
172
|
+
`${basePath}.ts`,
|
|
173
|
+
path.join(basePath, 'index.js'),
|
|
174
|
+
path.join(basePath, 'index.ts')
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
return candidates.find((candidate) => {
|
|
178
|
+
try {
|
|
179
|
+
return fs.statSync(candidate).isFile()
|
|
180
|
+
} catch {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const executeSandboxModule = ({
|
|
187
|
+
code,
|
|
188
|
+
contextData,
|
|
189
|
+
filePath,
|
|
190
|
+
moduleCache
|
|
191
|
+
}: {
|
|
192
|
+
code: string
|
|
193
|
+
contextData: SandboxExecutionContext
|
|
194
|
+
filePath: string
|
|
195
|
+
moduleCache: Map<string, unknown>
|
|
196
|
+
}): unknown => {
|
|
197
|
+
if (moduleCache.has(filePath)) {
|
|
198
|
+
return moduleCache.get(filePath)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sandboxModule: SandboxModule = { exports: {} }
|
|
202
|
+
moduleCache.set(filePath, sandboxModule.exports)
|
|
203
|
+
const baseRequire = createRequire(filePath)
|
|
204
|
+
|
|
205
|
+
const localRequire = ((specifier: string) => {
|
|
206
|
+
if (specifier.startsWith('.') || specifier.startsWith('/')) {
|
|
207
|
+
const resolvedPath = resolveModulePath(specifier, filePath)
|
|
208
|
+
if (resolvedPath) {
|
|
209
|
+
return executeSandboxModule({
|
|
210
|
+
code: fs.readFileSync(resolvedPath, 'utf-8'),
|
|
211
|
+
contextData,
|
|
212
|
+
filePath: resolvedPath,
|
|
213
|
+
moduleCache
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return baseRequire(specifier)
|
|
219
|
+
}) as NodeRequire
|
|
220
|
+
|
|
221
|
+
const vmContext = vm.createContext({
|
|
222
|
+
...contextData,
|
|
223
|
+
require: localRequire,
|
|
224
|
+
exports: sandboxModule.exports,
|
|
225
|
+
module: sandboxModule,
|
|
226
|
+
__filename: filePath,
|
|
227
|
+
__dirname: path.dirname(filePath),
|
|
228
|
+
__fb_require: localRequire,
|
|
229
|
+
__fb_filename: filePath,
|
|
230
|
+
__fb_dirname: path.dirname(filePath)
|
|
231
|
+
}) as SandboxContext
|
|
232
|
+
|
|
233
|
+
vm.runInContext(transpileSandboxModule(code), vmContext, { filename: filePath })
|
|
234
|
+
sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
|
|
235
|
+
moduleCache.set(filePath, sandboxModule.exports)
|
|
236
|
+
|
|
237
|
+
return sandboxModule.exports
|
|
238
|
+
}
|
|
239
|
+
|
|
126
240
|
const isExportedFunction = (value: unknown): value is ExportedFunction =>
|
|
127
241
|
typeof value === 'function'
|
|
128
242
|
|
|
@@ -141,10 +255,28 @@ const resolveExport = (ctx: SandboxContext): ExportedFunction | undefined => {
|
|
|
141
255
|
return getDefaultExport(moduleExports) ?? getDefaultExport(contextExports)
|
|
142
256
|
}
|
|
143
257
|
|
|
144
|
-
const buildVmContext = (
|
|
258
|
+
const buildVmContext = (
|
|
259
|
+
contextData: ReturnType<typeof generateContextData>,
|
|
260
|
+
currentFunction?: AppFunction
|
|
261
|
+
) => {
|
|
145
262
|
const sandboxModule: SandboxModule = { exports: {} }
|
|
146
|
-
const entryFile = require.main?.filename ?? process.cwd()
|
|
147
|
-
const
|
|
263
|
+
const entryFile = currentFunction?.sourcePath ?? require.main?.filename ?? process.cwd()
|
|
264
|
+
const moduleCache = new Map<string, unknown>()
|
|
265
|
+
const customRequire = ((specifier: string) => {
|
|
266
|
+
if ((specifier.startsWith('.') || specifier.startsWith('/')) && currentFunction?.sourcePath) {
|
|
267
|
+
const resolvedPath = resolveModulePath(specifier, currentFunction.sourcePath)
|
|
268
|
+
if (resolvedPath) {
|
|
269
|
+
return executeSandboxModule({
|
|
270
|
+
code: fs.readFileSync(resolvedPath, 'utf-8'),
|
|
271
|
+
contextData,
|
|
272
|
+
filePath: resolvedPath,
|
|
273
|
+
moduleCache
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return createRequire(entryFile)(specifier)
|
|
279
|
+
}) as NodeRequire
|
|
148
280
|
|
|
149
281
|
const vmContext: SandboxContext = vm.createContext({
|
|
150
282
|
...contextData,
|
|
@@ -206,7 +338,10 @@ export async function GenerateContext({
|
|
|
206
338
|
GenerateContextSync,
|
|
207
339
|
request
|
|
208
340
|
})
|
|
209
|
-
const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(
|
|
341
|
+
const { sandboxModule, entryFile, customRequire, vmContext } = buildVmContext(
|
|
342
|
+
contextData,
|
|
343
|
+
functionToRun
|
|
344
|
+
)
|
|
210
345
|
|
|
211
346
|
const vmModules = vm as typeof vm & {
|
|
212
347
|
SourceTextModule?: typeof vm.SourceTextModule
|
|
@@ -271,10 +406,7 @@ export async function GenerateContext({
|
|
|
271
406
|
}
|
|
272
407
|
|
|
273
408
|
if (!usedVmModules) {
|
|
274
|
-
|
|
275
|
-
? transformImportsToRequire(functionToRun.code)
|
|
276
|
-
: functionToRun.code
|
|
277
|
-
vm.runInContext(codeToRun, vmContext)
|
|
409
|
+
vm.runInContext(transpileSandboxModule(functionToRun.code), vmContext, { filename: entryFile })
|
|
278
410
|
}
|
|
279
411
|
|
|
280
412
|
sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
|
|
@@ -323,12 +455,9 @@ export function GenerateContextSync({
|
|
|
323
455
|
GenerateContextSync,
|
|
324
456
|
request
|
|
325
457
|
})
|
|
326
|
-
const { sandboxModule, vmContext } = buildVmContext(contextData)
|
|
327
|
-
const codeToRun = functionToRun.code.includes('import ')
|
|
328
|
-
? transformImportsToRequire(functionToRun.code)
|
|
329
|
-
: functionToRun.code
|
|
458
|
+
const { sandboxModule, entryFile, vmContext } = buildVmContext(contextData, functionToRun)
|
|
330
459
|
|
|
331
|
-
vm.runInContext(
|
|
460
|
+
vm.runInContext(transpileSandboxModule(functionToRun.code), vmContext, { filename: entryFile })
|
|
332
461
|
sandboxModule.exports = resolveExport(vmContext) ?? sandboxModule.exports
|
|
333
462
|
const fn = sandboxModule.exports as ExportedFunction
|
|
334
463
|
if (deserializeArgs) {
|