Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unique fields only capability to schema #7388

Open
wants to merge 3 commits into
base: alpha
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Jump directly to a version:

| 4.x |
|-------------------|
| [**4.5.0 (latest release)**](#450) |
| [**4.5.1 (latest release)**](#450) |
| [4.5.0](#450) |
| [4.4.0](#440) |
| [4.3.0](#430) |
| [4.2.0](#420) |
Expand Down Expand Up @@ -103,6 +104,7 @@ ___
- Added Deprecation Policy to govern the introduction of braking changes in a phased pattern that is more predictable for developers (Manuel Trezza) [#7199](https://github.com/parse-community/parse-server/pull/7199)
### Other Changes
- Fix error when a not yet inserted job is updated (Antonio Davi Macedo Coelho de Castro) [#7196](https://github.com/parse-community/parse-server/pull/7196)
- Fix error when a not yet inserted job is updated (Antonio Davi Macedo Coelho de Castro) [#7196](https://github.com/parse-community/parse-server/pull/7196)
- request.context for afterFind triggers (dblythy) [#7078](https://github.com/parse-community/parse-server/pull/7078)
- Winston Logger interpolating stdout to console (dplewis) [#7114](https://github.com/parse-community/parse-server/pull/7114)
- Move graphql-tag from devDependencies to dependencies (Antonio Davi Macedo Coelho de Castro) [#7183](https://github.com/parse-community/parse-server/pull/7183)
Expand Down Expand Up @@ -137,6 +139,7 @@ ___
### Breaking Changes
- FIX: Consistent casing for afterLiveQueryEvent. The afterLiveQueryEvent was introduced in 4.4.0 with inconsistent casing for the event names, which was fixed in 4.5.0. [#7023](https://github.com/parse-community/parse-server/pull/7023). Thanks to [dblythy](https://github.com/dblythy).
### Other Changes
- IMPROVE: Create schema request serialisation. [#6059](https://github.com/parse-community/parse-server/issues/6059).
- FIX: Properly handle serverURL and publicServerUrl in Batch requests. [#7049](https://github.com/parse-community/parse-server/pull/7049). Thanks to [Zach Goldberg](https://github.com/ZachGoldberg).
- IMPROVE: Prevent invalid column names (className and length). [#7053](https://github.com/parse-community/parse-server/pull/7053). Thanks to [Diamond Lewis](https://github.com/dplewis).
- IMPROVE: GraphQL: Remove viewer from logout mutation. [#7029](https://github.com/parse-community/parse-server/pull/7029). Thanks to [Antoine Cormouls](https://github.com/Moumouls).
Expand Down
103 changes: 103 additions & 0 deletions spec/ParseAPI.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1710,6 +1710,109 @@ describe('miscellaneous', function () {
});
});

describe('FilterOptions', () => {
it('Should reject default fields without filter options', done => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
'Content-Type': 'application/json',
};
request({
method: 'POST',
headers,
url: 'http://localhost:8378/1/schemas/MyObject',
json: true,
body: {
className: 'MyObject',
fields: {
objectId: {
type: 'String',
},
name: {
type: 'String',
},
},
},
})
.catch(e => {
expect(e.status).toEqual(400);
})
.then(() => done());
});

it('Should ignore default fields with body filter options', done => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
'Content-Type': 'application/json',
};
request({
method: 'POST',
headers,
url: 'http://localhost:8378/1/schemas/MyObject',
json: true,
body: {
options: {
ignoreDefaultFields: true,
},
className: 'MyObject',
fields: {
objectId: {
type: 'Number',
},
name: {
type: 'String',
},
},
},
})
.then(response => {
expect(response).toBeDefined();
expect(response.data.fields.objectId.type).not.toBe('Number');
expect(response.data.fields.objectId.type).toBe('String');
expect(response.data.fields.name.type).toBe('String');
})
.catch(e => expect(e).not.toBeDefined())
.then(() => done());
});

it('Should ignore default fields with query filter options', done => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
'Content-Type': 'application/json',
};
request({
method: 'POST',
headers,
url: 'http://localhost:8378/1/schemas/MyObject',
params: {
ignoreDefaultFields: true,
},
json: true,
body: {
className: 'MyObject',
fields: {
objectId: {
type: 'Number',
},
name: {
type: 'String',
},
},
},
})
.then(response => {
expect(response).toBeDefined();
expect(response.data.fields.objectId.type).not.toBe('Number');
expect(response.data.fields.objectId.type).toBe('String');
expect(response.data.fields.name.type).toBe('String');
})
.catch(e => expect(e).not.toBeDefined())
.then(() => done());
});
});

describe_only_db('mongo')('legacy _acl', () => {
it('should have _acl when locking down (regression for #2465)', done => {
const headers = {
Expand Down
17 changes: 17 additions & 0 deletions src/Controllers/SchemaController.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ import type {
LoadSchemaOptions,
} from './types';

const filterOptions: Array<{ label: string, type: string }> = [
{
label: 'ignoreDefaultFields',
defaultValue: false,
do: fields => {
const newFields = {};
Object.keys(fields)?.map(field => {
if (Object.keys(defaultColumns._Default).includes(field) === false) {
newFields[field] = fields[field];
}
});
return newFields;
},
},
];

const defaultColumns: { [string]: SchemaFields } = Object.freeze({
// Contain the default columns for every parse object type (except _Join collection)
_Default: {
Expand Down Expand Up @@ -1585,4 +1601,5 @@ export {
convertSchemaToAdapterSchema,
VolatileClassesSchemas,
SchemaController,
filterOptions,
};
45 changes: 42 additions & 3 deletions src/Routers/SchemasRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ var Parse = require('parse/node').Parse,

import PromiseRouter from '../PromiseRouter';
import * as middleware from '../middlewares';
import { filterOptions } from '../Controllers/SchemaController';
import Utils from '../Utils';

function classNameMismatchResponse(bodyClass, pathClass) {
throw new Parse.Error(
Expand Down Expand Up @@ -35,7 +37,38 @@ function getOneSchema(req) {
});
}

function handleSchemaOptions(options, requestSchema) {
if (
options.ignoreDefaultFields &&
typeof options.ignoreDefaultFields === 'boolean' &&
options.ignoreDefaultFields === true
) {
const filterOption = filterOptions.find(f => f.label === 'ignoreDefaultFields');
if (!filterOption) {
throw new Parse.Error(`ignoreDefaultFields not registered in filter options list`);
}
requestSchema.fields = filterOption.do(requestSchema.fields);
}
return requestSchema;
}

function getOptionParamsFromRequest(req) {
const options = new URLSearchParams(req.query);
const filtered = {};
for (let i = 0; i < filterOptions.length; i++) {
if (options.has(filterOptions[i].label)) {
filtered[filterOptions[i].label] = new Utils().convertType(
filterOptions[i].defaultValue,
options.get(filterOptions[i].label)
);
}
}
return Object.keys(filtered).length > 0 ? filtered : undefined;
}

function createSchema(req) {
let requestSchema = Object.assign({}, req.body);

if (req.auth.isReadOnly) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
Expand All @@ -53,14 +86,20 @@ function createSchema(req) {
throw new Parse.Error(135, `POST ${req.path} needs a class name.`);
}

// handle options.
const options = getOptionParamsFromRequest(req) || req.body.options;
if (options && typeof options === 'object') {
requestSchema = handleSchemaOptions(options, requestSchema);
}

return req.config.database
.loadSchema({ clearCache: true })
.then(schema =>
schema.addClassIfNotExists(
className,
req.body.fields,
req.body.classLevelPermissions,
req.body.indexes
requestSchema.fields,
requestSchema.classLevelPermissions,
requestSchema.indexes
)
)
.then(schema => ({ response: schema }));
Expand Down
8 changes: 8 additions & 0 deletions src/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ class Utils {
}
}
}

convertType(typedVar, ...args) {
return {
boolean: v => v == 'true',
number: Number,
string: String,
}[typeof typedVar](args);
}
}

module.exports = Utils;