|
1 | 1 | const { NIL: SYSTEM_USER } = require('uuid');
|
2 | 2 | const { Metadata, VersionMetadata } = require('../db/models');
|
3 |
| -const { getKeyValue } = require('../components/utils'); |
| 3 | +const { getObjectsByKeyValue } = require('../components/utils'); |
4 | 4 |
|
5 | 5 | /**
|
6 | 6 | * The Metadata DB Service
|
7 | 7 | */
|
8 | 8 | const service = {
|
9 | 9 |
|
| 10 | + |
10 | 11 | /**
|
11 |
| - * @function updateMetadata |
12 |
| - * Updates metadata and relates them to the associated version |
13 |
| - * Un-relates any existing metadata for this version |
| 12 | + * @function associateMetadata |
| 13 | + * Makes the incoming list of metadata the definitive set associated with versionId |
| 14 | + * Dissociaate extraneous metadata and also does collision detection for null versions (non-versioned) |
14 | 15 | * @param {string} versionId The uuid id column from version table
|
15 |
| - * @param {object} metadata Incoming object with `<key>:<value>` metadata to add for this version |
| 16 | + * @param {object[]} metadata Incoming array of metadata objects to add for this version (eg: [{ key: 'a', value: '1'}, {key: 'B', value: '2'}]). |
| 17 | + * This will always be the difinitive metadata we want on the version |
16 | 18 | * @param {string} [currentUserId=SYSTEM_USER] The optional userId uuid actor; defaults to system user if unspecified
|
17 | 19 | * @param {object} [etrx=undefined] An optional Objection Transaction object
|
18 | 20 | * @returns {Promise<object>} The result of running the insert operation
|
19 | 21 | * @throws The error encountered upon db transaction failure
|
20 | 22 | */
|
21 |
| - updateMetadata: async (versionId, metadata, currentUserId = SYSTEM_USER, etrx = undefined) => { |
| 23 | + associateMetadata: async (versionId, metadata, currentUserId = SYSTEM_USER, etrx = undefined) => { |
22 | 24 | let trx;
|
23 | 25 | try {
|
24 | 26 | trx = etrx ? etrx : await Metadata.startTransaction();
|
25 | 27 | let response = [];
|
26 | 28 |
|
27 |
| - // convert metadata to array for DB insert query |
28 |
| - const arr = getKeyValue(metadata); |
29 |
| - if (arr.length) { |
30 |
| - // insert/merge metadata records |
31 |
| - const insertMetadata = await Metadata.query(trx) |
32 |
| - .insert(arr) |
33 |
| - .onConflict(['key', 'value']) |
34 |
| - .merge(); // required to include id's of existing rows in result |
35 |
| - |
36 |
| - // un-relate all existing version_metadata |
37 |
| - // this only happens when updating the single 'null version' record in db, when using a non-versioned bucket |
38 |
| - // otherwise joining records remain in db for previous versions |
39 |
| - await VersionMetadata.query(trx) |
40 |
| - .delete() |
41 |
| - .where('versionId', versionId); |
42 |
| - |
43 |
| - // add new version_metadata records |
44 |
| - const relateArray = insertMetadata.map(({id}) => ({ |
45 |
| - versionId: versionId, |
46 |
| - metadataId: id, |
47 |
| - createdBy: currentUserId |
48 |
| - })); |
49 |
| - response = await VersionMetadata.query(trx) |
50 |
| - .insert(relateArray); |
| 29 | + if (metadata && metadata.length) { |
| 30 | + // get DB records of all input metadata |
| 31 | + const dbMetadata = await service.createMetadata(metadata, trx); |
| 32 | + |
| 33 | + // already joined |
| 34 | + const associatedMetadata = await VersionMetadata.query(trx) |
| 35 | + .modify('filterVersionId', versionId); |
| 36 | + |
| 37 | + // remove existing joins for metadata that is not in incomming set |
| 38 | + if (associatedMetadata.length) { |
| 39 | + const dissociateMetadata = associatedMetadata.filter(({ metadataId }) => !dbMetadata.some(({ id }) => id === metadataId)); |
| 40 | + if (dissociateMetadata.length) { |
| 41 | + await VersionMetadata.query(trx) |
| 42 | + .whereIn('id', dissociateMetadata.map(meta => meta.id)) |
| 43 | + .modify('filterVersionId', versionId) |
| 44 | + .delete(); |
| 45 | + |
| 46 | + // delete all orphaned metadata records |
| 47 | + await service.pruneOrphanedMetadata(trx); |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + // join new metadata |
| 52 | + const newJoins = associatedMetadata.length ? dbMetadata.filter(({ id }) => !associatedMetadata.some(({ metadataId }) => metadataId === id)) : dbMetadata; |
| 53 | + |
| 54 | + if (newJoins.length) { |
| 55 | + response = await VersionMetadata.query(trx) |
| 56 | + .insert(newJoins.map(({ id }) => ({ |
| 57 | + versionId: versionId, |
| 58 | + metadataId: id, |
| 59 | + createdBy: currentUserId |
| 60 | + }))); |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + if (!etrx) await trx.commit(); |
| 65 | + return Promise.resolve(response); |
| 66 | + } catch (err) { |
| 67 | + if (!etrx && trx) await trx.rollback(); |
| 68 | + throw err; |
| 69 | + } |
| 70 | + }, |
| 71 | + |
| 72 | + /** |
| 73 | + * @function deleteOrphanedMetadata |
| 74 | + * deletes Metadata records if they are no longer related to any versions |
| 75 | + * @param {object} [etrx=undefined] An optional Objection Transaction object |
| 76 | + * @returns {Promise<number>} The result of running the delete operation (number of rows deleted) |
| 77 | + * @throws The error encountered upon db transaction failure |
| 78 | + */ |
| 79 | + // TODO: check if deleteing a version will prune orphan metadata records (sister table) |
| 80 | + pruneOrphanedMetadata: async (etrx = undefined) => { |
| 81 | + let trx; |
| 82 | + try { |
| 83 | + trx = etrx ? etrx : await Metadata.startTransaction(); |
| 84 | + |
| 85 | + const deletedMetadataIds = await Metadata.query(trx) |
| 86 | + .allowGraph('versionMetadata') |
| 87 | + .withGraphJoined('versionMetadata') |
| 88 | + .select('metadata.id') |
| 89 | + .whereNull('versionMetadata.metadataId'); |
| 90 | + |
| 91 | + const response = await Metadata.query(trx) |
| 92 | + .delete() |
| 93 | + .whereIn('id', deletedMetadataIds.map(({ id }) => id)); |
| 94 | + |
| 95 | + if (!etrx) await trx.commit(); |
| 96 | + return Promise.resolve(response); |
| 97 | + } catch (err) { |
| 98 | + if (!etrx && trx) await trx.rollback(); |
| 99 | + throw err; |
| 100 | + } |
| 101 | + }, |
| 102 | + |
| 103 | + /** |
| 104 | + * @function createMetadata |
| 105 | + * Inserts any metadata records if they dont already exist in db |
| 106 | + * @param {object} metadata Incoming object with `<key>:<value>` metadata to add for this version |
| 107 | + * @param {object} [etrx=undefined] An optional Objection Transaction object |
| 108 | + * @returns {Promise<object>} an array of all input metadata |
| 109 | + * @throws The error encountered upon db transaction failure |
| 110 | + */ |
| 111 | + createMetadata: async (metadata, etrx = undefined) => { |
| 112 | + let trx; |
| 113 | + let response = []; |
| 114 | + try { |
| 115 | + trx = etrx ? etrx : await Metadata.startTransaction(); |
| 116 | + |
| 117 | + // get all metadata already in db |
| 118 | + const allMetadata = await Metadata.query(trx).select(); |
| 119 | + const existingMetadata = []; |
| 120 | + const newMetadata = []; |
| 121 | + |
| 122 | + metadata.forEach(({ key, value }) => { |
| 123 | + // if metadata is already in db |
| 124 | + if (getObjectsByKeyValue(allMetadata, key, value)) { |
| 125 | + // add metadata object to existingMetadata array |
| 126 | + existingMetadata.push({ id: getObjectsByKeyValue(allMetadata, key, value).id, key: key, value: value }); |
| 127 | + } |
| 128 | + // else add to array for inserting |
| 129 | + else { |
| 130 | + newMetadata.push({ key: key, value: value }); |
| 131 | + } |
| 132 | + }); |
| 133 | + // insert new metadata |
| 134 | + if (newMetadata.length) { |
| 135 | + const newMetadataRecords = await Metadata.query(trx) |
| 136 | + .insert(newMetadata) |
| 137 | + .returning('*'); |
| 138 | + // merge new with existing metadata |
| 139 | + response = existingMetadata.concat(newMetadataRecords); |
| 140 | + } |
| 141 | + else { |
| 142 | + response = existingMetadata; |
51 | 143 | }
|
52 | 144 |
|
53 | 145 | if (!etrx) await trx.commit();
|
|
0 commit comments