Skip to content

Commit 4c06294

Browse files
authored
Merge pull request #4749 from cloud-gov/feat-add-last-modified-attributes-to-file-query
feat: Add last modified attributes to file queries
2 parents 468cd78 + e35764c commit 4c06294

12 files changed

+525
-222
lines changed

api/controllers/file-storage-service.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ module.exports = wrapHandlers({
4949
const client = await siteStorageService.createClient();
5050
const fss = await client.createDirectory(parent, name);
5151

52-
const data = serializeFileStorageFile(fss);
52+
const data = serializeFileStorageFile(fss, { includeLastModified: false });
5353
return res.send(data);
5454
},
5555

@@ -162,7 +162,7 @@ module.exports = wrapHandlers({
162162
originalname,
163163
});
164164

165-
const data = serializeFileStorageFile(fss);
165+
const data = serializeFileStorageFile(fss, { includeLastModified: false });
166166
return res.send(data);
167167
},
168168
});

api/models/file-storage-file.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
function associate({ FileStorageFile, FileStorageService, FileStorageUserAction }) {
1+
function associate({
2+
FileStorageFile,
3+
FileStorageService,
4+
FileStorageUserAction,
5+
UAAIdentity,
6+
User,
7+
}) {
28
FileStorageFile.belongsTo(FileStorageService, {
39
foreignKey: 'fileStorageServiceId',
410
});
@@ -15,6 +21,23 @@ function associate({ FileStorageFile, FileStorageService, FileStorageUserAction
1521
},
1622
],
1723
}));
24+
25+
FileStorageFile.addScope('withLastAction', {
26+
include: {
27+
model: FileStorageUserAction,
28+
limit: 1,
29+
required: true,
30+
order: [['createdAt', 'DESC']],
31+
include: {
32+
model: User,
33+
required: true,
34+
include: {
35+
model: UAAIdentity,
36+
required: true,
37+
},
38+
},
39+
},
40+
});
1841
}
1942

2043
function define(sequelize, DataTypes) {

api/models/file-storage-user-action.js

+11-5
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,17 @@ function associate({
3434
});
3535

3636
FileStorageUserAction.addScope('withUserIdentity', {
37-
include: {
38-
model: User,
39-
required: true,
40-
include: { model: UAAIdentity, required: true },
41-
},
37+
include: [
38+
{
39+
model: User,
40+
required: true,
41+
include: { model: UAAIdentity, required: true },
42+
},
43+
{
44+
model: FileStorageFile,
45+
required: true,
46+
},
47+
],
4248
});
4349
}
4450

api/serializers/file-storage.js

+44-4
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,35 @@ const allowedFileStorageFileFields = [
99
'key',
1010
'type',
1111
'metadata',
12+
'lastModifiedAt',
13+
'lastModifiedBy',
14+
'lastModifiedByUserId',
1215
...dateFields,
1316
];
1417

