Progres towards moving mongo logic into its adapter

This commit is contained in:
Drew
2016-04-05 21:16:39 -07:00
parent cbbd66964a
commit 91ace4e718
9 changed files with 387 additions and 271 deletions

View File

@@ -27,6 +27,7 @@
"deepcopy": "^0.6.1", "deepcopy": "^0.6.1",
"express": "^4.13.4", "express": "^4.13.4",
"intersect": "^1.0.1", "intersect": "^1.0.1",
"lodash": "^4.8.2",
"lru-cache": "^4.0.0", "lru-cache": "^4.0.0",
"mailgun-js": "^0.7.7", "mailgun-js": "^0.7.7",
"mime": "^1.3.4", "mime": "^1.3.4",

View File

@@ -10,7 +10,7 @@ MockController.prototype = Object.create(AdaptableController.prototype);
MockController.prototype.constructor = AdaptableController; MockController.prototype.constructor = AdaptableController;
describe("AdaptableController", ()=>{ describe("AdaptableController", ()=>{
it("should use the provided adapter", (done) => { it("should use the provided adapter", (done) => {
var adapter = new FilesAdapter(); var adapter = new FilesAdapter();
var controller = new FilesController(adapter); var controller = new FilesController(adapter);
@@ -22,7 +22,7 @@ describe("AdaptableController", ()=>{
expect(controller.adapter).toBe(adapter); expect(controller.adapter).toBe(adapter);
done(); done();
}); });
it("should throw when creating a new mock controller", (done) => { it("should throw when creating a new mock controller", (done) => {
var adapter = new FilesAdapter(); var adapter = new FilesAdapter();
expect(() => { expect(() => {
@@ -30,7 +30,7 @@ describe("AdaptableController", ()=>{
}).toThrow(); }).toThrow();
done(); done();
}); });
it("should fail setting the wrong adapter to the controller", (done) => { it("should fail setting the wrong adapter to the controller", (done) => {
function WrongAdapter() {}; function WrongAdapter() {};
var adapter = new FilesAdapter(); var adapter = new FilesAdapter();
@@ -41,7 +41,7 @@ describe("AdaptableController", ()=>{
}).toThrow(); }).toThrow();
done(); done();
}); });
it("should fail to instantiate a controller with wrong adapter", (done) => { it("should fail to instantiate a controller with wrong adapter", (done) => {
function WrongAdapter() {}; function WrongAdapter() {};
var adapter = new WrongAdapter(); var adapter = new WrongAdapter();
@@ -50,14 +50,14 @@ describe("AdaptableController", ()=>{
}).toThrow(); }).toThrow();
done(); done();
}); });
it("should fail to instantiate a controller without an adapter", (done) => { it("should fail to instantiate a controller without an adapter", (done) => {
expect(() => { expect(() => {
new FilesController(); new FilesController();
}).toThrow(); }).toThrow();
done(); done();
}); });
it("should accept an object adapter", (done) => { it("should accept an object adapter", (done) => {
var adapter = { var adapter = {
createFile: function(config, filename, data) { }, createFile: function(config, filename, data) { },
@@ -70,18 +70,18 @@ describe("AdaptableController", ()=>{
}).not.toThrow(); }).not.toThrow();
done(); done();
}); });
it("should accept an object adapter", (done) => { it("should accept an object adapter", (done) => {
function AGoodAdapter() {}; function AGoodAdapter() {};
AGoodAdapter.prototype.createFile = function(config, filename, data) { }; AGoodAdapter.prototype.createFile = function(config, filename, data) { };
AGoodAdapter.prototype.deleteFile = function(config, filename) { }; AGoodAdapter.prototype.deleteFile = function(config, filename) { };
AGoodAdapter.prototype.getFileData = function(config, filename) { }; AGoodAdapter.prototype.getFileData = function(config, filename) { };
AGoodAdapter.prototype.getFileLocation = function(config, filename) { }; AGoodAdapter.prototype.getFileLocation = function(config, filename) { };
var adapter = new AGoodAdapter(); var adapter = new AGoodAdapter();
expect(() => { expect(() => {
new FilesController(adapter); new FilesController(adapter);
}).not.toThrow(); }).not.toThrow();
done(); done();
}); });
}); });

View File

