@wmfs/tymly-pg-plugin
Advanced tools
Comparing version 1.224.0 to 1.225.0
@@ -5,15 +5,15 @@ 'use strict' | ||
module.exports = function generateTriggerStatement (options) { | ||
const namespace = _.snakeCase(options.model.namespace) | ||
const name = _.snakeCase(options.model.name) | ||
const pk = options.model.primaryKey.map(_.snakeCase).join(',') | ||
function generateTriggerStatement (model, triggerName, func, action) { | ||
const namespace = _.snakeCase(model.namespace) | ||
const name = _.snakeCase(model.name) | ||
const pk = model.primaryKey.map(_.snakeCase).join(',') | ||
switch (options.action) { | ||
switch (action) { | ||
case 'ADD': | ||
return `CREATE TRIGGER ${namespace}_${name}_auditor | ||
BEFORE UPDATE ON ${namespace}.${name} | ||
return `CREATE TRIGGER ${triggerName} | ||
${func.when} ON ${namespace}.${name} | ||
FOR EACH ROW EXECUTE PROCEDURE | ||
tymly.${_.snakeCase(options.function)}('${namespace}.${name}', '{${pk}}');` | ||
tymly.${_.snakeCase(func.name)}('${namespace}.${name}', '{${pk}}');` | ||
case 'REMOVE': | ||
return `DROP TRIGGER IF EXISTS ${namespace}_${name}_auditor | ||
return `DROP TRIGGER IF EXISTS ${triggerName} | ||
ON ${namespace}.${name};` | ||
@@ -24,1 +24,3 @@ default: | ||
} | ||
module.exports = generateTriggerStatement |
@@ -16,37 +16,28 @@ 'use strict' | ||
this.auditLog = options.bootedServices.storage.models.tymly_rewind | ||
this.schemaNames = options.bootedServices.storage.schemaNames | ||
const pgScripts = options.blueprintComponents.pgScripts || {} | ||
this.auditFunctions = Object.keys(pgScripts) | ||
.map(script => path.parse(pgScripts[script].filename).name) | ||
.filter(filename => filename.split('-')[0] === 'audit') | ||
.map(filename => { | ||
debug(`Found audit function: ${filename.substring(filename.indexOf('-') + 1)}`) | ||
return filename.substring(filename.indexOf('-') + 1) | ||
}) | ||
const auditFunctions = gatherAuditFunctions(pgScripts) | ||
const schemaNames = options.bootedServices.storage.schemaNames | ||
await this.updateTriggers(options.messages) | ||
await this.updateTriggers(auditFunctions, schemaNames, options.messages) | ||
} // boot | ||
async updateTriggers (messages) { | ||
async updateTriggers (auditFunctions, schemaNames, messages) { | ||
const currentDbStructure = await pgInfo({ | ||
client: this.client, | ||
schemas: this.schemaNames | ||
schemas: schemaNames | ||
}) | ||
const allInstallers = this.auditFunctions | ||
.map(func => { | ||
messages.info(`Applying ${func} function`) | ||
const installers = Object.keys(this.models) | ||
.map(model => this.installTrigger(func, model, currentDbStructure, messages)) | ||
.filter(p => !!p) | ||
for (const func of auditFunctions) { | ||
messages.info(`Applying ${func.name} function`) | ||
const installers = Object.keys(this.models) | ||
.map(model => this.installTrigger(func, model, currentDbStructure, messages)) | ||
.filter(p => !!p) | ||
if (installers.length === 0) { | ||
messages.detail('Already in place') | ||
return | ||
} | ||
return Promise.all(installers) | ||
}) | ||
return Promise.all(allInstallers) | ||
if (installers.length) { | ||
await Promise.all(installers) | ||
} else { | ||
messages.detail('Already in place') | ||
} | ||
} | ||
} // updateTriggers | ||
@@ -59,3 +50,3 @@ | ||
const name = _.snakeCase(this.models[model].name) | ||
const triggerName = `${namespace}_${name}_auditor` | ||
const triggerName = `${namespace}_${name}_auditor${func.triggerSuffix}` | ||
@@ -68,7 +59,8 @@ const modelTriggers = currentDbStructure.schemas[namespace].tables[name].triggers | ||
const triggerSQL = generateTriggerStatement({ | ||
model: this.models[model], | ||
function: func, | ||
action: action | ||
}) | ||
const triggerSQL = generateTriggerStatement( | ||
this.models[model], | ||
triggerName, | ||
func, | ||
action | ||
) | ||
@@ -108,4 +100,39 @@ if (!triggerSQL) { | ||
function gatherAuditFunctions (pgScripts) { | ||
return Object.keys(pgScripts) | ||
.map(script => path.parse(pgScripts[script].filename).name) | ||
.filter(filename => filename.split('-')[0] === 'audit') | ||
.map(filename => { | ||
const functionName = filename.substring(filename.indexOf('-') + 1) | ||
debug(`Found audit function: ${functionName}`) | ||
return auditFunctionProperties(functionName) | ||
}) | ||
} // gatherAuditFunctions | ||
const whenTrigger = { | ||
insert: 'AFTER INSERT', | ||
update: 'BEFORE UPDATE', | ||
delete: 'BEFORE DELETE' | ||
} | ||
const triggerSuffix = { | ||
insert: '_insert', | ||
update: '', | ||
delete: '_delete' | ||
} | ||
function auditFunctionProperties (functionName) { | ||
const type = functionName.split('-')[0] | ||
return { | ||
name: functionName, | ||
triggerSuffix: triggerSuffix[type], | ||
when: whenTrigger[type] | ||
} | ||
} // auditFunctionProperties | ||
function formatLog (log, model, additionalFields) { | ||
const diffs = formatDiffs(log.diff, model) | ||
const action = (typeof log.diff.action === 'string') | ||
const diffs = action ? formatAction(log.diff.action, log.oldValues, model) : formatDiffs(log.diff, model) | ||
const when = formatDate(log.modified) | ||
@@ -124,2 +151,13 @@ | ||
function formatAction (action, record, model) { | ||
const actionText = _.capitalize(action) | ||
if (!model.label) return [actionText] | ||
const labelFields = Array.isArray(model.label) ? model.label : [model.label] | ||
return labelFields.map((field, index) => { | ||
const label = record[field] | ||
return (index === 0) ? `${actionText} ${label}` : label | ||
}).filter(l => l) | ||
} | ||
function formatDiffs (diffs, model) { | ||
@@ -126,0 +164,0 @@ return Object.entries(diffs) |
@@ -16,2 +16,4 @@ /** | ||
this.multicopy = resourceConfig.multicopy || false | ||
this.schemaName = resourceConfig.schemaName || false | ||
this.quote = resourceConfig.quote || null | ||
} | ||
@@ -45,4 +47,5 @@ | ||
client: this.client, | ||
schemaName: context.stateMachineMeta.schemaName, | ||
schemaName: this.schemaName || context.stateMachineMeta.schemaName, | ||
truncateTables: this.truncateTables, | ||
quote: this.quote, | ||
debug: true | ||
@@ -49,0 +52,0 @@ }) |
{ | ||
"name": "@wmfs/tymly-pg-plugin", | ||
"version": "1.224.0", | ||
"version": "1.225.0", | ||
"description": "Replace Tymly's out-the-box memory storage with PostgreSQL", | ||
@@ -33,5 +33,5 @@ "author": "West Midlands Fire Service", | ||
"@wmfs/pg-model": "1.24.0", | ||
"@wmfs/pg-telepods": "1.78.0", | ||
"@wmfs/pg-telepods": "1.79.0", | ||
"@wmfs/relationize": "1.24.0", | ||
"@wmfs/supercopy": "1.38.0" | ||
"@wmfs/supercopy": "1.39.0" | ||
}, | ||
@@ -43,4 +43,5 @@ "devDependencies": { | ||
"conventional-changelog-metahub": "4.0.1", | ||
"cz-conventional-changelog": "3.2.0", | ||
"mocha": "8.1.1", | ||
"cz-conventional-changelog": "3.3.0", | ||
"dirty-chai": "2.0.1", | ||
"mocha": "8.1.3", | ||
"nyc": "15.1.0", | ||
@@ -53,3 +54,3 @@ "rimraf": "3.0.2", | ||
"@semantic-release/exec": "5.0.0", | ||
"@wmfs/tymly": "1.157.0" | ||
"@wmfs/tymly": "1.160.0" | ||
}, | ||
@@ -56,0 +57,0 @@ "scripts": { |
{ | ||
"name": "pg", | ||
"version": "1.224.0", | ||
"version": "1.225.0", | ||
"label": "PG", | ||
@@ -5,0 +5,0 @@ "author": "Tim Needham", |
@@ -5,3 +5,5 @@ /* eslint-env mocha */ | ||
const expect = require('chai').expect | ||
const chai = require('chai') | ||
chai.use(require('dirty-chai')) | ||
const expect = chai.expect | ||
const tymly = require('@wmfs/tymly') | ||
@@ -15,3 +17,3 @@ const path = require('path') | ||
let tymlyService, models, rewindIdToDestroy, client | ||
let tymlyService, models, client | ||
@@ -25,4 +27,4 @@ before(function () { | ||
it('create some tymly services', (done) => { | ||
tymly.boot( | ||
before('create some tymly services', async () => { | ||
const tymlyServices = await tymly.boot( | ||
{ | ||
@@ -36,108 +38,173 @@ pluginPaths: [ | ||
config: {} | ||
}, | ||
(err, tymlyServices) => { | ||
expect(err).to.eql(null) | ||
tymlyService = tymlyServices.tymly | ||
client = tymlyServices.storage.client | ||
models = tymlyServices.storage.models | ||
done(err) | ||
} | ||
) | ||
tymlyService = tymlyServices.tymly | ||
client = tymlyServices.storage.client | ||
models = tymlyServices.storage.models | ||
}) | ||
it('insert a dog to animal-with-age', async () => { | ||
await models.tymlyTest_animalWithAge.create({ | ||
animal: 'dog', | ||
colour: 'brown' | ||
describe('Audited table', () => { | ||
function dogChanges () { | ||
return models.tymly_rewind.find({ | ||
where: { | ||
modelName: { equals: 'tymly_test.animal_with_age' } | ||
}, | ||
orderBy: ['-modified'] | ||
}) | ||
} | ||
async function dog () { | ||
const res = await models.tymlyTest_animalWithAge.find({}) | ||
return res.length ? res[0] : null | ||
} | ||
describe('insert', () => { | ||
it('insert a dog to animal-with-age', async () => { | ||
await models.tymlyTest_animalWithAge.create({ | ||
animal: 'dog', | ||
colour: 'brown' | ||
}) | ||
}) | ||
it('row in table', async () => { | ||
const res = await dog() | ||
expect(res.colour).to.eql('brown') | ||
}) | ||
it('insert captured in tymly.rewind', async () => { | ||
const res = await dogChanges() | ||
expect(res.length).to.eql(1) | ||
expect(res[0].modelName).to.eql('tymly_test.animal_with_age') | ||
expect(res[0].keyString).to.eql('dog') | ||
expect(res[0].diff.action).to.eql('insert') | ||
}) | ||
}) | ||
}) | ||
it('check the dog is brown', async () => { | ||
const res = await models.tymlyTest_animalWithAge.find({}) | ||
describe('update record', () => { | ||
it('update the dog\'s colour to black', async () => { | ||
await models.tymlyTest_animalWithAge.update({ | ||
animal: 'dog', | ||
colour: 'black' | ||
}, {}) | ||
}) | ||
expect(res[0].colour).to.eql('brown') | ||
}) | ||
it('change committed', async () => { | ||
const res = await dog() | ||
it('update the dog\'s colour to black', async () => { | ||
await models.tymlyTest_animalWithAge.update({ | ||
animal: 'dog', | ||
colour: 'black' | ||
}, {}) | ||
}) | ||
expect(res.colour).to.eql('black') | ||
}) | ||
it('confirm dog is black', async () => { | ||
const res = await models.tymlyTest_animalWithAge.find({}) | ||
it('update captured in tymly.rewind', async () => { | ||
const res = await dogChanges() | ||
expect(res[0].colour).to.eql('black') | ||
}) | ||
expect(res.length).to.eql(2) | ||
expect(res[0].modelName).to.eql('tymly_test.animal_with_age') | ||
expect(res[0].keyString).to.eql('dog') | ||
expect(res[0].diff.colour.from).to.eql('brown') | ||
expect(res[0].diff.colour.to).to.eql('black') | ||
}) | ||
}) | ||
it('check the change has been documented in tymly.rewind', async () => { | ||
const res = await models.tymly_rewind.find({ | ||
where: { | ||
modelName: { equals: 'tymly_test.animal_with_age' } | ||
} | ||
describe('update again', () => { | ||
it('update the dog\'s colour to piebald', async () => { | ||
await models.tymlyTest_animalWithAge.update({ | ||
animal: 'dog', | ||
colour: 'piebald' | ||
}, {}) | ||
}) | ||
it('confirm row changed again', async () => { | ||
const res = await dog() | ||
expect(res.colour).to.eql('piebald') | ||
}) | ||
it('second change captured in tymly.rewind', async () => { | ||
const res = await dogChanges() | ||
expect(res.length).to.eql(3) | ||
expect(res[0].modelName).to.eql('tymly_test.animal_with_age') | ||
expect(res[0].keyString).to.eql('dog') | ||
expect(res[0].diff.colour.from).to.eql('black') | ||
expect(res[0].diff.colour.to).to.eql('piebald') | ||
}) | ||
}) | ||
rewindIdToDestroy = res[0].id | ||
expect(res[0].modelName).to.eql('tymly_test.animal_with_age') | ||
expect(res[0].keyString).to.eql('dog') | ||
expect(res[0].diff.colour.from).to.eql('brown') | ||
expect(res[0].diff.colour.to).to.eql('black') | ||
}) | ||
describe('delete record', () => { | ||
it('delete row', async () => { | ||
await models.tymlyTest_animalWithAge.destroyById('dog') | ||
}) | ||
it('insert a cat to animal-with-year', async () => { | ||
await models.tymlyTest_animalWithYear.create({ | ||
animal: 'cat', | ||
colour: 'ginger' | ||
it('row is gone', async () => { | ||
const res = await dog() | ||
expect(res).to.be.null() | ||
}) | ||
it('delete is captured in tymly.rewind', async () => { | ||
const res = await dogChanges() | ||
expect(res.length).to.eql(4) | ||
expect(res[0].modelName).to.eql('tymly_test.animal_with_age') | ||
expect(res[0].keyString).to.eql('dog') | ||
expect(res[0].diff.action).to.eql('delete') | ||
}) | ||
}) | ||
}) | ||
it('check the cat is ginger', async () => { | ||
const res = await models.tymlyTest_animalWithYear.find({}) | ||
describe('Unaudited table', () => { | ||
it('insert a cat to animal-with-year', async () => { | ||
await models.tymlyTest_animalWithYear.create({ | ||
animal: 'cat', | ||
colour: 'ginger' | ||
}) | ||
}) | ||
expect(res[0].colour).to.eql('ginger') | ||
}) | ||
it('check the cat is ginger', async () => { | ||
const res = await models.tymlyTest_animalWithYear.find({}) | ||
it('update the cat update the cat\'s colour to white', async () => { | ||
await models.tymlyTest_animalWithYear.update({ | ||
animal: 'cat', | ||
colour: 'white' | ||
}, {}) | ||
}) | ||
expect(res[0].colour).to.eql('ginger') | ||
}) | ||
it('check the cat is white', async () => { | ||
const res = await models.tymlyTest_animalWithYear.find({}) | ||
it('update the cat update the cat\'s colour to white', async () => { | ||
await models.tymlyTest_animalWithYear.update({ | ||
animal: 'cat', | ||
colour: 'white' | ||
}, {}) | ||
}) | ||
expect(res[0].colour).to.eql('white') | ||
}) | ||
it('check the cat is white', async () => { | ||
const res = await models.tymlyTest_animalWithYear.find({}) | ||
it('check the change has NOT been documented in tymly.rewind', async () => { | ||
const res = await models.tymly_rewind.find({ | ||
where: { | ||
modelName: { equals: 'tymly_test.animal_with_year' } | ||
} | ||
expect(res[0].colour).to.eql('white') | ||
}) | ||
expect(res.length).to.eql(0) | ||
}) | ||
it('check the change has NOT been documented in tymly.rewind', async () => { | ||
const res = await models.tymly_rewind.find({ | ||
where: { | ||
modelName: { equals: 'tymly_test.animal_with_year' } | ||
} | ||
}) | ||
it('clean up animal-with-age', async () => { | ||
await models.tymlyTest_animalWithAge.destroyById('dog') | ||
expect(res.length).to.eql(0) | ||
}) | ||
}) | ||
it('clean up animal-with-year', async () => { | ||
after('clean up animal-with-year', async () => { | ||
await models.tymlyTest_animalWithYear.destroyById('cat') | ||
}) | ||
it('clean up rewind', async () => { | ||
await models.tymly_rewind.destroyById(rewindIdToDestroy) | ||
after('clean up rewind', async () => { | ||
await client.query("delete from tymly.rewind where model_name = 'tymly_test.animal_with_age'") | ||
}) | ||
it('uninstall test schemas', async () => { | ||
after('uninstall test schemas', async () => { | ||
await sqlScriptRunner.uninstall(client) | ||
}) | ||
it('shutdown Tymly', async () => { | ||
after('shutdown Tymly', async () => { | ||
await tymlyService.shutdown() | ||
}) | ||
}) |
@@ -19,2 +19,23 @@ /* eslint-env mocha */ | ||
diff: { | ||
action: 'insert' | ||
}, | ||
modifiedBy: 'bob', | ||
modified: DateTime.fromObject({ | ||
year: 2018, | ||
month: 12, | ||
day: 1, | ||
hour: 10, | ||
minute: 0 | ||
}).toJSDate() | ||
}, | ||
{ | ||
modelName: 'test_animal', | ||
keyString: 'dog_henry', | ||
oldValues: { | ||
name: 'henry', | ||
colour: null, | ||
size: 'small', | ||
animal: 'dog' | ||
}, | ||
diff: { | ||
colour: { | ||
@@ -85,2 +106,23 @@ from: '', | ||
}).toJSDate() | ||
}, | ||
{ | ||
modelName: 'test_animal', | ||
keyString: 'dog_henry', | ||
oldValues: { | ||
name: 'henry', | ||
colour: null, | ||
size: 'small', | ||
animal: 'dog' | ||
}, | ||
diff: { | ||
action: 'delete' | ||
}, | ||
modifiedBy: 'bob', | ||
modified: DateTime.fromObject({ | ||
year: 2028, | ||
month: 12, | ||
day: 1, | ||
hour: 10, | ||
minute: 0 | ||
}).toJSDate() | ||
} | ||
@@ -91,2 +133,7 @@ ] | ||
{ | ||
change: 'Delete henry', | ||
modifiedBy: 'bob', | ||
modified: '10:00 Dec 1, 2028' | ||
}, | ||
{ | ||
change: 'Size "small" was cleared', | ||
@@ -105,2 +152,7 @@ modifiedBy: 'bill', | ||
modified: '11:00 Dec 1, 2018' | ||
}, | ||
{ | ||
change: 'Insert henry', | ||
modifiedBy: 'bob', | ||
modified: '10:00 Dec 1, 2018' | ||
} | ||
@@ -144,3 +196,4 @@ ] | ||
} | ||
} | ||
}, | ||
label: 'name' | ||
} | ||
@@ -147,0 +200,0 @@ }, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
227202
121
3511
15
+ Added@wmfs/pg-telepods@1.79.0(transitive)
+ Added@wmfs/supercopy@1.39.0(transitive)
- Removed@wmfs/pg-telepods@1.78.0(transitive)
- Removed@wmfs/supercopy@1.38.0(transitive)
Updated@wmfs/pg-telepods@1.79.0
Updated@wmfs/supercopy@1.39.0