feat: Allow setting createdAt and updatedAt during Parse.Object creation with maintenance key (#8696)
This commit is contained in:
14
README.md
14
README.md
@@ -358,12 +358,14 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
|
|||||||
|
|
||||||
## Access Scopes
|
## Access Scopes
|
||||||
|
|
||||||
| Scope | Internal data | Custom data | Restricted by CLP, ACL | Key |
|
| Scope | Internal data | Read-only data <sub>(1)</sub> | Custom data | Restricted by CLP, ACL | Key |
|
||||||
|----------------|---------------|-------------|------------------------|---------------------|
|
|----------------|---------------|-------------------------------|-------------|------------------------|---------------------|
|
||||||
| Internal | r/w | r/w | no | `maintenanceKey` |
|
| Internal | r/w | r/w | r/w | no | `maintenanceKey` |
|
||||||
| Master | -/- | r/w | no | `masterKey` |
|
| Master | -/- | r/- | r/w | no | `masterKey` |
|
||||||
| ReadOnlyMaster | -/- | r/- | no | `readOnlyMasterKey` |
|
| ReadOnlyMaster | -/- | r/- | r/- | no | `readOnlyMasterKey` |
|
||||||
| Session | -/- | r/w | yes | `sessionToken` |
|
| Session | -/- | r/- | r/w | yes | `sessionToken` |
|
||||||
|
|
||||||
|
<sub>(1) `Parse.Object.createdAt`, `Parse.Object.updatedAt`.</sub>
|
||||||
|
|
||||||
## Email Verification and Password Reset
|
## Email Verification and Password Reset
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ const defaultConfiguration = {
|
|||||||
restAPIKey: 'rest',
|
restAPIKey: 'rest',
|
||||||
webhookKey: 'hook',
|
webhookKey: 'hook',
|
||||||
masterKey: 'test',
|
masterKey: 'test',
|
||||||
|
maintenanceKey: 'testing',
|
||||||
readOnlyMasterKey: 'read-only-test',
|
readOnlyMasterKey: 'read-only-test',
|
||||||
fileKey: 'test',
|
fileKey: 'test',
|
||||||
directAccess: true,
|
directAccess: true,
|
||||||
@@ -250,8 +251,8 @@ afterEach(function (done) {
|
|||||||
})
|
})
|
||||||
.then(() => Parse.User.logOut())
|
.then(() => Parse.User.logOut())
|
||||||
.then(
|
.then(
|
||||||
() => {},
|
() => { },
|
||||||
() => {}
|
() => { }
|
||||||
) // swallow errors
|
) // swallow errors
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Connection close events are not immediate on node 10+... wait a bit
|
// Connection close events are not immediate on node 10+... wait a bit
|
||||||
|
|||||||
@@ -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 => {
|
it('handles array, object, date', done => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const obj = {
|
const obj = {
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ module.exports.ParseServerOptions = {
|
|||||||
maintenanceKey: {
|
maintenanceKey: {
|
||||||
env: 'PARSE_SERVER_MAINTENANCE_KEY',
|
env: 'PARSE_SERVER_MAINTENANCE_KEY',
|
||||||
help:
|
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,
|
required: true,
|
||||||
},
|
},
|
||||||
maintenanceKeyIps: {
|
maintenanceKeyIps: {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
* @property {String} logLevel Sets the level for logs
|
* @property {String} logLevel Sets the level for logs
|
||||||
* @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events.
|
* @property {LogLevels} logLevels (Optional) Overrides the log levels used internally by Parse Server to log events.
|
||||||
* @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging
|
* @property {String} logsFolder Folder for the logs (defaults to './logs'); set to null to disable file based logging
|
||||||
* @property {String} maintenanceKey (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.
|
* @property {String} maintenanceKey (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.
|
||||||
* @property {String[]} maintenanceKeyIps (Optional) Restricts the use of maintenance key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key.
|
* @property {String[]} maintenanceKeyIps (Optional) Restricts the use of maintenance key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `maintenanceKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the maintenance key can be used from any IP address.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the maintenance key.
|
||||||
* @property {String} masterKey Your Parse Master Key
|
* @property {String} masterKey Your Parse Master Key
|
||||||
* @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.
|
* @property {String[]} masterKeyIps (Optional) Restricts the use of master key permissions to a list of IP addresses.<br><br>This option accepts a list of single IP addresses, for example:<br>`['10.0.0.1', '10.0.0.2']`<br><br>You can also use CIDR notation to specify an IP address range, for example:<br>`['10.0.1.0/24']`<br><br>Special cases:<br>- Setting an empty array `[]` means that `masterKey` cannot be used even in Parse Server Cloud Code.<br>- Setting `['0.0.0.0/0']` means disabling the filter and the master key can be used from any IP address.<br><br>To connect Parse Dashboard from a different server requires to add the IP address of the server that hosts Parse Dashboard because Parse Dashboard uses the master key.<br><br>Defaults to `['127.0.0.1', '::1']` which means that only `localhost`, the server itself, is allowed to use the master key.
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export interface ParseServerOptions {
|
|||||||
appId: string;
|
appId: string;
|
||||||
/* Your Parse Master Key */
|
/* Your Parse Master Key */
|
||||||
masterKey: string;
|
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;
|
maintenanceKey: string;
|
||||||
/* URL to your parse server with http:// or https://.
|
/* URL to your parse server with http:// or https://.
|
||||||
:ENV: PARSE_SERVER_URL */
|
:ENV: PARSE_SERVER_URL */
|
||||||
|
|||||||
@@ -368,9 +368,36 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add default fields
|
// Add default fields
|
||||||
this.data.updatedAt = this.updatedAt;
|
|
||||||
if (!this.query) {
|
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
|
// Only assign new objectId if we are creating new object
|
||||||
if (!this.data.objectId) {
|
if (!this.data.objectId) {
|
||||||
@@ -382,6 +409,8 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (schema) {
|
} else if (schema) {
|
||||||
|
this.data.updatedAt = this.updatedAt;
|
||||||
|
|
||||||
Object.keys(this.data).forEach(fieldName => {
|
Object.keys(this.data).forEach(fieldName => {
|
||||||
setRequiredFieldIfNeeded(fieldName, false);
|
setRequiredFieldIfNeeded(fieldName, false);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user