Skip to content

Commit

Permalink
feature/CPF-34 merge main, enable Users tab
Browse files Browse the repository at this point in the history
  • Loading branch information
Vikariusu committed Dec 18, 2023
2 parents df6310f + d26a07b commit c30d7a0
Show file tree
Hide file tree
Showing 28 changed files with 1,292 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,5 @@ dist
# Terraform cache
terraform/.terraform
.tool-versions

localstack/volume
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "createdAt" SET DATA TYPE TIMESTAMPTZ(6),
ALTER COLUMN "updatedAt" SET DATA TYPE TIMESTAMPTZ(6);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- DropForeignKey
ALTER TABLE "User" DROP CONSTRAINT "User_organizationId_fkey";

-- AlterTable
ALTER TABLE "User" ALTER COLUMN "organizationId" DROP NOT NULL;

-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;
8 changes: 4 additions & 4 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ model User {
email String
name String?
agencyId Int?
organizationId Int
organizationId Int?
roleId Int?
createdAt DateTime @default(now())
updatedAt DateTime
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
agency Agency? @relation(fields: [agencyId], references: [id])
organization Organization @relation(fields: [organizationId], references: [id])
organization Organization? @relation(fields: [organizationId], references: [id])
role Role? @relation(fields: [roleId], references: [id])
certified ReportingPeriod[]
uploaded Upload[]
Expand Down
4 changes: 4 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.472.0",
"@aws-sdk/client-ses": "^3.470.0",
"@aws-sdk/client-sqs": "^3.470.0",
"@aws-sdk/client-ssm": "^3.462.0",
"@aws-sdk/rds-signer": "^3.462.0",
"@aws-sdk/s3-request-presigner": "^3.472.0",
"@opentelemetry/instrumentation": "^0.45.1",
"@prisma/instrumentation": "^5.7.0",
"@redwoodjs/api": "6.4.2",
Expand Down
6 changes: 4 additions & 2 deletions api/src/graphql/users.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,28 @@ export const schema = gql`
organization: Organization!
role: Role
certified: [ReportingPeriod]!
uploaded: [Upload]!
validated: [UploadValidation]!
invalidated: [UploadValidation]!
}
type Query {
users: [User!]! @requireAuth
usersByOrganization(organizationId: Int!): [User!]! @requireAuth
user(id: Int!): User @requireAuth
}
input CreateUserInput {
email: String!
name: String
agencyId: Int
organizationId: Int!
roleId: Int
}
input UpdateUserInput {
email: String
name: String
agencyId: Int
organizationId: Int
roleId: Int
}
Expand Down
106 changes: 106 additions & 0 deletions api/src/lib/aws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
GetObjectCommand,
PutObjectCommand,
PutObjectCommandInput,
S3Client,
} from '@aws-sdk/client-s3'
import {ReceiveMessageCommand, SendMessageCommand, SQSClient} from '@aws-sdk/client-sqs'

Check warning on line 7 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Replace `ReceiveMessageCommand,·SendMessageCommand,·SQSClient` with `⏎··ReceiveMessageCommand,⏎··SendMessageCommand,⏎··SQSClient,⏎`
import {getSignedUrl as awsGetSignedUrl} from '@aws-sdk/s3-request-presigner'

Check warning on line 8 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Replace `getSignedUrl·as·awsGetSignedUrl` with `·getSignedUrl·as·awsGetSignedUrl·`

function getS3Client() {
let s3: S3Client;

Check warning on line 11 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
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"}'
3. Access bucket resource metadata through the following URL.
- awslocal s3api list-buckets
- awslocal s3api list-objects --bucket arpa-audit-reports
*/
console.log('------------ USING LOCALSTACK ------------');

Check warning on line 21 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
const endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:${process.env.EDGE_PORT || 4566}`;

Check warning on line 22 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Replace `process.env.EDGE_PORT·||·4566}`;` with `⏎······process.env.EDGE_PORT·||·4566⏎····}``
console.log(`endpoint: ${endpoint}`);

