Skip to content

Commit 1c59400

Browse files
committed
feat: centralize code table management and enum generation
Refactors how database lookup code tables are cached, generated, and consumed. This eliminates manual code table arrays by dynamically building memory caches and types from Prisma metadata models ending in `_code`. - Relocate and refactor code enum generation script to `src/db/codes/generate.ts` - Implement dynamic cache manager in `src/db/codes/cache.ts` using a `Proxy` mapping - Dynamically fetch active lookup tables in `services/code.ts` via Prisma DMMF models - Update `peachSync.ts` to initialize caches prior to executing the synchronization loop - Replace hardcoded constant validation arrays in endpoints and Joi schemas with live cache lookups - Update unit tests and Jest deep mocks to support the central code cache system Signed-off-by: qhanson55 <quinn.hanson@gov.bc.ca>
1 parent 9099137 commit 1c59400

36 files changed

Lines changed: 316 additions & 312 deletions

app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"clean:all": "tsx ./deploy-utils.ts clean",
1313
"clean": "rimraf coverage dist sbin",
1414
"format": "prettier ./src --write",
15-
"prisma:enums": "tsx ./src/db/utils/generate_enums.ts",
15+
"prisma:enums": "tsx ./src/db/codes/generate.ts",
1616
"knex:local": "tsx ./node_modules/knex/bin/cli.js",
1717
"lint": "eslint",
1818
"lint:fix": "eslint --fix",

app/peachSync.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { syncPeachRecords } from './src/controllers/peach.ts';
22
import { sendPermitUpdateNotifications } from './src/controllers/permit.ts';
33
import { getLogger } from './src/utils/log.ts';
44
import { state } from './state.ts';
5+
import { refreshCodeCaches } from './src/db/codes/cache.ts';
56

67
import type { Permit } from './src/types';
78

@@ -38,4 +39,12 @@ async function syncPeachToPcns() {
3839
}
3940
}
4041

41-
void syncPeachToPcns();
42+
async function main() {
43+
await refreshCodeCaches();
44+
await syncPeachToPcns();
45+
}
46+
47+
void main().catch((err) => {
48+
log.error('Fatal error in PEACH sync', err);
49+
process.exit(1);
50+
});

app/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createServer } from 'node:http';
66

77
import app from './app.ts';
88
import { checkDatabaseHealth, checkDatabaseSchema } from './src/db/utils/utils.ts';
9-
import { refreshCodeCaches } from './src/utils/cache/codes.ts';
9+
import { refreshCodeCaches } from './src/db/codes/cache.ts';
1010
import getLogger from './src/utils/log.ts';
1111
import { state } from './state.ts';
1212

app/src/controllers/generalProject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import config from 'config';
22
import { Prisma } from '@prisma/client';
33
import { v4 as uuidv4 } from 'uuid';
44

