Postgres exclude failing tests (#2081)

* reload the right data

More passing postgres tests

Handle schema updates, and $in for non array columns

remove authdata from user and implement ensureUniqueness

Make some tests work, detect existing classes

Throw proper error for unique index violation

fix findOneAndUpdate

Support more types

support more type

Support boolean, fix _rperm/_wperm, add TODO

Support string types and also simplify tests

Move operator flattening into Parse Server and out of mongo adapters

Move authdata transform for create into Parse Server

Move authdata transforms completely in to Parse Server

Fix test setup

inline addSchema

Inject default schema to response from DB adapter

* Mark tests that don't work in Postgres

* Exclude one more test

* Exclude some more failing tests

* Exclude more tests
This commit is contained in:
Drew
2016-06-17 09:59:16 -07:00
committed by Florent Vilmart
parent 7da4debbe0
commit ab06055369
47 changed files with 817 additions and 801 deletions

View File

@@ -72,19 +72,17 @@ function mongoSchemaToParseSchema(mongoSchema) {
}
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];
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 }) {
@@ -102,33 +100,6 @@ function parseFieldTypeToMongoFieldType({ type, targetClass }) {
}
}
// 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;
@@ -156,22 +127,6 @@ class MongoSchemaCollection {
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);
}
@@ -225,5 +180,6 @@ 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
export default MongoSchemaCollection

View File

@@ -49,6 +49,33 @@ 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) => {
let mongoObject = {
_id: className,
objectId: 'string',
updatedAt: 'string',
createdAt: 'string'
};
for (let fieldName in fields) {
mongoObject[fieldName] = MongoSchemaCollection.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;
}
export class MongoStorageAdapter {
// Private
_uri: string;
@@ -113,8 +140,18 @@ export class MongoStorageAdapter {
createClass(className, schema) {
schema = convertParseSchemaToMongoSchema(schema);
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions);
mongoObject._id = className;
return this._schemaCollection()
.then(schemaCollection => schemaCollection.addSchema(className, schema.fields, schema.classLevelPermissions));
.then(schemaCollection => schemaCollection._collection.insertOne(mongoObject))
.then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(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;
}
})
}
addFieldIfNotExists(className, fieldName, type) {

View File

@@ -250,11 +250,6 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
return {key: restKey, value: value};
}
// Handle update operators. TODO: handle within Parse Server. DB adapter shouldn't see update operators in creates.
if (typeof restValue === 'object' && '__op' in restValue) {
return {key: restKey, value: transformUpdateOperator(restValue, true)};
}
// 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");
@@ -264,9 +259,6 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) =>
}
const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => {
if (className == '_User') {
restCreate = transformAuthData(restCreate);
}
restCreate = addLegacyACL(restCreate);
let mongoCreate = {}
for (let restKey in restCreate) {
@@ -295,10 +287,6 @@ const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => {
// Main exposed method to help update old objects.
const transformUpdate = (className, restUpdate, parseFormatSchema) => {
if (className == '_User') {
restUpdate = transformAuthData(restUpdate);
}
let mongoUpdate = {};
let acl = addLegacyACL(restUpdate)._acl;
if (acl) {
@@ -331,23 +319,6 @@ const transformUpdate = (className, restUpdate, parseFormatSchema) => {
return mongoUpdate;
}
function transformAuthData(restObject) {
if (restObject.authData) {
Object.keys(restObject.authData).forEach((provider) => {
let providerData = restObject.authData[provider];
if (providerData == null) {
restObject[`_auth_data_${provider}`] = {
__op: 'Delete'
}
} else {
restObject[`_auth_data_${provider}`] = providerData;
}
});
delete restObject.authData;
}
return restObject;
}
// Add the legacy _acl format.
const addLegacyACL = restObject => {
let restObjectCopy = {...restObject};

View File

@@ -17,7 +17,7 @@ const parseTypeToPostgresType = type => {
if (type.contents && type.contents.type === 'String') {
return 'text[]';
} else {
throw `no type for ${JSON.stringify(type)} yet`;
return 'jsonb';
}
default: throw `no type for ${JSON.stringify(type)} yet`;
}
@@ -242,9 +242,28 @@ export class PostgresStorageAdapter {
case 'Pointer':
valuesArray.push(object[fieldName].objectId);
break;
default:
case 'Array':
if (['_rperm', '_wperm'].includes(fieldName)) {
valuesArray.push(object[fieldName]);
} else {
valuesArray.push(JSON.stringify(object[fieldName]));
}
break;
case 'Object':
valuesArray.push(object[fieldName]);
break;
case 'String':
valuesArray.push(object[fieldName]);
break;
case 'Number':
valuesArray.push(object[fieldName]);
break;
case 'Boolean':
valuesArray.push(object[fieldName]);
break;
default:
throw `Type ${schema.fields[fieldName].type} not supported yet`;
break;
}
});
let columnsPattern = columnsArray.map((col, index) => `$${index + 2}:name`).join(',');
@@ -294,12 +313,24 @@ export class PostgresStorageAdapter {
updatePatterns.push(`$${index}:name = COALESCE($${index}:name, 0) + $${index + 1}`);
values.push(fieldName, fieldValue.amount);
index += 2;
} else if (fieldValue.__op === 'Add') {
updatePatterns.push(`$${index}:name = COALESCE($${index}:name, '[]'::jsonb) || $${index + 1}`);
values.push(fieldName, fieldValue.objects);
index += 2;
} else if (fieldValue.__op === 'Remove') {
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Postgres does not support Remove operator.'));
} else if (fieldValue.__op === 'AddUnique') {
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Postgres does not support AddUnique operator'));
} else if (fieldName === 'updatedAt') { //TODO: stop special casing this. It should check for __type === 'Date' and use .iso
updatePatterns.push(`$${index}:name = $${index + 1}`)
values.push(fieldName, new Date(fieldValue));
index += 2;
} else if (typeof fieldValue === 'string') {
updatePatterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue);
index += 2;
} else {
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support this type of update yet`));
return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support update ${JSON.stringify(fieldValue)} yet`));
}
}
@@ -345,6 +376,9 @@ export class PostgresStorageAdapter {
if (object[fieldName] === null) {
delete object[fieldName];
}
if (object[fieldName] instanceof Date) {
object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() };
}
}
return object;
@@ -363,6 +397,13 @@ export class PostgresStorageAdapter {
const constraintPatterns = fieldNames.map((fieldName, index) => `$${index + 3}:name`);
const qs = `ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (${constraintPatterns.join(',')})`;
return this._client.query(qs,[className, constraintName, ...fieldNames])
.catch(error => {
if (error.code === PostgresDuplicateRelationError && error.message.includes(constraintName)) {
// Index already exists. Ignore error.
} else {
throw error;
}
});
}
// Executes a count.

