+
+ return (
+
+ {edit &&
+
+ }
+
+
{edit ? t('Edit Field') : t('Create Field')}
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/client/src/lists/fields/List.js b/client/src/lists/fields/List.js
new file mode 100644
index 00000000..da50988a
--- /dev/null
+++ b/client/src/lists/fields/List.js
@@ -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: ,
+ 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 (
+
+
+
+
+
+
{t('Fields')}
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/client/src/lists/fields/field-types.js b/client/src/lists/fields/field-types.js
new file mode 100644
index 00000000..eac9b0b9
--- /dev/null
+++ b/client/src/lists/fields/field-types.js
@@ -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:
+
+ },
+ 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;
+}
+
diff --git a/client/src/lists/root.js b/client/src/lists/root.js
index 28e08c0b..c1995c3b 100644
--- a/client/src/lists/root.js
+++ b/client/src/lists/root.js
@@ -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 => ()
+ },
+ create: {
+ title: t('Create'),
+ render: props => ()
+ },
+ share: {
+ title: t('Share'),
+ render: props => ()
+ }
+ }
+ },
+*/
edit: {
title: t('Edit List'),
params: [':id', ':action?'],
@@ -40,6 +65,24 @@ const getStructure = t => {
params: [':id'],
render: props => ( 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 => ()
+ },
+ create: {
+ title: t('Create Field'),
+ params: [':listId'],
+ render: props => ()
+ },
+ }
+ },
forms: {
title: t('Custom Forms'),
link: '/lists/forms',
diff --git a/config/default.toml b/config/default.toml
index 43908b66..abbf9c28 100644
--- a/config/default.toml
+++ b/config/default.toml
@@ -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"
diff --git a/lib/dt-helpers.js b/lib/dt-helpers.js
index 801f9953..26514c86 100644
--- a/lib/dt-helpers.js
+++ b/lib/dt-helpers.js
@@ -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
}
);
}
diff --git a/models/fields.js b/models/fields.js
index 40d50404..8719d280 100644
--- a/models/fields.js
+++ b/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
};
\ No newline at end of file
diff --git a/models/forms.js b/models/forms.js
index efdb0c0e..1a8109eb 100644
--- a/models/forms.js
+++ b/models/forms.js
@@ -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) {
diff --git a/models/lists.js b/models/lists.js
index e8bde8dd..84d21642 100644
--- a/models/lists.js
+++ b/models/lists.js
@@ -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);
});
diff --git a/models/namespaces.js b/models/namespaces.js
index f4585afb..ba3b86cd 100644
--- a/models/namespaces.js
+++ b/models/namespaces.js
@@ -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();
diff --git a/models/report-templates.js b/models/report-templates.js
index 29362ba2..cbd737d2 100644
--- a/models/report-templates.js
+++ b/models/report-templates.js
@@ -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();
}
diff --git a/models/reports.js b/models/reports.js
index 503e604a..2c47d365 100644
--- a/models/reports.js
+++ b/models/reports.js
@@ -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');
diff --git a/models/shares.js b/models/shares.js
index 714ef2fd..ba66d826 100644
--- a/models/shares.js
+++ b/models/shares.js
@@ -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,
diff --git a/models/users.js b/models/users.js
index fe431e9b..3a1c8d44 100644
--- a/models/users.js
+++ b/models/users.js
@@ -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();
});
diff --git a/routes/rest/fields.js b/routes/rest/fields.js
new file mode 100644
index 00000000..79b0db01
--- /dev/null
+++ b/routes/rest/fields.js
@@ -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;
\ No newline at end of file
diff --git a/setup/knex/migrations/20170506102634_base.js b/setup/knex/migrations/20170506102634_base.js
index b6b31ab7..a528dbbc 100644
--- a/setup/knex/migrations/20170506102634_base.js
+++ b/setup/knex/migrations/20170506102634_base.js
@@ -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');
-};
\ No newline at end of file
+})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170507083345_create_namespaces.js b/setup/knex/migrations/20170507083345_create_namespaces.js
index c6c43195..2f38a196 100644
--- a/setup/knex/migrations/20170507083345_create_namespaces.js
+++ b/setup/knex/migrations/20170507083345_create_namespaces.js
@@ -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');
-};
\ No newline at end of file
+exports.down = (knex, Promise) => (async() => {
+ await knex.schema.dropTable('namespaces');
+})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170507084114_create_permissions.js b/setup/knex/migrations/20170507084114_create_permissions.js
index de91a9e7..a3f6ef8c 100644
--- a/setup/knex/migrations/20170507084114_create_permissions.js
+++ b/setup/knex/migrations/20170507084114_create_permissions.js
@@ -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;
-};
\ No newline at end of file
+})();
diff --git a/setup/knex/migrations/20170617123450_create_user_name.js b/setup/knex/migrations/20170617123450_create_user_name.js
index f3bcb8e8..86b8b10f 100644
--- a/setup/knex/migrations/20170617123450_create_user_name.js
+++ b/setup/knex/migrations/20170617123450_create_user_name.js
@@ -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) {
-};
\ No newline at end of file
+ await knex('users').where('id', 1 /* Admin user id */).update({
+ name: 'Administrator'
+ });
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170726155118_create_user_role.js b/setup/knex/migrations/20170726155118_create_user_role.js
index ecdb33ba..0c5a3569 100644
--- a/setup/knex/migrations/20170726155118_create_user_role.js
+++ b/setup/knex/migrations/20170726155118_create_user_role.js
@@ -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) {
-};
\ No newline at end of file
+exports.down = (knex, Promise) => (async() => {
+})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170728220422_drop_id_in_custom_forms_data.js b/setup/knex/migrations/20170728220422_drop_id_in_custom_forms_data.js
index 9b20f2b3..1d075bdb 100644
--- a/setup/knex/migrations/20170728220422_drop_id_in_custom_forms_data.js
+++ b/setup/knex/migrations/20170728220422_drop_id_in_custom_forms_data.js
@@ -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) {
-};
\ No newline at end of file
+exports.down = (knex, Promise) => (async() => {
+})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170729160135_make_custom_forms_independent_of_list.js b/setup/knex/migrations/20170729160135_make_custom_forms_independent_of_list.js
index dca90e04..b9d4fe1d 100644
--- a/setup/knex/migrations/20170729160135_make_custom_forms_independent_of_list.js
+++ b/setup/knex/migrations/20170729160135_make_custom_forms_independent_of_list.js
@@ -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) {
-};
\ No newline at end of file
+exports.down = (knex, Promise) => (async() => {
+})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170729160422_move_form_field_order_to_custom_fields_and_make_all_fields_configurable.js b/setup/knex/migrations/20170729160422_move_form_field_order_to_custom_fields_and_make_all_fields_configurable.js
index 224b0100..d2de8813 100644
--- a/setup/knex/migrations/20170729160422_move_form_field_order_to_custom_fields_and_make_all_fields_configurable.js
+++ b/setup/knex/migrations/20170729160422_move_form_field_order_to_custom_fields_and_make_all_fields_configurable.js
@@ -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) {
-};
\ No newline at end of file
+exports.down = (knex, Promise) => (async() => {
+})();
\ No newline at end of file
diff --git a/setup/knex/migrations/20170731072050_change_field_group_template_to_settings_and_simplify_types.js b/setup/knex/migrations/20170731072050_change_field_group_template_to_settings_and_simplify_types.js
new file mode 100644
index 00000000..a599a15b
--- /dev/null
+++ b/setup/knex/migrations/20170731072050_change_field_group_template_to_settings_and_simplify_types.js
@@ -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() => {
+})();
\ No newline at end of file
diff --git a/shared/interoperable-errors.js b/shared/interoperable-errors.js
index cb42ddb0..3c867634 100644
--- a/shared/interoperable-errors.js
+++ b/shared/interoperable-errors.js
@@ -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,
diff --git a/shared/validators.js b/shared/validators.js
new file mode 100644
index 00000000..0fcfbcf4
--- /dev/null
+++ b/shared/validators.js
@@ -0,0 +1,9 @@
+'use strict';
+
+function mergeTagValid(mergeTag) {
+ return /^[A-Z][A-Z0-9_]*$/.test(mergeTag);
+}
+
+module.exports = {
+ mergeTagValid
+};
\ No newline at end of file