15-
const serializeFileStorageFile = (serializable) => {
18+
const serializeFileStorageFile = (serializable, { includeLastModified = true } = {}) => {
1619
if (!serializable) return {};
1720

18-
return pick(allowedFileStorageFileFields, serializable.dataValues);
21+
let lastModified = {};
22+
23+
if (includeLastModified) {
24+
const { FileStorageUserActions } = serializable;
25+
26+
const {
27+
createdAt: lastModifiedAt,
28+
User: {
29+
id: lastModifiedByUserId,
30+
UAAIdentity: { email: lastModifiedBy },
31+
},
32+
} = FileStorageUserActions[0];
33+
34+
lastModified = { lastModifiedAt, lastModifiedByUserId, lastModifiedBy };
35+
}
36+
37+
return pick(allowedFileStorageFileFields, {
38+
...serializable.dataValues,
39+
...lastModified,
40+
});
1941
};
2042

2143
const serializeFileStorageFiles = (list) => {
@@ -43,15 +65,33 @@ const allowedFileStorageUserActionFields = [
4365
'userId',
4466
'createdAt',
4567
'email',
68+
'fileKey',
69+
'fileName',
70+
'fileMetadata',
71+
'fileType',
4672
];
4773

4874
const serializeFileStorageUserAction = (serializable) => {
49-
const { User, ...rest } = serializable.dataValues;
75+
const { User, FileStorageFile, ...rest } = serializable.dataValues;
5076

5177
const { UAAIdentity } = User.dataValues;
5278
const { email } = UAAIdentity.dataValues;
5379

54-
return pick(allowedFileStorageUserActionFields, { ...rest, email });
80+
const {
81+
key: fileKey,
82+
name: fileName,
83+
metadata: fileMetadata,
84+
type: fileType,
85+
} = FileStorageFile;
86+
87+
return pick(allowedFileStorageUserActionFields, {
88+
...rest,
89+
email,
90+
fileKey,
91+
fileName,
92+
fileMetadata,
93+
fileType,
94+
});
5595
};
5696

5797
const serializeFileStorageUserActions = (list) => {

api/services/file-storage/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ class SiteFileStorageSerivce {
184184
}
185185

186186
async getFile(id) {
187-
const record = await FileStorageFile.findOne({
187+
const record = await FileStorageFile.scope(['withLastAction']).findOne({
188188
where: { id, fileStorageServiceId: this.id },
189189
});
190190

@@ -222,7 +222,7 @@ class SiteFileStorageSerivce {
222222
const key = this.#buildKeyPath(directory);
223223

224224
const results = await paginate(
225-
FileStorageFile,
225+
FileStorageFile.scope(['withLastAction']),
226226
serializeFileStorageFiles,
227227
{
228228
limit,

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@tanstack/react-query": "^5.59.20",
1616
"@uswds/uswds": "^3.8.1",
1717
"ajv": "^8.12.0",
18-
"axios": "^1.7.5",
18+
"axios": "1.8.3",
1919
"bullmq": "^5.7.0",
2020
"cfenv": "^1.2.3",
2121
"connect-flash": "^0.1.1",
@@ -171,8 +171,8 @@
171171
"eslint-plugin-svelte": "^2.46.1",
172172
"eslint-plugin-testing-library": "^6.4.0",
173173
"fetch-mock": "^12.1.0",
174-
"jest": "^29.6.2",
175-
"jest-environment-jsdom": "^29.6.2",
174+
"jest": "29.7.0",
175+
"jest-environment-jsdom": "29.7.0",
176176
"json-schema-deref-sync": "^0.14.0",
177177
"jsonschema": "^1.4.0",
178178
"lodash.clonedeep": "^4.5.0",

test/api/requests/file-storage-service.test.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ describe('File Storgage API', () => {
1515
beforeEach(async () => {
1616
sinon.stub(EventCreator, 'error').resolves();
1717
await factory.organization.truncate();
18+
await factory.fileStorageFile.truncate();
1819
});
1920

2021
afterEach(async () => {
2122
sinon.restore();
2223
await factory.organization.truncate();
24+
await factory.fileStorageFile.truncate();
2325
});
2426

2527
describe('GET /v0/file-storage/:file_storage_id', () => {
@@ -532,7 +534,7 @@ describe('File Storgage API', () => {
532534
validateAgainstJSONSchema('GET', endpoint, 200, body);
533535
});
534536

535-
it('returns a list of items in the directory path', async () => {
537+
it('returns a list of user actions', async () => {
536538
const { site, org, user } = await stubSiteS3({ roleName: 'manager' });
537539
const fss = await factory.fileStorageService.create({
538540
siteId: site.id,
@@ -678,7 +680,7 @@ describe('File Storgage API', () => {
678680
validateAgainstJSONSchema('GET', endpoint, 200, body);
679681
});
680682

681-
it('returns a list of items in the directory path', async () => {
683+
it('returns a list of user actions for a file', async () => {
682684
const { site, org, user } = await stubSiteS3({ roleName: 'manager' });
683685
const fss = await factory.fileStorageService.create({
684686
siteId: site.id,

test/api/support/factory/file-storage-file.js

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const path = require('node:path');
22
const fileStorageService = require('./file-storage-service');
3-
const { FileStorageFile } = require('../../../../api/models');
3+
const userWithIdentity = require('./user-with-uaa-identity');
4+
const { FileStorageFile, FileStorageUserAction } = require('../../../../api/models');
5+
const { getRandItem } = require('./utils');
46

57
const counters = {};
68

@@ -17,6 +19,7 @@ async function build(params = {}) {
1719
fileStorageServiceId = null,
1820
description = null,
1921
metadata = null,
22+
createFileUserAction = true,
2023
} = params;
2124

2225
if (!name) {
@@ -36,25 +39,41 @@ async function build(params = {}) {
3639
fileStorageServiceId = fss.id;
3740
}
3841

39-
return FileStorageFile.create({
42+
const file = await FileStorageFile.create({
4043
name,
4144
key,
4245
type,
4346
fileStorageServiceId,
4447
description,
4548
metadata,
4649
});
50+
51+
const { user } = await userWithIdentity.create();
52+
53+
if (createFileUserAction) {
54+
await FileStorageUserAction.create({
55+
userId: user.id,
56+
fileStorageServiceId,
57+
fileStorageFileId: file.id,
58+
method: getRandItem(FileStorageUserAction.METHODS),
59+
description: getRandItem(FileStorageUserAction.ACTION_TYPES),
60+
});
61+
}
62+
63+
return file;
4764
}
4865

4966
function create(params) {
5067
return build(params);
5168
}
5269

53-
function truncate() {
54-
return FileStorageFile.truncate({
70+
async function truncate() {
71+
await FileStorageFile.truncate({
5572
force: true,
5673
cascade: true,
5774
});
75+
76+
await userWithIdentity.truncate();
5877
}
5978

6079
async function createBulk(
@@ -64,7 +83,6 @@ async function createBulk(
6483
) {
6584
let totalFiles = [];
6685
let totalDiectories = [];
67-
6886
if (files > 0) {
6987
const fileList = new Array(files).fill(0);
7088

test/api/support/factory/file-storage-user-action.js

+5-9
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,7 @@ const fileStorageService = require('./file-storage-service');
33
const user = require('./user');
44
const uaaIdentity = require('./uaa-identity');
55
const { FileStorageUserAction } = require('../../../../api/models');
6-
7-
function getRandItem(kv) {
8-
const list = Object.keys(kv);
9-
const randomIndex = Math.floor(Math.random() * list.length);
10-
const key = list[randomIndex];
11-
12-
return kv[key];
13-
}
6+
const { getRandItem } = require('./utils');
147

158
async function build(params = {}) {
169
let {
@@ -78,7 +71,10 @@ async function createBulk(
7871
}
7972

8073
async function createBulkRandom({ fileStorageServiceId }, actions = 1) {
81-
const fsf = await fileStorageFile.create({ fileStorageServiceId });
74+
const fsf = await fileStorageFile.create({
75+
fileStorageServiceId,
76+
createFileUserAction: false,
77+
});
8278
const u = await user();
8379
await uaaIdentity.createUAAIdentity({ userId: u.id });
8480

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const { User } = require('../../../../api/models');
2+
const uaaIdentity = require('./uaa-identity');
3+
const user = require('./user');
4+
5+
const counters = {};
6+
7+
function increment(key = 'email') {
8+
counters[key] = (counters[key] || 0) + 1;
9+
return `user.${counters[key]}@example.gov`;
10+
}
11+
12+
async function build(params = {}) {
13+
let { email } = params;
14+
15+
if (!email) {
16+
email = increment();
17+
}
18+
19+
const createdUser = await user({ email });
20+
const createdUAAId = await uaaIdentity.createUAAIdentity({
21+
userId: createdUser.id,
22+
email,
23+
});
24+
25+
return {
26+
user: createdUser,
27+
uaaIdentity: createdUAAId,
28+
};
29+
}
30+
31+
function create(params) {
32+
return build(params);
33+
}
34+
35+
function truncate() {
36+
return User.truncate({
37+
force: true,
38+
cascade: true,
39+
});
40+
}
41+
42+
module.exports = {
43+
build,
44+
create,
45+
truncate,
46+
};

test/api/support/factory/utils.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
function getRandItem(kv) {
2+
const list = Object.keys(kv);
3+
const randomIndex = Math.floor(Math.random() * list.length);
4+
const key = list[randomIndex];
5+
6+
return kv[key];
7+
}
8+
9+
module.exports = {
10+
getRandItem,
11+
};

0 commit comments

Comments
 (0)