Skip to content

Commit

Permalink
Merge pull request #44 from PermanentOrg/per-9278
Browse files Browse the repository at this point in the history
Add GET /archive/:archiveId/tags/public endpoint
  • Loading branch information
liam-lloyd authored Jun 28, 2023
2 parents e77423a + 7056cad commit efdf616
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 2 deletions.
21 changes: 21 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,3 +247,24 @@ instead return a [Joi ValidationError object](https://joi.dev/api/?v=17.9.1#vali
token: string
}
```

## Archives

### GET `/archive/:archiveId/tags/public`

- Response
```
[
{
tagId: string,
name: string,
archiveId: string,
status: string,
type: string,
createdDt: string (date),
updatedDt: string (date)
}
]
```

- Note: If the archive doesn't exist, this will return an empty array
2 changes: 1 addition & 1 deletion src/account/validators.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { validateUpdateTagsRequest } from "./validators";

describe("validateUpdateTagsRequest", () => {
test("should find not errors in a valid request", () => {
test("should find no errors in a valid request", () => {
let error = null;
try {
validateUpdateTagsRequest({
Expand Down
23 changes: 23 additions & 0 deletions src/archive/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Router } from "express";
import type { Request, Response, NextFunction } from "express";
import { validateGetPublicTagsParams } from "./validators";
import { isValidationError } from "../validators/validator_util";
import { archiveService } from "./service";

export const archiveController = Router();
archiveController.get(
"/:archiveId/tags/public",
async (req: Request, res: Response, next: NextFunction) => {
try {
validateGetPublicTagsParams(req.params);
const tags = await archiveService.getPublicTags(req.params.archiveId);
res.json(tags);
} catch (err) {
if (isValidationError(err)) {
res.status(400).json({ error: err });
return;
}
next(err);
}
}
);
1 change: 1 addition & 0 deletions src/archive/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { archiveController } from "./controller";
9 changes: 9 additions & 0 deletions src/archive/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface Tag {
tagId: string;
name: string;
archiveId: string;
status: string;
type: string;
createdDt: Date;
updatedDt: Date;
}
29 changes: 29 additions & 0 deletions src/archive/queries/get_public_tags.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
SELECT DISTINCT
tag.tagId "tagId",
tag.name,
tag.archiveId "archiveId",
tag.status,
tag.type,
tag.createdDt "createdDt",
tag.updatedDt "updatedDt"
FROM
tag
JOIN
tag_link
ON tag.tagId = tag_link.tagId
LEFT JOIN
record
ON tag_link.refId = record.recordId AND tag_link.refTable = 'record'
LEFT JOIN
folder
ON tag_link.refId = folder.folderId AND tag_link.refTable = 'folder'
WHERE
tag.archiveId = :archiveId
AND (
(record.publicDt IS NOT NULL AND record.publicDt <= CURRENT_TIMESTAMP AND record.archiveId = :archiveId)
OR (folder.publicDt IS NOT NULL AND folder.publicDt <= CURRENT_TIMESTAMP AND folder.archiveId = :archiveId)
)
AND tag.status = 'status.generic.ok'
AND tag_link.status = 'status.generic.ok'
AND record.status IS DISTINCT FROM 'status.generic.deleted'
AND folder.status IS DISTINCT FROM 'status.generic.deleted';
63 changes: 63 additions & 0 deletions src/archive/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { InternalServerError } from "http-errors";
import { db } from "../database";
import { archiveService } from "./service";
import { logger } from "../log";

jest.mock("../database");
jest.mock("../log", () => ({
logger: {
error: jest.fn(),
},
}));

const loadFixtures = async (): Promise<void> => {
await db.sql("fixtures.create_test_accounts");
await db.sql("fixtures.create_test_archive");
await db.sql("fixtures.create_test_records");
await db.sql("fixtures.create_test_folders");
await db.sql("fixtures.create_test_tags");
await db.sql("fixtures.create_test_tag_links");
};

const clearDatabase = async (): Promise<void> => {
await db.query(
"TRUNCATE account, archive, record, folder, tag, tag_link CASCADE"
);
};

describe("getPublicTags", () => {
beforeEach(async () => {
await clearDatabase();
await loadFixtures();
});
afterEach(async () => {
await clearDatabase();
jest.clearAllMocks();
});

test("should return public tags and not private or deleted tags", async () => {
const tags = await archiveService.getPublicTags("1");
expect(tags.length).toEqual(2);
expect(tags.map((tag) => tag.name)).toContain("test_public_file");
expect(tags.map((tag) => tag.name)).toContain("test_public_folder");
});

test("should return empty list for nonexistent archive", async () => {
const tags = await archiveService.getPublicTags("1000");
expect(tags.length).toEqual(0);
});

test("should throw an internal server error if database query fails unexpectedly", async () => {
let error = null;
const testError = new Error("out of cheese - redo from start");
try {
jest.spyOn(db, "sql").mockRejectedValueOnce(testError);
await archiveService.getPublicTags("1");
} catch (err) {
error = err;
} finally {
expect(error instanceof InternalServerError).toEqual(true);
expect(logger.error).toHaveBeenCalledWith(testError);
}
});
});
18 changes: 18 additions & 0 deletions src/archive/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import createError from "http-errors";
import { db } from "../database";
import { logger } from "../log";
import type { Tag } from "./models";

const getPublicTags = async (archiveId: string): Promise<Tag[]> => {
const tagsResult = await db
.sql<Tag>("archive.queries.get_public_tags", { archiveId })
.catch((err) => {
logger.error(err);
throw new createError.InternalServerError("failed to retrieve tags");
});
return tagsResult.rows;
};

export const archiveService = {
getPublicTags,
};
40 changes: 40 additions & 0 deletions src/archive/validators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { validateGetPublicTagsParams } from "./validators";

describe("validateGetPublicTagsParams", () => {
test("should find no errors in valid parameters", () => {
let error = null;
try {
validateGetPublicTagsParams({
archiveId: "123",
});
} catch (err) {
error = err;
} finally {
expect(error).toBeNull();
}
});

test("should error if archiveId is missing", () => {
let error = null;
try {
validateGetPublicTagsParams({});
} catch (err) {
error = err;
} finally {
expect(error).not.toBeNull();
}
});

test("should error if archiveId is wrong type", () => {
let error = null;
try {
validateGetPublicTagsParams({
archiveId: 123,
});
} catch (err) {
error = err;
} finally {
expect(error).not.toBeNull();
}
});
});
12 changes: 12 additions & 0 deletions src/archive/validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Joi from "joi";

export function validateGetPublicTagsParams(
data: unknown
): asserts data is { archiveId: string } {
const validation = Joi.object()
.keys({ archiveId: Joi.string().required() })
.validate(data);
if (validation.error) {
throw validation.error;
}
}
3 changes: 2 additions & 1 deletion src/fixtures/create_test_archive.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
INSERT INTO
archive (archiveId, archiveNbr)
VALUES
(1, '0001-0001');
(1, '0001-0001'),
(2, '0001-0002');
8 changes: 8 additions & 0 deletions src/fixtures/create_test_folders.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
INSERT INTO
folder (folderId, archiveId, publicDt, displayName, status)
VALUES
(1, 1, '2023-06-21', 'Public Folder', 'status.generic.ok'),
(2, 1, NULL, 'Private Folder', 'status.generic.ok'),
(3, 1, CURRENT_TIMESTAMP + '1 day'::interval, 'Future Public Folder', 'status.generic.ok'),
(4, 1, '2023-06-21', 'Deleted Folder', 'status.generic.deleted'),
(5, 2, '2023-06-21', 'Public Folder', 'status.generic.ok');
8 changes: 8 additions & 0 deletions src/fixtures/create_test_records.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
INSERT INTO
record (recordId, archiveId, publicDt, displayName, uploadAccountId, uploadFilename, status, type)
VALUES
(1, 1, '2023-06-21', 'Public File', 2, 'public_file.jpg', 'status.generic.ok', 'type.record.image'),
(2, 1, NULL, 'Private File', 2, 'private_file.jpg', 'status.generic.ok', 'type.record.image'),
(3, 1, CURRENT_TIMESTAMP + '1 day'::interval, 'Future Public File', 2, 'future_public_file.jpg', 'status.generic.ok', 'type.record.image'),
(4, 1, '2023-06-21', 'Deleted File', 2, 'public_file.jpg', 'status.generic.deleted', 'type.record.image'),
(5, 2, '2023-06-21', 'Public File', 3, 'public_file.jpg', 'status.generic.ok', 'type.record.image');
15 changes: 15 additions & 0 deletions src/fixtures/create_test_tag_links.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
INSERT INTO
tag_link (tagId, refId, refTable, status, type, createdDt, updatedDt)
VALUES
(1, 1, 'record', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(2, 2, 'record', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(3, 3, 'record', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(4, 4, 'record', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(5, 1, 'record', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(6, 1, 'record', 'status.generic.deleted', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(7, 5, 'record', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(8, 1, 'folder', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(9, 2, 'folder', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(10, 3, 'folder', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(11, 4, 'folder', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(12, 5, 'folder', 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
16 changes: 16 additions & 0 deletions src/fixtures/create_test_tags.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
INSERT INTO
tag (tagId, name, archiveId, status, type, createdDt, updatedDt)
VALUES
(1, 'test_public_file', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(2, 'test_private_file', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(3, 'test_future_public_file', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(4, 'test_deleted_file', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(5, 'test_deleted_tag', 1, 'status.generic.deleted', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(6, 'test_deleted_link', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(7, 'test_public_file_in_other_archive', 2, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(8, 'test_public_folder', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(9, 'test_private_folder', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(10, 'test_future_public_folder', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(11, 'test_deleted_folder', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(12, 'test_public_folder_in_other_archive', 2, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
(13, 'test_unused_tag', 1, 'status.generic.ok', 'type.generic.placeholder', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
2 changes: 2 additions & 0 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { healthController } from "../health";
import { directiveController } from "../directive";
import { legacyContactController } from "../legacy_contact";
import { accountController } from "../account";
import { archiveController } from "../archive";

const apiRoutes = express.Router();
apiRoutes.get("/health", healthController.getHealth);
apiRoutes.use("/directive", directiveController);
apiRoutes.use("/legacy-contact", legacyContactController);
apiRoutes.use("/account", accountController);
apiRoutes.use("/archive", archiveController);

export { apiRoutes };

0 comments on commit efdf616

Please sign in to comment.