From ddedeb879984c6f8241ba8a0a7cbf1366f50a149 Mon Sep 17 00:00:00 2001 From: Stephen Cresswell <229672+cressie176@users.noreply.github.com> Date: Sun, 14 Jan 2024 13:19:45 +0000 Subject: [PATCH] Drop Entities --- .eslintrc.json | 1 + index.js | 4 +- lib/helpers.js | 5 + lib/marv-filby-driver.js | 1 + lib/partials/add-entities.hbs | 2 +- lib/partials/drop-entities.hbs | 11 +++ lib/schema.json | 26 +++++ lib/template.hbs | 1 + ...create-fby-projection-entity-relations.sql | 2 +- .../007.create-fby-data-frame-relations.sql | 2 +- test/database-schema.test.js | 27 +++++ test/dsl.test.js | 99 +++++++++++++++++++ 12 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 lib/partials/drop-entities.hbs diff --git a/.eslintrc.json b/.eslintrc.json index e2f7ad3..056cf86 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,6 +17,7 @@ "max-classes-per-file": 0, "max-len": 0, "no-await-in-loop": 0, + "no-console": "error", "no-dynamic-require": 0, "no-empty-function": 0, "no-plusplus": 0, diff --git a/index.js b/index.js index 2058bc1..26476f9 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ const { Pool } = require('pg'); const parseDuration = require('parse-duration'); const driver = require('./lib/marv-filby-driver'); -const { tableName } = require('./lib/helpers'); +const { aggregateFunctionName } = require('./lib/helpers'); module.exports = class Filby extends EventEmitter { @@ -104,7 +104,7 @@ LIMIT 1`, [projection.id]); async getAggregates(changeSetId, name, version) { return this.withTransaction(async (tx) => { - const functionName = `get_${tableName(name, version)}_aggregate`; + const functionName = aggregateFunctionName(name, version); const { rowCount: exists } = await tx.query('SELECT 1 FROM pg_proc WHERE proname = $1', [functionName]); if (!exists) throw new Error(`Function '${functionName}' does not exist`); diff --git a/lib/helpers.js b/lib/helpers.js index bac5838..fe5917b 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -9,6 +9,10 @@ function tableName(name, version) { return `${name.toLowerCase().replace(/\s/g, '_')}_v${version}`; } +function aggregateFunctionName(name, version) { + return `get_${tableName(name, version)}_aggregate`; +} + function loadCsv(path, options) { const { rows, errors } = parseCsv(path); if (errors[0]) throw new Error(`Error parsing ${path}:${errors[0].row + 2} - ${errors[0].message}`); @@ -53,6 +57,7 @@ function toString(result, item, index, items) { module.exports = { and, tableName, + aggregateFunctionName, loadCsv, xkeys, xvalues, diff --git a/lib/marv-filby-driver.js b/lib/marv-filby-driver.js index 78321ac..18e2a5e 100644 --- a/lib/marv-filby-driver.js +++ b/lib/marv-filby-driver.js @@ -18,6 +18,7 @@ const partials = { addHooks: fs.readFileSync(path.join(__dirname, 'partials', 'add-hooks.hbs'), 'utf-8'), addChangeSets: fs.readFileSync(path.join(__dirname, 'partials', 'add-change-sets.hbs'), 'utf-8'), dropEnums: fs.readFileSync(path.join(__dirname, 'partials', 'drop-enums.hbs'), 'utf-8'), + dropEntities: fs.readFileSync(path.join(__dirname, 'partials', 'drop-entities.hbs'), 'utf-8'), dropProjections: fs.readFileSync(path.join(__dirname, 'partials', 'drop-projections.hbs'), 'utf-8'), dropHooks: fs.readFileSync(path.join(__dirname, 'partials', 'drop-hooks.hbs'), 'utf-8'), }; diff --git a/lib/partials/add-entities.hbs b/lib/partials/add-entities.hbs index 750c02d..cc35c97 100644 --- a/lib/partials/add-entities.hbs +++ b/lib/partials/add-entities.hbs @@ -13,7 +13,7 @@ CREATE TABLE {{tableName name version}} ( {{/each}} ); -CREATE FUNCTION get_{{tableName name version}}_aggregate(p_change_set_id INTEGER) RETURNS TABLE ( +CREATE FUNCTION {{aggregateFunctionName name version}}(p_change_set_id INTEGER) RETURNS TABLE ( {{#fields}} {{name}} {{type}}{{#unless @last}},{{/unless}} {{/fields}} diff --git a/lib/partials/drop-entities.hbs b/lib/partials/drop-entities.hbs new file mode 100644 index 0000000..07fce1e --- /dev/null +++ b/lib/partials/drop-entities.hbs @@ -0,0 +1,11 @@ +{{#if entities}}-- Drop Entities{{/if}} +{{#entities}} +DELETE FROM fby_entity +WHERE name = '{{name}}' + AND version = {{version}}; + +DROP TABLE {{tableName name version}}; + +DROP FUNCTION {{aggregateFunctionName name version}}; + +{{/entities}} \ No newline at end of file diff --git a/lib/schema.json b/lib/schema.json index 3ee88ef..1b6904f 100644 --- a/lib/schema.json +++ b/lib/schema.json @@ -45,6 +45,12 @@ "drop_hooks": { "$ref": "#/definitions/dropHooksType" }, + "drop entities": { + "$ref": "#/definitions/dropEntitiesType" + }, + "drop_entities": { + "$ref": "#/definitions/dropEntitiesType" + }, "drop enums": { "$ref": "#/definitions/dropEnumsType" }, @@ -376,6 +382,26 @@ } } }, + "dropEntitiesType": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "name", + "version" + ] + } + }, "dropEnumsType": { "type": "array", "minItems": 1, diff --git a/lib/template.hbs b/lib/template.hbs index 86cfa9b..35eec5b 100644 --- a/lib/template.hbs +++ b/lib/template.hbs @@ -7,6 +7,7 @@ START TRANSACTION; {{> addChangeSets changeSets=add_change_sets}} {{> dropProjections projections=drop_projections}} {{> dropHooks hooks=drop_hooks}} +{{> dropEntities entities=drop_entities}} {{> dropEnums enums=drop_enums}} END TRANSACTION; \ No newline at end of file diff --git a/migrations/003.create-fby-projection-entity-relations.sql b/migrations/003.create-fby-projection-entity-relations.sql index 695a5b9..1433934 100644 --- a/migrations/003.create-fby-projection-entity-relations.sql +++ b/migrations/003.create-fby-projection-entity-relations.sql @@ -2,7 +2,7 @@ START TRANSACTION; CREATE TABLE fby_projection_entity ( projection_id INTEGER REFERENCES fby_projection (id) ON DELETE CASCADE NOT NULL, - entity_id INTEGER REFERENCES fby_entity (id) ON DELETE CASCADE NOT NULL, + entity_id INTEGER REFERENCES fby_entity (id) NOT NULL, PRIMARY KEY (projection_id, entity_id) ); diff --git a/migrations/007.create-fby-data-frame-relations.sql b/migrations/007.create-fby-data-frame-relations.sql index 0361c27..44895f9 100644 --- a/migrations/007.create-fby-data-frame-relations.sql +++ b/migrations/007.create-fby-data-frame-relations.sql @@ -5,7 +5,7 @@ CREATE TYPE fby_action_type AS ENUM ('POST', 'DELETE'); CREATE TABLE fby_data_frame ( id SERIAL PRIMARY KEY, change_set_id INTEGER REFERENCES fby_change_set (id) NOT NULL, - entity_id INTEGER REFERENCES fby_entity (id) NOT NULL, + entity_id INTEGER REFERENCES fby_entity (id) ON DELETE CASCADE NOT NULL, action fby_action_type NOT NULL ); diff --git a/test/database-schema.test.js b/test/database-schema.test.js index 07aae33..45e4806 100644 --- a/test/database-schema.test.js +++ b/test/database-schema.test.js @@ -91,6 +91,33 @@ describe('Database Schema', () => { }); }); + describe('Entities', () => { + + it('should prevent deletion when there are dependent projections', async () => { + await rejects(() => filby.withTransaction(async (tx) => { + await tx.query("INSERT INTO fby_projection (id, name, version) VALUES (1, 'Parks', 1)"); + await tx.query("INSERT INTO fby_entity (id, name, version) VALUES (1, 'Park', 1)"); + await tx.query('INSERT INTO fby_projection_entity (projection_id, entity_id) VALUES (1, 1)'); + await tx.query('DELETE FROM fby_entity'); + }), (err) => { + eq(err.code, '23503'); + return true; + }); + }); + + it('should cascade deletes to data frames', async () => { + await filby.withTransaction(async (tx) => { + await tx.query("INSERT INTO fby_entity (id, name, version) VALUES (1, 'Park', 1)"); + await tx.query('INSERT INTO fby_change_set (id, effective) VALUES (1, now())'); + await tx.query("INSERT INTO fby_data_frame (id, change_set_id, entity_id, action) VALUES (1, 1, 1, 'POST')"); + await tx.query('DELETE FROM fby_entity'); + }); + + const { rows: frames } = await filby.withTransaction((tx) => tx.query('SELECT * from fby_data_frame')); + eq(frames.length, 0); + }); + }); + describe('Hooks', () => { it('should prevent duplicate projection hooks', async () => { diff --git a/test/dsl.test.js b/test/dsl.test.js index cf8a51d..fcb2da5 100644 --- a/test/dsl.test.js +++ b/test/dsl.test.js @@ -3,6 +3,7 @@ const path = require('node:path'); const { ok, strictEqual: eq, deepEqual: deq, rejects, match } = require('node:assert'); const { describe, it, before, beforeEach, after, afterEach } = require('zunit'); +const { tableName, aggregateFunctionName } = require('../lib/helpers'); const TestFilby = require('./TestFilby'); const config = { @@ -584,6 +585,104 @@ describe('DSL', () => { }); }); + describe('Drop Entities', () => { + it('should drop entities', async (t) => { + await applyYaml(t.name, ` + add entities: + - name: VAT Rate + version: 1 + fields: + - name: type + type: TEXT + - name: rate + type: NUMERIC + identified by: + - type + drop entities: + - name: VAT Rate + version: 1 + `); + + const { rows: entities } = await filby.withTransaction((tx) => tx.query('SELECT * FROM fby_entity')); + eq(entities.length, 0); + + const { rows: tables } = await filby.withTransaction((tx) => tx.query('SELECT * FROM information_schema.tables WHERE table_name = $1', [tableName('vat_rate', 1)])); + eq(tables.length, 0); + + const { rows: functions } = await filby.withTransaction((tx) => tx.query('SELECT * FROM pg_proc WHERE proname = $1', [aggregateFunctionName('vat_rate', 1)])); + eq(functions.length, 0); + }); + + it('should ignore other entities', async (t) => { + await applyYaml(t.name, ` + add entities: + - name: VAT Rate + version: 1 + fields: + - name: type + type: TEXT + - name: rate + type: NUMERIC + identified by: + - type + - name: VAT Rate + version: 2 + fields: + - name: type + type: TEXT + - name: rate + type: NUMERIC + identified by: + - type + + drop entities: + - name: VAT Rate + version: 1 + `); + + const { rows: entities } = await filby.withTransaction((tx) => tx.query('SELECT * FROM fby_entity')); + eq(entities.length, 1); + + const { rows: tables } = await filby.withTransaction((tx) => tx.query('SELECT * FROM information_schema.tables WHERE table_name = $1', [tableName('vat_rate', 2)])); + eq(tables.length, 1); + + const { rows: functions } = await filby.withTransaction((tx) => tx.query('SELECT * FROM pg_proc WHERE proname = $1', [aggregateFunctionName('vat_rate', 2)])); + eq(functions.length, 1); + }); + + it('should require a name', async (t) => { + await rejects(() => applyYaml(t.name, ` + drop entities: + - version: 1 + `), (err) => { + match(err.message, new RegExp("^001.should-require-a-name.yaml: /drop_entities/0 must have required property 'name'$")); + return true; + }); + }); + + it('should require a version', async (t) => { + await rejects(() => applyYaml(t.name, ` + drop entities: + - name: VAT Rate + `), (err) => { + match(err.message, new RegExp("^001.should-require-a-version.yaml: /drop_entities/0 must have required property 'version'$")); + return true; + }); + }); + + it('should forbid additional properties', async (t) => { + await rejects(() => applyYaml(t.name, ` + drop entities: + - name: VAT Rate + version: 1 + wombat: Freddy + `), (err) => { + match(err.message, new RegExp('^001.should-forbid-additional-properties.yaml: /drop_entities/0 must NOT have additional properties$')); + return true; + }); + }); + }); + describe('Add Change Sets', () => { it('should require an effective date', async (t) => { await rejects(() => applyYaml(t.name, `