Skip to content

Commit 8abb3d8

Browse files
committed
feat: centralize code table management and enum generation
Refactors database lookup code tables to dynamically build caches and types directly from Prisma metadata. - Move enum generation script to `src/db/codes/generate.ts` - Implement dynamic cache manager in `src/db/codes/cache.ts` with a fail-fast Proxy to catch type sync errors - Fetch active tables dynamically via Prisma DMMF models - Initialize caches in `peachSync.ts` prior to sync loop - Replace static Joi `.valid()` calls with a `.custom()` Proxy to ensure code tables evaluate at runtime - Update unit tests and Jest mocks to support the central cache system Signed-off-by: qhanson55 <quinn.hanson@gov.bc.ca>
1 parent 35b3ac4 commit 8abb3d8

39 files changed

Lines changed: 531 additions & 333 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: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { syncPeachRecords } from './src/controllers/peach.ts';
22
import { sendPermitUpdateNotifications } from './src/controllers/permit.ts';
3+
import { refreshCodeCaches } from './src/db/codes/cache.ts';
34
import { getLogger } from './src/utils/log.ts';
45
import { state } from './state.ts';
56

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

41-
void syncPeachToPcns();
42+
async function main() {
43+
const cacheRefreshSucceeded = await refreshCodeCaches();
44+
if (!cacheRefreshSucceeded) {
45+
log.error('Fatal error in PEACH sync: failed to refresh code caches');
46+
process.exitCode = 1;
47+
return;
48+
}
49+
await syncPeachToPcns();
50+
}
51+
52+
void main().catch((error) => {
53+
log.error('Fatal error in PEACH sync', error);
54+
process.exit(1);
55+
});

app/server.ts

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

77
import app from './app.ts';
8+
import { refreshCodeCaches } from './src/db/codes/cache.ts';
89
import { checkDatabaseHealth, checkDatabaseSchema } from './src/db/utils/utils.ts';
9-
import { refreshCodeCaches } from './src/utils/cache/codes.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 { CachedCodeTable, CodeCache, CodeTableName, CodeTablesResult } from './types.ts';
7+
import type { PrismaTransactionClient } from '../dataConnection.ts';
8+
9+
const log = getLogger(module.filename);
10+
11+
const toCachedCodeTable = (
12+
rows: { code: string; display: string; definition: string | null }[] = []
13+
): CachedCodeTable => ({
14+
codes: rows.map((r) => r.code),
15+
displays: Object.fromEntries(rows.map((r) => [r.code, r.display])),
16+
definitions: Object.fromEntries(
17+
rows.filter((r): r is typeof r & { definition: string } => r.definition !== null).map((r) => [r.code, r.definition])
18+
)
19+
});
20+
21+
const buildCodeCache = (codeTables?: CodeTablesResult): CodeCache =>
22+
Object.fromEntries(
23+
CODE_TABLES.map(({ name }) => [name, toCachedCodeTable(codeTables?.[name as CodeTableName])])
24+
) as CodeCache;
25+
26+
let codeCache: CodeCache = buildCodeCache();
27+
28+
export const codeTable: CodeCache = new Proxy({} as Record<CodeTableName, CachedCodeTable>, {
29+
get: (_target, prop: string) => {
30+
const cachedTable = codeCache[prop as CodeTableName];
31+
32+
if (!cachedTable) {
33+
throw new Error(
34+
`Code table '${prop}' is missing from the cache. Run 'npm run prisma:enums' to ensure correct code enums.`
35+
);
36+
}
37+
38+
return cachedTable;
39+
}
40+
});
41+
42+
/**
43+
* Pull the currently‑active codes from the DB and refresh the in‑memory arrays.
44+
* @returns true if successful, 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)