Skip to content

Commit 9a14f53

Browse files
committed
Merge pull request #276 from drew-gross/schema-creation-logic
Schema creation logic
2 parents 6a3718e + a5440bc commit 9a14f53

File tree

4 files changed

+498
-14
lines changed

4 files changed

+498
-14
lines changed

ExportAdapter.js

+2-7
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,7 @@ ExportAdapter.prototype.connect = function() {
6060
var joinRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
6161
var otherRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
6262
ExportAdapter.prototype.collection = function(className) {
63-
if (className !== '_User' &&
64-
className !== '_Installation' &&
65-
className !== '_Session' &&
66-
className !== '_SCHEMA' &&
67-
className !== '_Role' &&
68-
!joinRegex.test(className) &&
69-
!otherRegex.test(className)) {
63+
if (!Schema.classNameIsValid(className)) {
7064
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
7165
'invalid className: ' + className);
7266
}
@@ -500,6 +494,7 @@ ExportAdapter.prototype.smartFind = function(coll, where, options) {
500494

501495
var index = {};
502496
index[key] = '2d';
497+
//TODO: condiser moving index creation logic into Schema.js
503498
return coll.createIndex(index).then(() => {
504499
// Retry, but just once.
505500
return coll.find(where, options).toArray();

Schema.js

+216-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,135 @@
1717
var Parse = require('parse/node').Parse;
1818
var transform = require('./transform');
1919

20+
defaultColumns = {
21+
// Contain the default columns for every parse object type (except _Join collection)
22+
_Default: {
23+
"objectId": {type:'String'},
24+
"createdAt": {type:'Date'},
25+
"updatedAt": {type:'Date'},
26+
"ACL": {type:'ACL'},
27+
},
28+
// The additional default columns for the _User collection (in addition to DefaultCols)
29+
_User: {
30+
"username": {type:'String'},
31+
"password": {type:'String'},
32+
"authData": {type:'Object'},
33+
"email": {type:'String'},
34+
"emailVerified": {type:'Boolean'},
35+
},
36+
// The additional default columns for the _User collection (in addition to DefaultCols)
37+
_Installation: {
38+
"installationId": {type:'String'},
39+
"deviceToken": {type:'String'},
40+
"channels": {type:'Array'},
41+
"deviceType": {type:'String'},
42+
"pushType": {type:'String'},
43+
"GCMSenderId": {type:'String'},
44+
"timeZone": {type:'String'},
45+
"localeIdentifier": {type:'String'},
46+
"badge": {type:'Number'},
47+
},
48+
// The additional default columns for the _User collection (in addition to DefaultCols)
49+
_Role: {
50+
"name": {type:'String'},
51+
"users": {type:'Relation',className:'_User'},
52+
"roles": {type:'Relation',className:'_Role'},
53+
},
54+
// The additional default columns for the _User collection (in addition to DefaultCols)
55+
_Session: {
56+
"restricted": {type:'Boolean'},
57+
"user": {type:'Pointer', className:'_User'},
58+
"installationId": {type:'String'},
59+
"sessionToken": {type:'String'},
60+
"expiresAt": {type:'Date'},
61+
"createdWith": {type:'Object'},
62+
},
63+
}
64+
65+
// Valid classes must:
66+
// Be one of _User, _Installation, _Role, _Session OR
67+
// Be a join table OR
68+
// Include only alpha-numeric and underscores, and not start with an underscore or number
69+
var joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
70+
var classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
71+
function classNameIsValid(className) {
72+
return (
73+
className === '_User' ||
74+
className === '_Installation' ||
75+
className === '_Session' ||
76+
className === '_SCHEMA' || //TODO: remove this, as _SCHEMA is not a valid class name for storing Parse Objects.
77+
className === '_Role' ||
78+
joinClassRegex.test(className) ||
79+
//Class names have the same constraints as field names, but also allow the previous additional names.
80+
fieldNameIsValid(className)
81+
);
82+
}
83+
84+
// Valid fields must be alpha-numeric, and not start with an underscore or number
85+
function fieldNameIsValid(fieldName) {
86+
return classAndFieldRegex.test(fieldName);
87+
}
88+
89+
// Checks that it's not trying to clobber one of the default fields of the class.
90+
function fieldNameIsValidForClass(fieldName, className) {
91+
if (!fieldNameIsValid(fieldName)) {
92+
return false;
93+
}
94+
if (defaultColumns._Default[fieldName]) {
95+
return false;
96+
}
97+
if (defaultColumns[className] && defaultColumns[className][fieldName]) {
98+
return false;
99+
}
100+
return true;
101+
}
102+
103+
function invalidClassNameMessage(className) {
104+
return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character ';
105+
}
106+
107+
// Returns { error: "message", code: ### } if the type could not be
108+
// converted, otherwise returns a returns { result: "mongotype" }
109+
// where mongotype is suitable for inserting into mongo _SCHEMA collection
110+
function schemaAPITypeToMongoFieldType(type) {
111+
var invalidJsonError = { error: "invalid JSON", code: Parse.Error.INVALID_JSON };
112+
if (type.type == 'Pointer') {
113+
if (!type.targetClass) {
114+
return { error: 'type Pointer needs a class name', code: 135 };
115+
} else if (typeof type.targetClass !== 'string') {
116+
return invalidJsonError;
117+
} else if (!classNameIsValid(type.targetClass)) {
118+
return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME };
119+
} else {
120+
return { result: '*' + type.targetClass };
121+
}
122+
}
123+
if (type.type == 'Relation') {
124+
if (!type.targetClass) {
125+
return { error: 'type Relation needs a class name', code: 135 };
126+
} else if (typeof type.targetClass !== 'string') {
127+
return invalidJsonError;
128+
} else if (!classNameIsValid(type.targetClass)) {
129+
return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME };
130+
} else {
131+
return { result: 'relation<' + type.targetClass + '>' };
132+
}
133+
}
134+
if (typeof type.type !== 'string') {
135+
return { error: "invalid JSON", code: Parse.Error.INVALID_JSON };
136+
}
137+
switch (type.type) {
138+
default: return { error: 'invalid field type: ' + type.type, code: Parse.Error.INCORRECT_TYPE };
139+
case 'Number': return { result: 'number' };
140+
case 'String': return { result: 'string' };
141+
case 'Boolean': return { result: 'boolean' };
142+
case 'Date': return { result: 'date' };
143+
case 'Object': return { result: 'object' };
144+
case 'Array': return { result: 'array' };
145+
case 'GeoPoint': return { result: 'geopoint' };
146+
case 'File': return { result: 'file' };
147+
}
148+
}
20149

21150
// Create a schema from a Mongo collection and the exported schema format.
22151
// mongoSchema should be a list of objects, each with:
@@ -71,9 +200,93 @@ Schema.prototype.reload = function() {
71200
return load(this.collection);
72201
};
73202

203+
// Create a new class that includes the three default fields.
204+
// ACL is an implicit column that does not get an entry in the
205+
// _SCHEMAS database. Returns a promise that resolves with the
206+
// created schema, in mongo format.
207+
// on success, and rejects with an error on fail. Ensure you
208+
// have authorization (master key, or client class creation
209+
// enabled) before calling this function.
210+
Schema.prototype.addClassIfNotExists = function(className, fields) {
211+
if (this.data[className]) {
212+
return Promise.reject({
213+
code: Parse.Error.INVALID_CLASS_NAME,
214+
error: 'class ' + className + ' already exists',
215+
});
216+
}
217+
218+
if (!classNameIsValid(className)) {
219+
return Promise.reject({
220+
code: Parse.Error.INVALID_CLASS_NAME,
221+
error: invalidClassNameMessage(className),
222+
});
223+
}
224+
for (fieldName in fields) {
225+
if (!fieldNameIsValid(fieldName)) {
226+
return Promise.reject({
227+
code: Parse.Error.INVALID_KEY_NAME,
228+
error: 'invalid field name: ' + fieldName,
229+
});
230+
}
231+
if (!fieldNameIsValidForClass(fieldName, className)) {
232+
return Promise.reject({
233+
code: 136,
234+
error: 'field ' + fieldName + ' cannot be added',
235+
});
236+
}
237+
}
238+
239+
var mongoObject = {
240+
_id: className,
241+
objectId: 'string',
242+
updatedAt: 'string',
243+
createdAt: 'string',
244+
};
245+
for (fieldName in defaultColumns[className]) {
246+
validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]);
247+
if (validatedField.code) {
248+
return Promise.reject(validatedField);
249+
}
250+
mongoObject[fieldName] = validatedField.result;
251+
}
252+
253+
for (fieldName in fields) {
254+
validatedField = schemaAPITypeToMongoFieldType(fields[fieldName]);
255+
if (validatedField.code) {
256+
return Promise.reject(validatedField);
257+
}
258+
mongoObject[fieldName] = validatedField.result;
259+
}
260+
261+
var geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint');
262+
263+
if (geoPoints.length > 1) {
264+
return Promise.reject({
265+
code: Parse.Error.INCORRECT_TYPE,
266+
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
267+
});
268+
}
269+
270+
return this.collection.insertOne(mongoObject)
271+
.then(result => result.ops[0])
272+
.catch(error => {
273+
if (error.code === 11000) { //Mongo's duplicate key error
274+
return Promise.reject({
275+
code: Parse.Error.INVALID_CLASS_NAME,
276+
error: 'class ' + className + ' already exists',
277+
});
278+
}
279+
return Promise.reject(error);
280+
});
281+
}
282+
74283
// Returns a promise that resolves successfully to the new schema
75-
// object.
284+
// object or fails with a reason.
76285
// If 'freeze' is true, refuse to update the schema.
286+
// WARNING: this function has side-effects, and doesn't actually
287+
// do any validation of the format of the className. You probably
288+
// should use classNameIsValid or addClassIfNotExists or something
289+
// like that instead. TODO: rename or remove this function.
77290
Schema.prototype.validateClassName = function(className, freeze) {
78291
if (this.data[className]) {
79292
return Promise.resolve(this);
@@ -348,5 +561,6 @@ function getObjectType(obj) {
348561

349562

350563
module.exports = {
351-
load: load
564+
load: load,
565+
classNameIsValid: classNameIsValid,
352566
};

schemas.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ var express = require('express'),
55

66
var router = new PromiseRouter();
77

8-
function mongoFieldTypeToApiResponseType(type) {
8+
function mongoFieldTypeToSchemaAPIType(type) {
99
if (type[0] === '*') {
1010
return {
1111
type: 'Pointer',
@@ -32,10 +32,10 @@ function mongoFieldTypeToApiResponseType(type) {
3232

3333
function mongoSchemaAPIResponseFields(schema) {
3434
fieldNames = Object.keys(schema).filter(key => key !== '_id');
35-
response = {};
36-
fieldNames.forEach(fieldName => {
37-
response[fieldName] = mongoFieldTypeToApiResponseType(schema[fieldName]);
38-
});
35+
response = fieldNames.reduce((obj, fieldName) => {
36+
obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName])
37+
return obj;
38+
}, {});
3939
response.ACL = {type: 'ACL'};
4040
response.createdAt = {type: 'Date'};
4141
response.updatedAt = {type: 'Date'};

0 commit comments

Comments
 (0)