diff --git a/CHANGELOG.md b/CHANGELOG.md index a834f9e..8e730ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Add content events @robgietema - Add profile metadata @robgietema - Add @site endpoint @robgietema +- Add upgrade profile functionality @robgietema ### Bugfix diff --git a/package.json b/package.json index 60ea6c4..e1880e7 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,9 @@ "lint": "./node_modules/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx,json}'", "migrate": "yarn knex migrate:latest", "prettier": "./node_modules/.bin/prettier --single-quote --check 'src/**/*.{js,jsx,ts,tsx,json}'", - "seed": "yarn knex seed:run", + "seed": "babel-node scripts/seed.js run", + "seed:status": "babel-node scripts/seed.js status", + "seed:upgrade": "babel-node scripts/seed.js upgrade", "start": "nodemon --exec babel-node src/server.js", "rollback": "yarn knex migrate:rollback --all", "reset": "yarn rollback && yarn migrate && yarn seed", diff --git a/scripts/seed.js b/scripts/seed.js new file mode 100644 index 0000000..0b101ad --- /dev/null +++ b/scripts/seed.js @@ -0,0 +1,113 @@ +/* eslint no-console: 0 */ +/** + * Seed script. + * @module scripts/seed + */ + +import { last, padEnd } from 'lodash'; + +import { Profile } from '../src/models'; +import { fileExists, knex, mapAsync, stripI18n } from '../src/helpers'; + +import { + seedProfile, + seedPermission, + seedRole, + seedGroup, + seedUser, + seedWorkflow, + seedType, + seedCatalog, + seedDocument, + seedRedirect, + seedAction, + seedControlpanel, + seedVocabulary, +} from '../src/seeds'; + +const { config } = require(`${process.cwd()}/config`); + +const reset = '\x1b[0m'; +const underline = '\x1b[4m'; + +const seed = async (knex, profilePath) => { + await seedProfile(knex, profilePath); + await seedPermission(knex, profilePath); + await seedRole(knex, profilePath); + await seedGroup(knex, profilePath); + await seedUser(knex, profilePath); + await seedWorkflow(knex, profilePath); + await seedType(knex, profilePath); + await seedCatalog(knex, profilePath); + await seedDocument(knex, profilePath); + await seedRedirect(knex, profilePath); + await seedAction(knex, profilePath); + await seedControlpanel(knex, profilePath); + await seedVocabulary(knex, profilePath); +}; + +/** + * Main function + * @function main + * @return {undefined} + */ +async function main() { + const command = last(process.argv); + + await mapAsync(config.profiles, async (profilePath, index) => { + if (fileExists(`${profilePath}/metadata`)) { + const metadata = stripI18n(require(`${profilePath}/metadata`)); + const profile = await Profile.fetchOne({ id: metadata.id }, {}, knex); + + switch (command) { + case 'status': + if (index === 0) { + console.log( + `${padEnd(`${underline}Profile${reset}`, 58)}${padEnd( + `${underline}Current${reset}`, + 18, + )}${padEnd(`${underline}Latest${reset}`, 18)}`, + ); + } + console.log( + `${padEnd(metadata.id, 50)}${padEnd( + profile ? profile.version : 'Not found', + 10, + )}${padEnd(metadata.version, 10)}`, + ); + break; + case 'upgrade': + if (!profile) { + console.log('Profile is not installed yet'); + } else if (metadata.version === parseInt(profile.version)) { + console.log('Profile already up to date'); + } else { + return mapAsync( + Array.apply(null, { + length: metadata.version - parseInt(profile.version), + }), + async (value, index) => { + const version = parseInt(profile.version) + 1 + index; + console.log(`Upgrading ${profilePath} to ${version}`); + return await seed(knex, `${profilePath}/upgrades/${version}`); + }, + ); + } + break; + default: + console.log(`Applying profile ${metadata.id}`); + if (profile && metadata.version === parseInt(profile.version)) { + console.log('Profile already up to date'); + } else { + return await seed(knex, profilePath); + } + break; + } + } + }); + + // Disconnect from db + knex.destroy(); +} + +main(); diff --git a/src/seeds/000_profile.js b/src/seeds/000_profile.js deleted file mode 100644 index ea1a4ed..0000000 --- a/src/seeds/000_profile.js +++ /dev/null @@ -1,22 +0,0 @@ -import { map } from 'lodash'; - -import { fileExists, log, stripI18n } from '../helpers'; -import { Profile } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await Promise.all( - map(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/metadata`)) { - const profile = stripI18n(require(`${profilePath}/metadata`)); - await Profile.create(profile, {}, knex); - } - }), - ); - log.info('Profile imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/001_permission.js b/src/seeds/001_permission.js deleted file mode 100644 index 5a5b400..0000000 --- a/src/seeds/001_permission.js +++ /dev/null @@ -1,29 +0,0 @@ -import { map } from 'lodash'; - -import { fileExists, log, mapAsync, stripI18n } from '../helpers'; -import { Permission } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/permissions`)) { - const profile = stripI18n(require(`${profilePath}/permissions`)); - if (profile.purge) { - await Permission.delete(knex); - } - await Promise.all( - map( - profile.permissions, - async (permission) => - await Permission.create(permission, {}, knex), - ), - ); - } - }); - log.info('Permissions imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/002_role.js b/src/seeds/002_role.js deleted file mode 100644 index f3bf3c9..0000000 --- a/src/seeds/002_role.js +++ /dev/null @@ -1,33 +0,0 @@ -import { map, omit } from 'lodash'; - -import { fileExists, log, mapAsync, stripI18n } from '../helpers'; -import { Role } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/roles`)) { - const profile = stripI18n(require(`${profilePath}/roles`)); - if (profile.purge) { - await Role.delete(knex); - } - await mapAsync(profile.roles, async (role, index) => { - await Role.create( - { - ...omit(role, ['permissions']), - _permissions: role.permissions, - order: role.order || index, - }, - {}, - knex, - ); - }); - } - }); - log.info('Roles imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/003_group.js b/src/seeds/003_group.js deleted file mode 100644 index cc77f8e..0000000 --- a/src/seeds/003_group.js +++ /dev/null @@ -1,34 +0,0 @@ -import { map, omit } from 'lodash'; - -import { fileExists, log, mapAsync, stripI18n } from '../helpers'; -import { Group } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/groups`)) { - const profile = stripI18n(require(`${profilePath}/groups`)); - if (profile.purge) { - await Group.delete(knex); - } - await Promise.all( - map(profile.groups, async (group) => { - await Group.create( - { - ...omit(group, ['roles']), - _roles: group.roles, - }, - {}, - knex, - ); - }), - ); - } - }); - log.info('Groups imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/004_user.js b/src/seeds/004_user.js deleted file mode 100644 index efa5fce..0000000 --- a/src/seeds/004_user.js +++ /dev/null @@ -1,38 +0,0 @@ -import { map, omit } from 'lodash'; -import bcrypt from 'bcrypt-promise'; - -import { fileExists, log, mapAsync, stripI18n } from '../helpers'; -import { User } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/users`)) { - const profile = stripI18n(require(`${profilePath}/users`)); - if (profile.purge) { - await User.delete(knex); - } - await Promise.all( - map(profile.users, async (user) => { - // Insert user - await User.create( - { - ...omit(user, ['password', 'roles', 'groups']), - password: await bcrypt.hash(user.password, 10), - _roles: user.roles, - _groups: user.groups, - }, - {}, - knex, - ); - }), - ); - } - }); - log.info('Users imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/005_workflow.js b/src/seeds/005_workflow.js deleted file mode 100644 index 6b7e409..0000000 --- a/src/seeds/005_workflow.js +++ /dev/null @@ -1,28 +0,0 @@ -import { map } from 'lodash'; - -import { fileExists, log, mapAsync, stripI18n } from '../helpers'; -import { Workflow } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/workflows`)) { - const profile = stripI18n(require(`${profilePath}/workflows`)); - if (profile.purge) { - await Workflow.delete(knex); - } - await Promise.all( - map( - profile.workflows, - async (workflow) => await Workflow.create(workflow, {}, knex), - ), - ); - log.info('Workflows imported'); - } - }); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/006_type.js b/src/seeds/006_type.js deleted file mode 100644 index 342a102..0000000 --- a/src/seeds/006_type.js +++ /dev/null @@ -1,52 +0,0 @@ -import { dropRight, map } from 'lodash'; -import { promises as fs } from 'fs'; - -import { dirExists, log, mapAsync, stripI18n } from '../helpers'; -import { Behavior, Type } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (dirExists(`${profilePath}/behaviors`)) { - // Get behavior profiles - const behaviors = map( - await fs.readdir(`${profilePath}/behaviors`), - (file) => dropRight(file.split('.')).join('.'), - ).sort(); - - // Import behaviors - await mapAsync(behaviors, async (behavior) => { - const data = stripI18n( - require(`${profilePath}/behaviors/${behavior}`), - ); - await Behavior.create(data, {}, knex); - }); - } - }); - - await mapAsync(config.profiles, async (profilePath) => { - if (dirExists(`${profilePath}/types`)) { - // Get type profiles - const types = map(await fs.readdir(`${profilePath}/types`), (file) => - dropRight(file.split('.')).join('.'), - ).sort(); - - // Import types - await mapAsync(types, async (type) => { - const data = stripI18n(require(`${profilePath}/types/${type}`)); - const typeModel = await Type.create({ - global_allow: true, - filter_content_types: false, - ...data, - }); - await typeModel.cacheSchema(); - }); - } - }); - log.info('Types imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/007_catalog.js b/src/seeds/007_catalog.js deleted file mode 100644 index d6eb397..0000000 --- a/src/seeds/007_catalog.js +++ /dev/null @@ -1,106 +0,0 @@ -import { map } from 'lodash'; - -import { fileExists, log, mapAsync, stripI18n } from '../helpers'; -import { Index } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/catalog`)) { - const profile = stripI18n(require(`${profilePath}/catalog`)); - await mapAsync(profile.indexes, async (index) => { - // Add index - if (index.enabled) { - await Index.create( - { - id: index.name, - title: index.title, - description: index.description, - group: index.group, - enabled: index.enabled, - sortable: index.sortable, - operators: index.operators, - vocabulary: index.vocabulary, - }, - {}, - knex, - ); - } - // Update catalog table - await knex.schema.alterTable('catalog', async (table) => { - const field = `_${index.name}`; - switch (index.type) { - case 'string': - table.string(field).index(); - break; - case 'integer': - table.integer(field).index(); - break; - case 'path': - table.string(field).index(); - break; - case 'uuid': - table.uuid(field).index(); - break; - case 'boolean': - table.boolean(field).index(); - break; - case 'date': - table.dateTime(field).index(); - break; - case 'string[]': - table - .specificType(field, 'character varying(255)[]') - .index(); - break; - case 'uuid[]': - table.specificType(field, 'uuid[]').index(); - break; - case 'text': - table.specificType(field, 'tsvector').index(null, 'GIN'); - break; - default: - log.warn(`Unhandled index type: ${index.type}`); - break; - } - }); - }); - await mapAsync(profile.metadata, async (metadata) => { - await knex.schema.alterTable('catalog', async (table) => { - switch (metadata.type) { - case 'uuid': - table.uuid(metadata.name); - break; - case 'string': - table.string(metadata.name); - break; - case 'date': - table.dateTime(metadata.name); - break; - case 'integer': - table.integer(metadata.name); - break; - case 'boolean': - table.boolean(metadata.name); - break; - case 'string[]': - table.specificType( - metadata.name, - 'character varying(255)[]', - ); - break; - default: - log.warn(`Unhandled index type: ${metadata.type}`); - break; - } - }); - }); - } - }); - log.info('Catalog imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/008_document.js b/src/seeds/008_document.js deleted file mode 100644 index 72be055..0000000 --- a/src/seeds/008_document.js +++ /dev/null @@ -1,185 +0,0 @@ -import { dropRight, endsWith, filter, last, map, omit } from 'lodash'; -import { promises as fs } from 'fs'; -import moment from 'moment'; -import { v4 as uuid } from 'uuid'; - -import { - dirExists, - handleFiles, - handleImages, - log, - mapAsync, - stripI18n, -} from '../helpers'; -import { Document, Type } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -const documentFields = [ - 'uuid', - 'parent', - 'id', - 'path', - 'created', - 'modified', - 'type', - 'position_in_parent', - 'version', - 'versions', - 'owner', - 'lock', - 'workflow_state', - 'workflow_history', - 'sharing', -]; - -const versionFields = ['uuid', 'version', 'id', 'created', 'actor', 'document']; - -export const seed = async (knex) => { - const trx = await knex.transaction(); - const uuids = []; - - try { - await mapAsync(config.profiles, async (profilePath) => { - if (dirExists(`${profilePath}/documents`)) { - const children = {}; - const files = map( - filter(await fs.readdir(`${profilePath}/documents`), (file) => - endsWith(file, '.json'), - ), - (file) => dropRight(file.split('.')).join('.'), - ).sort(); - await mapAsync(files, async (file) => { - let document = stripI18n( - require(`${profilePath}/documents/${file}`), - ); - const slugs = file.split('.'); - const id = last(slugs) === '_root' ? 'root' : last(slugs); - const path = last(slugs) === '_root' ? '/' : `/${slugs.join('/')}`; - const parent = - last(slugs) === '_root' - ? undefined - : ( - await Document.fetchOne( - { - path: `/${dropRight(slugs).join('/')}`, - }, - {}, - trx, - ) - ).uuid; - const position_in_parent = parent ? children[parent] || 0 : 0; - if (parent) { - children[parent] = parent in children ? children[parent] + 1 : 1; - } - - const versionCount = - 'versions' in document ? document.versions.length : 1; - - // Handle files and images - const type = await Type.fetchById(document.type || 'Page', {}, trx); - document = await handleFiles(document, type, profilePath); - document = await handleImages(document, type, profilePath); - - // Insert document - let insert = await Document.create( - { - uuid: document.uuid || uuid(), - version: - 'version' in document ? document.version : versionCount - 1, - id, - path, - parent, - position_in_parent: - document.position_in_parent || position_in_parent, - lock: document.lock || { - locked: false, - stealable: true, - }, - owner: document.owner || 'admin', - workflow_state: document.workflow_state || 'published', - workflow_history: JSON.stringify( - document.workflow_history || [], - ), - type: document.type || 'Page', - created: document.created || moment.utc().format(), - modified: document.modified || moment.utc().format(), - json: omit(document, documentFields), - }, - {}, - trx, - ); - - // Save uuid - uuids.push(insert.uuid); - - // Create versions - const versions = - 'versions' in document - ? map(document.versions, (version, index) => ({ - version: version.version || index, - created: version.created || moment.utc().format(), - actor: version.actor || 'admin', - id: version.id || id, - json: omit(version, versionFields), - })) - : [ - { - version: 0, - created: document.created || moment.utc().format(), - actor: document.owner || 'admin', - id, - json: omit(document, documentFields), - }, - ]; - - // Insert versions - await mapAsync(versions, async (version) => { - await insert.createRelated('_versions', version, trx); - }); - - // Insert sharing data for users - const sharingUsers = document.sharing?.users || []; - await mapAsync(sharingUsers, async (user) => - mapAsync( - user.roles, - async (role) => - await insert - .$relatedQuery('_userRoles', trx) - .relate({ id: role, user: user.id }), - ), - ); - - // Insert sharing data for groups - const sharingGroups = document.sharing?.groups || []; - await mapAsync(sharingGroups, async (group) => - mapAsync( - group.roles, - async (role) => - await insert - .$relatedQuery('_groupRoles', trx) - .relate({ id: role, group: group.id }), - ), - ); - }); - } - }); - - // Index documents - await mapAsync(uuids, async (uuid) => { - let document = await Document.fetchOne({ uuid }, {}, trx); - - // Apply behaviors - await document.applyBehaviors(trx); - - // Index object - await document.index(trx); - }); - - await trx.commit(); - log.info('Documents imported'); - } catch (err) { - await trx.rollback(); - log.error(err); - } -}; diff --git a/src/seeds/009_redirect.js b/src/seeds/009_redirect.js deleted file mode 100644 index 2dae589..0000000 --- a/src/seeds/009_redirect.js +++ /dev/null @@ -1,28 +0,0 @@ -import { map } from 'lodash'; - -import { fileExists, log, mapAsync, stripI18n } from '../helpers'; -import { Redirect } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/redirects`)) { - const profile = stripI18n(require(`${profilePath}/redirects`)); - if (profile.purge) { - await Redirect.delete(knex); - } - await Promise.all( - map( - profile.redirects, - async (redirects) => await Redirect.create(redirects, {}, knex), - ), - ); - } - }); - log.info('Redirects imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/010_action.js b/src/seeds/010_action.js deleted file mode 100644 index eb24e66..0000000 --- a/src/seeds/010_action.js +++ /dev/null @@ -1,38 +0,0 @@ -import { map } from 'lodash'; - -import { fileExists, log, mapAsync, stripI18n } from '../helpers'; -import { Action } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (fileExists(`${profilePath}/actions`)) { - const profile = stripI18n(require(`${profilePath}/actions`)); - if (profile.purge) { - await Action.delete(knex); - } - await mapAsync( - ['object', 'site_actions', 'object_buttons', 'user'], - async (category) => { - await mapAsync(profile[category], async (action, index) => { - await Action.create( - { - ...action, - category, - order: action.order || index, - }, - {}, - knex, - ); - }); - }, - ); - } - }); - log.info('Actions imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/011_controlpanel.js b/src/seeds/011_controlpanel.js deleted file mode 100644 index ccf664d..0000000 --- a/src/seeds/011_controlpanel.js +++ /dev/null @@ -1,44 +0,0 @@ -import { dropRight, map, merge } from 'lodash'; -import { promises as fs } from 'fs'; - -import { dirExists, log, mapAsync, stripI18n } from '../helpers'; -import { Controlpanel } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (dirExists(`${profilePath}/controlpanels`)) { - // Get controlpanel profiles - const controlpanels = map( - await fs.readdir(`${profilePath}/controlpanels`), - (file) => dropRight(file.split('.')).join('.'), - ).sort(); - - // Import controlpanels - await mapAsync(controlpanels, async (controlpanel) => { - const data = stripI18n( - require(`${profilePath}/controlpanels/${controlpanel}`), - ); - - // Check if controlpanel exists - const current = await Controlpanel.fetchById(data.id); - - // If doesn't exist - if (!current) { - await Controlpanel.create(data, {}, knex); - } else { - await Controlpanel.update( - data.id, - merge(current.$toDatabaseJson(), data), - knex, - ); - } - }); - } - }); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/012_vocabulary.js b/src/seeds/012_vocabulary.js deleted file mode 100644 index 44ef030..0000000 --- a/src/seeds/012_vocabulary.js +++ /dev/null @@ -1,32 +0,0 @@ -import { dropRight, map } from 'lodash'; -import { promises as fs } from 'fs'; - -import { dirExists, log, mapAsync, stripI18n } from '../helpers'; -import { Vocabulary } from '../models'; - -const { config } = require(`${process.cwd()}/config`); - -export const seed = async (knex) => { - try { - await mapAsync(config.profiles, async (profilePath) => { - if (dirExists(`${profilePath}/vocabularies`)) { - // Get vocabulary profiles - const vocabularies = map( - await fs.readdir(`${profilePath}/vocabularies`), - (file) => dropRight(file.split('.')).join('.'), - ).sort(); - - // Import vocabularies - await mapAsync(vocabularies, async (vocabulary) => { - const data = stripI18n( - require(`${profilePath}/vocabularies/${vocabulary}`), - ); - await Vocabulary.create(data, {}, knex); - }); - } - }); - log.info('Vocabularies imported'); - } catch (err) { - log.error(err); - } -}; diff --git a/src/seeds/action/action.js b/src/seeds/action/action.js new file mode 100644 index 0000000..b527b1e --- /dev/null +++ b/src/seeds/action/action.js @@ -0,0 +1,32 @@ +import { fileExists, mapAsync, stripI18n } from '../../helpers'; +import { Action } from '../../models'; + +export const seedAction = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/actions`)) { + const profile = stripI18n(require(`${profilePath}/actions`)); + if (profile.purge) { + await Action.delete(knex); + } + await mapAsync( + ['object', 'site_actions', 'object_buttons', 'user'], + async (category) => { + await mapAsync(profile[category], async (action, index) => { + await Action.create( + { + ...action, + category, + order: action.order || index, + }, + {}, + knex, + ); + }); + }, + ); + console.log('Actions imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/catalog/catalog.js b/src/seeds/catalog/catalog.js new file mode 100644 index 0000000..38f8527 --- /dev/null +++ b/src/seeds/catalog/catalog.js @@ -0,0 +1,95 @@ +import { fileExists, mapAsync, stripI18n } from '../../helpers'; +import { Index } from '../../models'; + +export const seedCatalog = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/catalog`)) { + const profile = stripI18n(require(`${profilePath}/catalog`)); + await mapAsync(profile.indexes, async (index) => { + // Add index + if (index.enabled) { + await Index.create( + { + id: index.name, + title: index.title, + description: index.description, + group: index.group, + enabled: index.enabled, + sortable: index.sortable, + operators: index.operators, + vocabulary: index.vocabulary, + }, + {}, + knex, + ); + } + // Update catalog table + await knex.schema.alterTable('catalog', async (table) => { + const field = `_${index.name}`; + switch (index.type) { + case 'string': + table.string(field).index(); + break; + case 'integer': + table.integer(field).index(); + break; + case 'path': + table.string(field).index(); + break; + case 'uuid': + table.uuid(field).index(); + break; + case 'boolean': + table.boolean(field).index(); + break; + case 'date': + table.dateTime(field).index(); + break; + case 'string[]': + table.specificType(field, 'character varying(255)[]').index(); + break; + case 'uuid[]': + table.specificType(field, 'uuid[]').index(); + break; + case 'text': + table.specificType(field, 'tsvector').index(null, 'GIN'); + break; + default: + console.log(`Unhandled index type: ${index.type}`); + break; + } + }); + }); + await mapAsync(profile.metadata, async (metadata) => { + await knex.schema.alterTable('catalog', async (table) => { + switch (metadata.type) { + case 'uuid': + table.uuid(metadata.name); + break; + case 'string': + table.string(metadata.name); + break; + case 'date': + table.dateTime(metadata.name); + break; + case 'integer': + table.integer(metadata.name); + break; + case 'boolean': + table.boolean(metadata.name); + break; + case 'string[]': + table.specificType(metadata.name, 'character varying(255)[]'); + break; + default: + console.log(`Unhandled index type: ${metadata.type}`); + break; + } + }); + }); + console.log('Catalog imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/controlpanel/controlpanel.js b/src/seeds/controlpanel/controlpanel.js new file mode 100644 index 0000000..78a6d38 --- /dev/null +++ b/src/seeds/controlpanel/controlpanel.js @@ -0,0 +1,41 @@ +import { dropRight, map, merge } from 'lodash'; +import { promises as fs } from 'fs'; + +import { dirExists, mapAsync, stripI18n } from '../../helpers'; +import { Controlpanel } from '../../models'; + +export const seedControlpanel = async (knex, profilePath) => { + try { + if (dirExists(`${profilePath}/controlpanels`)) { + // Get controlpanel profiles + const controlpanels = map( + await fs.readdir(`${profilePath}/controlpanels`), + (file) => dropRight(file.split('.')).join('.'), + ).sort(); + + // Import controlpanels + await mapAsync(controlpanels, async (controlpanel) => { + const data = stripI18n( + require(`${profilePath}/controlpanels/${controlpanel}`), + ); + + // Check if controlpanel exists + const current = await Controlpanel.fetchById(data.id); + + // If doesn't exist + if (!current) { + await Controlpanel.create(data, {}, knex); + } else { + await Controlpanel.update( + data.id, + merge(current.$toDatabaseJson(), data), + knex, + ); + } + }); + console.log('Controlpanels imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/document/document.js b/src/seeds/document/document.js new file mode 100644 index 0000000..e3016cb --- /dev/null +++ b/src/seeds/document/document.js @@ -0,0 +1,176 @@ +import { dropRight, endsWith, filter, last, map, omit } from 'lodash'; +import { promises as fs } from 'fs'; +import moment from 'moment'; +import { v4 as uuid } from 'uuid'; + +import { + dirExists, + handleFiles, + handleImages, + mapAsync, + stripI18n, +} from '../../helpers'; +import { Document, Type } from '../../models'; + +const documentFields = [ + 'uuid', + 'parent', + 'id', + 'path', + 'created', + 'modified', + 'type', + 'position_in_parent', + 'version', + 'versions', + 'owner', + 'lock', + 'workflow_state', + 'workflow_history', + 'sharing', +]; + +const versionFields = ['uuid', 'version', 'id', 'created', 'actor', 'document']; + +export const seedDocument = async (knex, profilePath) => { + const trx = await knex.transaction(); + const uuids = []; + + try { + if (dirExists(`${profilePath}/documents`)) { + const children = {}; + const files = map( + filter(await fs.readdir(`${profilePath}/documents`), (file) => + endsWith(file, '.json'), + ), + (file) => dropRight(file.split('.')).join('.'), + ).sort(); + await mapAsync(files, async (file) => { + let document = stripI18n(require(`${profilePath}/documents/${file}`)); + const slugs = file.split('.'); + const id = last(slugs) === '_root' ? 'root' : last(slugs); + const path = last(slugs) === '_root' ? '/' : `/${slugs.join('/')}`; + const parent = + last(slugs) === '_root' + ? undefined + : ( + await Document.fetchOne( + { + path: `/${dropRight(slugs).join('/')}`, + }, + {}, + trx, + ) + ).uuid; + const position_in_parent = parent ? children[parent] || 0 : 0; + if (parent) { + children[parent] = parent in children ? children[parent] + 1 : 1; + } + + const versionCount = + 'versions' in document ? document.versions.length : 1; + + // Handle files and images + const type = await Type.fetchById(document.type || 'Page', {}, trx); + document = await handleFiles(document, type, profilePath); + document = await handleImages(document, type, profilePath); + + // Insert document + let insert = await Document.create( + { + uuid: document.uuid || uuid(), + version: + 'version' in document ? document.version : versionCount - 1, + id, + path, + parent, + position_in_parent: + document.position_in_parent || position_in_parent, + lock: document.lock || { + locked: false, + stealable: true, + }, + owner: document.owner || 'admin', + workflow_state: document.workflow_state || 'published', + workflow_history: JSON.stringify(document.workflow_history || []), + type: document.type || 'Page', + created: document.created || moment.utc().format(), + modified: document.modified || moment.utc().format(), + json: omit(document, documentFields), + }, + {}, + trx, + ); + + // Save uuid + uuids.push(insert.uuid); + + // Create versions + const versions = + 'versions' in document + ? map(document.versions, (version, index) => ({ + version: version.version || index, + created: version.created || moment.utc().format(), + actor: version.actor || 'admin', + id: version.id || id, + json: omit(version, versionFields), + })) + : [ + { + version: 0, + created: document.created || moment.utc().format(), + actor: document.owner || 'admin', + id, + json: omit(document, documentFields), + }, + ]; + + // Insert versions + await mapAsync(versions, async (version) => { + await insert.createRelated('_versions', version, trx); + }); + + // Insert sharing data for users + const sharingUsers = document.sharing?.users || []; + await mapAsync(sharingUsers, async (user) => + mapAsync( + user.roles, + async (role) => + await insert + .$relatedQuery('_userRoles', trx) + .relate({ id: role, user: user.id }), + ), + ); + + // Insert sharing data for groups + const sharingGroups = document.sharing?.groups || []; + await mapAsync(sharingGroups, async (group) => + mapAsync( + group.roles, + async (role) => + await insert + .$relatedQuery('_groupRoles', trx) + .relate({ id: role, group: group.id }), + ), + ); + }); + + // Index documents + await mapAsync(uuids, async (uuid) => { + let document = await Document.fetchOne({ uuid }, {}, trx); + + // Apply behaviors + await document.applyBehaviors(trx); + + // Index object + await document.index(trx); + }); + + console.log('Documents imported'); + } + await trx.commit(); + } catch (err) { + await trx.rollback(); + console.log(err); + } +}; diff --git a/src/seeds/group/group.js b/src/seeds/group/group.js new file mode 100644 index 0000000..9eb5b24 --- /dev/null +++ b/src/seeds/group/group.js @@ -0,0 +1,30 @@ +import { map, omit } from 'lodash'; + +import { fileExists, stripI18n } from '../../helpers'; +import { Group } from '../../models'; + +export const seedGroup = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/groups`)) { + const profile = stripI18n(require(`${profilePath}/groups`)); + if (profile.purge) { + await Group.delete(knex); + } + await Promise.all( + map(profile.groups, async (group) => { + await Group.create( + { + ...omit(group, ['roles']), + _roles: group.roles, + }, + {}, + knex, + ); + }), + ); + console.log('Groups imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/index.js b/src/seeds/index.js new file mode 100644 index 0000000..fdd12c5 --- /dev/null +++ b/src/seeds/index.js @@ -0,0 +1,19 @@ +/** + * Point of contact for seeds. + * @module seeds + * @example import { seedProfile } from './seeds'; + */ + +export { seedProfile } from './profile/profile'; +export { seedPermission } from './permission/permission'; +export { seedRole } from './role/role'; +export { seedGroup } from './group/group'; +export { seedUser } from './user/user'; +export { seedWorkflow } from './workflow/workflow'; +export { seedType } from './type/type'; +export { seedCatalog } from './catalog/catalog'; +export { seedDocument } from './document/document'; +export { seedRedirect } from './redirect/redirect'; +export { seedAction } from './action/action'; +export { seedControlpanel } from './controlpanel/controlpanel'; +export { seedVocabulary } from './vocabulary/vocabulary'; diff --git a/src/seeds/permission/permission.js b/src/seeds/permission/permission.js new file mode 100644 index 0000000..42dd6d0 --- /dev/null +++ b/src/seeds/permission/permission.js @@ -0,0 +1,24 @@ +import { map } from 'lodash'; + +import { fileExists, stripI18n } from '../../helpers'; +import { Permission } from '../../models'; + +export const seedPermission = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/permissions`)) { + const profile = stripI18n(require(`${profilePath}/permissions`)); + if (profile.purge) { + await Permission.delete(knex); + } + await Promise.all( + map( + profile.permissions, + async (permission) => await Permission.create(permission, {}, knex), + ), + ); + console.log('Permissions imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/profile/profile.js b/src/seeds/profile/profile.js new file mode 100644 index 0000000..828875e --- /dev/null +++ b/src/seeds/profile/profile.js @@ -0,0 +1,15 @@ +import { fileExists, stripI18n } from '../../helpers'; +import { Profile } from '../../models'; + +export const seedProfile = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/metadata`)) { + const profile = stripI18n(require(`${profilePath}/metadata`)); + await Profile.deleteById(profile.id, knex); + await Profile.create(profile, {}, knex); + console.log('Profile imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/redirect/redirect.js b/src/seeds/redirect/redirect.js new file mode 100644 index 0000000..bc7d766 --- /dev/null +++ b/src/seeds/redirect/redirect.js @@ -0,0 +1,24 @@ +import { map } from 'lodash'; + +import { fileExists, stripI18n } from '../../helpers'; +import { Redirect } from '../../models'; + +export const seedRedirect = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/redirects`)) { + const profile = stripI18n(require(`${profilePath}/redirects`)); + if (profile.purge) { + await Redirect.delete(knex); + } + await Promise.all( + map( + profile.redirects, + async (redirects) => await Redirect.create(redirects, {}, knex), + ), + ); + console.log('Redirects imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/role/role.js b/src/seeds/role/role.js new file mode 100644 index 0000000..e50b97a --- /dev/null +++ b/src/seeds/role/role.js @@ -0,0 +1,29 @@ +import { omit } from 'lodash'; + +import { fileExists, mapAsync, stripI18n } from '../../helpers'; +import { Role } from '../../models'; + +export const seedRole = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/roles`)) { + const profile = stripI18n(require(`${profilePath}/roles`)); + if (profile.purge) { + await Role.delete(knex); + } + await mapAsync(profile.roles, async (role, index) => { + await Role.create( + { + ...omit(role, ['permissions']), + _permissions: role.permissions, + order: role.order || index, + }, + {}, + knex, + ); + }); + console.log('Roles imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/type/type.js b/src/seeds/type/type.js new file mode 100644 index 0000000..144f8f2 --- /dev/null +++ b/src/seeds/type/type.js @@ -0,0 +1,49 @@ +import { dropRight, map } from 'lodash'; +import { promises as fs } from 'fs'; + +import { dirExists, mapAsync, stripI18n } from '../../helpers'; +import { Behavior, Type } from '../../models'; + +export const seedType = async (knex, profilePath) => { + try { + if (dirExists(`${profilePath}/behaviors`)) { + // Get behavior profiles + const behaviors = map( + await fs.readdir(`${profilePath}/behaviors`), + (file) => dropRight(file.split('.')).join('.'), + ).sort(); + + // Import behaviors + await mapAsync(behaviors, async (behavior) => { + const data = stripI18n(require(`${profilePath}/behaviors/${behavior}`)); + await Behavior.create(data, {}, knex); + }); + console.log('Behaviors imported'); + } + + if (dirExists(`${profilePath}/types`)) { + // Get type profiles + const types = map(await fs.readdir(`${profilePath}/types`), (file) => + dropRight(file.split('.')).join('.'), + ).sort(); + + // Import types + await mapAsync(types, async (type) => { + const data = stripI18n(require(`${profilePath}/types/${type}`)); + const typeModel = await Type.create( + { + global_allow: true, + filter_content_types: false, + ...data, + }, + {}, + knex, + ); + await typeModel.cacheSchema(knex); + }); + console.log('Types imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/user/user.js b/src/seeds/user/user.js new file mode 100644 index 0000000..5b7f187 --- /dev/null +++ b/src/seeds/user/user.js @@ -0,0 +1,34 @@ +import { map, omit } from 'lodash'; +import bcrypt from 'bcrypt-promise'; + +import { fileExists, stripI18n } from '../../helpers'; +import { User } from '../../models'; + +export const seedUser = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/users`)) { + const profile = stripI18n(require(`${profilePath}/users`)); + if (profile.purge) { + await User.delete(knex); + } + await Promise.all( + map(profile.users, async (user) => { + // Insert user + await User.create( + { + ...omit(user, ['password', 'roles', 'groups']), + password: await bcrypt.hash(user.password, 10), + _roles: user.roles, + _groups: user.groups, + }, + {}, + knex, + ); + }), + ); + console.log('Users imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/vocabulary/vocabulary.js b/src/seeds/vocabulary/vocabulary.js new file mode 100644 index 0000000..d5d95a6 --- /dev/null +++ b/src/seeds/vocabulary/vocabulary.js @@ -0,0 +1,28 @@ +import { dropRight, map } from 'lodash'; +import { promises as fs } from 'fs'; + +import { dirExists, mapAsync, stripI18n } from '../../helpers'; +import { Vocabulary } from '../../models'; + +export const seedVocabulary = async (knex, profilePath) => { + try { + if (dirExists(`${profilePath}/vocabularies`)) { + // Get vocabulary profiles + const vocabularies = map( + await fs.readdir(`${profilePath}/vocabularies`), + (file) => dropRight(file.split('.')).join('.'), + ).sort(); + + // Import vocabularies + await mapAsync(vocabularies, async (vocabulary) => { + const data = stripI18n( + require(`${profilePath}/vocabularies/${vocabulary}`), + ); + await Vocabulary.create(data, {}, knex); + }); + console.log('Vocabularies imported'); + } + } catch (err) { + console.log(err); + } +}; diff --git a/src/seeds/workflow/workflow.js b/src/seeds/workflow/workflow.js new file mode 100644 index 0000000..2936511 --- /dev/null +++ b/src/seeds/workflow/workflow.js @@ -0,0 +1,24 @@ +import { map } from 'lodash'; + +import { fileExists, stripI18n } from '../../helpers'; +import { Workflow } from '../../models'; + +export const seedWorkflow = async (knex, profilePath) => { + try { + if (fileExists(`${profilePath}/workflows`)) { + const profile = stripI18n(require(`${profilePath}/workflows`)); + if (profile.purge) { + await Workflow.delete(knex); + } + await Promise.all( + map( + profile.workflows, + async (workflow) => await Workflow.create(workflow, {}, knex), + ), + ); + console.log('Workflows imported'); + } + } catch (err) { + console.log(err); + } +};