Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into prod
Browse files Browse the repository at this point in the history
  • Loading branch information
finlay-jisc committed Dec 19, 2024
2 parents 86047ce + 5a91c52 commit 0a2ada7
Show file tree
Hide file tree
Showing 40 changed files with 2,076 additions and 774 deletions.
1,395 changes: 1,326 additions & 69 deletions api/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"supertest": "^6.3.3"
},
"devDependencies": {
"@aws-sdk/client-ecs": "^3.709.0",
"@aws-sdk/client-s3": "^3.413.0",
"@aws-sdk/client-ses": "^3.413.0",
"@aws-sdk/client-sqs": "^3.413.0",
Expand Down
46 changes: 0 additions & 46 deletions api/serverless-config-default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -540,29 +540,6 @@ package:
- '!serverless*'
- '!src/**'
- '!tsconfig*'
# From devdependencies - due to an issue serverless's automatic devDependency exclusion doesn't work.
# https://github.com/serverless/serverless/issues/12911
# Please remove these rules when it's fixed.
- '!node_modules/.bin/esbuild'
- '!node_modules/@aws-crypto/**'
- '!node_modules/@babel/**'
- '!node_modules/@esbuild/**'
- '!node_modules/@eslint/**'
- '!node_modules/@hapi/**'
- '!node_modules/@types/**'
- '!node_modules/@typescript-eslint/**'
- '!node_modules/devtools-protocol/**'
- '!node_modules/es-abstract/**'
- '!node_modules/esbuild/**'
- '!node_modules/eslint/**'
- '!node_modules/eslint-plugin-import/**'
- '!node_modules/java-invoke-local/**'
- '!node_modules/prettier/**'
- '!node_modules/rxjs/**'
- '!node_modules/serverless/**'
- '!node_modules/terser/**'
- '!node_modules/web-streams-polyfill/**'
- '!node_modules/webpack/**'
# Inclusions.
- 'node_modules/.prisma/client/libquery_engine-linux-arm64-*'

Expand Down Expand Up @@ -600,29 +577,6 @@ package:
- '!serverless*'
- '!src/**'
- '!tsconfig*'
# From devdependencies - due to an issue serverless's automatic devDependency exclusion doesn't work.
# https://github.com/serverless/serverless/issues/12911
# Please remove these rules when it's fixed.
- '!node_modules/.bin/esbuild'
- '!node_modules/@aws-crypto/**'
- '!node_modules/@babel/**'
- '!node_modules/@esbuild/**'
- '!node_modules/@eslint/**'
- '!node_modules/@hapi/**'
- '!node_modules/@types/**'
- '!node_modules/@typescript-eslint/**'
- '!node_modules/devtools-protocol/**'
- '!node_modules/es-abstract/**'
- '!node_modules/esbuild/**'
- '!node_modules/eslint/**'
- '!node_modules/eslint-plugin-import/**'
- '!node_modules/java-invoke-local/**'
- '!node_modules/prettier/**'
- '!node_modules/rxjs/**'
- '!node_modules/serverless/**'
- '!node_modules/terser/**'
- '!node_modules/web-streams-polyfill/**'
- '!node_modules/webpack/**'
# Inclusions.
- 'node_modules/@sparticuz/chromium/**'
- 'node_modules/.prisma/client/libquery_engine-rhel-*'
Expand Down
9 changes: 8 additions & 1 deletion api/serverless-config-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,11 @@ functions:
generatePDFsFromQueue:
handler: dist/src/components/sqs/handler.generatePDFs
events:
- sqs: 'arn:aws:sqs:${aws:region}:${aws:accountId}:science-octopus-pdf-queue-${self:provider.stage}'
- sqs: 'arn:aws:sqs:${aws:region}:${aws:accountId}:science-octopus-pdf-queue-${self:provider.stage}'
triggerECSTask:
handler: dist/src/components/integration/routes.triggerECSTask
events:
- http:
path: ${self:custom.versions.v1}/integrations/simple-ecs-task
method: POST
cors: true
13 changes: 13 additions & 0 deletions api/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ provider:
TRIGGER_ARI_INGEST_API_KEY: ${ssm:/trigger_ari_ingest_api_key_${self:provider.stage}_octopus}
INGEST_REPORT_RECIPIENTS: ${ssm:/ingest_report_recipients_${self:provider.stage}_octopus}
PARTICIPATING_ARI_USER_IDS: ${ssm:/participating_ari_user_ids_${self:provider.stage}_octopus}
ECS_CLUSTER_ARN: ${ssm:/ecs_cluster_arn_${self:provider.stage}_octopus}
ECS_TASK_DEFINITION_ID: ${ssm:/ecs_task_definition_id_${self:provider.stage}_octopus}
ECS_TASK_SECURITY_GROUP_ID: ${ssm:/ecs_task_security_group_id_${self:provider.stage}_octopus}
PRIVATE_SUBNET_IDS: ${ssm:/${self:provider.stage}_octopus_private_subnet_az1},${ssm:/${self:provider.stage}_octopus_private_subnet_az2},${ssm:/${self:provider.stage}_octopus_private_subnet_az3}
deploymentBucket:
tags:
Project: Octopus
Expand Down Expand Up @@ -84,6 +88,15 @@ provider:
- 'sqs:GetQueueAttributes'
- 'sqs:ReceiveMessage'
- 'sqs:SendMessage'
# Required to trigger ECS tasks
- Effect: 'Allow'
Resource: "arn:aws:ecs:${aws:region}:${aws:accountId}:task-definition/${ssm:/ecs_task_definition_id_${self:provider.stage}_octopus}"
Action: 'ecs:RunTask'
- Effect: 'Allow'
Resource:
- 'arn:aws:iam::${aws:accountId}:role/octopus-ecs-task-role-${self:provider.stage}'
- 'arn:aws:iam::${aws:accountId}:role/octopus-ecs-task-exec-role-${self:provider.stage}'
Action: 'iam:PassRole'
custom:
splitStacks:
perFunction: true
Expand Down
11 changes: 2 additions & 9 deletions api/src/components/integration/__tests__/ari.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,16 +415,9 @@ describe('ARI import processes', () => {
test('Incremental import endpoint requires API key', async () => {
const triggerImport = await testUtils.agent.post('/integrations/ari/incremental');

expect(triggerImport.status).toEqual(400);
expect(triggerImport.status).toEqual(401);
expect(triggerImport.body).toMatchObject({
message: [
{
keyword: 'required',
params: {
missingProperty: 'apiKey'
}
}
]
message: "Please provide a valid 'apiKey'."
});
});

Expand Down
19 changes: 7 additions & 12 deletions api/src/components/integration/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,9 @@ import * as response from 'lib/response';
export const incrementalAriIngest = async (
event: I.APIRequest | I.EventBridgeEvent<'Scheduled Event', string>
): Promise<I.JSONResponse> => {
const triggeredByHttp = event && 'headers' in event;

// This can also be triggered on a schedule, in which case we don't need to check for an API key,
// so only check for the API key if the event is an API request.
if (triggeredByHttp) {
const apiKey = event.queryStringParameters?.apiKey;

if (apiKey !== process.env.TRIGGER_ARI_INGEST_API_KEY) {
return response.json(401, { message: "Please provide a valid 'apiKey'." });
}
}

// Check if a process is currently running.
const lastLog = await ingestLogService.getMostRecentLog('ARI', true);
const triggeredByHttp = event && 'headers' in event;
const dryRun = triggeredByHttp ? !!event.queryStringParameters?.dryRun : false;
const dryRunMessages: string[] = [];

Expand Down Expand Up @@ -48,3 +37,9 @@ export const incrementalAriIngest = async (
return response.json(500, { message: 'Unknown server error.' });
}
};

export const triggerECSTask = async (): Promise<I.JSONResponse> => {
const triggerTaskOutput = await integrationService.triggerECSTask();

return response.json(200, { message: triggerTaskOutput });
};
8 changes: 8 additions & 0 deletions api/src/components/integration/routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import middy from '@middy/core';

import * as Helpers from 'lib/helpers';
import * as integrationController from 'integration/controller';
import * as integrationSchema from 'integration/schema';
import * as middleware from 'middleware';

const triggerAriIngestApiKey = Helpers.checkEnvVariable('TRIGGER_ARI_INGEST_API_KEY');

export const incrementalAriIngest = middy(integrationController.incrementalAriIngest)
.use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true }))
.use(middleware.authentication(false, false, triggerAriIngestApiKey))
.use(middleware.validator(integrationSchema.incrementalAriIngestHttp, 'queryStringParameters'));

