Skip to content

Commit

Permalink
Merge pull request #15 from bcgov/submission-docs-backend
Browse files Browse the repository at this point in the history
Document upload
  • Loading branch information
TimCsaky authored Jan 13, 2024
2 parents c0465fb + 5db55c0 commit ca181e3
Show file tree
Hide file tree
Showing 28 changed files with 424 additions and 34 deletions.
1 change: 1 addition & 0 deletions .github/environments/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ config:
configMap:
FRONTEND_APIPATH: api/v1
FRONTEND_COMS_APIPATH: https://coms-dev.api.gov.bc.ca/api/v1
FRONTEND_COMS_BUCKETID: 1f9e1451-c130-4804-aeb0-b78b5b109c47
FRONTEND_OIDC_AUTHORITY: https://dev.loginproxy.gov.bc.ca/auth/realms/standard
FRONTEND_OIDC_CLIENTID: nr-permit-connect-navigator-service-5188
SERVER_APIPATH: /api/v1
Expand Down
4 changes: 4 additions & 0 deletions app/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"frontend": {
"apiPath": "FRONTEND_APIPATH",
"coms": {
"apiPath": "FRONTEND_COMS_APIPATH",
"bucketId": "FRONTEND_COMS_BUCKETID"
},
"notificationBanner": "FRONTEND_NOTIFICATION_BANNER",
"oidc": {
"authority": "FRONTEND_OIDC_AUTHORITY",
Expand Down
39 changes: 39 additions & 0 deletions app/src/controllers/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { documentService } from '../services';

import type { NextFunction, Request, Response } from '../interfaces/IExpress';

const controller = {
async createDocument(
req: Request<
never,
never,
{ documentId: string; submissionId: string; filename: string; mimeType: string; length: number }
>,
res: Response,
next: NextFunction
) {
try {
const response = await documentService.createDocument(
req.body.documentId,
req.body.submissionId,
req.body.filename,
req.body.mimeType,
req.body.length
);
res.status(200).send(response);
} catch (e: unknown) {
next(e);
}
},

async listDocuments(req: Request<{ submissionId: string }>, res: Response, next: NextFunction) {
try {
const response = await documentService.listDocuments(req.params.submissionId);
res.status(200).send(response);
} catch (e: unknown) {
next(e);
}
}
};

export default controller;
1 change: 1 addition & 0 deletions app/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as chefsController } from './chefs';
export { default as documentController } from './document';
export { default as userController } from './user';
33 changes: 33 additions & 0 deletions app/src/db/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# NR PermitConnect Navigator Service Database

## Directory Structure

```txt
db/ - Database Root
├── migrations/ - Knex database migrations files
├── models/ - Database/Application conversion layer
├── prisma/ - Location of the Prisma schema
└── utils/ - Utility functions
dataConnection.ts - Defines the Prisma database connection
stamps.ts - Defines default timestamp columns
```

## Models

The files in `models/` contain two key sections: type definitions, and `toPrismaModel`/`fromPrismaModel` conversion functions.

The type definitions are necessary to generate the appropriate hard typings for the conversions. They do not need to be exported as they should never need to be referenced outsite their respective files.

Due to the way Prisma handles foreign keys multiple types may need to be created.

Types beginning with `PrismaRelation` are type definitions for an object going to the database. This type may or may not include relational information, but for consistency are named with the same prefix.

Types beginning with `PrismaGraph` are type definitions for an object coming from the database. The incoming type may also begin with `PrismaRelation` - it depends if there is any relational information required or not.

See `user.ts` and `document.ts` for examples of the differences.

The `toPrismaModel` and `fromPrismaModel` functions are used to convert Prisma database models to application `src/types/` and vice versa. These functions should only ever be used in the application service layer.

## Future Considerations

Consider the use of namespaces/modules to wrap particular sections of the application. As more initiatives are added to the system there will be naming conflicts.
26 changes: 26 additions & 0 deletions app/src/db/migrations/20231212000000_init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ export async function up(knex: Knex): Promise<void> {
})
)

.then(() =>
knex.schema.createTable('document', (table) => {
table.uuid('documentId').primary();
table
.uuid('submissionId')
.notNullable()
.references('submissionId')
.inTable('submission')
.onUpdate('CASCADE')
.onDelete('CASCADE');
table.text('filename').notNullable();
table.text('mimeType').defaultTo('application/octet-stream').notNullable();
table.bigInteger('filesize').notNullable();
stamps(knex, table);
table.unique(['documentId', 'submissionId']);
})
)

// Create audit schema and logged_actions table
.then(() => knex.schema.raw('CREATE SCHEMA IF NOT EXISTS audit'))

Expand Down Expand Up @@ -147,6 +165,12 @@ export async function up(knex: Knex): Promise<void> {
FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)
)

