Skip to content

Commit 6bf3a11

Browse files
yog27rayclaude
andcommitted
fix: Use JSON.parse(JSON.stringify()) for HTTP parity, strip query params, add tests
- Replace custom stripUndefined with JSON.parse(JSON.stringify()) for correct JSON.stringify semantics (handles toJSON, nested objects, special types) - Strip undefined from GET query params for full HTTP parity - Add nested undefined assertions to existing update tests - Add POST (create) tests with undefined values for both directAccess and HTTP mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 002d52c commit 6bf3a11

2 files changed

Lines changed: 52 additions & 20 deletions

File tree

spec/ParseServerRESTController.spec.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -745,13 +745,31 @@ describe('ParseServerRESTController', () => {
745745
await RESTController.request('PUT', `/classes/MyObject/${createRes.objectId}`, {
746746
presentField: 'updated',
747747
absentField: undefined,
748+
nested: { absentField: undefined, presentField: 'value' },
748749
});
749750

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

752753
expect(getRes.presentField).toBe('updated');
753754
expect(getRes.absentField).toBeUndefined();
754755
expect('absentField' in getRes).toBe(false);
756+
expect(getRes.nested).toBeDefined();
757+
expect(getRes.nested.presentField).toBe('value');
758+
expect('absentField' in getRes.nested).toBe(false);
759+
});
760+
761+
it('should not convert undefined values to null on create with directAccess', async () => {
762+
const createRes = await RESTController.request('POST', '/classes/MyObject', {
763+
presentField: 'hello',
764+
absentField: undefined,
765+
});
766+
expect(createRes.objectId).toBeDefined();
767+
768+
const getRes = await RESTController.request('GET', `/classes/MyObject/${createRes.objectId}`);
769+
770+
expect(getRes.presentField).toBe('hello');
771+
expect(getRes.absentField).toBeUndefined();
772+
expect('absentField' in getRes).toBe(false);
755773
});
756774

757775
it('should not convert undefined values to null on update without directAccess (HTTP mode)', async () => {
@@ -774,7 +792,7 @@ describe('ParseServerRESTController', () => {
774792
method: 'PUT',
775793
headers,
776794
url: `${serverURL}/classes/MyObject/${createRes.data.objectId}`,
777-
body: { presentField: 'updated', absentField: undefined },
795+
body: { presentField: 'updated', absentField: undefined, nested: { absentField: undefined, presentField: 'value' } },
778796
});
779797

780798
const getRes = await request({
@@ -786,5 +804,35 @@ describe('ParseServerRESTController', () => {
786804
expect(getRes.data.presentField).toBe('updated');
787805
expect(getRes.data.absentField).toBeUndefined();
788806
expect('absentField' in getRes.data).toBe(false);
807+
expect(getRes.data.nested).toBeDefined();
808+
expect(getRes.data.nested.presentField).toBe('value');
809+
expect('absentField' in getRes.data.nested).toBe(false);
810+
});
811+
812+
it('should not convert undefined values to null on create without directAccess (HTTP mode)', async () => {
813+
const serverURL = 'http://localhost:8378/1';
814+
const headers = {
815+
'Content-Type': 'application/json',
816+
'X-Parse-Application-Id': Parse.applicationId,
817+
'X-Parse-Master-Key': Parse.masterKey,
818+
};
819+
820+
const createRes = await request({
821+
method: 'POST',
822+
headers,
823+
url: `${serverURL}/classes/MyObject`,
824+
body: { presentField: 'hello', absentField: undefined },
825+
});
826+
expect(createRes.data.objectId).toBeDefined();
827+
828+
const getRes = await request({
829+
method: 'GET',
830+
headers,
831+
url: `${serverURL}/classes/MyObject/${createRes.data.objectId}`,
832+
});
833+
834+
expect(getRes.data.presentField).toBe('hello');
835+
expect(getRes.data.absentField).toBeUndefined();
836+
expect('absentField' in getRes.data).toBe(false);
789837
});
790838
});

src/ParseServerRESTController.js

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,6 @@ function getAuth(options = {}, config) {
2929
});
3030
}
3131

32-
function stripUndefined(obj) {
33-
if (obj === null || obj === undefined || typeof obj !== 'object') {
34-
return obj;
35-
}
36-
if (Array.isArray(obj)) {
37-
return obj.map(item => item === undefined ? null : stripUndefined(item));
38-
}
39-
const result = {};
40-
for (const key of Object.keys(obj)) {
41-
if (obj[key] !== undefined) {
42-
result[key] = stripUndefined(obj[key]);
43-
}
44-
}
45-
return result;
46-
}
47-
4832
function ParseServerRESTController(applicationId, router) {
4933
function handleRequest(method, path, data = {}, options = {}, config) {
5034
// Store the arguments, for later use if internal fails
@@ -129,7 +113,7 @@ function ParseServerRESTController(applicationId, router) {
129113
return new Promise((resolve, reject) => {
130114
getAuth(options, config).then(auth => {
131115
const request = {
132-
body: stripUndefined(data),
116+
body: JSON.parse(JSON.stringify(data)),
133117
config,
134118
auth,
135119
info: {
@@ -138,7 +122,7 @@ function ParseServerRESTController(applicationId, router) {
138122
installationId: options.installationId,
139123
context: options.context || {},
140124
},
141-
query,
125+
query: query ? JSON.parse(JSON.stringify(query)) : query,
142126
};
143127
return Promise.resolve()
144128
.then(() => {
@@ -147,7 +131,7 @@ function ParseServerRESTController(applicationId, router) {
147131
.then(
148132
resp => {
149133
const { response, status, headers = {} } = resp;
150-
const strippedResponse = stripUndefined(response);
134+
const strippedResponse = JSON.parse(JSON.stringify(response));
151135
if (options.returnStatus) {
152136
resolve({ ...strippedResponse, _status: status, _headers: headers });
153137
} else {

0 commit comments

Comments
 (0)