Required fields and default values (#5835)
* Add field options to mongo schema metadata * Add/fix test with fields options * Add required validation failing test * Add more tests * Only set default value if field is undefined * Fix redis test * Fix tests * Test for creating a new class with field options * Validate default value type * fix lint (weird) * Fix lint another way * Add tests for beforeSave trigger and solve small issue regarding the use of unset in the beforeSave trigger
This commit is contained in:
committed by
GitHub
parent
d3810c2eba
commit
fd637ff4f8
@@ -46,6 +46,17 @@ function mongoSchemaFieldsToParseSchemaFields(schema) {
|
||||
);
|
||||
var response = fieldNames.reduce((obj, fieldName) => {
|
||||
obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName]);
|
||||
if (
|
||||
schema._metadata &&
|
||||
schema._metadata.fields_options &&
|
||||
schema._metadata.fields_options[fieldName]
|
||||
) {
|
||||
obj[fieldName] = Object.assign(
|
||||
{},
|
||||
obj[fieldName],
|
||||
schema._metadata.fields_options[fieldName]
|
||||
);
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
response.ACL = { type: 'ACL' };
|
||||
@@ -212,7 +223,7 @@ class MongoSchemaCollection {
|
||||
// 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) {
|
||||
addFieldIfNotExists(className: string, fieldName: string, fieldType: string) {
|
||||
return this._fetchOneSchemaFrom_SCHEMA(className)
|
||||
.then(
|
||||
schema => {
|
||||
@@ -221,7 +232,7 @@ class MongoSchemaCollection {
|
||||
return;
|
||||
}
|
||||
// The schema exists. Check for existing GeoPoints.
|
||||
if (type.type === 'GeoPoint') {
|
||||
if (fieldType.type === 'GeoPoint') {
|
||||
// Make sure there are not other geopoint fields
|
||||
if (
|
||||
Object.keys(schema.fields).some(
|
||||
@@ -247,13 +258,37 @@ class MongoSchemaCollection {
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const { type, targetClass, ...fieldOptions } = fieldType;
|
||||
// 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) } }
|
||||
);
|
||||
if (fieldOptions && Object.keys(fieldOptions).length > 0) {
|
||||
return this.upsertSchema(
|
||||
className,
|
||||
{ [fieldName]: { $exists: false } },
|
||||
{
|
||||
$set: {
|
||||
[fieldName]: parseFieldTypeToMongoFieldType({
|
||||
type,
|
||||
targetClass,
|
||||
}),
|
||||
[`_metadata.fields_options.${fieldName}`]: fieldOptions,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return this.upsertSchema(
|
||||
className,
|
||||
{ [fieldName]: { $exists: false } },
|
||||
{
|
||||
$set: {
|
||||
[fieldName]: parseFieldTypeToMongoFieldType({
|
||||
type,
|
||||
targetClass,
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,9 +84,19 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (
|
||||
};
|
||||
|
||||
for (const fieldName in fields) {
|
||||
const { type, targetClass, ...fieldOptions } = fields[fieldName];
|
||||
mongoObject[
|
||||
fieldName
|
||||
] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]);
|
||||
] = MongoSchemaCollection.parseFieldTypeToMongoFieldType({
|
||||
type,
|
||||
targetClass,
|
||||
});
|
||||
if (fieldOptions && Object.keys(fieldOptions).length > 0) {
|
||||
mongoObject._metadata = mongoObject._metadata || {};
|
||||
mongoObject._metadata.fields_options =
|
||||
mongoObject._metadata.fields_options || {};
|
||||
mongoObject._metadata.fields_options[fieldName] = fieldOptions;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof classLevelPermissions !== 'undefined') {
|
||||
@@ -425,6 +435,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
const schemaUpdate = { $unset: {} };
|
||||
fieldNames.forEach(name => {
|
||||
schemaUpdate['$unset'][name] = null;
|
||||
schemaUpdate['$unset'][`_metadata.fields_options.${name}`] = null;
|
||||
});
|
||||
|
||||
return this._adaptiveCollection(className)
|
||||
|
||||
@@ -664,6 +664,13 @@ export default class SchemaController {
|
||||
classLevelPermissions
|
||||
);
|
||||
if (validationError) {
|
||||
if (validationError instanceof Parse.Error) {
|
||||
return Promise.reject(validationError);
|
||||
} else if (validationError.code && validationError.error) {
|
||||
return Promise.reject(
|
||||
new Parse.Error(validationError.code, validationError.error)
|
||||
);
|
||||
}
|
||||
return Promise.reject(validationError);
|
||||
}
|
||||
|
||||
@@ -884,8 +891,23 @@ export default class SchemaController {
|
||||
error: 'field ' + fieldName + ' cannot be added',
|
||||
};
|
||||
}
|
||||
const error = fieldTypeIsInvalid(fields[fieldName]);
|
||||
const type = fields[fieldName];
|
||||
const error = fieldTypeIsInvalid(type);
|
||||
if (error) return { code: error.code, error: error.message };
|
||||
if (type.defaultValue !== undefined) {
|
||||
let defaultValueType = getType(type.defaultValue);
|
||||
if (typeof defaultValueType === 'string') {
|
||||
defaultValueType = { type: defaultValueType };
|
||||
}
|
||||
if (!dbTypeMatchesObjectType(type, defaultValueType)) {
|
||||
return {
|
||||
code: Parse.Error.INCORRECT_TYPE,
|
||||
error: `schema mismatch for ${className}.${fieldName} default value; expected ${typeToString(
|
||||
type
|
||||
)} but got ${typeToString(defaultValueType)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,7 +969,22 @@ export default class SchemaController {
|
||||
|
||||
const expectedType = this.getExpectedType(className, fieldName);
|
||||
if (typeof type === 'string') {
|
||||
type = { type };
|
||||
type = ({ type }: SchemaField);
|
||||
}
|
||||
|
||||
if (type.defaultValue !== undefined) {
|
||||
let defaultValueType = getType(type.defaultValue);
|
||||
if (typeof defaultValueType === 'string') {
|
||||
defaultValueType = { type: defaultValueType };
|
||||
}
|
||||
if (!dbTypeMatchesObjectType(type, defaultValueType)) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
`schema mismatch for ${className}.${fieldName} default value; expected ${typeToString(
|
||||
type
|
||||
)} but got ${typeToString(defaultValueType)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedType) {
|
||||
|
||||
@@ -5,6 +5,8 @@ export type LoadSchemaOptions = {
|
||||
export type SchemaField = {
|
||||
type: string,
|
||||
targetClass?: ?string,
|
||||
required?: ?boolean,
|
||||
defaultValue?: ?any,
|
||||
};
|
||||
|
||||
export type SchemaFields = { [string]: SchemaField };
|
||||
|
||||
@@ -323,16 +323,66 @@ RestWrite.prototype.runBeforeLoginTrigger = async function(userData) {
|
||||
|
||||
RestWrite.prototype.setRequiredFieldsIfNeeded = function() {
|
||||
if (this.data) {
|
||||
// Add default fields
|
||||
this.data.updatedAt = this.updatedAt;
|
||||
if (!this.query) {
|
||||
this.data.createdAt = this.updatedAt;
|
||||
return this.validSchemaController.getAllClasses().then(allClasses => {
|
||||
const schema = allClasses.find(
|
||||
oneClass => oneClass.className === this.className
|
||||
);
|
||||
const setRequiredFieldIfNeeded = (fieldName, setDefault) => {
|
||||
if (
|
||||
this.data[fieldName] === undefined ||
|
||||
this.data[fieldName] === null ||
|
||||
this.data[fieldName] === '' ||
|
||||
(typeof this.data[fieldName] === 'object' &&
|
||||
this.data[fieldName].__op === 'Delete')
|
||||
) {
|
||||
if (
|
||||
setDefault &&
|
||||
schema.fields[fieldName] &&
|
||||
schema.fields[fieldName].defaultValue &&
|
||||
(this.data[fieldName] === undefined ||
|
||||
(typeof this.data[fieldName] === 'object' &&
|
||||
this.data[fieldName].__op === 'Delete'))
|
||||
) {
|
||||
this.data[fieldName] = schema.fields[fieldName].defaultValue;
|
||||
this.storage.fieldsChangedByTrigger =
|
||||
this.storage.fieldsChangedByTrigger || [];
|
||||
if (this.storage.fieldsChangedByTrigger.indexOf(fieldName) < 0) {
|
||||
this.storage.fieldsChangedByTrigger.push(fieldName);
|
||||
}
|
||||
} else if (
|
||||
schema.fields[fieldName] &&
|
||||
schema.fields[fieldName].required === true
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.VALIDATION_ERROR,
|
||||
`${fieldName} is required`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Only assign new objectId if we are creating new object
|
||||
if (!this.data.objectId) {
|
||||
this.data.objectId = cryptoUtils.newObjectId(this.config.objectIdSize);
|
||||
// Add default fields
|
||||
this.data.updatedAt = this.updatedAt;
|
||||
if (!this.query) {
|
||||
this.data.createdAt = this.updatedAt;
|
||||
|
||||
// Only assign new objectId if we are creating new object
|
||||
if (!this.data.objectId) {
|
||||
this.data.objectId = cryptoUtils.newObjectId(
|
||||
this.config.objectIdSize
|
||||
);
|
||||
}
|
||||
if (schema) {
|
||||
Object.keys(schema.fields).forEach(fieldName => {
|
||||
setRequiredFieldIfNeeded(fieldName, true);
|
||||
});
|
||||
}
|
||||
} else if (schema) {
|
||||
Object.keys(this.data).forEach(fieldName => {
|
||||
setRequiredFieldIfNeeded(fieldName, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user