Skip to content

Commit

Permalink
feat: Allow setting createdAt and updatedAt during Parse.Object
Browse files Browse the repository at this point in the history
… creation with maintenance key (#8696)
  • Loading branch information
westhom authored Sep 29, 2023
1 parent 9b9c3a4 commit 77bbfb3
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 13 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,14 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo

## Access Scopes

| Scope | Internal data | Custom data | Restricted by CLP, ACL | Key |
|----------------|---------------|-------------|------------------------|---------------------|
| Internal | r/w | r/w | no | `maintenanceKey` |
| Master | -/- | r/w | no | `masterKey` |
| ReadOnlyMaster | -/- | r/- | no | `readOnlyMasterKey` |
| Session | -/- | r/w | yes | `sessionToken` |
| Scope | Internal data | Read-only data <sub>(1)</sub> | Custom data | Restricted by CLP, ACL | Key |
|----------------|---------------|-------------------------------|-------------|------------------------|---------------------|
| Internal | r/w | r/w | r/w | no | `maintenanceKey` |
| Master | -/- | r/- | r/w | no | `masterKey` |
| ReadOnlyMaster | -/- | r/- | r/- | no | `readOnlyMasterKey` |
| Session | -/- | r/- | r/w | yes | `sessionToken` |

<sub>(1) `Parse.Object.createdAt`, `Parse.Object.updatedAt`.</sub>

## Email Verification and Password Reset

Expand Down
5 changes: 3 additions & 2 deletions spec/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const defaultConfiguration = {
restAPIKey: 'rest',
webhookKey: 'hook',
masterKey: 'test',
maintenanceKey: 'testing',
readOnlyMasterKey: 'read-only-test',
fileKey: 'test',
directAccess: true,
Expand Down Expand Up @@ -250,8 +251,8 @@ afterEach(function (done) {
})
.then(() => Parse.User.logOut())
.then(
() => {},
() => {}
() => { },
() => { }
) // swallow errors
.then(() => {
// Connection close events are not immediate on node 10+... wait a bit
Expand Down
113 changes: 113 additions & 0 deletions spec/rest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,119 @@ describe('rest create', () => {
});
});

describe('with maintenance key', () => {
let req;

async function getObject(id) {
const res = await request({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest'
},
method: 'GET',
url: `http://localhost:8378/1/classes/TestObject/${id}`
});

return res.data;
}

beforeEach(() => {
req = {
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Maintenance-Key': 'testing'
},
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject'
};
});

it('allows createdAt', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
req.body = { createdAt };

const res = await request(req);
expect(res.data.createdAt).toEqual(createdAt.iso);
});

it('allows createdAt and updatedAt', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
req.body = { createdAt, updatedAt };

const res = await request(req);

const obj = await getObject(res.data.objectId);
expect(obj.createdAt).toEqual(createdAt.iso);
expect(obj.updatedAt).toEqual(updatedAt.iso);
});

it('allows createdAt, updatedAt, and additional field', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
req.body = { createdAt, updatedAt, testing: 123 };

const res = await request(req);

const obj = await getObject(res.data.objectId);
expect(obj.createdAt).toEqual(createdAt.iso);
expect(obj.updatedAt).toEqual(updatedAt.iso);
expect(obj.testing).toEqual(123);
});

it('cannot set updatedAt dated before createdAt', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' };
req.body = { createdAt, updatedAt };

try {
await request(req);
fail();
}
catch (err) {
expect(err.data.code).toEqual(Parse.Error.VALIDATION_ERROR);
}
});

it('cannot set updatedAt without createdAt', async () => {
const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' };
req.body = { updatedAt };

const res = await request(req);

const obj = await getObject(res.data.objectId);
expect(obj.updatedAt).not.toEqual(updatedAt.iso);
});

it('handles bad types for createdAt and updatedAt', async () => {
const createdAt = 12345;
const updatedAt = true;
req.body = { createdAt, updatedAt };

try {
await request(req);
fail();
}
catch (err) {
expect(err.data.code).toEqual(Parse.Error.INCORRECT_TYPE);
}
});

it('cannot set createdAt or updatedAt without maintenance key', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
req.body = { createdAt, updatedAt };
delete req.headers['X-Parse-Maintenance-Key'];

const res = await request(req);

expect(res.data.createdAt).not.toEqual(createdAt.iso);
expect(res.data.updatedAt).not.toEqual(updatedAt.iso);
});
});

it('handles array, object, date', done => {
const now = new Date();
const obj = {
Expand Down
2 changes: 1 addition & 1 deletion src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ module.exports.ParseServerOptions = {
maintenanceKey: {
env: 'PARSE_SERVER_MAINTENANCE_KEY',
help:
'(Optional) The maintenance key is used for modifying internal fields of Parse Server.<br><br>\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.',
'(Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.<br><br>\u26A0\uFE0F This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server.',
required: true,
},
maintenanceKeyIps: {
Expand Down
2 changes: 1 addition & 1 deletion src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface ParseServerOptions {
appId: string;
/* Your Parse Master Key */
masterKey: string;
/* (Optional) The maintenance key is used for modifying internal fields of Parse Server.<br><br>⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. */
/* (Optional) The maintenance key is used for modifying internal and read-only fields of Parse Server.<br><br>⚠️ This key is not intended to be used as part of a regular operation of Parse Server. This key is intended to conduct out-of-band changes such as one-time migrations or data correction tasks. Internal fields are not officially documented and may change at any time without publication in release changelogs. We strongly advice not to rely on internal fields as part of your regular operation and to investigate the implications of any planned changes *directly in the source code* of your current version of Parse Server. */
maintenanceKey: string;
/* URL to your parse server with http:// or https://.
:ENV: PARSE_SERVER_URL */
Expand Down
33 changes: 31 additions & 2 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,36 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () {
};

// Add default fields
this.data.updatedAt = this.updatedAt;
if (!this.query) {
this.data.createdAt = this.updatedAt;
// allow customizing createdAt and updatedAt when using maintenance key
if (
this.auth.isMaintenance &&
this.data.createdAt &&
this.data.createdAt.__type === 'Date'
) {
this.data.createdAt = this.data.createdAt.iso;

if (this.data.updatedAt && this.data.updatedAt.__type === 'Date') {
const createdAt = new Date(this.data.createdAt);
const updatedAt = new Date(this.data.updatedAt.iso);

if (updatedAt < createdAt) {
throw new Parse.Error(
Parse.Error.VALIDATION_ERROR,
'updatedAt cannot occur before createdAt'
);
}

this.data.updatedAt = this.data.updatedAt.iso;
}
// if no updatedAt is provided, set it to createdAt to match default behavior
else {
this.data.updatedAt = this.data.createdAt;
}
} else {
this.data.updatedAt = this.updatedAt;
this.data.createdAt = this.updatedAt;
}

// Only assign new objectId if we are creating new object
if (!this.data.objectId) {
Expand All @@ -382,6 +409,8 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () {
});
}
} else if (schema) {
this.data.updatedAt = this.updatedAt;

Object.keys(this.data).forEach(fieldName => {
setRequiredFieldIfNeeded(fieldName, false);
});
Expand Down

0 comments on commit 77bbfb3

Please sign in to comment.