export const triggerECSTask = middy(integrationController.triggerECSTask)
.use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true }))
.use(middleware.authentication(false, false, triggerAriIngestApiKey));
13 changes: 13 additions & 0 deletions api/src/components/integration/service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import axios from 'axios';
import * as ariUtils from 'integration/ariUtils';
import * as ecs from 'lib/ecs';
import * as ingestLogService from 'ingestLog/service';
import * as Helpers from 'lib/helpers';

/**
* Incremental ARI ingest.
Expand Down Expand Up @@ -151,3 +153,14 @@ export const incrementalAriIngest = async (dryRun: boolean, reportFormat: 'email

return `${preamble} ${writeCount} publication${writeCount !== 1 ? 's' : ''}.`;
};

export const triggerECSTask = async (): Promise<string> => {
await ecs.runFargateTask({
clusterArn: Helpers.checkEnvVariable('ECS_CLUSTER_ARN'),
securityGroups: [Helpers.checkEnvVariable('ECS_TASK_SECURITY_GROUP_ID')],
subnetIds: Helpers.checkEnvVariable('PRIVATE_SUBNET_IDS').split(','),
taskDefinitionId: Helpers.checkEnvVariable('ECS_TASK_DEFINITION_ID')
});

return 'Done';
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('Get many publication versions', () => {
});

expect(getPublications.status).toEqual(200);
const publicationDates = getPublications.body.data.map((version) => version.publishedDate);
const publicationDates = getPublications.body.data.map((version) => version.publishedDate as Date);
// Sort a copy of the dates from the results to confirm order.
const sortedPublicationDates = [...publicationDates].sort(
(a, b) => new Date(b).getTime() - new Date(a).getTime()
Expand All @@ -67,9 +67,9 @@ describe('Get many publication versions', () => {
});

expect(getPublications.status).toEqual(200);
const publicationDates = getPublications.body.data.map((version) => version.publishedDate);
const publicationDates = getPublications.body.data.map((version) => version.publishedDate as Date);
// Sort a copy of the dates from the results to confirm order.
const sortedPublicationDates = [...publicationDates].sort(
const sortedPublicationDates: Date[] = [...publicationDates].sort(
(a, b) => new Date(a).getTime() - new Date(b).getTime()
);
expect(publicationDates).toEqual(sortedPublicationDates);
Expand Down
29 changes: 22 additions & 7 deletions api/src/components/user/__tests__/getUserPublications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ describe("Get a given user's publications", () => {
.query({ apiKey: 123456789, offset: 0, limit: 100 });

expect(publications.status).toEqual(200);
expect(publications.body.results.length).toEqual(25);
expect(publications.body.data.length).toEqual(25);
expect(
publications.body.results.some(
publications.body.data.some(
(publication) => publication.versions.some((version) => version.currentStatus === 'DRAFT') as boolean
)
).toEqual(true);
Expand All @@ -24,9 +24,9 @@ describe("Get a given user's publications", () => {
const publications = await testUtils.agent.get('/users/test-user-1/publications');

expect(publications.status).toEqual(200);
expect(publications.body.results.length).toEqual(10);
expect(publications.body.data.length).toEqual(10);
expect(
publications.body.results.some(
publications.body.data.some(
(publication) => publication.versions.some((version) => version.currentStatus === 'DRAFT') as boolean
)
).toEqual(false);
Expand All @@ -36,9 +36,9 @@ describe("Get a given user's publications", () => {
const publications = await testUtils.agent.get('/users/test-user-1/publications').query({ apiKey: 987654321 });

expect(publications.status).toEqual(200);
expect(publications.body.results.length).toEqual(10);
expect(publications.body.data.length).toEqual(10);
expect(
publications.body.results.some(
publications.body.data.some(
(publication) => publication.versions.some((version) => version.currentStatus === 'DRAFT') as boolean
)
).toEqual(false);
Expand All @@ -49,8 +49,23 @@ describe("Get a given user's publications", () => {
.get('/users/user-does-not-exist/publications')
.query({ apiKey: 987654321 });

expect(publications.body.results).toBe(undefined);
expect(publications.body.data).toBe(undefined);
expect(publications.body.message).toBe('User not found');
expect(publications.status).toEqual(400);
});

test('Results can be filtered by a query term', async () => {
const queryTerm = 'interpretation';
const publications = await testUtils.agent.get('/users/test-user-1/publications').query({ query: queryTerm });

expect(publications.status).toEqual(200);
expect(publications.body.data.length).toEqual(1);
expect(
publications.body.data.every((publication) =>
publication.versions.some(

Check warning on line 65 in api/src/components/user/__tests__/getUserPublications.test.ts

View workflow job for this annotation

GitHub Actions / eslint

Unsafe return of an `any` typed value
(version) => version.isLatestLiveVersion && version.title.toLowerCase().includes(queryTerm)

Check warning on line 66 in api/src/components/user/__tests__/getUserPublications.test.ts

View workflow job for this annotation

GitHub Actions / eslint

Unsafe return of an `any` typed value
)
)
).toEqual(true);
});
});
8 changes: 1 addition & 7 deletions api/src/components/user/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,7 @@ export const getPublications = async (
}
};

export const getUserList = async (event: I.APIRequest): Promise<I.JSONResponse> => {
const apiKey = event.queryStringParameters?.apiKey;

if (apiKey !== process.env.LIST_USERS_API_KEY) {
return response.json(401, { message: "Please provide a valid 'apiKey'." });
}

export const getUserList = async (): Promise<I.JSONResponse> => {
try {
const userList = await userService.getUserList();

Expand Down
5 changes: 4 additions & 1 deletion api/src/components/user/routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import middy from '@middy/core';

import * as Helpers from 'lib/helpers';
import * as middleware from 'middleware';
import * as userController from 'user/controller';
import * as userSchema from 'user/schema';
Expand All @@ -20,7 +21,9 @@ export const getPublications = middy(userController.getPublications)
.use(middleware.authentication(true))
.use(middleware.validator(userSchema.getPublications, 'queryStringParameters'));

export const getUserList = userController.getUserList;
export const getUserList = middy(userController.getUserList)
.use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true }))
.use(middleware.authentication(false, false, Helpers.checkEnvVariable('LIST_USERS_API_KEY')));

export const getUserControlRequests = middy(userController.getUserControlRequests)
.use(middleware.doNotWaitForEmptyEventLoop({ runOnError: true, runOnBefore: true, runOnAfter: true }))
Expand Down
4 changes: 4 additions & 0 deletions api/src/components/user/schema/getPublications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const getPublicationsSchema: I.JSONSchemaType<I.UserPublicationsFilters> = {
minimum: 1,
default: 10
},
query: {
type: 'string',
nullable: true
},
versionStatus: {
type: 'string',
nullable: true
Expand Down
17 changes: 15 additions & 2 deletions api/src/components/user/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,20 @@ export const getPublications = async (
}
: {}
: // But if the user is not the owner, get only publications that have a published version
{ versions: { some: { isLatestLiveVersion: true } } })
{ versions: { some: { isLatestLiveVersion: true } } }),
// And, if a query is supplied, where the query matches the latest live title.
...(params.query
? {
versions: {
some: {
isLatestLiveVersion: true,
title: {
search: Helpers.sanitizeSearchQuery(params.query)
}
}
}
}
: {})
};

const userPublications = await client.prisma.publication.findMany({
Expand Down Expand Up @@ -391,7 +404,7 @@ export const getPublications = async (
}
});

return { offset, limit, total: totalUserPublications, results: sortedPublications };
return { data: sortedPublications, metadata: { offset, limit, total: totalUserPublications } };
};

export const getUserList = async () => {
Expand Down
25 changes: 25 additions & 0 deletions api/src/lib/ecs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ECSClient, RunTaskCommand, RunTaskCommandInput } from '@aws-sdk/client-ecs';

const client = new ECSClient();

export const runFargateTask = async (config: {
clusterArn: string;
securityGroups: string[];
subnetIds: string[];
taskDefinitionId: string;
}): Promise<void> => {
const input: RunTaskCommandInput = {
cluster: config.clusterArn,
launchType: 'FARGATE',
networkConfiguration: {
awsvpcConfiguration: {
securityGroups: config.securityGroups,
subnets: config.subnetIds
}
},
taskDefinition: config.taskDefinitionId
};
const command = new RunTaskCommand(input);
const response = await client.send(command);
console.log(response);
};
1 change: 1 addition & 0 deletions api/src/lib/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ export interface UpdateAffiliationsBody {
export interface UserPublicationsFilters {
offset: number;
limit: number;
query?: string;
versionStatus?: string;
}

Expand Down
Loading

0 comments on commit 0a2ada7

Please sign in to comment.