Skip to content

Break schemaController dependency. #1901

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 12 additions & 27 deletions spec/MongoTransform.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,11 @@ let transform = require('../src/Adapters/Storage/Mongo/MongoTransform');
let dd = require('deep-diff');
let mongodb = require('mongodb');

var dummySchema = {
data: {},
getExpectedType: function(className, key) {
if (key == 'userPointer') {
return { type: 'Pointer', targetClass: '_User' };
} else if (key == 'picture') {
return { type: 'File' };
} else if (key == 'location') {
return { type: 'GeoPoint' };
}
return;
},
};


describe('parseObjectToMongoObjectForCreate', () => {

it('a basic number', (done) => {
var input = {five: 5};
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, {
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {five: {type: 'Number'}}
});
jequal(input, output);
Expand All @@ -36,7 +21,7 @@ describe('parseObjectToMongoObjectForCreate', () => {
createdAt: "2015-10-06T21:24:50.332Z",
updatedAt: "2015-10-06T21:24:50.332Z"
};
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} });
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
expect(output._created_at instanceof Date).toBe(true);
expect(output._updated_at instanceof Date).toBe(true);
done();
Expand All @@ -48,7 +33,7 @@ describe('parseObjectToMongoObjectForCreate', () => {
objectId: 'myId',
className: 'Blah',
};
var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {pointers: [pointer]},{
var out = transform.parseObjectToMongoObjectForCreate(null, {pointers: [pointer]},{
fields: {pointers: {type: 'Array'}}
});
jequal([pointer], out.pointers);
Expand All @@ -59,22 +44,22 @@ describe('parseObjectToMongoObjectForCreate', () => {
//have __op delete in a new object. Figure out what this should actually be testing.
notWorking('a delete op', (done) => {
var input = {deleteMe: {__op: 'Delete'}};
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} });
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
jequal(output, {});
done();
});

it('basic ACL', (done) => {
var input = {ACL: {'0123': {'read': true, 'write': true}}};
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} });
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
// This just checks that it doesn't crash, but it should check format.
done();
});

