feat: Add request context middleware for config and dependency injection in hooks (#8480)

This commit is contained in:
Antoine Cormouls
2025-10-14 20:16:31 +02:00
committed by GitHub
parent e704de83e6
commit 64f104e5c5
12 changed files with 219 additions and 29 deletions

View File

@@ -11,8 +11,8 @@ const mockAdapter = {
name: filename,
location: `http://www.somewhere.com/${filename}`,
}),
deleteFile: () => {},
getFileData: () => {},
deleteFile: () => { },
getFileData: () => { },
getFileLocation: (config, filename) => `http://www.somewhere.com/${filename}`,
validateFilename: () => {
return null;
@@ -49,7 +49,7 @@ describe('Cloud Code', () => {
});
it('cloud code must be valid type', async () => {
spyOn(console, 'error').and.callFake(() => {});
spyOn(console, 'error').and.callFake(() => { });
await expectAsync(reconfigureServer({ cloud: true })).toBeRejectedWith(
"argument 'cloud' must either be a string or a function"
);
@@ -114,7 +114,7 @@ describe('Cloud Code', () => {
it('show warning on duplicate cloud functions', done => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'warn').and.callFake(() => {});
spyOn(logger, 'warn').and.callFake(() => { });
Parse.Cloud.define('hello', () => {
return 'Hello world!';
});
@@ -1672,7 +1672,7 @@ describe('Cloud Code', () => {
});
it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => {
Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {});
Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => { });
const TestObject = Parse.Object.extend('TestObject');
const NoBeforeSaveObject = Parse.Object.extend('NoBeforeSave');
@@ -1745,7 +1745,7 @@ describe('Cloud Code', () => {
});
it('beforeSave should not affect fetched pointers', done => {
Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {});
Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => { });
Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) {
req.object.set('foo', 'baz');
@@ -2059,7 +2059,7 @@ describe('Cloud Code', () => {
});
it('pointer should not be cleared by triggers', async () => {
Parse.Cloud.afterSave('MyObject', () => {});
Parse.Cloud.afterSave('MyObject', () => { });
const foo = await new Parse.Object('Test', { foo: 'bar' }).save();
const obj = await new Parse.Object('MyObject', { foo }).save();
const foo2 = obj.get('foo');
@@ -2067,7 +2067,7 @@ describe('Cloud Code', () => {
});
it('can set a pointer in triggers', async () => {
Parse.Cloud.beforeSave('MyObject', () => {});
Parse.Cloud.beforeSave('MyObject', () => { });
Parse.Cloud.afterSave(
'MyObject',
async ({ object }) => {
@@ -2168,7 +2168,7 @@ describe('Cloud Code', () => {
it('should not run without master key', done => {
expect(() => {
Parse.Cloud.job('myJob', () => {});
Parse.Cloud.job('myJob', () => { });
}).not.toThrow();
request({
@@ -2354,6 +2354,14 @@ describe('cloud functions', () => {
Parse.Cloud.run('myFunction', {}).then(() => done());
});
it('should have request config', async () => {
Parse.Cloud.define('myConfigFunction', req => {
expect(req.config).toBeDefined();
return 'success';
});
await Parse.Cloud.run('myConfigFunction', {});
});
});
describe('beforeSave hooks', () => {
@@ -2377,6 +2385,16 @@ describe('beforeSave hooks', () => {
myObject.save().then(() => done());
});
it('should have request config', async () => {
Parse.Cloud.beforeSave('MyObject', req => {
expect(req.config).toBeDefined();
});
const MyObject = Parse.Object.extend('MyObject');
const myObject = new MyObject();
await myObject.save();
});
it('should respect custom object ids (#6733)', async () => {
Parse.Cloud.beforeSave('TestObject', req => {
expect(req.object.id).toEqual('test_6733');
@@ -2432,6 +2450,16 @@ describe('afterSave hooks', () => {
myObject.save().then(() => done());
});
it('should have request config', async () => {
Parse.Cloud.afterSave('MyObject', req => {
expect(req.config).toBeDefined();
});
const MyObject = Parse.Object.extend('MyObject');
const myObject = new MyObject();
await myObject.save();
});
it('should unset in afterSave', async () => {
Parse.Cloud.afterSave(
'MyObject',
@@ -2489,6 +2517,17 @@ describe('beforeDelete hooks', () => {
.then(myObj => myObj.destroy())
.then(() => done());
});
it('should have request config', async () => {
Parse.Cloud.beforeDelete('MyObject', req => {
expect(req.config).toBeDefined();
});
const MyObject = Parse.Object.extend('MyObject');
const myObject = new MyObject();
await myObject.save();
await myObject.destroy();
});
});
describe('afterDelete hooks', () => {
@@ -2517,6 +2556,17 @@ describe('afterDelete hooks', () => {
.then(myObj => myObj.destroy())
.then(() => done());
});
it('should have request config', async () => {
Parse.Cloud.afterDelete('MyObject', req => {
expect(req.config).toBeDefined();
});
const MyObject = Parse.Object.extend('MyObject');
const myObject = new MyObject();
await myObject.save();
await myObject.destroy();
});
});
describe('beforeFind hooks', () => {
@@ -2824,6 +2874,18 @@ describe('beforeFind hooks', () => {
.then(() => done());
});
it('should have request config', async () => {
Parse.Cloud.beforeFind('MyObject', req => {
expect(req.config).toBeDefined();
});
const MyObject = Parse.Object.extend('MyObject');
const myObject = new MyObject();
await myObject.save();
const query = new Parse.Query('MyObject');
query.equalTo('objectId', myObject.id);
await Promise.all([query.get(myObject.id), query.first(), query.find()]);
})
it('should run beforeFind on pointers and array of pointers from an object', async () => {
const obj1 = new Parse.Object('TestObject');
const obj2 = new Parse.Object('TestObject2');
@@ -3208,54 +3270,67 @@ describe('afterFind hooks', () => {
.catch(done.fail);
});
it('should have request config', async () => {
Parse.Cloud.afterFind('MyObject', req => {
expect(req.config).toBeDefined();
});
const MyObject = Parse.Object.extend('MyObject');
const myObject = new MyObject();
await myObject.save();
const query = new Parse.Query('MyObject');
query.equalTo('objectId', myObject.id);
await Promise.all([query.get(myObject.id), query.first(), query.find()]);
});
it('should validate triggers correctly', () => {
expect(() => {
Parse.Cloud.beforeSave('_Session', () => {});
Parse.Cloud.beforeSave('_Session', () => { });
}).toThrow('Only the afterLogout trigger is allowed for the _Session class.');
expect(() => {
Parse.Cloud.afterSave('_Session', () => {});
Parse.Cloud.afterSave('_Session', () => { });
}).toThrow('Only the afterLogout trigger is allowed for the _Session class.');
expect(() => {
Parse.Cloud.beforeSave('_PushStatus', () => {});
Parse.Cloud.beforeSave('_PushStatus', () => { });
}).toThrow('Only afterSave is allowed on _PushStatus');
expect(() => {
Parse.Cloud.afterSave('_PushStatus', () => {});
Parse.Cloud.afterSave('_PushStatus', () => { });
}).not.toThrow();
expect(() => {
Parse.Cloud.beforeLogin(() => {});
Parse.Cloud.beforeLogin(() => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.beforeLogin('_User', () => {});
Parse.Cloud.beforeLogin('_User', () => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.beforeLogin(Parse.User, () => {});
Parse.Cloud.beforeLogin(Parse.User, () => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.beforeLogin('SomeClass', () => {});
Parse.Cloud.beforeLogin('SomeClass', () => { });
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.afterLogin(() => {});
Parse.Cloud.afterLogin(() => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.afterLogin('_User', () => {});
Parse.Cloud.afterLogin('_User', () => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.afterLogin(Parse.User, () => {});
Parse.Cloud.afterLogin(Parse.User, () => { });
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.afterLogin('SomeClass', () => {});
Parse.Cloud.afterLogin('SomeClass', () => { });
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
expect(() => {
Parse.Cloud.afterLogout(() => {});
Parse.Cloud.afterLogout(() => { });
}).not.toThrow();
expect(() => {
Parse.Cloud.afterLogout('_Session', () => {});
Parse.Cloud.afterLogout('_Session', () => { });
}).not.toThrow();
expect(() => {
Parse.Cloud.afterLogout('_User', () => {});
Parse.Cloud.afterLogout('_User', () => { });
}).toThrow('Only the _Session class is allowed for the afterLogout trigger.');
expect(() => {
Parse.Cloud.afterLogout('SomeClass', () => {});
Parse.Cloud.afterLogout('SomeClass', () => { });
}).toThrow('Only the _Session class is allowed for the afterLogout trigger.');
});
@@ -3695,6 +3770,7 @@ describe('beforeLogin hook', () => {
expect(req.ip).toBeDefined();
expect(req.installationId).toBeDefined();
expect(req.context).toBeDefined();
expect(req.config).toBeDefined();
});
await Parse.User.signUp('tupac', 'shakur');
@@ -3812,6 +3888,7 @@ describe('afterLogin hook', () => {
expect(req.ip).toBeDefined();
expect(req.installationId).toBeDefined();
expect(req.context).toBeDefined();
expect(req.config).toBeDefined();
});
await Parse.User.signUp('testuser', 'p@ssword');
@@ -4014,6 +4091,15 @@ describe('saveFile hooks', () => {
}
});
it('beforeSaveFile should have config', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
Parse.Cloud.beforeSave(Parse.File, req => {
expect(req.config).toBeDefined();
});
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save({ useMasterKey: true });
});
it('beforeSave(Parse.File) should change values of uploaded file by editing fileObject directly', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();
@@ -4316,7 +4402,7 @@ describe('Parse.File hooks', () => {
beforeFind() {
throw 'unauthorized';
},
afterFind() {},
afterFind() { },
};
for (const hook in hooks) {
spyOn(hooks, hook).and.callThrough();
@@ -4344,7 +4430,7 @@ describe('Parse.File hooks', () => {
await file.save({ useMasterKey: true });
const user = await Parse.User.signUp('username', 'password');
const hooks = {
beforeFind() {},
beforeFind() { },
afterFind() {
throw 'unauthorized';
},
@@ -4563,7 +4649,7 @@ describe('sendEmail', () => {
it('cannot send email without adapter', async () => {
const logger = require('../lib/logger').logger;
spyOn(logger, 'error').and.callFake(() => {});
spyOn(logger, 'error').and.callFake(() => { });
await Parse.Cloud.sendEmail({});
expect(logger.error).toHaveBeenCalledWith(
'Failed to send email because no mail adapter is configured for Parse Server.'

View File

@@ -607,6 +607,41 @@ describe('ParseGraphQLServer', () => {
]);
};
describe('Context', () => {
it('should support dependency injection on graphql api', async () => {
const requestContextMiddleware = (req, res, next) => {
req.config.aCustomController = 'aCustomController';
next();
};
let called;
const parseServer = await reconfigureServer({ requestContextMiddleware });
await createGQLFromParseServer(parseServer);
Parse.Cloud.beforeSave('_User', request => {
expect(request.config.aCustomController).toEqual('aCustomController');
called = true;
});
await apolloClient.query({
query: gql`
mutation {
createUser(input: { fields: { username: "test", password: "test" } }) {
user {
objectId
}
}
}
`,
context: {
headers: {
'X-Parse-Master-Key': 'test',
},
}
})
expect(called).toBe(true);
})
})
describe('Introspection', () => {
it('should have public introspection disabled by default without master key', async () => {

View File

@@ -1139,3 +1139,25 @@ describe('read-only masterKey', () => {
});
});
});
describe('rest context', () => {
it('should support dependency injection on rest api', async () => {
const requestContextMiddleware = (req, res, next) => {
req.config.aCustomController = 'aCustomController';
next();
};
let called
await reconfigureServer({ requestContextMiddleware });
Parse.Cloud.beforeSave('_User', request => {
expect(request.config.aCustomController).toEqual('aCustomController');
called = true;
});
const user = new Parse.User();
user.setUsername('test');
user.setPassword('test');
await user.signUp();
expect(called).toBe(true);
});
});