Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
56d6516
feat: add Parse Server config to standard request object
Moumouls Jun 24, 2022
d85f37f
wip
Moumouls Mar 12, 2023
5926924
Merge remote-tracking branch 'origin/moumouls/addConfigUnderRequest' …
Moumouls Mar 12, 2023
65f83c1
feat: requestContextMiddleware and config in hooks
Moumouls Mar 12, 2023
8340f5b
fix: restore lock
Moumouls Mar 12, 2023
8c8e1eb
fix: defs
Moumouls Mar 12, 2023
748f68f
fix: import
Moumouls Mar 12, 2023
c98199c
Merge branch 'upstream/alpha' into moumouls/requestContextMiddleWare
Moumouls Oct 8, 2025
fbe389b
fix: lint
Moumouls Oct 8, 2025
344116a
test: fix
Moumouls Oct 8, 2025
9bbc7df
fix: ai suggestion
Moumouls Oct 8, 2025
98b1287
test: use pure fetch
Moumouls Oct 9, 2025
099d109
test: try to fix
Moumouls Oct 9, 2025
d08b157
test: try to fix
Moumouls Oct 9, 2025
29a0d90
test: try again
Moumouls Oct 9, 2025
264b1fa
fix: describe name
Moumouls Oct 9, 2025
1841072
fix: use scoped path
Moumouls Oct 9, 2025
03bc937
test: use single test
Moumouls Oct 9, 2025
5804a19
test: split
Moumouls Oct 9, 2025
3b99da0
Merge branch 'alpha' into moumouls/requestContextMiddleWare
Moumouls Oct 9, 2025
481a5e4
test: avoid race
Moumouls Oct 9, 2025
41aa216
Merge branch 'moumouls/requestContextMiddleWare' of github.com:Moumou…
Moumouls Oct 9, 2025
4d42d53
Merge branch 'alpha' into moumouls/requestContextMiddleWare
mtrezza Oct 9, 2025
cedb2d2
Update spec/ParseGraphQLServer.spec.js
mtrezza Oct 9, 2025
29feaf0
Merge branch 'alpha' into moumouls/requestContextMiddleWare
Moumouls Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 114 additions & 28 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"
);
Expand Down Expand Up @@ -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!';
});
Expand Down Expand Up @@ -1332,7 +1332,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');
Expand Down Expand Up @@ -1405,7 +1405,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');
Expand Down Expand Up @@ -1719,15 +1719,15 @@ 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');
expect(foo2.get('foo')).toBe('bar');
});

it('can set a pointer in triggers', async () => {
Parse.Cloud.beforeSave('MyObject', () => {});
Parse.Cloud.beforeSave('MyObject', () => { });
Parse.Cloud.afterSave(
'MyObject',
async ({ object }) => {
Expand Down Expand Up @@ -1828,7 +1828,7 @@ describe('Cloud Code', () => {

it('should not run without master key', done => {
expect(() => {
Parse.Cloud.job('myJob', () => {});
Parse.Cloud.job('myJob', () => { });
}).not.toThrow();

request({
Expand Down Expand Up @@ -2014,6 +2014,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', () => {
Expand All @@ -2037,6 +2045,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');
Expand Down Expand Up @@ -2092,6 +2110,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',
Expand Down Expand Up @@ -2149,6 +2177,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', () => {
Expand Down Expand Up @@ -2177,6 +2216,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', () => {
Expand Down Expand Up @@ -2484,6 +2534,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');
Expand Down Expand Up @@ -2868,54 +2930,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.');
});

Expand Down Expand Up @@ -3355,6 +3430,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');
Expand Down Expand Up @@ -3472,6 +3548,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');
Expand Down Expand Up @@ -3674,6 +3751,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();
Expand Down Expand Up @@ -3976,7 +4062,7 @@ describe('Parse.File hooks', () => {
beforeFind() {
throw 'unauthorized';
},
afterFind() {},
afterFind() { },
};
for (const hook in hooks) {
spyOn(hooks, hook).and.callThrough();
Expand Down Expand Up @@ -4004,7 +4090,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';
},
Expand Down Expand Up @@ -4223,7 +4309,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.'
Expand Down
35 changes: 35 additions & 0 deletions spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {

Expand Down
22 changes: 22 additions & 0 deletions spec/rest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
2 changes: 2 additions & 0 deletions src/Controllers/HooksController.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ function wrapToHTTPRequest(hook, key) {
return req => {
const jsonBody = {};
for (var i in req) {
// Parse Server config is not serializable
if (i === 'config') { continue; }
jsonBody[i] = req[i];
}
if (req.object) {
Expand Down
Loading
Loading