diff --git a/client/src/lib/namespace.js b/client/src/lib/namespace.js new file mode 100644 index 00000000..aeaddc7e --- /dev/null +++ b/client/src/lib/namespace.js @@ -0,0 +1,30 @@ +'use strict'; + +import React, { Component } from 'react'; +import { translate } from 'react-i18next'; +import { TreeTableSelect } from './form'; + + +@translate() +class NamespaceSelect extends Component { + render() { + const t = this.props.t; + + return ( + + ); + } +} + +function validateNamespace(t, state) { + if (!state.getIn(['namespace', 'value'])) { + state.setIn(['namespace', 'error'], t('Namespace must be selected')); + } else { + state.setIn(['namespace', 'error'], null); + } +} + +export { + NamespaceSelect, + validateNamespace +}; \ No newline at end of file diff --git a/client/src/lib/tree.js b/client/src/lib/tree.js index edfbe497..bf68d921 100644 --- a/client/src/lib/tree.js +++ b/client/src/lib/tree.js @@ -149,14 +149,14 @@ class TreeTable extends Component { updateSelection() { const tree = this.tree; if (this.selectMode === TreeSelectMode.MULTI) { - const selectSet = new Set(this.props.selection); + const selectSet = new Set(this.props.selection.map(key => this.stringifyKey(key))); tree.enableUpdate(false); tree.visit(node => node.setSelected(selectSet.has(node.key))); tree.enableUpdate(true); } else if (this.selectMode === TreeSelectMode.SINGLE) { - this.tree.activateKey(this.props.selection); + this.tree.activateKey(this.stringifyKey(this.props.selection)); } } @@ -167,9 +167,26 @@ class TreeTable extends Component { } } + stringifyKey(key) { + if (key !== null && key !== undefined) { + return key.toString(); + } else { + return key; + } + } + + destringifyKey(key) { + if (/^(\-|\+)?([0-9]+|Infinity)$/.test(key)) { + return Number(key); + } else { + return key; + } + } + // Single-select onActivate(event, data) { - const selection = this.tree.getActiveNode().key; + const selection = this.destringifyKey(this.tree.getActiveNode().key); + if (selection !== this.props.selection) { this.onSelectionChanged(selection); } @@ -177,7 +194,7 @@ class TreeTable extends Component { // Multi-select onSelect(event, data) { - const newSel = this.tree.getSelectedNodes().map(node => node.key).sort(); + const newSel = this.tree.getSelectedNodes().map(node => this.destringifyKey(node.key)).sort(); const oldSel = this.props.selection; let updated = false; diff --git a/client/src/namespaces/CUD.js b/client/src/namespaces/CUD.js index bd7277a4..00802dba 100644 --- a/client/src/namespaces/CUD.js +++ b/client/src/namespaces/CUD.js @@ -81,9 +81,7 @@ export default class CUD extends Component { @withAsyncErrorHandler async loadFormValues() { - await this.getFormValuesFromURL(`/rest/namespaces/${this.state.entityId}`, data => { - if (data.parent) data.parent = data.parent.toString(); - }); + await this.getFormValuesFromURL(`/rest/namespaces/${this.state.entityId}`); } componentDidMount() { @@ -93,7 +91,7 @@ export default class CUD extends Component { this.populateFormValues({ name: '', description: '', - parent: null + namespace: null }); } @@ -112,10 +110,10 @@ export default class CUD extends Component { } if (!this.isEditGlobal()) { - if (!state.getIn(['parent', 'value'])) { - state.setIn(['parent', 'error'], t('Parent Namespace must be selected')); + if (!state.getIn(['namespace', 'value'])) { + state.setIn(['namespace', 'error'], t('Parent Namespace must be selected')); } else { - state.setIn(['parent', 'error'], null); + state.setIn(['namespace', 'error'], null); } } } @@ -137,9 +135,7 @@ export default class CUD extends Component { this.disableForm(); this.setFormStatusMessage('info', t('Saving namespace ...')); - const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { - if (data.parent) data.parent = parseInt(data.parent); - }); + const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url); if (submitSuccessful) { this.navigateToWithFlashMessage('/namespaces', 'success', t('Namespace saved')); @@ -232,7 +228,7 @@ export default class CUD extends Component { {!this.isEditGlobal() && - } + } diff --git a/client/src/reports/CUD.js b/client/src/reports/CUD.js index cc77327b..eea2e16f 100644 --- a/client/src/reports/CUD.js +++ b/client/src/reports/CUD.js @@ -12,6 +12,7 @@ import axios from '../lib/axios'; import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import { ModalDialog } from '../lib/bootstrap-components'; import moment from 'moment'; +import { validateNamespace, NamespaceSelect } from '../lib/namespace'; @translate() @withForm @@ -76,6 +77,7 @@ export default class CUD extends Component { name: '', description: '', report_template: null, + namespace: null, user_fields: null }); } @@ -122,6 +124,8 @@ export default class CUD extends Component { } } } + + validateNamespace(t, state); } async submitHandler() { @@ -191,7 +195,7 @@ export default class CUD extends Component { const t = this.props.t; const edit = this.props.edit; - const columns = [ + const reportTemplateColumns = [ { data: 0, title: "#" }, { data: 1, title: t('Name') }, { data: 2, title: t('Description') }, @@ -258,7 +262,9 @@ export default class CUD extends Component { - + + + {userFieldsSpec ? userFields.length > 0 && diff --git a/client/src/reports/List.js b/client/src/reports/List.js index f758edb6..685c2ca6 100644 --- a/client/src/reports/List.js +++ b/client/src/reports/List.js @@ -32,9 +32,9 @@ export default class List extends Component { const actions = data => { let view, startStop, refreshTimeout; - const state = data[5]; + const state = data[6]; const id = data[0]; - const mimeType = data[6]; + const mimeType = data[7]; if (state === ReportState.PROCESSING || state === ReportState.SCHEDULED) { view = { @@ -98,7 +98,8 @@ export default class List extends Component { { data: 1, title: t('Name') }, { data: 2, title: t('Template') }, { data: 3, title: t('Description') }, - { data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' } + { data: 4, title: t('Created'), render: data => data ? moment(data).fromNow() : '' }, + { data: 5, title: t('Namespace') } ]; return ( diff --git a/client/src/reports/templates/CUD.js b/client/src/reports/templates/CUD.js index 7f197286..8c6579dc 100644 --- a/client/src/reports/templates/CUD.js +++ b/client/src/reports/templates/CUD.js @@ -8,6 +8,7 @@ import { withForm, Form, FormSendMethod, InputField, TextArea, Dropdown, ACEEdit import axios from '../../lib/axios'; import { withErrorHandling, withAsyncErrorHandler } from '../../lib/error-handling'; import { ModalDialog } from '../../lib/bootstrap-components'; +import { validateNamespace, NamespaceSelect } from '../../lib/namespace'; @translate() @withForm @@ -50,6 +51,7 @@ export default class CUD extends Component { this.populateFormValues({ name: '', description: 'Generates a campaign report listing all subscribers along with their statistics.', + namespace: null, mime_type: 'text/html', user_fields: '[\n' + @@ -99,6 +101,7 @@ export default class CUD extends Component { this.populateFormValues({ name: '', description: 'Generates a campaign report with results are aggregated by some "Country" custom field.', + namespace: null, mime_type: 'text/html', user_fields: '[\n' + @@ -169,6 +172,7 @@ export default class CUD extends Component { this.populateFormValues({ name: '', description: 'Exports a list as a CSV file.', + namespace: null, mime_type: 'text/csv', user_fields: '[\n' + @@ -193,6 +197,7 @@ export default class CUD extends Component { this.populateFormValues({ name: '', description: '', + namespace: null, mime_type: 'text/html', user_fields: '', js: '', @@ -226,6 +231,8 @@ export default class CUD extends Component { state.setIn(['user_fields', 'error'], t('Syntax error in the user fields specification')); } } + + validateNamespace(t, state); } async submitAndStay() { @@ -310,6 +317,7 @@ export default class CUD extends Component { + Write the body of the JavaScript function with signature function(inputs, callback) that returns an object to be rendered by the Handlebars template below.}/> Use HTML with Handlebars syntax. See documentation here.}/> diff --git a/client/src/reports/templates/List.js b/client/src/reports/templates/List.js index edf9e091..2137e0e5 100644 --- a/client/src/reports/templates/List.js +++ b/client/src/reports/templates/List.js @@ -33,7 +33,8 @@ export default class List extends Component { { data: 0, title: "#" }, { data: 1, title: t('Name') }, { data: 2, title: t('Description') }, - { data: 3, title: t('Created'), render: data => moment(data).fromNow() } + { data: 3, title: t('Created'), render: data => moment(data).fromNow() }, + { data: 4, title: t('Namespace') } ]; return ( diff --git a/client/src/users/CUD.js b/client/src/users/CUD.js index 429811f3..b614167f 100644 --- a/client/src/users/CUD.js +++ b/client/src/users/CUD.js @@ -4,7 +4,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { translate } from 'react-i18next'; import { requiresAuthenticatedUser, withPageHelpers, Title } from '../lib/page'; -import { withForm, Form, FormSendMethod, InputField, ButtonRow, Button } from '../lib/form'; +import { withForm, Form, FormSendMethod, InputField, ButtonRow, Button, TreeTableSelect } from '../lib/form'; import axios from '../lib/axios'; import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'; import interoperableErrors from '../../../shared/interoperable-errors'; @@ -12,6 +12,7 @@ import passwordValidator from '../../../shared/password-validator'; import validators from '../../../shared/validators'; import { ModalDialog } from '../lib/bootstrap-components'; import mailtrainConfig from 'mailtrainConfig'; +import { validateNamespace, NamespaceSelect } from '../lib/namespace'; @translate() @withForm @@ -64,7 +65,8 @@ export default class CUD extends Component { name: '', email: '', password: '', - password2: '' + password2: '', + namespace: null }); } } @@ -129,6 +131,8 @@ export default class CUD extends Component { state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null); state.setIn(['password2', 'error'], password !== password2 ? t('Passwords must match') : null); + + validateNamespace(t, state); } async submitHandler() { @@ -229,6 +233,7 @@ export default class CUD extends Component { + diff --git a/client/src/users/List.js b/client/src/users/List.js index 2ff3aa41..d60627b1 100644 --- a/client/src/users/List.js +++ b/client/src/users/List.js @@ -35,6 +35,8 @@ export default class List extends Component { ]; } + columns.push({ data: 3, title: "Namespace" }); + return ( diff --git a/lib/namespace-helpers.js b/lib/namespace-helpers.js new file mode 100644 index 00000000..11233c8e --- /dev/null +++ b/lib/namespace-helpers.js @@ -0,0 +1,16 @@ +'use strict'; + +const knex = require('./knex'); +const { enforce } = require('./helpers'); +const interoperableErrors = require('../shared/interoperable-errors'); + +async function validateEntity(tx, entity) { + enforce(entity.namespace, 'Entity namespace not set'); + if (!await tx('namespaces').where('id', entity.namespace).first()) { + throw new interoperableErrors.DependencyNotFoundError(); + } +} + +module.exports = { + validateEntity +}; \ No newline at end of file diff --git a/models/namespaces.js b/models/namespaces.js index 86809805..ed0cde04 100644 --- a/models/namespaces.js +++ b/models/namespaces.js @@ -5,7 +5,7 @@ const hasher = require('node-object-hash')(); const { enforce, filterObject } = require('../lib/helpers'); const interoperableErrors = require('../shared/interoperable-errors'); -const allowedKeys = new Set(['name', 'description', 'parent']); +const allowedKeys = new Set(['name', 'description', 'namespace']); async function list() { return await knex('namespaces'); @@ -28,8 +28,8 @@ async function create(entity) { await knex.transaction(async tx => { const id = await tx('namespaces').insert(filterObject(entity, allowedKeys)); - if (entity.parent) { - if (!await tx('namespaces').select(['id']).where('id', entity.parent).first()) { + if (entity.namespace) { + if (!await tx('namespaces').select(['id']).where('id', entity.namespace).first()) { throw new interoperableErrors.DependencyNotFoundError(); } } @@ -39,7 +39,7 @@ async function create(entity) { } async function updateWithConsistencyCheck(entity) { - enforce(entity.id !== 1 || entity.parent === null, 'Cannot assign a parent to the root namespace.'); + enforce(entity.id !== 1 || entity.namespace === null, 'Cannot assign a parent to the root namespace.'); await knex.transaction(async tx => { const existing = await tx('namespaces').where('id', entity.id).first(); @@ -53,8 +53,8 @@ async function updateWithConsistencyCheck(entity) { } let iter = entity; - while (iter.parent != null) { - iter = await tx('namespaces').where('id', iter.parent).first(); + while (iter.namespace != null) { + iter = await tx('namespaces').where('id', iter.namespace).first(); if (!iter) { throw new interoperableErrors.DependencyNotFoundError(); @@ -73,7 +73,7 @@ async function remove(id) { enforce(id !== 1, 'Cannot delete the root namespace.'); await knex.transaction(async tx => { - const childNs = await tx('namespaces').where('parent', id).first(); + 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 897346ad..637d0ac9 100644 --- a/models/report-templates.js +++ b/models/report-templates.js @@ -5,8 +5,9 @@ const hasher = require('node-object-hash')(); const { enforce, filterObject } = require('../lib/helpers'); const dtHelpers = require('../lib/dt-helpers'); const interoperableErrors = require('../shared/interoperable-errors'); +const namespaceHelpers = require('../lib/namespace-helpers'); -const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs']); +const allowedKeys = new Set(['name', 'description', 'mime_type', 'user_fields', 'js', 'hbs', 'namespace']); function hash(entity) { return hasher.hash(filterObject(entity, allowedKeys)); @@ -22,27 +23,36 @@ async function getById(id) { } async function listDTAjax(params) { - return await dtHelpers.ajaxList(params, tx => tx('report_templates'), ['report_templates.id', 'report_templates.name', 'report_templates.description', 'report_templates.created']); + return await dtHelpers.ajaxList( + params, + tx => tx('report_templates').innerJoin('namespaces', 'namespaces.id', 'report_templates.namespace'), + ['report_templates.id', 'report_templates.name', 'report_templates.description', 'report_templates.created', 'namespaces.name'] + ); } async function create(entity) { - const id = await knex('report_templates').insert(filterObject(entity, allowedKeys)); - return id; + await knex.transaction(async tx => { + await namespaceHelpers.validateEntity(tx, entity); + const id = await tx('report_templates').insert(filterObject(entity, allowedKeys)); + return id; + }); } -async function updateWithConsistencyCheck(template) { +async function updateWithConsistencyCheck(entity) { await knex.transaction(async tx => { - const existing = await tx('report_templates').where('id', template.id).first(); - if (!template) { + const existing = await tx('report_templates').where('id', entity.id).first(); + if (!entity) { throw new interoperableErrors.NotFoundError(); } const existingHash = hash(existing); - if (existingHash != template.originalHash) { + if (existingHash != entity.originalHash) { throw new interoperableErrors.ChangedError(); } - await tx('report_templates').where('id', template.id).update(filterObject(template, allowedKeys)); + await namespaceHelpers.validateEntity(tx, entity); + + await tx('report_templates').where('id', entity.id).update(filterObject(entity, allowedKeys)); }); } diff --git a/models/reports.js b/models/reports.js index 630cbb8b..30daf820 100644 --- a/models/reports.js +++ b/models/reports.js @@ -6,10 +6,11 @@ const { enforce, filterObject } = require('../lib/helpers'); const dtHelpers = require('../lib/dt-helpers'); const interoperableErrors = require('../shared/interoperable-errors'); const fields = require('./fields'); +const namespaceHelpers = require('../lib/namespace-helpers'); const ReportState = require('../shared/reports').ReportState; -const allowedKeys = new Set(['name', 'description', 'report_template', 'params']); +const allowedKeys = new Set(['name', 'description', 'report_template', 'params', 'namespace']); function hash(entity) { @@ -17,7 +18,12 @@ function hash(entity) { } async function getByIdWithTemplate(id) { - const entity = await knex('reports').where('reports.id', id).innerJoin('report_templates', 'reports.report_template', 'report_templates.id').select(['reports.id', 'reports.name', 'reports.description', 'reports.report_template', 'reports.params', 'reports.state', 'report_templates.user_fields', 'report_templates.mime_type', 'report_templates.hbs', 'report_templates.js']).first(); + const entity = await knex('reports') + .where('reports.id', id) + .innerJoin('report_templates', 'reports.report_template', 'report_templates.id') + .select(['reports.id', 'reports.name', 'reports.description', 'reports.report_template', 'reports.params', 'reports.state', 'reports.namespace', 'report_templates.user_fields', 'report_templates.mime_type', 'report_templates.hbs', 'report_templates.js']) + .first(); + if (!entity) { throw new interoperableErrors.NotFoundError(); } @@ -29,12 +35,20 @@ async function getByIdWithTemplate(id) { } async function listDTAjax(params) { - return await dtHelpers.ajaxList(params, tx => tx('reports').innerJoin('report_templates', 'reports.report_template', 'report_templates.id'), ['reports.id', 'reports.name', 'report_templates.name', 'reports.description', 'reports.last_run', 'reports.state', 'report_templates.mime_type']); + return await dtHelpers.ajaxList( + params, + tx => tx('reports') + .innerJoin('report_templates', 'reports.report_template', 'report_templates.id') + .innerJoin('namespaces', 'namespaces.id', 'reports.namespace'), + ['reports.id', 'reports.name', 'report_templates.name', 'reports.description', 'reports.last_run', 'namespaces.name', 'reports.state', 'report_templates.mime_type'] + ); } async function create(entity) { let id; await knex.transaction(async tx => { + await namespaceHelpers.validateEntity(tx, entity); + if (!await tx('report_templates').select(['id']).where('id', entity.report_template).first()) { throw new interoperableErrors.DependencyNotFoundError(); } @@ -67,6 +81,8 @@ async function updateWithConsistencyCheck(entity) { throw new interoperableErrors.DependencyNotFoundError(); } + await namespaceHelpers.validateEntity(tx, entity); + entity.params = JSON.stringify(entity.params); const filteredUpdates = filterObject(entity, allowedKeys); diff --git a/models/users.js b/models/users.js index aee96f2e..bb334c16 100644 --- a/models/users.js +++ b/models/users.js @@ -22,12 +22,16 @@ const bcryptCompare = bluebird.promisify(bcrypt.compare); const mailer = require('../lib/mailer'); const mailerSendMail = bluebird.promisify(mailer.sendMail); -const allowedKeys = new Set(['username', 'name', 'email', 'password']); +const passport = require('../lib/passport'); + +const namespaceHelpers = require('../lib/namespace-helpers'); + +const allowedKeys = new Set(['username', 'name', 'email', 'password', 'namespace']); const allowedKeysExternal = new Set(['username']); const ownAccountAllowedKeys = new Set(['name', 'email', 'password']); -const hashKeys = new Set(['username', 'name', 'email']); +const hashKeys = new Set(['username', 'name', 'email', 'namespace']); -const passport = require('../lib/passport'); +const defaultNamespace = 1; function hash(entity) { @@ -35,7 +39,7 @@ function hash(entity) { } async function _getBy(key, value, extraColumns) { - const columns = ['id', 'username', 'name', 'email']; + const columns = ['id', 'username', 'name', 'email', 'namespace']; if (extraColumns) { columns.push(...extraColumns); @@ -97,19 +101,24 @@ async function serverValidate(data, isOwnAccount) { } async function listDTAjax(params) { - return await dtHelpers.ajaxList(params, tx => tx('users'), ['users.id', 'users.username', 'users.name']); + return await dtHelpers.ajaxList( + params, + tx => tx('users').innerJoin('namespaces', 'namespaces.id', 'users.namespace'), + ['users.id', 'users.username', 'users.name', 'namespaces.name'] + ); } async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) { enforce(await tools.validateEmail(user.email) === 0, 'Invalid email'); + await namespaceHelpers.validateEntity(tx, user); + const otherUserWithSameEmailQuery = tx('users').where('email', user.email); if (user.id) { otherUserWithSameEmailQuery.andWhereNot('id', user.id); } - const otherUserWithSameUsername = await otherUserWithSameEmailQuery.first(); - if (otherUserWithSameUsername) { + if (await otherUserWithSameEmailQuery.first()) { throw new interoperableErrors.DuplicitEmailError(); } @@ -122,8 +131,7 @@ async function _validateAndPreprocess(tx, user, isCreate, isOwnAccount) { otherUserWithSameUsernameQuery.andWhereNot('id', user.id); } - const otherUserWithSameUsername = await otherUserWithSameUsernameQuery.first(); - if (otherUserWithSameUsername) { + if (await otherUserWithSameUsernameQuery.first()) { throw new interoperableErrors.DuplicitNameError(); } } @@ -157,7 +165,10 @@ async function create(user) { async function createExternal(user) { enforce(!passport.isAuthMethodLocal, 'External user management is required'); - const userId = await knex('users').insert(filterObject(user, allowedKeysExternal)); + const filteredUser = filterObject(user, allowedKeysExternal); + filteredUser.namespace = defaultNamespace; + + const userId = await knex('users').insert(filteredUser); return userId; } @@ -167,7 +178,7 @@ async function updateWithConsistencyCheck(user, isOwnAccount) { await knex.transaction(async tx => { await _validateAndPreprocess(tx, user, false, isOwnAccount); - const existingUser = await tx('users').select(['id', 'username', 'name', 'email', 'password']).where('id', user.id).first(); + const existingUser = await tx('users').where('id', user.id).first(); if (!user) { throw new interoperableErrors.NotFoundError(); } diff --git a/routes/rest/namespaces.js b/routes/rest/namespaces.js index 5e28c240..50d34422 100644 --- a/routes/rest/namespaces.js +++ b/routes/rest/namespaces.js @@ -49,14 +49,14 @@ router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => { entry = entries[row.id]; } - if (row.parent) { - if (!entries[row.parent]) { - entries[row.parent] = { + if (row.namespace) { + if (!entries[row.namespace]) { + entries[row.namespace] = { children: [] }; } - entries[row.parent].children.push(entry); + entries[row.namespace].children.push(entry); } else { root = entry; diff --git a/setup/knex/migrations/20170507083345_create_namespaces.js b/setup/knex/migrations/20170507083345_create_namespaces.js index 38a4bc2a..4437a733 100644 --- a/setup/knex/migrations/20170507083345_create_namespaces.js +++ b/setup/knex/migrations/20170507083345_create_namespaces.js @@ -3,10 +3,7 @@ exports.up = function(knex, Promise) { table.increments('id').primary(); table.string('name'); table.text('description'); - table.integer('parent').unsigned().references('namespaces.id').onDelete('CASCADE'); - }) - .table('lists', table => { - table.integer('namespace').unsigned().notNullable(); + table.integer('namespace').unsigned().references('namespaces.id').onDelete('CASCADE'); }) .then(() => knex('namespaces').insert({ @@ -15,11 +12,43 @@ exports.up = function(knex, Promise) { description: 'Global namespace' })) - .then(() => knex('lists').update({ + .then(() => knex.schema.table('users', table => { + table.integer('namespace').unsigned().notNullable(); + })) + .then(() => knex('users').update({ namespace: 1 })) + .then(() => knex.schema.table('users', table => { + table.foreign('namespace').references('namespaces.id').onDelete('CASCADE'); + })) .then(() => knex.schema.table('lists', table => { + table.integer('namespace').unsigned().notNullable(); + })) + .then(() => knex('lists').update({ + namespace: 1 + })) + .then(() => knex.schema.table('lists', table => { + table.foreign('namespace').references('namespaces.id').onDelete('CASCADE'); + })) + + .then(() => knex.schema.table('report_templates', table => { + table.integer('namespace').unsigned().notNullable(); + })) + .then(() => knex('report_templates').update({ + namespace: 1 + })) + .then(() => knex.schema.table('report_templates', table => { + table.foreign('namespace').references('namespaces.id').onDelete('CASCADE'); + })) + + .then(() => knex.schema.table('reports', table => { + table.integer('namespace').unsigned().notNullable(); + })) + .then(() => knex('reports').update({ + namespace: 1 + })) + .then(() => knex.schema.table('reports', table => { table.foreign('namespace').references('namespaces.id').onDelete('CASCADE'); }))
function(inputs, callback)