Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 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
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
160 changes: 160 additions & 0 deletions spec/ParseServerRESTController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 @@ -519,6 +520,65 @@ 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('ensures sessionTokens are properly handled', async () => {
const user = await Parse.User.signUp('user', 'pass');
const sessionToken = user.getSessionToken();
Expand Down Expand Up @@ -675,4 +735,104 @@ describe('ParseServerRESTController', () => {
const result = await Parse.Push.getPushStatus(pushStatusId);
expect(result.id).toBe(pushStatusId);
});

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 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);
});
});
9 changes: 5 additions & 4 deletions src/ParseServerRESTController.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function ParseServerRESTController(applicationId, router) {
return new Promise((resolve, reject) => {
getAuth(options, config).then(auth => {
const request = {
body: data,
body: JSON.parse(JSON.stringify(data)),
config,
auth,
info: {
Expand All @@ -122,7 +122,7 @@ function ParseServerRESTController(applicationId, router) {
installationId: options.installationId,
context: options.context || {},
},
query,
query: query ? JSON.parse(JSON.stringify(query)) : query,
};
return Promise.resolve()
.then(() => {
Expand All @@ -131,10 +131,11 @@ function ParseServerRESTController(applicationId, router) {
.then(
resp => {
const { response, status, headers = {} } = resp;
const strippedResponse = JSON.parse(JSON.stringify(response));
if (options.returnStatus) {
resolve({ ...response, _status: status, _headers: headers });
resolve({ ...strippedResponse, _status: status, _headers: headers });
} else {
resolve(response);
resolve(strippedResponse);
}
},
err => {
Expand Down
Loading