Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a423f0c
fix: strip undefined values in `ParseServerRESTController` to match H…
faceapps Mar 21, 2026
e98ffc9
refactor: use JSON.parse(JSON.stringify()) instead of stripUndefined
faceapps Mar 21, 2026
5275d96
refactor: use recursive stripUndefined instead of JSON.parse(JSON.str…
faceapps Mar 21, 2026
4385494
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Mar 22, 2026
41223c0
test: Add HTTP mode comparison test for undefined values on update
faceapps Mar 23, 2026
2928931
refactor: Let HTTP layer handle JSON serialization in test
faceapps Mar 23, 2026
07cba86
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Mar 23, 2026
5910c02
fix: strip undefined values from directAccess response to match HTTP …
faceapps Mar 23, 2026
32383e0
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Mar 23, 2026
d26fe93
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Mar 23, 2026
561a65a
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Mar 24, 2026
1f19361
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Mar 25, 2026
c2bf2c7
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Mar 26, 2026
002d52c
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Mar 27, 2026
6bf3a11
fix: Use JSON.parse(JSON.stringify()) for HTTP parity, strip query pa…
yog27ray Mar 28, 2026
2f9d017
Merge branch 'alpha' into fix/directaccess-undefined-to-null
mtrezza Mar 31, 2026
3ce7a10
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Apr 2, 2026
0e8482f
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Apr 9, 2026
82af995
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Apr 10, 2026
455910c
Merge branch 'alpha' into fix/directaccess-undefined-to-null
mtrezza Apr 12, 2026
cee25d3
Merge branch 'alpha' of github.com:yog27ray/parse-server into fix/dir…
yog27ray Apr 13, 2026
537c412
test: add GET query params undefined stripping tests for directAccess…
yog27ray Apr 13, 2026
0e56fdf
perf: replace JSON.parse(JSON.stringify()) with in-place stripUndefined
yog27ray Apr 16, 2026
4802acb
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray May 14, 2026
3a47380
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray May 20, 2026
475df0a
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Jun 3, 2026
6e62c66
Merge branch 'alpha' into fix/directaccess-undefined-to-null
mtrezza Aug 21, 2026
e8a4e18
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Sep 8, 2026
afaa510
Merge branch 'alpha' into fix/directaccess-undefined-to-null
mtrezza Sep 9, 2026
b65e383
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Sep 10, 2026
c1ef2ca
Merge branch 'alpha' into fix/directaccess-undefined-to-null
yog27ray Sep 22, 2026
ee1ae41
Merge remote-tracking branch 'upstream/alpha' into fix/directaccess-u…
yog27ray Oct 12, 2026
6ef99fd
fix: avoid mutating request body in stripUndefined
yog27ray Oct 12, 2026
953547f
fix: preserve toJSON() values in stripUndefined for directAccess parity
yog27ray Oct 12, 2026
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
345 changes: 330 additions & 15 deletions spec/ParseServerRESTController.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const ParseServerRESTController = require('../lib/ParseServerRESTController')
.ParseServerRESTController;
const ParseServerRESTController =
require('../lib/ParseServerRESTController').ParseServerRESTController;
const ParseServer = require('../lib/ParseServer').default;
const Parse = require('parse/node').Parse;
const request = require('../lib/request');

let RESTController;

Expand Down Expand Up @@ -163,9 +164,7 @@ describe('ParseServerRESTController', () => {
const results = await query.find();
expect(createSpy.calls.count()).toBe(2);
for (let i = 0; i + 1 < createSpy.calls.length; i = i + 2) {
expect(createSpy.calls.argsFor(i)[3]).toBe(
createSpy.calls.argsFor(i + 1)[3]
);
expect(createSpy.calls.argsFor(i)[3]).toBe(createSpy.calls.argsFor(i + 1)[3]);
}
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
});
Expand Down Expand Up @@ -610,6 +609,129 @@ describe('ParseServerRESTController', () => {
}
});

it('should strip undefined values from cloud function responses (with directAccess)', async () => {
Parse.Cloud.define('returnUndefinedValues', () => {
return {
definedKey: 'value',
undefinedKey: undefined,
nested: { a: 1, b: undefined },
arrayWithUndefined: [1, undefined, 3],
};
});

const res = await RESTController.request(
'POST',
'/functions/returnUndefinedValues',
{},
{ useMasterKey: true }
);

expect(res.result.definedKey).toEqual('value');
expect(res.result.undefinedKey).toBeUndefined();
expect(Object.hasOwnProperty.call(res.result, 'undefinedKey')).toBe(false);
expect(res.result.nested.a).toEqual(1);
expect(Object.hasOwnProperty.call(res.result.nested, 'b')).toBe(false);
expect(res.result.arrayWithUndefined).toEqual([1, null, 3]);
});

