2017-12-30 20:44:18 -05:00
|
|
|
|
// @flow
|
|
|
|
|
|
// A database adapter that works with data exported from the hosted
|
2016-01-28 10:58:12 -08:00
|
|
|
|
// Parse database.
|
|
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// @flow-disable-next
|
2018-09-01 13:58:06 -04:00
|
|
|
|
import { Parse } from 'parse/node';
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// @flow-disable-next
|
2018-09-01 13:58:06 -04:00
|
|
|
|
import _ from 'lodash';
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// @flow-disable-next
|
2018-09-01 13:58:06 -04:00
|
|
|
|
import intersect from 'intersect';
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// @flow-disable-next
|
2018-09-01 13:58:06 -04:00
|
|
|
|
import deepcopy from 'deepcopy';
|
|
|
|
|
|
import logger from '../logger';
|
2022-03-24 02:54:07 +01:00
|
|
|
|
import Utils from '../Utils';
|
2018-09-01 13:58:06 -04:00
|
|
|
|
import * as SchemaController from './SchemaController';
|
|
|
|
|
|
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
2021-03-16 16:05:36 -05:00
|
|
|
|
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
|
2022-01-02 13:25:53 -05:00
|
|
|
|
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
|
2021-03-16 16:05:36 -05:00
|
|
|
|
import SchemaCache from '../Adapters/Cache/SchemaCache';
|
|
|
|
|
|
import type { LoadSchemaOptions } from './types';
|
2022-03-12 14:47:23 +01:00
|
|
|
|
import type { ParseServerOptions } from '../Options';
|
2020-10-25 15:06:58 -05:00
|
|
|
|
import type { QueryOptions, FullQueryOptions } from '../Adapters/Storage/StorageAdapter';
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2016-04-22 18:44:03 -07:00
|
|
|
|
function addWriteACL(query, acl) {
|
2016-12-07 15:17:05 -08:00
|
|
|
|
const newQuery = _.cloneDeep(query);
|
2016-04-22 18:44:03 -07:00
|
|
|
|
//Can't be any existing '_wperm' query, we don't allow client queries on that, no need to $and
|
2018-09-01 13:58:06 -04:00
|
|
|
|
newQuery._wperm = { $in: [null, ...acl] };
|
2016-04-22 18:44:03 -07:00
|
|
|
|
return newQuery;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addReadACL(query, acl) {
|
2016-12-07 15:17:05 -08:00
|
|
|
|
const newQuery = _.cloneDeep(query);
|
2016-04-22 18:44:03 -07:00
|
|
|
|
//Can't be any existing '_rperm' query, we don't allow client queries on that, no need to $and
|
2018-09-01 13:58:06 -04:00
|
|
|
|
newQuery._rperm = { $in: [null, '*', ...acl] };
|
2016-04-22 18:44:03 -07:00
|
|
|
|
return newQuery;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-06-12 13:39:41 -07:00
|
|
|
|
// Transforms a REST API formatted ACL object to our two-field mongo format.
|
|
|
|
|
|
const transformObjectACL = ({ ACL, ...result }) => {
|
|
|
|
|
|
if (!ACL) {
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result._wperm = [];
|
|
|
|
|
|
result._rperm = [];
|
|
|
|
|
|
|
2016-12-07 15:17:05 -08:00
|
|
|
|
for (const entry in ACL) {
|
2016-06-12 13:39:41 -07:00
|
|
|
|
if (ACL[entry].read) {
|
|
|
|
|
|
result._rperm.push(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ACL[entry].write) {
|
|
|
|
|
|
result._wperm.push(entry);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
2018-09-01 13:58:06 -04:00
|
|
|
|
};
|
2016-06-12 13:39:41 -07:00
|
|
|
|
|
2018-09-01 13:58:06 -04:00
|
|
|
|
const specialQuerykeys = [
|
|
|
|
|
|
'$and',
|
|
|
|
|
|
'$or',
|
|
|
|
|
|
'$nor',
|
|
|
|
|
|
'_rperm',
|
|
|
|
|
|
'_wperm',
|
|
|
|
|
|
'_perishable_token',
|
|
|
|
|
|
'_email_verify_token',
|
|
|
|
|
|
'_email_verify_token_expires_at',
|
|
|
|
|
|
'_account_lockout_expires_at',
|
|
|
|
|
|
'_failed_login_count',
|
|
|
|
|
|
];
|
2016-09-24 13:53:15 -04:00
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
const isSpecialQueryKey = key => {
|
2016-09-24 13:53:15 -04:00
|
|
|
|
return specialQuerykeys.indexOf(key) >= 0;
|
2018-09-01 13:58:06 -04:00
|
|
|
|
};
|
2016-09-24 13:53:15 -04:00
|
|
|
|
|
2020-02-27 10:56:14 -08:00
|
|
|
|
const validateQuery = (query: any): void => {
|
2016-05-18 18:10:57 -07:00
|
|
|
|
if (query.ACL) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (query.$or) {
|
|
|
|
|
|
if (query.$or instanceof Array) {
|
2020-02-27 10:56:14 -08:00
|
|
|
|
query.$or.forEach(validateQuery);
|
2016-05-18 18:10:57 -07:00
|
|
|
|
} else {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.');
|
2016-05-18 18:10:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (query.$and) {
|
|
|
|
|
|
if (query.$and instanceof Array) {
|
2020-02-27 10:56:14 -08:00
|
|
|
|
query.$and.forEach(validateQuery);
|
2016-05-18 18:10:57 -07:00
|
|
|
|
} else {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.');
|
2016-05-18 18:10:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-05-18 15:26:33 -04:00
|
|
|
|
if (query.$nor) {
|
|
|
|
|
|
if (query.$nor instanceof Array && query.$nor.length > 0) {
|
2020-02-27 10:56:14 -08:00
|
|
|
|
query.$nor.forEach(validateQuery);
|
2018-05-18 15:26:33 -04:00
|
|
|
|
} else {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.INVALID_QUERY,
|
|
|
|
|
|
'Bad $nor format - use an array of at least 1 value.'
|
|
|
|
|
|
);
|
2018-05-18 15:26:33 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
Object.keys(query).forEach(key => {
|
2016-06-13 12:57:20 -07:00
|
|
|
|
if (query && query[key] && query[key].$regex) {
|
|
|
|
|
|
if (typeof query[key].$options === 'string') {
|
2016-06-13 01:14:26 -07:00
|
|
|
|
if (!query[key].$options.match(/^[imxs]+$/)) {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.INVALID_QUERY,
|
|
|
|
|
|
`Bad $options value for query: ${query[key].$options}`
|
|
|
|
|
|
);
|
2016-06-13 01:14:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2016-09-24 13:53:15 -04:00
|
|
|
|
if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`);
|
2016-05-18 18:10:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2018-09-01 13:58:06 -04:00
|
|
|
|
};
|
2016-05-18 18:10:57 -07:00
|
|
|
|
|
2016-01-28 10:58:12 -08:00
|
|
|
|
// Filters out any data that shouldn't be on this REST-formatted object.
|
2019-01-29 08:52:49 +00:00
|
|
|
|
const filterSensitiveData = (
|
2019-08-22 21:01:50 +02:00
|
|
|
|
isMaster: boolean,
|
|
|
|
|
|
aclGroup: any[],
|
|
|
|
|
|
auth: any,
|
|
|
|
|
|
operation: any,
|
2022-06-30 13:01:40 +02:00
|
|
|
|
schema: SchemaController.SchemaController | any,
|
2019-08-22 21:01:50 +02:00
|
|
|
|
className: string,
|
|
|
|
|
|
protectedFields: null | Array<any>,
|
|
|
|
|
|
object: any
|
2019-01-29 08:52:49 +00:00
|
|
|
|
) => {
|
2019-08-22 21:01:50 +02:00
|
|
|
|
let userId = null;
|
|
|
|
|
|
if (auth && auth.user) userId = auth.user.id;
|
|
|
|
|
|
|
|
|
|
|
|
// replace protectedFields when using pointer-permissions
|
2022-06-30 13:01:40 +02:00
|
|
|
|
const perms =
|
|
|
|
|
|
schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {};
|
2019-08-22 21:01:50 +02:00
|
|
|
|
if (perms) {
|
|
|
|
|
|
const isReadOperation = ['get', 'find'].indexOf(operation) > -1;
|
|
|
|
|
|
|
|
|
|
|
|
if (isReadOperation && perms.protectedFields) {
|
|
|
|
|
|
// extract protectedFields added with the pointer-permission prefix
|
|
|
|
|
|
const protectedFieldsPointerPerm = Object.keys(perms.protectedFields)
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.filter(key => key.startsWith('userField:'))
|
|
|
|
|
|
.map(key => {
|
2019-08-22 21:01:50 +02:00
|
|
|
|
return { key: key.substring(10), value: perms.protectedFields[key] };
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2020-02-19 12:34:08 +03:00
|
|
|
|
const newProtectedFields: Array<string>[] = [];
|
2019-08-22 21:01:50 +02:00
|
|
|
|
let overrideProtectedFields = false;
|
|
|
|
|
|
|
|
|
|
|
|
// check if the object grants the current user access based on the extracted fields
|
2020-07-13 17:13:08 -05:00
|
|
|
|
protectedFieldsPointerPerm.forEach(pointerPerm => {
|
2019-08-22 21:01:50 +02:00
|
|
|
|
let pointerPermIncludesUser = false;
|
|
|
|
|
|
const readUserFieldValue = object[pointerPerm.key];
|
|
|
|
|
|
if (readUserFieldValue) {
|
|
|
|
|
|
if (Array.isArray(readUserFieldValue)) {
|
|
|
|
|
|
pointerPermIncludesUser = readUserFieldValue.some(
|
2020-07-13 17:13:08 -05:00
|
|
|
|
user => user.objectId && user.objectId === userId
|
2019-08-22 21:01:50 +02:00
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pointerPermIncludesUser =
|
2020-10-25 15:06:58 -05:00
|
|
|
|
readUserFieldValue.objectId && readUserFieldValue.objectId === userId;
|
2019-08-22 21:01:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (pointerPermIncludesUser) {
|
|
|
|
|
|
overrideProtectedFields = true;
|
2020-02-19 12:34:08 +03:00
|
|
|
|
newProtectedFields.push(pointerPerm.value);
|
2019-08-22 21:01:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
});
|
2019-01-29 08:52:49 +00:00
|
|
|
|
|
2020-02-19 12:34:08 +03:00
|
|
|
|
// if at least one pointer-permission affected the current user
|
|
|
|
|
|
// intersect vs protectedFields from previous stage (@see addProtectedFields)
|
|
|
|
|
|
// Sets theory (intersections): A x (B x C) == (A x B) x C
|
|
|
|
|
|
if (overrideProtectedFields && protectedFields) {
|
|
|
|
|
|
newProtectedFields.push(protectedFields);
|
|
|
|
|
|
}
|
|
|
|
|
|
// intersect all sets of protectedFields
|
2020-07-13 17:13:08 -05:00
|
|
|
|
newProtectedFields.forEach(fields => {
|
2020-02-19 12:34:08 +03:00
|
|
|
|
if (fields) {
|
|
|
|
|
|
// if there're no protctedFields by other criteria ( id / role / auth)
|
|
|
|
|
|
// then we must intersect each set (per userField)
|
|
|
|
|
|
if (!protectedFields) {
|
|
|
|
|
|
protectedFields = fields;
|
|
|
|
|
|
} else {
|
2020-07-13 17:13:08 -05:00
|
|
|
|
protectedFields = protectedFields.filter(v => fields.includes(v));
|
2020-02-19 12:34:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2019-08-22 21:01:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const isUserClass = className === '_User';
|
|
|
|
|
|
|
|
|
|
|
|
/* special treat for the user class: don't filter protectedFields if currently loggedin user is
|
|
|
|
|
|
the retrieved user */
|
2020-02-19 12:34:08 +03:00
|
|
|
|
if (!(isUserClass && userId && object.objectId === userId)) {
|
2020-07-13 17:13:08 -05:00
|
|
|
|
protectedFields && protectedFields.forEach(k => delete object[k]);
|
2019-08-22 21:01:50 +02:00
|
|
|
|
|
2020-02-19 12:34:08 +03:00
|
|
|
|
// fields not requested by client (excluded),
|
|
|
|
|
|
//but were needed to apply protecttedFields
|
|
|
|
|
|
perms.protectedFields &&
|
|
|
|
|
|
perms.protectedFields.temporaryKeys &&
|
2020-07-13 17:13:08 -05:00
|
|
|
|
perms.protectedFields.temporaryKeys.forEach(k => delete object[k]);
|
2020-02-19 12:34:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2019-08-22 21:01:50 +02:00
|
|
|
|
if (!isUserClass) {
|
2016-01-28 10:58:12 -08:00
|
|
|
|
return object;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-05-27 19:41:09 -07:00
|
|
|
|
object.password = object._hashed_password;
|
|
|
|
|
|
delete object._hashed_password;
|
|
|
|
|
|
|
2016-04-14 14:50:16 -07:00
|
|
|
|
delete object.sessionToken;
|
|
|
|
|
|
|
2016-08-15 16:48:39 -04:00
|
|
|
|
if (isMaster) {
|
2016-01-28 10:58:12 -08:00
|
|
|
|
return object;
|
|
|
|
|
|
}
|
2016-08-15 16:48:39 -04:00
|
|
|
|
delete object._email_verify_token;
|
|
|
|
|
|
delete object._perishable_token;
|
2016-11-17 22:07:51 +05:30
|
|
|
|
delete object._perishable_token_expires_at;
|
2016-08-15 16:48:39 -04:00
|
|
|
|
delete object._tombstone;
|
|
|
|
|
|
delete object._email_verify_token_expires_at;
|
2016-09-02 17:00:47 -07:00
|
|
|
|
delete object._failed_login_count;
|
|
|
|
|
|
delete object._account_lockout_expires_at;
|
2016-11-21 21:16:38 +05:30
|
|
|
|
delete object._password_changed_at;
|
2018-04-11 01:54:35 +08:00
|
|
|
|
delete object._password_history;
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2018-09-01 13:58:06 -04:00
|
|
|
|
if (aclGroup.indexOf(object.objectId) > -1) {
|
2016-08-15 16:48:39 -04:00
|
|
|
|
return object;
|
|
|
|
|
|
}
|
2016-04-20 15:42:18 -07:00
|
|
|
|
delete object.authData;
|
2016-01-28 10:58:12 -08:00
|
|
|
|
return object;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Runs an update on the database.
|
|
|
|
|
|
// Returns a promise for an object with the new values for field
|
|
|
|
|
|
// modifications that don't know their results ahead of time, like
|
|
|
|
|
|
// 'increment'.
|
|
|
|
|
|
// Options:
|
|
|
|
|
|
// acl: a list of strings. If the object to be updated has an ACL,
|
|
|
|
|
|
// one of the provided strings must provide the caller with
|
|
|
|
|
|
// write permissions.
|
2018-09-01 13:58:06 -04:00
|
|
|
|
const specialKeysForUpdate = [
|
|
|
|
|
|
'_hashed_password',
|
|
|
|
|
|
'_perishable_token',
|
|
|
|
|
|
'_email_verify_token',
|
|
|
|
|
|
'_email_verify_token_expires_at',
|
|
|
|
|
|
'_account_lockout_expires_at',
|
|
|
|
|
|
'_failed_login_count',
|
|
|
|
|
|
'_perishable_token_expires_at',
|
|
|
|
|
|
'_password_changed_at',
|
|
|
|
|
|
'_password_history',
|
|
|
|
|
|
];
|
2016-09-24 13:53:15 -04:00
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
const isSpecialUpdateKey = key => {
|
2016-09-24 13:53:15 -04:00
|
|
|
|
return specialKeysForUpdate.indexOf(key) >= 0;
|
2018-09-01 13:58:06 -04:00
|
|
|
|
};
|
2016-09-24 13:53:15 -04:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
function joinTableName(className, key) {
|
|
|
|
|
|
return `_Join:${key}:${className}`;
|
2017-05-22 12:34:00 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
const flattenUpdateOperatorsForCreate = object => {
|
2016-12-07 15:17:05 -08:00
|
|
|
|
for (const key in object) {
|
2016-06-17 09:59:16 -07:00
|
|
|
|
if (object[key] && object[key].__op) {
|
|
|
|
|
|
switch (object[key].__op) {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
case 'Increment':
|
|
|
|
|
|
if (typeof object[key].amount !== 'number') {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
|
2018-09-01 13:58:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
object[key] = object[key].amount;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Add':
|
|
|
|
|
|
if (!(object[key].objects instanceof Array)) {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
|
2018-09-01 13:58:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
object[key] = object[key].objects;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'AddUnique':
|
|
|
|
|
|
if (!(object[key].objects instanceof Array)) {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
|
2018-09-01 13:58:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
object[key] = object[key].objects;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Remove':
|
|
|
|
|
|
if (!(object[key].objects instanceof Array)) {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
|
2018-09-01 13:58:06 -04:00
|
|
|
|
}
|
|
|
|
|
|
object[key] = [];
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'Delete':
|
|
|
|
|
|
delete object[key];
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.COMMAND_UNAVAILABLE,
|
|
|
|
|
|
`The ${object[key].__op} operator is not supported yet.`
|
|
|
|
|
|
);
|
2016-06-17 09:59:16 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2018-09-01 13:58:06 -04:00
|
|
|
|
};
|
2016-06-17 09:59:16 -07:00
|
|
|
|
|
|
|
|
|
|
const transformAuthData = (className, object, schema) => {
|
|
|
|
|
|
if (object.authData && className === '_User') {
|
2020-07-13 17:13:08 -05:00
|
|
|
|
Object.keys(object.authData).forEach(provider => {
|
2016-06-17 09:59:16 -07:00
|
|
|
|
const providerData = object.authData[provider];
|
|
|
|
|
|
const fieldName = `_auth_data_${provider}`;
|
|
|
|
|
|
if (providerData == null) {
|
|
|
|
|
|
object[fieldName] = {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
__op: 'Delete',
|
|
|
|
|
|
};
|
2016-06-17 09:59:16 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
object[fieldName] = providerData;
|
2018-09-01 13:58:06 -04:00
|
|
|
|
schema.fields[fieldName] = { type: 'Object' };
|
2016-06-17 09:59:16 -07:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
delete object.authData;
|
|
|
|
|
|
}
|
2018-09-01 13:58:06 -04:00
|
|
|
|
};
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Transforms a Database format ACL to a REST API format ACL
|
2018-09-01 13:58:06 -04:00
|
|
|
|
const untransformObjectACL = ({ _rperm, _wperm, ...output }) => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (_rperm || _wperm) {
|
|
|
|
|
|
output.ACL = {};
|
2016-06-17 09:59:16 -07:00
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
(_rperm || []).forEach(entry => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (!output.ACL[entry]) {
|
|
|
|
|
|
output.ACL[entry] = { read: true };
|
|
|
|
|
|
} else {
|
|
|
|
|
|
output.ACL[entry]['read'] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
(_wperm || []).forEach(entry => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (!output.ACL[entry]) {
|
|
|
|
|
|
output.ACL[entry] = { write: true };
|
|
|
|
|
|
} else {
|
|
|
|
|
|
output.ACL[entry]['write'] = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2016-03-07 23:07:24 -05:00
|
|
|
|
}
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return output;
|
2018-09-01 13:58:06 -04:00
|
|
|
|
};
|
2016-03-07 23:07:24 -05:00
|
|
|
|
|
2018-06-07 15:47:18 -07:00
|
|
|
|
/**
|
2018-06-07 16:27:11 -07:00
|
|
|
|
* When querying, the fieldName may be compound, extract the root fieldName
|
2018-06-07 15:47:18 -07:00
|
|
|
|
* `temperature.celsius` becomes `temperature`
|
|
|
|
|
|
* @param {string} fieldName that may be a compound field name
|
2018-06-07 16:27:11 -07:00
|
|
|
|
* @returns {string} the root name of the field
|
2018-06-07 15:47:18 -07:00
|
|
|
|
*/
|
2018-06-07 16:27:11 -07:00
|
|
|
|
const getRootFieldName = (fieldName: string): string => {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return fieldName.split('.')[0];
|
|
|
|
|
|
};
|
2018-06-07 15:47:18 -07:00
|
|
|
|
|
2018-09-01 13:58:06 -04:00
|
|
|
|
const relationSchema = {
|
|
|
|
|
|
fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } },
|
|
|
|
|
|
};
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
class DatabaseController {
|
|
|
|
|
|
adapter: StorageAdapter;
|
|
|
|
|
|
schemaCache: any;
|
|
|
|
|
|
schemaPromise: ?Promise<SchemaController.SchemaController>;
|
2019-07-31 02:41:07 -07:00
|
|
|
|
_transactionalSession: ?any;
|
2022-03-12 14:47:23 +01:00
|
|
|
|
options: ParseServerOptions;
|
|
|
|
|
|
idempotencyOptions: any;
|
2017-12-30 20:44:18 -05:00
|
|
|
|
|
2022-03-12 14:47:23 +01:00
|
|
|
|
constructor(adapter: StorageAdapter, options: ParseServerOptions) {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
this.adapter = adapter;
|
2022-03-12 14:47:23 +01:00
|
|
|
|
this.options = options || {};
|
|
|
|
|
|
this.idempotencyOptions = this.options.idempotencyOptions || {};
|
|
|
|
|
|
// Prevent mutable this.schema, otherwise one request could use
|
|
|
|
|
|
// multiple schemas, so instead use loadSchema to get a schema.
|
2017-12-30 20:44:18 -05:00
|
|
|
|
this.schemaPromise = null;
|
2019-07-31 02:41:07 -07:00
|
|
|
|
this._transactionalSession = null;
|
2022-03-12 13:49:57 +01:00
|
|
|
|
this.options = options;
|
2017-11-14 14:46:51 -05:00
|
|
|
|
}
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
collectionExists(className: string): Promise<boolean> {
|
|
|
|
|
|
return this.adapter.classExists(className);
|
|
|
|
|
|
}
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
purgeCollection(className: string): Promise<void> {
|
|
|
|
|
|
return this.loadSchema()
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(schemaController => schemaController.getOneSchema(className))
|
|
|
|
|
|
.then(schema => this.adapter.deleteObjectsByQuery(className, schema, {}));
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2016-03-07 19:26:40 -08:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
validateClassName(className: string): Promise<void> {
|
|
|
|
|
|
if (!SchemaController.classNameIsValid(className)) {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return Promise.reject(
|
2020-10-25 15:06:58 -05:00
|
|
|
|
new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className)
|
2018-09-01 13:58:06 -04:00
|
|
|
|
);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
return Promise.resolve();
|
2016-03-02 14:33:51 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Returns a promise for a schemaController.
|
2018-09-01 13:58:06 -04:00
|
|
|
|
loadSchema(
|
|
|
|
|
|
options: LoadSchemaOptions = { clearCache: false }
|
|
|
|
|
|
): Promise<SchemaController.SchemaController> {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (this.schemaPromise != null) {
|
|
|
|
|
|
return this.schemaPromise;
|
2017-06-21 09:23:20 -03:00
|
|
|
|
}
|
2021-03-16 16:05:36 -05:00
|
|
|
|
this.schemaPromise = SchemaController.load(this.adapter, options);
|
2018-09-01 13:58:06 -04:00
|
|
|
|
this.schemaPromise.then(
|
|
|
|
|
|
() => delete this.schemaPromise,
|
|
|
|
|
|
() => delete this.schemaPromise
|
|
|
|
|
|
);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return this.loadSchema(options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-05-30 11:14:05 -05:00
|
|
|
|
loadSchemaIfNeeded(
|
|
|
|
|
|
schemaController: SchemaController.SchemaController,
|
|
|
|
|
|
options: LoadSchemaOptions = { clearCache: false }
|
|
|
|
|
|
): Promise<SchemaController.SchemaController> {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return schemaController ? Promise.resolve(schemaController) : this.loadSchema(options);
|
2019-05-30 11:14:05 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Returns a promise for the classname that is related to the given
|
|
|
|
|
|
// classname through the key.
|
|
|
|
|
|
// TODO: make this not in the DatabaseController interface
|
|
|
|
|
|
redirectClassNameForKey(className: string, key: string): Promise<?string> {
|
2020-07-13 17:13:08 -05:00
|
|
|
|
return this.loadSchema().then(schema => {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
var t = schema.getExpectedType(className, key);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (t != null && typeof t !== 'string' && t.type === 'Relation') {
|
|
|
|
|
|
return t.targetClass;
|
|
|
|
|
|
}
|
|
|
|
|
|
return className;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Uses the schema to validate the object (REST API format).
|
|
|
|
|
|
// Returns a promise that resolves to the new schema.
|
|
|
|
|
|
// This does not update this.schema, because in a situation like a
|
|
|
|
|
|
// batch request, that could confuse other users of the schema.
|
2018-09-01 13:58:06 -04:00
|
|
|
|
validateObject(
|
|
|
|
|
|
className: string,
|
|
|
|
|
|
object: any,
|
|
|
|
|
|
query: any,
|
2020-01-28 09:21:30 +03:00
|
|
|
|
runOptions: QueryOptions
|
2018-09-01 13:58:06 -04:00
|
|
|
|
): Promise<boolean> {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
let schema;
|
2020-01-28 09:21:30 +03:00
|
|
|
|
const acl = runOptions.acl;
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const isMaster = acl === undefined;
|
2018-09-01 13:58:06 -04:00
|
|
|
|
var aclGroup: string[] = acl || [];
|
|
|
|
|
|
return this.loadSchema()
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(s => {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
schema = s;
|
|
|
|
|
|
if (isMaster) {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return this.canAddField(schema, className, object, aclGroup, runOptions);
|
2018-09-01 13:58:06 -04:00
|
|
|
|
})
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
return schema.validateObject(className, object, query);
|
|
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-09-01 13:58:06 -04:00
|
|
|
|
update(
|
|
|
|
|
|
className: string,
|
|
|
|
|
|
query: any,
|
|
|
|
|
|
update: any,
|
2020-01-28 09:21:30 +03:00
|
|
|
|
{ acl, many, upsert, addsField }: FullQueryOptions = {},
|
2019-05-11 10:37:27 -07:00
|
|
|
|
skipSanitization: boolean = false,
|
2019-05-30 11:14:05 -05:00
|
|
|
|
validateOnly: boolean = false,
|
|
|
|
|
|
validSchemaController: SchemaController.SchemaController
|
2018-09-01 13:58:06 -04:00
|
|
|
|
): Promise<any> {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const originalQuery = query;
|
|
|
|
|
|
const originalUpdate = update;
|
|
|
|
|
|
// Make a copy of the object, so we don't mutate the incoming data.
|
|
|
|
|
|
update = deepcopy(update);
|
|
|
|
|
|
var relationUpdates = [];
|
|
|
|
|
|
var isMaster = acl === undefined;
|
|
|
|
|
|
var aclGroup = acl || [];
|
2019-05-30 11:14:05 -05:00
|
|
|
|
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return this.loadSchemaIfNeeded(validSchemaController).then(schemaController => {
|
|
|
|
|
|
return (isMaster
|
|
|
|
|
|
? Promise.resolve()
|
|
|
|
|
|
: schemaController.validatePermission(className, aclGroup, 'update')
|
|
|
|
|
|
)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
relationUpdates = this.collectRelationUpdates(className, originalQuery.objectId, update);
|
|
|
|
|
|
if (!isMaster) {
|
|
|
|
|
|
query = this.addPointerPermissions(
|
|
|
|
|
|
schemaController,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
className,
|
2020-10-25 15:06:58 -05:00
|
|
|
|
'update',
|
|
|
|
|
|
query,
|
|
|
|
|
|
aclGroup
|
2018-09-01 13:58:06 -04:00
|
|
|
|
);
|
2020-01-28 09:21:30 +03:00
|
|
|
|
|
2020-10-25 15:06:58 -05:00
|
|
|
|
if (addsField) {
|
|
|
|
|
|
query = {
|
|
|
|
|
|
$and: [
|
|
|
|
|
|
query,
|
|
|
|
|
|
this.addPointerPermissions(
|
|
|
|
|
|
schemaController,
|
2019-05-30 11:14:05 -05:00
|
|
|
|
className,
|
2020-10-25 15:06:58 -05:00
|
|
|
|
'addField',
|
2019-05-30 11:14:05 -05:00
|
|
|
|
query,
|
2020-10-25 15:06:58 -05:00
|
|
|
|
aclGroup
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!query) {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (acl) {
|
|
|
|
|
|
query = addWriteACL(query, acl);
|
|
|
|
|
|
}
|
|
|
|
|
|
validateQuery(query);
|
|
|
|
|
|
return schemaController
|
|
|
|
|
|
.getOneSchema(className, true)
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
// If the schema doesn't exist, pretend it exists with no fields. This behavior
|
|
|
|
|
|
// will likely need revisiting.
|
|
|
|
|
|
if (error === undefined) {
|
|
|
|
|
|
return { fields: {} };
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(schema => {
|
|
|
|
|
|
Object.keys(update).forEach(fieldName => {
|
|
|
|
|
|
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
|
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.INVALID_KEY_NAME,
|
|
|
|
|
|
`Invalid field name for update: ${fieldName}`
|
2019-05-30 11:14:05 -05:00
|
|
|
|
);
|
2020-10-25 15:06:58 -05:00
|
|
|
|
}
|
|
|
|
|
|
const rootFieldName = getRootFieldName(fieldName);
|
|
|
|
|
|
if (
|
2020-12-09 12:19:15 -06:00
|
|
|
|
!SchemaController.fieldNameIsValid(rootFieldName, className) &&
|
2020-10-25 15:06:58 -05:00
|
|
|
|
!isSpecialUpdateKey(rootFieldName)
|
|
|
|
|
|
) {
|
|
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.INVALID_KEY_NAME,
|
|
|
|
|
|
`Invalid field name for update: ${fieldName}`
|
2018-09-01 13:58:06 -04:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2019-05-30 11:14:05 -05:00
|
|
|
|
});
|
2020-10-25 15:06:58 -05:00
|
|
|
|
for (const updateOperation in update) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
update[updateOperation] &&
|
|
|
|
|
|
typeof update[updateOperation] === 'object' &&
|
|
|
|
|
|
Object.keys(update[updateOperation]).some(
|
|
|
|
|
|
innerKey => innerKey.includes('$') || innerKey.includes('.')
|
|
|
|
|
|
)
|
|
|
|
|
|
) {
|
|
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.INVALID_NESTED_KEY,
|
|
|
|
|
|
"Nested keys should not contain the '$' or '.' characters"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
update = transformObjectACL(update);
|
|
|
|
|
|
transformAuthData(className, update, schema);
|
|
|
|
|
|
if (validateOnly) {
|
|
|
|
|
|
return this.adapter.find(className, schema, query, {}).then(result => {
|
|
|
|
|
|
if (!result || !result.length) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
|
|
|
|
|
}
|
|
|
|
|
|
return {};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (many) {
|
|
|
|
|
|
return this.adapter.updateObjectsByQuery(
|
|
|
|
|
|
className,
|
|
|
|
|
|
schema,
|
|
|
|
|
|
query,
|
|
|
|
|
|
update,
|
|
|
|
|
|
this._transactionalSession
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (upsert) {
|
|
|
|
|
|
return this.adapter.upsertOneObject(
|
|
|
|
|
|
className,
|
|
|
|
|
|
schema,
|
|
|
|
|
|
query,
|
|
|
|
|
|
update,
|
|
|
|
|
|
this._transactionalSession
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return this.adapter.findOneAndUpdate(
|
|
|
|
|
|
className,
|
|
|
|
|
|
schema,
|
|
|
|
|
|
query,
|
|
|
|
|
|
update,
|
|
|
|
|
|
this._transactionalSession
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2017-12-30 20:44:18 -05:00
|
|
|
|
});
|
2020-10-25 15:06:58 -05:00
|
|
|
|
})
|
|
|
|
|
|
.then((result: any) => {
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (validateOnly) {
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
return this.handleRelationUpdates(
|
|
|
|
|
|
className,
|
|
|
|
|
|
originalQuery.objectId,
|
|
|
|
|
|
update,
|
|
|
|
|
|
relationUpdates
|
|
|
|
|
|
).then(() => {
|
|
|
|
|
|
return result;
|
2017-12-30 20:44:18 -05:00
|
|
|
|
});
|
2020-10-25 15:06:58 -05:00
|
|
|
|
})
|
|
|
|
|
|
.then(result => {
|
|
|
|
|
|
if (skipSanitization) {
|
|
|
|
|
|
return Promise.resolve(result);
|
|
|
|
|
|
}
|
2022-03-12 14:47:23 +01:00
|
|
|
|
return this._sanitizeDatabaseResult(originalUpdate, result);
|
2020-10-25 15:06:58 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Collect all relation-updating operations from a REST-format update.
|
|
|
|
|
|
// Returns a list of all relation updates to perform
|
|
|
|
|
|
// This mutates update.
|
|
|
|
|
|
collectRelationUpdates(className: string, objectId: ?string, update: any) {
|
|
|
|
|
|
var ops = [];
|
|
|
|
|
|
var deleteMe = [];
|
|
|
|
|
|
objectId = update.objectId || objectId;
|
|
|
|
|
|
|
|
|
|
|
|
var process = (op, key) => {
|
|
|
|
|
|
if (!op) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (op.__op == 'AddRelation') {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
ops.push({ key, op });
|
2017-12-30 20:44:18 -05:00
|
|
|
|
deleteMe.push(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (op.__op == 'RemoveRelation') {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
ops.push({ key, op });
|
2017-12-30 20:44:18 -05:00
|
|
|
|
deleteMe.push(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (op.__op == 'Batch') {
|
|
|
|
|
|
for (var x of op.ops) {
|
|
|
|
|
|
process(x, key);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (const key in update) {
|
|
|
|
|
|
process(update[key], key);
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const key of deleteMe) {
|
|
|
|
|
|
delete update[key];
|
2017-06-21 09:23:20 -03:00
|
|
|
|
}
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return ops;
|
|
|
|
|
|
}
|
2016-04-04 14:05:03 -04:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Processes relation-updating operations from a REST-format update.
|
|
|
|
|
|
// Returns a promise that resolves when all updates have been performed
|
2020-10-25 15:06:58 -05:00
|
|
|
|
handleRelationUpdates(className: string, objectId: string, update: any, ops: any) {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
var pending = [];
|
|
|
|
|
|
objectId = update.objectId || objectId;
|
2018-09-01 13:58:06 -04:00
|
|
|
|
ops.forEach(({ key, op }) => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (!op) {
|
|
|
|
|
|
return;
|
2017-06-21 09:23:20 -03:00
|
|
|
|
}
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (op.__op == 'AddRelation') {
|
|
|
|
|
|
for (const object of op.objects) {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
pending.push(this.addRelation(key, className, objectId, object.objectId));
|
2016-04-04 14:05:03 -04:00
|
|
|
|
}
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (op.__op == 'RemoveRelation') {
|
|
|
|
|
|
for (const object of op.objects) {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
pending.push(this.removeRelation(key, className, objectId, object.objectId));
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2017-06-21 09:23:20 -03:00
|
|
|
|
});
|
2016-04-04 14:05:03 -04:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return Promise.all(pending);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Adds a relation.
|
|
|
|
|
|
// Returns a promise that resolves successfully iff the add was successful.
|
2020-10-25 15:06:58 -05:00
|
|
|
|
addRelation(key: string, fromClassName: string, fromId: string, toId: string) {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const doc = {
|
|
|
|
|
|
relatedId: toId,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
owningId: fromId,
|
2017-12-30 20:44:18 -05:00
|
|
|
|
};
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return this.adapter.upsertOneObject(
|
|
|
|
|
|
`_Join:${key}:${fromClassName}`,
|
|
|
|
|
|
relationSchema,
|
|
|
|
|
|
doc,
|
2019-07-31 02:41:07 -07:00
|
|
|
|
doc,
|
|
|
|
|
|
this._transactionalSession
|
2018-09-01 13:58:06 -04:00
|
|
|
|
);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Removes a relation.
|
|
|
|
|
|
// Returns a promise that resolves successfully iff the remove was
|
|
|
|
|
|
// successful.
|
2020-10-25 15:06:58 -05:00
|
|
|
|
removeRelation(key: string, fromClassName: string, fromId: string, toId: string) {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
var doc = {
|
|
|
|
|
|
relatedId: toId,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
owningId: fromId,
|
2017-12-30 20:44:18 -05:00
|
|
|
|
};
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return this.adapter
|
|
|
|
|
|
.deleteObjectsByQuery(
|
|
|
|
|
|
`_Join:${key}:${fromClassName}`,
|
|
|
|
|
|
relationSchema,
|
2019-07-31 02:41:07 -07:00
|
|
|
|
doc,
|
|
|
|
|
|
this._transactionalSession
|
2018-09-01 13:58:06 -04:00
|
|
|
|
)
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.catch(error => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// We don't care if they try to delete a non-existent relation.
|
|
|
|
|
|
if (error.code == Parse.Error.OBJECT_NOT_FOUND) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Removes objects matches this query from the database.
|
|
|
|
|
|
// Returns a promise that resolves successfully iff the object was
|
|
|
|
|
|
// deleted.
|
|
|
|
|
|
// Options:
|
|
|
|
|
|
// acl: a list of strings. If the object to be updated has an ACL,
|
|
|
|
|
|
// one of the provided strings must provide the caller with
|
|
|
|
|
|
// write permissions.
|
2018-09-01 13:58:06 -04:00
|
|
|
|
destroy(
|
|
|
|
|
|
className: string,
|
|
|
|
|
|
query: any,
|
2019-05-30 11:14:05 -05:00
|
|
|
|
{ acl }: QueryOptions = {},
|
|
|
|
|
|
validSchemaController: SchemaController.SchemaController
|
2018-09-01 13:58:06 -04:00
|
|
|
|
): Promise<any> {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const isMaster = acl === undefined;
|
|
|
|
|
|
const aclGroup = acl || [];
|
|
|
|
|
|
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return this.loadSchemaIfNeeded(validSchemaController).then(schemaController => {
|
|
|
|
|
|
return (isMaster
|
|
|
|
|
|
? Promise.resolve()
|
|
|
|
|
|
: schemaController.validatePermission(className, aclGroup, 'delete')
|
|
|
|
|
|
).then(() => {
|
|
|
|
|
|
if (!isMaster) {
|
|
|
|
|
|
query = this.addPointerPermissions(
|
|
|
|
|
|
schemaController,
|
|
|
|
|
|
className,
|
|
|
|
|
|
'delete',
|
|
|
|
|
|
query,
|
|
|
|
|
|
aclGroup
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!query) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// delete by query
|
|
|
|
|
|
if (acl) {
|
|
|
|
|
|
query = addWriteACL(query, acl);
|
|
|
|
|
|
}
|
|
|
|
|
|
validateQuery(query);
|
|
|
|
|
|
return schemaController
|
|
|
|
|
|
.getOneSchema(className)
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
// If the schema doesn't exist, pretend it exists with no fields. This behavior
|
|
|
|
|
|
// will likely need revisiting.
|
|
|
|
|
|
if (error === undefined) {
|
|
|
|
|
|
return { fields: {} };
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(parseFormatSchema =>
|
|
|
|
|
|
this.adapter.deleteObjectsByQuery(
|
2019-05-30 11:14:05 -05:00
|
|
|
|
className,
|
2020-10-25 15:06:58 -05:00
|
|
|
|
parseFormatSchema,
|
2019-05-30 11:14:05 -05:00
|
|
|
|
query,
|
2020-10-25 15:06:58 -05:00
|
|
|
|
this._transactionalSession
|
2018-09-01 13:58:06 -04:00
|
|
|
|
)
|
2020-10-25 15:06:58 -05:00
|
|
|
|
)
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
// When deleting sessions while changing passwords, don't throw an error if they don't have any sessions.
|
|
|
|
|
|
if (className === '_Session' && error.code === Parse.Error.OBJECT_NOT_FOUND) {
|
|
|
|
|
|
return Promise.resolve({});
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Inserts an object into the database.
|
|
|
|
|
|
// Returns a promise that resolves successfully iff the object saved.
|
2018-09-01 13:58:06 -04:00
|
|
|
|
create(
|
|
|
|
|
|
className: string,
|
|
|
|
|
|
object: any,
|
2019-05-11 10:37:27 -07:00
|
|
|
|
{ acl }: QueryOptions = {},
|
2019-05-30 11:14:05 -05:00
|
|
|
|
validateOnly: boolean = false,
|
|
|
|
|
|
validSchemaController: SchemaController.SchemaController
|
2018-09-01 13:58:06 -04:00
|
|
|
|
): Promise<any> {
|
|
|
|
|
|
// Make a copy of the object, so we don't mutate the incoming data.
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const originalObject = object;
|
|
|
|
|
|
object = transformObjectACL(object);
|
|
|
|
|
|
|
|
|
|
|
|
object.createdAt = { iso: object.createdAt, __type: 'Date' };
|
|
|
|
|
|
object.updatedAt = { iso: object.updatedAt, __type: 'Date' };
|
|
|
|
|
|
|
|
|
|
|
|
var isMaster = acl === undefined;
|
|
|
|
|
|
var aclGroup = acl || [];
|
2020-10-25 15:06:58 -05:00
|
|
|
|
const relationUpdates = this.collectRelationUpdates(className, null, object);
|
2019-05-30 11:14:05 -05:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return this.validateClassName(className)
|
2019-05-30 11:14:05 -05:00
|
|
|
|
.then(() => this.loadSchemaIfNeeded(validSchemaController))
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(schemaController => {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return (isMaster
|
|
|
|
|
|
? Promise.resolve()
|
|
|
|
|
|
: schemaController.validatePermission(className, aclGroup, 'create')
|
|
|
|
|
|
)
|
2017-12-30 20:44:18 -05:00
|
|
|
|
.then(() => schemaController.enforceClassExists(className))
|
|
|
|
|
|
.then(() => schemaController.getOneSchema(className, true))
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(schema => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
transformAuthData(className, object, schema);
|
|
|
|
|
|
flattenUpdateOperatorsForCreate(object);
|
2019-05-11 10:37:27 -07:00
|
|
|
|
if (validateOnly) {
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return this.adapter.createObject(
|
|
|
|
|
|
className,
|
|
|
|
|
|
SchemaController.convertSchemaToAdapterSchema(schema),
|
2019-07-31 02:41:07 -07:00
|
|
|
|
object,
|
|
|
|
|
|
this._transactionalSession
|
2018-09-01 13:58:06 -04:00
|
|
|
|
);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
})
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(result => {
|
2019-05-11 10:37:27 -07:00
|
|
|
|
if (validateOnly) {
|
|
|
|
|
|
return originalObject;
|
|
|
|
|
|
}
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return this.handleRelationUpdates(
|
|
|
|
|
|
className,
|
|
|
|
|
|
object.objectId,
|
|
|
|
|
|
object,
|
|
|
|
|
|
relationUpdates
|
|
|
|
|
|
).then(() => {
|
2022-03-12 14:47:23 +01:00
|
|
|
|
return this._sanitizeDatabaseResult(originalObject, result.ops[0]);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2018-09-01 13:58:06 -04:00
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-09-01 13:58:06 -04:00
|
|
|
|
canAddField(
|
|
|
|
|
|
schema: SchemaController.SchemaController,
|
|
|
|
|
|
className: string,
|
|
|
|
|
|
object: any,
|
2020-01-28 09:21:30 +03:00
|
|
|
|
aclGroup: string[],
|
|
|
|
|
|
runOptions: QueryOptions
|
2018-09-01 13:58:06 -04:00
|
|
|
|
): Promise<void> {
|
2018-10-08 14:16:29 -04:00
|
|
|
|
const classSchema = schema.schemaData[className];
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (!classSchema) {
|
2017-06-21 09:23:20 -03:00
|
|
|
|
return Promise.resolve();
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
const fields = Object.keys(object);
|
2018-10-08 14:16:29 -04:00
|
|
|
|
const schemaFields = Object.keys(classSchema.fields);
|
2020-07-13 17:13:08 -05:00
|
|
|
|
const newKeys = fields.filter(field => {
|
2018-03-08 10:31:53 -06:00
|
|
|
|
// Skip fields that are unset
|
2020-10-25 15:06:58 -05:00
|
|
|
|
if (object[field] && object[field].__op && object[field].__op === 'Delete') {
|
2018-03-08 10:31:53 -06:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2021-12-06 18:52:59 -05:00
|
|
|
|
return schemaFields.indexOf(getRootFieldName(field)) < 0;
|
2018-03-08 10:31:53 -06:00
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (newKeys.length > 0) {
|
2020-01-28 09:21:30 +03:00
|
|
|
|
// adds a marker that new field is being adding during update
|
|
|
|
|
|
runOptions.addsField = true;
|
|
|
|
|
|
|
|
|
|
|
|
const action = runOptions.action;
|
|
|
|
|
|
return schema.validatePermission(className, aclGroup, 'addField', action);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
2016-04-04 14:05:03 -04:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Won't delete collections in the system namespace
|
2018-07-03 11:13:08 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Delete all classes and clears the schema cache
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {boolean} fast set to true if it's ok to just delete rows and not indexes
|
|
|
|
|
|
* @returns {Promise<void>} when the deletions completes
|
|
|
|
|
|
*/
|
2018-07-18 14:42:50 +00:00
|
|
|
|
deleteEverything(fast: boolean = false): Promise<any> {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
this.schemaPromise = null;
|
2021-03-16 16:05:36 -05:00
|
|
|
|
SchemaCache.clear();
|
|
|
|
|
|
return this.adapter.deleteAllClasses(fast);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2016-03-07 19:26:40 -08:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Returns a promise for a list of related ids given an owning id.
|
|
|
|
|
|
// className here is the owning className.
|
2018-09-01 13:58:06 -04:00
|
|
|
|
relatedIds(
|
|
|
|
|
|
className: string,
|
|
|
|
|
|
key: string,
|
|
|
|
|
|
owningId: string,
|
|
|
|
|
|
queryOptions: QueryOptions
|
|
|
|
|
|
): Promise<Array<string>> {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const { skip, limit, sort } = queryOptions;
|
|
|
|
|
|
const findOptions = {};
|
|
|
|
|
|
if (sort && sort.createdAt && this.adapter.canSortOnJoinTables) {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
findOptions.sort = { _id: sort.createdAt };
|
2017-12-30 20:44:18 -05:00
|
|
|
|
findOptions.limit = limit;
|
|
|
|
|
|
findOptions.skip = skip;
|
|
|
|
|
|
queryOptions.skip = 0;
|
|
|
|
|
|
}
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return this.adapter
|
2020-10-25 15:06:58 -05:00
|
|
|
|
.find(joinTableName(className, key), relationSchema, { owningId }, findOptions)
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(results => results.map(result => result.relatedId));
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2016-03-07 19:26:40 -08:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Returns a promise for a list of owning ids given some related ids.
|
|
|
|
|
|
// className here is the owning className.
|
2020-10-25 15:06:58 -05:00
|
|
|
|
owningIds(className: string, key: string, relatedIds: string[]): Promise<string[]> {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return this.adapter
|
|
|
|
|
|
.find(
|
|
|
|
|
|
joinTableName(className, key),
|
|
|
|
|
|
relationSchema,
|
|
|
|
|
|
{ relatedId: { $in: relatedIds } },
|
perf: Allow covering relation queries with minimal index (#6581)
* Apply linter changes on files I'm about to update
My actual changes were quite difficult to find when buried in this sea
of style changes, which were getting automatically applied during a
pre-commit hook. Here I just run the hooks against the files I'm going
to be touching in the following commit, so that a reviewer can ignore
these automatically generated diffs and just view the meaningful commit.
* perf: Allow covering relation queries with minimal index
When finding objects through a relation, we're sending Mongo queries
that look like this:
```
db.getCollection('_Join:foo:bar').find({ relatedId: { $in: [...] } });
```
From the result of that query, we're only reading the `owningId` field,
so we can start by adding it as a projection:
```
db.getCollection('_Join:foo:bar')
.find({ relatedId: { $in: [...] } })
.project({ owningId: 1 });
```
This seems like the perfect example of a query that could be satisfied
with an index scan: we are querying on one field, and only need one
field from the matching document.
For example, this can allow users to speed up the fetching of user roles
in authentication, because they query a `roles` relation on the `_Role`
collection. To add a covering index on that, you could now add an index
like the following:
```
db.getCollection('_Join:roles:_Role').createIndex(
{ relatedId: 1, owningId: 1 },
{ background: true }
);
```
One caveat there is that the index I propose above doesn't include the
`_id` column. For the query in question, we don't actually care about
the ID of the row in the join table, just the `owningId` field, so we
can avoid some overhead of putting the `_id` column into the index if we
can also drop it from the projection. This requires adding a small
special case to the MongoStorageAdapter, because the `_id` field is
special: you have to opt-out of using it by projecting `{ _id: 0 }`.
2020-04-08 11:43:45 -07:00
|
|
|
|
{ keys: ['owningId'] }
|
2018-09-01 13:58:06 -04:00
|
|
|
|
)
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(results => results.map(result => result.owningId));
|
2016-03-02 14:33:51 -05:00
|
|
|
|
}
|
2016-03-07 19:26:40 -08:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Modifies query so that it no longer has $in on relation fields, or
|
|
|
|
|
|
// equal-to-pointer constraints on relation fields.
|
|
|
|
|
|
// Returns a promise that resolves when query is mutated
|
|
|
|
|
|
reduceInRelation(className: string, query: any, schema: any): Promise<any> {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
// Search for an in-relation or equal-to-relation
|
|
|
|
|
|
// Make it sequential for now, not sure of paralleization side effects
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (query['$or']) {
|
|
|
|
|
|
const ors = query['$or'];
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return Promise.all(
|
|
|
|
|
|
ors.map((aQuery, index) => {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return this.reduceInRelation(className, aQuery, schema).then(aQuery => {
|
|
|
|
|
|
query['$or'][index] = aQuery;
|
|
|
|
|
|
});
|
2018-09-01 13:58:06 -04:00
|
|
|
|
})
|
|
|
|
|
|
).then(() => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return Promise.resolve(query);
|
2017-06-21 02:54:13 -03:00
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2021-10-30 01:03:50 +08:00
|
|
|
|
if (query['$and']) {
|
2021-11-25 10:16:46 -08:00
|
|
|
|
const ands = query['$and'];
|
2021-10-30 01:03:50 +08:00
|
|
|
|
return Promise.all(
|
2021-11-25 10:16:46 -08:00
|
|
|
|
ands.map((aQuery, index) => {
|
2021-10-30 01:03:50 +08:00
|
|
|
|
return this.reduceInRelation(className, aQuery, schema).then(aQuery => {
|
|
|
|
|
|
query['$and'][index] = aQuery;
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
).then(() => {
|
|
|
|
|
|
return Promise.resolve(query);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
const promises = Object.keys(query).map(key => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const t = schema.getExpectedType(className, key);
|
|
|
|
|
|
if (!t || t.type !== 'Relation') {
|
|
|
|
|
|
return Promise.resolve(query);
|
|
|
|
|
|
}
|
2018-09-01 13:58:06 -04:00
|
|
|
|
let queries: ?(any[]) = null;
|
|
|
|
|
|
if (
|
|
|
|
|
|
query[key] &&
|
|
|
|
|
|
(query[key]['$in'] ||
|
|
|
|
|
|
query[key]['$ne'] ||
|
|
|
|
|
|
query[key]['$nin'] ||
|
|
|
|
|
|
query[key].__type == 'Pointer')
|
|
|
|
|
|
) {
|
|
|
|
|
|
// Build the list of queries
|
2020-07-13 17:13:08 -05:00
|
|
|
|
queries = Object.keys(query[key]).map(constraintKey => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
let relatedIds;
|
|
|
|
|
|
let isNegation = false;
|
|
|
|
|
|
if (constraintKey === 'objectId') {
|
|
|
|
|
|
relatedIds = [query[key].objectId];
|
|
|
|
|
|
} else if (constraintKey == '$in') {
|
2020-07-13 17:13:08 -05:00
|
|
|
|
relatedIds = query[key]['$in'].map(r => r.objectId);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
} else if (constraintKey == '$nin') {
|
|
|
|
|
|
isNegation = true;
|
2020-07-13 17:13:08 -05:00
|
|
|
|
relatedIds = query[key]['$nin'].map(r => r.objectId);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
} else if (constraintKey == '$ne') {
|
|
|
|
|
|
isNegation = true;
|
|
|
|
|
|
relatedIds = [query[key]['$ne'].objectId];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
isNegation,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
relatedIds,
|
|
|
|
|
|
};
|
2017-12-30 20:44:18 -05:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
queries = [{ isNegation: false, relatedIds: [] }];
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2016-03-30 19:35:54 -07:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// remove the current queryKey as we don,t need it anymore
|
|
|
|
|
|
delete query[key];
|
|
|
|
|
|
// execute each query independently to build the list of
|
|
|
|
|
|
// $in / $nin
|
2020-07-13 17:13:08 -05:00
|
|
|
|
const promises = queries.map(q => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (!q) {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
2020-07-13 17:13:08 -05:00
|
|
|
|
return this.owningIds(className, key, q.relatedIds).then(ids => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (q.isNegation) {
|
|
|
|
|
|
this.addNotInObjectIdsIds(ids, query);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.addInObjectIdsIds(ids, query);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2016-03-30 19:35:54 -07:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return Promise.all(promises).then(() => {
|
|
|
|
|
|
return Promise.resolve();
|
2018-09-01 13:58:06 -04:00
|
|
|
|
});
|
|
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
|
|
|
|
|
|
return Promise.all(promises).then(() => {
|
|
|
|
|
|
return Promise.resolve(query);
|
2018-09-01 13:58:06 -04:00
|
|
|
|
});
|
2016-03-03 08:40:30 -05:00
|
|
|
|
}
|
2016-03-30 19:35:54 -07:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Modifies query so that it no longer has $relatedTo
|
|
|
|
|
|
// Returns a promise that resolves when query is mutated
|
2020-10-25 15:06:58 -05:00
|
|
|
|
reduceRelationKeys(className: string, query: any, queryOptions: any): ?Promise<void> {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (query['$or']) {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return Promise.all(
|
2020-07-13 17:13:08 -05:00
|
|
|
|
query['$or'].map(aQuery => {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
return this.reduceRelationKeys(className, aQuery, queryOptions);
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2021-10-30 01:03:50 +08:00
|
|
|
|
if (query['$and']) {
|
|
|
|
|
|
return Promise.all(
|
|
|
|
|
|
query['$and'].map(aQuery => {
|
|
|
|
|
|
return this.reduceRelationKeys(className, aQuery, queryOptions);
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2017-12-30 20:44:18 -05:00
|
|
|
|
var relatedTo = query['$relatedTo'];
|
|
|
|
|
|
if (relatedTo) {
|
|
|
|
|
|
return this.relatedIds(
|
|
|
|
|
|
relatedTo.object.className,
|
|
|
|
|
|
relatedTo.key,
|
|
|
|
|
|
relatedTo.object.objectId,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
queryOptions
|
|
|
|
|
|
)
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(ids => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
delete query['$relatedTo'];
|
|
|
|
|
|
this.addInObjectIdsIds(ids, query);
|
|
|
|
|
|
return this.reduceRelationKeys(className, query, queryOptions);
|
2018-09-01 13:58:06 -04:00
|
|
|
|
})
|
|
|
|
|
|
.then(() => {});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2016-03-30 19:35:54 -07:00
|
|
|
|
}
|
2016-03-07 08:26:35 -05:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
addInObjectIdsIds(ids: ?Array<string> = null, query: any) {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
const idsFromString: ?Array<string> =
|
|
|
|
|
|
typeof query.objectId === 'string' ? [query.objectId] : null;
|
|
|
|
|
|
const idsFromEq: ?Array<string> =
|
|
|
|
|
|
query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null;
|
|
|
|
|
|
const idsFromIn: ?Array<string> =
|
|
|
|
|
|
query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null;
|
2016-03-03 08:40:30 -05:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// @flow-disable-next
|
2020-10-25 15:06:58 -05:00
|
|
|
|
const allIds: Array<Array<string>> = [idsFromString, idsFromEq, idsFromIn, ids].filter(
|
|
|
|
|
|
list => list !== null
|
|
|
|
|
|
);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
|
2016-04-04 14:05:03 -04:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
let idsIntersection = [];
|
|
|
|
|
|
if (totalLength > 125) {
|
|
|
|
|
|
idsIntersection = intersect.big(allIds);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
idsIntersection = intersect(allIds);
|
|
|
|
|
|
}
|
2016-04-04 14:05:03 -04:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Need to make sure we don't clobber existing shorthand $eq constraints on objectId.
|
|
|
|
|
|
if (!('objectId' in query)) {
|
|
|
|
|
|
query.objectId = {
|
|
|
|
|
|
$in: undefined,
|
|
|
|
|
|
};
|
|
|
|
|
|
} else if (typeof query.objectId === 'string') {
|
|
|
|
|
|
query.objectId = {
|
|
|
|
|
|
$in: undefined,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
$eq: query.objectId,
|
2017-12-30 20:44:18 -05:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
query.objectId['$in'] = idsIntersection;
|
|
|
|
|
|
|
|
|
|
|
|
return query;
|
2016-04-04 14:05:03 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
addNotInObjectIdsIds(ids: string[] = [], query: any) {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
const idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : [];
|
2020-07-13 17:13:08 -05:00
|
|
|
|
let allIds = [...idsFromNin, ...ids].filter(list => list !== null);
|
2016-04-04 14:05:03 -04:00
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// make a set and spread to remove duplicates
|
|
|
|
|
|
allIds = [...new Set(allIds)];
|
|
|
|
|
|
|
|
|
|
|
|
// Need to make sure we don't clobber existing shorthand $eq constraints on objectId.
|
|
|
|
|
|
if (!('objectId' in query)) {
|
|
|
|
|
|
query.objectId = {
|
|
|
|
|
|
$nin: undefined,
|
|
|
|
|
|
};
|
|
|
|
|
|
} else if (typeof query.objectId === 'string') {
|
|
|
|
|
|
query.objectId = {
|
|
|
|
|
|
$nin: undefined,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
$eq: query.objectId,
|
2017-12-30 20:44:18 -05:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
query.objectId['$nin'] = allIds;
|
|
|
|
|
|
return query;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Runs a query on the database.
|
|
|
|
|
|
// Returns a promise that resolves to a list of items.
|
|
|
|
|
|
// Options:
|
|
|
|
|
|
// skip number of results to skip.
|
|
|
|
|
|
// limit limit to this number of results.
|
|
|
|
|
|
// sort an object where keys are the fields to sort by.
|
|
|
|
|
|
// the value is +1 for ascending, -1 for descending.
|
|
|
|
|
|
// count run a count instead of returning results.
|
|
|
|
|
|
// acl restrict this operation with an ACL for the provided array
|
|
|
|
|
|
// of user objectIds and roles. acl: null means no user.
|
|
|
|
|
|
// when this field is not present, don't do anything regarding ACLs.
|
2020-02-14 09:44:51 -08:00
|
|
|
|
// caseInsensitive make string comparisons case insensitive
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// TODO: make userIds not needed here. The db adapter shouldn't know
|
|
|
|
|
|
// anything about users, ideally. Then, improve the format of the ACL
|
|
|
|
|
|
// arg to work like the others.
|
2018-09-01 13:58:06 -04:00
|
|
|
|
find(
|
|
|
|
|
|
className: string,
|
|
|
|
|
|
query: any,
|
|
|
|
|
|
{
|
|
|
|
|
|
skip,
|
|
|
|
|
|
limit,
|
|
|
|
|
|
acl,
|
|
|
|
|
|
sort = {},
|
|
|
|
|
|
count,
|
|
|
|
|
|
keys,
|
|
|
|
|
|
op,
|
|
|
|
|
|
distinct,
|
|
|
|
|
|
pipeline,
|
|
|
|
|
|
readPreference,
|
2020-01-14 01:14:43 -07:00
|
|
|
|
hint,
|
2020-02-14 09:44:51 -08:00
|
|
|
|
caseInsensitive = false,
|
2020-01-14 01:14:43 -07:00
|
|
|
|
explain,
|
2019-01-29 08:52:49 +00:00
|
|
|
|
}: any = {},
|
2019-05-30 11:14:05 -05:00
|
|
|
|
auth: any = {},
|
|
|
|
|
|
validSchemaController: SchemaController.SchemaController
|
2018-09-01 13:58:06 -04:00
|
|
|
|
): Promise<any> {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
const isMaster = acl === undefined;
|
|
|
|
|
|
const aclGroup = acl || [];
|
2018-09-01 13:58:06 -04:00
|
|
|
|
op =
|
2020-10-25 15:06:58 -05:00
|
|
|
|
op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find');
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Count operation if counting
|
2018-09-01 13:58:06 -04:00
|
|
|
|
op = count === true ? 'count' : op;
|
2017-12-30 20:44:18 -05:00
|
|
|
|
|
|
|
|
|
|
let classExists = true;
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return this.loadSchemaIfNeeded(validSchemaController).then(schemaController => {
|
|
|
|
|
|
//Allow volatile classes if querying with Master (for _PushStatus)
|
|
|
|
|
|
//TODO: Move volatile classes concept into mongo adapter, postgres adapter shouldn't care
|
|
|
|
|
|
//that api.parse.com breaks when _PushStatus exists in mongo.
|
|
|
|
|
|
return schemaController
|
|
|
|
|
|
.getOneSchema(className, isMaster)
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
// Behavior for non-existent classes is kinda weird on Parse.com. Probably doesn't matter too much.
|
|
|
|
|
|
// For now, pretend the class exists but has no objects,
|
|
|
|
|
|
if (error === undefined) {
|
|
|
|
|
|
classExists = false;
|
|
|
|
|
|
return { fields: {} };
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
})
|
|
|
|
|
|
.then(schema => {
|
|
|
|
|
|
// Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt,
|
|
|
|
|
|
// so duplicate that behavior here. If both are specified, the correct behavior to match Parse.com is to
|
|
|
|
|
|
// use the one that appears first in the sort list.
|
|
|
|
|
|
if (sort._created_at) {
|
|
|
|
|
|
sort.createdAt = sort._created_at;
|
|
|
|
|
|
delete sort._created_at;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sort._updated_at) {
|
|
|
|
|
|
sort.updatedAt = sort._updated_at;
|
|
|
|
|
|
delete sort._updated_at;
|
|
|
|
|
|
}
|
|
|
|
|
|
const queryOptions = {
|
|
|
|
|
|
skip,
|
|
|
|
|
|
limit,
|
|
|
|
|
|
sort,
|
|
|
|
|
|
keys,
|
|
|
|
|
|
readPreference,
|
|
|
|
|
|
hint,
|
|
|
|
|
|
caseInsensitive,
|
|
|
|
|
|
explain,
|
|
|
|
|
|
};
|
|
|
|
|
|
Object.keys(sort).forEach(fieldName => {
|
|
|
|
|
|
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`);
|
2017-06-20 09:15:26 -07:00
|
|
|
|
}
|
2020-10-25 15:06:58 -05:00
|
|
|
|
const rootFieldName = getRootFieldName(fieldName);
|
2020-12-09 12:19:15 -06:00
|
|
|
|
if (!SchemaController.fieldNameIsValid(rootFieldName, className)) {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.INVALID_KEY_NAME,
|
|
|
|
|
|
`Invalid field name: ${fieldName}.`
|
|
|
|
|
|
);
|
2019-05-30 11:14:05 -05:00
|
|
|
|
}
|
2020-10-25 15:06:58 -05:00
|
|
|
|
});
|
|
|
|
|
|
return (isMaster
|
|
|
|
|
|
? Promise.resolve()
|
|
|
|
|
|
: schemaController.validatePermission(className, aclGroup, op)
|
|
|
|
|
|
)
|
|
|
|
|
|
.then(() => this.reduceRelationKeys(className, query, queryOptions))
|
|
|
|
|
|
.then(() => this.reduceInRelation(className, query, schemaController))
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
let protectedFields;
|
|
|
|
|
|
if (!isMaster) {
|
|
|
|
|
|
query = this.addPointerPermissions(
|
|
|
|
|
|
schemaController,
|
|
|
|
|
|
className,
|
|
|
|
|
|
op,
|
|
|
|
|
|
query,
|
|
|
|
|
|
aclGroup
|
2019-05-30 11:14:05 -05:00
|
|
|
|
);
|
2020-10-25 15:06:58 -05:00
|
|
|
|
/* Don't use projections to optimize the protectedFields since the protectedFields
|
2019-08-22 21:01:50 +02:00
|
|
|
|
based on pointer-permissions are determined after querying. The filtering can
|
|
|
|
|
|
overwrite the protected fields. */
|
2020-10-25 15:06:58 -05:00
|
|
|
|
protectedFields = this.addProtectedFields(
|
|
|
|
|
|
schemaController,
|
|
|
|
|
|
className,
|
|
|
|
|
|
query,
|
|
|
|
|
|
aclGroup,
|
|
|
|
|
|
auth,
|
|
|
|
|
|
queryOptions
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!query) {
|
|
|
|
|
|
if (op === 'get') {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!isMaster) {
|
|
|
|
|
|
if (op === 'update' || op === 'delete') {
|
|
|
|
|
|
query = addWriteACL(query, aclGroup);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
query = addReadACL(query, aclGroup);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
validateQuery(query);
|
|
|
|
|
|
if (count) {
|
|
|
|
|
|
if (!classExists) {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return this.adapter.count(
|
2018-09-01 13:58:06 -04:00
|
|
|
|
className,
|
2020-10-25 15:06:58 -05:00
|
|
|
|
schema,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
query,
|
2020-10-25 15:06:58 -05:00
|
|
|
|
readPreference,
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
hint
|
2018-09-01 13:58:06 -04:00
|
|
|
|
);
|
2017-06-20 09:15:26 -07:00
|
|
|
|
}
|
2020-10-25 15:06:58 -05:00
|
|
|
|
} else if (distinct) {
|
|
|
|
|
|
if (!classExists) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return this.adapter.distinct(className, schema, query, distinct);
|
2019-05-30 11:14:05 -05:00
|
|
|
|
}
|
2020-10-25 15:06:58 -05:00
|
|
|
|
} else if (pipeline) {
|
|
|
|
|
|
if (!classExists) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return this.adapter.aggregate(
|
2020-01-14 01:14:43 -07:00
|
|
|
|
className,
|
|
|
|
|
|
schema,
|
2020-10-25 15:06:58 -05:00
|
|
|
|
pipeline,
|
|
|
|
|
|
readPreference,
|
|
|
|
|
|
hint,
|
|
|
|
|
|
explain
|
2020-01-14 01:14:43 -07:00
|
|
|
|
);
|
2019-05-30 11:14:05 -05:00
|
|
|
|
}
|
2020-10-25 15:06:58 -05:00
|
|
|
|
} else if (explain) {
|
|
|
|
|
|
return this.adapter.find(className, schema, query, queryOptions);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return this.adapter
|
|
|
|
|
|
.find(className, schema, query, queryOptions)
|
|
|
|
|
|
.then(objects =>
|
|
|
|
|
|
objects.map(object => {
|
|
|
|
|
|
object = untransformObjectACL(object);
|
|
|
|
|
|
return filterSensitiveData(
|
|
|
|
|
|
isMaster,
|
|
|
|
|
|
aclGroup,
|
|
|
|
|
|
auth,
|
|
|
|
|
|
op,
|
|
|
|
|
|
schemaController,
|
|
|
|
|
|
className,
|
|
|
|
|
|
protectedFields,
|
|
|
|
|
|
object
|
|
|
|
|
|
);
|
|
|
|
|
|
})
|
|
|
|
|
|
)
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2016-06-12 13:39:41 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
deleteSchema(className: string): Promise<void> {
|
2021-03-16 16:05:36 -05:00
|
|
|
|
let schemaController;
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return this.loadSchema({ clearCache: true })
|
2021-03-16 16:05:36 -05:00
|
|
|
|
.then(s => {
|
|
|
|
|
|
schemaController = s;
|
|
|
|
|
|
return schemaController.getOneSchema(className, true);
|
|
|
|
|
|
})
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.catch(error => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (error === undefined) {
|
|
|
|
|
|
return { fields: {} };
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.then((schema: any) => {
|
|
|
|
|
|
return this.collectionExists(className)
|
2020-10-25 15:06:58 -05:00
|
|
|
|
.then(() => this.adapter.count(className, { fields: {} }, null, '', false))
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(count => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (count > 0) {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
255,
|
|
|
|
|
|
`Class ${className} is not empty, contains ${count} objects, cannot drop schema.`
|
|
|
|
|
|
);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
return this.adapter.deleteClass(className);
|
|
|
|
|
|
})
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.then(wasParseCollection => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (wasParseCollection) {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
const relationFieldNames = Object.keys(schema.fields).filter(
|
2020-07-13 17:13:08 -05:00
|
|
|
|
fieldName => schema.fields[fieldName].type === 'Relation'
|
2018-09-01 13:58:06 -04:00
|
|
|
|
);
|
|
|
|
|
|
return Promise.all(
|
2020-07-13 17:13:08 -05:00
|
|
|
|
relationFieldNames.map(name =>
|
2018-09-01 13:58:06 -04:00
|
|
|
|
this.adapter.deleteClass(joinTableName(className, name))
|
|
|
|
|
|
)
|
|
|
|
|
|
).then(() => {
|
2021-03-16 16:05:36 -05:00
|
|
|
|
SchemaCache.del(className);
|
|
|
|
|
|
return schemaController.reloadData();
|
2017-12-30 20:44:18 -05:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2018-09-01 13:58:06 -04:00
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2016-04-14 19:24:56 -04:00
|
|
|
|
|
2020-12-16 06:41:14 +01:00
|
|
|
|
// This helps to create intermediate objects for simpler comparison of
|
|
|
|
|
|
// key value pairs used in query objects. Each key value pair will represented
|
|
|
|
|
|
// in a similar way to json
|
|
|
|
|
|
objectToEntriesStrings(query: any): Array<string> {
|
|
|
|
|
|
return Object.entries(query).map(a => a.map(s => JSON.stringify(s)).join(':'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Naive logic reducer for OR operations meant to be used only for pointer permissions.
|
|
|
|
|
|
reduceOrOperation(query: { $or: Array<any> }): any {
|
|
|
|
|
|
if (!query.$or) {
|
|
|
|
|
|
return query;
|
|
|
|
|
|
}
|
|
|
|
|
|
const queries = query.$or.map(q => this.objectToEntriesStrings(q));
|
|
|
|
|
|
let repeat = false;
|
|
|
|
|
|
do {
|
|
|
|
|
|
repeat = false;
|
|
|
|
|
|
for (let i = 0; i < queries.length - 1; i++) {
|
|
|
|
|
|
for (let j = i + 1; j < queries.length; j++) {
|
|
|
|
|
|
const [shorter, longer] = queries[i].length > queries[j].length ? [j, i] : [i, j];
|
|
|
|
|
|
const foundEntries = queries[shorter].reduce(
|
|
|
|
|
|
(acc, entry) => acc + (queries[longer].includes(entry) ? 1 : 0),
|
|
|
|
|
|
0
|
|
|
|
|
|
);
|
|
|
|
|
|
const shorterEntries = queries[shorter].length;
|
|
|
|
|
|
if (foundEntries === shorterEntries) {
|
|
|
|
|
|
// If the shorter query is completely contained in the longer one, we can strike
|
|
|
|
|
|
// out the longer query.
|
|
|
|
|
|
query.$or.splice(longer, 1);
|
|
|
|
|
|
queries.splice(longer, 1);
|
|
|
|
|
|
repeat = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} while (repeat);
|
|
|
|
|
|
if (query.$or.length === 1) {
|
|
|
|
|
|
query = { ...query, ...query.$or[0] };
|
|
|
|
|
|
delete query.$or;
|
|
|
|
|
|
}
|
|
|
|
|
|
return query;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Naive logic reducer for AND operations meant to be used only for pointer permissions.
|
|
|
|
|
|
reduceAndOperation(query: { $and: Array<any> }): any {
|
|
|
|
|
|
if (!query.$and) {
|
|
|
|
|
|
return query;
|
|
|
|
|
|
}
|
|
|
|
|
|
const queries = query.$and.map(q => this.objectToEntriesStrings(q));
|
|
|
|
|
|
let repeat = false;
|
|
|
|
|
|
do {
|
|
|
|
|
|
repeat = false;
|
|
|
|
|
|
for (let i = 0; i < queries.length - 1; i++) {
|
|
|
|
|
|
for (let j = i + 1; j < queries.length; j++) {
|
|
|
|
|
|
const [shorter, longer] = queries[i].length > queries[j].length ? [j, i] : [i, j];
|
|
|
|
|
|
const foundEntries = queries[shorter].reduce(
|
|
|
|
|
|
(acc, entry) => acc + (queries[longer].includes(entry) ? 1 : 0),
|
|
|
|
|
|
0
|
|
|
|
|
|
);
|
|
|
|
|
|
const shorterEntries = queries[shorter].length;
|
|
|
|
|
|
if (foundEntries === shorterEntries) {
|
|
|
|
|
|
// If the shorter query is completely contained in the longer one, we can strike
|
|
|
|
|
|
// out the shorter query.
|
|
|
|
|
|
query.$and.splice(shorter, 1);
|
|
|
|
|
|
queries.splice(shorter, 1);
|
|
|
|
|
|
repeat = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} while (repeat);
|
|
|
|
|
|
if (query.$and.length === 1) {
|
|
|
|
|
|
query = { ...query, ...query.$and[0] };
|
|
|
|
|
|
delete query.$and;
|
|
|
|
|
|
}
|
|
|
|
|
|
return query;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-01-28 09:21:30 +03:00
|
|
|
|
// Constraints query using CLP's pointer permissions (PP) if any.
|
|
|
|
|
|
// 1. Etract the user id from caller's ACLgroup;
|
|
|
|
|
|
// 2. Exctract a list of field names that are PP for target collection and operation;
|
|
|
|
|
|
// 3. Constraint the original query so that each PP field must
|
|
|
|
|
|
// point to caller's id (or contain it in case of PP field being an array)
|
2018-09-01 13:58:06 -04:00
|
|
|
|
addPointerPermissions(
|
2018-10-17 17:53:49 -04:00
|
|
|
|
schema: SchemaController.SchemaController,
|
2018-09-01 13:58:06 -04:00
|
|
|
|
className: string,
|
|
|
|
|
|
operation: string,
|
|
|
|
|
|
query: any,
|
|
|
|
|
|
aclGroup: any[] = []
|
2020-01-28 09:21:30 +03:00
|
|
|
|
): any {
|
2018-09-01 13:58:06 -04:00
|
|
|
|
// Check if class has public permission for operation
|
|
|
|
|
|
// If the BaseCLP pass, let go through
|
2018-10-17 17:53:49 -04:00
|
|
|
|
if (schema.testPermissionsForClassName(className, aclGroup, operation)) {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return query;
|
|
|
|
|
|
}
|
2018-10-17 17:53:49 -04:00
|
|
|
|
const perms = schema.getClassLevelPermissions(className);
|
2020-01-28 09:21:30 +03:00
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
const userACL = aclGroup.filter(acl => {
|
2017-12-30 20:44:18 -05:00
|
|
|
|
return acl.indexOf('role:') != 0 && acl != '*';
|
|
|
|
|
|
});
|
2020-01-28 09:21:30 +03:00
|
|
|
|
|
|
|
|
|
|
const groupKey =
|
2020-10-25 15:06:58 -05:00
|
|
|
|
['get', 'find', 'count'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields';
|
2020-01-28 09:21:30 +03:00
|
|
|
|
|
|
|
|
|
|
const permFields = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (perms[operation] && perms[operation].pointerFields) {
|
|
|
|
|
|
permFields.push(...perms[operation].pointerFields);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (perms[groupKey]) {
|
|
|
|
|
|
for (const field of perms[groupKey]) {
|
|
|
|
|
|
if (!permFields.includes(field)) {
|
|
|
|
|
|
permFields.push(field);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// the ACL should have exactly 1 user
|
2020-01-28 09:21:30 +03:00
|
|
|
|
if (permFields.length > 0) {
|
|
|
|
|
|
// the ACL should have exactly 1 user
|
2018-09-01 13:58:06 -04:00
|
|
|
|
// No user set return undefined
|
|
|
|
|
|
// If the length is > 1, that means we didn't de-dupe users correctly
|
2017-12-30 20:44:18 -05:00
|
|
|
|
if (userACL.length != 1) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const userId = userACL[0];
|
2018-09-01 13:58:06 -04:00
|
|
|
|
const userPointer = {
|
|
|
|
|
|
__type: 'Pointer',
|
|
|
|
|
|
className: '_User',
|
|
|
|
|
|
objectId: userId,
|
2016-04-20 21:51:11 -04:00
|
|
|
|
};
|
2017-12-30 20:44:18 -05:00
|
|
|
|
|
2020-07-19 10:37:36 -07:00
|
|
|
|
const queries = permFields.map(key => {
|
2020-07-17 14:14:43 -04:00
|
|
|
|
const fieldDescriptor = schema.getExpectedType(className, key);
|
|
|
|
|
|
const fieldType =
|
|
|
|
|
|
fieldDescriptor &&
|
|
|
|
|
|
typeof fieldDescriptor === 'object' &&
|
|
|
|
|
|
Object.prototype.hasOwnProperty.call(fieldDescriptor, 'type')
|
|
|
|
|
|
? fieldDescriptor.type
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
let queryClause;
|
|
|
|
|
|
|
|
|
|
|
|
if (fieldType === 'Pointer') {
|
|
|
|
|
|
// constraint for single pointer setup
|
|
|
|
|
|
queryClause = { [key]: userPointer };
|
|
|
|
|
|
} else if (fieldType === 'Array') {
|
|
|
|
|
|
// constraint for users-array setup
|
|
|
|
|
|
queryClause = { [key]: { $all: [userPointer] } };
|
|
|
|
|
|
} else if (fieldType === 'Object') {
|
|
|
|
|
|
// constraint for object setup
|
|
|
|
|
|
queryClause = { [key]: userPointer };
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// This means that there is a CLP field of an unexpected type. This condition should not happen, which is
|
|
|
|
|
|
// why is being treated as an error.
|
|
|
|
|
|
throw Error(
|
|
|
|
|
|
`An unexpected condition occurred when resolving pointer permissions: ${className} ${key}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// if we already have a constraint on the key, use the $and
|
2019-08-14 16:57:00 -05:00
|
|
|
|
if (Object.prototype.hasOwnProperty.call(query, key)) {
|
2020-12-16 06:41:14 +01:00
|
|
|
|
return this.reduceAndOperation({ $and: [queryClause, query] });
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
|
|
|
|
|
// otherwise just add the constaint
|
2020-07-17 14:14:43 -04:00
|
|
|
|
return Object.assign({}, query, queryClause);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
});
|
2020-07-17 14:14:43 -04:00
|
|
|
|
|
2020-12-16 06:41:14 +01:00
|
|
|
|
return queries.length === 1 ? queries[0] : this.reduceOrOperation({ $or: queries });
|
2017-12-30 20:44:18 -05:00
|
|
|
|
} else {
|
|
|
|
|
|
return query;
|
2016-04-20 21:51:11 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-01-29 08:52:49 +00:00
|
|
|
|
addProtectedFields(
|
2022-06-30 13:01:40 +02:00
|
|
|
|
schema: SchemaController.SchemaController | any,
|
2019-01-29 08:52:49 +00:00
|
|
|
|
className: string,
|
|
|
|
|
|
query: any = {},
|
|
|
|
|
|
aclGroup: any[] = [],
|
2020-02-19 12:34:08 +03:00
|
|
|
|
auth: any = {},
|
|
|
|
|
|
queryOptions: FullQueryOptions = {}
|
2020-01-28 09:21:30 +03:00
|
|
|
|
): null | string[] {
|
2022-06-30 13:01:40 +02:00
|
|
|
|
const perms =
|
|
|
|
|
|
schema && schema.getClassLevelPermissions
|
|
|
|
|
|
? schema.getClassLevelPermissions(className)
|
|
|
|
|
|
: schema;
|
2019-01-29 08:52:49 +00:00
|
|
|
|
if (!perms) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const protectedFields = perms.protectedFields;
|
|
|
|
|
|
if (!protectedFields) return null;
|
|
|
|
|
|
|
|
|
|
|
|
if (aclGroup.indexOf(query.objectId) > -1) return null;
|
2019-08-22 21:01:50 +02:00
|
|
|
|
|
2020-02-19 12:34:08 +03:00
|
|
|
|
// for queries where "keys" are set and do not include all 'userField':{field},
|
|
|
|
|
|
// we have to transparently include it, and then remove before returning to client
|
|
|
|
|
|
// Because if such key not projected the permission won't be enforced properly
|
|
|
|
|
|
// PS this is called when 'excludeKeys' already reduced to 'keys'
|
|
|
|
|
|
const preserveKeys = queryOptions.keys;
|
|
|
|
|
|
|
|
|
|
|
|
// these are keys that need to be included only
|
|
|
|
|
|
// to be able to apply protectedFields by pointer
|
|
|
|
|
|
// and then unset before returning to client (later in filterSensitiveFields)
|
|
|
|
|
|
const serverOnlyKeys = [];
|
|
|
|
|
|
|
|
|
|
|
|
const authenticated = auth.user;
|
|
|
|
|
|
|
|
|
|
|
|
// map to allow check without array search
|
|
|
|
|
|
const roles = (auth.userRoles || []).reduce((acc, r) => {
|
|
|
|
|
|
acc[r] = protectedFields[r];
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
|
|
// array of sets of protected fields. separate item for each applicable criteria
|
|
|
|
|
|
const protectedKeysSets = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const key in protectedFields) {
|
|
|
|
|
|
// skip userFields
|
|
|
|
|
|
if (key.startsWith('userField:')) {
|
|
|
|
|
|
if (preserveKeys) {
|
|
|
|
|
|
const fieldName = key.substring(10);
|
|
|
|
|
|
if (!preserveKeys.includes(fieldName)) {
|
|
|
|
|
|
// 1. put it there temporarily
|
|
|
|
|
|
queryOptions.keys && queryOptions.keys.push(fieldName);
|
|
|
|
|
|
// 2. preserve it delete later
|
|
|
|
|
|
serverOnlyKeys.push(fieldName);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// add public tier
|
|
|
|
|
|
if (key === '*') {
|
|
|
|
|
|
protectedKeysSets.push(protectedFields[key]);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (authenticated) {
|
|
|
|
|
|
if (key === 'authenticated') {
|
|
|
|
|
|
// for logged in users
|
|
|
|
|
|
protectedKeysSets.push(protectedFields[key]);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (roles[key] && key.startsWith('role:')) {
|
|
|
|
|
|
// add applicable roles
|
|
|
|
|
|
protectedKeysSets.push(roles[key]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// check if there's a rule for current user's id
|
|
|
|
|
|
if (authenticated) {
|
|
|
|
|
|
const userId = auth.user.id;
|
|
|
|
|
|
if (perms.protectedFields[userId]) {
|
|
|
|
|
|
protectedKeysSets.push(perms.protectedFields[userId]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// preserve fields to be removed before sending response to client
|
|
|
|
|
|
if (serverOnlyKeys.length > 0) {
|
|
|
|
|
|
perms.protectedFields.temporaryKeys = serverOnlyKeys;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let protectedKeys = protectedKeysSets.reduce((acc, next) => {
|
|
|
|
|
|
if (next) {
|
|
|
|
|
|
acc.push(...next);
|
|
|
|
|
|
}
|
|
|
|
|
|
return acc;
|
2019-08-22 21:01:50 +02:00
|
|
|
|
}, []);
|
|
|
|
|
|
|
2020-02-19 12:34:08 +03:00
|
|
|
|
// intersect all sets of protectedFields
|
2020-07-13 17:13:08 -05:00
|
|
|
|
protectedKeysSets.forEach(fields => {
|
2019-01-31 21:44:24 +00:00
|
|
|
|
if (fields) {
|
2020-07-13 17:13:08 -05:00
|
|
|
|
protectedKeys = protectedKeys.filter(v => fields.includes(v));
|
2019-01-29 08:52:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return protectedKeys;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2019-07-31 02:41:07 -07:00
|
|
|
|
createTransactionalSession() {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return this.adapter.createTransactionalSession().then(transactionalSession => {
|
|
|
|
|
|
this._transactionalSession = transactionalSession;
|
|
|
|
|
|
});
|
2019-07-31 02:41:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
commitTransactionalSession() {
|
|
|
|
|
|
if (!this._transactionalSession) {
|
|
|
|
|
|
throw new Error('There is no transactional session to commit');
|
|
|
|
|
|
}
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return this.adapter.commitTransactionalSession(this._transactionalSession).then(() => {
|
|
|
|
|
|
this._transactionalSession = null;
|
|
|
|
|
|
});
|
2019-07-31 02:41:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
abortTransactionalSession() {
|
|
|
|
|
|
if (!this._transactionalSession) {
|
|
|
|
|
|
throw new Error('There is no transactional session to abort');
|
|
|
|
|
|
}
|
2020-10-25 15:06:58 -05:00
|
|
|
|
return this.adapter.abortTransactionalSession(this._transactionalSession).then(() => {
|
|
|
|
|
|
this._transactionalSession = null;
|
|
|
|
|
|
});
|
2019-07-31 02:41:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
|
|
|
|
|
|
// have a Parse app without it having a _User collection.
|
2021-03-10 13:31:35 -06:00
|
|
|
|
async performInitialization() {
|
|
|
|
|
|
await this.adapter.performInitialization({
|
|
|
|
|
|
VolatileClassesSchemas: SchemaController.VolatileClassesSchemas,
|
|
|
|
|
|
});
|
2018-09-01 13:58:06 -04:00
|
|
|
|
const requiredUserFields = {
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
...SchemaController.defaultColumns._Default,
|
|
|
|
|
|
...SchemaController.defaultColumns._User,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
const requiredRoleFields = {
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
...SchemaController.defaultColumns._Default,
|
|
|
|
|
|
...SchemaController.defaultColumns._Role,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2020-07-15 20:10:33 +02:00
|
|
|
|
const requiredIdempotencyFields = {
|
|
|
|
|
|
fields: {
|
|
|
|
|
|
...SchemaController.defaultColumns._Default,
|
|
|
|
|
|
...SchemaController.defaultColumns._Idempotency,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2021-03-16 16:05:36 -05:00
|
|
|
|
await this.loadSchema().then(schema => schema.enforceClassExists('_User'));
|
|
|
|
|
|
await this.loadSchema().then(schema => schema.enforceClassExists('_Role'));
|
2022-01-02 13:25:53 -05:00
|
|
|
|
await this.loadSchema().then(schema => schema.enforceClassExists('_Idempotency'));
|
2017-12-30 20:44:18 -05:00
|
|
|
|
|
2021-03-16 16:05:36 -05:00
|
|
|
|
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']).catch(error => {
|
|
|
|
|
|
logger.warn('Unable to ensure uniqueness for usernames: ', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
2017-12-30 20:44:18 -05:00
|
|
|
|
|
2021-03-16 16:05:36 -05:00
|
|
|
|
await this.adapter
|
|
|
|
|
|
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.catch(error => {
|
2021-03-16 16:05:36 -05:00
|
|
|
|
logger.warn('Unable to create case insensitive username index: ', error);
|
2017-12-30 20:44:18 -05:00
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
2021-03-16 16:05:36 -05:00
|
|
|
|
await this.adapter
|
|
|
|
|
|
.ensureIndex('_User', requiredUserFields, ['username'], 'case_insensitive_username', true)
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.catch(error => {
|
2020-10-25 15:06:58 -05:00
|
|
|
|
logger.warn('Unable to create case insensitive username index: ', error);
|
2020-02-14 09:44:51 -08:00
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2021-03-16 16:05:36 -05:00
|
|
|
|
await this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']).catch(error => {
|
|
|
|
|
|
logger.warn('Unable to ensure uniqueness for user email addresses: ', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
2017-03-04 22:42:19 +02:00
|
|
|
|
|
2021-03-16 16:05:36 -05:00
|
|
|
|
await this.adapter
|
|
|
|
|
|
.ensureIndex('_User', requiredUserFields, ['email'], 'case_insensitive_email', true)
|
2020-07-13 17:13:08 -05:00
|
|
|
|
.catch(error => {
|
2020-02-14 09:44:51 -08:00
|
|
|
|
logger.warn('Unable to create case insensitive email index: ', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2021-03-16 16:05:36 -05:00
|
|
|
|
await this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']).catch(error => {
|
|
|
|
|
|
logger.warn('Unable to ensure uniqueness for role name: ', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
2016-08-15 16:48:39 -04:00
|
|
|
|
|
2022-01-02 13:25:53 -05:00
|
|
|
|
await this.adapter
|
|
|
|
|
|
.ensureUniqueness('_Idempotency', requiredIdempotencyFields, ['reqId'])
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
logger.warn('Unable to ensure uniqueness for idempotency request ID: ', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const isMongoAdapter = this.adapter instanceof MongoStorageAdapter;
|
|
|
|
|
|
const isPostgresAdapter = this.adapter instanceof PostgresStorageAdapter;
|
|
|
|
|
|
if (isMongoAdapter || isPostgresAdapter) {
|
|
|
|
|
|
let options = {};
|
|
|
|
|
|
if (isMongoAdapter) {
|
|
|
|
|
|
options = {
|
2021-03-16 16:05:36 -05:00
|
|
|
|
ttl: 0,
|
2022-01-02 13:25:53 -05:00
|
|
|
|
};
|
|
|
|
|
|
} else if (isPostgresAdapter) {
|
|
|
|
|
|
options = this.idempotencyOptions;
|
|
|
|
|
|
options.setIdempotencyFunction = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.adapter
|
|
|
|
|
|
.ensureIndex('_Idempotency', requiredIdempotencyFields, ['expire'], 'ttl', false, options)
|
2021-03-16 16:05:36 -05:00
|
|
|
|
.catch(error => {
|
|
|
|
|
|
logger.warn('Unable to create TTL index for idempotency expire date: ', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
await this.adapter.updateSchemaWithIndexes();
|
2017-12-30 20:44:18 -05:00
|
|
|
|
}
|
2016-08-07 23:02:53 -04:00
|
|
|
|
|
2022-03-12 14:47:23 +01:00
|
|
|
|
_expandResultOnKeyPath(object: any, key: string, value: any): any {
|
|
|
|
|
|
if (key.indexOf('.') < 0) {
|
|
|
|
|
|
object[key] = value[key];
|
|
|
|
|
|
return object;
|
|
|
|
|
|
}
|
|
|
|
|
|
const path = key.split('.');
|
|
|
|
|
|
const firstKey = path[0];
|
|
|
|
|
|
const nextPath = path.slice(1).join('.');
|
|
|
|
|
|
|
|
|
|
|
|
// Scan request data for denied keywords
|
|
|
|
|
|
if (this.options && this.options.requestKeywordDenylist) {
|
|
|
|
|
|
// Scan request data for denied keywords
|
|
|
|
|
|
for (const keyword of this.options.requestKeywordDenylist) {
|
2022-03-24 02:54:07 +01:00
|
|
|
|
const match = Utils.objectContainsKeyValue({ firstKey: undefined }, keyword.key, undefined);
|
|
|
|
|
|
if (match) {
|
2022-03-12 14:47:23 +01:00
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.INVALID_KEY_NAME,
|
|
|
|
|
|
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
object[firstKey] = this._expandResultOnKeyPath(
|
|
|
|
|
|
object[firstKey] || {},
|
|
|
|
|
|
nextPath,
|
|
|
|
|
|
value[firstKey]
|
|
|
|
|
|
);
|
|
|
|
|
|
delete object[key];
|
|
|
|
|
|
return object;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_sanitizeDatabaseResult(originalObject: any, result: any): Promise<any> {
|
|
|
|
|
|
const response = {};
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
|
return Promise.resolve(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
Object.keys(originalObject).forEach(key => {
|
|
|
|
|
|
const keyUpdate = originalObject[key];
|
|
|
|
|
|
// determine if that was an op
|
|
|
|
|
|
if (
|
|
|
|
|
|
keyUpdate &&
|
|
|
|
|
|
typeof keyUpdate === 'object' &&
|
|
|
|
|
|
keyUpdate.__op &&
|
|
|
|
|
|
['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1
|
|
|
|
|
|
) {
|
|
|
|
|
|
// only valid ops that produce an actionable result
|
|
|
|
|
|
// the op may have happened on a keypath
|
|
|
|
|
|
this._expandResultOnKeyPath(response, key, result);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return Promise.resolve(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-07-13 17:13:08 -05:00
|
|
|
|
static _validateQuery: any => void;
|
2022-06-30 13:01:40 +02:00
|
|
|
|
static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void;
|
2016-03-01 20:04:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-02-27 02:02:33 -08:00
|
|
|
|
module.exports = DatabaseController;
|
2017-12-30 20:44:18 -05:00
|
|
|
|
// Expose validateQuery for tests
|
|
|
|
|
|
module.exports._validateQuery = validateQuery;
|
2022-06-30 13:01:40 +02:00
|
|
|
|
module.exports.filterSensitiveData = filterSensitiveData;
|