Introduces flow types for storage (#4349)

* Introduces flow types for storage

* Better typing of QueryOptions

* Adds flow types to SchemaCOntroller,

- runs flow on pre tests
- fixes flow

* Adds ClassLevelPermissions type

* Moves Controller types into a single file

* Changes import styles

* Changes import styles

* fixing method setIndexesWithSchemaFormat (#4454)

Fixing invalid database code in method `setIndexesWithSchemaFormat`:

* It must be a transaction, not a task, as it executes multiple database changes
* It should contain the initial queries inside the transaction, providing the context, not outside it;
* Replaced the code with the ES6 Generator notation
* Removing the use of batch, as the value of the result promise is irrelevant, only success/failure that matters

* nits

* Fixes tests, improves flow typing
This commit is contained in:
Florent Vilmart
2017-12-30 20:44:18 -05:00
committed by GitHub
parent f0f1870aa3
commit 10631371e6
20 changed files with 1085 additions and 915 deletions

View File

@@ -7,3 +7,4 @@
[libs]
[options]
suppress_comment= \\(.\\|\n\\)*\\@flow-disable-next

View File

@@ -56,6 +56,7 @@
"deep-diff": "0.3.8",
"eslint": "^4.9.0",
"eslint-plugin-flowtype": "^2.39.1",
"flow-bin": "^0.59.0",
"gaze": "1.1.2",
"jasmine": "2.8.0",
"jasmine-spec-reporter": "^4.1.0",
@@ -66,7 +67,7 @@
},
"scripts": {
"dev": "npm run build && node bin/dev",
"lint": "eslint --cache ./",
"lint": "flow && eslint --cache ./",
"build": "babel src/ -d lib/ --copy-files",
"pretest": "npm run lint",
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 jasmine",

View File

@@ -1,7 +1,7 @@
'use strict';
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
const MongoClient = require('mongodb').MongoClient;
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
const { MongoClient } = require('mongodb');
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
// These tests are specific to the mongo storage adapter + mongo storage format

View File

@@ -1,5 +1,5 @@
const TestObject = Parse.Object.extend('TestObject');
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
const rp = require('request-promise');
const defaultHeaders = {

View File

@@ -1,8 +1,8 @@
'use strict';
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
import PostgresStorageAdapter from '../src/Adapters/Storage/Postgres/PostgresStorageAdapter';
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
const Parse = require('parse/node');
const rp = require('request-promise');

View File

@@ -1,7 +1,8 @@
'use strict';
/* Tests for ParseServer.js */
const express = require('express');
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
import PostgresStorageAdapter from '../src/Adapters/Storage/Postgres/PostgresStorageAdapter';
import ParseServer from '../src/ParseServer';
describe('Server Url Checks', () => {
@@ -35,8 +36,6 @@ describe('Server Url Checks', () => {
});
it('handleShutdown, close connection', (done) => {
var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
let databaseAdapter;

View File

@@ -1,5 +1,5 @@
const Parse = require('parse/node').Parse;
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
import PostgresStorageAdapter from '../src/Adapters/Storage/Postgres/PostgresStorageAdapter';
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
const ParseServer = require("../src/index");
const express = require('express');

View File

@@ -1,4 +1,4 @@
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
import PostgresStorageAdapter from '../src/Adapters/Storage/Postgres/PostgresStorageAdapter';
const databaseURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
describe_only_db('postgres')('PostgresStorageAdapter', () => {

View File

@@ -27,10 +27,10 @@ var cache = require('../src/cache').default;
var ParseServer = require('../src/index').ParseServer;
var path = require('path');
var TestUtils = require('../src/TestUtils');
var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
const GridStoreAdapter = require('../src/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
const FSAdapter = require('@parse/fs-files-adapter');
const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter');
import PostgresStorageAdapter from '../src/Adapters/Storage/Postgres/PostgresStorageAdapter';
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
const RedisCacheAdapter = require('../src/Adapters/Cache/RedisCacheAdapter').default;
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';

View File

@@ -6,7 +6,7 @@ var ParseServer = require("../src/index");
var Config = require('../src/Config');
var express = require('express');
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
describe('server', () => {
it('requires a master key and app id', done => {

View File

@@ -6,6 +6,7 @@
@flow weak
*/
// @flow-disable-next
import { MongoClient, GridStore, Db} from 'mongodb';
import { FilesAdapter } from './FilesAdapter';
import defaults from '../../defaults';

View File

@@ -137,6 +137,18 @@ class MongoSchemaCollection {
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []);
}
insertSchema(schema: any) {
return this._collection.insertOne(schema)
.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);
}

View File

@@ -1,5 +1,11 @@
// @flow
import MongoCollection from './MongoCollection';
import MongoSchemaCollection from './MongoSchemaCollection';
import { StorageAdapter } from '../StorageAdapter';
import type { SchemaType,
QueryType,
StorageClass,
QueryOptions } from '../StorageAdapter';
import {
parse as parseUrl,
format as formatUrl,
@@ -12,10 +18,13 @@ import {
transformUpdate,
transformPointerString,
} from './MongoTransform';
// @flow-disable-next
import Parse from 'parse/node';
// @flow-disable-next
import _ from 'lodash';
import defaults from '../../../defaults';
// @flow-disable-next
const mongodb = require('mongodb');
const MongoClient = mongodb.MongoClient;
const ReadPreference = mongodb.ReadPreference;
@@ -59,7 +68,8 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
_id: className,
objectId: 'string',
updatedAt: 'string',
createdAt: 'string'
createdAt: 'string',
_metadata: undefined,
};
for (const fieldName in fields) {
@@ -80,24 +90,31 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
mongoObject._metadata.indexes = indexes;
}
if (!mongoObject._metadata) { // cleanup the unused _metadata
delete mongoObject._metadata;
}
return mongoObject;
}
export class MongoStorageAdapter {
export class MongoStorageAdapter implements StorageAdapter {
// Private
_uri: string;
_collectionPrefix: string;
_mongoOptions: Object;
// Public
connectionPromise;
database;
canSortOnJoinTables;
connectionPromise: Promise<any>;
database: any;
client: MongoClient;
_maxTimeMS: ?number;
canSortOnJoinTables: boolean;
constructor({
uri = defaults.DefaultMongoURI,
collectionPrefix = '',
mongoOptions = {},
}) {
}: any) {
this._uri = uri;
this._collectionPrefix = collectionPrefix;
this._mongoOptions = mongoOptions;
@@ -156,13 +173,13 @@ export class MongoStorageAdapter {
.then(rawCollection => new MongoCollection(rawCollection));
}
_schemaCollection() {
_schemaCollection(): Promise<MongoSchemaCollection> {
return this.connect()
.then(() => this._adaptiveCollection(MongoSchemaCollectionName))
.then(collection => new MongoSchemaCollection(collection));
}
classExists(name) {
classExists(name: string) {
return this.connect().then(() => {
return this.database.listCollections({ name: this._collectionPrefix + name }).toArray();
}).then(collections => {
@@ -170,14 +187,14 @@ export class MongoStorageAdapter {
});
}
setClassLevelPermissions(className, CLPs) {
setClassLevelPermissions(className: string, CLPs: any): Promise<void> {
return this._schemaCollection()
.then(schemaCollection => schemaCollection.updateSchema(className, {
$set: { '_metadata.class_permissions': CLPs }
}));
}
setIndexesWithSchemaFormat(className, submittedIndexes, existingIndexes = {}, fields) {
setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any = {}, fields: any): Promise<void> {
if (submittedIndexes === undefined) {
return Promise.resolve();
}
@@ -223,7 +240,7 @@ export class MongoStorageAdapter {
}));
}
setIndexesFromMongo(className) {
setIndexesFromMongo(className: string) {
return this.getIndexes(className).then((indexes) => {
indexes = indexes.reduce((obj, index) => {
if (index.key._fts) {
@@ -246,24 +263,16 @@ export class MongoStorageAdapter {
});
}
createClass(className, schema) {
createClass(className: string, schema: SchemaType): Promise<void> {
schema = convertParseSchemaToMongoSchema(schema);
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes);
mongoObject._id = className;
return this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields)
.then(() => this._schemaCollection())
.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;
}
})
.then(schemaCollection => schemaCollection.insertSchema(mongoObject));
}
addFieldIfNotExists(className, fieldName, type) {
addFieldIfNotExists(className: string, fieldName: string, type: any): Promise<void> {
return this._schemaCollection()
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type))
.then(() => this.createIndexesIfNeeded(className, fieldName, type));
@@ -271,7 +280,7 @@ export class MongoStorageAdapter {
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
deleteClass(className) {
deleteClass(className: string) {
return this._adaptiveCollection(className)
.then(collection => collection.drop())
.catch(error => {
@@ -312,7 +321,7 @@ export class MongoStorageAdapter {
// may do so.
// Returns a Promise.
deleteFields(className, schema, fieldNames) {
deleteFields(className: string, schema: SchemaType, fieldNames: string[]) {
const mongoFormatNames = fieldNames.map(fieldName => {
if (schema.fields[fieldName].type === 'Pointer') {
return `_p_${fieldName}`
@@ -339,14 +348,14 @@ export class MongoStorageAdapter {
// 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 for the
// rejection reason are TBD.
getAllClasses() {
getAllClasses(): Promise<StorageClass[]> {
return this._schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA());
}
// 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.
getClass(className) {
getClass(className: string): Promise<StorageClass> {
return this._schemaCollection()
.then(schemasCollection => schemasCollection._fetchOneSchemaFrom_SCHEMA(className))
}
@@ -354,7 +363,7 @@ export class MongoStorageAdapter {
// 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 schema only for the legacy mongo format. We'll figure that out later.
createObject(className, schema, object) {
createObject(className: string, schema: SchemaType, object: any) {
schema = convertParseSchemaToMongoSchema(schema);
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
return this._adaptiveCollection(className)
@@ -378,7 +387,7 @@ export class MongoStorageAdapter {
// 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.
deleteObjectsByQuery(className, schema, query) {
deleteObjectsByQuery(className: string, schema: SchemaType, query: QueryType) {
schema = convertParseSchemaToMongoSchema(schema);
return this._adaptiveCollection(className)
.then(collection => {
@@ -396,7 +405,7 @@ export class MongoStorageAdapter {
}
// Apply the update to all objects that match the given Parse Query.
updateObjectsByQuery(className, schema, query, update) {
updateObjectsByQuery(className: string, schema: SchemaType, query: QueryType, update: any) {
schema = convertParseSchemaToMongoSchema(schema);
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
@@ -406,7 +415,7 @@ export class MongoStorageAdapter {
// Atomically finds and updates an object based on query.
// Return value not currently well specified.
findOneAndUpdate(className, schema, query, update) {
findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any) {
schema = convertParseSchemaToMongoSchema(schema);
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
@@ -416,7 +425,7 @@ export class MongoStorageAdapter {
}
// Hopefully we can get rid of this. It's only used for config and hooks.
upsertOneObject(className, schema, query, update) {
upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any) {
schema = convertParseSchemaToMongoSchema(schema);
const mongoUpdate = transformUpdate(className, update, schema);
const mongoWhere = transformWhere(className, query, schema);
@@ -425,7 +434,7 @@ export class MongoStorageAdapter {
}
// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
find(className, schema, query, { skip, limit, sort, keys, readPreference }) {
find(className: string, schema: SchemaType, query: QueryType, { skip, limit, sort, keys, readPreference }: QueryOptions): Promise<any> {
schema = convertParseSchemaToMongoSchema(schema);
const mongoWhere = transformWhere(className, query, schema);
const mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
@@ -453,7 +462,7 @@ export class MongoStorageAdapter {
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
// which is why we use sparse indexes.
ensureUniqueness(className, schema, fieldNames) {
ensureUniqueness(className: string, schema: SchemaType, fieldNames: string[]) {
schema = convertParseSchemaToMongoSchema(schema);
const indexCreationRequest = {};
const mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema));
@@ -471,14 +480,14 @@ export class MongoStorageAdapter {
}
// Used in tests
_rawFind(className, query) {
_rawFind(className: string, query: QueryType) {
return this._adaptiveCollection(className).then(collection => collection.find(query, {
maxTimeMS: this._maxTimeMS,
}));
}
// Executes a count.
count(className, schema, query, readPreference) {
count(className: string, schema: SchemaType, query: QueryType, readPreference: ?string) {
schema = convertParseSchemaToMongoSchema(schema);
readPreference = this._parseReadPreference(readPreference);
return this._adaptiveCollection(className)
@@ -488,7 +497,7 @@ export class MongoStorageAdapter {
}));
}
distinct(className, schema, query, fieldName) {
distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string) {
schema = convertParseSchemaToMongoSchema(schema);
const isPointerField = schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer';
if (isPointerField) {
@@ -505,7 +514,7 @@ export class MongoStorageAdapter {
}));
}
aggregate(className, schema, pipeline, readPreference) {
aggregate(className: string, schema: any, pipeline: any, readPreference: ?string) {
readPreference = this._parseReadPreference(readPreference);
return this._adaptiveCollection(className)
.then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS }))
@@ -521,7 +530,7 @@ export class MongoStorageAdapter {
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)));
}
_parseReadPreference(readPreference) {
_parseReadPreference(readPreference: ?string): ?string {
switch (readPreference) {
case 'PRIMARY':
readPreference = ReadPreference.PRIMARY;
@@ -548,21 +557,21 @@ export class MongoStorageAdapter {
return readPreference;
}
performInitialization() {
performInitialization(): Promise<void> {
return Promise.resolve();
}
createIndex(className, index) {
createIndex(className: string, index: any) {
return this._adaptiveCollection(className)
.then(collection => collection._mongoCollection.createIndex(index));
}
createIndexes(className, indexes) {
createIndexes(className: string, indexes: any) {
return this._adaptiveCollection(className)
.then(collection => collection._mongoCollection.createIndexes(indexes));
}
createIndexesIfNeeded(className, fieldName, type) {
createIndexesIfNeeded(className: string, fieldName: string, type: any) {
if (type && type.type === 'Polygon') {
const index = {
[fieldName]: '2dsphere'
@@ -572,7 +581,7 @@ export class MongoStorageAdapter {
return Promise.resolve();
}
createTextIndexesIfNeeded(className, query, schema) {
createTextIndexesIfNeeded(className: string, query: QueryType, schema: any): Promise<void> {
for(const fieldName in query) {
if (!query[fieldName] || !query[fieldName].$text) {
continue;
@@ -599,22 +608,22 @@ export class MongoStorageAdapter {
return Promise.resolve();
}
getIndexes(className) {
getIndexes(className: string) {
return this._adaptiveCollection(className)
.then(collection => collection._mongoCollection.indexes());
}
dropIndex(className, index) {
dropIndex(className: string, index: any) {
return this._adaptiveCollection(className)
.then(collection => collection._mongoCollection.dropIndex(index));
}
dropAllIndexes(className) {
dropAllIndexes(className: string) {
return this._adaptiveCollection(className)
.then(collection => collection._mongoCollection.dropIndexes());
}
updateSchemaWithIndexes() {
updateSchemaWithIndexes(): Promise<any> {
return this.getAllClasses()
.then((classes) => {
const promises = classes.map((schema) => {
@@ -626,4 +635,3 @@ export class MongoStorageAdapter {
}
export default MongoStorageAdapter;
module.exports = MongoStorageAdapter; // Required for tests

View File

@@ -1,5 +1,8 @@
// @flow
import { createClient } from './PostgresClient';
// @flow-disable-next
import Parse from 'parse/node';
// @flow-disable-next
import _ from 'lodash';
import sql from './sql';
@@ -12,13 +15,17 @@ const PostgresUniqueIndexViolationError = '23505';
const PostgresTransactionAbortedError = '25P02';
const logger = require('../../../logger');
const debug = function(){
let args = [...arguments];
const debug = function(...args: any) {
args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length));
const log = logger.getLogger();
log.debug.apply(log, args);
}
import { StorageAdapter } from '../StorageAdapter';
import type { SchemaType,
QueryType,
QueryOptions } from '../StorageAdapter';
const parseTypeToPostgresType = type => {
switch (type.type) {
case 'String': return 'text';
@@ -563,17 +570,17 @@ const buildWhereClause = ({ schema, query, index }) => {
return { pattern: patterns.join(' AND '), values, sorts };
}
export class PostgresStorageAdapter {
export class PostgresStorageAdapter implements StorageAdapter {
// Private
_collectionPrefix: string;
_client;
_pgp;
_client: any;
_pgp: any;
constructor({
uri,
collectionPrefix = '',
databaseOptions
}) {
}: any) {
this._collectionPrefix = collectionPrefix;
const { client, pgp } = createClient(uri, databaseOptions);
this._client = client;
@@ -587,7 +594,7 @@ export class PostgresStorageAdapter {
this._client.$pool.end();
}
_ensureSchemaCollectionExists(conn) {
_ensureSchemaCollectionExists(conn: any) {
conn = conn || this._client;
return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
.catch(error => {
@@ -601,11 +608,11 @@ export class PostgresStorageAdapter {
});
}
classExists(name) {
classExists(name: string) {
return this._client.one('SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1)', [name], a => a.exists);
}
setClassLevelPermissions(className, CLPs) {
setClassLevelPermissions(className: string, CLPs: any) {
const self = this;
return this._client.task('set-class-level-permissions', function * (t) {
yield self._ensureSchemaCollectionExists(t);
@@ -614,7 +621,7 @@ export class PostgresStorageAdapter {
});
}
setIndexesWithSchemaFormat(className, submittedIndexes, existingIndexes = {}, fields, conn) {
setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any = {}, fields: any, conn: ?any): Promise<void> {
conn = conn || this._client;
const self = this;
if (submittedIndexes === undefined) {
@@ -661,7 +668,7 @@ export class PostgresStorageAdapter {
});
}
createClass(className, schema, conn) {
createClass(className: string, schema: SchemaType, conn: ?any) {
conn = conn || this._client;
return conn.tx('create-class', t => {
const q1 = this.createTable(className, schema, t);
@@ -684,7 +691,7 @@ export class PostgresStorageAdapter {
}
// Just create a table, do not insert in schema
createTable(className, schema, conn) {
createTable(className: string, schema: SchemaType, conn: any) {
conn = conn || this._client;
const self = this;
debug('createTable', className, schema);
@@ -743,7 +750,7 @@ export class PostgresStorageAdapter {
});
}
addFieldIfNotExists(className, fieldName, type) {
addFieldIfNotExists(className: string, fieldName: string, type: any) {
// TODO: Must be revised for invalid logic...
debug('addFieldIfNotExists', {className, fieldName, type});
const self = this;
@@ -781,7 +788,7 @@ export class PostgresStorageAdapter {
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
deleteClass(className) {
deleteClass(className: string) {
const operations = [
{query: `DROP TABLE IF EXISTS $1:name`, values: [className]},
{query: `DELETE FROM "_SCHEMA" WHERE "className" = $1`, values: [className]}
@@ -829,7 +836,7 @@ export class PostgresStorageAdapter {
// may do so.
// Returns a Promise.
deleteFields(className, schema, fieldNames) {
deleteFields(className: string, schema: SchemaType, fieldNames: string[]): Promise<void> {
debug('deleteFields', className, fieldNames);
fieldNames = fieldNames.reduce((list, fieldName) => {
const field = schema.fields[fieldName]
@@ -867,7 +874,7 @@ export class PostgresStorageAdapter {
// 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.
getClass(className) {
getClass(className: string) {
debug('getClass', className);
return this._client.any('SELECT * FROM "_SCHEMA" WHERE "className"=$<className>', { className })
.then(result => {
@@ -880,7 +887,7 @@ export class PostgresStorageAdapter {
}
// TODO: remove the mongo format dependency in the return value
createObject(className, schema, object) {
createObject(className: string, schema: SchemaType, object: any) {
debug('createObject', className, object);
let columnsArray = [];
const valuesArray = [];
@@ -1021,7 +1028,7 @@ export class PostgresStorageAdapter {
// 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.
deleteObjectsByQuery(className, schema, query) {
deleteObjectsByQuery(className: string, schema: SchemaType, query: QueryType) {
debug('deleteObjectsByQuery', className, query);
const values = [className];
const index = 2;
@@ -1048,13 +1055,13 @@ export class PostgresStorageAdapter {
});
}
// Return value not currently well specified.
findOneAndUpdate(className, schema, query, update) {
findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any): Promise<any> {
debug('findOneAndUpdate', className, query, update);
return this.updateObjectsByQuery(className, schema, query, update).then((val) => val[0]);
}
// Apply the update to all objects that match the given Parse Query.
updateObjectsByQuery(className, schema, query, update) {
updateObjectsByQuery(className: string, schema: SchemaType, query: QueryType, update: any): Promise<[any]> {
debug('updateObjectsByQuery', className, query, update);
const updatePatterns = [];
const values = [className]
@@ -1238,7 +1245,7 @@ export class PostgresStorageAdapter {
}
// Hopefully, we can get rid of this. It's only used for config and hooks.
upsertOneObject(className, schema, query, update) {
upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any) {
debug('upsertOneObject', {className, query, update});
const createValue = Object.assign({}, query, update);
return this.createObject(className, schema, createValue).catch((err) => {
@@ -1250,7 +1257,7 @@ export class PostgresStorageAdapter {
});
}
find(className, schema, query, { skip, limit, sort, keys }) {
find(className: string, schema: SchemaType, query: QueryType, { skip, limit, sort, keys }: QueryOptions) {
debug('find', className, query, {skip, limit, sort, keys });
const hasLimit = limit !== undefined;
const hasSkip = skip !== undefined;
@@ -1270,16 +1277,17 @@ export class PostgresStorageAdapter {
let sortPattern = '';
if (sort) {
const sortCopy: any = sort;
const sorting = Object.keys(sort).map((key) => {
// Using $idx pattern gives: non-integer constant in ORDER BY
if (sort[key] === 1) {
if (sortCopy[key] === 1) {
return `"${key}" ASC`;
}
return `"${key}" DESC`;
}).join();
sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : '';
}
if (where.sorts && Object.keys(where.sorts).length > 0) {
if (where.sorts && Object.keys((where.sorts: any)).length > 0) {
sortPattern = `ORDER BY ${where.sorts.join()}`;
}
@@ -1313,7 +1321,7 @@ export class PostgresStorageAdapter {
// Converts from a postgres-format object to a REST-format object.
// Does not strip out anything based on a lack of authentication.
postgresObjectToParseObject(className, object, schema) {
postgresObjectToParseObject(className: string, object: any, schema: any) {
Object.keys(schema.fields).forEach(fieldName => {
if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) {
object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass };
@@ -1392,7 +1400,7 @@ export class PostgresStorageAdapter {
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
// which is why we use sparse indexes.
ensureUniqueness(className, schema, fieldNames) {
ensureUniqueness(className: string, schema: SchemaType, fieldNames: string[]) {
// Use the same name for every ensureUniqueness attempt, because postgres
// Will happily create the same index with multiple names.
const constraintName = `unique_${fieldNames.sort().join('_')}`;
@@ -1412,7 +1420,7 @@ export class PostgresStorageAdapter {
}
// Executes a count.
count(className, schema, query) {
count(className: string, schema: SchemaType, query: QueryType) {
debug('count', className, query);
const values = [className];
const where = buildWhereClause({ schema, query, index: 2 });
@@ -1428,7 +1436,7 @@ export class PostgresStorageAdapter {
});
}
distinct(className, schema, query, fieldName) {
distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string) {
debug('distinct', className, query);
let field = fieldName;
let column = fieldName;
@@ -1476,10 +1484,10 @@ export class PostgresStorageAdapter {
}).then(results => results.map(object => this.postgresObjectToParseObject(className, object, schema)));
}
aggregate(className, schema, pipeline) {
aggregate(className: string, schema: any, pipeline: any) {
debug('aggregate', className, pipeline);
const values = [className];
let columns = [];
let columns: string[] = [];
let countField = null;
let wherePattern = '';
let limitPattern = '';
@@ -1578,7 +1586,7 @@ export class PostgresStorageAdapter {
});
}
performInitialization({ VolatileClassesSchemas }) {
performInitialization({ VolatileClassesSchemas }: any) {
debug('performInitialization');
const promises = VolatileClassesSchemas.map((schema) => {
return this.createTable(schema.className, schema).catch((err) => {
@@ -1610,23 +1618,27 @@ export class PostgresStorageAdapter {
});
}
createIndexes(className, indexes, conn) {
createIndexes(className: string, indexes: any, conn: ?any): Promise<void> {
return (conn || this._client).tx(t => t.batch(indexes.map(i => {
return t.none('CREATE INDEX $1:name ON $2:name ($3:name)', [i.name, className, i.key]);
})));
}
dropIndexes(className, indexes, conn) {
createIndexesIfNeeded(className: string, fieldName: string, type: any, conn: ?any): Promise<void> {
return (conn || this._client).none('CREATE INDEX $1:name ON $2:name ($3:name)', [fieldName, className, type]);
}
dropIndexes(className: string, indexes: any, conn: any): Promise<void> {
const queries = indexes.map(i => ({query: 'DROP INDEX $1:name', values: i}));
return (conn || this._client).tx(t => t.none(this._pgp.helpers.concat(queries)));
}
getIndexes(className) {
getIndexes(className: string) {
const qs = 'SELECT * FROM pg_indexes WHERE tablename = ${className}';
return this._client.any(qs, {className});
}
updateSchemaWithIndexes() {
updateSchemaWithIndexes(): Promise<void> {
return Promise.resolve();
}
}
@@ -1708,10 +1720,10 @@ function createLiteralRegex(remaining) {
}).join('');
}
function literalizeRegexPart(s) {
function literalizeRegexPart(s: string) {
const matcher1 = /\\Q((?!\\E).*)\\E$/
const result1 = s.match(matcher1);
if(result1 && result1.length > 1 && result1.index > -1){
const result1: any = s.match(matcher1);
if(result1 && result1.length > 1 && result1.index > -1) {
// process regex that has a beginning and an end specified for the literal text
const prefix = s.substr(0, result1.index);
const remaining = result1[1];
@@ -1721,7 +1733,7 @@ function literalizeRegexPart(s) {
// process regex that has a beginning specified for the literal text
const matcher2 = /\\Q((?!\\E).*)$/
const result2 = s.match(matcher2);
const result2: any = s.match(matcher2);
if(result2 && result2.length > 1 && result2.index > -1){
const prefix = s.substr(0, result2.index);
const remaining = result2[1];
@@ -1741,4 +1753,3 @@ function literalizeRegexPart(s) {
}
export default PostgresStorageAdapter;
module.exports = PostgresStorageAdapter; // Required for tests

View File

@@ -0,0 +1,53 @@
// @flow
export type SchemaType = any;
export type StorageClass = any;
export type QueryType = any;
export type QueryOptions = {
skip?: number,
limit?: number,
acl?: string[],
sort?: {[string]: number},
count?: boolean | number,
keys?: string[],
op?: string,
distinct?: boolean,
pipeline?: any,
readPreference?: ?string,
};
export type UpdateQueryOptions = {
many?: boolean,
upsert?: boolean
}
export type FullQueryOptions = QueryOptions & UpdateQueryOptions;
export interface StorageAdapter {
classExists(className: string): Promise<boolean>;
setClassLevelPermissions(className: string, clps: any): Promise<void>;
createClass(className: string, schema: SchemaType): Promise<void>;
addFieldIfNotExists(className: string, fieldName: string, type: any): Promise<void>;
deleteClass(className: string): Promise<void>;
deleteAllClasses(): Promise<void>;
deleteFields(className: string, schema: SchemaType, fieldNames: Array<string>): Promise<void>;
getAllClasses(): Promise<StorageClass[]>;
getClass(className: string): Promise<StorageClass>;
createObject(className: string, schema: SchemaType, object: any): Promise<any>;
deleteObjectsByQuery(className: string, schema: SchemaType, query: QueryType): Promise<void>;
updateObjectsByQuery(className: string, schema: SchemaType, query: QueryType, update: any): Promise<[any]>;
findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any): Promise<any>;
upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any): Promise<any>;
find(className: string, schema: SchemaType, query: QueryType, options: QueryOptions): Promise<[any]>;
ensureUniqueness(className: string, schema: SchemaType, fieldNames: Array<string>): Promise<void>;
count(className: string, schema: SchemaType, query: QueryType, readPreference: ?string): Promise<number>;
distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string): Promise<any>;
aggregate(className: string, schema: any, pipeline: any, readPreference: ?string): Promise<any>;
performInitialization(options: ?any): Promise<void>;
// Indexing
createIndexes(className: string, indexes: any, conn: ?any): Promise<void>;
getIndexes(className: string, connection: ?any): Promise<void>;
updateSchemaWithIndexes(): Promise<void>;
setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any, fields: any, conn: ?any): Promise<void>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,9 @@
/** @flow weak */
import * as triggers from "../triggers";
// @flow-disable-next
import * as Parse from "parse/node";
// @flow-disable-next
import * as request from "request";
import { logger } from '../logger';
@@ -28,7 +30,7 @@ export class HooksController {
}
getFunction(functionName) {
return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
return this._getHooks({ functionName: functionName }).then(results => results[0]);
}
getFunctions() {
@@ -36,7 +38,7 @@ export class HooksController {
}
getTrigger(className, triggerName) {
return this._getHooks({ className: className, triggerName: triggerName }, 1).then(results => results[0]);
return this._getHooks({ className: className, triggerName: triggerName }).then(results => results[0]);
}
getTriggers() {

View File

@@ -1,3 +1,4 @@
// @flow
// This class handles schema validation, persistence, and modification.
//
// Each individual Schema object should be immutable. The helpers to
@@ -13,9 +14,19 @@
// DatabaseController. This will let us replace the schema logic for
// different databases.
// TODO: hide all schema logic inside the database adapter.
// @flow-disable-next
const Parse = require('parse/node').Parse;
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
import DatabaseController from './DatabaseController';
import type {
Schema,
SchemaFields,
ClassLevelPermissions,
SchemaField,
LoadSchemaOptions,
} from './types';
const defaultColumns = Object.freeze({
const defaultColumns: {[string]: SchemaFields} = Object.freeze({
// Contain the default columns for every parse object type (except _Join collection)
_Default: {
"objectId": {type:'String'},
@@ -158,7 +169,7 @@ function verifyPermissionKey(key) {
}
const CLPValidKeys = Object.freeze(['find', 'count', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']);
function validateCLP(perms, fields) {
function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields) {
if (!perms) {
return;
}
@@ -166,9 +177,13 @@ function validateCLP(perms, fields) {
if (CLPValidKeys.indexOf(operation) == -1) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`);
}
if (!perms[operation]) {
return;
}
if (operation === 'readUserFields' || operation === 'writeUserFields') {
if (!Array.isArray(perms[operation])) {
// @flow-disable-next
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perms[operation]}' is not a valid value for class level permissions ${operation}`);
} else {
perms[operation].forEach((key) => {
@@ -180,10 +195,13 @@ function validateCLP(perms, fields) {
return;
}
// @flow-disable-next
Object.keys(perms[operation]).forEach((key) => {
verifyPermissionKey(key);
// @flow-disable-next
const perm = perms[operation][key];
if (perm !== true) {
// @flow-disable-next
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`);
}
});
@@ -191,7 +209,7 @@ function validateCLP(perms, fields) {
}
const joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
const classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
function classNameIsValid(className) {
function classNameIsValid(className: string): boolean {
// Valid classes must:
return (
// Be one of _User, _Installation, _Role, _Session OR
@@ -204,12 +222,12 @@ function classNameIsValid(className) {
}
// Valid fields must be alpha-numeric, and not start with an underscore or number
function fieldNameIsValid(fieldName) {
function fieldNameIsValid(fieldName: string): boolean {
return classAndFieldRegex.test(fieldName);
}
// Checks that it's not trying to clobber one of the default fields of the class.
function fieldNameIsValidForClass(fieldName, className) {
function fieldNameIsValidForClass(fieldName: string, className: string): boolean {
if (!fieldNameIsValid(fieldName)) {
return false;
}
@@ -222,7 +240,7 @@ function fieldNameIsValidForClass(fieldName, className) {
return true;
}
function invalidClassNameMessage(className) {
function invalidClassNameMessage(className: string): string {
return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character ';
}
@@ -261,7 +279,7 @@ const fieldTypeIsInvalid = ({ type, targetClass }) => {
return undefined;
}
const convertSchemaToAdapterSchema = schema => {
const convertSchemaToAdapterSchema = (schema: any) => {
schema = injectDefaultSchema(schema);
delete schema.fields.ACL;
schema.fields._rperm = { type: 'Array' };
@@ -294,8 +312,8 @@ const convertAdapterSchemaToParseSchema = ({...schema}) => {
return schema;
}
const injectDefaultSchema = ({className, fields, classLevelPermissions, indexes}) => {
const defaultSchema = {
const injectDefaultSchema = ({className, fields, classLevelPermissions, indexes}: Schema) => {
const defaultSchema: Schema = {
className,
fields: {
...defaultColumns._Default,
@@ -329,11 +347,12 @@ const _JobScheduleSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
}));
const _AudienceSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
className: "_Audience",
fields: defaultColumns._Audience
fields: defaultColumns._Audience,
classLevelPermissions: {}
}));
const VolatileClassesSchemas = [_HooksSchema, _JobStatusSchema, _JobScheduleSchema, _PushStatusSchema, _GlobalConfigSchema, _AudienceSchema];
const dbTypeMatchesObjectType = (dbType, objectType) => {
const dbTypeMatchesObjectType = (dbType: SchemaField | string, objectType: SchemaField) => {
if (dbType.type !== objectType.type) return false;
if (dbType.targetClass !== objectType.targetClass) return false;
if (dbType === objectType.type) return true;
@@ -341,22 +360,27 @@ const dbTypeMatchesObjectType = (dbType, objectType) => {
return false;
}
const typeToString = (type) => {
const typeToString = (type: SchemaField | string): string => {
if (typeof type === 'string') {
return type;
}
if (type.targetClass) {
return `${type.type}<${type.targetClass}>`;
}
return `${type.type || type}`;
return `${type.type}`;
}
// Stores the entire schema of the app in a weird hybrid format somewhere between
// the mongo format and the Parse format. Soon, this will all be Parse format.
export default class SchemaController {
_dbAdapter;
data;
perms;
indexes;
_dbAdapter: StorageAdapter;
data: any;
perms: any;
indexes: any;
_cache: any;
reloadDataPromise: Promise<any>;
constructor(databaseAdapter, schemaCache) {
constructor(databaseAdapter: StorageAdapter, schemaCache: any) {
this._dbAdapter = databaseAdapter;
this._cache = schemaCache;
// this.data[className][fieldName] tells you the type of that field, in mongo format
@@ -367,7 +391,7 @@ export default class SchemaController {
this.indexes = {};
}
reloadData(options = {clearCache: false}) {
reloadData(options: LoadSchemaOptions = {clearCache: false}): Promise<any> {
let promise = Promise.resolve();
if (options.clearCache) {
promise = promise.then(() => {
@@ -378,9 +402,7 @@ export default class SchemaController {
return this.reloadDataPromise;
}
this.reloadDataPromise = promise.then(() => {
return this.getAllClasses(options);
})
.then(allSchemas => {
return this.getAllClasses(options).then((allSchemas) => {
const data = {};
const perms = {};
const indexes = {};
@@ -392,7 +414,7 @@ export default class SchemaController {
// Inject the in-memory classes
volatileClasses.forEach(className => {
const schema = injectDefaultSchema({ className });
const schema = injectDefaultSchema({ className, fields: {}, classLevelPermissions: {} });
data[className] = schema.fields;
perms[className] = schema.classLevelPermissions;
indexes[className] = schema.indexes;
@@ -407,11 +429,12 @@ export default class SchemaController {
this.indexes = {};
delete this.reloadDataPromise;
throw err;
});
})
}).then(() => {});
return this.reloadDataPromise;
}
getAllClasses(options = {clearCache: false}) {
getAllClasses(options: LoadSchemaOptions = {clearCache: false}): Promise<Array<Schema>> {
let promise = Promise.resolve();
if (options.clearCache) {
promise = this._cache.clear();
@@ -432,7 +455,7 @@ export default class SchemaController {
});
}
getOneSchema(className, allowVolatileClasses = false, options = {clearCache: false}) {
getOneSchema(className: string, allowVolatileClasses: boolean = false, options: LoadSchemaOptions = {clearCache: false}): Promise<Schema> {
let promise = Promise.resolve();
if (options.clearCache) {
promise = this._cache.clear();
@@ -468,7 +491,7 @@ export default class SchemaController {
// on success, and rejects with an error on fail. Ensure you
// have authorization (master key, or client class creation
// enabled) before calling this function.
addClassIfNotExists(className, fields = {}, classLevelPermissions, indexes = {}) {
addClassIfNotExists(className: string, fields: SchemaFields = {}, classLevelPermissions: any, indexes: any = {}): Promise<void> {
var validationError = this.validateNewClass(className, fields, classLevelPermissions);
if (validationError) {
return Promise.reject(validationError);
@@ -490,7 +513,7 @@ export default class SchemaController {
});
}
updateClass(className, submittedFields, classLevelPermissions, indexes, database) {
updateClass(className: string, submittedFields: SchemaFields, classLevelPermissions: any, indexes: any, database: DatabaseController) {
return this.getOneSchema(className)
.then(schema => {
const existingFields = schema.fields;
@@ -514,7 +537,7 @@ export default class SchemaController {
// Finally we have checked to make sure the request is valid and we can start deleting fields.
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
const deletedFields = [];
const deletedFields: string[] = [];
const insertedFields = [];
Object.keys(submittedFields).forEach(fieldName => {
if (submittedFields[fieldName].__op === 'Delete') {
@@ -542,7 +565,7 @@ export default class SchemaController {
.then(() => this.reloadData({ clearCache: true }))
//TODO: Move this logic into the database adapter
.then(() => {
const reloadedSchema = {
const reloadedSchema: Schema = {
className: className,
fields: this.data[className],
classLevelPermissions: this.perms[className],
@@ -564,7 +587,7 @@ export default class SchemaController {
// Returns a promise that resolves successfully to the new schema
// object or fails with a reason.
enforceClassExists(className) {
enforceClassExists(className: string): Promise<SchemaController> {
if (this.data[className]) {
return Promise.resolve(this);
}
@@ -593,7 +616,7 @@ export default class SchemaController {
});
}
validateNewClass(className, fields = {}, classLevelPermissions) {
validateNewClass(className: string, fields: SchemaFields = {}, classLevelPermissions: any): any {
if (this.data[className]) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
}
@@ -606,7 +629,7 @@ export default class SchemaController {
return this.validateSchemaData(className, fields, classLevelPermissions, []);
}
validateSchemaData(className, fields, classLevelPermissions, existingFieldNames) {
validateSchemaData(className: string, fields: SchemaFields, classLevelPermissions: ClassLevelPermissions, existingFieldNames: Array<string>) {
for (const fieldName in fields) {
if (existingFieldNames.indexOf(fieldName) < 0) {
if (!fieldNameIsValid(fieldName)) {
@@ -641,7 +664,7 @@ export default class SchemaController {
}
// Sets the Class-level permissions for a given className, which must exist.
setPermissions(className, perms, newSchema) {
setPermissions(className: string, perms: any, newSchema: SchemaFields) {
if (typeof perms === 'undefined') {
return Promise.resolve();
}
@@ -653,7 +676,7 @@ export default class SchemaController {
// object if the provided className-fieldName-type tuple is valid.
// The className must already be validated.
// If 'freeze' is true, refuse to update the schema for this field.
enforceFieldExists(className, fieldName, type) {
enforceFieldExists(className: string, fieldName: string, type: string | SchemaField) {
if (fieldName.indexOf(".") > 0) {
// subdocument key (x.y) => ok if x is of type 'object'
fieldName = fieldName.split(".")[ 0 ];
@@ -698,7 +721,11 @@ export default class SchemaController {
return this.reloadData({ clearCache: true });
}).then(() => {
// Ensure that the schema now validates
if (!dbTypeMatchesObjectType(this.getExpectedType(className, fieldName), type)) {
const expectedType = this.getExpectedType(className, fieldName);
if (typeof type === 'string') {
type = { type };
}
if (!expectedType || !dbTypeMatchesObjectType(expectedType, type)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `Could not add field ${fieldName}`);
}
// Remove the cached schema
@@ -709,7 +736,7 @@ export default class SchemaController {
}
// maintain compatibility
deleteField(fieldName, className, database) {
deleteField(fieldName: string, className: string, database: DatabaseController) {
return this.deleteFields([fieldName], className, database);
}
@@ -720,7 +747,7 @@ export default class SchemaController {
// Passing the database and prefix is necessary in order to drop relation collections
// and remove fields from objects. Ideally the database would belong to
// a database adapter and this function would close over it or access it via member.
deleteFields(fieldNames, className, database) {
deleteFields(fieldNames: Array<string>, className: string, database: DatabaseController) {
if (!classNameIsValid(className)) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
}
@@ -770,7 +797,7 @@ export default class SchemaController {
// Validates an object provided in REST format.
// Returns a promise that resolves to the new schema if this object is
// valid.
validateObject(className, object, query) {
validateObject(className: string, object: any, query: any) {
let geocount = 0;
let promise = this.enforceClassExists(className);
for (const fieldName in object) {
@@ -804,7 +831,7 @@ export default class SchemaController {
}
// Validates that all the properties are set for the object
validateRequiredColumns(className, object, query) {
validateRequiredColumns(className: string, object: any, query: any) {
const columns = requiredColumns[className];
if (!columns || columns.length == 0) {
return Promise.resolve(this);
@@ -831,7 +858,7 @@ export default class SchemaController {
}
// Validates the base CLP for an operation
testBaseCLP(className, aclGroup, operation) {
testBaseCLP(className: string, aclGroup: string[], operation: string) {
if (!this.perms[className] || !this.perms[className][operation]) {
return true;
}
@@ -849,7 +876,7 @@ export default class SchemaController {
}
// Validates an operation passes class-level-permissions set in the schema
validatePermission(className, aclGroup, operation) {
validatePermission(className: string, aclGroup: string[], operation: string) {
if (this.testBaseCLP(className, aclGroup, operation)) {
return Promise.resolve();
@@ -897,7 +924,7 @@ export default class SchemaController {
// Returns the expected type for a className+key combination
// or undefined if the schema is not set
getExpectedType(className, fieldName) {
getExpectedType(className: string, fieldName: string): ?(SchemaField | string) {
if (this.data && this.data[className]) {
const expectedType = this.data[className][fieldName]
return expectedType === 'map' ? 'Object' : expectedType;
@@ -906,13 +933,13 @@ export default class SchemaController {
}
// Checks if a given class is in the schema.
hasClass(className) {
hasClass(className: string) {
return this.reloadData().then(() => !!(this.data[className]));
}
}
// Returns a promise for a new Schema.
const load = (dbAdapter, schemaCache, options) => {
const load = (dbAdapter: StorageAdapter, schemaCache: any, options: any): Promise<SchemaController> => {
const schema = new SchemaController(dbAdapter, schemaCache);
return schema.reloadData(options).then(() => schema);
}
@@ -922,8 +949,9 @@ const load = (dbAdapter, schemaCache, options) => {
// does not include the default fields, as it is intended to be passed
// to mongoSchemaFromFieldsAndClassName. No validation is done here, it
// is done in mongoSchemaFromFieldsAndClassName.
function buildMergedSchemaObject(existingFields, putRequest) {
function buildMergedSchemaObject(existingFields: SchemaFields, putRequest: any): SchemaFields {
const newSchema = {};
// @flow-disable-next
const sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]);
for (const oldField in existingFields) {
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
@@ -960,7 +988,7 @@ function thenValidateRequiredColumns(schemaPromise, className, object, query) {
// type system.
// The output should be a valid schema value.
// TODO: ensure that this is compatible with the format used in Open DB
function getType(obj) {
function getType(obj: any): ?(SchemaField | string) {
const type = typeof obj;
switch(type) {
case 'boolean':
@@ -986,7 +1014,7 @@ function getType(obj) {
// This gets the type for non-JSON types like pointers and files, but
// also gets the appropriate type for $ operators.
// Returns null if the type is unknown.
function getObjectType(obj) {
function getObjectType(obj): ?(SchemaField | string) {
if (obj instanceof Array) {
return 'Array';
}
@@ -1074,4 +1102,5 @@ export {
defaultColumns,
convertSchemaToAdapterSchema,
VolatileClassesSchemas,
SchemaController,
};

29
src/Controllers/types.js Normal file
View File

@@ -0,0 +1,29 @@
export type LoadSchemaOptions = {
clearCache: boolean
};
export type SchemaField = {
type: string;
targetClass?: ?string;
}
export type SchemaFields = { [string]: SchemaField }
export type Schema = {
className: string,
fields: SchemaFields,
classLevelPermissions: ClassLevelPermissions,
indexes?: ?any
};
export type ClassLevelPermissions = {
find?: {[string]: boolean};
count?: {[string]: boolean};
get?: {[string]: boolean};
create?: {[string]: boolean};
update?: {[string]: boolean};
delete?: {[string]: boolean};
addField?: {[string]: boolean};
readUserFields?: string[];
writeUserFields?: string[];
};

View File

@@ -1,4 +1,5 @@
// @flow
// @flow-disable-next
import deepcopy from 'deepcopy';
import AdaptableController from '../Controllers/AdaptableController';
import { master } from '../Auth';