it('should strip undefined values from cloud function responses (without directAccess)', async () => {
Parse.Cloud.define('returnUndefinedValuesHTTP', () => {
return {
definedKey: 'value',
undefinedKey: undefined,
nested: { a: 1, b: undefined },
arrayWithUndefined: [1, undefined, 3],
};
});

const serverURL = 'http://localhost:8378/1';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
};

const res = await request({
method: 'POST',
headers,
url: `${serverURL}/functions/returnUndefinedValuesHTTP`,
body: {},
});

const result = res.data.result;

expect(result.definedKey).toEqual('value');
expect(result.undefinedKey).toBeUndefined();
expect(Object.hasOwnProperty.call(result, 'undefinedKey')).toBe(false);
expect(result.nested.a).toEqual(1);
expect(Object.hasOwnProperty.call(result.nested, 'b')).toBe(false);
expect(result.arrayWithUndefined).toEqual([1, null, 3]);
});

it('should call toJSON() on cloud function results with a custom toJSON (with directAccess)', async () => {
Parse.Cloud.define('returnCustomToJSON', () => {
return {
raw: 'should be replaced',
undefinedKey: undefined,
toJSON() {
return { serialized: true, undefinedKey: undefined, nested: { b: undefined, c: 2 } };
},
};
});

const res = await RESTController.request(
'POST',
'/functions/returnCustomToJSON',
{},
{ useMasterKey: true }
);

// toJSON() must be invoked (matching JSON.stringify in HTTP mode), not treated
// as a plain record, and undefined keys inside its result must be stripped.
expect(res.result.serialized).toEqual(true);
expect(res.result.raw).toBeUndefined();
expect(Object.hasOwnProperty.call(res.result, 'raw')).toBe(false);
expect(Object.hasOwnProperty.call(res.result, 'undefinedKey')).toBe(false);
expect(Object.hasOwnProperty.call(res.result, 'toJSON')).toBe(false);
expect(res.result.nested.c).toEqual(2);
expect(Object.hasOwnProperty.call(res.result.nested, 'b')).toBe(false);
});

it('should call toJSON() on cloud function results with a custom toJSON (without directAccess)', async () => {
Parse.Cloud.define('returnCustomToJSONHTTP', () => {
return {
raw: 'should be replaced',
undefinedKey: undefined,
toJSON() {
return { serialized: true, undefinedKey: undefined, nested: { b: undefined, c: 2 } };
},
};
});

const serverURL = 'http://localhost:8378/1';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
};

const res = await request({
method: 'POST',
headers,
url: `${serverURL}/functions/returnCustomToJSONHTTP`,
body: {},
});

const result = res.data.result;
expect(result.serialized).toEqual(true);
expect(result.raw).toBeUndefined();
expect(Object.hasOwnProperty.call(result, 'raw')).toBe(false);
expect(Object.hasOwnProperty.call(result, 'undefinedKey')).toBe(false);
expect(Object.hasOwnProperty.call(result, 'toJSON')).toBe(false);
expect(result.nested.c).toEqual(2);
expect(Object.hasOwnProperty.call(result.nested, 'b')).toBe(false);
});

it('ensures sessionTokens are properly handled', async () => {
const user = await Parse.User.signUp('user', 'pass');
const sessionToken = user.getSessionToken();
Expand Down Expand Up @@ -750,23 +872,216 @@ describe('ParseServerRESTController', () => {
data: { alert: 'We return status!' },
where: { deviceType: 'ios' },
};
const res = await RESTController.request('POST', 'batch', {
requests: [{
method: 'POST',
path: '/push',
body: payload,
}],
}, {
useMasterKey: true,
returnStatus: true,
});
const res = await RESTController.request(
'POST',
'batch',
{
requests: [
{
method: 'POST',
path: '/push',
body: payload,
},
],
},
{
useMasterKey: true,
returnStatus: true,
}
);
const pushStatusId = res[0]._headers['X-Parse-Push-Status-Id'];
expect(pushStatusId).toBeDefined();

const result = await Parse.Push.getPushStatus(pushStatusId);
expect(result.id).toBe(pushStatusId);
});

it('should strip undefined values from GET query params with directAccess', async () => {
let capturedWhere;
Parse.Cloud.beforeFind('QueryStripObject', req => {
capturedWhere = req.query.toJSON().where;
});

const uniqueValue = `directAccess_${Date.now()}`;
await RESTController.request('POST', '/classes/QueryStripObject', {
presentField: uniqueValue,
absentField: undefined,
});

// If undefined values in "where" are not stripped, they would become null
// and the query would filter for absentField: null, failing to match the object.
const getRes = await RESTController.request('GET', '/classes/QueryStripObject', {
where: { presentField: uniqueValue, absentField: undefined },
});

expect(getRes.results.length).toBe(1);
expect(getRes.results[0].presentField).toBe(uniqueValue);
expect(capturedWhere.presentField).toBe(uniqueValue);
expect('absentField' in capturedWhere).toBe(false);
});

it('should strip undefined values from GET query params without directAccess (HTTP mode)', async () => {
let capturedWhere;
Parse.Cloud.beforeFind('QueryStripObjectHTTP', req => {
capturedWhere = req.query.toJSON().where;
});

const serverURL = 'http://localhost:8378/1';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
};
const uniqueValue = `httpMode_${Date.now()}`;

await request({
method: 'POST',
headers,
url: `${serverURL}/classes/QueryStripObjectHTTP`,
body: { presentField: uniqueValue, absentField: undefined },
});

// In HTTP mode, JSON.stringify naturally strips undefined values from the where clause.
const getRes = await request({
method: 'GET',
headers,
url: `${serverURL}/classes/QueryStripObjectHTTP?where=${encodeURIComponent(JSON.stringify({ presentField: uniqueValue, absentField: undefined }))}`,
});

expect(getRes.data.results.length).toBe(1);
expect(getRes.data.results[0].presentField).toBe(uniqueValue);
expect(capturedWhere.presentField).toBe(uniqueValue);
expect('absentField' in capturedWhere).toBe(false);
});

