Skip to content

Commit e50e048

Browse files
authored
Merge pull request #47 from TimCsaky/metadata-database
Metadata and Tag database migration and models
2 parents 2643192 + 964cda8 commit e50e048

File tree

21 files changed

+552
-101
lines changed

21 files changed

+552
-101
lines changed

app/src/components/utils.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,17 @@ const utils = {
173173
else return '';
174174
},
175175

176+
/**
177+
* @function getKeyValue
178+
* Transforms array of {<key>:<value>} objects to {key: <key>, value: <value>}
179+
* @param {any}
180+
* @param {object} input of key value tuples like `<key>:<value>`
181+
* @returns {object[]} array of objects like `{key: <key>, value: <value>}`
182+
*/
183+
getKeyValue(input) {
184+
return Object.entries(input).map(([k, v]) => ({ key: k, value: v }));
185+
},
186+
176187
/**
177188
* @function mixedQueryToArray
178189
* Standardizes query params to yield an array of unique string values

app/src/controllers/object.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,11 @@ const controller = {
111111
id: objId,
112112
fieldName: name,
113113
mimeType: info.mimeType,
114-
originalName: info.filename,
115-
metadata: getMetadata(req.headers),
114+
metadata: {
115+
name: info.filename, // provide a default of `name: <file name>`
116+
...getMetadata(req.headers),
117+
id: objId
118+
}
116119
// TODO: Implement tag support - request shape TBD
117120
// tags: { foo: 'bar', baz: 'bam' }
118121
};
@@ -134,6 +137,7 @@ const controller = {
134137
// create version in DB
135138
const objectVersionArray = objects.map((object) => object.data);
136139
await versionService.create(objectVersionArray, userId);
140+
137141
// merge returned responses into a result
138142
const result = objects.map((object) => ({
139143
...object.data,
@@ -236,8 +240,7 @@ const controller = {
236240
id: objId,
237241
DeleteMarker: true,
238242
VersionId: s3Response.VersionId,
239-
mimeType: null,
240-
originalName: null
243+
mimeType: null
241244
};
242245
await versionService.create([deleteMarker], userId);
243246
}
@@ -408,7 +411,7 @@ const controller = {
408411
const objIds = mixedQueryToArray(req.query.objId);
409412
const params = {
410413
id: objIds ? objIds.map(id => addDashesToUuid(id)) : objIds,
411-
originalName: req.query.originalName,
414+
name: req.query.name,
412415
path: req.query.path,
413416
mimeType: req.query.mimeType,
414417
public: isTruthy(req.query.public),
@@ -472,8 +475,11 @@ const controller = {
472475
id: objId,
473476
fieldName: name,
474477
mimeType: info.mimeType,
475-
originalName: info.filename,
476-
metadata: getMetadata(req.headers)
478+
metadata: {
479+
name: info.filename,
480+
...getMetadata(req.headers),
481+
id: objId
482+
}
477483
// TODO: Implement tag support - request shape TBD
478484
// tags: { foo: 'bar', baz: 'bam' }
479485
};
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
const stamps = require('../stamps');
2+
3+
exports.up = function (knex) {
4+
return Promise.resolve()
5+
6+
// create metadata table
7+
.then(() => knex.schema.createTable('metadata', table => {
8+
table.specificType(
9+
'id',
10+
'integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY'
11+
);
12+
table.string('key', 255).index();
13+
table.string('value', 255).index();
14+
table.unique(['key', 'value']);
15+
}))
16+
17+
// create version_metadata table
18+
.then(() => knex.schema.createTable('version_metadata', table => {
19+
table.primary(['versionId', 'metadataId']);
20+
table.uuid('versionId').notNullable().references('id').inTable('version').onDelete('CASCADE').onUpdate('CASCADE');
21+
table.integer('metadataId').notNullable().references('id').inTable('metadata');
22+
stamps(knex, table);
23+
}))
24+
25+
// create tag table
26+
.then(() => knex.schema.createTable('tag', table => {
27+
table.specificType(
28+
'id',
29+
'integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY'
30+
);
31+
table.string('key', 255).index();
32+
table.string('value', 255).index();
33+
table.unique(['key', 'value']);
34+
}))
35+
36+
// create version_tag table
37+
.then(() => knex.schema.createTable('version_tag', table => {
38+
table.primary(['versionId', 'tagId']);
39+
table.uuid('versionId').notNullable().references('id').inTable('version').onDelete('CASCADE').onUpdate('CASCADE');
40+
table.integer('tagId').notNullable().references('id').inTable('tag');
41+
stamps(knex, table);
42+
}))
43+
44+
// Create metadata audit trigger
45+
.then(() => knex.schema.raw(`CREATE TRIGGER audit_version_metadata_trigger
46+
AFTER UPDATE OR DELETE ON version_metadata
47+
FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`))
48+
// Create tag audit trigger
49+
.then(() => knex.schema.raw(`CREATE TRIGGER audit_version_tag_trigger
50+
AFTER UPDATE OR DELETE ON version_tag
51+
FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`))
52+
53+
/**
54+
* for each version, move originalName and objectId to records in metadata table
55+
* and create joining version_metadata records
56+
*/
57+
.then(() => knex('version').where('deleteMarker', false))
58+
.then((rows) => {
59+
const versions = rows.map(v => ({
60+
value: v.originalName,
61+
versionId: v.id,
62+
objectId: v.objectId
63+
}));
64+
65+
return Promise.all(versions.map((row) => {
66+
// insert into metadata table
67+
knex('metadata').insert([
68+
{ key: 'name', value: row.value },
69+
{ key: 'id', value: row.objectId }
70+
]).onConflict(['key', 'value'])
71+
.merge()
72+
// Return just id column
73+
.returning('id')
74+
// add joining records
75+
.then((result) => {
76+
return knex('version_metadata').insert(result.map((metadata) => ({
77+
metadataId: metadata.id,
78+
versionId: row.versionId
79+
})));
80+
});
81+
}));
82+
})
83+
84+
// remove column originalName from version table
85+
.then(() => knex.schema.alterTable('version', table => {
86+
table.dropColumn('originalName');
87+
}));
88+
};
89+
90+
91+
exports.down = function (knex) {
92+
return Promise.resolve()
93+
94+
// re-add columns originalName version table
95+
.then(() => knex.schema.alterTable('version', table => {
96+
table.string('originalName', 255);
97+
}))
98+
99+
// move originalName back to version table
100+
.then(() => knex('version_metadata')
101+
.join('metadata', 'metadata.id', 'version_metadata.metadataId')
102+
.select('version_metadata.versionId', 'metadata.value')
103+
.where('metadata.key', 'name')
104+
)
105+
.then((versionMetadata) => {
106+
return Promise.all(versionMetadata.map((vm) => {
107+
return knex('version')
108+
.update({ originalName: vm.value })
109+
.where({ id: vm.versionId });
110+
}));
111+
})
112+
113+
// Drop audit triggers
114+
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_version_tag_trigger ON version_tag'))
115+
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_version_metadata_trigger ON version_metadata'))
116+
117+
// Drop tables
118+
.then(() => knex.schema.dropTableIfExists('version_tag'))
119+
.then(() => knex.schema.dropTableIfExists('tag'))
120+
.then(() => knex.schema.dropTableIfExists('version_metadata'))
121+
.then(() => knex.schema.dropTableIfExists('metadata'));
122+
};