Check warning on line 23 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
s3 = new S3Client({
endpoint,
forcePathStyle: true,
region: process.env.AWS_DEFAULT_REGION,
});

Check warning on line 28 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
} else {
s3 = new S3Client();

Check warning on line 30 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
}
return s3;

Check warning on line 32 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Delete `;`
}

async function sendPutObjectToS3Bucket(bucketName: string, key: string, body: any) {

Check warning on line 35 in api/src/lib/aws.ts

View workflow job for this annotation

GitHub Actions / qa / Lint JavaScript

Replace `bucketName:·string,·key:·string,·body:·any` with `⏎··bucketName:·string,⏎··key:·string,⏎··body:·any⏎`
const s3 = getS3Client();
const uploadParams : PutObjectCommandInput = {
Bucket: bucketName,
Key: key,
Body: body,
ServerSideEncryption: 'AES256',
};
await s3.send(new PutObjectCommand(uploadParams));
}

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

/**
* 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.
*/
async function getSignedUrl(bucketName: string, key: string) {
const s3 = getS3Client();
const baseParams = { Bucket: bucketName, Key: key };
return awsGetSignedUrl(s3, new GetObjectCommand(baseParams), { expiresIn: 60 });
}

function getSQSClient() {
let sqs: SQSClient;
if (process.env.LOCALSTACK_HOSTNAME) {
console.log('------------ USING LOCALSTACK FOR SQS ------------');
const endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:${process.env.EDGE_PORT || 4566}`;
sqs = new SQSClient({ endpoint, region: process.env.AWS_DEFAULT_REGION });
} else {
sqs = new SQSClient();
}
return sqs;
}

async function sendSqsMessage(queueUrl: string, messageBody: any) {
const sqs = getSQSClient();
await sqs.send(new SendMessageCommand({
QueueUrl: queueUrl,
MessageBody: JSON.stringify(messageBody),
}));
}

async function receiveSqsMessage(queueUrl: string) {
const sqs = getSQSClient();
// const receiveResp = await sqs.send(new ReceiveMessageCommand({
// QueueUrl: process.env.TASK_QUEUE_URL, WaitTimeSeconds: 20, MaxNumberOfMessages: 1,
// }));

// const receiveResp = await sqs.send(new ReceiveMessageCommand({
// QueueUrl: process.env.TASK_QUEUE_URL, WaitTimeSeconds: 20, MaxNumberOfMessages: 1,
// }));

await sqs.send(new ReceiveMessageCommand({
QueueUrl: queueUrl, WaitTimeSeconds: 20, MaxNumberOfMessages: 1,
}));
}

export default {
sendPutObjectToS3Bucket,
sendHeadObjectToS3Bucket,
getSignedUrl,
sendSqsMessage,
receiveSqsMessage,
};
4 changes: 2 additions & 2 deletions api/src/services/users/users.scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ export const standard = defineScenario<Prisma.UserCreateArgs>({
one: {
data: {
email: 'String',
updatedAt: '2023-12-07T18:20:20.679Z',
updatedAt: '2023-12-10T00:37:26.049Z',
organization: { create: { name: 'String' } },
},
},
two: {
data: {
email: 'String',
updatedAt: '2023-12-07T18:20:20.679Z',
updatedAt: '2023-12-10T00:37:26.049Z',
organization: { create: { name: 'String' } },
},
},
Expand Down
4 changes: 2 additions & 2 deletions api/src/services/users/users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ describe('users', () => {
input: {
email: 'String',
organizationId: scenario.user.two.organizationId,
updatedAt: '2023-12-07T18:20:20.664Z',
updatedAt: '2023-12-10T00:37:26.029Z',
},
})

expect(result.email).toEqual('String')
expect(result.organizationId).toEqual(scenario.user.two.organizationId)
expect(result.updatedAt).toEqual(new Date('2023-12-07T18:20:20.664Z'))
expect(result.updatedAt).toEqual(new Date('2023-12-10T00:37:26.029Z'))
})

scenario('updates a user', async (scenario: StandardScenario) => {
Expand Down
23 changes: 23 additions & 0 deletions api/src/services/users/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ export const deleteUser: MutationResolvers['deleteUser'] = ({ id }) => {
})
}

export const usersByOrganization: QueryResolvers['usersByOrganization'] =
async ({ organizationId }) => {
try {
const users = await db.user.findMany({
where: { organizationId },
})
return users || [] // Return an empty array if null is received
} catch (error) {
console.error(error)
// Handle the error appropriately; maybe log it and return an empty array
return []
}
}

export const User: UserRelationResolvers = {
agency: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).agency()
Expand All @@ -48,4 +62,13 @@ export const User: UserRelationResolvers = {
certified: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).certified()
},
uploaded: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).uploaded()
},
validated: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).validated()
},
invalidated: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).invalidated()
},
}
20 changes: 18 additions & 2 deletions api/types/graphql.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ export type CreateUserInput = {
agencyId?: InputMaybe<Scalars['Int']>;
email: Scalars['String'];
name?: InputMaybe<Scalars['String']>;
organizationId: Scalars['Int'];
roleId?: InputMaybe<Scalars['Int']>;
};

Expand Down Expand Up @@ -460,6 +459,7 @@ export type Query = {
uploads: Array<Upload>;
user?: Maybe<User>;
users: Array<User>;
usersByOrganization: Array<User>;
};


Expand Down Expand Up @@ -540,6 +540,12 @@ export type QueryuserArgs = {
id: Scalars['Int'];
};


/** About the Redwood queries. */
export type QueryusersByOrganizationArgs = {
organizationId: Scalars['Int'];
};

/**
* The RedwoodJS Root Schema
*
Expand Down Expand Up @@ -689,7 +695,6 @@ export type UpdateUserInput = {
agencyId?: InputMaybe<Scalars['Int']>;
email?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
organizationId?: InputMaybe<Scalars['Int']>;
roleId?: InputMaybe<Scalars['Int']>;
};

Expand Down Expand Up @@ -743,12 +748,15 @@ export type User = {
createdAt: Scalars['DateTime'];
email: Scalars['String'];
id: Scalars['Int'];
invalidated: Array<Maybe<UploadValidation>>;
name?: Maybe<Scalars['String']>;
organization: Organization;
organizationId: Scalars['Int'];
role?: Maybe<Role>;
roleId?: Maybe<Scalars['Int']>;
updatedAt: Scalars['DateTime'];
uploaded: Array<Maybe<Upload>>;
validated: Array<Maybe<UploadValidation>>;
};

type MaybeOrArrayOfMaybe<T> = T | Maybe<T> | Maybe<T>[];
Expand Down Expand Up @@ -1184,6 +1192,7 @@ export type QueryResolvers<ContextType = RedwoodGraphQLContext, ParentType exten
uploads: OptArgsResolverFn<Array<ResolversTypes['Upload']>, ParentType, ContextType>;
user: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<QueryuserArgs, 'id'>>;
users: OptArgsResolverFn<Array<ResolversTypes['User']>, ParentType, ContextType>;
usersByOrganization: Resolver<Array<ResolversTypes['User']>, ParentType, ContextType, RequireFields<QueryusersByOrganizationArgs, 'organizationId'>>;
};

export type QueryRelationResolvers<ContextType = RedwoodGraphQLContext, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query']> = {
Expand Down Expand Up @@ -1213,6 +1222,7 @@ export type QueryRelationResolvers<ContextType = RedwoodGraphQLContext, ParentTy
uploads?: RequiredResolverFn<Array<ResolversTypes['Upload']>, ParentType, ContextType>;
user?: RequiredResolverFn<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<QueryuserArgs, 'id'>>;
users?: RequiredResolverFn<Array<ResolversTypes['User']>, ParentType, ContextType>;
usersByOrganization?: RequiredResolverFn<Array<ResolversTypes['User']>, ParentType, ContextType, RequireFields<QueryusersByOrganizationArgs, 'organizationId'>>;
};

export type RedwoodResolvers<ContextType = RedwoodGraphQLContext, ParentType extends ResolversParentTypes['Redwood'] = ResolversParentTypes['Redwood']> = {
Expand Down Expand Up @@ -1412,12 +1422,15 @@ export type UserResolvers<ContextType = RedwoodGraphQLContext, ParentType extend
createdAt: OptArgsResolverFn<ResolversTypes['DateTime'], ParentType, ContextType>;
email: OptArgsResolverFn<ResolversTypes['String'], ParentType, ContextType>;
id: OptArgsResolverFn<ResolversTypes['Int'], ParentType, ContextType>;
invalidated: OptArgsResolverFn<Array<Maybe<ResolversTypes['UploadValidation']>>, ParentType, ContextType>;
name: OptArgsResolverFn<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
organization: OptArgsResolverFn<ResolversTypes['Organization'], ParentType, ContextType>;
organizationId: OptArgsResolverFn<ResolversTypes['Int'], ParentType, ContextType>;
role: OptArgsResolverFn<Maybe<ResolversTypes['Role']>, ParentType, ContextType>;
roleId: OptArgsResolverFn<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
updatedAt: OptArgsResolverFn<ResolversTypes['DateTime'], ParentType, ContextType>;
uploaded: OptArgsResolverFn<Array<Maybe<ResolversTypes['Upload']>>, ParentType, ContextType>;
validated: OptArgsResolverFn<Array<Maybe<ResolversTypes['UploadValidation']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

Expand All @@ -1428,12 +1441,15 @@ export type UserRelationResolvers<ContextType = RedwoodGraphQLContext, ParentTyp
createdAt?: RequiredResolverFn<ResolversTypes['DateTime'], ParentType, ContextType>;
email?: RequiredResolverFn<ResolversTypes['String'], ParentType, ContextType>;
id?: RequiredResolverFn<ResolversTypes['Int'], ParentType, ContextType>;
invalidated?: RequiredResolverFn<Array<Maybe<ResolversTypes['UploadValidation']>>, ParentType, ContextType>;
name?: RequiredResolverFn<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
organization?: RequiredResolverFn<ResolversTypes['Organization'], ParentType, ContextType>;
organizationId?: RequiredResolverFn<ResolversTypes['Int'], ParentType, ContextType>;
role?: RequiredResolverFn<Maybe<ResolversTypes['Role']>, ParentType, ContextType>;
roleId?: RequiredResolverFn<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
updatedAt?: RequiredResolverFn<ResolversTypes['DateTime'], ParentType, ContextType>;
uploaded?: RequiredResolverFn<Array<Maybe<ResolversTypes['Upload']>>, ParentType, ContextType>;
validated?: RequiredResolverFn<Array<Maybe<ResolversTypes['UploadValidation']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};

Expand Down
17 changes: 17 additions & 0 deletions localstack/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: "3.8"

services:
localstack:
container_name: "cpf-reporter-localstack-main"
image: localstack/localstack
ports:
- "4566:4566" # LocalStack Gateway
- "4510-4559:4510-4559" # external services port range
environment:
- DEBUG=${DEBUG-}
- DOCKER_HOST=unix:///var/run/docker.sock
- AWS_DEFAULT_REGION=${AWS_REGION:-us-west-2}
volumes:
- "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
- "./entrypoint/init-aws.sh:/etc/localstack/init/ready.d/init-aws.sh"
16 changes: 16 additions & 0 deletions localstack/entrypoint/init-aws.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#! /bin/bash

export AWS_ACCESS_KEY_ID="test"
export AWS_SECRET_ACCESS_KEY="test"

VALID_EMAILS=(
"[email protected]"
)

for email in "${VALID_EMAILS[@]}"; do
awslocal ses verify-email-identity --email-address ${email}
echo "Verified ${email} to send with localstack SES"
done

awslocal s3api create-bucket --bucket cpf-reporter --region us-west-2 --create-bucket-configuration '{"LocationConstraint": "us-west-2"}'

Loading

0 comments on commit c30d7a0

Please sign in to comment.