* Remove adaptiveCollection * Remove an adaptiveCollection use * Remove an adaptiveCollection * make adaptiveCollection private * Remove collection from mongoadapter * Move schema collection usage into mongo adapter * stop relying on mongo format for removing join tables * reduce usage of schemaCollection * remove uses of _collection * Move CLP setting into mongo adapter * remove all uses of schemaCollection * make schemaCollection private * remove transform from schemaCollection * rename some stuff * Tweak paramaters and stuff * reorder some params * reorder find() arguments * finishsh touching up argument order * Accept a database adapter as a parameter * First passing test with postgres! * Actually use the provided className * index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)" * Start dealing with test shittyness * Make specific server config for tests async * Fix email validation * Fix broken cloud code * Save callback to variable * undo * Fix tests * Setup travis * fix travis maybe * try removing db user * indentation? * remove postgres version setting * sudo maybe? * use postgres username * fix check for _PushStatus * excludes * remove db=mongo * allow postgres to fail * Fix allow failure * postgres 9.4 * Remove mongo implementations and fix test * Fix test leaving behind connections
231 lines
7.7 KiB
JavaScript
231 lines
7.7 KiB
JavaScript
|
||
import MongoCollection from './MongoCollection';
|
||
|
||
function mongoFieldToParseSchemaField(type) {
|
||
if (type[0] === '*') {
|
||
return {
|
||
type: 'Pointer',
|
||
targetClass: type.slice(1),
|
||
};
|
||
}
|
||
if (type.startsWith('relation<')) {
|
||
return {
|
||
type: 'Relation',
|
||
targetClass: type.slice('relation<'.length, type.length - 1),
|
||
};
|
||
}
|
||
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'};
|
||
}
|
||
}
|
||
|
||
const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions'];
|
||
function mongoSchemaFieldsToParseSchemaFields(schema) {
|
||
var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1);
|
||
var response = fieldNames.reduce((obj, fieldName) => {
|
||
obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName])
|
||
return obj;
|
||
}, {});
|
||
response.ACL = {type: 'ACL'};
|
||
response.createdAt = {type: 'Date'};
|
||
response.updatedAt = {type: 'Date'};
|
||
response.objectId = {type: 'String'};
|
||
return response;
|
||
}
|
||
|
||
const emptyCLPS = Object.freeze({
|
||
find: {},
|
||
get: {},
|
||
create: {},
|
||
update: {},
|
||
delete: {},
|
||
addField: {},
|
||
});
|
||
|
||
const defaultCLPS = Object.freeze({
|
||
find: {'*': true},
|
||
get: {'*': true},
|
||
create: {'*': true},
|
||
update: {'*': true},
|
||
delete: {'*': true},
|
||
addField: {'*': true},
|
||
});
|
||
|
||
function mongoSchemaToParseSchema(mongoSchema) {
|
||
let clps = defaultCLPS;
|
||
if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) {
|
||
clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions};
|
||
}
|
||
return {
|
||
className: mongoSchema._id,
|
||
fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema),
|
||
classLevelPermissions: clps,
|
||
};
|
||
}
|
||
|
||
function _mongoSchemaQueryFromNameQuery(name: string, query) {
|
||
return _mongoSchemaObjectFromNameFields(name, query);
|
||
}
|
||
|
||
function _mongoSchemaObjectFromNameFields(name: string, fields) {
|
||
let object = { _id: name };
|
||
if (fields) {
|
||
Object.keys(fields).forEach(key => {
|
||
object[key] = fields[key];
|
||
});
|
||
}
|
||
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';
|
||
}
|
||
}
|
||
|
||
// Returns { code, error } if invalid, or { result }, an object
|
||
// suitable for inserting into _SCHEMA collection, otherwise.
|
||
function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) {
|
||
|
||
let mongoObject = {
|
||
_id: className,
|
||
objectId: 'string',
|
||
updatedAt: 'string',
|
||
createdAt: 'string'
|
||
};
|
||
|
||
for (let fieldName in fields) {
|
||
mongoObject[fieldName] = parseFieldTypeToMongoFieldType(fields[fieldName]);
|
||
}
|
||
|
||
if (typeof classLevelPermissions !== 'undefined') {
|
||
mongoObject._metadata = mongoObject._metadata || {};
|
||
if (!classLevelPermissions) {
|
||
delete mongoObject._metadata.class_permissions;
|
||
} else {
|
||
mongoObject._metadata.class_permissions = classLevelPermissions;
|
||
}
|
||
}
|
||
|
||
return mongoObject;
|
||
}
|
||
|
||
class MongoSchemaCollection {
|
||
_collection: MongoCollection;
|
||
|
||
constructor(collection: MongoCollection) {
|
||
this._collection = collection;
|
||
}
|
||
|
||
_fetchAllSchemasFrom_SCHEMA() {
|
||
return this._collection._rawFind({})
|
||
.then(schemas => schemas.map(mongoSchemaToParseSchema));
|
||
}
|
||
|
||
_fechOneSchemaFrom_SCHEMA(name: string) {
|
||
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), []);
|
||
}
|
||
|
||
// Returns a promise that is expected to resolve with the newly created schema, in Parse format.
|
||
// If the class already exists, returns a promise that rejects with DUPLICATE_VALUE as the reason.
|
||
addSchema(name: string, fields, classLevelPermissions) {
|
||
let mongoSchema = mongoSchemaFromFieldsAndClassNameAndCLP(fields, name, classLevelPermissions);
|
||
let mongoObject = _mongoSchemaObjectFromNameFields(name, mongoSchema);
|
||
return this._collection.insertOne(mongoObject)
|
||
.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.');
|
||
} else {
|
||
throw error;
|
||
}
|
||
});
|
||
}
|
||
|
||
updateSchema(name: string, update) {
|
||
return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update);
|
||
}
|
||
|
||
upsertSchema(name: string, query: string, update) {
|
||
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
|
||
}
|
||
|
||
// Add a field to the schema. If database does not support the field
|
||
// type (e.g. mongo doesn't support more than one GeoPoint in a class) reject with an "Incorrect Type"
|
||
// Parse error with a desciptive message. If the field already exists, this function must
|
||
// not modify the schema, and must reject with DUPLICATE_VALUE error.
|
||
// If this is called for a class that doesn't exist, this function must create that class.
|
||
|
||
// TODO: throw an error if an unsupported field type is passed. Deciding whether a type is supported
|
||
// should be the job of the adapter. Some adapters may not support GeoPoint at all. Others may
|
||
// Support additional types that Mongo doesn't, like Money, or something.
|
||
|
||
// 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._fechOneSchemaFrom_SCHEMA(className)
|
||
.then(schema => {
|
||
// 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;
|
||
}
|
||
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)
|
||
return this.upsertSchema(
|
||
className,
|
||
{ [fieldName]: { '$exists': false } },
|
||
{ '$set' : { [fieldName]: parseFieldTypeToMongoFieldType(type) } }
|
||
);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Exported for testing reasons and because we haven't moved all mongo schema format
|
||
// related logic into the database adapter yet.
|
||
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
|
||
|
||
export default MongoSchemaCollection
|