app/src/db/models/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
const models = {
22
// Tables
33
IdentityProvider: require('./tables/identityProvider'),
4+
Metadata: require('./tables/metadata'),
45
ObjectModel: require('./tables/objectModel'),
56
ObjectPermission: require('./tables/objectPermission'),
67
Permission: require('./tables/permission'),
8+
Tag: require('./tables/tag'),
79
User: require('./tables/user'),
810
Version: require('./tables/version'),
11+
VersionMetadata: require('./tables/versionMetadata'),
12+
VersionTag: require('./tables/versionTag'),
913

1014
// Views
1115
};

app/src/db/models/tables/metadata.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const { Model } = require('objection');
2+
3+
class Metadata extends Model {
4+
static get tableName() {
5+
return 'metadata';
6+
}
7+
8+
static get relationMappings() {
9+
const Version = require('./version');
10+
11+
return {
12+
version: {
13+
relation: Model.ManyToManyRelation,
14+
modelClass: Version,
15+
join: {
16+
from: 'metadata.id',
17+
through: {
18+
from: 'version_metadata.metadataId',
19+
to: 'version_metadata.versionId'
20+
},
21+
to: 'version.id'
22+
}
23+
}
24+
};
25+
}
26+
27+
static get jsonSchema() {
28+
return {
29+
type: 'object',
30+
required: ['key', 'value'],
31+
properties: {
32+
id: { type: 'integer' },
33+
key: { type: 'string', minLength: 1, maxLength: 255 },
34+
value: { type: 'string', minLength: 1, maxLength: 255 }
35+
},
36+
additionalProperties: false
37+
};
38+
}
39+
}
40+
41+
module.exports = Metadata;

