work in progress on custom fields
This commit is contained in:
parent
361af18384
commit
86fce404a9
29 changed files with 1088 additions and 198 deletions
2
app.js
2
app.js
|
@ -52,6 +52,7 @@ const reportsRest = require('./routes/rest/reports');
|
|||
const campaignsRest = require('./routes/rest/campaigns');
|
||||
const listsRest = require('./routes/rest/lists');
|
||||
const formsRest = require('./routes/rest/forms');
|
||||
const fieldsRest = require('./routes/rest/fields');
|
||||
const sharesRest = require('./routes/rest/shares');
|
||||
|
||||
const namespacesLegacyIntegration = require('./routes/namespaces-legacy-integration');
|
||||
|
@ -274,6 +275,7 @@ app.use('/rest', accountRest);
|
|||
app.use('/rest', campaignsRest);
|
||||
app.use('/rest', listsRest);
|
||||
app.use('/rest', formsRest);
|
||||
app.use('/rest', fieldsRest);
|
||||
app.use('/rest', sharesRest);
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
|
|
|
@ -23,16 +23,17 @@ class PageContent extends Component {
|
|||
const structure = children[routeKey];
|
||||
|
||||
let path = urlPrefix + routeKey;
|
||||
let pathWithParams = path;
|
||||
|
||||
if (structure.params) {
|
||||
path = path + '/' + structure.params.join('/');
|
||||
pathWithParams = pathWithParams + '/' + structure.params.join('/');
|
||||
}
|
||||
|
||||
if (structure.component || structure.render) {
|
||||
const route = {
|
||||
component: structure.component,
|
||||
render: structure.render,
|
||||
path: (path === '' ? '/' : path)
|
||||
path: (pathWithParams === '' ? '/' : pathWithParams)
|
||||
};
|
||||
|
||||
routes.push(route);
|
||||
|
|
|
@ -59,6 +59,13 @@ export default class List extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
if (perms.includes('manageFields')) {
|
||||
actions.push({
|
||||
label: <span className="glyphicon glyphicon-th-list" aria-hidden="true" title="Manage Fields"></span>,
|
||||
link: '/lists/fields/' + data[0]
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
|
|
224
client/src/lists/fields/CUD.js
Normal file
224
client/src/lists/fields/CUD.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../../lib/page';
|
||||
import {
|
||||
withForm, Form, FormSendMethod, InputField, TextArea, TableSelect, ButtonRow, Button,
|
||||
Fieldset, Dropdown, AlignedRow, ACEEditor
|
||||
} from '../../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling';
|
||||
import {DeleteModalDialog} from "../../lib/delete";
|
||||
import { getFieldTypes } from './field-types';
|
||||
import axios from '../../lib/axios';
|
||||
import interoperableErrors from '../../../../shared/interoperable-errors';
|
||||
import validators from '../../../../shared/validators';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class CUD extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.state.listId = parseInt(props.match.params.listId);
|
||||
|
||||
if (props.edit) {
|
||||
this.state.entityId = parseInt(props.match.params.fieldId);
|
||||
}
|
||||
|
||||
this.fieldTypes = getFieldTypes(props.t);
|
||||
|
||||
this.initForm({
|
||||
serverValidation: {
|
||||
url: `/rest/fields-validate/${this.state.listId}`,
|
||||
changed: ['key'],
|
||||
extra: ['id']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
edit: PropTypes.bool
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadFormValues() {
|
||||
await this.getFormValuesFromURL(`/rest/fields/${this.state.listId}/${this.state.entityId}`, data => {
|
||||
if (data.default_value === null) {
|
||||
data.default_value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
async loadOrderOptions() {
|
||||
const t = this.props.t;
|
||||
|
||||
const flds = await axios.get(`/rest/fields/${this.state.listId}`);
|
||||
|
||||
const getOrderOptions = fld => {
|
||||
return [
|
||||
{key: 'none', label: t('Not visible')},
|
||||
...flds.data.filter(x => x.id !== this.state.entityId && x[fld] !== null).sort((x, y) => x[fld] - y[fld]).map(x => ({ key: x.id, label: `${x.name} (${this.fieldTypes[x.type].label})`})),
|
||||
{key: 'end', label: t('End of list')}
|
||||
];
|
||||
};
|
||||
|
||||
this.setState({
|
||||
orderListOptions: getOrderOptions('order_list'),
|
||||
orderSubscribeOptions: getOrderOptions('order_subscribe'),
|
||||
orderManageOptions: getOrderOptions('order_manage')
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.edit) {
|
||||
this.loadFormValues();
|
||||
} else {
|
||||
this.populateFormValues({
|
||||
name: '',
|
||||
type: 'text',
|
||||
key: '',
|
||||
default_value: '',
|
||||
group: null,
|
||||
orderListBefore: 'end', // possible values are <numeric id> / 'end' / 'none'
|
||||
orderSubscribeBefore: 'end',
|
||||
orderManageBefore: 'end',
|
||||
orderListOptions: [],
|
||||
orderSubscribeOptions: [],
|
||||
orderManageOptions: []
|
||||
});
|
||||
}
|
||||
|
||||
this.loadOrderOptions();
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
if (!state.getIn(['name', 'value'])) {
|
||||
state.setIn(['name', 'error'], t('Name must not be empty'));
|
||||
} else {
|
||||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
|
||||
const keyServerValidation = state.getIn(['key', 'serverValidation']);
|
||||
if (!validators.mergeTagValid(state.getIn(['key', 'value']))) {
|
||||
state.setIn(['key', 'error'], t('Merge tag is invalid. May must be uppercase and contain only characters A-Z, 0-9, _. It must start with a letter.'));
|
||||
} else if (!keyServerValidation) {
|
||||
state.setIn(['key', 'error'], t('Validation is in progress...'));
|
||||
} else if (keyServerValidation.exists) {
|
||||
state.setIn(['key', 'error'], t('Another field with the same merge tag exists. Please choose another merge tag.'));
|
||||
} else {
|
||||
state.setIn(['key', 'error'], null);
|
||||
}
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
let sendMethod, url;
|
||||
if (edit) {
|
||||
sendMethod = FormSendMethod.PUT;
|
||||
url = `/rest/fields/${this.state.listId}/${this.state.entityId}`
|
||||
} else {
|
||||
sendMethod = FormSendMethod.POST;
|
||||
url = `/rest/fields/${this.state.listId}`
|
||||
}
|
||||
|
||||
try {
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving field ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
if (data.default_value.trim() === '') {
|
||||
data.default_value = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.navigateToWithFlashMessage(`/rest/fields/${this.state.listId}`, 'success', t('Field saved'));
|
||||
} else {
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof interoperableErrors.DependencyNotFoundError) {
|
||||
this.setFormStatusMessage('danger',
|
||||
<span>
|
||||
<strong>{t('Your updates cannot be saved.')}</strong>{' '}
|
||||
{t('It seems that another field upon which sort field order was established has been deleted in the meantime. Refresh your page to start anew. Please note that your changes will be lost.')}
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const edit = this.props.edit;
|
||||
|
||||
/*
|
||||
const orderColumns = [
|
||||
{ data: 1, title: t('Field name'), sortable: false, searchable: false }
|
||||
];
|
||||
*/
|
||||
|
||||
const typeOptions = Object.keys(this.fieldTypes).map(key => ({key, label:this.fieldTypes[key].label}));
|
||||
|
||||
const type = this.getFormValue('type');
|
||||
|
||||
// <ACEEditor id={selectedTemplate} height="500px" mode={this.templateSettings[selectedTemplate].mode}/>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{edit &&
|
||||
<DeleteModalDialog
|
||||
stateOwner={this}
|
||||
visible={this.props.match.params.action === 'delete'}
|
||||
deleteUrl={`/rest/fields/${this.state.listId}/${this.state.entityId}`}
|
||||
cudUrl={`/lists/fields/edit/${this.state.listId}/${this.state.entityId}`}
|
||||
listUrl={`/lists/fields/${this.state.listId}`}
|
||||
deletingMsg={t('Deleting field ...')}
|
||||
deletedMsg={t('Field deleted')}/>
|
||||
}
|
||||
|
||||
<Title>{edit ? t('Edit Field') : t('Create Field')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
|
||||
<Dropdown id="type" label={t('Type')} options={typeOptions}/>
|
||||
|
||||
<InputField id="key" label={t('Merge tag')}/>
|
||||
|
||||
{/* type && this.fieldTypes[type].renderSettings */}
|
||||
<Fieldset label={t('Field settings')}>
|
||||
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
|
||||
</Fieldset>
|
||||
<Fieldset label={t('Field order')}>
|
||||
<Dropdown id="orderListBefore" label={t('Listings (before)')} options={this.state.orderListOptions} help={t('Select the field before which this field should appeara in listings. To exclude the field from listings, select "Not visible".')}/>
|
||||
<Dropdown id="orderSubscribeBefore" label={t('Subscription form (before)')} options={this.state.orderSubscribeOptions} help={t('Select the field before which this field should appear in new subscription form. To exclude the field from the new subscription form, select "Not visible".')}/>
|
||||
<Dropdown id="orderManageBefore" label={t('Management form (before)')} options={this.state.orderManageOptions} help={t('Select the field before which this field should appear in subscription management. To exclude the field from the subscription management form, select "Not visible".')}/>
|
||||
</Fieldset>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
{edit && <NavButton className="btn-danger" icon="remove" label={t('Delete')} linkTo={`/lists/fields/edit/${this.state.listId}/${this.state.entityId}/delete`}/>}
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
54
client/src/lists/fields/List.js
Normal file
54
client/src/lists/fields/List.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { translate } from 'react-i18next';
|
||||
import {requiresAuthenticatedUser, withPageHelpers, Title, Toolbar, NavButton} from '../../lib/page';
|
||||
import { withErrorHandling } from '../../lib/error-handling';
|
||||
import { Table } from '../../lib/table';
|
||||
import { getFieldTypes } from './field-types';
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class List extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.state.listId = parseInt(props.match.params.listId);
|
||||
this.fieldTypes = getFieldTypes(props.t);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actions = data => [{
|
||||
label: <span className="glyphicon glyphicon-edit" aria-hidden="true" title="Edit"></span>,
|
||||
link: `/lists/fields/edit/${this.state.listId}/${data[0]}`
|
||||
}];
|
||||
|
||||
const columns = [
|
||||
{ data: 4, title: "#" },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Type'), render: data => this.fieldTypes[data].label, sortable: false, searchable: false },
|
||||
{ data: 3, title: t('Merge Tag') }
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Toolbar>
|
||||
<NavButton linkTo={`/lists/fields/${this.state.listId}/create`} className="btn-primary" icon="plus" label={t('Create Field')}/>
|
||||
</Toolbar>
|
||||
|
||||
<Title>{t('Fields')}</Title>
|
||||
|
||||
<Table withHeader dataUrl={`/rest/fields-table/${this.state.listId}`} columns={columns} actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
59
client/src/lists/fields/field-types.js
Normal file
59
client/src/lists/fields/field-types.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import {Fieldset, InputField} from "../../lib/form";
|
||||
|
||||
export function getFieldTypes(t) {
|
||||
|
||||
const fieldTypes = {
|
||||
text: {
|
||||
label: t('Text'),
|
||||
renderSettings:
|
||||
<Fieldset label={t('Field settings')}>
|
||||
<InputField id="default_value" label={t('Default value')} help={t('Default value used when the field is empty.')}/>
|
||||
</Fieldset>
|
||||
},
|
||||
website: {
|
||||
label: t('Website')
|
||||
},
|
||||
longtext: {
|
||||
label: t('Multi-line text')
|
||||
},
|
||||
gpg: {
|
||||
label: t('GPG Public Key')
|
||||
},
|
||||
number: {
|
||||
label: t('Number')
|
||||
},
|
||||
checkbox: {
|
||||
label: t('Checkboxes (from option fields)')
|
||||
},
|
||||
'radio-grouped': {
|
||||
label: t('Radio Buttons (from option fields)')
|
||||
},
|
||||
'dropdown-grouped': {
|
||||
label: t('Drop Down (from option fields)')
|
||||
},
|
||||
'radio-enum': {
|
||||
label: t('Radio Buttons (enumerated)')
|
||||
},
|
||||
'dropdown-enum': {
|
||||
label: t('Drop Down (enumerated)')
|
||||
},
|
||||
'date': {
|
||||
label: t('Date')
|
||||
},
|
||||
'birthday': {
|
||||
label: t('Birthday')
|
||||
},
|
||||
json: {
|
||||
label: t('JSON value for custom rendering')
|
||||
},
|
||||
option: {
|
||||
label: t('Option')
|
||||
}
|
||||
};
|
||||
|
||||
return fieldTypes;
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ import ListsList from './List';
|
|||
import ListsCUD from './CUD';
|
||||
import FormsList from './forms/List';
|
||||
import FormsCUD from './forms/CUD';
|
||||
import FieldsList from './fields/List';
|
||||
import FieldsCUD from './fields/CUD';
|
||||
import Share from '../shares/Share';
|
||||
|
||||
|
||||
|
@ -26,6 +28,29 @@ const getStructure = t => {
|
|||
link: '/lists',
|
||||
component: ListsList,
|
||||
children: {
|
||||
/* FIXME
|
||||
':listId': {
|
||||
title: resolved => t('List "{{name}}"', {name: resolved.list.name}),
|
||||
resolve: {
|
||||
list: match => `/rest/lists/${match.params.listId}`
|
||||
},
|
||||
actions: {
|
||||
edit: {
|
||||
title: t('Edit'),
|
||||
params: [':action?'],
|
||||
render: props => (<ListsCUD edit entity={resolved.list} {...props} />)
|
||||
},
|
||||
create: {
|
||||
title: t('Create'),
|
||||
render: props => (<ListsCUD entity={resolved.list} {...props} />)
|
||||
},
|
||||
share: {
|
||||
title: t('Share'),
|
||||
render: props => (<Share title={t('Share')} entity={resolved.list} entityTypeId="list" {...props} />)
|
||||
}
|
||||
}
|
||||
},
|
||||
*/
|
||||
edit: {
|
||||
title: t('Edit List'),
|
||||
params: [':id', ':action?'],
|
||||
|
@ -40,6 +65,24 @@ const getStructure = t => {
|
|||
params: [':id'],
|
||||
render: props => (<Share title={entity => t('Share List "{{name}}"', {name: entity.name})} getUrl={id => `/rest/lists/${id}`} entityTypeId="list" {...props} />)
|
||||
},
|
||||
fields: {
|
||||
title: t('Fields'),
|
||||
params: [':listId'],
|
||||
link: match => `/lists/fields/${match.params.listId}`,
|
||||
component: FieldsList,
|
||||
children: {
|
||||
edit: {
|
||||
title: t('Edit Field'),
|
||||
params: [':listId', ':fieldId', ':action?'],
|
||||
render: props => (<FieldsCUD edit {...props} />)
|
||||
},
|
||||
create: {
|
||||
title: t('Create Field'),
|
||||
params: [':listId'],
|
||||
render: props => (<FieldsCUD {...props} />)
|
||||
},
|
||||
}
|
||||
},
|
||||
forms: {
|
||||
title: t('Custom Forms'),
|
||||
link: '/lists/forms',
|
||||
|
|
|
@ -206,8 +206,8 @@ description="All permissions"
|
|||
permissions=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "manageUsers"]
|
||||
|
||||
[roles.namespace.master.children]
|
||||
list=["view", "edit", "delete"]
|
||||
customForm=["view", "edit", "delete"]
|
||||
list=["view", "edit", "delete", "share", "manageFields"]
|
||||
customForm=["view", "edit", "delete", "share"]
|
||||
report=["view", "edit", "delete", "share", "execute", "viewContent", "viewOutput"]
|
||||
reportTemplate=["view", "edit", "delete", "share", "execute"]
|
||||
namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "createCustomForm", "createReport", "createReportTemplate", "manageUsers"]
|
||||
|
@ -215,12 +215,12 @@ namespace=["view", "edit", "delete", "share", "createNamespace", "createList", "
|
|||
[roles.list.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete"]
|
||||
permissions=["view", "edit", "delete", "share", "manageFields"]
|
||||
|
||||
[roles.customForm.master]
|
||||
name="Master"
|
||||
description="All permissions"
|
||||
permissions=["view", "edit", "delete"]
|
||||
permissions=["view", "edit", "delete", "share"]
|
||||
|
||||
[roles.report.master]
|
||||
name="Master"
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
const knex = require('../lib/knex');
|
||||
const permissions = require('../lib/permissions');
|
||||
|
||||
async function ajaxList(params, queryFun, columns, mapFun) {
|
||||
async function ajaxList(params, queryFun, columns, options) {
|
||||
options = options || {};
|
||||
|
||||
return await knex.transaction(async (tx) => {
|
||||
const columnsNames = [];
|
||||
const columnsSelect = [];
|
||||
|
@ -69,7 +71,11 @@ async function ajaxList(params, queryFun, columns, mapFun) {
|
|||
query.select(columnsSelect);
|
||||
|
||||
for (const order of params.order) {
|
||||
query.orderBy(columnsNames[params.columns[order.column].data], order.dir);
|
||||
if (options.orderByBuilder) {
|
||||
options.orderByBuilder(query, columnsNames[params.columns[order.column].data], order.dir);
|
||||
} else {
|
||||
query.orderBy(columnsNames[params.columns[order.column].data], order.dir);
|
||||
}
|
||||
}
|
||||
|
||||
query.options({rowsAsArray:true});
|
||||
|
@ -78,8 +84,8 @@ async function ajaxList(params, queryFun, columns, mapFun) {
|
|||
const rowsOfArray = rows.map(row => {
|
||||
const arr = Object.keys(row).map(field => row[field]);
|
||||
|
||||
if (mapFun) {
|
||||
const result = mapFun(arr);
|
||||
if (options.mapFun) {
|
||||
const result = options.mapFun(arr);
|
||||
return result || arr;
|
||||
} else {
|
||||
return arr;
|
||||
|
@ -98,7 +104,9 @@ async function ajaxList(params, queryFun, columns, mapFun) {
|
|||
});
|
||||
}
|
||||
|
||||
async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, columns, map) {
|
||||
async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, columns, options) {
|
||||
options = options || {};
|
||||
|
||||
const permCols = [];
|
||||
for (const fetchSpec of fetchSpecs) {
|
||||
const entityType = permissions.getEntityType(fetchSpec.entityTypeId);
|
||||
|
@ -136,10 +144,21 @@ async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, co
|
|||
...columns,
|
||||
...permCols
|
||||
],
|
||||
data => {
|
||||
for (let idx = 0; idx < fetchSpecs.length; idx++) {
|
||||
data[columns.length + idx] = data[columns.length + idx].split(';');
|
||||
}
|
||||
{
|
||||
mapFun: data => {
|
||||
for (let idx = 0; idx < fetchSpecs.length; idx++) {
|
||||
data[columns.length + idx] = data[columns.length + idx].split(';');
|
||||
}
|
||||
|
||||
if (options.mapFun) {
|
||||
const result = options.mapFun(data);
|
||||
return result || data;
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
orderByBuilder: options.orderByBuilder
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
333
models/fields.js
333
models/fields.js
|
@ -1,11 +1,342 @@
|
|||
'use strict';
|
||||
|
||||
const knex = require('../lib/knex');
|
||||
const hasher = require('node-object-hash')();
|
||||
const slugify = require('slugify');
|
||||
const { enforce, filterObject } = require('../lib/helpers');
|
||||
const dtHelpers = require('../lib/dt-helpers');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const shares = require('./shares');
|
||||
const fieldsLegacy = require('../lib/models/fields');
|
||||
const bluebird = require('bluebird');
|
||||
const validators = require('../shared/validators');
|
||||
|
||||
const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
|
||||
const allowedKeysUpdate = new Set(['name', 'key', 'default_value', 'group', 'settings']);
|
||||
const hashKeys = allowedKeysCreate;
|
||||
|
||||
const fieldTypes = {};
|
||||
|
||||
fieldTypes.text = fieldTypes.website = {
|
||||
validate: entity => {},
|
||||
addColumn: (table, name) => table.string(name),
|
||||
indexed: true,
|
||||
grouped: false
|
||||
};
|
||||
|
||||
fieldTypes.longtext = fieldTypes.gpg = {
|
||||
validate: entity => {},
|
||||
addColumn: (table, name) => table.text(name),
|
||||
indexed: false,
|
||||
grouped: false
|
||||
};
|
||||
|
||||
fieldTypes.json = {
|
||||
validate: entity => {},
|
||||
addColumn: (table, name) => table.json(name),
|
||||
indexed: false,
|
||||
grouped: false
|
||||
};
|
||||
|
||||
fieldTypes.number = {
|
||||
validate: entity => {},
|
||||
addColumn: (table, name) => table.integer(name),
|
||||
indexed: true,
|
||||
grouped: false
|
||||
};
|
||||
|
||||
fieldTypes.checkbox = fieldTypes['radio-grouped'] = fieldTypes['dropdown-grouped'] = {
|
||||
validate: entity => {},
|
||||
indexed: true,
|
||||
grouped: true
|
||||
};
|
||||
|
||||
fieldTypes['radio-enum'] = fieldTypes['dropdown-enum'] = {
|
||||
validate: entity => {
|
||||
enforce(entity.settings.options, 'Options missing in settings');
|
||||
enforce(Object.keys(entity.settings.options).includes(entity.default_value), 'Default value not present in options');
|
||||
},
|
||||
addColumn: (table, name) => table.string(name),
|
||||
indexed: true,
|
||||
grouped: false
|
||||
};
|
||||
|
||||
fieldTypes.option = {
|
||||
validate: entity => [],
|
||||
addColumn: (table, name) => table.boolean(name),
|
||||
indexed: true,
|
||||
grouped: false
|
||||
};
|
||||
|
||||
fieldTypes['date'] = fieldTypes['birthday'] = {
|
||||
validate: entity => {
|
||||
enforce(['eur', 'us'].includes(entity.settings.dateFormat), 'Date format incorrect');
|
||||
},
|
||||
addColumn: (table, name) => table.dateTime(name),
|
||||
indexed: true,
|
||||
grouped: false
|
||||
};
|
||||
|
||||
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, hashKeys));
|
||||
}
|
||||
|
||||
async function getById(context, listId, id) {
|
||||
let entity;
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||
|
||||
entity = await tx('custom_fields').where({list: listId, id}).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const orderFields = {
|
||||
order_list: 'orderListBefore',
|
||||
order_subscribe: 'orderSubscribeBefore',
|
||||
order_manage: 'orderManageBefore'
|
||||
};
|
||||
|
||||
for (const key in orderFields) {
|
||||
if (entity[key] !== null) {
|
||||
const orderIdRow = await tx('custom_fields').where('list', listId).where(key, '>', entity[key]).orderBy(key, 'asc').select(['id']).first();
|
||||
if (orderIdRow) {
|
||||
entity[orderFields[key]] = orderIdRow.id;
|
||||
} else {
|
||||
entity[orderFields[key]] = 'end';
|
||||
}
|
||||
} else {
|
||||
entity[orderFields[key]] = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
async function list(context, listId) {
|
||||
let rows;
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||
rows = await tx('custom_fields').where({list: listId}).select(['id', 'name', 'type', 'order_list', 'order_subscribe', 'order_manage']);
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function listDTAjax(context, listId, params) {
|
||||
return await dtHelpers.ajaxListWithPermissions(
|
||||
context,
|
||||
[{ entityTypeId: 'list', requiredOperations: ['manageFields'] }],
|
||||
params,
|
||||
builder => builder
|
||||
.from('custom_fields')
|
||||
.innerJoin('lists', 'custom_fields.list', 'lists.id')
|
||||
.where('custom_fields.list', listId),
|
||||
[ 'custom_fields.id', 'custom_fields.name', 'custom_fields.type', 'custom_fields.key', 'custom_fields.order_list' ],
|
||||
{
|
||||
orderByBuilder: (builder, orderColumn, orderDir) => {
|
||||
if (orderColumn === 'custom_fields.order_list') {
|
||||
builder.orderBy(knex.raw('-custom_fields.order_list'), orderDir === 'asc' ? 'desc' : 'asc'); // This is MySQL speciality. It sorts the rows in ascending order with NULL values coming last
|
||||
} else {
|
||||
builder.orderBy(orderColumn, orderDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function serverValidate(context, listId, data) {
|
||||
const result = {};
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||
|
||||
if (data.key) {
|
||||
const existing = await tx('custom_fields').where({
|
||||
list: listId,
|
||||
key: data.key
|
||||
}).whereNot('id', data.id).first();
|
||||
|
||||
result.key = {
|
||||
exists: !!existing
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function _validateAndPreprocess(tx, listId, entity, isCreate) {
|
||||
enforce(entity.type === 'option' || !entity.group, 'Only option may have a group assigned');
|
||||
enforce(entity.type !== 'option' || entity.group, 'Option must have a group assigned.');
|
||||
enforce(!entity.group || await tx('custom_fields').where({list: listId, id: entity.group}).first(), 'Group field does not exist');
|
||||
enforce(entity.name, 'Name must be present');
|
||||
|
||||
const fieldType = fieldTypes[entity.type];
|
||||
enforce(fieldType, 'Unknown field type');
|
||||
|
||||
const validateErrs = fieldType.validate(entity);
|
||||
enforce(!validateErrs.length, 'Invalid field');
|
||||
|
||||
enforce(validators.mergeTagValid(entity.key), 'Merge tag is not valid.');
|
||||
|
||||
const existingWithKeyQuery = knex('custom_fields').where({
|
||||
list: listId,
|
||||
key: entity.key
|
||||
});
|
||||
if (!isCreate) {
|
||||
existingWithKeyQuery.whereNot('id', data.id);
|
||||
}
|
||||
const existingWithKey = await existingWithKeyQuery.first();
|
||||
if (existingWithKey) {
|
||||
throw new interoperableErrors.DuplicitKeyError();
|
||||
}
|
||||
|
||||
entity.settings = JSON.stringify(entity.settings);
|
||||
}
|
||||
|
||||
async function _sortIn(tx, listId, entityId, orderListBefore, orderSubscribeBefore, orderManageBefore) {
|
||||
const flds = await tx('custom_fields').where('list', listId).whereNot('id', entityId);
|
||||
|
||||
const order = {};
|
||||
for (const row of flds) {
|
||||
order[row.id] = {
|
||||
order_list: null,
|
||||
order_subscribe: null,
|
||||
order_manage: null
|
||||
};
|
||||
}
|
||||
|
||||
order[entityId] = {
|
||||
order_list: null,
|
||||
order_subscribe: null,
|
||||
order_manage: null
|
||||
};
|
||||
|
||||
function computeOrder(fldName, sortInBefore) {
|
||||
flds.sort((x, y) => x[fldName] - y[fldName]);
|
||||
const ids = flds.filter(x => x[fldName] !== null).map(x => x.id);
|
||||
|
||||
let sortedIn = false;
|
||||
let idx = 1;
|
||||
for (const id of ids) {
|
||||
if (sortInBefore === id) {
|
||||
order[entityId][fldName] = idx;
|
||||
sortedIn = true;
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
order[id][fldName] = idx;
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
if (!sortedIn && sortInBefore !== 'none') {
|
||||
order[entityId][fldName] = idx;
|
||||
}
|
||||
}
|
||||
|
||||
computeOrder('order_list', orderListBefore);
|
||||
computeOrder('order_subscribe', orderSubscribeBefore);
|
||||
computeOrder('order_manage', orderManageBefore);
|
||||
|
||||
for (const id in order) {
|
||||
await tx('custom_fields').where({list: listId, id}).update(order[id]);
|
||||
}
|
||||
}
|
||||
|
||||
async function create(context, listId, entity) {
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||
|
||||
await _validateAndPreprocess(tx, listId, entity, true);
|
||||
|
||||
let columnName;
|
||||
if (!fieldType.grouped) {
|
||||
columnName = ('custom_' + slugify(name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
|
||||
}
|
||||
|
||||
const filteredEntity = filterObject(entity, allowedKeysCreate);
|
||||
filteredEntity.list = listId;
|
||||
filteredEntity.column = columnName;
|
||||
|
||||
const ids = await tx('custom_fields').insert(filteredEntity);
|
||||
id = ids[0];
|
||||
|
||||
_sortIn(tx, listId, id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
|
||||
|
||||
if (columnName) {
|
||||
await knex.schema.table('subscription__' + listId, table => {
|
||||
fieldType.addColumn(table, columnName);
|
||||
if (fieldType.indexed) {
|
||||
table.index(columnName);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, listId, entity) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||
|
||||
const existing = await tx('custom_fields').where({list: listId, id: entity.id}).first();
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
enforce(entity.type === existing.type, 'Field type cannot be changed');
|
||||
await _validateAndPreprocess(tx, listId, entity, true);
|
||||
|
||||
await tx('custom_fields').where('id', entity.id).update(filterObject(entity, allowedKeysUpdate));
|
||||
_sortIn(tx, listId, entity.id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
|
||||
});
|
||||
}
|
||||
|
||||
async function remove(context, listId, id) {
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageFields');
|
||||
|
||||
const existing = await tx('custom_fields').where({list: listId, id: id}).first();
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const fieldType = fieldTypes[existing.type];
|
||||
|
||||
await tx('custom_fields').where({list: listId, id}).del();
|
||||
|
||||
if (fieldType.grouped) {
|
||||
await tx('custom_fields').where({list: listId, group: id}).del();
|
||||
|
||||
} else {
|
||||
await knex.schema.table('subscription__' + listId, table => {
|
||||
table.dropColumn(existing.column);
|
||||
});
|
||||
|
||||
await tx('segemnt_rules').where({column: existing.column}).del();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
list: bluebird.promisify(fieldsLegacy.list)
|
||||
hash,
|
||||
getById,
|
||||
list,
|
||||
listDTAjax,
|
||||
create,
|
||||
updateWithConsistencyCheck,
|
||||
remove,
|
||||
serverValidate
|
||||
};
|
|
@ -85,10 +85,9 @@ async function _getById(tx, id) {
|
|||
|
||||
|
||||
async function getById(context, id) {
|
||||
shares.enforceEntityPermission(context, 'customForm', id, 'view');
|
||||
|
||||
let entity;
|
||||
await knex.transaction(async tx => {
|
||||
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'view');
|
||||
entity = await _getById(tx, id);
|
||||
});
|
||||
|
||||
|
@ -114,10 +113,10 @@ async function serverValidate(context, data) {
|
|||
|
||||
|
||||
async function create(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createCustomForm');
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCustomForm');
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
const form = filterObject(entity, allowedFormKeys);
|
||||
|
@ -141,13 +140,13 @@ async function create(context, entity) {
|
|||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'customForm', entity.id, 'edit');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'customForm', entity.id, 'edit');
|
||||
|
||||
const existing = await _getById(tx, entity.id);
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash != entity.originalHash) {
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
@ -173,9 +172,9 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
async function remove(context, id) {
|
||||
shares.enforceEntityPermission(context, 'customForm', id, 'delete');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'delete');
|
||||
|
||||
const entity = await tx('custom_forms').where('id', id).first();
|
||||
|
||||
if (!entity) {
|
||||
|
|
|
@ -42,10 +42,10 @@ async function getById(context, id) {
|
|||
}
|
||||
|
||||
async function create(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createList');
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createList');
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
enforce(entity.unsubscription_mode >= 0 && entity.unsubscription_mode < UnsubscriptionMode.MAX, 'Unknown unsubscription mode');
|
||||
|
||||
|
@ -64,16 +64,16 @@ async function create(context, entity) {
|
|||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'list', entity.id, 'edit');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'edit');
|
||||
|
||||
const existing = await tx('lists').where('id', entity.id).first();
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash != entity.originalHash) {
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
@ -88,9 +88,9 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
async function remove(context, id) {
|
||||
await shares.enforceEntityPermission(context, 'list', id, 'delete');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'delete');
|
||||
|
||||
await tx('lists').where('id', id).del();
|
||||
await knex.schema.dropTableIfExists('subscription__' + id);
|
||||
});
|
||||
|
|
|
@ -118,13 +118,10 @@ async function getById(context, id) {
|
|||
|
||||
async function create(context, entity) {
|
||||
enforce(entity.namespace, 'Parent namespace must be set');
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createNamespace');
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
if (!await tx('namespaces').select(['id']).where('id', entity.namespace).first()) {
|
||||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createNamespace');
|
||||
|
||||
const ids = await tx('namespaces').insert(filterObject(entity, allowedKeys));
|
||||
id = ids[0];
|
||||
|
@ -138,16 +135,17 @@ async function create(context, entity) {
|
|||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
enforce(entity.id !== 1 || entity.namespace === null, 'Cannot assign a parent to the root namespace.');
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.id, 'edit');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.id, 'edit');
|
||||
|
||||
const existing = await tx('namespaces').where('id', entity.id).first();
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash != entity.originalHash) {
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
@ -175,9 +173,10 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
|
||||
async function remove(context, id) {
|
||||
enforce(id !== 1, 'Cannot delete the root namespace.');
|
||||
await shares.enforceEntityPermission(context, 'namespace', id, 'delete');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', id, 'delete');
|
||||
|
||||
const childNs = await tx('namespaces').where('namespace', id).first();
|
||||
if (childNs) {
|
||||
throw new interoperableErrors.ChildDetectedError();
|
||||
|
|
|
@ -36,10 +36,9 @@ async function listDTAjax(context, params) {
|
|||
}
|
||||
|
||||
async function create(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createReportTemplate');
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createReportTemplate');
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
const ids = await tx('report_templates').insert(filterObject(entity, allowedKeys));
|
||||
|
@ -52,16 +51,16 @@ async function create(context, entity) {
|
|||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'reportTemplate', entity.id, 'edit');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'reportTemplate', entity.id, 'edit');
|
||||
|
||||
const existing = await tx('report_templates').where('id', entity.id).first();
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash != entity.originalHash) {
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
|
|
|
@ -56,16 +56,12 @@ async function listDTAjax(context, params) {
|
|||
}
|
||||
|
||||
async function create(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, 'createReport');
|
||||
await shares.enforceEntityPermission(context, 'reportTemplate', entity.report_template, 'execute');
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createReport');
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'reportTemplate', entity.report_template, 'execute');
|
||||
|
||||
if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) {
|
||||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
entity.params = JSON.stringify(entity.params);
|
||||
|
||||
|
@ -81,10 +77,10 @@ async function create(context, entity) {
|
|||
}
|
||||
|
||||
async function updateWithConsistencyCheck(context, entity) {
|
||||
await shares.enforceEntityPermission(context, 'report', entity.id, 'edit');
|
||||
await shares.enforceEntityPermission(context, 'reportTemplate', entity.report_template, 'execute');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'report', entity.id, 'edit');
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'reportTemplate', entity.report_template, 'execute');
|
||||
|
||||
const existing = await tx('reports').where('id', entity.id).first();
|
||||
if (!existing) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
|
@ -93,14 +89,10 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
existing.params = JSON.parse(existing.params);
|
||||
|
||||
const existingHash = hash(existing);
|
||||
if (existingHash != entity.originalHash) {
|
||||
if (texistingHash !== entity.originalHash) {
|
||||
throw new interoperableErrors.ChangedError();
|
||||
}
|
||||
|
||||
if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) {
|
||||
throw new interoperableErrors.DependencyNotFoundError();
|
||||
}
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
await namespaceHelpers.validateMove(context, entity, existing, 'report', 'createReport', 'delete');
|
||||
|
||||
|
|
|
@ -83,9 +83,9 @@ async function listRolesDTAjax(context, entityTypeId, params) {
|
|||
async function assign(context, entityTypeId, entityId, userId, role) {
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
|
||||
await enforceEntityPermission(context, entityTypeId, entityId, 'share');
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
|
||||
|
||||
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');
|
||||
enforce(await tx(entityType.entitiesTable).where('id', entityId).select('id').first(), 'Invalid entity id');
|
||||
|
||||
|
@ -427,28 +427,37 @@ function enforceGlobalPermission(context, requiredOperations) {
|
|||
throwPermissionDenied();
|
||||
}
|
||||
|
||||
async function _checkPermission(context, entityTypeId, entityId, requiredOperations) {
|
||||
if (context.user.admin) { // This handles the getAdminContext() case
|
||||
return true;
|
||||
}
|
||||
|
||||
async function _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
|
||||
const entityType = permissions.getEntityType(entityTypeId);
|
||||
|
||||
if (typeof requiredOperations === 'string') {
|
||||
requiredOperations = [ requiredOperations ];
|
||||
if (context.user.admin) { // This handles the getAdminContext() case. In this case we don't check the permission, but just the existence.
|
||||
const existsQuery = tx(entityType.entitiesTable);
|
||||
|
||||
if (entityId) {
|
||||
existsQuery.where('id', entityId);
|
||||
}
|
||||
|
||||
const exists = await existsQuery.first();
|
||||
|
||||
return !!exists;
|
||||
|
||||
} else {
|
||||
if (typeof requiredOperations === 'string') {
|
||||
requiredOperations = [ requiredOperations ];
|
||||
}
|
||||
|
||||
const permsQuery = tx(entityType.permissionsTable)
|
||||
.where('user', context.user.id)
|
||||
.whereIn('operation', requiredOperations);
|
||||
|
||||
if (entityId) {
|
||||
permsQuery.andWhere('entity', entityId);
|
||||
}
|
||||
|
||||
const perms = await permsQuery.first();
|
||||
|
||||
return !!perms;
|
||||
}
|
||||
|
||||
const permsQuery = knex(entityType.permissionsTable)
|
||||
.where('user', context.user.id)
|
||||
.whereIn('operation', requiredOperations);
|
||||
|
||||
if (entityId) {
|
||||
permsQuery.andWhere('entity', entityId);
|
||||
}
|
||||
|
||||
const perms = await permsQuery.first();
|
||||
|
||||
return !!perms;
|
||||
}
|
||||
|
||||
async function checkEntityPermission(context, entityTypeId, entityId, requiredOperations) {
|
||||
|
@ -456,23 +465,55 @@ async function checkEntityPermission(context, entityTypeId, entityId, requiredOp
|
|||
return false;
|
||||
}
|
||||
|
||||
return await _checkPermission(context, entityTypeId, entityId, requiredOperations);
|
||||
let result;
|
||||
await knex.transaction(async tx => {
|
||||
result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async function checkTypePermission(context, entityTypeId, requiredOperations) {
|
||||
return await _checkPermission(context, entityTypeId, null, requiredOperations);
|
||||
let result;
|
||||
await knex.transaction(async tx => {
|
||||
result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
async function enforceEntityPermission(context, entityTypeId, entityId, requiredOperations) {
|
||||
const perms = await checkEntityPermission(context, entityTypeId, entityId, requiredOperations);
|
||||
if (!perms) {
|
||||
if (!entityId) {
|
||||
throwPermissionDenied();
|
||||
}
|
||||
await knex.transaction(async tx => {
|
||||
const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
|
||||
if (!result) {
|
||||
throwPermissionDenied();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function enforceEntityPermissionTx(tx, context, entityTypeId, entityId, requiredOperations) {
|
||||
if (!entityId) {
|
||||
throwPermissionDenied();
|
||||
}
|
||||
const result = await _checkPermissionTx(tx, context, entityTypeId, entityId, requiredOperations);
|
||||
if (!result) {
|
||||
throwPermissionDenied();
|
||||
}
|
||||
}
|
||||
|
||||
async function enforceTypePermission(context, entityTypeId, requiredOperations) {
|
||||
const perms = await checkTypePermission(context, entityTypeId, requiredOperations);
|
||||
if (!perms) {
|
||||
await knex.transaction(async tx => {
|
||||
const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
|
||||
if (!result) {
|
||||
throwPermissionDenied();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function enforceTypePermissionTx(tx, context, entityTypeId, requiredOperations) {
|
||||
const result = await _checkPermissionTx(tx, context, entityTypeId, null, requiredOperations);
|
||||
if (!result) {
|
||||
throwPermissionDenied();
|
||||
}
|
||||
}
|
||||
|
@ -487,7 +528,9 @@ module.exports = {
|
|||
rebuildPermissions,
|
||||
removeDefaultShares,
|
||||
enforceEntityPermission,
|
||||
enforceEntityPermissionTx,
|
||||
enforceTypePermission,
|
||||
enforceTypePermissionTx,
|
||||
checkEntityPermission,
|
||||
checkTypePermission,
|
||||
enforceGlobalPermission,
|
||||
|
|
|
@ -123,14 +123,14 @@ async function listDTAjax(context, params) {
|
|||
);
|
||||
}
|
||||
|
||||
async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
|
||||
enforce(await tools.validateEmail(user.email) === 0, 'Invalid email');
|
||||
async function _validateAndPreprocess(tx, entity, isCreate, isOwnAccount) {
|
||||
enforce(await tools.validateEmail(entity.email) === 0, 'Invalid email');
|
||||
|
||||
await namespaceHelpers.validateEntity(tx, user);
|
||||
await namespaceHelpers.validateEntity(tx, entity);
|
||||
|
||||
const otherUserWithSameEmailQuery = tx('users').where('email', user.email);
|
||||
if (user.id) {
|
||||
otherUserWithSameEmailQuery.andWhereNot('id', user.id);
|
||||
const otherUserWithSameEmailQuery = tx('users').where('email', entity.email);
|
||||
if (entity.id) {
|
||||
otherUserWithSameEmailQuery.andWhereNot('id', entity.id);
|
||||
}
|
||||
|
||||
if (await otherUserWithSameEmailQuery.first()) {
|
||||
|
@ -139,9 +139,9 @@ async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
|
|||
|
||||
|
||||
if (!isOwnAccount) {
|
||||
const otherUserWithSameUsernameQuery = tx('users').where('username', user.username);
|
||||
if (user.id) {
|
||||
otherUserWithSameUsernameQuery.andWhereNot('id', user.id);
|
||||
const otherUserWithSameUsernameQuery = tx('users').where('username', entity.username);
|
||||
if (entity.id) {
|
||||
otherUserWithSameUsernameQuery.andWhereNot('id', entity.id);
|
||||
}
|
||||
|
||||
if (await otherUserWithSameUsernameQuery.first()) {
|
||||
|
@ -149,30 +149,28 @@ async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) {
|
|||
}
|
||||
}
|
||||
|
||||
enforce(user.role in config.roles.global, 'Unknown role');
|
||||
enforce(entity.role in config.roles.global, 'Unknown role');
|
||||
|
||||
enforce(!isCreate || user.password.length > 0, 'Password not set');
|
||||
enforce(!isCreate || entity.password.length > 0, 'Password not set');
|
||||
|
||||
if (user.password) {
|
||||
const passwordValidatorResults = passwordValidator.test(user.password);
|
||||
if (entity.password) {
|
||||
const passwordValidatorResults = passwordValidator.test(entity.password);
|
||||
if (passwordValidatorResults.errors.length > 0) {
|
||||
// This is not an interoperable error because this is not supposed to happen unless the client is tampered with.
|
||||
throw new Error('Invalid password');
|
||||
}
|
||||
|
||||
user.password = await bcryptHash(user.password, null, null);
|
||||
entity.password = await bcryptHash(entity.password, null, null);
|
||||
} else {
|
||||
delete user.password;
|
||||
delete entity.password;
|
||||
}
|
||||
}
|
||||
|
||||
async function create(context, user) {
|
||||
if (context) { // Is also called internally from ldap handling in passport
|
||||
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
|
||||
}
|
||||
|
||||
let id;
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers');
|
||||
|
||||
if (passport.isAuthMethodLocal) {
|
||||
await _validateAndPreprocess(tx, user, true);
|
||||
|
||||
|
@ -208,8 +206,8 @@ async function updateWithConsistencyCheck(context, user, isOwnAccount) {
|
|||
}
|
||||
|
||||
if (!isOwnAccount) {
|
||||
await shares.enforceEntityPermission(context, 'namespace', user.namespace, 'manageUsers');
|
||||
await shares.enforceEntityPermission(context, 'namespace', existing.namespace, 'manageUsers');
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', user.namespace, 'manageUsers');
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', existing.namespace, 'manageUsers');
|
||||
}
|
||||
|
||||
if (passport.isAuthMethodLocal) {
|
||||
|
@ -251,7 +249,7 @@ async function remove(context, userId) {
|
|||
shares.throwPermissionDenied();
|
||||
}
|
||||
|
||||
await shares.enforceEntityPermission(context, 'namespace', existing.namespace, 'manageUsers');
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'namespace', existing.namespace, 'manageUsers');
|
||||
|
||||
await tx('users').where('id', userId).del();
|
||||
});
|
||||
|
|
47
routes/rest/fields.js
Normal file
47
routes/rest/fields.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../../lib/passport');
|
||||
const fields = require('../../models/fields');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
|
||||
|
||||
router.postAsync('/fields-table/:listId', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await fields.listDTAjax(req.context, req.params.listId, req.body));
|
||||
});
|
||||
|
||||
router.getAsync('/fields/:listId/:fieldId', passport.loggedIn, async (req, res) => {
|
||||
const entity = await fields.getById(req.context, req.params.listId, req.params.fieldId);
|
||||
entity.hash = fields.hash(entity);
|
||||
return res.json(entity);
|
||||
});
|
||||
|
||||
router.getAsync('/fields/:listId', passport.loggedIn, async (req, res) => {
|
||||
const rows = await fields.list(req.context, req.params.listId);
|
||||
return res.json(rows);
|
||||
});
|
||||
|
||||
router.postAsync('/fields/:listId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await fields.create(req.context, req.params.listId, req.body);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.putAsync('/fields/:listId/:fieldId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const entity = req.body;
|
||||
entity.id = parseInt(req.params.fieldId);
|
||||
|
||||
await fields.updateWithConsistencyCheck(req.context, req.params.listId, entity);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.deleteAsync('/fields/:listId/:fieldId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
await fields.remove(req.context, req.params.listId, req.params.fieldId);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
router.postAsync('/fields-validate/:listId', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await fields.serverValidate(req.context, req.params.listId, req.body));
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -1,4 +1,4 @@
|
|||
exports.up = function(knex, Promise) {
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
/* This is shows what it would look like when we specify the "users" table with Knex.
|
||||
In some sense, this is probably the most complicated table we have in Mailtrain.
|
||||
|
||||
|
@ -36,25 +36,22 @@ exports.up = function(knex, Promise) {
|
|||
|
||||
// We should check here if the tables already exist and upgrade them to db_schema_version 28, which is the baseline.
|
||||
// For now, we just check whether our DB is up-to-date based on the existing SQL migration infrastructure in Mailtrain.
|
||||
return knex('settings').where({key: 'db_schema_version'}).first('value')
|
||||
.then(row => {
|
||||
if (!row || Number(row.value) !== 29) {
|
||||
throw new Error('Unsupported DB schema version: ' + row.value);
|
||||
}
|
||||
})
|
||||
const row = await knex('settings').where({key: 'db_schema_version'}).first('value');
|
||||
if (!row || Number(row.value) !== 29) {
|
||||
throw new Error('Unsupported DB schema version: ' + row.value);
|
||||
}
|
||||
|
||||
// We have to update data types of primary keys and related foreign keys. Mailtrain uses unsigned int(11), while
|
||||
// Knex uses unsigned int (which is unsigned int(10) ).
|
||||
.then(() => knex.schema
|
||||
.raw('ALTER TABLE `users` MODIFY `id` int unsigned not null auto_increment')
|
||||
.raw('ALTER TABLE `lists` MODIFY `id` int unsigned not null auto_increment')
|
||||
.raw('ALTER TABLE `confirmations` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `custom_fields` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `importer` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `segments` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `triggers` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `custom_forms` MODIFY `list` int unsigned not null')
|
||||
)
|
||||
await knex.schema
|
||||
.raw('ALTER TABLE `users` MODIFY `id` int unsigned not null auto_increment')
|
||||
.raw('ALTER TABLE `lists` MODIFY `id` int unsigned not null auto_increment')
|
||||
.raw('ALTER TABLE `confirmations` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `custom_fields` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `importer` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `segments` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `triggers` MODIFY `list` int unsigned not null')
|
||||
.raw('ALTER TABLE `custom_forms` MODIFY `list` int unsigned not null');
|
||||
|
||||
/*
|
||||
Remaining foreign keys:
|
||||
|
@ -68,8 +65,8 @@ exports.up = function(knex, Promise) {
|
|||
custom_forms_data form custom_forms id
|
||||
report_template report_template report_templates id
|
||||
*/
|
||||
};
|
||||
})();
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
// return knex.schema.dropTable('users');
|
||||
};
|
||||
})();
|
|
@ -1,33 +1,33 @@
|
|||
exports.up = function(knex, Promise) {
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
const entityTypesAddNamespace = ['list', 'custom_form', 'report', 'report_template', 'user'];
|
||||
let promise = knex.schema.createTable('namespaces', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('name');
|
||||
table.text('description');
|
||||
table.integer('namespace').unsigned().references('namespaces.id').onDelete('CASCADE');
|
||||
})
|
||||
.then(() => knex('namespaces').insert({
|
||||
id: 1, /* Global namespace id */
|
||||
name: 'Root',
|
||||
description: 'Root namespace'
|
||||
}));
|
||||
await knex.schema.createTable('namespaces', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('name');
|
||||
table.text('description');
|
||||
table.integer('namespace').unsigned().references('namespaces.id').onDelete('CASCADE');
|
||||
});
|
||||
|
||||
await knex('namespaces').insert({
|
||||
id: 1, /* Global namespace id */
|
||||
name: 'Root',
|
||||
description: 'Root namespace'
|
||||
});
|
||||
|
||||
for (const entityType of entityTypesAddNamespace) {
|
||||
promise = promise
|
||||
.then(() => knex.schema.table(`${entityType}s`, table => {
|
||||
table.integer('namespace').unsigned().notNullable();
|
||||
}))
|
||||
.then(() => knex(`${entityType}s`).update({
|
||||
namespace: 1 /* Global namespace id */
|
||||
}))
|
||||
.then(() => knex.schema.table(`${entityType}s`, table => {
|
||||
table.foreign('namespace').references('namespaces.id').onDelete('CASCADE');
|
||||
}));
|
||||
await knex.schema.table(`${entityType}s`, table => {
|
||||
table.integer('namespace').unsigned().notNullable();
|
||||
});
|
||||
|
||||
await knex(`${entityType}s`).update({
|
||||
namespace: 1 /* Global namespace id */
|
||||
});
|
||||
|
||||
await knex.schema.table(`${entityType}s`, table => {
|
||||
table.foreign('namespace').references('namespaces.id').onDelete('CASCADE');
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
return knex.schema.dropTable('namespaces');
|
||||
};
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
await knex.schema.dropTable('namespaces');
|
||||
})();
|
|
@ -1,10 +1,9 @@
|
|||
const shareableEntityTypes = ['list', 'custom_form', 'report', 'report_template', 'namespace'];
|
||||
|
||||
exports.up = function(knex, Promise) {
|
||||
let schema = knex.schema;
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
|
||||
for (const entityType of shareableEntityTypes) {
|
||||
schema = schema
|
||||
await knex.schema
|
||||
.createTable(`shares_${entityType}`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`${entityType}s.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
|
@ -21,7 +20,7 @@ exports.up = function(knex, Promise) {
|
|||
}
|
||||
/* The global share for admin is set automatically in rebuildPermissions, which is called upon every start */
|
||||
|
||||
schema = schema
|
||||
await knex.schema
|
||||
.createTable('generated_role_names', table => {
|
||||
table.string('entity_type', 32).notNullable();
|
||||
table.string('role', 128).notNullable();
|
||||
|
@ -30,18 +29,12 @@ exports.up = function(knex, Promise) {
|
|||
table.primary(['entity_type', 'role']);
|
||||
});
|
||||
/* The generate_role_names table is repopulated in regenerateRoleNamesTable, which is called upon every start */
|
||||
})();
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
let schema = knex.schema;
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
for (const entityType of shareableEntityTypes) {
|
||||
schema = schema
|
||||
await knex.schema
|
||||
.dropTable(`shares_${entityType}`)
|
||||
.dropTable(`permissions_${entityType}`);
|
||||
}
|
||||
|
||||
return schema;
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table('users', table => {
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.table('users', table => {
|
||||
// name and password can be null in case of LDAP login
|
||||
table.string('name');
|
||||
table.string('password').alter();
|
||||
})
|
||||
.then(() => knex('users').where('id', 1 /* Admin user id */).update({
|
||||
name: 'Administrator'
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
};
|
||||
await knex('users').where('id', 1 /* Admin user id */).update({
|
||||
name: 'Administrator'
|
||||
});
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
|
@ -1,9 +1,9 @@
|
|||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table('users', table => {
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.table('users', table => {
|
||||
table.string('role');
|
||||
});
|
||||
/* The user role is set automatically in rebuild permissions, which is called upon every start */
|
||||
};
|
||||
})();
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
};
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
|
@ -1,10 +1,10 @@
|
|||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table('custom_forms_data', table => {
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.table('custom_forms_data', table => {
|
||||
table.dropColumn('id');
|
||||
table.string('data_key', 128).alter();
|
||||
table.primary(['form', 'data_key']);
|
||||
})
|
||||
};
|
||||
})();
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
};
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
|
@ -1,9 +1,9 @@
|
|||
exports.up = function(knex, Promise) {
|
||||
return knex.schema.table('custom_forms', table => {
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.table('custom_forms', table => {
|
||||
table.dropForeign('list', 'custom_forms_ibfk_1');
|
||||
table.dropColumn('list');
|
||||
})
|
||||
};
|
||||
})();
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
};
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
|
@ -25,7 +25,7 @@ exports.up = (knex, Promise) => (async() => {
|
|||
key: 'FIRST_NAME',
|
||||
type: 'text',
|
||||
column: 'first_name',
|
||||
visible: 1 // FIXME - Revise the need for this field
|
||||
visible: 1
|
||||
});
|
||||
|
||||
const [lastNameFieldId] = await knex('custom_fields').insert({
|
||||
|
@ -34,7 +34,7 @@ exports.up = (knex, Promise) => (async() => {
|
|||
key: 'LAST_NAME',
|
||||
type: 'text',
|
||||
column: 'last_name',
|
||||
visible: 1 // FIXME - Revise the need for this field
|
||||
visible: 1
|
||||
});
|
||||
|
||||
let orderSubscribe;
|
||||
|
@ -66,6 +66,11 @@ exports.up = (knex, Promise) => (async() => {
|
|||
}
|
||||
|
||||
const orderList = [firstNameFieldId, lastNameFieldId];
|
||||
for (const fld of fields) {
|
||||
if (fld.visible && fld.type === 'text') {
|
||||
orderList.push(fld.id);
|
||||
}
|
||||
}
|
||||
|
||||
let idx = 0;
|
||||
for (const fldId of orderSubscribe) {
|
||||
|
@ -90,8 +95,12 @@ exports.up = (knex, Promise) => (async() => {
|
|||
table.dropColumn('fields_shown_on_subscribe');
|
||||
table.dropColumn('fields_shown_on_manage');
|
||||
});
|
||||
|
||||
await knex.schema.table('custom_fields', table => {
|
||||
table.dropColumn('visible');
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
exports.down = function(knex, Promise) {
|
||||
};
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
|
@ -0,0 +1,57 @@
|
|||
"use strict";
|
||||
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.table('custom_fields', table => {
|
||||
table.json('settings');
|
||||
});
|
||||
|
||||
const fields = await knex('custom_fields');
|
||||
|
||||
for (const field of fields) {
|
||||
const settings = {};
|
||||
let type = field.type;
|
||||
|
||||
if (type === 'json') {
|
||||
settings.groupTemplate = field.group_template;
|
||||
}
|
||||
|
||||
if (type === 'checkbox') {
|
||||
settings.groupTemplate = field.group_template;
|
||||
}
|
||||
|
||||
if (['dropdown', 'radio'].includes(type)) {
|
||||
settings.groupTemplate = field.group_template;
|
||||
type = type + '-grouped';
|
||||
}
|
||||
|
||||
if (type === 'date-eur') {
|
||||
type = 'date';
|
||||
settings.dateFormat = 'eur';
|
||||
}
|
||||
|
||||
if (type === 'date-us') {
|
||||
type = 'date';
|
||||
settings.dateFormat = 'us';
|
||||
}
|
||||
|
||||
if (type === 'birthday-eur') {
|
||||
type = 'birthday';
|
||||
settings.dateFormat = 'eur';
|
||||
}
|
||||
|
||||
if (type === 'birthday-us') {
|
||||
type = 'birthday';
|
||||
settings.dateFormat = 'us';
|
||||
}
|
||||
|
||||
await knex('custom_fields').where('id', field.id).update({type, settings: JSON.stringify(settings)});
|
||||
}
|
||||
|
||||
await knex.schema.table('custom_fields', table => {
|
||||
table.dropColumn('group_template');
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
|
@ -50,6 +50,12 @@ class DuplicitEmailError extends InteroperableError {
|
|||
}
|
||||
}
|
||||
|
||||
class DuplicitKeyError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('DuplicitKeyError', msg, data);
|
||||
}
|
||||
}
|
||||
|
||||
class IncorrectPasswordError extends InteroperableError {
|
||||
constructor(msg, data) {
|
||||
super('IncorrectPasswordError', msg, data);
|
||||
|
@ -84,6 +90,7 @@ const errorTypes = {
|
|||
ChildDetectedError,
|
||||
DuplicitNameError,
|
||||
DuplicitEmailError,
|
||||
DuplicitKeyError,
|
||||
IncorrectPasswordError,
|
||||
InvalidTokenError,
|
||||
DependencyNotFoundError,
|
||||
|
|
9
shared/validators.js
Normal file
9
shared/validators.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
function mergeTagValid(mergeTag) {
|
||||
return /^[A-Z][A-Z0-9_]*$/.test(mergeTag);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mergeTagValid
|
||||
};
|
Loading…
Reference in a new issue