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:
@@ -7,3 +7,4 @@
|
|||||||
[libs]
|
[libs]
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
suppress_comment= \\(.\\|\n\\)*\\@flow-disable-next
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"deep-diff": "0.3.8",
|
"deep-diff": "0.3.8",
|
||||||
"eslint": "^4.9.0",
|
"eslint": "^4.9.0",
|
||||||
"eslint-plugin-flowtype": "^2.39.1",
|
"eslint-plugin-flowtype": "^2.39.1",
|
||||||
|
"flow-bin": "^0.59.0",
|
||||||
"gaze": "1.1.2",
|
"gaze": "1.1.2",
|
||||||
"jasmine": "2.8.0",
|
"jasmine": "2.8.0",
|
||||||
"jasmine-spec-reporter": "^4.1.0",
|
"jasmine-spec-reporter": "^4.1.0",
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run build && node bin/dev",
|
"dev": "npm run build && node bin/dev",
|
||||||
"lint": "eslint --cache ./",
|
"lint": "flow && eslint --cache ./",
|
||||||
"build": "babel src/ -d lib/ --copy-files",
|
"build": "babel src/ -d lib/ --copy-files",
|
||||||
"pretest": "npm run lint",
|
"pretest": "npm run lint",
|
||||||
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 jasmine",
|
"test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 jasmine",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||||
const MongoClient = require('mongodb').MongoClient;
|
const { MongoClient } = require('mongodb');
|
||||||
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||||
|
|
||||||
// These tests are specific to the mongo storage adapter + mongo storage format
|
// These tests are specific to the mongo storage adapter + mongo storage format
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const TestObject = Parse.Object.extend('TestObject');
|
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 mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||||
const rp = require('request-promise');
|
const rp = require('request-promise');
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use strict';
|
'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 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 postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||||
const Parse = require('parse/node');
|
const Parse = require('parse/node');
|
||||||
const rp = require('request-promise');
|
const rp = require('request-promise');
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
/* Tests for ParseServer.js */
|
/* Tests for ParseServer.js */
|
||||||
const express = require('express');
|
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';
|
import ParseServer from '../src/ParseServer';
|
||||||
|
|
||||||
describe('Server Url Checks', () => {
|
describe('Server Url Checks', () => {
|
||||||
@@ -35,8 +36,6 @@ describe('Server Url Checks', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handleShutdown, close connection', (done) => {
|
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 mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||||
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||||
let databaseAdapter;
|
let databaseAdapter;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const Parse = require('parse/node').Parse;
|
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 postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||||
const ParseServer = require("../src/index");
|
const ParseServer = require("../src/index");
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
|||||||
@@ -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';
|
const databaseURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database';
|
||||||
|
|
||||||
describe_only_db('postgres')('PostgresStorageAdapter', () => {
|
describe_only_db('postgres')('PostgresStorageAdapter', () => {
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ var cache = require('../src/cache').default;
|
|||||||
var ParseServer = require('../src/index').ParseServer;
|
var ParseServer = require('../src/index').ParseServer;
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var TestUtils = require('../src/TestUtils');
|
var TestUtils = require('../src/TestUtils');
|
||||||
var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
|
||||||
const GridStoreAdapter = require('../src/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
const GridStoreAdapter = require('../src/Adapters/Files/GridStoreAdapter').GridStoreAdapter;
|
||||||
const FSAdapter = require('@parse/fs-files-adapter');
|
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 RedisCacheAdapter = require('../src/Adapters/Cache/RedisCacheAdapter').default;
|
||||||
|
|
||||||
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ var ParseServer = require("../src/index");
|
|||||||
var Config = require('../src/Config');
|
var Config = require('../src/Config');
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
|
|
||||||
const MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter');
|
import MongoStorageAdapter from '../src/Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||||
|
|
||||||
describe('server', () => {
|
describe('server', () => {
|
||||||
it('requires a master key and app id', done => {
|
it('requires a master key and app id', done => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
@flow weak
|
@flow weak
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// @flow-disable-next
|
||||||
import { MongoClient, GridStore, Db} from 'mongodb';
|
import { MongoClient, GridStore, Db} from 'mongodb';
|
||||||
import { FilesAdapter } from './FilesAdapter';
|
import { FilesAdapter } from './FilesAdapter';
|
||||||
import defaults from '../../defaults';
|
import defaults from '../../defaults';
|
||||||
|
|||||||
@@ -137,6 +137,18 @@ class MongoSchemaCollection {
|
|||||||
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []);
|
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) {
|
updateSchema(name: string, update) {
|
||||||
return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update);
|
return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
// @flow
|
||||||
import MongoCollection from './MongoCollection';
|
import MongoCollection from './MongoCollection';
|
||||||
import MongoSchemaCollection from './MongoSchemaCollection';
|
import MongoSchemaCollection from './MongoSchemaCollection';
|
||||||
|
import { StorageAdapter } from '../StorageAdapter';
|
||||||
|
import type { SchemaType,
|
||||||
|
QueryType,
|
||||||
|
StorageClass,
|
||||||
|
QueryOptions } from '../StorageAdapter';
|
||||||
import {
|
import {
|
||||||
parse as parseUrl,
|
parse as parseUrl,
|
||||||
format as formatUrl,
|
format as formatUrl,
|
||||||
@@ -12,10 +18,13 @@ import {
|
|||||||
transformUpdate,
|
transformUpdate,
|
||||||
transformPointerString,
|
transformPointerString,
|
||||||
} from './MongoTransform';
|
} from './MongoTransform';
|
||||||
|
// @flow-disable-next
|
||||||
import Parse from 'parse/node';
|
import Parse from 'parse/node';
|
||||||
|
// @flow-disable-next
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import defaults from '../../../defaults';
|
import defaults from '../../../defaults';
|
||||||
|
|
||||||
|
// @flow-disable-next
|
||||||
const mongodb = require('mongodb');
|
const mongodb = require('mongodb');
|
||||||
const MongoClient = mongodb.MongoClient;
|
const MongoClient = mongodb.MongoClient;
|
||||||
const ReadPreference = mongodb.ReadPreference;
|
const ReadPreference = mongodb.ReadPreference;
|
||||||
@@ -59,7 +68,8 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
|
|||||||
_id: className,
|
_id: className,
|
||||||
objectId: 'string',
|
objectId: 'string',
|
||||||
updatedAt: 'string',
|
updatedAt: 'string',
|
||||||
createdAt: 'string'
|
createdAt: 'string',
|
||||||
|
_metadata: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const fieldName in fields) {
|
for (const fieldName in fields) {
|
||||||
@@ -80,24 +90,31 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
|
|||||||
mongoObject._metadata.indexes = indexes;
|
mongoObject._metadata.indexes = indexes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!mongoObject._metadata) { // cleanup the unused _metadata
|
||||||
|
delete mongoObject._metadata;
|
||||||
|
}
|
||||||
|
|
||||||
return mongoObject;
|
return mongoObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class MongoStorageAdapter {
|
export class MongoStorageAdapter implements StorageAdapter {
|
||||||
// Private
|
// Private
|
||||||
_uri: string;
|
_uri: string;
|
||||||
_collectionPrefix: string;
|
_collectionPrefix: string;
|
||||||
_mongoOptions: Object;
|
_mongoOptions: Object;
|
||||||
// Public
|
// Public
|
||||||
connectionPromise;
|
connectionPromise: Promise<any>;
|
||||||
database;
|
database: any;
|
||||||
canSortOnJoinTables;
|
client: MongoClient;
|
||||||
|
_maxTimeMS: ?number;
|
||||||
|
canSortOnJoinTables: boolean;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
uri = defaults.DefaultMongoURI,
|
uri = defaults.DefaultMongoURI,
|
||||||
collectionPrefix = '',
|
collectionPrefix = '',
|
||||||
mongoOptions = {},
|
mongoOptions = {},
|
||||||
}) {
|
}: any) {
|
||||||
this._uri = uri;
|
this._uri = uri;
|
||||||
this._collectionPrefix = collectionPrefix;
|
this._collectionPrefix = collectionPrefix;
|
||||||
this._mongoOptions = mongoOptions;
|
this._mongoOptions = mongoOptions;
|
||||||
@@ -156,13 +173,13 @@ export class MongoStorageAdapter {
|
|||||||
.then(rawCollection => new MongoCollection(rawCollection));
|
.then(rawCollection => new MongoCollection(rawCollection));
|
||||||
}
|
}
|
||||||
|
|
||||||
_schemaCollection() {
|
_schemaCollection(): Promise<MongoSchemaCollection> {
|
||||||
return this.connect()
|
return this.connect()
|
||||||
.then(() => this._adaptiveCollection(MongoSchemaCollectionName))
|
.then(() => this._adaptiveCollection(MongoSchemaCollectionName))
|
||||||
.then(collection => new MongoSchemaCollection(collection));
|
.then(collection => new MongoSchemaCollection(collection));
|
||||||
}
|
}
|
||||||
|
|
||||||
classExists(name) {
|
classExists(name: string) {
|
||||||
return this.connect().then(() => {
|
return this.connect().then(() => {
|
||||||
return this.database.listCollections({ name: this._collectionPrefix + name }).toArray();
|
return this.database.listCollections({ name: this._collectionPrefix + name }).toArray();
|
||||||
}).then(collections => {
|
}).then(collections => {
|
||||||
@@ -170,14 +187,14 @@ export class MongoStorageAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setClassLevelPermissions(className, CLPs) {
|
setClassLevelPermissions(className: string, CLPs: any): Promise<void> {
|
||||||
return this._schemaCollection()
|
return this._schemaCollection()
|
||||||
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||||
$set: { '_metadata.class_permissions': CLPs }
|
$set: { '_metadata.class_permissions': CLPs }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
setIndexesWithSchemaFormat(className, submittedIndexes, existingIndexes = {}, fields) {
|
setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any = {}, fields: any): Promise<void> {
|
||||||
if (submittedIndexes === undefined) {
|
if (submittedIndexes === undefined) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -223,7 +240,7 @@ export class MongoStorageAdapter {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
setIndexesFromMongo(className) {
|
setIndexesFromMongo(className: string) {
|
||||||
return this.getIndexes(className).then((indexes) => {
|
return this.getIndexes(className).then((indexes) => {
|
||||||
indexes = indexes.reduce((obj, index) => {
|
indexes = indexes.reduce((obj, index) => {
|
||||||
if (index.key._fts) {
|
if (index.key._fts) {
|
||||||
@@ -246,24 +263,16 @@ export class MongoStorageAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createClass(className, schema) {
|
createClass(className: string, schema: SchemaType): Promise<void> {
|
||||||
schema = convertParseSchemaToMongoSchema(schema);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes);
|
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes);
|
||||||
mongoObject._id = className;
|
mongoObject._id = className;
|
||||||
return this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields)
|
return this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields)
|
||||||
.then(() => this._schemaCollection())
|
.then(() => this._schemaCollection())
|
||||||
.then(schemaCollection => schemaCollection._collection.insertOne(mongoObject))
|
.then(schemaCollection => schemaCollection.insertSchema(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) {
|
addFieldIfNotExists(className: string, fieldName: string, type: any): Promise<void> {
|
||||||
return this._schemaCollection()
|
return this._schemaCollection()
|
||||||
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type))
|
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type))
|
||||||
.then(() => this.createIndexesIfNeeded(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.)
|
// 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.
|
// 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)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection.drop())
|
.then(collection => collection.drop())
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -312,7 +321,7 @@ export class MongoStorageAdapter {
|
|||||||
// may do so.
|
// may do so.
|
||||||
|
|
||||||
// Returns a Promise.
|
// Returns a Promise.
|
||||||
deleteFields(className, schema, fieldNames) {
|
deleteFields(className: string, schema: SchemaType, fieldNames: string[]) {
|
||||||
const mongoFormatNames = fieldNames.map(fieldName => {
|
const mongoFormatNames = fieldNames.map(fieldName => {
|
||||||
if (schema.fields[fieldName].type === 'Pointer') {
|
if (schema.fields[fieldName].type === 'Pointer') {
|
||||||
return `_p_${fieldName}`
|
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
|
// 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
|
// schemas cannot be retrieved, returns a promise that rejects. Requirements for the
|
||||||
// rejection reason are TBD.
|
// rejection reason are TBD.
|
||||||
getAllClasses() {
|
getAllClasses(): Promise<StorageClass[]> {
|
||||||
return this._schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA());
|
return this._schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a promise for the schema with the given name, in Parse format. If
|
// 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
|
// this adapter doesn't know about the schema, return a promise that rejects with
|
||||||
// undefined as the reason.
|
// undefined as the reason.
|
||||||
getClass(className) {
|
getClass(className: string): Promise<StorageClass> {
|
||||||
return this._schemaCollection()
|
return this._schemaCollection()
|
||||||
.then(schemasCollection => schemasCollection._fetchOneSchemaFrom_SCHEMA(className))
|
.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,
|
// 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
|
// 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.
|
// 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);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
|
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
@@ -378,7 +387,7 @@ export class MongoStorageAdapter {
|
|||||||
// Remove all objects that match the given Parse Query.
|
// 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 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.
|
// 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);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => {
|
.then(collection => {
|
||||||
@@ -396,7 +405,7 @@ export class MongoStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply the update to all objects that match the given Parse Query.
|
// 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);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const mongoUpdate = transformUpdate(className, update, schema);
|
const mongoUpdate = transformUpdate(className, update, schema);
|
||||||
const mongoWhere = transformWhere(className, query, schema);
|
const mongoWhere = transformWhere(className, query, schema);
|
||||||
@@ -406,7 +415,7 @@ export class MongoStorageAdapter {
|
|||||||
|
|
||||||
// Atomically finds and updates an object based on query.
|
// Atomically finds and updates an object based on query.
|
||||||
// Return value not currently well specified.
|
// Return value not currently well specified.
|
||||||
findOneAndUpdate(className, schema, query, update) {
|
findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any) {
|
||||||
schema = convertParseSchemaToMongoSchema(schema);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const mongoUpdate = transformUpdate(className, update, schema);
|
const mongoUpdate = transformUpdate(className, update, schema);
|
||||||
const mongoWhere = transformWhere(className, query, 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.
|
// 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);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const mongoUpdate = transformUpdate(className, update, schema);
|
const mongoUpdate = transformUpdate(className, update, schema);
|
||||||
const mongoWhere = transformWhere(className, query, 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 }.
|
// 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);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const mongoWhere = transformWhere(className, query, schema);
|
const mongoWhere = transformWhere(className, query, schema);
|
||||||
const mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, 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
|
// 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,
|
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
|
||||||
// which is why we use sparse indexes.
|
// which is why we use sparse indexes.
|
||||||
ensureUniqueness(className, schema, fieldNames) {
|
ensureUniqueness(className: string, schema: SchemaType, fieldNames: string[]) {
|
||||||
schema = convertParseSchemaToMongoSchema(schema);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const indexCreationRequest = {};
|
const indexCreationRequest = {};
|
||||||
const mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema));
|
const mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema));
|
||||||
@@ -471,14 +480,14 @@ export class MongoStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Used in tests
|
// Used in tests
|
||||||
_rawFind(className, query) {
|
_rawFind(className: string, query: QueryType) {
|
||||||
return this._adaptiveCollection(className).then(collection => collection.find(query, {
|
return this._adaptiveCollection(className).then(collection => collection.find(query, {
|
||||||
maxTimeMS: this._maxTimeMS,
|
maxTimeMS: this._maxTimeMS,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Executes a count.
|
// Executes a count.
|
||||||
count(className, schema, query, readPreference) {
|
count(className: string, schema: SchemaType, query: QueryType, readPreference: ?string) {
|
||||||
schema = convertParseSchemaToMongoSchema(schema);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
readPreference = this._parseReadPreference(readPreference);
|
readPreference = this._parseReadPreference(readPreference);
|
||||||
return this._adaptiveCollection(className)
|
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);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const isPointerField = schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer';
|
const isPointerField = schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer';
|
||||||
if (isPointerField) {
|
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);
|
readPreference = this._parseReadPreference(readPreference);
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS }))
|
.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)));
|
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)));
|
||||||
}
|
}
|
||||||
|
|
||||||
_parseReadPreference(readPreference) {
|
_parseReadPreference(readPreference: ?string): ?string {
|
||||||
switch (readPreference) {
|
switch (readPreference) {
|
||||||
case 'PRIMARY':
|
case 'PRIMARY':
|
||||||
readPreference = ReadPreference.PRIMARY;
|
readPreference = ReadPreference.PRIMARY;
|
||||||
@@ -548,21 +557,21 @@ export class MongoStorageAdapter {
|
|||||||
return readPreference;
|
return readPreference;
|
||||||
}
|
}
|
||||||
|
|
||||||
performInitialization() {
|
performInitialization(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
createIndex(className, index) {
|
createIndex(className: string, index: any) {
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection._mongoCollection.createIndex(index));
|
.then(collection => collection._mongoCollection.createIndex(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
createIndexes(className, indexes) {
|
createIndexes(className: string, indexes: any) {
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection._mongoCollection.createIndexes(indexes));
|
.then(collection => collection._mongoCollection.createIndexes(indexes));
|
||||||
}
|
}
|
||||||
|
|
||||||
createIndexesIfNeeded(className, fieldName, type) {
|
createIndexesIfNeeded(className: string, fieldName: string, type: any) {
|
||||||
if (type && type.type === 'Polygon') {
|
if (type && type.type === 'Polygon') {
|
||||||
const index = {
|
const index = {
|
||||||
[fieldName]: '2dsphere'
|
[fieldName]: '2dsphere'
|
||||||
@@ -572,7 +581,7 @@ export class MongoStorageAdapter {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
createTextIndexesIfNeeded(className, query, schema) {
|
createTextIndexesIfNeeded(className: string, query: QueryType, schema: any): Promise<void> {
|
||||||
for(const fieldName in query) {
|
for(const fieldName in query) {
|
||||||
if (!query[fieldName] || !query[fieldName].$text) {
|
if (!query[fieldName] || !query[fieldName].$text) {
|
||||||
continue;
|
continue;
|
||||||
@@ -599,22 +608,22 @@ export class MongoStorageAdapter {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndexes(className) {
|
getIndexes(className: string) {
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection._mongoCollection.indexes());
|
.then(collection => collection._mongoCollection.indexes());
|
||||||
}
|
}
|
||||||
|
|
||||||
dropIndex(className, index) {
|
dropIndex(className: string, index: any) {
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection._mongoCollection.dropIndex(index));
|
.then(collection => collection._mongoCollection.dropIndex(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
dropAllIndexes(className) {
|
dropAllIndexes(className: string) {
|
||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection._mongoCollection.dropIndexes());
|
.then(collection => collection._mongoCollection.dropIndexes());
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSchemaWithIndexes() {
|
updateSchemaWithIndexes(): Promise<any> {
|
||||||
return this.getAllClasses()
|
return this.getAllClasses()
|
||||||
.then((classes) => {
|
.then((classes) => {
|
||||||
const promises = classes.map((schema) => {
|
const promises = classes.map((schema) => {
|
||||||
@@ -626,4 +635,3 @@ export class MongoStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default MongoStorageAdapter;
|
export default MongoStorageAdapter;
|
||||||
module.exports = MongoStorageAdapter; // Required for tests
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
// @flow
|
||||||
import { createClient } from './PostgresClient';
|
import { createClient } from './PostgresClient';
|
||||||
|
// @flow-disable-next
|
||||||
import Parse from 'parse/node';
|
import Parse from 'parse/node';
|
||||||
|
// @flow-disable-next
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import sql from './sql';
|
import sql from './sql';
|
||||||
|
|
||||||
@@ -12,13 +15,17 @@ const PostgresUniqueIndexViolationError = '23505';
|
|||||||
const PostgresTransactionAbortedError = '25P02';
|
const PostgresTransactionAbortedError = '25P02';
|
||||||
const logger = require('../../../logger');
|
const logger = require('../../../logger');
|
||||||
|
|
||||||
const debug = function(){
|
const debug = function(...args: any) {
|
||||||
let args = [...arguments];
|
|
||||||
args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length));
|
args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length));
|
||||||
const log = logger.getLogger();
|
const log = logger.getLogger();
|
||||||
log.debug.apply(log, args);
|
log.debug.apply(log, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { StorageAdapter } from '../StorageAdapter';
|
||||||
|
import type { SchemaType,
|
||||||
|
QueryType,
|
||||||
|
QueryOptions } from '../StorageAdapter';
|
||||||
|
|
||||||
const parseTypeToPostgresType = type => {
|
const parseTypeToPostgresType = type => {
|
||||||
switch (type.type) {
|
switch (type.type) {
|
||||||
case 'String': return 'text';
|
case 'String': return 'text';
|
||||||
@@ -563,17 +570,17 @@ const buildWhereClause = ({ schema, query, index }) => {
|
|||||||
return { pattern: patterns.join(' AND '), values, sorts };
|
return { pattern: patterns.join(' AND '), values, sorts };
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PostgresStorageAdapter {
|
export class PostgresStorageAdapter implements StorageAdapter {
|
||||||
// Private
|
// Private
|
||||||
_collectionPrefix: string;
|
_collectionPrefix: string;
|
||||||
_client;
|
_client: any;
|
||||||
_pgp;
|
_pgp: any;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
uri,
|
uri,
|
||||||
collectionPrefix = '',
|
collectionPrefix = '',
|
||||||
databaseOptions
|
databaseOptions
|
||||||
}) {
|
}: any) {
|
||||||
this._collectionPrefix = collectionPrefix;
|
this._collectionPrefix = collectionPrefix;
|
||||||
const { client, pgp } = createClient(uri, databaseOptions);
|
const { client, pgp } = createClient(uri, databaseOptions);
|
||||||
this._client = client;
|
this._client = client;
|
||||||
@@ -587,7 +594,7 @@ export class PostgresStorageAdapter {
|
|||||||
this._client.$pool.end();
|
this._client.$pool.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
_ensureSchemaCollectionExists(conn) {
|
_ensureSchemaCollectionExists(conn: any) {
|
||||||
conn = conn || this._client;
|
conn = conn || this._client;
|
||||||
return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
|
return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
|
||||||
.catch(error => {
|
.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);
|
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;
|
const self = this;
|
||||||
return this._client.task('set-class-level-permissions', function * (t) {
|
return this._client.task('set-class-level-permissions', function * (t) {
|
||||||
yield self._ensureSchemaCollectionExists(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;
|
conn = conn || this._client;
|
||||||
const self = this;
|
const self = this;
|
||||||
if (submittedIndexes === undefined) {
|
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;
|
conn = conn || this._client;
|
||||||
return conn.tx('create-class', t => {
|
return conn.tx('create-class', t => {
|
||||||
const q1 = this.createTable(className, schema, t);
|
const q1 = this.createTable(className, schema, t);
|
||||||
@@ -684,7 +691,7 @@ export class PostgresStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Just create a table, do not insert in schema
|
// Just create a table, do not insert in schema
|
||||||
createTable(className, schema, conn) {
|
createTable(className: string, schema: SchemaType, conn: any) {
|
||||||
conn = conn || this._client;
|
conn = conn || this._client;
|
||||||
const self = this;
|
const self = this;
|
||||||
debug('createTable', className, schema);
|
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...
|
// TODO: Must be revised for invalid logic...
|
||||||
debug('addFieldIfNotExists', {className, fieldName, type});
|
debug('addFieldIfNotExists', {className, fieldName, type});
|
||||||
const self = this;
|
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.)
|
// 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.
|
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
|
||||||
deleteClass(className) {
|
deleteClass(className: string) {
|
||||||
const operations = [
|
const operations = [
|
||||||
{query: `DROP TABLE IF EXISTS $1:name`, values: [className]},
|
{query: `DROP TABLE IF EXISTS $1:name`, values: [className]},
|
||||||
{query: `DELETE FROM "_SCHEMA" WHERE "className" = $1`, values: [className]}
|
{query: `DELETE FROM "_SCHEMA" WHERE "className" = $1`, values: [className]}
|
||||||
@@ -829,7 +836,7 @@ export class PostgresStorageAdapter {
|
|||||||
// may do so.
|
// may do so.
|
||||||
|
|
||||||
// Returns a Promise.
|
// Returns a Promise.
|
||||||
deleteFields(className, schema, fieldNames) {
|
deleteFields(className: string, schema: SchemaType, fieldNames: string[]): Promise<void> {
|
||||||
debug('deleteFields', className, fieldNames);
|
debug('deleteFields', className, fieldNames);
|
||||||
fieldNames = fieldNames.reduce((list, fieldName) => {
|
fieldNames = fieldNames.reduce((list, fieldName) => {
|
||||||
const field = schema.fields[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
|
// 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
|
// this adapter doesn't know about the schema, return a promise that rejects with
|
||||||
// undefined as the reason.
|
// undefined as the reason.
|
||||||
getClass(className) {
|
getClass(className: string) {
|
||||||
debug('getClass', className);
|
debug('getClass', className);
|
||||||
return this._client.any('SELECT * FROM "_SCHEMA" WHERE "className"=$<className>', { className })
|
return this._client.any('SELECT * FROM "_SCHEMA" WHERE "className"=$<className>', { className })
|
||||||
.then(result => {
|
.then(result => {
|
||||||
@@ -880,7 +887,7 @@ export class PostgresStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: remove the mongo format dependency in the return value
|
// 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);
|
debug('createObject', className, object);
|
||||||
let columnsArray = [];
|
let columnsArray = [];
|
||||||
const valuesArray = [];
|
const valuesArray = [];
|
||||||
@@ -1021,7 +1028,7 @@ export class PostgresStorageAdapter {
|
|||||||
// Remove all objects that match the given Parse Query.
|
// 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 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.
|
// 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);
|
debug('deleteObjectsByQuery', className, query);
|
||||||
const values = [className];
|
const values = [className];
|
||||||
const index = 2;
|
const index = 2;
|
||||||
@@ -1048,13 +1055,13 @@ export class PostgresStorageAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Return value not currently well specified.
|
// 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);
|
debug('findOneAndUpdate', className, query, update);
|
||||||
return this.updateObjectsByQuery(className, schema, query, update).then((val) => val[0]);
|
return this.updateObjectsByQuery(className, schema, query, update).then((val) => val[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the update to all objects that match the given Parse Query.
|
// 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);
|
debug('updateObjectsByQuery', className, query, update);
|
||||||
const updatePatterns = [];
|
const updatePatterns = [];
|
||||||
const values = [className]
|
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.
|
// 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});
|
debug('upsertOneObject', {className, query, update});
|
||||||
const createValue = Object.assign({}, query, update);
|
const createValue = Object.assign({}, query, update);
|
||||||
return this.createObject(className, schema, createValue).catch((err) => {
|
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 });
|
debug('find', className, query, {skip, limit, sort, keys });
|
||||||
const hasLimit = limit !== undefined;
|
const hasLimit = limit !== undefined;
|
||||||
const hasSkip = skip !== undefined;
|
const hasSkip = skip !== undefined;
|
||||||
@@ -1270,16 +1277,17 @@ export class PostgresStorageAdapter {
|
|||||||
|
|
||||||
let sortPattern = '';
|
let sortPattern = '';
|
||||||
if (sort) {
|
if (sort) {
|
||||||
|
const sortCopy: any = sort;
|
||||||
const sorting = Object.keys(sort).map((key) => {
|
const sorting = Object.keys(sort).map((key) => {
|
||||||
// Using $idx pattern gives: non-integer constant in ORDER BY
|
// Using $idx pattern gives: non-integer constant in ORDER BY
|
||||||
if (sort[key] === 1) {
|
if (sortCopy[key] === 1) {
|
||||||
return `"${key}" ASC`;
|
return `"${key}" ASC`;
|
||||||
}
|
}
|
||||||
return `"${key}" DESC`;
|
return `"${key}" DESC`;
|
||||||
}).join();
|
}).join();
|
||||||
sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : '';
|
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()}`;
|
sortPattern = `ORDER BY ${where.sorts.join()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1313,7 +1321,7 @@ export class PostgresStorageAdapter {
|
|||||||
|
|
||||||
// Converts from a postgres-format object to a REST-format object.
|
// Converts from a postgres-format object to a REST-format object.
|
||||||
// Does not strip out anything based on a lack of authentication.
|
// 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 => {
|
Object.keys(schema.fields).forEach(fieldName => {
|
||||||
if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) {
|
if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) {
|
||||||
object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass };
|
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
|
// 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,
|
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
|
||||||
// which is why we use sparse indexes.
|
// 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
|
// Use the same name for every ensureUniqueness attempt, because postgres
|
||||||
// Will happily create the same index with multiple names.
|
// Will happily create the same index with multiple names.
|
||||||
const constraintName = `unique_${fieldNames.sort().join('_')}`;
|
const constraintName = `unique_${fieldNames.sort().join('_')}`;
|
||||||
@@ -1412,7 +1420,7 @@ export class PostgresStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Executes a count.
|
// Executes a count.
|
||||||
count(className, schema, query) {
|
count(className: string, schema: SchemaType, query: QueryType) {
|
||||||
debug('count', className, query);
|
debug('count', className, query);
|
||||||
const values = [className];
|
const values = [className];
|
||||||
const where = buildWhereClause({ schema, query, index: 2 });
|
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);
|
debug('distinct', className, query);
|
||||||
let field = fieldName;
|
let field = fieldName;
|
||||||
let column = fieldName;
|
let column = fieldName;
|
||||||
@@ -1476,10 +1484,10 @@ export class PostgresStorageAdapter {
|
|||||||
}).then(results => results.map(object => this.postgresObjectToParseObject(className, object, schema)));
|
}).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);
|
debug('aggregate', className, pipeline);
|
||||||
const values = [className];
|
const values = [className];
|
||||||
let columns = [];
|
let columns: string[] = [];
|
||||||
let countField = null;
|
let countField = null;
|
||||||
let wherePattern = '';
|
let wherePattern = '';
|
||||||
let limitPattern = '';
|
let limitPattern = '';
|
||||||
@@ -1578,7 +1586,7 @@ export class PostgresStorageAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
performInitialization({ VolatileClassesSchemas }) {
|
performInitialization({ VolatileClassesSchemas }: any) {
|
||||||
debug('performInitialization');
|
debug('performInitialization');
|
||||||
const promises = VolatileClassesSchemas.map((schema) => {
|
const promises = VolatileClassesSchemas.map((schema) => {
|
||||||
return this.createTable(schema.className, schema).catch((err) => {
|
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 (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]);
|
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}));
|
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)));
|
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}';
|
const qs = 'SELECT * FROM pg_indexes WHERE tablename = ${className}';
|
||||||
return this._client.any(qs, {className});
|
return this._client.any(qs, {className});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSchemaWithIndexes() {
|
updateSchemaWithIndexes(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1708,9 +1720,9 @@ function createLiteralRegex(remaining) {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function literalizeRegexPart(s) {
|
function literalizeRegexPart(s: string) {
|
||||||
const matcher1 = /\\Q((?!\\E).*)\\E$/
|
const matcher1 = /\\Q((?!\\E).*)\\E$/
|
||||||
const result1 = s.match(matcher1);
|
const result1: any = s.match(matcher1);
|
||||||
if(result1 && result1.length > 1 && result1.index > -1) {
|
if(result1 && result1.length > 1 && result1.index > -1) {
|
||||||
// process regex that has a beginning and an end specified for the literal text
|
// process regex that has a beginning and an end specified for the literal text
|
||||||
const prefix = s.substr(0, result1.index);
|
const prefix = s.substr(0, result1.index);
|
||||||
@@ -1721,7 +1733,7 @@ function literalizeRegexPart(s) {
|
|||||||
|
|
||||||
// process regex that has a beginning specified for the literal text
|
// process regex that has a beginning specified for the literal text
|
||||||
const matcher2 = /\\Q((?!\\E).*)$/
|
const matcher2 = /\\Q((?!\\E).*)$/
|
||||||
const result2 = s.match(matcher2);
|
const result2: any = s.match(matcher2);
|
||||||
if(result2 && result2.length > 1 && result2.index > -1){
|
if(result2 && result2.length > 1 && result2.index > -1){
|
||||||
const prefix = s.substr(0, result2.index);
|
const prefix = s.substr(0, result2.index);
|
||||||
const remaining = result2[1];
|
const remaining = result2[1];
|
||||||
@@ -1741,4 +1753,3 @@ function literalizeRegexPart(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default PostgresStorageAdapter;
|
export default PostgresStorageAdapter;
|
||||||
module.exports = PostgresStorageAdapter; // Required for tests
|
|
||||||
|
|||||||
53
src/Adapters/Storage/StorageAdapter.js
Normal file
53
src/Adapters/Storage/StorageAdapter.js
Normal 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>;
|
||||||
|
}
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
// A database adapter that works with data exported from the hosted
|
// @flow
|
||||||
|
// A database adapter that works with data exported from the hosted
|
||||||
// Parse database.
|
// Parse database.
|
||||||
|
|
||||||
|
// @flow-disable-next
|
||||||
import { Parse } from 'parse/node';
|
import { Parse } from 'parse/node';
|
||||||
|
// @flow-disable-next
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
// @flow-disable-next
|
||||||
import intersect from 'intersect';
|
import intersect from 'intersect';
|
||||||
|
// @flow-disable-next
|
||||||
import deepcopy from 'deepcopy';
|
import deepcopy from 'deepcopy';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import * as SchemaController from './SchemaController';
|
import * as SchemaController from './SchemaController';
|
||||||
|
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
||||||
|
import type { QueryOptions,
|
||||||
|
FullQueryOptions } from '../Adapters/Storage/StorageAdapter';
|
||||||
|
|
||||||
function addWriteACL(query, acl) {
|
function addWriteACL(query, acl) {
|
||||||
const newQuery = _.cloneDeep(query);
|
const newQuery = _.cloneDeep(query);
|
||||||
@@ -48,7 +56,7 @@ const isSpecialQueryKey = key => {
|
|||||||
return specialQuerykeys.indexOf(key) >= 0;
|
return specialQuerykeys.indexOf(key) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateQuery = query => {
|
const validateQuery = (query: any): void => {
|
||||||
if (query.ACL) {
|
if (query.ACL) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
||||||
}
|
}
|
||||||
@@ -117,75 +125,6 @@ const validateQuery = query => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function DatabaseController(adapter, schemaCache) {
|
|
||||||
this.adapter = adapter;
|
|
||||||
this.schemaCache = schemaCache;
|
|
||||||
// We don't want a mutable this.schema, because then you could have
|
|
||||||
// one request that uses different schemas for different parts of
|
|
||||||
// it. Instead, use loadSchema to get a schema.
|
|
||||||
this.schemaPromise = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
DatabaseController.prototype.collectionExists = function(className) {
|
|
||||||
return this.adapter.classExists(className);
|
|
||||||
};
|
|
||||||
|
|
||||||
DatabaseController.prototype.purgeCollection = function(className) {
|
|
||||||
return this.loadSchema()
|
|
||||||
.then(schemaController => schemaController.getOneSchema(className))
|
|
||||||
.then(schema => this.adapter.deleteObjectsByQuery(className, schema, {}));
|
|
||||||
};
|
|
||||||
|
|
||||||
DatabaseController.prototype.validateClassName = function(className) {
|
|
||||||
if (!SchemaController.classNameIsValid(className)) {
|
|
||||||
return Promise.reject(new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className));
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns a promise for a schemaController.
|
|
||||||
DatabaseController.prototype.loadSchema = function(options = {clearCache: false}) {
|
|
||||||
if (!this.schemaPromise) {
|
|
||||||
this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, options);
|
|
||||||
this.schemaPromise.then(() => delete this.schemaPromise,
|
|
||||||
() => delete this.schemaPromise);
|
|
||||||
}
|
|
||||||
return this.schemaPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Returns a promise for the classname that is related to the given
|
|
||||||
// classname through the key.
|
|
||||||
// TODO: make this not in the DatabaseController interface
|
|
||||||
DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
|
|
||||||
return this.loadSchema().then((schema) => {
|
|
||||||
var t = schema.getExpectedType(className, key);
|
|
||||||
if (t && t.type == 'Relation') {
|
|
||||||
return t.targetClass;
|
|
||||||
} else {
|
|
||||||
return className;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Uses the schema to validate the object (REST API format).
|
|
||||||
// Returns a promise that resolves to the new schema.
|
|
||||||
// This does not update this.schema, because in a situation like a
|
|
||||||
// batch request, that could confuse other users of the schema.
|
|
||||||
DatabaseController.prototype.validateObject = function(className, object, query, { acl }) {
|
|
||||||
let schema;
|
|
||||||
const isMaster = acl === undefined;
|
|
||||||
var aclGroup = acl || [];
|
|
||||||
return this.loadSchema().then(s => {
|
|
||||||
schema = s;
|
|
||||||
if (isMaster) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return this.canAddField(schema, className, object, aclGroup);
|
|
||||||
}).then(() => {
|
|
||||||
return schema.validateObject(className, object, query);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filters out any data that shouldn't be on this REST-formatted object.
|
// Filters out any data that shouldn't be on this REST-formatted object.
|
||||||
const filterSensitiveData = (isMaster, aclGroup, className, object) => {
|
const filterSensitiveData = (isMaster, aclGroup, className, object) => {
|
||||||
if (className !== '_User') {
|
if (className !== '_User') {
|
||||||
@@ -216,6 +155,8 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => {
|
|||||||
return object;
|
return object;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import type { LoadSchemaOptions } from './types';
|
||||||
|
|
||||||
// Runs an update on the database.
|
// Runs an update on the database.
|
||||||
// Returns a promise for an object with the new values for field
|
// Returns a promise for an object with the new values for field
|
||||||
// modifications that don't know their results ahead of time, like
|
// modifications that don't know their results ahead of time, like
|
||||||
@@ -230,84 +171,6 @@ const isSpecialUpdateKey = key => {
|
|||||||
return specialKeysForUpdate.indexOf(key) >= 0;
|
return specialKeysForUpdate.indexOf(key) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseController.prototype.update = function(className, query, update, {
|
|
||||||
acl,
|
|
||||||
many,
|
|
||||||
upsert,
|
|
||||||
} = {}, skipSanitization = false) {
|
|
||||||
const originalQuery = query;
|
|
||||||
const originalUpdate = update;
|
|
||||||
// Make a copy of the object, so we don't mutate the incoming data.
|
|
||||||
update = deepcopy(update);
|
|
||||||
var relationUpdates = [];
|
|
||||||
var isMaster = acl === undefined;
|
|
||||||
var aclGroup = acl || [];
|
|
||||||
return this.loadSchema()
|
|
||||||
.then(schemaController => {
|
|
||||||
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update'))
|
|
||||||
.then(() => {
|
|
||||||
relationUpdates = this.collectRelationUpdates(className, originalQuery.objectId, update);
|
|
||||||
if (!isMaster) {
|
|
||||||
query = this.addPointerPermissions(schemaController, className, 'update', query, aclGroup);
|
|
||||||
}
|
|
||||||
if (!query) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
if (acl) {
|
|
||||||
query = addWriteACL(query, acl);
|
|
||||||
}
|
|
||||||
validateQuery(query);
|
|
||||||
return schemaController.getOneSchema(className, true)
|
|
||||||
.catch(error => {
|
|
||||||
// If the schema doesn't exist, pretend it exists with no fields. This behavior
|
|
||||||
// will likely need revisiting.
|
|
||||||
if (error === undefined) {
|
|
||||||
return { fields: {} };
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
})
|
|
||||||
.then(schema => {
|
|
||||||
Object.keys(update).forEach(fieldName => {
|
|
||||||
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
|
|
||||||
}
|
|
||||||
fieldName = fieldName.split('.')[0];
|
|
||||||
if (!SchemaController.fieldNameIsValid(fieldName) && !isSpecialUpdateKey(fieldName)) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
for (const updateOperation in update) {
|
|
||||||
if (Object.keys(updateOperation).some(innerKey => innerKey.includes('$') || innerKey.includes('.'))) {
|
|
||||||
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
update = transformObjectACL(update);
|
|
||||||
transformAuthData(className, update, schema);
|
|
||||||
if (many) {
|
|
||||||
return this.adapter.updateObjectsByQuery(className, schema, query, update);
|
|
||||||
} else if (upsert) {
|
|
||||||
return this.adapter.upsertOneObject(className, schema, query, update);
|
|
||||||
} else {
|
|
||||||
return this.adapter.findOneAndUpdate(className, schema, query, update)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(result => {
|
|
||||||
if (!result) {
|
|
||||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'));
|
|
||||||
}
|
|
||||||
return this.handleRelationUpdates(className, originalQuery.objectId, update, relationUpdates).then(() => {
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}).then((result) => {
|
|
||||||
if (skipSanitization) {
|
|
||||||
return Promise.resolve(result);
|
|
||||||
}
|
|
||||||
return sanitizeDatabaseResult(originalUpdate, result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function expandResultOnKeyPath(object, key, value) {
|
function expandResultOnKeyPath(object, key, value) {
|
||||||
if (key.indexOf('.') < 0) {
|
if (key.indexOf('.') < 0) {
|
||||||
object[key] = value[key];
|
object[key] = value[key];
|
||||||
@@ -321,7 +184,7 @@ function expandResultOnKeyPath(object, key, value) {
|
|||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeDatabaseResult(originalObject, result) {
|
function sanitizeDatabaseResult(originalObject, result): Promise<any> {
|
||||||
const response = {};
|
const response = {};
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
@@ -339,148 +202,9 @@ function sanitizeDatabaseResult(originalObject, result) {
|
|||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all relation-updating operations from a REST-format update.
|
function joinTableName(className, key) {
|
||||||
// Returns a list of all relation updates to perform
|
return `_Join:${key}:${className}`;
|
||||||
// This mutates update.
|
|
||||||
DatabaseController.prototype.collectRelationUpdates = function(className, objectId, update) {
|
|
||||||
var ops = [];
|
|
||||||
var deleteMe = [];
|
|
||||||
objectId = update.objectId || objectId;
|
|
||||||
|
|
||||||
var process = (op, key) => {
|
|
||||||
if (!op) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (op.__op == 'AddRelation') {
|
|
||||||
ops.push({key, op});
|
|
||||||
deleteMe.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op.__op == 'RemoveRelation') {
|
|
||||||
ops.push({key, op});
|
|
||||||
deleteMe.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op.__op == 'Batch') {
|
|
||||||
for (var x of op.ops) {
|
|
||||||
process(x, key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const key in update) {
|
|
||||||
process(update[key], key);
|
|
||||||
}
|
|
||||||
for (const key of deleteMe) {
|
|
||||||
delete update[key];
|
|
||||||
}
|
|
||||||
return ops;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Processes relation-updating operations from a REST-format update.
|
|
||||||
// Returns a promise that resolves when all updates have been performed
|
|
||||||
DatabaseController.prototype.handleRelationUpdates = function(className, objectId, update, ops) {
|
|
||||||
var pending = [];
|
|
||||||
objectId = update.objectId || objectId;
|
|
||||||
ops.forEach(({key, op}) => {
|
|
||||||
if (!op) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (op.__op == 'AddRelation') {
|
|
||||||
for (const object of op.objects) {
|
|
||||||
pending.push(this.addRelation(key, className,
|
|
||||||
objectId,
|
|
||||||
object.objectId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op.__op == 'RemoveRelation') {
|
|
||||||
for (const object of op.objects) {
|
|
||||||
pending.push(this.removeRelation(key, className,
|
|
||||||
objectId,
|
|
||||||
object.objectId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(pending);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Adds a relation.
|
|
||||||
// Returns a promise that resolves successfully iff the add was successful.
|
|
||||||
const relationSchema = { fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } } };
|
|
||||||
DatabaseController.prototype.addRelation = function(key, fromClassName, fromId, toId) {
|
|
||||||
const doc = {
|
|
||||||
relatedId: toId,
|
|
||||||
owningId : fromId
|
|
||||||
};
|
|
||||||
return this.adapter.upsertOneObject(`_Join:${key}:${fromClassName}`, relationSchema, doc, doc);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Removes a relation.
|
|
||||||
// Returns a promise that resolves successfully iff the remove was
|
|
||||||
// successful.
|
|
||||||
DatabaseController.prototype.removeRelation = function(key, fromClassName, fromId, toId) {
|
|
||||||
var doc = {
|
|
||||||
relatedId: toId,
|
|
||||||
owningId: fromId
|
|
||||||
};
|
|
||||||
return this.adapter.deleteObjectsByQuery(`_Join:${key}:${fromClassName}`, relationSchema, doc)
|
|
||||||
.catch(error => {
|
|
||||||
// We don't care if they try to delete a non-existent relation.
|
|
||||||
if (error.code == Parse.Error.OBJECT_NOT_FOUND) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Removes objects matches this query from the database.
|
|
||||||
// Returns a promise that resolves successfully iff the object was
|
|
||||||
// deleted.
|
|
||||||
// Options:
|
|
||||||
// acl: a list of strings. If the object to be updated has an ACL,
|
|
||||||
// one of the provided strings must provide the caller with
|
|
||||||
// write permissions.
|
|
||||||
DatabaseController.prototype.destroy = function(className, query, { acl } = {}) {
|
|
||||||
const isMaster = acl === undefined;
|
|
||||||
const aclGroup = acl || [];
|
|
||||||
|
|
||||||
return this.loadSchema()
|
|
||||||
.then(schemaController => {
|
|
||||||
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'delete'))
|
|
||||||
.then(() => {
|
|
||||||
if (!isMaster) {
|
|
||||||
query = this.addPointerPermissions(schemaController, className, 'delete', query, aclGroup);
|
|
||||||
if (!query) {
|
|
||||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// delete by query
|
|
||||||
if (acl) {
|
|
||||||
query = addWriteACL(query, acl);
|
|
||||||
}
|
|
||||||
validateQuery(query);
|
|
||||||
return schemaController.getOneSchema(className)
|
|
||||||
.catch(error => {
|
|
||||||
// If the schema doesn't exist, pretend it exists with no fields. This behavior
|
|
||||||
// will likely need revisiting.
|
|
||||||
if (error === undefined) {
|
|
||||||
return { fields: {} };
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
})
|
|
||||||
.then(parseFormatSchema => this.adapter.deleteObjectsByQuery(className, parseFormatSchema, query))
|
|
||||||
.catch(error => {
|
|
||||||
// When deleting sessions while changing passwords, don't throw an error if they don't have any sessions.
|
|
||||||
if (className === "_Session" && error.code === Parse.Error.OBJECT_NOT_FOUND) {
|
|
||||||
return Promise.resolve({});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const flattenUpdateOperatorsForCreate = object => {
|
const flattenUpdateOperatorsForCreate = object => {
|
||||||
for (const key in object) {
|
for (const key in object) {
|
||||||
@@ -537,10 +261,329 @@ const transformAuthData = (className, object, schema) => {
|
|||||||
delete object.authData;
|
delete object.authData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Transforms a Database format ACL to a REST API format ACL
|
||||||
|
const untransformObjectACL = ({_rperm, _wperm, ...output}) => {
|
||||||
|
if (_rperm || _wperm) {
|
||||||
|
output.ACL = {};
|
||||||
|
|
||||||
|
(_rperm || []).forEach(entry => {
|
||||||
|
if (!output.ACL[entry]) {
|
||||||
|
output.ACL[entry] = { read: true };
|
||||||
|
} else {
|
||||||
|
output.ACL[entry]['read'] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(_wperm || []).forEach(entry => {
|
||||||
|
if (!output.ACL[entry]) {
|
||||||
|
output.ACL[entry] = { write: true };
|
||||||
|
} else {
|
||||||
|
output.ACL[entry]['write'] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationSchema = { fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } } };
|
||||||
|
|
||||||
|
class DatabaseController {
|
||||||
|
adapter: StorageAdapter;
|
||||||
|
schemaCache: any;
|
||||||
|
schemaPromise: ?Promise<SchemaController.SchemaController>;
|
||||||
|
|
||||||
|
constructor(adapter: StorageAdapter, schemaCache: any) {
|
||||||
|
this.adapter = adapter;
|
||||||
|
this.schemaCache = schemaCache;
|
||||||
|
// We don't want a mutable this.schema, because then you could have
|
||||||
|
// one request that uses different schemas for different parts of
|
||||||
|
// it. Instead, use loadSchema to get a schema.
|
||||||
|
this.schemaPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionExists(className: string): Promise<boolean> {
|
||||||
|
return this.adapter.classExists(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
purgeCollection(className: string): Promise<void> {
|
||||||
|
return this.loadSchema()
|
||||||
|
.then(schemaController => schemaController.getOneSchema(className))
|
||||||
|
.then(schema => this.adapter.deleteObjectsByQuery(className, schema, {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
validateClassName(className: string): Promise<void> {
|
||||||
|
if (!SchemaController.classNameIsValid(className)) {
|
||||||
|
return Promise.reject(new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a schemaController.
|
||||||
|
loadSchema(options: LoadSchemaOptions = {clearCache: false}): Promise<SchemaController.SchemaController> {
|
||||||
|
if (this.schemaPromise != null) {
|
||||||
|
return this.schemaPromise;
|
||||||
|
}
|
||||||
|
this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, options);
|
||||||
|
this.schemaPromise.then(() => delete this.schemaPromise,
|
||||||
|
() => delete this.schemaPromise);
|
||||||
|
return this.loadSchema(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for the classname that is related to the given
|
||||||
|
// classname through the key.
|
||||||
|
// TODO: make this not in the DatabaseController interface
|
||||||
|
redirectClassNameForKey(className: string, key: string): Promise<?string> {
|
||||||
|
return this.loadSchema().then((schema) => {
|
||||||
|
var t = schema.getExpectedType(className, key);
|
||||||
|
if (t != null && typeof t !== 'string' && t.type === 'Relation') {
|
||||||
|
return t.targetClass;
|
||||||
|
}
|
||||||
|
return className;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uses the schema to validate the object (REST API format).
|
||||||
|
// Returns a promise that resolves to the new schema.
|
||||||
|
// This does not update this.schema, because in a situation like a
|
||||||
|
// batch request, that could confuse other users of the schema.
|
||||||
|
validateObject(className: string, object: any, query: any, { acl }: QueryOptions): Promise<boolean> {
|
||||||
|
let schema;
|
||||||
|
const isMaster = acl === undefined;
|
||||||
|
var aclGroup: string[] = acl || [];
|
||||||
|
return this.loadSchema().then(s => {
|
||||||
|
schema = s;
|
||||||
|
if (isMaster) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return this.canAddField(schema, className, object, aclGroup);
|
||||||
|
}).then(() => {
|
||||||
|
return schema.validateObject(className, object, query);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(className: string, query: any, update: any, {
|
||||||
|
acl,
|
||||||
|
many,
|
||||||
|
upsert,
|
||||||
|
}: FullQueryOptions = {}, skipSanitization: boolean = false): Promise<any> {
|
||||||
|
const originalQuery = query;
|
||||||
|
const originalUpdate = update;
|
||||||
|
// Make a copy of the object, so we don't mutate the incoming data.
|
||||||
|
update = deepcopy(update);
|
||||||
|
var relationUpdates = [];
|
||||||
|
var isMaster = acl === undefined;
|
||||||
|
var aclGroup = acl || [];
|
||||||
|
return this.loadSchema()
|
||||||
|
.then(schemaController => {
|
||||||
|
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update'))
|
||||||
|
.then(() => {
|
||||||
|
relationUpdates = this.collectRelationUpdates(className, originalQuery.objectId, update);
|
||||||
|
if (!isMaster) {
|
||||||
|
query = this.addPointerPermissions(schemaController, className, 'update', query, aclGroup);
|
||||||
|
}
|
||||||
|
if (!query) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (acl) {
|
||||||
|
query = addWriteACL(query, acl);
|
||||||
|
}
|
||||||
|
validateQuery(query);
|
||||||
|
return schemaController.getOneSchema(className, true)
|
||||||
|
.catch(error => {
|
||||||
|
// If the schema doesn't exist, pretend it exists with no fields. This behavior
|
||||||
|
// will likely need revisiting.
|
||||||
|
if (error === undefined) {
|
||||||
|
return { fields: {} };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
.then(schema => {
|
||||||
|
Object.keys(update).forEach(fieldName => {
|
||||||
|
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
|
||||||
|
}
|
||||||
|
fieldName = fieldName.split('.')[0];
|
||||||
|
if (!SchemaController.fieldNameIsValid(fieldName) && !isSpecialUpdateKey(fieldName)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const updateOperation: any in update) {
|
||||||
|
if (Object.keys(updateOperation).some(innerKey => innerKey.includes('$') || innerKey.includes('.'))) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update = transformObjectACL(update);
|
||||||
|
transformAuthData(className, update, schema);
|
||||||
|
if (many) {
|
||||||
|
return this.adapter.updateObjectsByQuery(className, schema, query, update);
|
||||||
|
} else if (upsert) {
|
||||||
|
return this.adapter.upsertOneObject(className, schema, query, update);
|
||||||
|
} else {
|
||||||
|
return this.adapter.findOneAndUpdate(className, schema, query, update)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((result: any) => {
|
||||||
|
if (!result) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||||
|
}
|
||||||
|
return this.handleRelationUpdates(className, originalQuery.objectId, update, relationUpdates).then(() => {
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}).then((result) => {
|
||||||
|
if (skipSanitization) {
|
||||||
|
return Promise.resolve(result);
|
||||||
|
}
|
||||||
|
return sanitizeDatabaseResult(originalUpdate, result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all relation-updating operations from a REST-format update.
|
||||||
|
// Returns a list of all relation updates to perform
|
||||||
|
// This mutates update.
|
||||||
|
collectRelationUpdates(className: string, objectId: ?string, update: any) {
|
||||||
|
var ops = [];
|
||||||
|
var deleteMe = [];
|
||||||
|
objectId = update.objectId || objectId;
|
||||||
|
|
||||||
|
var process = (op, key) => {
|
||||||
|
if (!op) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (op.__op == 'AddRelation') {
|
||||||
|
ops.push({key, op});
|
||||||
|
deleteMe.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op.__op == 'RemoveRelation') {
|
||||||
|
ops.push({key, op});
|
||||||
|
deleteMe.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op.__op == 'Batch') {
|
||||||
|
for (var x of op.ops) {
|
||||||
|
process(x, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key in update) {
|
||||||
|
process(update[key], key);
|
||||||
|
}
|
||||||
|
for (const key of deleteMe) {
|
||||||
|
delete update[key];
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processes relation-updating operations from a REST-format update.
|
||||||
|
// Returns a promise that resolves when all updates have been performed
|
||||||
|
handleRelationUpdates(className: string, objectId: string, update: any, ops: any) {
|
||||||
|
var pending = [];
|
||||||
|
objectId = update.objectId || objectId;
|
||||||
|
ops.forEach(({key, op}) => {
|
||||||
|
if (!op) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (op.__op == 'AddRelation') {
|
||||||
|
for (const object of op.objects) {
|
||||||
|
pending.push(this.addRelation(key, className,
|
||||||
|
objectId,
|
||||||
|
object.objectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op.__op == 'RemoveRelation') {
|
||||||
|
for (const object of op.objects) {
|
||||||
|
pending.push(this.removeRelation(key, className,
|
||||||
|
objectId,
|
||||||
|
object.objectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds a relation.
|
||||||
|
// Returns a promise that resolves successfully iff the add was successful.
|
||||||
|
addRelation(key: string, fromClassName: string, fromId: string, toId: string) {
|
||||||
|
const doc = {
|
||||||
|
relatedId: toId,
|
||||||
|
owningId : fromId
|
||||||
|
};
|
||||||
|
return this.adapter.upsertOneObject(`_Join:${key}:${fromClassName}`, relationSchema, doc, doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes a relation.
|
||||||
|
// Returns a promise that resolves successfully iff the remove was
|
||||||
|
// successful.
|
||||||
|
removeRelation(key: string, fromClassName: string, fromId: string, toId: string) {
|
||||||
|
var doc = {
|
||||||
|
relatedId: toId,
|
||||||
|
owningId: fromId
|
||||||
|
};
|
||||||
|
return this.adapter.deleteObjectsByQuery(`_Join:${key}:${fromClassName}`, relationSchema, doc)
|
||||||
|
.catch(error => {
|
||||||
|
// We don't care if they try to delete a non-existent relation.
|
||||||
|
if (error.code == Parse.Error.OBJECT_NOT_FOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes objects matches this query from the database.
|
||||||
|
// Returns a promise that resolves successfully iff the object was
|
||||||
|
// deleted.
|
||||||
|
// Options:
|
||||||
|
// acl: a list of strings. If the object to be updated has an ACL,
|
||||||
|
// one of the provided strings must provide the caller with
|
||||||
|
// write permissions.
|
||||||
|
destroy(className: string, query: any, { acl }: QueryOptions = {}): Promise<any> {
|
||||||
|
const isMaster = acl === undefined;
|
||||||
|
const aclGroup = acl || [];
|
||||||
|
|
||||||
|
return this.loadSchema()
|
||||||
|
.then(schemaController => {
|
||||||
|
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'delete'))
|
||||||
|
.then(() => {
|
||||||
|
if (!isMaster) {
|
||||||
|
query = this.addPointerPermissions(schemaController, className, 'delete', query, aclGroup);
|
||||||
|
if (!query) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// delete by query
|
||||||
|
if (acl) {
|
||||||
|
query = addWriteACL(query, acl);
|
||||||
|
}
|
||||||
|
validateQuery(query);
|
||||||
|
return schemaController.getOneSchema(className)
|
||||||
|
.catch(error => {
|
||||||
|
// If the schema doesn't exist, pretend it exists with no fields. This behavior
|
||||||
|
// will likely need revisiting.
|
||||||
|
if (error === undefined) {
|
||||||
|
return { fields: {} };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
})
|
||||||
|
.then(parseFormatSchema => this.adapter.deleteObjectsByQuery(className, parseFormatSchema, query))
|
||||||
|
.catch(error => {
|
||||||
|
// When deleting sessions while changing passwords, don't throw an error if they don't have any sessions.
|
||||||
|
if (className === "_Session" && error.code === Parse.Error.OBJECT_NOT_FOUND) {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Inserts an object into the database.
|
// Inserts an object into the database.
|
||||||
// Returns a promise that resolves successfully iff the object saved.
|
// Returns a promise that resolves successfully iff the object saved.
|
||||||
DatabaseController.prototype.create = function(className, object, { acl } = {}) {
|
create(className: string, object: any, { acl }: QueryOptions = {}): Promise<any> {
|
||||||
// Make a copy of the object, so we don't mutate the incoming data.
|
// Make a copy of the object, so we don't mutate the incoming data.
|
||||||
const originalObject = object;
|
const originalObject = object;
|
||||||
object = transformObjectACL(object);
|
object = transformObjectACL(object);
|
||||||
@@ -564,14 +607,14 @@ DatabaseController.prototype.create = function(className, object, { acl } = {})
|
|||||||
return this.adapter.createObject(className, SchemaController.convertSchemaToAdapterSchema(schema), object);
|
return this.adapter.createObject(className, SchemaController.convertSchemaToAdapterSchema(schema), object);
|
||||||
})
|
})
|
||||||
.then(result => {
|
.then(result => {
|
||||||
return this.handleRelationUpdates(className, null, object, relationUpdates).then(() => {
|
return this.handleRelationUpdates(className, object.objectId, object, relationUpdates).then(() => {
|
||||||
return sanitizeDatabaseResult(originalObject, result.ops[0])
|
return sanitizeDatabaseResult(originalObject, result.ops[0])
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
DatabaseController.prototype.canAddField = function(schema, className, object, aclGroup) {
|
canAddField(schema: SchemaController.SchemaController, className: string, object: any, aclGroup: string[]): Promise<void> {
|
||||||
const classSchema = schema.data[className];
|
const classSchema = schema.data[className];
|
||||||
if (!classSchema) {
|
if (!classSchema) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -589,17 +632,18 @@ DatabaseController.prototype.canAddField = function(schema, className, object, a
|
|||||||
|
|
||||||
// Won't delete collections in the system namespace
|
// Won't delete collections in the system namespace
|
||||||
// Returns a promise.
|
// Returns a promise.
|
||||||
DatabaseController.prototype.deleteEverything = function() {
|
deleteEverything() {
|
||||||
this.schemaPromise = null;
|
this.schemaPromise = null;
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.adapter.deleteAllClasses(),
|
this.adapter.deleteAllClasses(),
|
||||||
this.schemaCache.clear()
|
this.schemaCache.clear()
|
||||||
]);
|
]);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
// Returns a promise for a list of related ids given an owning id.
|
// Returns a promise for a list of related ids given an owning id.
|
||||||
// className here is the owning className.
|
// className here is the owning className.
|
||||||
DatabaseController.prototype.relatedIds = function(className, key, owningId, queryOptions) {
|
relatedIds(className: string, key: string, owningId: string, queryOptions: QueryOptions): Promise<Array<string>> {
|
||||||
const { skip, limit, sort } = queryOptions;
|
const { skip, limit, sort } = queryOptions;
|
||||||
const findOptions = {};
|
const findOptions = {};
|
||||||
if (sort && sort.createdAt && this.adapter.canSortOnJoinTables) {
|
if (sort && sort.createdAt && this.adapter.canSortOnJoinTables) {
|
||||||
@@ -610,20 +654,19 @@ DatabaseController.prototype.relatedIds = function(className, key, owningId, que
|
|||||||
}
|
}
|
||||||
return this.adapter.find(joinTableName(className, key), relationSchema, { owningId }, findOptions)
|
return this.adapter.find(joinTableName(className, key), relationSchema, { owningId }, findOptions)
|
||||||
.then(results => results.map(result => result.relatedId));
|
.then(results => results.map(result => result.relatedId));
|
||||||
};
|
}
|
||||||
|
|
||||||
// Returns a promise for a list of owning ids given some related ids.
|
// Returns a promise for a list of owning ids given some related ids.
|
||||||
// className here is the owning className.
|
// className here is the owning className.
|
||||||
DatabaseController.prototype.owningIds = function(className, key, relatedIds) {
|
owningIds(className: string, key: string, relatedIds: string): Promise<string[]> {
|
||||||
return this.adapter.find(joinTableName(className, key), relationSchema, { relatedId: { '$in': relatedIds } }, {})
|
return this.adapter.find(joinTableName(className, key), relationSchema, { relatedId: { '$in': relatedIds } }, {})
|
||||||
.then(results => results.map(result => result.owningId));
|
.then(results => results.map(result => result.owningId));
|
||||||
};
|
}
|
||||||
|
|
||||||
// Modifies query so that it no longer has $in on relation fields, or
|
// Modifies query so that it no longer has $in on relation fields, or
|
||||||
// equal-to-pointer constraints on relation fields.
|
// equal-to-pointer constraints on relation fields.
|
||||||
// Returns a promise that resolves when query is mutated
|
// Returns a promise that resolves when query is mutated
|
||||||
DatabaseController.prototype.reduceInRelation = function(className, query, schema) {
|
reduceInRelation(className: string, query: any, schema: any): Promise<any> {
|
||||||
|
|
||||||
// Search for an in-relation or equal-to-relation
|
// Search for an in-relation or equal-to-relation
|
||||||
// Make it sequential for now, not sure of paralleization side effects
|
// Make it sequential for now, not sure of paralleization side effects
|
||||||
if (query['$or']) {
|
if (query['$or']) {
|
||||||
@@ -642,7 +685,7 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem
|
|||||||
if (!t || t.type !== 'Relation') {
|
if (!t || t.type !== 'Relation') {
|
||||||
return Promise.resolve(query);
|
return Promise.resolve(query);
|
||||||
}
|
}
|
||||||
let queries = null;
|
let queries: ?any[] = null;
|
||||||
if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) {
|
if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) {
|
||||||
// Build the list of queries
|
// Build the list of queries
|
||||||
queries = Object.keys(query[key]).map((constraintKey) => {
|
queries = Object.keys(query[key]).map((constraintKey) => {
|
||||||
@@ -697,11 +740,11 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem
|
|||||||
return Promise.all(promises).then(() => {
|
return Promise.all(promises).then(() => {
|
||||||
return Promise.resolve(query);
|
return Promise.resolve(query);
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// Modifies query so that it no longer has $relatedTo
|
// Modifies query so that it no longer has $relatedTo
|
||||||
// Returns a promise that resolves when query is mutated
|
// Returns a promise that resolves when query is mutated
|
||||||
DatabaseController.prototype.reduceRelationKeys = function(className, query, queryOptions) {
|
reduceRelationKeys(className: string, query: any, queryOptions: any): ?Promise<void> {
|
||||||
|
|
||||||
if (query['$or']) {
|
if (query['$or']) {
|
||||||
return Promise.all(query['$or'].map((aQuery) => {
|
return Promise.all(query['$or'].map((aQuery) => {
|
||||||
@@ -720,16 +763,17 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query, que
|
|||||||
delete query['$relatedTo'];
|
delete query['$relatedTo'];
|
||||||
this.addInObjectIdsIds(ids, query);
|
this.addInObjectIdsIds(ids, query);
|
||||||
return this.reduceRelationKeys(className, query, queryOptions);
|
return this.reduceRelationKeys(className, query, queryOptions);
|
||||||
});
|
}).then(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
|
addInObjectIdsIds(ids: ?Array<string> = null, query: any) {
|
||||||
const idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null;
|
const idsFromString: ?Array<string> = typeof query.objectId === 'string' ? [query.objectId] : null;
|
||||||
const idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null;
|
const idsFromEq: ?Array<string> = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null;
|
||||||
const idsFromIn = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null;
|
const idsFromIn: ?Array<string> = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null;
|
||||||
|
|
||||||
const allIds = [idsFromString, idsFromEq, idsFromIn, ids].filter(list => list !== null);
|
// @flow-disable-next
|
||||||
|
const allIds: Array<Array<string>> = [idsFromString, idsFromEq, idsFromIn, ids].filter(list => list !== null);
|
||||||
const totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
|
const totalLength = allIds.reduce((memo, list) => memo + list.length, 0);
|
||||||
|
|
||||||
let idsIntersection = [];
|
let idsIntersection = [];
|
||||||
@@ -741,9 +785,12 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
|
|||||||
|
|
||||||
// Need to make sure we don't clobber existing shorthand $eq constraints on objectId.
|
// Need to make sure we don't clobber existing shorthand $eq constraints on objectId.
|
||||||
if (!('objectId' in query)) {
|
if (!('objectId' in query)) {
|
||||||
query.objectId = {};
|
query.objectId = {
|
||||||
|
$in: undefined,
|
||||||
|
};
|
||||||
} else if (typeof query.objectId === 'string') {
|
} else if (typeof query.objectId === 'string') {
|
||||||
query.objectId = {
|
query.objectId = {
|
||||||
|
$in: undefined,
|
||||||
$eq: query.objectId
|
$eq: query.objectId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -752,7 +799,7 @@ DatabaseController.prototype.addInObjectIdsIds = function(ids = null, query) {
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseController.prototype.addNotInObjectIdsIds = function(ids = [], query) {
|
addNotInObjectIdsIds(ids: string[] = [], query: any) {
|
||||||
const idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : [];
|
const idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : [];
|
||||||
let allIds = [...idsFromNin,...ids].filter(list => list !== null);
|
let allIds = [...idsFromNin,...ids].filter(list => list !== null);
|
||||||
|
|
||||||
@@ -761,9 +808,12 @@ DatabaseController.prototype.addNotInObjectIdsIds = function(ids = [], query) {
|
|||||||
|
|
||||||
// Need to make sure we don't clobber existing shorthand $eq constraints on objectId.
|
// Need to make sure we don't clobber existing shorthand $eq constraints on objectId.
|
||||||
if (!('objectId' in query)) {
|
if (!('objectId' in query)) {
|
||||||
query.objectId = {};
|
query.objectId = {
|
||||||
|
$nin: undefined,
|
||||||
|
};
|
||||||
} else if (typeof query.objectId === 'string') {
|
} else if (typeof query.objectId === 'string') {
|
||||||
query.objectId = {
|
query.objectId = {
|
||||||
|
$nin: undefined,
|
||||||
$eq: query.objectId
|
$eq: query.objectId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -786,7 +836,7 @@ DatabaseController.prototype.addNotInObjectIdsIds = function(ids = [], query) {
|
|||||||
// TODO: make userIds not needed here. The db adapter shouldn't know
|
// TODO: make userIds not needed here. The db adapter shouldn't know
|
||||||
// anything about users, ideally. Then, improve the format of the ACL
|
// anything about users, ideally. Then, improve the format of the ACL
|
||||||
// arg to work like the others.
|
// arg to work like the others.
|
||||||
DatabaseController.prototype.find = function(className, query, {
|
find(className: string, query: any, {
|
||||||
skip,
|
skip,
|
||||||
limit,
|
limit,
|
||||||
acl,
|
acl,
|
||||||
@@ -797,7 +847,7 @@ DatabaseController.prototype.find = function(className, query, {
|
|||||||
distinct,
|
distinct,
|
||||||
pipeline,
|
pipeline,
|
||||||
readPreference
|
readPreference
|
||||||
} = {}) {
|
}: any = {}): Promise<any> {
|
||||||
const isMaster = acl === undefined;
|
const isMaster = acl === undefined;
|
||||||
const aclGroup = acl || [];
|
const aclGroup = acl || [];
|
||||||
op = op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find');
|
op = op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find');
|
||||||
@@ -832,6 +882,7 @@ DatabaseController.prototype.find = function(className, query, {
|
|||||||
sort.updatedAt = sort._updated_at;
|
sort.updatedAt = sort._updated_at;
|
||||||
delete sort._updated_at;
|
delete sort._updated_at;
|
||||||
}
|
}
|
||||||
|
const queryOptions = { skip, limit, sort, keys, readPreference };
|
||||||
Object.keys(sort).forEach(fieldName => {
|
Object.keys(sort).forEach(fieldName => {
|
||||||
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`);
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`);
|
||||||
@@ -840,7 +891,6 @@ DatabaseController.prototype.find = function(className, query, {
|
|||||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const queryOptions = { skip, limit, sort, keys, readPreference };
|
|
||||||
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op))
|
return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op))
|
||||||
.then(() => this.reduceRelationKeys(className, query, queryOptions))
|
.then(() => this.reduceRelationKeys(className, query, queryOptions))
|
||||||
.then(() => this.reduceInRelation(className, query, schemaController))
|
.then(() => this.reduceInRelation(className, query, schemaController))
|
||||||
@@ -877,9 +927,6 @@ DatabaseController.prototype.find = function(className, query, {
|
|||||||
} else {
|
} else {
|
||||||
return this.adapter.aggregate(className, schema, pipeline, readPreference);
|
return this.adapter.aggregate(className, schema, pipeline, readPreference);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (!classExists) {
|
|
||||||
return [];
|
|
||||||
} else {
|
} else {
|
||||||
return this.adapter.find(className, schema, query, queryOptions)
|
return this.adapter.find(className, schema, query, queryOptions)
|
||||||
.then(objects => objects.map(object => {
|
.then(objects => objects.map(object => {
|
||||||
@@ -889,38 +936,13 @@ DatabaseController.prototype.find = function(className, query, {
|
|||||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error);
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
// Transforms a Database format ACL to a REST API format ACL
|
|
||||||
const untransformObjectACL = ({_rperm, _wperm, ...output}) => {
|
|
||||||
if (_rperm || _wperm) {
|
|
||||||
output.ACL = {};
|
|
||||||
|
|
||||||
(_rperm || []).forEach(entry => {
|
|
||||||
if (!output.ACL[entry]) {
|
|
||||||
output.ACL[entry] = { read: true };
|
|
||||||
} else {
|
|
||||||
output.ACL[entry]['read'] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
(_wperm || []).forEach(entry => {
|
|
||||||
if (!output.ACL[entry]) {
|
|
||||||
output.ACL[entry] = { write: true };
|
|
||||||
} else {
|
|
||||||
output.ACL[entry]['write'] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseController.prototype.deleteSchema = function(className) {
|
deleteSchema(className: string): Promise<void> {
|
||||||
return this.loadSchema(true)
|
return this.loadSchema({ clearCache: true })
|
||||||
.then(schemaController => schemaController.getOneSchema(className, true))
|
.then(schemaController => schemaController.getOneSchema(className, true))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error === undefined) {
|
if (error === undefined) {
|
||||||
@@ -929,7 +951,7 @@ DatabaseController.prototype.deleteSchema = function(className) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(schema => {
|
.then((schema: any) => {
|
||||||
return this.collectionExists(className)
|
return this.collectionExists(className)
|
||||||
.then(() => this.adapter.count(className, { fields: {} }))
|
.then(() => this.adapter.count(className, { fields: {} }))
|
||||||
.then(count => {
|
.then(count => {
|
||||||
@@ -941,7 +963,9 @@ DatabaseController.prototype.deleteSchema = function(className) {
|
|||||||
.then(wasParseCollection => {
|
.then(wasParseCollection => {
|
||||||
if (wasParseCollection) {
|
if (wasParseCollection) {
|
||||||
const relationFieldNames = Object.keys(schema.fields).filter(fieldName => schema.fields[fieldName].type === 'Relation');
|
const relationFieldNames = Object.keys(schema.fields).filter(fieldName => schema.fields[fieldName].type === 'Relation');
|
||||||
return Promise.all(relationFieldNames.map(name => this.adapter.deleteClass(joinTableName(className, name))));
|
return Promise.all(relationFieldNames.map(name => this.adapter.deleteClass(joinTableName(className, name)))).then(() => {
|
||||||
|
return;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -949,7 +973,7 @@ DatabaseController.prototype.deleteSchema = function(className) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
DatabaseController.prototype.addPointerPermissions = function(schema, className, operation, query, aclGroup = []) {
|
addPointerPermissions(schema: any, className: string, operation: string, query: any, aclGroup: any[] = []) {
|
||||||
// Check if class has public permission for operation
|
// Check if class has public permission for operation
|
||||||
// If the BaseCLP pass, let go through
|
// If the BaseCLP pass, let go through
|
||||||
if (schema.testBaseCLP(className, aclGroup, operation)) {
|
if (schema.testBaseCLP(className, aclGroup, operation)) {
|
||||||
@@ -999,7 +1023,7 @@ DatabaseController.prototype.addPointerPermissions = function(schema, className,
|
|||||||
|
|
||||||
// TODO: create indexes on first creation of a _User object. Otherwise it's impossible to
|
// 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.
|
// have a Parse app without it having a _User collection.
|
||||||
DatabaseController.prototype.performInitialization = function() {
|
performInitialization() {
|
||||||
const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } };
|
const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } };
|
||||||
const requiredRoleFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._Role } };
|
const requiredRoleFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._Role } };
|
||||||
|
|
||||||
@@ -1036,10 +1060,9 @@ DatabaseController.prototype.performInitialization = function() {
|
|||||||
return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit, indexPromise]);
|
return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit, indexPromise]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinTableName(className, key) {
|
static _validateQuery: ((any) => void)
|
||||||
return `_Join:${key}:${className}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose validateQuery for tests
|
|
||||||
DatabaseController._validateQuery = validateQuery;
|
|
||||||
module.exports = DatabaseController;
|
module.exports = DatabaseController;
|
||||||
|
// Expose validateQuery for tests
|
||||||
|
module.exports._validateQuery = validateQuery;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
/** @flow weak */
|
/** @flow weak */
|
||||||
|
|
||||||
import * as triggers from "../triggers";
|
import * as triggers from "../triggers";
|
||||||
|
// @flow-disable-next
|
||||||
import * as Parse from "parse/node";
|
import * as Parse from "parse/node";
|
||||||
|
// @flow-disable-next
|
||||||
import * as request from "request";
|
import * as request from "request";
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ export class HooksController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFunction(functionName) {
|
getFunction(functionName) {
|
||||||
return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
|
return this._getHooks({ functionName: functionName }).then(results => results[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFunctions() {
|
getFunctions() {
|
||||||
@@ -36,7 +38,7 @@ export class HooksController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTrigger(className, triggerName) {
|
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() {
|
getTriggers() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @flow
|
||||||
// This class handles schema validation, persistence, and modification.
|
// This class handles schema validation, persistence, and modification.
|
||||||
//
|
//
|
||||||
// Each individual Schema object should be immutable. The helpers to
|
// Each individual Schema object should be immutable. The helpers to
|
||||||
@@ -13,9 +14,19 @@
|
|||||||
// DatabaseController. This will let us replace the schema logic for
|
// DatabaseController. This will let us replace the schema logic for
|
||||||
// different databases.
|
// different databases.
|
||||||
// TODO: hide all schema logic inside the database adapter.
|
// TODO: hide all schema logic inside the database adapter.
|
||||||
|
// @flow-disable-next
|
||||||
const Parse = require('parse/node').Parse;
|
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)
|
// Contain the default columns for every parse object type (except _Join collection)
|
||||||
_Default: {
|
_Default: {
|
||||||
"objectId": {type:'String'},
|
"objectId": {type:'String'},
|
||||||
@@ -158,7 +169,7 @@ function verifyPermissionKey(key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CLPValidKeys = Object.freeze(['find', 'count', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']);
|
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) {
|
if (!perms) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -166,9 +177,13 @@ function validateCLP(perms, fields) {
|
|||||||
if (CLPValidKeys.indexOf(operation) == -1) {
|
if (CLPValidKeys.indexOf(operation) == -1) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`);
|
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 (operation === 'readUserFields' || operation === 'writeUserFields') {
|
||||||
if (!Array.isArray(perms[operation])) {
|
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}`);
|
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perms[operation]}' is not a valid value for class level permissions ${operation}`);
|
||||||
} else {
|
} else {
|
||||||
perms[operation].forEach((key) => {
|
perms[operation].forEach((key) => {
|
||||||
@@ -180,10 +195,13 @@ function validateCLP(perms, fields) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @flow-disable-next
|
||||||
Object.keys(perms[operation]).forEach((key) => {
|
Object.keys(perms[operation]).forEach((key) => {
|
||||||
verifyPermissionKey(key);
|
verifyPermissionKey(key);
|
||||||
|
// @flow-disable-next
|
||||||
const perm = perms[operation][key];
|
const perm = perms[operation][key];
|
||||||
if (perm !== true) {
|
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}`);
|
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 joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
||||||
const classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
const classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
||||||
function classNameIsValid(className) {
|
function classNameIsValid(className: string): boolean {
|
||||||
// Valid classes must:
|
// Valid classes must:
|
||||||
return (
|
return (
|
||||||
// Be one of _User, _Installation, _Role, _Session OR
|
// 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
|
// 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);
|
return classAndFieldRegex.test(fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks that it's not trying to clobber one of the default fields of the class.
|
// 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)) {
|
if (!fieldNameIsValid(fieldName)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -222,7 +240,7 @@ function fieldNameIsValidForClass(fieldName, className) {
|
|||||||
return true;
|
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 ';
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertSchemaToAdapterSchema = schema => {
|
const convertSchemaToAdapterSchema = (schema: any) => {
|
||||||
schema = injectDefaultSchema(schema);
|
schema = injectDefaultSchema(schema);
|
||||||
delete schema.fields.ACL;
|
delete schema.fields.ACL;
|
||||||
schema.fields._rperm = { type: 'Array' };
|
schema.fields._rperm = { type: 'Array' };
|
||||||
@@ -294,8 +312,8 @@ const convertAdapterSchemaToParseSchema = ({...schema}) => {
|
|||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
const injectDefaultSchema = ({className, fields, classLevelPermissions, indexes}) => {
|
const injectDefaultSchema = ({className, fields, classLevelPermissions, indexes}: Schema) => {
|
||||||
const defaultSchema = {
|
const defaultSchema: Schema = {
|
||||||
className,
|
className,
|
||||||
fields: {
|
fields: {
|
||||||
...defaultColumns._Default,
|
...defaultColumns._Default,
|
||||||
@@ -329,11 +347,12 @@ const _JobScheduleSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
|
|||||||
}));
|
}));
|
||||||
const _AudienceSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
|
const _AudienceSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
|
||||||
className: "_Audience",
|
className: "_Audience",
|
||||||
fields: defaultColumns._Audience
|
fields: defaultColumns._Audience,
|
||||||
|
classLevelPermissions: {}
|
||||||
}));
|
}));
|
||||||
const VolatileClassesSchemas = [_HooksSchema, _JobStatusSchema, _JobScheduleSchema, _PushStatusSchema, _GlobalConfigSchema, _AudienceSchema];
|
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.type !== objectType.type) return false;
|
||||||
if (dbType.targetClass !== objectType.targetClass) return false;
|
if (dbType.targetClass !== objectType.targetClass) return false;
|
||||||
if (dbType === objectType.type) return true;
|
if (dbType === objectType.type) return true;
|
||||||
@@ -341,22 +360,27 @@ const dbTypeMatchesObjectType = (dbType, objectType) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const typeToString = (type) => {
|
const typeToString = (type: SchemaField | string): string => {
|
||||||
|
if (typeof type === 'string') {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
if (type.targetClass) {
|
if (type.targetClass) {
|
||||||
return `${type.type}<${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
|
// 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.
|
// the mongo format and the Parse format. Soon, this will all be Parse format.
|
||||||
export default class SchemaController {
|
export default class SchemaController {
|
||||||
_dbAdapter;
|
_dbAdapter: StorageAdapter;
|
||||||
data;
|
data: any;
|
||||||
perms;
|
perms: any;
|
||||||
indexes;
|
indexes: any;
|
||||||
|
_cache: any;
|
||||||
|
reloadDataPromise: Promise<any>;
|
||||||
|
|
||||||
constructor(databaseAdapter, schemaCache) {
|
constructor(databaseAdapter: StorageAdapter, schemaCache: any) {
|
||||||
this._dbAdapter = databaseAdapter;
|
this._dbAdapter = databaseAdapter;
|
||||||
this._cache = schemaCache;
|
this._cache = schemaCache;
|
||||||
// this.data[className][fieldName] tells you the type of that field, in mongo format
|
// this.data[className][fieldName] tells you the type of that field, in mongo format
|
||||||
@@ -367,7 +391,7 @@ export default class SchemaController {
|
|||||||
this.indexes = {};
|
this.indexes = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadData(options = {clearCache: false}) {
|
reloadData(options: LoadSchemaOptions = {clearCache: false}): Promise<any> {
|
||||||
let promise = Promise.resolve();
|
let promise = Promise.resolve();
|
||||||
if (options.clearCache) {
|
if (options.clearCache) {
|
||||||
promise = promise.then(() => {
|
promise = promise.then(() => {
|
||||||
@@ -378,9 +402,7 @@ export default class SchemaController {
|
|||||||
return this.reloadDataPromise;
|
return this.reloadDataPromise;
|
||||||
}
|
}
|
||||||
this.reloadDataPromise = promise.then(() => {
|
this.reloadDataPromise = promise.then(() => {
|
||||||
return this.getAllClasses(options);
|
return this.getAllClasses(options).then((allSchemas) => {
|
||||||
})
|
|
||||||
.then(allSchemas => {
|
|
||||||
const data = {};
|
const data = {};
|
||||||
const perms = {};
|
const perms = {};
|
||||||
const indexes = {};
|
const indexes = {};
|
||||||
@@ -392,7 +414,7 @@ export default class SchemaController {
|
|||||||
|
|
||||||
// Inject the in-memory classes
|
// Inject the in-memory classes
|
||||||
volatileClasses.forEach(className => {
|
volatileClasses.forEach(className => {
|
||||||
const schema = injectDefaultSchema({ className });
|
const schema = injectDefaultSchema({ className, fields: {}, classLevelPermissions: {} });
|
||||||
data[className] = schema.fields;
|
data[className] = schema.fields;
|
||||||
perms[className] = schema.classLevelPermissions;
|
perms[className] = schema.classLevelPermissions;
|
||||||
indexes[className] = schema.indexes;
|
indexes[className] = schema.indexes;
|
||||||
@@ -407,11 +429,12 @@ export default class SchemaController {
|
|||||||
this.indexes = {};
|
this.indexes = {};
|
||||||
delete this.reloadDataPromise;
|
delete this.reloadDataPromise;
|
||||||
throw err;
|
throw err;
|
||||||
});
|
})
|
||||||
|
}).then(() => {});
|
||||||
return this.reloadDataPromise;
|
return this.reloadDataPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllClasses(options = {clearCache: false}) {
|
getAllClasses(options: LoadSchemaOptions = {clearCache: false}): Promise<Array<Schema>> {
|
||||||
let promise = Promise.resolve();
|
let promise = Promise.resolve();
|
||||||
if (options.clearCache) {
|
if (options.clearCache) {
|
||||||
promise = this._cache.clear();
|
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();
|
let promise = Promise.resolve();
|
||||||
if (options.clearCache) {
|
if (options.clearCache) {
|
||||||
promise = this._cache.clear();
|
promise = this._cache.clear();
|
||||||
@@ -468,7 +491,7 @@ export default class SchemaController {
|
|||||||
// on success, and rejects with an error on fail. Ensure you
|
// on success, and rejects with an error on fail. Ensure you
|
||||||
// have authorization (master key, or client class creation
|
// have authorization (master key, or client class creation
|
||||||
// enabled) before calling this function.
|
// 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);
|
var validationError = this.validateNewClass(className, fields, classLevelPermissions);
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
return Promise.reject(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)
|
return this.getOneSchema(className)
|
||||||
.then(schema => {
|
.then(schema => {
|
||||||
const existingFields = schema.fields;
|
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.
|
// 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.
|
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||||
const deletedFields = [];
|
const deletedFields: string[] = [];
|
||||||
const insertedFields = [];
|
const insertedFields = [];
|
||||||
Object.keys(submittedFields).forEach(fieldName => {
|
Object.keys(submittedFields).forEach(fieldName => {
|
||||||
if (submittedFields[fieldName].__op === 'Delete') {
|
if (submittedFields[fieldName].__op === 'Delete') {
|
||||||
@@ -542,7 +565,7 @@ export default class SchemaController {
|
|||||||
.then(() => this.reloadData({ clearCache: true }))
|
.then(() => this.reloadData({ clearCache: true }))
|
||||||
//TODO: Move this logic into the database adapter
|
//TODO: Move this logic into the database adapter
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const reloadedSchema = {
|
const reloadedSchema: Schema = {
|
||||||
className: className,
|
className: className,
|
||||||
fields: this.data[className],
|
fields: this.data[className],
|
||||||
classLevelPermissions: this.perms[className],
|
classLevelPermissions: this.perms[className],
|
||||||
@@ -564,7 +587,7 @@ export default class SchemaController {
|
|||||||
|
|
||||||
// Returns a promise that resolves successfully to the new schema
|
// Returns a promise that resolves successfully to the new schema
|
||||||
// object or fails with a reason.
|
// object or fails with a reason.
|
||||||
enforceClassExists(className) {
|
enforceClassExists(className: string): Promise<SchemaController> {
|
||||||
if (this.data[className]) {
|
if (this.data[className]) {
|
||||||
return Promise.resolve(this);
|
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]) {
|
if (this.data[className]) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
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, []);
|
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) {
|
for (const fieldName in fields) {
|
||||||
if (existingFieldNames.indexOf(fieldName) < 0) {
|
if (existingFieldNames.indexOf(fieldName) < 0) {
|
||||||
if (!fieldNameIsValid(fieldName)) {
|
if (!fieldNameIsValid(fieldName)) {
|
||||||
@@ -641,7 +664,7 @@ export default class SchemaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sets the Class-level permissions for a given className, which must exist.
|
// 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') {
|
if (typeof perms === 'undefined') {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -653,7 +676,7 @@ export default class SchemaController {
|
|||||||
// object if the provided className-fieldName-type tuple is valid.
|
// object if the provided className-fieldName-type tuple is valid.
|
||||||
// The className must already be validated.
|
// The className must already be validated.
|
||||||
// If 'freeze' is true, refuse to update the schema for this field.
|
// 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) {
|
if (fieldName.indexOf(".") > 0) {
|
||||||
// subdocument key (x.y) => ok if x is of type 'object'
|
// subdocument key (x.y) => ok if x is of type 'object'
|
||||||
fieldName = fieldName.split(".")[ 0 ];
|
fieldName = fieldName.split(".")[ 0 ];
|
||||||
@@ -698,7 +721,11 @@ export default class SchemaController {
|
|||||||
return this.reloadData({ clearCache: true });
|
return this.reloadData({ clearCache: true });
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Ensure that the schema now validates
|
// 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}`);
|
throw new Parse.Error(Parse.Error.INVALID_JSON, `Could not add field ${fieldName}`);
|
||||||
}
|
}
|
||||||
// Remove the cached schema
|
// Remove the cached schema
|
||||||
@@ -709,7 +736,7 @@ export default class SchemaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// maintain compatibility
|
// maintain compatibility
|
||||||
deleteField(fieldName, className, database) {
|
deleteField(fieldName: string, className: string, database: DatabaseController) {
|
||||||
return this.deleteFields([fieldName], className, database);
|
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
|
// Passing the database and prefix is necessary in order to drop relation collections
|
||||||
// and remove fields from objects. Ideally the database would belong to
|
// 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.
|
// 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)) {
|
if (!classNameIsValid(className)) {
|
||||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(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.
|
// Validates an object provided in REST format.
|
||||||
// Returns a promise that resolves to the new schema if this object is
|
// Returns a promise that resolves to the new schema if this object is
|
||||||
// valid.
|
// valid.
|
||||||
validateObject(className, object, query) {
|
validateObject(className: string, object: any, query: any) {
|
||||||
let geocount = 0;
|
let geocount = 0;
|
||||||
let promise = this.enforceClassExists(className);
|
let promise = this.enforceClassExists(className);
|
||||||
for (const fieldName in object) {
|
for (const fieldName in object) {
|
||||||
@@ -804,7 +831,7 @@ export default class SchemaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validates that all the properties are set for the object
|
// 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];
|
const columns = requiredColumns[className];
|
||||||
if (!columns || columns.length == 0) {
|
if (!columns || columns.length == 0) {
|
||||||
return Promise.resolve(this);
|
return Promise.resolve(this);
|
||||||
@@ -831,7 +858,7 @@ export default class SchemaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validates the base CLP for an operation
|
// 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]) {
|
if (!this.perms[className] || !this.perms[className][operation]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -849,7 +876,7 @@ export default class SchemaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validates an operation passes class-level-permissions set in the schema
|
// 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)) {
|
if (this.testBaseCLP(className, aclGroup, operation)) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -897,7 +924,7 @@ export default class SchemaController {
|
|||||||
|
|
||||||
// Returns the expected type for a className+key combination
|
// Returns the expected type for a className+key combination
|
||||||
// or undefined if the schema is not set
|
// or undefined if the schema is not set
|
||||||
getExpectedType(className, fieldName) {
|
getExpectedType(className: string, fieldName: string): ?(SchemaField | string) {
|
||||||
if (this.data && this.data[className]) {
|
if (this.data && this.data[className]) {
|
||||||
const expectedType = this.data[className][fieldName]
|
const expectedType = this.data[className][fieldName]
|
||||||
return expectedType === 'map' ? 'Object' : expectedType;
|
return expectedType === 'map' ? 'Object' : expectedType;
|
||||||
@@ -906,13 +933,13 @@ export default class SchemaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Checks if a given class is in the schema.
|
// Checks if a given class is in the schema.
|
||||||
hasClass(className) {
|
hasClass(className: string) {
|
||||||
return this.reloadData().then(() => !!(this.data[className]));
|
return this.reloadData().then(() => !!(this.data[className]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise for a new Schema.
|
// 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);
|
const schema = new SchemaController(dbAdapter, schemaCache);
|
||||||
return schema.reloadData(options).then(() => schema);
|
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
|
// does not include the default fields, as it is intended to be passed
|
||||||
// to mongoSchemaFromFieldsAndClassName. No validation is done here, it
|
// to mongoSchemaFromFieldsAndClassName. No validation is done here, it
|
||||||
// is done in mongoSchemaFromFieldsAndClassName.
|
// is done in mongoSchemaFromFieldsAndClassName.
|
||||||
function buildMergedSchemaObject(existingFields, putRequest) {
|
function buildMergedSchemaObject(existingFields: SchemaFields, putRequest: any): SchemaFields {
|
||||||
const newSchema = {};
|
const newSchema = {};
|
||||||
|
// @flow-disable-next
|
||||||
const sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]);
|
const sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]);
|
||||||
for (const oldField in existingFields) {
|
for (const oldField in existingFields) {
|
||||||
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
|
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
|
||||||
@@ -960,7 +988,7 @@ function thenValidateRequiredColumns(schemaPromise, className, object, query) {
|
|||||||
// type system.
|
// type system.
|
||||||
// The output should be a valid schema value.
|
// The output should be a valid schema value.
|
||||||
// TODO: ensure that this is compatible with the format used in Open DB
|
// 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;
|
const type = typeof obj;
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
@@ -986,7 +1014,7 @@ function getType(obj) {
|
|||||||
// This gets the type for non-JSON types like pointers and files, but
|
// This gets the type for non-JSON types like pointers and files, but
|
||||||
// also gets the appropriate type for $ operators.
|
// also gets the appropriate type for $ operators.
|
||||||
// Returns null if the type is unknown.
|
// Returns null if the type is unknown.
|
||||||
function getObjectType(obj) {
|
function getObjectType(obj): ?(SchemaField | string) {
|
||||||
if (obj instanceof Array) {
|
if (obj instanceof Array) {
|
||||||
return 'Array';
|
return 'Array';
|
||||||
}
|
}
|
||||||
@@ -1074,4 +1102,5 @@ export {
|
|||||||
defaultColumns,
|
defaultColumns,
|
||||||
convertSchemaToAdapterSchema,
|
convertSchemaToAdapterSchema,
|
||||||
VolatileClassesSchemas,
|
VolatileClassesSchemas,
|
||||||
|
SchemaController,
|
||||||
};
|
};
|
||||||
|
|||||||
29
src/Controllers/types.js
Normal file
29
src/Controllers/types.js
Normal 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[];
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
|
// @flow-disable-next
|
||||||
import deepcopy from 'deepcopy';
|
import deepcopy from 'deepcopy';
|
||||||
import AdaptableController from '../Controllers/AdaptableController';
|
import AdaptableController from '../Controllers/AdaptableController';
|
||||||
import { master } from '../Auth';
|
import { master } from '../Auth';
|
||||||
|
|||||||
Reference in New Issue
Block a user