Skip to content

Commit

Permalink
Merge branch 'main' into feat/qa-reporting-enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
TylerHendrickson committed Jan 5, 2024
2 parents 9d9e287 + 1927dbe commit e6e3bf4
Show file tree
Hide file tree
Showing 49 changed files with 11,614 additions and 3,897 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,4 @@ dist
terraform/.terraform
.tool-versions

localstack/volume
localstack/volume/
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:
- Added the required column `organizationId` to the `ReportingPeriod` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "ReportingPeriod" ADD COLUMN "organizationId" INTEGER NOT NULL;

-- AddForeignKey
ALTER TABLE "ReportingPeriod" ADD CONSTRAINT "ReportingPeriod_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-- AlterTable
ALTER TABLE "ExpenditureCategory" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "InputTemplate" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "OutputTemplate" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "Project" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "ReportingPeriod" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "Subrecipient" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "Upload" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "UploadValidation" ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP;

-- AlterTable
ALTER TABLE "User" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ(6),
ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP,
ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ(6);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Upload" ADD COLUMN "notes" TEXT;
22 changes: 13 additions & 9 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ model Organization {
agencies Agency[]
users User[]
name String
reportingPeriods ReportingPeriod[]
uploads Upload[]
uploadValidations UploadValidation[]
subrecipients Subrecipient[]
Expand All @@ -41,7 +42,7 @@ model User {
organizationId Int?
roleId Int?
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
agency Agency? @relation(fields: [agencyId], references: [id])
organization Organization? @relation(fields: [organizationId], references: [id])
role Role? @relation(fields: [roleId], references: [id])
Expand All @@ -67,7 +68,7 @@ model InputTemplate {
effectiveDate DateTime @db.Date
rulesGeneratedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
reportingPeriods ReportingPeriod[]
uploadValidations UploadValidation[]
}
Expand All @@ -79,7 +80,7 @@ model OutputTemplate {
effectiveDate DateTime @db.Date
rulesGeneratedAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
reportingPeriods ReportingPeriod[]
}

Expand All @@ -88,6 +89,8 @@ model ReportingPeriod {
name String
startDate DateTime @db.Date
endDate DateTime @db.Date
organizationId Int
organization Organization @relation(fields: [organizationId], references: [id])
certifiedAt DateTime? @db.Timestamptz(6)
certifiedById Int?
certifiedBy User? @relation(fields: [certifiedById], references: [id], onDelete: NoAction, onUpdate: NoAction)
Expand All @@ -97,7 +100,7 @@ model ReportingPeriod {
outputTemplate OutputTemplate @relation(fields: [outputTemplateId], references: [id], onDelete: NoAction, onUpdate: NoAction)
isCurrentPeriod Boolean @default(false)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
uploads Upload[]
projects Project[]
}
Expand All @@ -107,13 +110,14 @@ model ExpenditureCategory {
name String
code String
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
Uploads Upload[]
}

model Upload {
id Int @id @default(autoincrement())
filename String
notes String?
uploadedById Int
uploadedBy User @relation(fields: [uploadedById], references: [id])
agencyId Int
Expand All @@ -125,7 +129,7 @@ model Upload {
expenditureCategoryId Int
expenditureCategory ExpenditureCategory @relation(fields: [expenditureCategoryId], references: [id])
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
validations UploadValidation[]
subrecipients Subrecipient[]
}
Expand All @@ -149,7 +153,7 @@ model UploadValidation {
invalidatedById Int?
invalidatedBy User? @relation("InvalidatedUploads", fields: [invalidatedById], references: [id])
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
}

model Subrecipient {
Expand All @@ -165,7 +169,7 @@ model Subrecipient {
originationUploadId Int
originationUpload Upload @relation(fields: [originationUploadId], references: [id])
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
}

model Project {
Expand All @@ -181,5 +185,5 @@ model Project {
originationPeriodId Int
originationPeriod ReportingPeriod @relation(fields: [originationPeriodId], references: [id])
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @db.Timestamptz(6)
updatedAt DateTime @default(now()) @db.Timestamptz(6)
}
3 changes: 2 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@prisma/instrumentation": "^5.7.0",
"@redwoodjs/api": "6.4.2",
"@redwoodjs/graphql-server": "6.4.2",
"dd-trace": "^4.20.0"
"dd-trace": "^4.20.0",
"exceljs": "^4.4.0"
}
}
6 changes: 6 additions & 0 deletions api/src/graphql/reportingPeriods.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ export const schema = gql`
type Query {
reportingPeriods: [ReportingPeriod!]! @requireAuth
reportingPeriodsByOrg(organizationId: Int!): [ReportingPeriod!]!
@requireAuth
reportingPeriod(id: Int!): ReportingPeriod @requireAuth
previousReportingPeriods(
id: Int!
organizationId: Int!
): [ReportingPeriod!]! @requireAuth
}
input CreateReportingPeriodInput {
Expand Down
4 changes: 4 additions & 0 deletions api/src/graphql/uploads.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const schema = gql`
type Upload {
id: Int!
filename: String!
notes: String
uploadedById: Int!
uploadedBy: User!
agencyId: Int!
Expand All @@ -15,6 +16,7 @@ export const schema = gql`
createdAt: DateTime!
updatedAt: DateTime!
validations: [UploadValidation]!
signedUrl: String
}
type Query {
Expand All @@ -24,6 +26,7 @@ export const schema = gql`
input CreateUploadInput {
filename: String!
notes: String
uploadedById: Int!
agencyId: Int!
organizationId: Int!
Expand All @@ -33,6 +36,7 @@ export const schema = gql`
input UpdateUploadInput {
filename: String
notes: String
uploadedById: Int
agencyId: Int
organizationId: Int
Expand Down
17 changes: 6 additions & 11 deletions api/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseJWT, Decoded } from '@redwoodjs/api'
import { Decoded } from '@redwoodjs/api'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server'

/**
Expand Down Expand Up @@ -32,17 +32,12 @@ type RedwoodUser = Record<string, unknown> & { roles?: string[] }
export const getCurrentUser = async (
decoded: Decoded
): Promise<RedwoodUser | null> => {
if (!decoded) {
return null
console.log(decoded)
return {
id: 1,
email: '[email protected]',
roles: ['admin'],
}

const { roles } = parseJWT({ decoded })

if (roles) {
return { ...decoded, roles }
}

return { ...decoded }
}

/**
Expand Down
72 changes: 68 additions & 4 deletions api/src/lib/aws.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
GetObjectCommand,
HeadObjectCommand,
HeadObjectCommandInput,
PutObjectCommand,
PutObjectCommandInput,
S3Client,
Expand All @@ -11,17 +13,35 @@ import {
} from '@aws-sdk/client-sqs'
import { getSignedUrl as awsGetSignedUrl } from '@aws-sdk/s3-request-presigner'
import { StreamingBlobPayloadInputTypes } from '@smithy/types'
import { QueryResolvers, CreateUploadInput } from 'types/graphql'

const CPF_REPORTER_BUCKET_NAME = 'cpf-reporter'

function getS3Client() {
let s3: S3Client
if (process.env.LOCALSTACK_HOSTNAME) {
/*
1. Make sure the local environment has awslocal installed.
2. Use the commands to create a bucket to test with.
- awslocal s3api create-bucket --bucket arpa-audit-reports --region us-west-2 --create-bucket-configuration '{"LocationConstraint": "us-west-2"}'
- awslocal s3api create-bucket --bucket cpf-reporter --region us-west-2 --create-bucket-configuration '{"LocationConstraint": "us-west-2"}'
3. Access bucket resource metadata through the following URL.
- awslocal s3api list-buckets
- awslocal s3api list-objects --bucket arpa-audit-reports
- awslocal s3api list-objects --bucket cpf-reporter
4. Configure cors to allow uploads via signed URLs
===== cors-config.json =====
{
"CORSRules": [
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "POST", "PUT"],
"AllowedOrigins": ["http://localhost:8910"],
"ExposeHeaders": ["ETag"]
}
]
}
- awslocal s3api put-bucket-cors --bucket cpf-reporter --cors-configuration file://cors-config.json
*/
console.log('------------ USING LOCALSTACK ------------')
const endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:${
Expand All @@ -39,6 +59,15 @@ function getS3Client() {
return s3
}

export function uploadWorkbook(
upload: CreateUploadInput,
uploadId: number,
body: StreamingBlobPayloadInputTypes
) {
const folderName = `${upload.organizationId}/${upload.agencyId}/${upload.reportingPeriodId}/uploads/${upload.expenditureCategoryId}/${uploadId}/${upload.filename}`
return sendPutObjectToS3Bucket(CPF_REPORTER_BUCKET_NAME, folderName, body)
}

async function sendPutObjectToS3Bucket(
bucketName: string,
key: string,
Expand All @@ -54,19 +83,45 @@ async function sendPutObjectToS3Bucket(
await s3.send(new PutObjectCommand(uploadParams))
}

export function getTemplateRules(inputTemplateId: number) {
return sendHeadObjectToS3Bucket(
CPF_REPORTER_BUCKET_NAME,
`templates/input_templates/${inputTemplateId}/rules/`
)
}

async function sendHeadObjectToS3Bucket(bucketName: string, key: string) {
const s3 = getS3Client()
const uploadParams: PutObjectCommandInput = {
const uploadParams: HeadObjectCommandInput = {
Bucket: bucketName,
Key: key,
}
await s3.send(new PutObjectCommand(uploadParams))
await s3.send(new HeadObjectCommand(uploadParams))
}

export async function s3PutSignedUrl(
upload: CreateUploadInput,
uploadId: number
): Promise<string> {
const s3 = getS3Client()
const key = `${upload.organizationId}/${upload.agencyId}/${upload.reportingPeriodId}/uploads/${upload.expenditureCategoryId}/${uploadId}/${upload.filename}`
const baseParams: PutObjectCommandInput = {
Bucket: CPF_REPORTER_BUCKET_NAME,
Key: key,
ContentType:
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
const url = await awsGetSignedUrl(s3, new PutObjectCommand(baseParams), {
expiresIn: 60,
})
return url
}
/**
* This function is a wrapper around the getSignedUrl function from the @aws-sdk/s3-request-presigner package.
* Exists to organize the imports and to make it easier to mock in tests.
*/

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function getSignedUrl(bucketName: string, key: string) {
const s3 = getS3Client()
const baseParams = { Bucket: bucketName, Key: key }
Expand All @@ -89,6 +144,7 @@ function getSQSClient() {
return sqs
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function sendSqsMessage(queueUrl: string, messageBody: unknown) {
const sqs = getSQSClient()
await sqs.send(
Expand All @@ -99,6 +155,7 @@ async function sendSqsMessage(queueUrl: string, messageBody: unknown) {
)
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function receiveSqsMessage(queueUrl: string) {
const sqs = getSQSClient()
// const receiveResp = await sqs.send(new ReceiveMessageCommand({
Expand All @@ -118,6 +175,13 @@ async function receiveSqsMessage(queueUrl: string) {
)
}

export const s3PutObjectSignedUrl: QueryResolvers['s3PutObjectSignedUrl'] = ({
upload,
uploadId,
}) => {
return s3PutSignedUrl(upload, uploadId)
}

export default {
sendPutObjectToS3Bucket,
sendHeadObjectToS3Bucket,
Expand Down
Loading

0 comments on commit e6e3bf4

Please sign in to comment.