it('should not convert undefined values to null on update with directAccess', async () => {
const createRes = await RESTController.request('POST', '/classes/MyObject', {
presentField: 'hello',
});
expect(createRes.objectId).toBeDefined();

await RESTController.request('PUT', `/classes/MyObject/${createRes.objectId}`, {
presentField: 'updated',
absentField: undefined,
nested: { absentField: undefined, presentField: 'value' },
});

const getRes = await RESTController.request('GET', `/classes/MyObject/${createRes.objectId}`);

expect(getRes.presentField).toBe('updated');
expect(getRes.absentField).toBeUndefined();
expect('absentField' in getRes).toBe(false);
expect(getRes.nested).toBeDefined();
expect(getRes.nested.presentField).toBe('value');
expect('absentField' in getRes.nested).toBe(false);
});

it('should not convert undefined values to null on create with directAccess', async () => {
const createRes = await RESTController.request('POST', '/classes/MyObject', {
presentField: 'hello',
absentField: undefined,
});
expect(createRes.objectId).toBeDefined();

const getRes = await RESTController.request('GET', `/classes/MyObject/${createRes.objectId}`);

expect(getRes.presentField).toBe('hello');
expect(getRes.absentField).toBeUndefined();
expect('absentField' in getRes).toBe(false);
});

it('should not throw when request body is frozen (directAccess)', async () => {
const createRes = await RESTController.request('POST', '/classes/MyObject', {
presentField: 'hello',
});
expect(createRes.objectId).toBeDefined();

const body = Object.freeze({
presentField: 'updated',
absentField: undefined,
nested: Object.freeze({ absentField: undefined, presentField: 'value' }),
arrayWithUndefined: Object.freeze([1, undefined, 3]),
});

await RESTController.request('PUT', `/classes/MyObject/${createRes.objectId}`, body);

const getRes = await RESTController.request('GET', `/classes/MyObject/${createRes.objectId}`);

expect(getRes.presentField).toBe('updated');
expect('absentField' in getRes).toBe(false);
expect(getRes.nested.presentField).toBe('value');
expect('absentField' in getRes.nested).toBe(false);
expect(getRes.arrayWithUndefined).toEqual([1, null, 3]);
});

it('should not convert undefined values to null on update without directAccess (HTTP mode)', async () => {
const serverURL = 'http://localhost:8378/1';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
};

const createRes = await request({
method: 'POST',
headers,
url: `${serverURL}/classes/MyObject`,
body: { presentField: 'hello' },
});
expect(createRes.data.objectId).toBeDefined();

await request({
method: 'PUT',
headers,
url: `${serverURL}/classes/MyObject/${createRes.data.objectId}`,
body: {
presentField: 'updated',
absentField: undefined,
nested: { absentField: undefined, presentField: 'value' },
},
});

const getRes = await request({
method: 'GET',
headers,
url: `${serverURL}/classes/MyObject/${createRes.data.objectId}`,
});

expect(getRes.data.presentField).toBe('updated');
expect(getRes.data.absentField).toBeUndefined();
expect('absentField' in getRes.data).toBe(false);
expect(getRes.data.nested).toBeDefined();
expect(getRes.data.nested.presentField).toBe('value');
expect('absentField' in getRes.data.nested).toBe(false);
});

it('should not convert undefined values to null on create without directAccess (HTTP mode)', async () => {
const serverURL = 'http://localhost:8378/1';
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
};

const createRes = await request({
method: 'POST',
headers,
url: `${serverURL}/classes/MyObject`,
body: { presentField: 'hello', absentField: undefined },
});
expect(createRes.data.objectId).toBeDefined();

const getRes = await request({
method: 'GET',
headers,
url: `${serverURL}/classes/MyObject/${createRes.data.objectId}`,
});

expect(getRes.data.presentField).toBe('hello');
expect(getRes.data.absentField).toBeUndefined();
expect('absentField' in getRes.data).toBe(false);
});

it('handles an object ID without alphanumeric characters', async () => {
await reconfigureServer({ allowCustomObjectId: true });
await RESTController.request('POST', '/classes/TestObject', { objectId: '-', key: 'value' });
Expand Down
Loading
Loading