Skip to content

Commit

Permalink
Merge pull request #26 from bcgov/bug/update_stamps
Browse files Browse the repository at this point in the history
Resolve issue setting updatedBy/At on update
  • Loading branch information
TimCsaky authored Feb 7, 2024
2 parents 5b3028a + 25a0798 commit 389086a
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 27 deletions.
37 changes: 36 additions & 1 deletion app/src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import config from 'config';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

import { AuthType } from './constants';
import { getLogger } from './log';

import type { ChefsFormConfig, ChefsFormConfigData } from '../types';
import type { JwtPayload } from 'jsonwebtoken';
import type { ChefsFormConfig, ChefsFormConfigData, CurrentUser } from '../types';

const log = getLogger(module.filename);

Expand Down Expand Up @@ -68,6 +70,39 @@ export function getGitRevision(): string {
}
}

/**
* @function getCurrentIdentity
* Attempts to acquire current identity value.
* Always takes first non-default value available. Yields `defaultValue` otherwise.
* @param {object} currentUser The express request currentUser object
* @param {string} [defaultValue=undefined] An optional default return value
* @returns {string} The current user identifier if applicable, or `defaultValue`
*/
export function getCurrentIdentity(currentUser: CurrentUser | undefined, defaultValue: string | undefined = undefined) {
return parseIdentityKeyClaims()
.map((claim) => getCurrentTokenClaim(currentUser, claim, undefined))
.filter((value) => value) // Drop falsy values from array
.concat(defaultValue)[0]; // Add defaultValue as last element of array
}

/**
* @function getCurrentTokenClaim
* Attempts to acquire a specific current token claim. Yields `defaultValue` otherwise
* @param {object} currentUser The express request currentUser object
* @param {string} claim The requested token claim
* @param {string} [defaultValue=undefined] An optional default return value
* @returns {object} The requested current token claim if applicable, or `defaultValue`
*/
export function getCurrentTokenClaim(
currentUser: CurrentUser | undefined,
claim: string,
defaultValue: string | undefined = undefined
) {
return currentUser && currentUser.authType === AuthType.BEARER
? (currentUser.tokenPayload as JwtPayload)[claim]
: defaultValue;
}