@@ -0,0 +1,55 @@
'use strict';
const MongoSchemaCollection = require('../src/Adapters/Storage/Mongo/MongoSchemaCollection').default;
describe('MongoSchemaCollection', () => {
it('can transform legacy _client_permissions keys to parse format', done => {
expect(MongoSchemaCollection._TESTmongoSchemaToParseSchema({
"_id":"_Installation",
"_client_permissions":{
"get":true,
"find":true,
"update":true,
"create":true,
"delete":true,
},
"_metadata":{
"class_permissions":{
"get":{"*":true},
"find":{"*":true},
"update":{"*":true},
"create":{"*":true},
"delete":{"*":true},
"addField":{"*":true},
}
},
"installationId":"string",
"deviceToken":"string",
"deviceType":"string",
"channels":"array",
"user":"*_User",
})).toEqual({
className: '_Installation',
fields: {
installationId: { type: 'String' },
deviceToken: { type: 'String' },
deviceType: { type: 'String' },
channels: { type: 'Array' },
user: { type: 'Pointer', targetClass: '_User' },
ACL: { type: 'ACL' },
createdAt: { type: 'Date' },
updatedAt: { type: 'Date' },
objectId: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
}
});
done();
});
});

View File

@@ -242,22 +242,21 @@ describe('OAuth', function() {
it("should only create a single user with REST API", (done) => { it("should only create a single user with REST API", (done) => {
var objectId; var objectId;
createOAuthUser((error, response, body) => { createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
objectId = b.objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null); expect(error).toBe(null);
var b = JSON.parse(body); var b = JSON.parse(body);
expect(b.objectId).not.toBeNull(); expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined(); expect(b.objectId).not.toBeUndefined();
objectId = b.objectId; expect(b.objectId).toBe(objectId);
done();
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
expect(b.objectId).toBe(objectId);
done();
});
}); });
});
}); });
it("unlink and link with custom provider", (done) => { it("unlink and link with custom provider", (done) => {

View File

@@ -163,14 +163,26 @@ describe('Schema', () => {
.then(schema => schema.addClassIfNotExists('NewClass', { .then(schema => schema.addClassIfNotExists('NewClass', {
foo: {type: 'String'} foo: {type: 'String'}
})) }))
.then(result => { .then(actualSchema => {
expect(result).toEqual({ const expectedSchema = {
_id: 'NewClass', className: 'NewClass',
objectId: 'string', fields: {
updatedAt: 'string', objectId: { type: 'String' },
createdAt: 'string', updatedAt: { type: 'Date' },
foo: 'string', createdAt: { type: 'Date' },
}) ACL: { type: 'ACL' },
foo: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done(); done();
}) })
.catch(error => { .catch(error => {
@@ -201,15 +213,27 @@ describe('Schema', () => {
.then(schema => { .then(schema => {
var p1 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}}); var p1 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
var p2 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}}); var p2 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}});
Promise.race([p1, p2]) //Use race because we expect the first completed promise to be the successful one Promise.race([p1, p2])
.then(response => { .then(actualSchema => {
expect(response).toEqual({ const expectedSchema = {
_id: 'NewClass', className: 'NewClass',
objectId: 'string', fields: {
updatedAt: 'string', objectId: { type: 'String' },
createdAt: 'string', updatedAt: { type: 'Date' },
foo: 'string', createdAt: { type: 'Date' },
}); ACL: { type: 'ACL' },
foo: { type: 'String' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
}); });
Promise.all([p1,p2]) Promise.all([p1,p2])
.catch(error => { .catch(error => {
@@ -373,23 +397,36 @@ describe('Schema', () => {
aPointer: {type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet'}, aPointer: {type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet'},
aRelation: {type: 'Relation', targetClass: 'NewClass'}, aRelation: {type: 'Relation', targetClass: 'NewClass'},
})) }))
.then(mongoObj => { .then(actualSchema => {
expect(mongoObj).toEqual({ const expectedSchema = {
_id: 'NewClass', className: 'NewClass',
objectId: 'string', fields: {
createdAt: 'string', objectId: { type: 'String' },
updatedAt: 'string', updatedAt: { type: 'Date' },
aNumber: 'number', createdAt: { type: 'Date' },
aString: 'string', ACL: { type: 'ACL' },
aBool: 'boolean', aString: { type: 'String' },
aDate: 'date', aNumber: { type: 'Number' },
aObject: 'object', aString: { type: 'String' },
aArray: 'array', aBool: { type: 'Boolean' },
aGeoPoint: 'geopoint', aDate: { type: 'Date' },
aFile: 'file', aObject: { type: 'Object' },
aPointer: '*ThisClassDoesNotExistYet', aArray: { type: 'Array' },
aRelation: 'relation<NewClass>', aGeoPoint: { type: 'GeoPoint' },
}); aFile: { type: 'File' },
aPointer: { type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet' },
aRelation: { type: 'Relation', targetClass: 'NewClass' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done(); done();
}); });
}); });
@@ -399,23 +436,35 @@ describe('Schema', () => {
.then(schema => schema.addClassIfNotExists('_Installation', { .then(schema => schema.addClassIfNotExists('_Installation', {
foo: {type: 'Number'}, foo: {type: 'Number'},
})) }))
.then(mongoObj => { .then(actualSchema => {
expect(mongoObj).toEqual({ const expectedSchema = {
_id: '_Installation', className: '_Installation',
createdAt: 'string', fields: {
updatedAt: 'string', objectId: { type: 'String' },
objectId: 'string', updatedAt: { type: 'Date' },
foo: 'number', createdAt: { type: 'Date' },
installationId: 'string', ACL: { type: 'ACL' },
deviceToken: 'string', foo: { type: 'Number' },
channels: 'array', installationId: { type: 'String' },
deviceType: 'string', deviceToken: { type: 'String' },
pushType: 'string', channels: { type: 'Array' },
GCMSenderId: 'string', deviceType: { type: 'String' },
timeZone: 'string', pushType: { type: 'String' },
localeIdentifier: 'string', GCMSenderId: { type: 'String' },
badge: 'number', timeZone: { type: 'String' },
}); localeIdentifier: { type: 'String' },
badge: { type: 'Number' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
}
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done(); done();
}); });
}); });
@@ -423,16 +472,28 @@ describe('Schema', () => {
it('creates non-custom classes which include relation field', done => { it('creates non-custom classes which include relation field', done => {
config.database.loadSchema() config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('_Role', {})) .then(schema => schema.addClassIfNotExists('_Role', {}))
.then(mongoObj => { .then(actualSchema => {
expect(mongoObj).toEqual({ const expectedSchema = {
_id: '_Role', className: '_Role',
createdAt: 'string', fields: {
updatedAt: 'string', objectId: { type: 'String' },
objectId: 'string', updatedAt: { type: 'Date' },
name: 'string', createdAt: { type: 'Date' },
users: 'relation<_User>', ACL: { type: 'ACL' },
roles: 'relation<_Role>', name: { type: 'String' },
}); users: { type: 'Relation', targetClass: '_User' },
roles: { type: 'Relation', targetClass: '_Role' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done(); done();
}); });
}); });
@@ -440,19 +501,31 @@ describe('Schema', () => {
it('creates non-custom classes which include pointer field', done => { it('creates non-custom classes which include pointer field', done => {
config.database.loadSchema() config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('_Session', {})) .then(schema => schema.addClassIfNotExists('_Session', {}))
.then(mongoObj => { .then(actualSchema => {
expect(mongoObj).toEqual({ const expectedSchema = {
_id: '_Session', className: '_Session',
createdAt: 'string', fields: {
updatedAt: 'string', objectId: { type: 'String' },
objectId: 'string', updatedAt: { type: 'Date' },
restricted: 'boolean', createdAt: { type: 'Date' },
user: '*_User', restricted: { type: 'Boolean' },
installationId: 'string', user: { type: 'Pointer', targetClass: '_User' },
sessionToken: 'string', installationId: { type: 'String' },
expiresAt: 'date', sessionToken: { type: 'String' },
createdWith: 'object' expiresAt: { type: 'Date' },
}); createdWith: { type: 'Object' },
ACL: { type: 'ACL' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
done(); done();
}); });
}); });
@@ -583,14 +656,26 @@ describe('Schema', () => {
schema.addClassIfNotExists('NewClass', { schema.addClassIfNotExists('NewClass', {
relationField: {type: 'Relation', targetClass: '_User'} relationField: {type: 'Relation', targetClass: '_User'}
}) })
.then(mongoObj => { .then(actualSchema => {
expect(mongoObj).toEqual({ const expectedSchema = {
_id: 'NewClass', className: 'NewClass',
objectId: 'string', fields: {
updatedAt: 'string', objectId: { type: 'String' },
createdAt: 'string', updatedAt: { type: 'Date' },
relationField: 'relation<_User>', createdAt: { type: 'Date' },
}); ACL: { type: 'ACL' },
relationField: { type: 'Relation', targetClass: '_User' },
},
classLevelPermissions: {
find: { '*': true },
get: { '*': true },
create: { '*': true },
update: { '*': true },
delete: { '*': true },
addField: { '*': true },
},
};
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
}) })
.then(() => config.database.collectionExists('_Join:relationField:NewClass')) .then(() => config.database.collectionExists('_Join:relationField:NewClass'))
.then(exist => { .then(exist => {
@@ -703,33 +788,4 @@ describe('Schema', () => {
}); });
done(); done();
}); });
it('handles legacy _client_permissions keys without crashing', done => {
Schema.mongoSchemaToSchemaAPIResponse({
"_id":"_Installation",
"_client_permissions":{
"get":true,
"find":true,
"update":true,
"create":true,
"delete":true,
},
"_metadata":{
"class_permissions":{
"get":{"*":true},
"find":{"*":true},
"update":{"*":true},
"create":{"*":true},
"delete":{"*":true},
"addField":{"*":true},
}
},
"installationId":"string",
"deviceToken":"string",
"deviceType":"string",
"channels":"array",
"user":"*_User",
});
done();
});
}); });

View File

@@ -1,6 +1,67 @@
import MongoCollection from './MongoCollection'; 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'};
}
}
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 defaultCLPS = Object.freeze({
find: {'*': true},
get: {'*': true},
create: {'*': true},
update: {'*': true},
delete: {'*': true},
addField: {'*': true},
});
function mongoSchemaToParseSchema(mongoSchema) {
let clpsFromMongoObject = {};
if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) {
clpsFromMongoObject = mongoSchema._metadata.class_permissions;
}
return {
className: mongoSchema._id,
fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema),
classLevelPermissions: {...defaultCLPS, ...clpsFromMongoObject},
};
}
function _mongoSchemaQueryFromNameQuery(name: string, query) { function _mongoSchemaQueryFromNameQuery(name: string, query) {
return _mongoSchemaObjectFromNameFields(name, query); return _mongoSchemaObjectFromNameFields(name, query);
} }
@@ -15,20 +76,31 @@ function _mongoSchemaObjectFromNameFields(name: string, fields) {
return object; return object;
} }
export default class MongoSchemaCollection { class MongoSchemaCollection {
_collection: MongoCollection; _collection: MongoCollection;
constructor(collection: MongoCollection) { constructor(collection: MongoCollection) {
this._collection = collection; this._collection = collection;
} }
// Return a promise for all schemas known to this adapter, in Parse format. In case the
// schemas cannot be retrieved, returns a promise that rejects. Requirements fot the
// rejection reason are TBD.
getAllSchemas() { getAllSchemas() {
return this._collection._rawFind({}); return this._collection._rawFind({})
.then(schemas => schemas.map(mongoSchemaToParseSchema));
} }
// Return a promise for the schema with the given name, in Parse format. If
// this adapter doesn't know about the schema, return a promise that rejects with
// undefined as the reason.
findSchema(name: string) { findSchema(name: string) {
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => { return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
return results[0]; if (results.length === 1) {
return mongoSchemaToParseSchema(results[0]);
} else {
return Promise.reject();
}
}); });
} }
@@ -56,3 +128,13 @@ export default class MongoSchemaCollection {
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update); return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
} }
} }
// Exported for testing reasons and because we haven't moved all mongo schema format
// related logic into the database adapter yet.
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
// Exported because we haven't moved all mongo schema format related logic
// into the database adapter yet. We will remove this before too long.
MongoSchemaCollection._DONOTUSEmongoFieldToParseSchemaField = mongoFieldToParseSchemaField
export default MongoSchemaCollection

View File

@@ -17,7 +17,6 @@ function classNameMismatchResponse(bodyClass, pathClass) {
function getAllSchemas(req) { function getAllSchemas(req) {
return req.config.database.schemaCollection() return req.config.database.schemaCollection()
.then(collection => collection.getAllSchemas()) .then(collection => collection.getAllSchemas())
.then(schemas => schemas.map(Schema.mongoSchemaToSchemaAPIResponse))
.then(schemas => ({ response: { results: schemas } })); .then(schemas => ({ response: { results: schemas } }));
} }
@@ -25,11 +24,13 @@ function getOneSchema(req) {
const className = req.params.className; const className = req.params.className;
return req.config.database.schemaCollection() return req.config.database.schemaCollection()
.then(collection => collection.findSchema(className)) .then(collection => collection.findSchema(className))
.then(mongoSchema => { .then(schema => ({ response: schema }))
if (!mongoSchema) { .catch(error => {
if (error === undefined) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
} else {
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error.');
} }
return { response: Schema.mongoSchemaToSchemaAPIResponse(mongoSchema) };
}); });
} }
@@ -47,7 +48,7 @@ function createSchema(req) {
return req.config.database.loadSchema() return req.config.database.loadSchema()
.then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions)) .then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions))
.then(result => ({ response: Schema.mongoSchemaToSchemaAPIResponse(result) })); .then(schema => ({ response: schema }));
} }
function modifySchema(req) { function modifySchema(req) {
@@ -55,8 +56,8 @@ function modifySchema(req) {
return classNameMismatchResponse(req.body.className, req.params.className); return classNameMismatchResponse(req.body.className, req.params.className);
} }
var submittedFields = req.body.fields || {}; let submittedFields = req.body.fields || {};
var className = req.params.className; let className = req.params.className;
return req.config.database.loadSchema() return req.config.database.loadSchema()
.then(schema => { .then(schema => {

View File

@@ -16,6 +16,8 @@
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;
var transform = require('./transform'); var transform = require('./transform');
import MongoSchemaCollection from './Adapters/Storage/Mongo/MongoSchemaCollection';
import _ from 'lodash';
const defaultColumns = Object.freeze({ const defaultColumns = Object.freeze({
// Contain the default columns for every parse object type (except _Join collection) // Contain the default columns for every parse object type (except _Join collection)
@@ -113,15 +115,6 @@ function verifyPermissionKey(key) {
} }
const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField']); const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField']);
let DefaultClassLevelPermissions = () => {
return CLPValidKeys.reduce((perms, key) => {
perms[key] = {
'*': true
};
return perms;
}, {});
}
function validateCLP(perms) { function validateCLP(perms) {
if (!perms) { if (!perms) {
return; return;
@@ -220,11 +213,8 @@ function schemaAPITypeToMongoFieldType(type) {
} }
} }
// Create a schema from a Mongo collection and the exported schema format. // Stores the entire schema of the app in a weird hybrid format somewhere between
// mongoSchema should be a list of objects, each with: // the mongo format and the Parse format. Soon, this will all be Parse format.
// '_id' indicates the className
// '_metadata' is ignored for now
// Everything else is expected to be a userspace field.
class Schema { class Schema {
_collection; _collection;
data; data;
@@ -233,7 +223,8 @@ class Schema {
constructor(collection) { constructor(collection) {
this._collection = collection; this._collection = collection;
// this.data[className][fieldName] tells you the type of that field // this.data[className][fieldName] tells you the type of that field, in mongo format
// TODO: use Parse format
this.data = {}; this.data = {};
// this.perms[className][operation] tells you the acl-style permissions // this.perms[className][operation] tells you the acl-style permissions
this.perms = {}; this.perms = {};
@@ -242,43 +233,24 @@ class Schema {
reloadData() { reloadData() {
this.data = {}; this.data = {};
this.perms = {}; this.perms = {};
return this._collection.getAllSchemas().then(results => { return this._collection.getAllSchemas().then(allSchemas => {
for (let obj of results) { allSchemas.forEach(schema => {
let className = null; const parseFormatSchema = {
let classData = {}; ...defaultColumns._Default,
let permsData = null; ...(defaultColumns[schema.className] || {}),
Object.keys(obj).forEach(key => { ...schema.fields,
let value = obj[key];
switch (key) {
case '_id':
className = value;
break;
case '_metadata':
if (value && value['class_permissions']) {
permsData = value['class_permissions'];
}
break;
default:
classData[key] = value;
}
});
if (className) {
// merge with the default schema
let defaultClassData = Object.assign({}, defaultColumns._Default, defaultColumns[className]);
defaultClassData = Object.keys(defaultClassData).reduce((memo, key) => {
let type = schemaAPITypeToMongoFieldType(defaultClassData[key]).result;
if (type) {
memo[key] = type;
}
return memo;
}, {});
classData = Object.assign({}, defaultClassData, classData);
this.data[className] = classData;
if (permsData) {
this.perms[className] = permsData;
}
} }
} // ACL doesn't show up in mongo, it's implicit
delete parseFormatSchema.ACL;
// createdAt and updatedAt are wacky and have legacy baggage
parseFormatSchema.createdAt = { type: 'String' };
parseFormatSchema.updatedAt = { type: 'String' };
this.data[schema.className] = _.mapValues(parseFormatSchema, parseField =>
schemaAPITypeToMongoFieldType(parseField).result
);
this.perms[schema.className] = schema.classLevelPermissions;
});
}); });
} }
@@ -300,7 +272,8 @@ class Schema {
} }
return this._collection.addSchema(className, mongoObject.result) return this._collection.addSchema(className, mongoObject.result)
.then(result => result.ops[0]) //TODO: Move this logic into the database adapter
.then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(result.ops[0]))
.catch(error => { .catch(error => {
if (error.code === 11000) { //Mongo's duplicate key error if (error.code === 11000) { //Mongo's duplicate key error
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
@@ -354,7 +327,8 @@ class Schema {
.then(() => { .then(() => {
return this.setPermissions(className, classLevelPermissions) return this.setPermissions(className, classLevelPermissions)
}) })
.then(() => { return mongoSchemaToSchemaAPIResponse(mongoObject.result) }); //TODO: Move this logic into the database adapter
.then(() => MongoSchemaCollection._TESTmongoSchemaToParseSchema(mongoObject.result));
} }
@@ -384,7 +358,7 @@ class Schema {
'schema is frozen, cannot add: ' + className); 'schema is frozen, cannot add: ' + className);
} }
// We don't have this class. Update the schema // We don't have this class. Update the schema
return this._collection.addSchema(className).then(() => { return this.addClassIfNotExists(className, []).then(() => {
// The schema update succeeded. Reload the schema // The schema update succeeded. Reload the schema
return this.reloadData(); return this.reloadData();
}, () => { }, () => {
@@ -421,20 +395,20 @@ class Schema {
} }
// Returns a promise that resolves successfully to the new schema // Returns a promise that resolves successfully to the new schema
// object if the provided className-key-type tuple is valid. // object if the provided className-fieldName-type tuple is valid.
// The className must already be validated. // The className must already be validated.
// If 'freeze' is true, refuse to update the schema for this field. // If 'freeze' is true, refuse to update the schema for this field.
validateField(className, key, type, freeze) { validateField(className, fieldName, type, freeze) {
// Just to check that the key is valid // Just to check that the fieldName is valid
transform.transformKey(this, className, key); transform.transformKey(this, className, fieldName);
if( key.indexOf(".") > 0 ) { if( fieldName.indexOf(".") > 0 ) {
// subdocument key (x.y) => ok if x is of type 'object' // subdocument key (x.y) => ok if x is of type 'object'
key = key.split(".")[ 0 ]; fieldName = fieldName.split(".")[ 0 ];
type = 'object'; type = 'object';
} }
var expected = this.data[className][key]; let expected = this.data[className][fieldName];
if (expected) { if (expected) {
expected = (expected === 'map' ? 'object' : expected); expected = (expected === 'map' ? 'object' : expected);
if (expected === type) { if (expected === type) {
@@ -442,14 +416,13 @@ class Schema {
} else { } else {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.INCORRECT_TYPE, Parse.Error.INCORRECT_TYPE,
'schema mismatch for ' + className + '.' + key + `schema mismatch for ${className}.${fieldName}; expected ${expected} but got ${type}`
'; expected ' + expected + ' but got ' + type); );
} }
} }
if (freeze) { if (freeze) {
throw new Parse.Error(Parse.Error.INVALID_JSON, throw new Parse.Error(Parse.Error.INVALID_JSON, `schema is frozen, cannot add ${fieldName} field`);
'schema is frozen, cannot add ' + key + ' field');
} }
// We don't have this field, but if the value is null or undefined, // We don't have this field, but if the value is null or undefined,
@@ -473,9 +446,9 @@ class Schema {
// Note that we use the $exists guard and $set to avoid race // Note that we use the $exists guard and $set to avoid race
// conditions in the database. This is important! // conditions in the database. This is important!
let query = {}; let query = {};
query[key] = { '$exists': false }; query[fieldName] = { '$exists': false };
var update = {}; var update = {};
update[key] = type; update[fieldName] = type;
update = {'$set': update}; update = {'$set': update};
return this._collection.upsertSchema(className, query, update).then(() => { return this._collection.upsertSchema(className, query, update).then(() => {
// The update succeeded. Reload the schema // The update succeeded. Reload the schema
@@ -487,7 +460,7 @@ class Schema {
return this.reloadData(); return this.reloadData();
}).then(() => { }).then(() => {
// Ensure that the schema now validates // Ensure that the schema now validates
return this.validateField(className, key, type, true); return this.validateField(className, fieldName, type, true);
}, (error) => { }, (error) => {
// The schema still doesn't validate. Give up // The schema still doesn't validate. Give up
throw new Parse.Error(Parse.Error.INVALID_JSON, throw new Parse.Error(Parse.Error.INVALID_JSON,
@@ -557,11 +530,11 @@ class Schema {
validateObject(className, object, query) { validateObject(className, object, query) {
var geocount = 0; var geocount = 0;
var promise = this.validateClassName(className); var promise = this.validateClassName(className);
for (var key in object) { for (let fieldName in object) {
if (object[key] === undefined) { if (object[fieldName] === undefined) {
continue; continue;
} }
var expected = getType(object[key]); var expected = getType(object[fieldName]);
if (expected === 'geopoint') { if (expected === 'geopoint') {
geocount++; geocount++;
} }
@@ -576,7 +549,12 @@ class Schema {
if (!expected) { if (!expected) {
continue; continue;
} }
promise = thenValidateField(promise, className, key, expected); if (fieldName === 'ACL') {
// Every object has ACL implicitly.
continue;
}
promise = thenValidateField(promise, className, fieldName, expected);
} }
promise = thenValidateRequiredColumns(promise, className, object, query); promise = thenValidateRequiredColumns(promise, className, object, query);
return promise; return promise;
@@ -735,32 +713,6 @@ function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPe
return { result: mongoObject }; return { result: mongoObject };
} }
function mongoFieldTypeToSchemaAPIType(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'};
}
}
// Builds a new schema (in schema API response format) out of an // Builds a new schema (in schema API response format) out of an
// existing mongo schema + a schemas API put request. This response // existing mongo schema + a schemas API put request. This response
// does not include the default fields, as it is intended to be passed // does not include the default fields, as it is intended to be passed
@@ -776,7 +728,7 @@ function buildMergedSchemaObject(mongoObject, putRequest) {
} }
var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete' var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
if (!fieldIsDeleted) { if (!fieldIsDeleted) {
newSchema[oldField] = mongoFieldTypeToSchemaAPIType(mongoObject[oldField]); newSchema[oldField] = MongoSchemaCollection._DONOTUSEmongoFieldToParseSchemaField(mongoObject[oldField]);
} }
} }
} }
@@ -891,41 +843,11 @@ function getObjectType(obj) {
return 'object'; return 'object';
} }
const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions'];
function mongoSchemaAPIResponseFields(schema) {
var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1);
var response = fieldNames.reduce((obj, fieldName) => {
obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName])
return obj;
}, {});
response.ACL = {type: 'ACL'};
response.createdAt = {type: 'Date'};
response.updatedAt = {type: 'Date'};
response.objectId = {type: 'String'};
return response;
}
function mongoSchemaToSchemaAPIResponse(schema) {
let result = {
className: schema._id,
fields: mongoSchemaAPIResponseFields(schema),
};
let classLevelPermissions = DefaultClassLevelPermissions();
if (schema._metadata && schema._metadata.class_permissions) {
classLevelPermissions = Object.assign({}, classLevelPermissions, schema._metadata.class_permissions);
}
result.classLevelPermissions = classLevelPermissions;
return result;
}
export { export {
load, load,
classNameIsValid, classNameIsValid,
invalidClassNameMessage, invalidClassNameMessage,
schemaAPITypeToMongoFieldType, schemaAPITypeToMongoFieldType,
buildMergedSchemaObject, buildMergedSchemaObject,
mongoFieldTypeToSchemaAPIType,
mongoSchemaToSchemaAPIResponse,
systemClasses, systemClasses,
}; };

View File

@@ -1,5 +1,5 @@
import cache from './cache'; import cache from './cache';
import log from './logger'; import log from './logger';
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;