.then(() =>
knex.schema.raw(`CREATE TRIGGER audit_document_trigger
AFTER UPDATE OR DELETE ON document
FOR EACH ROW EXECUTE PROCEDURE audit.if_modified_func();`)
)

// Populate Baseline Data
.then(() => {
const users = ['system'];
Expand All @@ -165,6 +189,7 @@ export async function down(knex: Knex): Promise<void> {
return (
Promise.resolve()
// Drop audit triggers
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_document_trigger ON document'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_submission_trigger ON submission'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_user_trigger ON "user"'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS audit_identity_provider_trigger ON identity_provider'))
Expand All @@ -173,6 +198,7 @@ export async function down(knex: Knex): Promise<void> {
.then(() => knex.schema.withSchema('audit').dropTableIfExists('logged_actions'))
.then(() => knex.schema.dropSchemaIfExists('audit'))
// Drop public schema COMS tables
.then(() => knex.schema.dropTableIfExists('document'))
.then(() => knex.schema.dropTableIfExists('submission'))
.then(() => knex.schema.dropTableIfExists('user'))
.then(() => knex.schema.dropTableIfExists('identity_provider'))
Expand Down
50 changes: 50 additions & 0 deletions app/src/db/models/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Prisma } from '@prisma/client';
import disconnectRelation from '../utils/disconnectRelation';

import type { IStamps } from '../../interfaces/IStamps';
import type { Document } from '../../types';

// Define types
const _document = Prisma.validator<Prisma.documentDefaultArgs>()({});
const _documentWithGraph = Prisma.validator<Prisma.documentDefaultArgs>()({});

type SubmissionRelation = {
submission:
| {
connect: {
submissionId: string;
};
}
| {
disconnect: boolean;
};
};

type PrismaRelationDocument = Omit<Prisma.documentGetPayload<typeof _document>, 'submissionId' | keyof IStamps> &
SubmissionRelation;

type PrismaGraphDocument = Prisma.documentGetPayload<typeof _documentWithGraph>;

export default {
toPrismaModel(input: Document): PrismaRelationDocument {
return {
documentId: input.documentId as string,
filename: input.filename,
mimeType: input.mimeType,
filesize: BigInt(input.filesize),
submission: input.submissionId ? { connect: { submissionId: input.submissionId } } : disconnectRelation
};
},

fromPrismaModel(input: PrismaGraphDocument | null): Document | null {
if (!input) return null;

return {
documentId: input.documentId,
filename: input.filename,
mimeType: input.mimeType,
filesize: Number(input.filesize),
submissionId: input.submissionId as string
};
}
};
6 changes: 3 additions & 3 deletions app/src/db/models/identity_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import type { IdentityProvider } from '../../types';

// Define a type
const _identityProvider = Prisma.validator<Prisma.identity_providerDefaultArgs>()({});
type DBIdentityProvider = Omit<Prisma.identity_providerGetPayload<typeof _identityProvider>, keyof IStamps>;
type PrismaRelationIdentityProvider = Omit<Prisma.identity_providerGetPayload<typeof _identityProvider>, keyof IStamps>;

export default {
toDBModel(input: IdentityProvider): DBIdentityProvider {
toPrismaModel(input: IdentityProvider): PrismaRelationIdentityProvider {
return {
idp: input.idp,
active: input.active
};
},

fromDBModel(input: DBIdentityProvider | null): IdentityProvider | null {
fromPrismaModel(input: PrismaRelationIdentityProvider | null): IdentityProvider | null {
if (!input) return null;

return {
Expand Down
1 change: 1 addition & 0 deletions app/src/db/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as document } from './document';
export { default as identity_provider } from './identity_provider';
export { default as submission } from './submission';
export { default as user } from './user';
13 changes: 8 additions & 5 deletions app/src/db/models/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ type UserRelation = {
disconnect: boolean;
};
};
type DBSubmission = Omit<Prisma.submissionGetPayload<typeof _submission>, 'assignedToUserId' | keyof IStamps> &
type PrismaRelationSubmission = Omit<
Prisma.submissionGetPayload<typeof _submission>,
'assignedToUserId' | keyof IStamps
> &
UserRelation;

type Submission = Prisma.submissionGetPayload<typeof _submissionWithRelations>;
type PrismaGraphSubmission = Prisma.submissionGetPayload<typeof _submissionWithRelations>;

export default {
toDBModel(input: ChefsSubmissionForm): DBSubmission {
toPrismaModel(input: ChefsSubmissionForm): PrismaRelationSubmission {
return {
submissionId: input.submissionId,
confirmationId: input.confirmationId,
Expand Down Expand Up @@ -65,7 +68,7 @@ export default {
};
},

fromDBModel(input: Submission | null): ChefsSubmissionForm | null {
fromPrismaModel(input: PrismaGraphSubmission | null): ChefsSubmissionForm | null {
if (!input) return null;

return {
Expand Down Expand Up @@ -98,7 +101,7 @@ export default {
waitingOn: input.waitingOn,
bringForwardDate: input.bringForwardDate?.toISOString() ?? null,
notes: input.notes,
user: user.fromDBModel(input.user),
user: user.fromPrismaModel(input.user),
intakeStatus: input.intakeStatus,
applicationStatus: input.applicationStatus
};
Expand Down
9 changes: 5 additions & 4 deletions app/src/db/models/user.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Prisma } from '@prisma/client';

import type { IStamps } from '../../interfaces/IStamps';
import type { User } from '../../types';
import type { User } from '../../types/User';

// Define types
const _user = Prisma.validator<Prisma.userDefaultArgs>()({});
type DBUser = Omit<Prisma.userGetPayload<typeof _user>, keyof IStamps>;

type PrismaRelationUser = Omit<Prisma.userGetPayload<typeof _user>, keyof IStamps>;

export default {
toDBModel(input: User): DBUser {
toPrismaModel(input: User): PrismaRelationUser {
return {
userId: input.userId as string,
identityId: input.identityId,
Expand All @@ -22,7 +23,7 @@ export default {
};
},

fromDBModel(input: DBUser | null): User | null {
fromPrismaModel(input: PrismaRelationUser | null): User | null {
if (!input) return null;

return {
Expand Down
32 changes: 24 additions & 8 deletions app/src/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ model knex_migrations_lock {
}

model submission {
submissionId String @id @db.Uuid
assignedToUserId String? @db.Uuid
submissionId String @id @db.Uuid
assignedToUserId String? @db.Uuid
confirmationId String
submittedAt DateTime @db.Timestamptz(6)
submittedAt DateTime @db.Timestamptz(6)
submittedBy String
locationPIDs String?
contactName String?
Expand All @@ -58,15 +58,16 @@ model submission {
financiallySupportedNonProfit Boolean?
financiallySupportedHousingCoop Boolean?
waitingOn String?
bringForwardDate DateTime? @db.Timestamptz(6)
bringForwardDate DateTime? @db.Timestamptz(6)
notes String?
intakeStatus String?
applicationStatus String?
createdBy String? @default("00000000-0000-0000-0000-000000000000")
createdAt DateTime? @default(now()) @db.Timestamptz(6)
createdBy String? @default("00000000-0000-0000-0000-000000000000")
createdAt DateTime? @default(now()) @db.Timestamptz(6)
updatedBy String?
updatedAt DateTime? @db.Timestamptz(6)
user user? @relation(fields: [assignedToUserId], references: [userId], onDelete: Cascade, map: "submission_assignedtouserid_foreign")
updatedAt DateTime? @db.Timestamptz(6)
document document[]
user user? @relation(fields: [assignedToUserId], references: [userId], onDelete: Cascade, map: "submission_assignedtouserid_foreign")
}

model user {
Expand All @@ -90,3 +91,18 @@ model user {
@@index([identityId], map: "user_identityid_index")
@@index([username], map: "user_username_index")
}

model document {
documentId String @id @db.Uuid
submissionId String @db.Uuid
filename String
mimeType String @default("application/octet-stream")
filesize BigInt
createdBy String? @default("00000000-0000-0000-0000-000000000000")
createdAt DateTime? @default(now()) @db.Timestamptz(6)
updatedBy String?
updatedAt DateTime? @db.Timestamptz(6)
submission submission @relation(fields: [submissionId], references: [submissionId], onDelete: Cascade, map: "document_submissionid_foreign")
@@unique([documentId, submissionId], map: "document_documentid_submissionid_unique")
}
18 changes: 18 additions & 0 deletions app/src/routes/v1/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import express from 'express';
import { documentController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';

import type { NextFunction, Request, Response } from '../../interfaces/IExpress';

const router = express.Router();
router.use(requireSomeAuth);

router.put('/', (req: Request, res: Response, next: NextFunction): void => {
documentController.createDocument(req, res, next);
});

router.get('/list/:submissionId', (req: Request, res: Response, next: NextFunction): void => {
documentController.listDocuments(req, res, next);
});

export default router;
5 changes: 3 additions & 2 deletions app/src/routes/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { currentUser } from '../../middleware/authentication';
import express from 'express';
import chefs from './chefs';
import document from './document';
import user from './user';

const router = express.Router();
Expand All @@ -9,12 +10,12 @@ router.use(currentUser);
// Base v1 Responder
router.get('/', (_req, res) => {
res.status(200).json({
endpoints: ['/chefs', '/user']
endpoints: ['/chefs', '/document', '/user']
});
});

/** CHEFS Router */
router.use('/chefs', chefs);
router.use('/document', document);
router.use('/user', user);

export default router;
Loading

0 comments on commit ca181e3

Please sign in to comment.