5-
import { PermitStage, PermitState } from '../db/utils/codeEnums.ts';
5+
import { PermitStage, PermitState } from '../db/codes/enums.ts';
66
import { transactionWrapper } from '../db/utils/transactionWrapper.ts';
77
import {
88
generateCreateStamps,

app/src/controllers/housingProject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import config from 'config';
22
import { Prisma } from '@prisma/client';
33
import { v4 as uuidv4 } from 'uuid';
44

5-
import { PermitStage, PermitState } from '../db/utils/codeEnums.ts';
5+
import { PermitStage, PermitState } from '../db/codes/enums.ts';
66
import { transactionWrapper } from '../db/utils/transactionWrapper.ts';
77
import {
88
generateCreateStamps,

app/src/controllers/peach.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PermitStage, PermitState } from '../db/utils/codeEnums.ts';
1+
import { PermitStage, PermitState } from '../db/codes/enums.ts';
22
import { transactionWrapper } from '../db/utils/transactionWrapper.ts';
33
import { generateUpdateStamps } from '../db/utils/utils.ts';
44
import { parsePeachRecords, summarizePeachRecord } from '../parsers/peach.ts';

app/src/controllers/permit.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import config from 'config';
22
import { v4 as uuidv4 } from 'uuid';
33

44
import { findPriorityPermitTracking } from './peach.ts';
5-
import { PermitStage } from '../db/utils/codeEnums.ts';
5+
import { PermitStage } from '../db/codes/enums.ts';
66
import { transactionWrapper } from '../db/utils/transactionWrapper.ts';
77
import { generateCreateStamps, generateUpdateStamps } from '../db/utils/utils.ts';
88
import { summarizePeachRecord } from '../parsers/peach.ts';
@@ -44,6 +44,20 @@ import type {
4444
SearchPermitsOptions,
4545
SourceSystemKind
4646
} from '../types/index.ts';
47+
import { codeTable } from '../db/codes/cache.ts';
48+
49+
function checkIfPeachIntegratedAuthType(sourceSystem: string, sourceSystemKinds: SourceSystemKind[]): boolean {
50+
const sourceSystemKind = sourceSystemKinds.find((ssk) => ssk.integrated && ssk.sourceSystem === sourceSystem);
51+
return !!sourceSystemKind;
52+
}
53+
54+
const snapshotPermitStatus = (p: Partial<Permit>) => ({
55+
state: p.state,
56+
stage: p.stage,
57+
decisionDate: p.decisionDate,
58+
submittedDate: p.submittedDate,
59+
statusLastChanged: p.statusLastChanged
60+
});
4761

4862
export const deletePermitController = async (req: Request<{ permitId: string }>, res: Response) => {
4963
await transactionWrapper<void>(async (tx: PrismaTransactionClient) => {
@@ -187,18 +201,26 @@ export const sendPermitUpdateNotifications = async (
187201
});
188202
}
189203

204+
const stateDisplay = codeTable.PermitState.displays[permit.state];
205+
const stageDisplay = codeTable.PermitStage.displays[permit.stage];
206+
207+
if (!stateDisplay || !stageDisplay) {
208+
throw new Error(`Invalid permit.state: ${permit.state} or permit.stage: ${permit.stage}`);
209+
}
210+
190211
// Create update note for status change
191212
const permitNote = await createPermitNote(tx, {
192213
permitNoteId: uuidv4(),
193214
permitId: permit.permitId,
194215
note:
195-
note ?? `This application is ${permit.state.toLocaleLowerCase()} in the ${permit.stage.toLocaleLowerCase()}.`,
216+
note ?? `This application is ${stateDisplay.toLocaleLowerCase()} in the ${stageDisplay.toLocaleLowerCase()}.`,
196217
...generateCreateStamps(undefined),
197218
updatedBy: null,
198219
updatedAt: null,
199220
deletedBy: null,
200221
deletedAt: null
201222
});
223+
202224
// Add proponent update email to email jobs
203225
const primaryContact = project?.activity?.activityContact?.find(
204226
(ac) => ac.role === ActivityContactRole.PRIMARY
@@ -210,7 +232,7 @@ export const sendPermitUpdateNotifications = async (
210232
const isOnlyTemplate = permitNote.note.trim() === peachUpdateNotePlaceholder;
211233
const isFirstNote = !permit?.permitNote?.length;
212234

213-
const usePeachTemplate = isOnlyTemplate && isFirstNote && state.features.peach;
235+
const useInitialPeachTemplate = isOnlyTemplate && isFirstNote && state.features.peach;
214236

215237
if (
216238
project.projectId &&
@@ -223,7 +245,7 @@ export const sendPermitUpdateNotifications = async (
223245
dearName: primaryContact?.firstName ?? '',
224246
projectId: project.projectId,
225247
toEmails: [primaryContact.email],
226-
emailTemplate: usePeachTemplate ? initialPeachPermitUpdateTemplate : permitNoteUpdateTemplate
248+
emailTemplate: useInitialPeachTemplate ? initialPeachPermitUpdateTemplate : permitNoteUpdateTemplate
227249
});
228250
}
229251
};
@@ -240,19 +262,6 @@ export const sendPermitUpdateNotifications = async (
240262
}
241263
};
242264

243-
function checkIfPeachIntegratedAuthType(sourceSystem: string, sourceSystemKinds: SourceSystemKind[]): boolean {
244-
const sourceSystemKind = sourceSystemKinds.find((ssk) => ssk.integrated && ssk.sourceSystem === sourceSystem);
245-
return !!sourceSystemKind;
246-
}
247-
248-
const snapshotPermitStatus = (p: Partial<Permit>) => ({
249-
state: p.state,
250-
stage: p.stage,
251-
decisionDate: p.decisionDate,
252-
submittedDate: p.submittedDate,
253-
statusLastChanged: p.statusLastChanged
254-
});
255-
256265
export const upsertPermitController = async (req: Request<never, never, Permit>, res: Response) => {
257266
const response = await transactionWrapper<Permit>(async (tx: PrismaTransactionClient) => {
258267
const createStamps = generateCreateStamps(req.currentContext);

app/src/controllers/roadmap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { v4 as uuidv4 } from 'uuid';
22

3-
import { PermitStage } from '../db/utils/codeEnums.ts';
3+
import { PermitStage } from '../db/codes/enums.ts';
44
import { transactionWrapper } from '../db/utils/transactionWrapper';
55
import { generateCreateStamps, generateNullDeleteStamps, generateNullUpdateStamps } from '../db/utils/utils';
66
import { getObject } from '../services/coms';

app/src/db/codes/cache.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { transactionWrapper } from '../utils/transactionWrapper.ts';
2+
import { listAllCodeTables } from '../../services/code.ts';
3+
import { getLogger } from '../../utils/log.ts';
4+
import { CODE_TABLES } from './tables.ts';
5+
6+
import type { CodeTableName } from './types.ts';
7+
import type { PrismaTransactionClient } from '../dataConnection.ts';
8+
type CodeTablesResult = Awaited<ReturnType<typeof listAllCodeTables>>;
9+
10+
type CachedCodeTable = Readonly<{
11+
codes: readonly string[];
12+
displays: Readonly<Record<string, string>>;
13+
definitions: Readonly<Record<string, string>>;
14+
}>;
15+
16+
type CodeCache = Readonly<Record<CodeTableName, CachedCodeTable>>;
17+
18+
const toCachedCodeTable = (
19+
rows: { code: string; display: string; definition: string | null }[] = []
20+
): CachedCodeTable => ({
21+
codes: rows.map((r) => r.code),
22+
displays: Object.fromEntries(rows.map((r) => [r.code, r.display])),
23+
definitions: Object.fromEntries(
24+
rows.filter((r): r is typeof r & { definition: string } => r.definition !== null).map((r) => [r.code, r.definition])
25+
)
26+
});
27+
28+
const buildCodeCache = (codeTables?: CodeTablesResult): CodeCache =>
29+
Object.fromEntries(
30+
CODE_TABLES.map(({ name }) => [name, toCachedCodeTable(codeTables?.[name as CodeTableName])])
31+
) as CodeCache;
32+
33+
let codeCache: CodeCache = buildCodeCache();
34+
35+
export const codeTable: CodeCache = new Proxy({} as Record<CodeTableName, CachedCodeTable>, {
36+
get: (_target, prop: string) => codeCache[prop as CodeTableName]
37+
});
38+
39+
const log = getLogger(module.filename);
40+
41+
/**
42+
* Pull the currently‑active codes from the DB and refresh
43+
* the in‑memory arrays.
44+
* @returns true if succesful, false otherwise
45+
*/
46+
export async function refreshCodeCaches(): Promise<boolean> {
47+
try {
48+
const codeTables: CodeTablesResult = await transactionWrapper(async (tx: PrismaTransactionClient) => {
49+
return await listAllCodeTables(tx);
50+
});
51+
52+
codeCache = buildCodeCache(codeTables);
53+
54+
log.debug('Codes cache refreshed');
55+
return true;
56+
} catch (error) {
57+
log.error('Codes cache refresh failed', error);
58+
return false;
59+
}
60+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* AUTO-GENERATED FILE - DO NOT EDIT
3-
* @see app/src/db/utils/generate_enums.ts
3+
* @see app/src/db/codes/generate.ts
44
*
55
* To update this file when updating or adding code tables to the db, run:
66
* `npm run prisma:enums`

0 commit comments

Comments
 (0)