@@ -2,9 +2,9 @@ const mongodb = require('mongodb');
|
||||
const Collection = mongodb.Collection;
|
||||
|
||||
export default class MongoCollection {
|
||||
_mongoCollection:Collection;
|
||||
_mongoCollection: Collection;
|
||||
|
||||
constructor(mongoCollection:Collection) {
|
||||
constructor(mongoCollection: Collection) {
|
||||
this._mongoCollection = mongoCollection;
|
||||
}
|
||||
|
||||
@@ -15,33 +15,58 @@ export default class MongoCollection {
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
find(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) {
|
||||
// Support for Full Text Search - $text
|
||||
if(keys && keys.$score) {
|
||||
if (keys && keys.$score) {
|
||||
delete keys.$score;
|
||||
keys.score = {$meta: 'textScore'};
|
||||
keys.score = { $meta: 'textScore' };
|
||||
}
|
||||
return this._rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference })
|
||||
.catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
|
||||
throw error;
|
||||
}
|
||||
// Figure out what key needs an index
|
||||
const key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
return this._rawFind(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
}).catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (
|
||||
error.code != 17007 &&
|
||||
!error.message.match(/unable to find index for .geoNear/)
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
// Figure out what key needs an index
|
||||
const key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
return this._mongoCollection.createIndex(index)
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
return (
|
||||
this._mongoCollection
|
||||
.createIndex(index)
|
||||
// Retry, but just once.
|
||||
.then(() => this._rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference }));
|
||||
});
|
||||
.then(() =>
|
||||
this._rawFind(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) {
|
||||
let findOperation = this._mongoCollection
|
||||
.find(query, { skip, limit, sort, readPreference })
|
||||
let findOperation = this._mongoCollection.find(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
readPreference,
|
||||
});
|
||||
|
||||
if (keys) {
|
||||
findOperation = findOperation.project(keys);
|
||||
@@ -55,7 +80,13 @@ export default class MongoCollection {
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort, maxTimeMS, readPreference } = {}) {
|
||||
const countOperation = this._mongoCollection.count(query, { skip, limit, sort, maxTimeMS, readPreference });
|
||||
const countOperation = this._mongoCollection.count(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
});
|
||||
|
||||
return countOperation;
|
||||
}
|
||||
@@ -65,7 +96,9 @@ export default class MongoCollection {
|
||||
}
|
||||
|
||||
aggregate(pipeline, { maxTimeMS, readPreference } = {}) {
|
||||
return this._mongoCollection.aggregate(pipeline, { maxTimeMS, readPreference }).toArray();
|
||||
return this._mongoCollection
|
||||
.aggregate(pipeline, { maxTimeMS, readPreference })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
insertOne(object) {
|
||||
@@ -76,7 +109,7 @@ export default class MongoCollection {
|
||||
// If there is nothing that matches the query - does insert
|
||||
// Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5.
|
||||
upsertOne(query, update) {
|
||||
return this._mongoCollection.update(query, update, { upsert: true })
|
||||
return this._mongoCollection.update(query, update, { upsert: true });
|
||||
}
|
||||
|
||||
updateOne(query, update) {
|
||||
@@ -93,13 +126,17 @@ export default class MongoCollection {
|
||||
|
||||
_ensureSparseUniqueIndexInBackground(indexRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
this._mongoCollection.ensureIndex(
|
||||
indexRequest,
|
||||
{ unique: true, background: true, sparse: true },
|
||||
error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import MongoCollection from './MongoCollection';
|
||||
import Parse from 'parse/node';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
function mongoFieldToParseSchemaField(type) {
|
||||
if (type[0] === '*') {
|
||||
@@ -15,31 +15,43 @@ function mongoFieldToParseSchemaField(type) {
|
||||
};
|
||||
}
|
||||
switch (type) {
|
||||
case 'number': return {type: 'Number'};
|
||||
case 'string': return {type: 'String'};
|
||||
case 'boolean': return {type: 'Boolean'};
|
||||
case 'date': return {type: 'Date'};
|
||||
case 'map':
|
||||
case 'object': return {type: 'Object'};
|
||||
case 'array': return {type: 'Array'};
|
||||
case 'geopoint': return {type: 'GeoPoint'};
|
||||
case 'file': return {type: 'File'};
|
||||
case 'bytes': return {type: 'Bytes'};
|
||||
case 'polygon': return {type: 'Polygon'};
|
||||
case 'number':
|
||||
return { type: 'Number' };
|
||||
case 'string':
|
||||
return { type: 'String' };
|
||||
case 'boolean':
|
||||
return { type: 'Boolean' };
|
||||
case 'date':
|
||||
return { type: 'Date' };
|
||||
case 'map':
|
||||
case 'object':
|
||||
return { type: 'Object' };
|
||||
case 'array':
|
||||
return { type: 'Array' };
|
||||
case 'geopoint':
|
||||
return { type: 'GeoPoint' };
|
||||
case 'file':
|
||||
return { type: 'File' };
|
||||
case 'bytes':
|
||||
return { type: 'Bytes' };
|
||||
case 'polygon':
|
||||
return { type: 'Polygon' };
|
||||
}
|
||||
}
|
||||
|
||||
const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions'];
|
||||
function mongoSchemaFieldsToParseSchemaFields(schema) {
|
||||
var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1);
|
||||
var fieldNames = Object.keys(schema).filter(
|
||||
key => nonFieldSchemaKeys.indexOf(key) === -1
|
||||
);
|
||||
var response = fieldNames.reduce((obj, fieldName) => {
|
||||
obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName])
|
||||
obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName]);
|
||||
return obj;
|
||||
}, {});
|
||||
response.ACL = {type: 'ACL'};
|
||||
response.createdAt = {type: 'Date'};
|
||||
response.updatedAt = {type: 'Date'};
|
||||
response.objectId = {type: 'String'};
|
||||
response.ACL = { type: 'ACL' };
|
||||
response.createdAt = { type: 'Date' };
|
||||
response.updatedAt = { type: 'Date' };
|
||||
response.objectId = { type: 'String' };
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -53,23 +65,23 @@ const emptyCLPS = Object.freeze({
|
||||
});
|
||||
|
||||
const defaultCLPS = Object.freeze({
|
||||
find: {'*': true},
|
||||
get: {'*': true},
|
||||
create: {'*': true},
|
||||
update: {'*': true},
|
||||
delete: {'*': true},
|
||||
addField: {'*': true},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
});
|
||||
|
||||
function mongoSchemaToParseSchema(mongoSchema) {
|
||||
let clps = defaultCLPS;
|
||||
let indexes = {}
|
||||
let indexes = {};
|
||||
if (mongoSchema._metadata) {
|
||||
if (mongoSchema._metadata.class_permissions) {
|
||||
clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions};
|
||||
clps = { ...emptyCLPS, ...mongoSchema._metadata.class_permissions };
|
||||
}
|
||||
if (mongoSchema._metadata.indexes) {
|
||||
indexes = {...mongoSchema._metadata.indexes};
|
||||
indexes = { ...mongoSchema._metadata.indexes };
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -90,23 +102,34 @@ function _mongoSchemaQueryFromNameQuery(name: string, query) {
|
||||
return object;
|
||||
}
|
||||
|
||||
|
||||
// Returns a type suitable for inserting into mongo _SCHEMA collection.
|
||||
// Does no validation. That is expected to be done in Parse Server.
|
||||
function parseFieldTypeToMongoFieldType({ type, targetClass }) {
|
||||
switch (type) {
|
||||
case 'Pointer': return `*${targetClass}`;
|
||||
case 'Relation': return `relation<${targetClass}>`;
|
||||
case 'Number': return 'number';
|
||||
case 'String': return 'string';
|
||||
case 'Boolean': return 'boolean';
|
||||
case 'Date': return 'date';
|
||||
case 'Object': return 'object';
|
||||
case 'Array': return 'array';
|
||||
case 'GeoPoint': return 'geopoint';
|
||||
case 'File': return 'file';
|
||||
case 'Bytes': return 'bytes';
|
||||
case 'Polygon': return 'polygon';
|
||||
case 'Pointer':
|
||||
return `*${targetClass}`;
|
||||
case 'Relation':
|
||||
return `relation<${targetClass}>`;
|
||||
case 'Number':
|
||||
return 'number';
|
||||
case 'String':
|
||||
return 'string';
|
||||
case 'Boolean':
|
||||
return 'boolean';
|
||||
case 'Date':
|
||||
return 'date';
|
||||
case 'Object':
|
||||
return 'object';
|
||||
case 'Array':
|
||||
return 'array';
|
||||
case 'GeoPoint':
|
||||
return 'geopoint';
|
||||
case 'File':
|
||||
return 'file';
|
||||
case 'Bytes':
|
||||
return 'bytes';
|
||||
case 'Polygon':
|
||||
return 'polygon';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,43 +141,60 @@ class MongoSchemaCollection {
|
||||
}
|
||||
|
||||
_fetchAllSchemasFrom_SCHEMA() {
|
||||
return this._collection._rawFind({})
|
||||
return this._collection
|
||||
._rawFind({})
|
||||
.then(schemas => schemas.map(mongoSchemaToParseSchema));
|
||||
}
|
||||
|
||||
_fetchOneSchemaFrom_SCHEMA(name: string) {
|
||||
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
|
||||
if (results.length === 1) {
|
||||
return mongoSchemaToParseSchema(results[0]);
|
||||
} else {
|
||||
throw undefined;
|
||||
}
|
||||
});
|
||||
return this._collection
|
||||
._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 })
|
||||
.then(results => {
|
||||
if (results.length === 1) {
|
||||
return mongoSchemaToParseSchema(results[0]);
|
||||
} else {
|
||||
throw undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Atomically find and delete an object based on query.
|
||||
findAndDeleteSchema(name: string) {
|
||||
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []);
|
||||
return this._collection._mongoCollection.findAndRemove(
|
||||
_mongoSchemaQueryFromNameQuery(name),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
insertSchema(schema: any) {
|
||||
return this._collection.insertOne(schema)
|
||||
return this._collection
|
||||
.insertOne(schema)
|
||||
.then(result => mongoSchemaToParseSchema(result.ops[0]))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { //Mongo's duplicate key error
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Class already exists.');
|
||||
if (error.code === 11000) {
|
||||
//Mongo's duplicate key error
|
||||
throw new Parse.Error(
|
||||
Parse.Error.DUPLICATE_VALUE,
|
||||
'Class already exists.'
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
updateSchema(name: string, update) {
|
||||
return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update);
|
||||
return this._collection.updateOne(
|
||||
_mongoSchemaQueryFromNameQuery(name),
|
||||
update
|
||||
);
|
||||
}
|
||||
|
||||
upsertSchema(name: string, query: string, update) {
|
||||
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
|
||||
return this._collection.upsertOne(
|
||||
_mongoSchemaQueryFromNameQuery(name, query),
|
||||
update
|
||||
);
|
||||
}
|
||||
|
||||
// Add a field to the schema. If database does not support the field
|
||||
@@ -170,34 +210,45 @@ class MongoSchemaCollection {
|
||||
// TODO: don't spend an extra query on finding the schema if the type we are trying to add isn't a GeoPoint.
|
||||
addFieldIfNotExists(className: string, fieldName: string, type: string) {
|
||||
return this._fetchOneSchemaFrom_SCHEMA(className)
|
||||
.then(schema => {
|
||||
// If a field with this name already exists, it will be handled elsewhere.
|
||||
if (schema.fields[fieldName] != undefined) {
|
||||
return;
|
||||
}
|
||||
// The schema exists. Check for existing GeoPoints.
|
||||
if (type.type === 'GeoPoint') {
|
||||
// Make sure there are not other geopoint fields
|
||||
if (Object.keys(schema.fields).some(existingField => schema.fields[existingField].type === 'GeoPoint')) {
|
||||
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.');
|
||||
.then(
|
||||
schema => {
|
||||
// If a field with this name already exists, it will be handled elsewhere.
|
||||
if (schema.fields[fieldName] != undefined) {
|
||||
return;
|
||||
}
|
||||
// The schema exists. Check for existing GeoPoints.
|
||||
if (type.type === 'GeoPoint') {
|
||||
// Make sure there are not other geopoint fields
|
||||
if (
|
||||
Object.keys(schema.fields).some(
|
||||
existingField =>
|
||||
schema.fields[existingField].type === 'GeoPoint'
|
||||
)
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
'MongoDB only supports one GeoPoint field in a class.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}, error => {
|
||||
// If error is undefined, the schema doesn't exist, and we can create the schema with the field.
|
||||
// If some other error, reject with it.
|
||||
if (error === undefined) {
|
||||
return;
|
||||
},
|
||||
error => {
|
||||
// If error is undefined, the schema doesn't exist, and we can create the schema with the field.
|
||||
// If some other error, reject with it.
|
||||
if (error === undefined) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
// We use $exists and $set to avoid overwriting the field type if it
|
||||
// already exists. (it could have added inbetween the last query and the update)
|
||||
// We use $exists and $set to avoid overwriting the field type if it
|
||||
// already exists. (it could have added inbetween the last query and the update)
|
||||
return this.upsertSchema(
|
||||
className,
|
||||
{ [fieldName]: { '$exists': false } },
|
||||
{ '$set' : { [fieldName]: parseFieldTypeToMongoFieldType(type) } }
|
||||
{ [fieldName]: { $exists: false } },
|
||||
{ $set: { [fieldName]: parseFieldTypeToMongoFieldType(type) } }
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -205,7 +256,7 @@ class MongoSchemaCollection {
|
||||
|
||||
// Exported for testing reasons and because we haven't moved all mongo schema format
|
||||
// related logic into the database adapter yet.
|
||||
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
|
||||
MongoSchemaCollection.parseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType
|
||||
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema;
|
||||
MongoSchemaCollection.parseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType;
|
||||
|
||||
export default MongoSchemaCollection
|
||||
export default MongoSchemaCollection;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
import MongoCollection from './MongoCollection';
|
||||
import MongoCollection from './MongoCollection';
|
||||
import MongoSchemaCollection from './MongoSchemaCollection';
|
||||
import { StorageAdapter } from '../StorageAdapter';
|
||||
import type { SchemaType,
|
||||
import { StorageAdapter } from '../StorageAdapter';
|
||||
import type {
|
||||
SchemaType,
|
||||
QueryType,
|
||||
StorageClass,
|
||||
QueryOptions } from '../StorageAdapter';
|
||||
QueryOptions,
|
||||
} from '../StorageAdapter';
|
||||
import {
|
||||
parse as parseUrl,
|
||||
format as formatUrl,
|
||||
@@ -19,11 +21,11 @@ import {
|
||||
transformPointerString,
|
||||
} from './MongoTransform';
|
||||
// @flow-disable-next
|
||||
import Parse from 'parse/node';
|
||||
import Parse from 'parse/node';
|
||||
// @flow-disable-next
|
||||
import _ from 'lodash';
|
||||
import defaults from '../../../defaults';
|
||||
import logger from '../../../logger';
|
||||
import _ from 'lodash';
|
||||
import defaults from '../../../defaults';
|
||||
import logger from '../../../logger';
|
||||
|
||||
// @flow-disable-next
|
||||
const mongodb = require('mongodb');
|
||||
@@ -33,7 +35,8 @@ const ReadPreference = mongodb.ReadPreference;
|
||||
const MongoSchemaCollectionName = '_SCHEMA';
|
||||
|
||||
const storageAdapterAllCollections = mongoAdapter => {
|
||||
return mongoAdapter.connect()
|
||||
return mongoAdapter
|
||||
.connect()
|
||||
.then(() => mongoAdapter.database.collections())
|
||||
.then(collections => {
|
||||
return collections.filter(collection => {
|
||||
@@ -42,12 +45,14 @@ const storageAdapterAllCollections = mongoAdapter => {
|
||||
}
|
||||
// TODO: If you have one app with a collection prefix that happens to be a prefix of another
|
||||
// apps prefix, this will go very very badly. We should fix that somehow.
|
||||
return (collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0);
|
||||
return (
|
||||
collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const convertParseSchemaToMongoSchema = ({...schema}) => {
|
||||
const convertParseSchemaToMongoSchema = ({ ...schema }) => {
|
||||
delete schema.fields._rperm;
|
||||
delete schema.fields._wperm;
|
||||
|
||||
@@ -60,11 +65,16 @@ const convertParseSchemaToMongoSchema = ({...schema}) => {
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns { code, error } if invalid, or { result }, an object
|
||||
// suitable for inserting into _SCHEMA collection, otherwise.
|
||||
const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions, indexes) => {
|
||||
const mongoSchemaFromFieldsAndClassNameAndCLP = (
|
||||
fields,
|
||||
className,
|
||||
classLevelPermissions,
|
||||
indexes
|
||||
) => {
|
||||
const mongoObject = {
|
||||
_id: className,
|
||||
objectId: 'string',
|
||||
@@ -74,7 +84,9 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
|
||||
};
|
||||
|
||||
for (const fieldName in fields) {
|
||||
mongoObject[fieldName] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]);
|
||||
mongoObject[
|
||||
fieldName
|
||||
] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]);
|
||||
}
|
||||
|
||||
if (typeof classLevelPermissions !== 'undefined') {
|
||||
@@ -86,18 +98,22 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
|
||||
}
|
||||
}
|
||||
|
||||
if (indexes && typeof indexes === 'object' && Object.keys(indexes).length > 0) {
|
||||
if (
|
||||
indexes &&
|
||||
typeof indexes === 'object' &&
|
||||
Object.keys(indexes).length > 0
|
||||
) {
|
||||
mongoObject._metadata = mongoObject._metadata || {};
|
||||
mongoObject._metadata.indexes = indexes;
|
||||
}
|
||||
|
||||
if (!mongoObject._metadata) { // cleanup the unused _metadata
|
||||
if (!mongoObject._metadata) {
|
||||
// cleanup the unused _metadata
|
||||
delete mongoObject._metadata;
|
||||
}
|
||||
|
||||
return mongoObject;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Private
|
||||
@@ -135,34 +151,40 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// encoded
|
||||
const encodedUri = formatUrl(parseUrl(this._uri));
|
||||
|
||||
this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(client => {
|
||||
// Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client
|
||||
// Fortunately, we can get back the options and use them to select the proper DB.
|
||||
// https://github.com/mongodb/node-mongodb-native/blob/2c35d76f08574225b8db02d7bef687123e6bb018/lib/mongo_client.js#L885
|
||||
const options = client.s.options;
|
||||
const database = client.db(options.dbName);
|
||||
if (!database) {
|
||||
delete this.connectionPromise;
|
||||
return;
|
||||
}
|
||||
database.on('error', () => {
|
||||
this.connectionPromise = MongoClient.connect(
|
||||
encodedUri,
|
||||
this._mongoOptions
|
||||
)
|
||||
.then(client => {
|
||||
// Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client
|
||||
// Fortunately, we can get back the options and use them to select the proper DB.
|
||||
// https://github.com/mongodb/node-mongodb-native/blob/2c35d76f08574225b8db02d7bef687123e6bb018/lib/mongo_client.js#L885
|
||||
const options = client.s.options;
|
||||
const database = client.db(options.dbName);
|
||||
if (!database) {
|
||||
delete this.connectionPromise;
|
||||
return;
|
||||
}
|
||||
database.on('error', () => {
|
||||
delete this.connectionPromise;
|
||||
});
|
||||
database.on('close', () => {
|
||||
delete this.connectionPromise;
|
||||
});
|
||||
this.client = client;
|
||||
this.database = database;
|
||||
})
|
||||
.catch(err => {
|
||||
delete this.connectionPromise;
|
||||
return Promise.reject(err);
|
||||
});
|
||||
database.on('close', () => {
|
||||
delete this.connectionPromise;
|
||||
});
|
||||
this.client = client;
|
||||
this.database = database;
|
||||
}).catch((err) => {
|
||||
delete this.connectionPromise;
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
handleError<T>(error: ?(Error | Parse.Error)): Promise<T> {
|
||||
if (error && error.code === 13) { // Unauthorized error
|
||||
if (error && error.code === 13) {
|
||||
// Unauthorized error
|
||||
delete this.client;
|
||||
delete this.database;
|
||||
delete this.connectionPromise;
|
||||
@@ -192,36 +214,55 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
|
||||
classExists(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.listCollections({ name: this._collectionPrefix + name }).toArray();
|
||||
}).then(collections => {
|
||||
return collections.length > 0;
|
||||
}).catch(err => this.handleError(err));
|
||||
return this.connect()
|
||||
.then(() => {
|
||||
return this.database
|
||||
.listCollections({ name: this._collectionPrefix + name })
|
||||
.toArray();
|
||||
})
|
||||
.then(collections => {
|
||||
return collections.length > 0;
|
||||
})
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
setClassLevelPermissions(className: string, CLPs: any): Promise<void> {
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.class_permissions': CLPs }
|
||||
})).catch(err => this.handleError(err));
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.class_permissions': CLPs },
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any = {}, fields: any): Promise<void> {
|
||||
setIndexesWithSchemaFormat(
|
||||
className: string,
|
||||
submittedIndexes: any,
|
||||
existingIndexes: any = {},
|
||||
fields: any
|
||||
): Promise<void> {
|
||||
if (submittedIndexes === undefined) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (Object.keys(existingIndexes).length === 0) {
|
||||
existingIndexes = { _id_: { _id: 1} };
|
||||
existingIndexes = { _id_: { _id: 1 } };
|
||||
}
|
||||
const deletePromises = [];
|
||||
const insertedIndexes = [];
|
||||
Object.keys(submittedIndexes).forEach(name => {
|
||||
const field = submittedIndexes[name];
|
||||
if (existingIndexes[name] && field.__op !== 'Delete') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Index ${name} exists, cannot update.`
|
||||
);
|
||||
}
|
||||
if (!existingIndexes[name] && field.__op === 'Delete') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} does not exist, cannot delete.`);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Index ${name} does not exist, cannot delete.`
|
||||
);
|
||||
}
|
||||
if (field.__op === 'Delete') {
|
||||
const promise = this.dropIndex(className, name);
|
||||
@@ -230,7 +271,10 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
} else {
|
||||
Object.keys(field).forEach(key => {
|
||||
if (!fields.hasOwnProperty(key)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Field ${key} does not exist, cannot add index.`
|
||||
);
|
||||
}
|
||||
});
|
||||
existingIndexes[name] = field;
|
||||
@@ -247,30 +291,34 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
return Promise.all(deletePromises)
|
||||
.then(() => insertPromise)
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.indexes': existingIndexes }
|
||||
}))
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.indexes': existingIndexes },
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
setIndexesFromMongo(className: string) {
|
||||
return this.getIndexes(className).then((indexes) => {
|
||||
indexes = indexes.reduce((obj, index) => {
|
||||
if (index.key._fts) {
|
||||
delete index.key._fts;
|
||||
delete index.key._ftsx;
|
||||
for (const field in index.weights) {
|
||||
index.key[field] = 'text';
|
||||
return this.getIndexes(className)
|
||||
.then(indexes => {
|
||||
indexes = indexes.reduce((obj, index) => {
|
||||
if (index.key._fts) {
|
||||
delete index.key._fts;
|
||||
delete index.key._ftsx;
|
||||
for (const field in index.weights) {
|
||||
index.key[field] = 'text';
|
||||
}
|
||||
}
|
||||
}
|
||||
obj[index.name] = index.key;
|
||||
return obj;
|
||||
}, {});
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.indexes': indexes }
|
||||
}));
|
||||
})
|
||||
obj[index.name] = index.key;
|
||||
return obj;
|
||||
}, {});
|
||||
return this._schemaCollection().then(schemaCollection =>
|
||||
schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.indexes': indexes },
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(err => this.handleError(err))
|
||||
.catch(() => {
|
||||
// Ignore if collection not found
|
||||
@@ -280,17 +328,33 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
|
||||
createClass(className: string, schema: SchemaType): Promise<void> {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes);
|
||||
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(
|
||||
schema.fields,
|
||||
className,
|
||||
schema.classLevelPermissions,
|
||||
schema.indexes
|
||||
);
|
||||
mongoObject._id = className;
|
||||
return this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields)
|
||||
return this.setIndexesWithSchemaFormat(
|
||||
className,
|
||||
schema.indexes,
|
||||
{},
|
||||
schema.fields
|
||||
)
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.insertSchema(mongoObject))
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
addFieldIfNotExists(className: string, fieldName: string, type: any): Promise<void> {
|
||||
addFieldIfNotExists(
|
||||
className: string,
|
||||
fieldName: string,
|
||||
type: any
|
||||
): Promise<void> {
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type))
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.addFieldIfNotExists(className, fieldName, type)
|
||||
)
|
||||
.then(() => this.createIndexesIfNeeded(className, fieldName, type))
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
@@ -298,24 +362,33 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
|
||||
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
|
||||
deleteClass(className: string) {
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.drop())
|
||||
.catch(error => {
|
||||
// 'ns not found' means collection was already gone. Ignore deletion attempt.
|
||||
if (error.message == 'ns not found') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
// We've dropped the collection, now remove the _SCHEMA document
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.findAndDeleteSchema(className))
|
||||
.catch(err => this.handleError(err));
|
||||
return (
|
||||
this._adaptiveCollection(className)
|
||||
.then(collection => collection.drop())
|
||||
.catch(error => {
|
||||
// 'ns not found' means collection was already gone. Ignore deletion attempt.
|
||||
if (error.message == 'ns not found') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
// We've dropped the collection, now remove the _SCHEMA document
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.findAndDeleteSchema(className)
|
||||
)
|
||||
.catch(err => this.handleError(err))
|
||||
);
|
||||
}
|
||||
|
||||
deleteAllClasses(fast: boolean) {
|
||||
return storageAdapterAllCollections(this)
|
||||
.then(collections => Promise.all(collections.map(collection => fast ? collection.remove({}) : collection.drop())));
|
||||
return storageAdapterAllCollections(this).then(collections =>
|
||||
Promise.all(
|
||||
collections.map(
|
||||
collection => (fast ? collection.remove({}) : collection.drop())
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the column and all the data. For Relations, the _Join collection is handled
|
||||
@@ -341,17 +414,17 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
deleteFields(className: string, schema: SchemaType, fieldNames: string[]) {
|
||||
const mongoFormatNames = fieldNames.map(fieldName => {
|
||||
if (schema.fields[fieldName].type === 'Pointer') {
|
||||
return `_p_${fieldName}`
|
||||
return `_p_${fieldName}`;
|
||||
} else {
|
||||
return fieldName;
|
||||
}
|
||||
});
|
||||
const collectionUpdate = { '$unset' : {} };
|
||||
const collectionUpdate = { $unset: {} };
|
||||
mongoFormatNames.forEach(name => {
|
||||
collectionUpdate['$unset'][name] = null;
|
||||
});
|
||||
|
||||
const schemaUpdate = { '$unset' : {} };
|
||||
const schemaUpdate = { $unset: {} };
|
||||
fieldNames.forEach(name => {
|
||||
schemaUpdate['$unset'][name] = null;
|
||||
});
|
||||
@@ -359,7 +432,9 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.updateMany({}, collectionUpdate))
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, schemaUpdate))
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.updateSchema(className, schemaUpdate)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -367,7 +442,10 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// schemas cannot be retrieved, returns a promise that rejects. Requirements for the
|
||||
// rejection reason are TBD.
|
||||
getAllClasses(): Promise<StorageClass[]> {
|
||||
return this._schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA())
|
||||
return this._schemaCollection()
|
||||
.then(schemasCollection =>
|
||||
schemasCollection._fetchAllSchemasFrom_SCHEMA()
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -376,7 +454,9 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// undefined as the reason.
|
||||
getClass(className: string): Promise<StorageClass> {
|
||||
return this._schemaCollection()
|
||||
.then(schemasCollection => schemasCollection._fetchOneSchemaFrom_SCHEMA(className))
|
||||
.then(schemasCollection =>
|
||||
schemasCollection._fetchOneSchemaFrom_SCHEMA(className)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -385,15 +465,25 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// the schema only for the legacy mongo format. We'll figure that out later.
|
||||
createObject(className: string, schema: SchemaType, object: any) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
|
||||
const mongoObject = parseObjectToMongoObjectForCreate(
|
||||
className,
|
||||
object,
|
||||
schema
|
||||
);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.insertOne(mongoObject))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { // Duplicate value
|
||||
const err = new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
|
||||
if (error.code === 11000) {
|
||||
// Duplicate value
|
||||
const err = new Parse.Error(
|
||||
Parse.Error.DUPLICATE_VALUE,
|
||||
'A duplicate value for a field with unique values was provided'
|
||||
);
|
||||
err.underlyingError = error;
|
||||
if (error.message) {
|
||||
const matches = error.message.match(/index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/);
|
||||
const matches = error.message.match(
|
||||
/index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/
|
||||
);
|
||||
if (matches && Array.isArray(matches)) {
|
||||
err.userInfo = { duplicated_field: matches[1] };
|
||||
}
|
||||
@@ -408,26 +498,44 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Remove all objects that match the given Parse Query.
|
||||
// If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
|
||||
// If there is some other error, reject with INTERNAL_SERVER_ERROR.
|
||||
deleteObjectsByQuery(className: string, schema: SchemaType, query: QueryType) {
|
||||
deleteObjectsByQuery(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => {
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
return collection.deleteMany(mongoWhere)
|
||||
return collection.deleteMany(mongoWhere);
|
||||
})
|
||||
.catch(err => this.handleError(err))
|
||||
.then(({ result }) => {
|
||||
if (result.n === 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
.then(
|
||||
({ result }) => {
|
||||
if (result.n === 0) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
() => {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
'Database adapter error'
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}, () => {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error');
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// Apply the update to all objects that match the given Parse Query.
|
||||
updateObjectsByQuery(className: string, schema: SchemaType, query: QueryType, update: any) {
|
||||
updateObjectsByQuery(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
@@ -438,16 +546,28 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
|
||||
// Atomically finds and updates an object based on query.
|
||||
// Return value not currently well specified.
|
||||
findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any) {
|
||||
findOneAndUpdate(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, { new: true }))
|
||||
.then(collection =>
|
||||
collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, {
|
||||
new: true,
|
||||
})
|
||||
)
|
||||
.then(result => mongoObjectToParseObject(className, result.value, schema))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) {
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.DUPLICATE_VALUE,
|
||||
'A duplicate value for a field with unique values was provided'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
@@ -455,7 +575,12 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
|
||||
// Hopefully we can get rid of this. It's only used for config and hooks.
|
||||
upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any) {
|
||||
upsertOneObject(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
@@ -465,32 +590,49 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
|
||||
// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
|
||||
find(className: string, schema: SchemaType, query: QueryType, { skip, limit, sort, keys, readPreference }: QueryOptions): Promise<any> {
|
||||
find(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
{ skip, limit, sort, keys, readPreference }: QueryOptions
|
||||
): Promise<any> {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
const mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
|
||||
const mongoKeys = _.reduce(keys, (memo, key) => {
|
||||
if (key === 'ACL') {
|
||||
memo['_rperm'] = 1;
|
||||
memo['_wperm'] = 1;
|
||||
} else {
|
||||
memo[transformKey(className, key, schema)] = 1;
|
||||
}
|
||||
return memo;
|
||||
}, {});
|
||||
const mongoSort = _.mapKeys(sort, (value, fieldName) =>
|
||||
transformKey(className, fieldName, schema)
|
||||
);
|
||||
const mongoKeys = _.reduce(
|
||||
keys,
|
||||
(memo, key) => {
|
||||
if (key === 'ACL') {
|
||||
memo['_rperm'] = 1;
|
||||
memo['_wperm'] = 1;
|
||||
} else {
|
||||
memo[transformKey(className, key, schema)] = 1;
|
||||
}
|
||||
return memo;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
return this.createTextIndexesIfNeeded(className, query, schema)
|
||||
.then(() => this._adaptiveCollection(className))
|
||||
.then(collection => collection.find(mongoWhere, {
|
||||
skip,
|
||||
limit,
|
||||
sort: mongoSort,
|
||||
keys: mongoKeys,
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
}))
|
||||
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
|
||||
.then(collection =>
|
||||
collection.find(mongoWhere, {
|
||||
skip,
|
||||
limit,
|
||||
sort: mongoSort,
|
||||
keys: mongoKeys,
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
})
|
||||
)
|
||||
.then(objects =>
|
||||
objects.map(object =>
|
||||
mongoObjectToParseObject(className, object, schema)
|
||||
)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -499,18 +641,29 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
|
||||
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
|
||||
// which is why we use sparse indexes.
|
||||
ensureUniqueness(className: string, schema: SchemaType, fieldNames: string[]) {
|
||||
ensureUniqueness(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
fieldNames: string[]
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const indexCreationRequest = {};
|
||||
const mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema));
|
||||
const mongoFieldNames = fieldNames.map(fieldName =>
|
||||
transformKey(className, fieldName, schema)
|
||||
);
|
||||
mongoFieldNames.forEach(fieldName => {
|
||||
indexCreationRequest[fieldName] = 1;
|
||||
});
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection._ensureSparseUniqueIndexInBackground(indexCreationRequest))
|
||||
.then(collection =>
|
||||
collection._ensureSparseUniqueIndexInBackground(indexCreationRequest)
|
||||
)
|
||||
.catch(error => {
|
||||
if (error.code === 11000) {
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Tried to ensure field uniqueness for a class that already has duplicates.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.DUPLICATE_VALUE,
|
||||
'Tried to ensure field uniqueness for a class that already has duplicates.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
@@ -519,33 +672,52 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
|
||||
// Used in tests
|
||||
_rawFind(className: string, query: QueryType) {
|
||||
return this._adaptiveCollection(className).then(collection => collection.find(query, {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
})).catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
// Executes a count.
|
||||
count(className: string, schema: SchemaType, query: QueryType, readPreference: ?string) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.count(transformWhere(className, query, schema), {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
}))
|
||||
.then(collection =>
|
||||
collection.find(query, {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string) {
|
||||
// Executes a count.
|
||||
count(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
readPreference: ?string
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const isPointerField = schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer';
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection =>
|
||||
collection.count(transformWhere(className, query, schema), {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
distinct(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
fieldName: string
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const isPointerField =
|
||||
schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer';
|
||||
if (isPointerField) {
|
||||
fieldName = `_p_${fieldName}`
|
||||
fieldName = `_p_${fieldName}`;
|
||||
}
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.distinct(fieldName, transformWhere(className, query, schema)))
|
||||
.then(collection =>
|
||||
collection.distinct(fieldName, transformWhere(className, query, schema))
|
||||
)
|
||||
.then(objects => {
|
||||
objects = objects.filter((obj) => obj != null);
|
||||
objects = objects.filter(obj => obj != null);
|
||||
return objects.map(object => {
|
||||
if (isPointerField) {
|
||||
const field = fieldName.substring(3);
|
||||
@@ -557,12 +729,21 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
aggregate(className: string, schema: any, pipeline: any, readPreference: ?string) {
|
||||
aggregate(
|
||||
className: string,
|
||||
schema: any,
|
||||
pipeline: any,
|
||||
readPreference: ?string
|
||||
) {
|
||||
let isPointerField = false;
|
||||
pipeline = pipeline.map((stage) => {
|
||||
pipeline = pipeline.map(stage => {
|
||||
if (stage.$group) {
|
||||
stage.$group = this._parseAggregateGroupArgs(schema, stage.$group);
|
||||
if (stage.$group._id && (typeof stage.$group._id === 'string') && stage.$group._id.indexOf('$_p_') >= 0) {
|
||||
if (
|
||||
stage.$group._id &&
|
||||
typeof stage.$group._id === 'string' &&
|
||||
stage.$group._id.indexOf('$_p_') >= 0
|
||||
) {
|
||||
isPointerField = true;
|
||||
}
|
||||
}
|
||||
@@ -570,13 +751,21 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
stage.$match = this._parseAggregateArgs(schema, stage.$match);
|
||||
}
|
||||
if (stage.$project) {
|
||||
stage.$project = this._parseAggregateProjectArgs(schema, stage.$project);
|
||||
stage.$project = this._parseAggregateProjectArgs(
|
||||
schema,
|
||||
stage.$project
|
||||
);
|
||||
}
|
||||
return stage;
|
||||
});
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS }))
|
||||
.then(collection =>
|
||||
collection.aggregate(pipeline, {
|
||||
readPreference,
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
if (error.code === 16006) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, error.message);
|
||||
@@ -598,7 +787,11 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
});
|
||||
return results;
|
||||
})
|
||||
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
|
||||
.then(objects =>
|
||||
objects.map(object =>
|
||||
mongoObjectToParseObject(className, object, schema)
|
||||
)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -623,7 +816,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// down a tree to find a "leaf node" and checking to see if it needs to be converted.
|
||||
_parseAggregateArgs(schema: any, pipeline: any): any {
|
||||
if (Array.isArray(pipeline)) {
|
||||
return pipeline.map((value) => this._parseAggregateArgs(schema, value));
|
||||
return pipeline.map(value => this._parseAggregateArgs(schema, value));
|
||||
} else if (typeof pipeline === 'object') {
|
||||
const returnValue = {};
|
||||
for (const field in pipeline) {
|
||||
@@ -632,12 +825,20 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Pass objects down to MongoDB...this is more than likely an $exists operator.
|
||||
returnValue[`_p_${field}`] = pipeline[field];
|
||||
} else {
|
||||
returnValue[`_p_${field}`] = `${schema.fields[field].targetClass}$${pipeline[field]}`;
|
||||
returnValue[`_p_${field}`] = `${schema.fields[field].targetClass}$${
|
||||
pipeline[field]
|
||||
}`;
|
||||
}
|
||||
} else if (schema.fields[field] && schema.fields[field].type === 'Date') {
|
||||
} else if (
|
||||
schema.fields[field] &&
|
||||
schema.fields[field].type === 'Date'
|
||||
) {
|
||||
returnValue[field] = this._convertToDate(pipeline[field]);
|
||||
} else {
|
||||
returnValue[field] = this._parseAggregateArgs(schema, pipeline[field]);
|
||||
returnValue[field] = this._parseAggregateArgs(
|
||||
schema,
|
||||
pipeline[field]
|
||||
);
|
||||
}
|
||||
|
||||
if (field === 'objectId') {
|
||||
@@ -690,11 +891,16 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// updatedAt or objectId and change it accordingly.
|
||||
_parseAggregateGroupArgs(schema: any, pipeline: any): any {
|
||||
if (Array.isArray(pipeline)) {
|
||||
return pipeline.map((value) => this._parseAggregateGroupArgs(schema, value));
|
||||
return pipeline.map(value =>
|
||||
this._parseAggregateGroupArgs(schema, value)
|
||||
);
|
||||
} else if (typeof pipeline === 'object') {
|
||||
const returnValue = {};
|
||||
for (const field in pipeline) {
|
||||
returnValue[field] = this._parseAggregateGroupArgs(schema, pipeline[field]);
|
||||
returnValue[field] = this._parseAggregateGroupArgs(
|
||||
schema,
|
||||
pipeline[field]
|
||||
);
|
||||
}
|
||||
return returnValue;
|
||||
} else if (typeof pipeline === 'string') {
|
||||
@@ -719,34 +925,37 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
const returnValue = {}
|
||||
const returnValue = {};
|
||||
for (const field in value) {
|
||||
returnValue[field] = this._convertToDate(value[field])
|
||||
returnValue[field] = this._convertToDate(value[field]);
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
_parseReadPreference(readPreference: ?string): ?string {
|
||||
switch (readPreference) {
|
||||
case 'PRIMARY':
|
||||
readPreference = ReadPreference.PRIMARY;
|
||||
break;
|
||||
case 'PRIMARY_PREFERRED':
|
||||
readPreference = ReadPreference.PRIMARY_PREFERRED;
|
||||
break;
|
||||
case 'SECONDARY':
|
||||
readPreference = ReadPreference.SECONDARY;
|
||||
break;
|
||||
case 'SECONDARY_PREFERRED':
|
||||
readPreference = ReadPreference.SECONDARY_PREFERRED;
|
||||
break;
|
||||
case 'NEAREST':
|
||||
readPreference = ReadPreference.NEAREST;
|
||||
break;
|
||||
case undefined:
|
||||
break;
|
||||
default:
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Not supported read preference.');
|
||||
case 'PRIMARY':
|
||||
readPreference = ReadPreference.PRIMARY;
|
||||
break;
|
||||
case 'PRIMARY_PREFERRED':
|
||||
readPreference = ReadPreference.PRIMARY_PREFERRED;
|
||||
break;
|
||||
case 'SECONDARY':
|
||||
readPreference = ReadPreference.SECONDARY;
|
||||
break;
|
||||
case 'SECONDARY_PREFERRED':
|
||||
readPreference = ReadPreference.SECONDARY_PREFERRED;
|
||||
break;
|
||||
case 'NEAREST':
|
||||
readPreference = ReadPreference.NEAREST;
|
||||
break;
|
||||
case undefined:
|
||||
break;
|
||||
default:
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'Not supported read preference.'
|
||||
);
|
||||
}
|
||||
return readPreference;
|
||||
}
|
||||
@@ -770,15 +979,19 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
createIndexesIfNeeded(className: string, fieldName: string, type: any) {
|
||||
if (type && type.type === 'Polygon') {
|
||||
const index = {
|
||||
[fieldName]: '2dsphere'
|
||||
[fieldName]: '2dsphere',
|
||||
};
|
||||
return this.createIndex(className, index);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
createTextIndexesIfNeeded(className: string, query: QueryType, schema: any): Promise<void> {
|
||||
for(const fieldName in query) {
|
||||
createTextIndexesIfNeeded(
|
||||
className: string,
|
||||
query: QueryType,
|
||||
schema: any
|
||||
): Promise<void> {
|
||||
for (const fieldName in query) {
|
||||
if (!query[fieldName] || !query[fieldName].$text) {
|
||||
continue;
|
||||
}
|
||||
@@ -791,15 +1004,20 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
const indexName = `${fieldName}_text`;
|
||||
const textIndex = {
|
||||
[indexName]: { [fieldName]: 'text' }
|
||||
[indexName]: { [fieldName]: 'text' },
|
||||
};
|
||||
return this.setIndexesWithSchemaFormat(className, textIndex, existingIndexes, schema.fields)
|
||||
.catch((error) => {
|
||||
if (error.code === 85) { // Index exist with different options
|
||||
return this.setIndexesFromMongo(className);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return this.setIndexesWithSchemaFormat(
|
||||
className,
|
||||
textIndex,
|
||||
existingIndexes,
|
||||
schema.fields
|
||||
).catch(error => {
|
||||
if (error.code === 85) {
|
||||
// Index exist with different options
|
||||
return this.setIndexesFromMongo(className);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -824,8 +1042,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
|
||||
updateSchemaWithIndexes(): Promise<any> {
|
||||
return this.getAllClasses()
|
||||
.then((classes) => {
|
||||
const promises = classes.map((schema) => {
|
||||
.then(classes => {
|
||||
const promises = classes.map(schema => {
|
||||
return this.setIndexesFromMongo(schema.className);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user