View File

@@ -238,6 +238,7 @@ DatabaseController.prototype.update = function(className, query, update, {
}
}
update = transformObjectACL(update);
transformAuthData(className, update, schema);
if (many) {
return this.adapter.updateObjectsByQuery(className, schema, query, update);
} else if (upsert) {
@@ -399,6 +400,62 @@ DatabaseController.prototype.destroy = function(className, query, { acl } = {})
});
};
const flattenUpdateOperatorsForCreate = object => {
for (let key in object) {
if (object[key] && object[key].__op) {
switch (object[key].__op) {
case 'Increment':
if (typeof object[key].amount !== 'number') {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
}
object[key] = object[key].amount;
break;
case 'Add':
if (!(object[key].objects instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
}
object[key] = object[key].objects;
break;
case 'AddUnique':
if (!(object[key].objects instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
}
object[key] = object[key].objects;
break;
case 'Remove':
if (!(object[key].objects instanceof Array)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array');
}
object[key] = []
break;
case 'Delete':
delete object[key];
break;
default:
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, `The ${object[key].__op} operator is not supported yet.`);
}
}
}
}
const transformAuthData = (className, object, schema) => {
if (object.authData && className === '_User') {
Object.keys(object.authData).forEach(provider => {
const providerData = object.authData[provider];
const fieldName = `_auth_data_${provider}`;
if (providerData == null) {
object[fieldName] = {
__op: 'Delete'
}
} else {
object[fieldName] = providerData;
schema.fields[fieldName] = { type: 'Object' }
}
});
delete object.authData;
}
}
// Inserts an object into the database.
// Returns a promise that resolves successfully iff the object saved.
DatabaseController.prototype.create = function(className, object, { acl } = {}) {
@@ -420,7 +477,11 @@ DatabaseController.prototype.create = function(className, object, { acl } = {})
.then(() => schemaController.enforceClassExists(className))
.then(() => schemaController.reloadData())
.then(() => schemaController.getOneSchema(className, true))
.then(schema => this.adapter.createObject(className, SchemaController.convertSchemaToAdapterSchema(schema), object))
.then(schema => {
transformAuthData(className, object, schema);
flattenUpdateOperatorsForCreate(object);
return this.adapter.createObject(className, SchemaController.convertSchemaToAdapterSchema(schema), object);
})
.then(result => sanitizeDatabaseResult(originalObject, result.ops[0]));
})
};

View File

@@ -288,7 +288,7 @@ class SchemaController {
return this.getAllClasses()
.then(allSchemas => {
allSchemas.forEach(schema => {
this.data[schema.className] = schema.fields;
this.data[schema.className] = injectDefaultSchema(schema).fields;
this.perms[schema.className] = schema.classLevelPermissions;
});

View File

@@ -194,23 +194,23 @@ class ParseServer {
const databaseController = new DatabaseController(databaseAdapter);
const hooksController = new HooksController(appId, databaseController, webhookKey);
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
// have a Parse app without it having a _User collection.
let userClassPromise = databaseController.loadSchema()
.then(schema => schema.enforceClassExists('_User'))
let usernameUniqueness = userClassPromise
.then(() => databaseController.adapter.ensureUniqueness('_User', requiredUserFields, ['username']))
.catch(error => {
logger.warn('Unable to ensure uniqueness for usernames: ', error);
return Promise.reject();
return Promise.reject(error);
});
let emailUniqueness = userClassPromise
.then(() => databaseController.adapter.ensureUniqueness('_User', requiredUserFields, ['email']))
.catch(error => {
logger.warn('Unabled to ensure uniqueness for user email addresses: ', error);
return Promise.reject();
return Promise.reject(error);
})
AppCache.put(appId, {