work in progress on custom fields

This commit is contained in:
Tomas Bures 2017-08-11 08:51:30 +02:00
parent 361af18384
commit 86fce404a9
29 changed files with 1088 additions and 198 deletions

2
app.js
View file

@ -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) {

View file

@ -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);

View file

@ -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;
};

View 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>
);
}
}

View 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>
);
}
}

View 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;
}

View file

@ -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',

View file

@ -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"

View file

@ -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
}
);
}

View file

@ -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
};

View file

@ -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) {

View file

@ -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);
});

View file

@ -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();

View file

@ -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();
}

View file

@ -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');

View file

@ -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,

View file

@ -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
View 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;

View file

@ -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');
};
})();

View file

@ -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');
})();

View file

@ -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;
};
})();

View file

@ -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() => {
})();

View file

@ -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() => {
})();

View file

@ -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() => {
})();

View file

@ -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() => {
})();

View file

@ -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() => {
})();

View file

@ -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() => {
})();

View file

@ -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
View file

@ -0,0 +1,9 @@
'use strict';
function mergeTagValid(mergeTag) {
return /^[A-Z][A-Z0-9_]*$/.test(mergeTag);
}
module.exports = {
mergeTagValid
};