/**
* @function isTruthy
* Returns true if the element name in the object contains a truthy value
Expand Down
8 changes: 5 additions & 3 deletions app/src/controllers/chefs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import config from 'config';
import { NIL } from 'uuid';

import { chefsService } from '../services';
import { addDashesToUuid, isTruthy } from '../components/utils';
import { chefsService, userService } from '../services';
import { addDashesToUuid, getCurrentIdentity, isTruthy } from '../components/utils';
import { IdentityProvider } from '../components/constants';

import type { JwtPayload } from 'jsonwebtoken';
Expand Down Expand Up @@ -112,7 +113,8 @@ const controller = {

updateSubmission: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await chefsService.updateSubmission(req.body);
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL);
const response = await chefsService.updateSubmission({ ...(req.body as ChefsSubmissionForm), updatedBy: userId });
res.status(200).send(response);
} catch (e: unknown) {
next(e);
Expand Down
9 changes: 7 additions & 2 deletions app/src/controllers/permit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { permitService } from '../services';
import { NIL } from 'uuid';

import { permitService, userService } from '../services';
import { getCurrentIdentity } from '../components/utils';

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

const controller = {
createPermit: async (req: Request, res: Response, next: NextFunction) => {
Expand Down Expand Up @@ -41,7 +45,8 @@ const controller = {

updatePermit: async (req: Request, res: Response, next: NextFunction) => {
try {
const response = await permitService.updatePermit(req.body);
const userId = await userService.getCurrentUserId(getCurrentIdentity(req.currentUser, NIL), NIL);
const response = await permitService.updatePermit({ ...(req.body as Permit), updatedBy: userId });
res.status(200).send(response);
} catch (e: unknown) {
next(e);
Expand Down
92 changes: 76 additions & 16 deletions app/src/db/migrations/20231212000000_init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ import type { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return (
Promise.resolve()
// Create public schema triggers
.then(() =>
knex.schema.raw(`create or replace function set_updatedAt()
returns trigger
language plpgsql
as $$
begin
new."updatedAt" = now();
return new;
end;
$$`)
)

// Create public schema tables
.then(() =>
knex.schema.createTable('identity_provider', (table) => {
Expand All @@ -16,6 +29,11 @@ export async function up(knex: Knex): Promise<void> {
stamps(knex, table);
})
)
.then(() =>
knex.schema.raw(`create trigger before_update_identity_provider_trigger
before update on public.identity_provider
for each row execute procedure public.set_updatedAt();`)
)

.then(() =>
knex.schema.createTable('user', (table) => {
Expand All @@ -31,6 +49,11 @@ export async function up(knex: Knex): Promise<void> {
stamps(knex, table);
})
)
.then(() =>
knex.schema.raw(`create trigger before_update_user_trigger
before update on "user"
for each row execute procedure public.set_updatedAt();`)
)

.then(() =>
knex.schema.createTable('submission', (table) => {
Expand Down Expand Up @@ -76,6 +99,11 @@ export async function up(knex: Knex): Promise<void> {
stamps(knex, table);
})
)
.then(() =>
knex.schema.raw(`create trigger before_update_submission_trigger
before update on public.submission
for each row execute procedure public.set_updatedAt();`)
)

.then(() =>
knex.schema.createTable('document', (table) => {
Expand All @@ -94,6 +122,11 @@ export async function up(knex: Knex): Promise<void> {
table.unique(['documentId', 'submissionId']);
})
)
.then(() =>
knex.schema.raw(`create trigger before_update_document_trigger
before update on public.document
for each row execute procedure public.set_updatedAt();`)
)

.then(() =>
knex.schema.createTable('permit_type', (table) => {
Expand All @@ -113,6 +146,11 @@ export async function up(knex: Knex): Promise<void> {
stamps(knex, table);
})
)
.then(() =>
knex.schema.raw(`create trigger before_update_permit_type_trigger
before update on public.permit_type
for each row execute procedure public.set_updatedAt();`)
)

.then(() =>
knex.schema.createTable('permit', (table) => {
Expand Down Expand Up @@ -142,24 +180,18 @@ export async function up(knex: Knex): Promise<void> {
table.unique(['permitId', 'permitTypeId', 'submissionId']);
})
)

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

.then(() =>
knex.schema.withSchema('audit').createTable('logged_actions', (table) => {
table.specificType('id', 'integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY');
table.text('schemaName').notNullable().index();
table.text('tableName').notNullable().index();
table.text('dbUser').notNullable();
table.text('updatedByUsername');
table.timestamp('actionTimestamp', { useTz: true }).defaultTo(knex.fn.now()).index();
table.text('action').notNullable().index();
table.json('originalData');
table.json('newData');
})
knex.schema.raw(`create trigger before_insert_permit_trigger
before insert on public.permit
for each row execute procedure public.set_updatedAt();`)
)
.then(() =>
knex.schema.raw(`create trigger before_update_permit_trigger
before update on public.permit
for each row execute procedure public.set_updatedAt();`)
)

// Create public schema functions
.then(() =>
knex.schema.raw(`create or replace function public.get_activity_statistics(
date_from text,
Expand Down Expand Up @@ -224,6 +256,23 @@ export async function up(knex: Knex): Promise<void> {
end; $$`)
)

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

.then(() =>
knex.schema.withSchema('audit').createTable('logged_actions', (table) => {
table.specificType('id', 'integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY');
table.text('schemaName').notNullable().index();
table.text('tableName').notNullable().index();
table.text('dbUser').notNullable();
table.text('updatedByUsername');
table.timestamp('actionTimestamp', { useTz: true }).defaultTo(knex.fn.now()).index();
table.text('action').notNullable().index();
table.json('originalData');
table.json('newData');
})
)

.then(() =>
knex.schema.raw(`CREATE OR REPLACE FUNCTION audit.if_modified_func() RETURNS trigger AS $body$
DECLARE
Expand Down Expand Up @@ -576,12 +625,23 @@ export async function down(knex: Knex): Promise<void> {
.then(() => knex.schema.dropSchemaIfExists('audit'))
// Drop public schema functions
.then(() => knex.schema.raw('DROP FUNCTION IF EXISTS public.get_activity_statistics'))
// Drop public schema PCNS tables
// Drop public schema tables and triggers
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_update_permit_trigger ON permit'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_insert_permit_trigger ON permit'))
.then(() => knex.schema.dropTableIfExists('permit'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_update_permit_type_trigger ON permit_type'))
.then(() => knex.schema.dropTableIfExists('permit_type'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_update_document_trigger ON document'))
.then(() => knex.schema.dropTableIfExists('document'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_update_submission_trigger ON submission'))
.then(() => knex.schema.dropTableIfExists('submission'))
.then(() => knex.schema.raw('DROP TRIGGER IF EXISTS before_update_user_trigger ON "user"'))
.then(() => knex.schema.dropTableIfExists('user'))
.then(() =>
knex.schema.raw('DROP TRIGGER IF EXISTS before_update_identity_provider_trigger ON identity_provider')
)
.then(() => knex.schema.dropTableIfExists('identity_provider'))
// Drop public schema triggers
.then(() => knex.schema.raw('DROP FUNCTION IF EXISTS public.set_updatedAt'))
);
}
2 changes: 1 addition & 1 deletion app/src/services/chefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ const service = {
updateSubmission: async (data: ChefsSubmissionForm) => {
try {
await prisma.submission.update({
data: submission.toPrismaModel(data),
data: { ...submission.toPrismaModel(data), updatedBy: data.updatedBy },
where: {
submissionId: data.submissionId
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/services/permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const service = {
updatePermit: async (data: Permit) => {
try {
await prisma.permit.update({
data: permit.toPrismaModel(data),
data: { ...permit.toPrismaModel(data), updatedBy: data.updatedBy },
where: {
permitId: data.permitId
}
Expand Down
6 changes: 3 additions & 3 deletions app/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const service = {
* @param {string} [defaultValue=undefined] An optional default return value
* @returns {string} The current userId if applicable, or `defaultValue`
*/
getCurrentUserId: async (identityId: string, defaultValue = undefined) => {
getCurrentUserId: async (identityId: string, defaultValue: string | undefined = undefined) => {
// TODO: Consider conditionally skipping when identityId is undefined?
const user = await prisma.user.findFirst({
where: {
Expand Down Expand Up @@ -296,12 +296,12 @@ const service = {
lastName: data.lastName,
idp: data.idp,
active: data.active,
updatedBy: data.userId
updatedBy: data.updatedBy
};

// TODO: Add support for updating userId primary key in the event it changes
response = await trx?.user.update({
data: user.toPrismaModel(obj),
data: { ...user.toPrismaModel(obj), updatedBy: obj.updatedBy },
where: {
userId: userId
}
Expand Down

0 comments on commit 389086a

Please sign in to comment.