Files
kami-parse-server/src/Adapters/Storage/Mongo/MongoSchemaCollection.js
2016-12-07 18:17:05 -05:00

186 lines
6.1 KiB
JavaScript

import MongoCollection from './MongoCollection';
import Parse from 'parse/node';
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) {
const object = { _id: name };
if (query) {
Object.keys(query).forEach(key => {
object[key] = query[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';
}
}
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), []);
}
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
MongoSchemaCollection.parseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType
export default MongoSchemaCollection