Skip to content

Commit

Permalink
Deploy cloudfront function initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fongsean committed Oct 1, 2024
1 parent c4db2d6 commit 523ec6e
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 0 deletions.
56 changes: 56 additions & 0 deletions .github/workflows/deploy_cloudfront_function.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Deploy IG Routing Cloudfront function Workflow

on:
push:

jobs:
build:
name: Deploy IG Routing Cloudfront function
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Use Node.js 16.x
uses: actions/setup-node@v4
with:
node-version: 16
cache: npm

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::209248795938:role/SmartFormsReactAppDeployment
aws-region: ap-southeast-2

- name: Describe the SmartFormsIgRouting function to get current ETag
id: describe_function
run: |
OUTPUT=$(aws cloudfront get-function --name SmartFormsIgRouting)
ETag=$(echo $OUTPUT | jq -r '.ETag')
echo "::set-output name=etag::$ETag"
shell: bash

- name: Update the SmartFormsIgRouting Function
id: update_function
run: |
OUTPUT=$(aws cloudfront update-function \
--name SmartFormsIgRouting \
--if-match ${{ steps.describe_function.outputs.etag }} \
--function-config "{\"Comment\":\"Manages routing within the Smart Forms IG\",\"Runtime\":\"cloudfront-js-2.0\"}" \
--function-code fileb://./SmartFormsIgRouting.js)
NEW_ETAG=$(echo $OUTPUT | jq -r '.ETag')
echo "::set-output name=new_etag::$NEW_ETAG"
shell: bash

- name: Publish the SmartFormsIgRouting Function
run: |
aws cloudfront publish-function \
--name SmartFormsIgRouting \
--if-match ${{ steps.update_function.outputs.new_etag }}
shell: bash

- name: Log the new ETag
run: |
echo "New ETag after updating and publishing: ${{ steps.update_function.outputs.new_etag }}"
shell: bash
193 changes: 193 additions & 0 deletions SmartFormsIgRouting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* This function is used to handle routing for the Smart Forms IG in Cloudfront.
* The base IG url should be https://smartforms.csiro.au/ig
*
* When updating the IG, the "latestVersion" variable (line 10) should be updated.
* It triggers a workflow in GitHub Actions to update the deployed function in Cloudfront.
*/

// Latest IG version, update this every time the IG is updated
const latestVersion = "0.2.0-draft";

const basePathIg = "ig"
const implementationGuideCanonical = `/${basePathIg}/ImplementationGuide/csiro.fhir.au.smartforms`

const validCanonicalResourceTypes = [
"ActivityDefinition",
"CapabilityStatement",
"ChargeItemDefinition",
"CodeSystem",
"CompartmentDefinition",
"ConceptMap",
"EffectEvidenceSynthesis",
"EventDefinition",
"Evidence",
"EvidenceVariable",
"ExampleScenario",
"GraphDefinition",
"ImplementationGuide",
"Library",
"Measure",
"MessageDefinition",
"NamingSystem",
"OperationDefinition",
"PlanDefinition",
"Questionnaire",
"ResearchDefinition",
"ResearchElementDefinition",
"RiskEvidenceSynthesis",
"SearchParameter",
"StructureDefinition",
"StructureMap",
"TerminologyCapabilities",
"TestScript",
"ValueSet"
];

function handler(event) {
let request = event.request;

// Add headers to control caching
request.headers['cache-control'] = {
'value': 'no-store, no-cache, must-revalidate'
}

// Ignore double slashes
request.uri = request.uri.replace(/\/\//g, '/');

let uri = request.uri;

// Handle IG routes
if (uri.startsWith(`/${basePathIg}`)) {
// If the URI is /test-ig or /test-ig/ redirect to the index.html
if (uri === `/${basePathIg}` || uri === `/${basePathIg}/` || uri === implementationGuideCanonical) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
"location": {"value": `/${basePathIg}/index.html`},
"cache-control": {"value": "no-store, no-cache, must-revalidate"}
}
};
}

// Handle URI with a version
if (uri.includes(".")) {
const parts = uri.split(".");
// if the last part is the version, redirect to the version's index.html
if (parts[parts.length - 1].match(/[0-9]/)) {
const pathRegex = new RegExp(`/${basePathIg}/[a-z0-9]+`, 'i'); // Create regex with basePath
const version = getVersionFromURI(uri, `/${basePathIg}`)
const versionIsLast = versionIsLastPart(uri, version)
if (uri.match(pathRegex) && version && versionIsLast) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
"location": {"value": `/${basePathIg}/${version}/index.html`},
"cache-control": {"value": "no-store, no-cache, must-revalidate"}
}
};
}
}
}


// Add latest version to URIs without a version
const subRoutes = uri
.split("/")
.filter((part) => part !== "")
.slice(1);
if (subRoutes.length > 0) {
const firstSubRoute = subRoutes[0];

// Check if the first sub route is a version, otherwise add the latest version to the URI
if (!/^[0-9]/.test(firstSubRoute)) {
request.uri = `/${basePathIg}/${latestVersion}/${subRoutes.join("/")}`;
}

// Check if uri is canonical. if so, redirect to the resolvable link
if (isCanonicalUrl(uri, subRoutes)) {
return {
statusCode: 301,
headers: {
"location": {"value": transformCanonicalUrl(uri, basePathIg, latestVersion)},
"cache-control": {"value": "no-store, no-cache, must-revalidate"}
}
};
}
}

// Add .html to URIs without an extension
let uriChunks = uri.split('/');
let lastUriChunk = uriChunks[uriChunks.length - 1];
if (!lastUriChunk.includes('.')) {
request.uri += '.html';
return request;
}
}


return request;
}


function getVersionFromURI(uri, prefix) {
// Check if the uri starts with the prefix
if (uri.startsWith(prefix)) {
// Extract the part after the prefix
const afterPrefix = uri.substring(prefix.length + 1);

// Split by the slash to isolate the version, i.e. 0.1.0-draft
return afterPrefix.split('/')[0];
}

return null; // Return null if the prefix is not found
}

function versionIsLastPart(uri, version) {
// Split the uri by the slash
const parts = uri.split('/').filter(part => part !== '');

// Check if the last part is the version
return parts[parts.length - 1] === version;
}

function isCanonicalUrl(uri, subRoutes) {
// Not a valid canonical URL if uri contains ".html"
if (uri.includes(".html")) {
return false;
}

// Not a valid canonical URL if the first part has a version
if (subRoutes[0].match(/[0-9]/)) {
return false;
}

const indexValidCanonicalResourceType = subRoutes.findIndex((subRoute) =>
validCanonicalResourceTypes.includes(subRoute)
);

if (indexValidCanonicalResourceType === -1) {
return false;
}

// Not a valid canonical URL if the last part is the index
if (indexValidCanonicalResourceType === subRoutes.length - 1) {
return false;
}

return true;
}

function transformCanonicalUrl(uri, basePathIg, latestVersion) {
const lastSlashIndex = uri.lastIndexOf("/");

const front = uri.substring(0, lastSlashIndex);
const back = uri.substring(lastSlashIndex + 1);

const resolvableUrl = `${front}-${back}.html`;

// Add latest version to the resolvable URL
return resolvableUrl.replace(basePathIg, `${basePathIg}/${latestVersion}`);
}

0 comments on commit 523ec6e

Please sign in to comment.