describe('GeoPoints', () => {
it('plain', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {location: geoPoint},{
var out = transform.parseObjectToMongoObjectForCreate(null, {location: geoPoint},{
fields: {location: {type: 'GeoPoint'}}
});
expect(out.location).toEqual([180, -180]);
Expand All @@ -83,7 +68,7 @@ describe('parseObjectToMongoObjectForCreate', () => {

it('in array', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {locations: [geoPoint, geoPoint]},{
var out = transform.parseObjectToMongoObjectForCreate(null, {locations: [geoPoint, geoPoint]},{
fields: {locations: {type: 'Array'}}
});
expect(out.locations).toEqual([geoPoint, geoPoint]);
Expand All @@ -92,7 +77,7 @@ describe('parseObjectToMongoObjectForCreate', () => {

it('in sub-object', (done) => {
var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180};
var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, { locations: { start: geoPoint }},{
var out = transform.parseObjectToMongoObjectForCreate(null, { locations: { start: geoPoint }},{
fields: {locations: {type: 'Object'}}
});
expect(out).toEqual({ locations: { start: geoPoint } });
Expand Down Expand Up @@ -206,7 +191,7 @@ describe('transform schema key changes', () => {
var input = {
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
};
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, {
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {somePointer: {type: 'Pointer'}}
});
expect(typeof output._p_somePointer).toEqual('string');
Expand All @@ -218,7 +203,7 @@ describe('transform schema key changes', () => {
var input = {
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
};
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, {
var output = transform.parseObjectToMongoObjectForCreate(null, input, {
fields: {userPointer: {type: 'Pointer'}}
});
expect(typeof output._p_userPointer).toEqual('string');
Expand All @@ -233,7 +218,7 @@ describe('transform schema key changes', () => {
"Kevin": { "write": true }
}
};
var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} });
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
expect(typeof output._rperm).toEqual('object');
expect(typeof output._wperm).toEqual('object');
expect(output.ACL).toBeUndefined();
Expand All @@ -250,7 +235,7 @@ describe('transform schema key changes', () => {
}
};

var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} });
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
expect(typeof output._acl).toEqual('object');
expect(output._acl["Kevin"].w).toBeTruthy();
expect(output._acl["Kevin"].r).toBeUndefined();
Expand Down
68 changes: 47 additions & 21 deletions src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection';
import {parse as parseUrl, format as formatUrl} from '../../../vendor/mongodbUrl';
import * as transform from './MongoTransform';
import _ from 'lodash';
import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection';
import {
parse as parseUrl,
format as formatUrl,
} from '../../../vendor/mongodbUrl';
import {
parseObjectToMongoObjectForCreate,
mongoObjectToParseObject,
transformKey,
transformWhere,
transformUpdate,
} from './MongoTransform';
import _ from 'lodash';

let mongodb = require('mongodb');
let MongoClient = mongodb.MongoClient;
Expand Down Expand Up @@ -159,12 +168,11 @@ export class MongoStorageAdapter {
.then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className));
}

// TODO: As yet not particularly well specified. Creates an object. Shouldn't need the
// schemaController, but MongoTransform still needs it :( maybe shouldn't even need the schema,
// TODO: As yet not particularly well specified. Creates an object. Maybe shouldn't even need the schema,
// and should infer from the type. Or maybe does need the schema for validations. Or maybe needs
// the schem only for the legacy mongo format. We'll figure that out later.
createObject(className, object, schemaController, parseFormatSchema) {
const mongoObject = transform.parseObjectToMongoObjectForCreate(schemaController, className, object, parseFormatSchema);
createObject(className, object, schema) {
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
return this.adaptiveCollection(className)
.then(collection => collection.insertOne(mongoObject))
.catch(error => {
Expand All @@ -176,15 +184,13 @@ export class MongoStorageAdapter {
});
}

// Remove all objects that match the given parse query. Parse Query should be in Parse Format.
// 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.

// Currently accepts the schema, that may not actually be necessary.
deleteObjectsByQuery(className, query, schema) {
return this.adaptiveCollection(className)
.then(collection => {
let mongoWhere = transform.transformWhere(className, query, schema);
let mongoWhere = transformWhere(className, query, schema);
return collection.deleteMany(mongoWhere)
})
.then(({ result }) => {
Expand All @@ -197,23 +203,43 @@ export class MongoStorageAdapter {
});
}

// Apply the update to all objects that match the given Parse Query.
updateObjectsByQuery(className, query, schema, update) {
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
return this.adaptiveCollection(className)
.then(collection => collection.updateMany(mongoWhere, mongoUpdate));
}

// Hopefully we can get rid of this in favor of updateObjectsByQuery.
findOneAndUpdate(className, query, schema, update) {
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
return this.adaptiveCollection(className)
.then(collection => collection.findOneAndUpdate(mongoWhere, mongoUpdate));
}

// Hopefully we can get rid of this. It's only used for config and hooks.
upsertOneObject(className, query, schema, update) {
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
return this.adaptiveCollection(className)
.then(collection => collection.upsertOne(mongoWhere, mongoUpdate));
}

// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
find(className, query, schema, { skip, limit, sort }) {
let mongoWhere = this.transform.transformWhere(className, query, schema);
let mongoSort = _.mapKeys(sort, (value, fieldName) => transform.transformKey(className, fieldName, schema));
let mongoWhere = transformWhere(className, query, schema);
let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
return this.adaptiveCollection(className)
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort }))
.then(objects => objects.map(object => transform.mongoObjectToParseObject(className, object, schema)));
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)));
}

// Executs a count.
count(className, query, schema) {
return this.adaptiveCollection(className)
.then(collection => collection.count(transform.transformWhere(className, query, schema)));
}

get transform() {
return transform;
.then(collection => collection.count(transformWhere(className, query, schema)));
}
}

Expand Down
53 changes: 8 additions & 45 deletions src/Adapters/Storage/Mongo/MongoTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const transformKey = (className, fieldName, schema) => {
return fieldName;
}

const transformKeyValueForUpdate = (schema, className, restKey, restValue) => {
const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSchema) => {
// Check if the schema is known since it's a built-in field.
var key = restKey;
var timeField = false;
Expand All @@ -38,12 +38,6 @@ const transformKeyValueForUpdate = (schema, className, restKey, restValue) => {
key = '_updated_at';
timeField = true;
break;
case '_email_verify_token':
key = "_email_verify_token";
break;
case '_perishable_token':
key = "_perishable_token";
break;
case 'sessionToken':
case '_session_token':
key = '_session_token';
Expand All @@ -57,26 +51,9 @@ const transformKeyValueForUpdate = (schema, className, restKey, restValue) => {
case '_wperm':
return {key: key, value: restValue};
break;
case '$or':
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $or in queries');
case '$and':
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $and in queries');
default:
// Other auth data
var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/);
if (authDataMatch) {
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + key);
}
}

// Handle special schema key changes
// TODO: it seems like this is likely to have edge cases where
// pointer types are missed
var expected = undefined;
if (schema && schema.getExpectedType) {
expected = schema.getExpectedType(className, key);
}
if ((expected && expected.type == 'Pointer') || (!expected && restValue && restValue.__type == 'Pointer')) {
if ((parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer') || (!parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may be worth null checking parseFormatSchema and parseFormatSchema.fields

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the contract we are going to make with the DB adapters is that we always provide a valid schema. All validation is going to happen inside Parse Server, so if those are ever null, we should actually crash and not just go forward without knowing what the real schema is.

key = '_p_' + key;
}

Expand All @@ -101,9 +78,6 @@ const transformKeyValueForUpdate = (schema, className, restKey, restValue) => {
}

// Handle normal objects by recursing
if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) {
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
}
value = _.mapValues(restValue, transformInteriorValue);
return {key, value};
}
Expand Down Expand Up @@ -223,13 +197,7 @@ function transformWhere(className, restWhere, schema) {
return mongoWhere;
}

const parseObjectKeyValueToMongoObjectKeyValue = (
schema,
className,
restKey,
restValue,
parseFormatSchema
) => {
const parseObjectKeyValueToMongoObjectKeyValue = (className, restKey, restValue, schema) => {
// Check if the schema is known since it's a built-in field.
let transformedValue;
let coercedToDate;
Expand Down Expand Up @@ -267,7 +235,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = (
if (restValue && restValue.__type !== 'Bytes') {
//Note: We may not know the type of a field here, as the user could be saving (null) to a field
//That never existed before, meaning we can't infer the type.
if (parseFormatSchema.fields[restKey] && parseFormatSchema.fields[restKey].type == 'Pointer' || restValue.__type == 'Pointer') {
if (schema.fields[restKey] && schema.fields[restKey].type == 'Pointer' || restValue.__type == 'Pointer') {
restKey = '_p_' + restKey;
}
}
Expand Down Expand Up @@ -305,18 +273,17 @@ const parseObjectKeyValueToMongoObjectKeyValue = (

// Main exposed method to create new objects.
// restCreate is the "create" clause in REST API form.
function parseObjectToMongoObjectForCreate(schema, className, restCreate, parseFormatSchema) {
function parseObjectToMongoObjectForCreate(className, restCreate, schema) {
if (className == '_User') {
restCreate = transformAuthData(restCreate);
}
var mongoCreate = transformACL(restCreate);
for (let restKey in restCreate) {
let { key, value } = parseObjectKeyValueToMongoObjectKeyValue(
schema,
className,
restKey,
restCreate[restKey],
parseFormatSchema
schema
);
if (value !== undefined) {
mongoCreate[key] = value;
Expand All @@ -326,10 +293,7 @@ function parseObjectToMongoObjectForCreate(schema, className, restCreate, parseF
}

// Main exposed method to help update old objects.
function transformUpdate(schema, className, restUpdate) {
if (!restUpdate) {
throw 'got empty restUpdate';
}
const transformUpdate = (className, restUpdate, parseFormatSchema) => {
if (className == '_User') {
restUpdate = transformAuthData(restUpdate);
}
Expand All @@ -348,9 +312,8 @@ function transformUpdate(schema, className, restUpdate) {
mongoUpdate['$set']['_acl'] = acl._acl;
}
}

for (var restKey in restUpdate) {
var out = transformKeyValueForUpdate(schema, className, restKey, restUpdate[restKey]);
var out = transformKeyValueForUpdate(className, restKey, restUpdate[restKey], parseFormatSchema);

// If the output value is an object with any $ keys, it's an
// operator that needs to be lifted onto the top level update
Expand Down
Loading