app/src/db/models/tables/objectModel.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,18 @@ class ObjectModel extends Timestamps(Model) {
6060
},
6161
filterMimeType(query, value) {
6262
if (value) {
63-
const subquery = Version.query()
63+
query.whereIn('id', Version.query()
6464
.distinct('objectId')
65-
.where('mimeType', 'ilike', `%${value}%`);
66-
query.whereIn('id', subquery);
65+
.where('mimeType', 'ilike', `%${value}%`));
6766
}
6867
},
69-
filterOriginalName(query, value) {
68+
filterName(query, value) {
7069
if (value) {
71-
const subquery = Version.query()
72-
.distinct('objectId')
73-
.where('originalName', 'ilike', `%${value}%`);
74-
query.whereIn('id', subquery);
70+
query.whereIn('id', Version.query()
71+
.distinct('version.objectId')
72+
.joinRelated('metadata')
73+
.where('value', 'ilike', `%${value}%`)
74+
.andWhere('key', 'name'));
7575
}
7676
}
7777
// TODO: consider chaining Version modifiers in a way that they are combined. Example:

app/src/db/models/tables/tag.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const { Model } = require('objection');
2+
3+
class Tag extends Model {
4+
static get tableName() {
5+
return 'tag';
6+
}
7+
8+
static get relationMappings() {
9+
const Version = require('./version');
10+
11+
return {
12+
version: {
13+
relation: Model.ManyToManyRelation,
14+
modelClass: Version,
15+
join: {
16+
from: 'tag.id',
17+
through: {
18+
from: 'version_tag.tagId',
19+
to: 'version_tag.versionId'
20+
},
21+
to: 'version.id'
22+
}
23+
}
24+
};
25+
}
26+
27+
static get jsonSchema() {
28+
return {
29+
type: 'object',
30+
required: ['key', 'value'],
31+
properties: {
32+
id: { type: 'integer' },
33+
key: { type: 'string', minLength: 1, maxLength: 255 },
34+
value: { type: 'string', minLength: 1, maxLength: 255 }
35+
},
36+
additionalProperties: false
37+
};
38+
}
39+
}
40+
41+
module.exports = Tag;

app/src/db/models/tables/version.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class Version extends Timestamps(Model) {
1111

1212
static get relationMappings() {
1313
const ObjectModel = require('./objectModel');
14+
const Metadata = require('./metadata');
15+
const Tag = require('./tag');
1416

1517
return {
1618
object: {
@@ -21,6 +23,32 @@ class Version extends Timestamps(Model) {
2123
to: 'object.id'
2224
}
2325
},
26+
27+
metadata: {
28+
relation: Model.ManyToManyRelation,
29+
modelClass: Metadata,
30+
join: {
31+
from: 'version.id',
32+
through: {
33+
from: 'version_metadata.versionId',
34+
to: 'version_metadata.metadataId'
35+
},
36+
to: 'metadata.id'
37+
}
38+
},
39+
40+
tag: {
41+
relation: Model.ManyToManyRelation,
42+
modelClass: Tag,
43+
join: {
44+
from: 'version.id',
45+
through: {
46+
from: 'version_tag.versionId',
47+
to: 'version_tag.tagId'
48+
},
49+
to: 'tag.id'
50+
}
51+
}
2452
};
2553
}
2654

@@ -29,9 +57,6 @@ class Version extends Timestamps(Model) {
2957
filterObjectId(query, value) {
3058
filterOneOrMany(query, value, 'objectId');
3159
},
32-
filterOriginalName(query, value) {
33-
filterILike(query, value, 'originalName');
34-
},
3560
filterMimeType(query, value) {
3661
filterILike(query, value, 'mimeType');
3762
}
@@ -41,12 +66,11 @@ class Version extends Timestamps(Model) {
4166
static get jsonSchema() {
4267
return {
4368
type: 'object',
44-
required: ['objectId', 'originalName', 'mimeType'],
69+
required: ['objectId', 'mimeType'],
4570
properties: {
4671
id: { type: 'string', minLength: 1, maxLength: 255 },
4772
versionId: { type: ['string', 'null'], maxLength: 1024 },
4873
objectId:{ type: 'string', minLength: 1, maxLength: 255 },
49-
originalName: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
5074
mimeType: { type: ['string', 'null'], minLength: 1, maxLength: 255 },
5175
deleteMarker: { type: 'boolean' },
5276
...stamps

0 commit comments

Comments
 (0)