Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/apps/web/src/server/static/scripts/*
/apps/web/src/server/static/styles/*
/apps/functions/migration/database/db-client/*
/apps/api/src/database/client/*
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,6 @@ build
**/.terraform/*
**/.terraform
**/.terragrunt-cache

# Prisma
apps/api/src/database/client
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22.22.0
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ coverage
apps/e2e/cypress/fixtures/*
.vscode
.build
.turbo
apps/api/src/database/client
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Planning Inspectorate Back Office

This is the Planning Inspectorate Back Office monorepo that contains all the apps for running the back office.
This is the Planning Inspectorate Back Office monorepo that contains all the apps for running the back office.

The back office system contains the applications back office features, including a JSON API which retrieves data from a database, and a web front-end (utilising [server-side rendering](https://web.dev/rendering-on-the-web/#server-rendering)). There are also some [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-overview) for background tasks (such as virus scans), [Azure Blob Storage](https://azure.microsoft.com/en-gb/products/storage/blobs) is used for documents, and [Azure Service Bus](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview) for integration.

Expand All @@ -23,12 +23,12 @@ Most of the apps are built with [Express.js](https://expressjs.com/), and the fr

The current node version can be found in the package.json at the root of the project. It is recommended to use a node version manager, such as [nvm](https://github.com/nvm-sh/nvm#installing-and-updating).

Example node installation, using `nvm`:
Example node installation, using `nvm`:

```
nvm install 20.11.1
nvm use 20.11.1
nvm alias default 20
nvm install 22
nvm use 22
nvm alias default 22
```

### Database Setup
Expand Down Expand Up @@ -362,8 +362,8 @@ if (featureFlagClient.isFeatureActive('<feature-flag-name>')) {
```

## Dependencies
The repo is currently utilising the [NPM Workspaces feature](https://docs.npmjs.com/cli/v8/using-npm/workspaces). This allows us to have a single root node_modules that holds all the project dependencies and a root package.json + package-lock.json that has every dependency + version that's used in the repository listed in it.
Every app/function/package (known as workspaces) in the repo also has a package.json file where their dependency list has only the dependencies that the app/function/package in question requires and their versions are denoted as `*` which implies that they're relying on the root package.json for their versioning - this helps us keep versioning consistent across the repo.
The repo is currently utilising the [NPM Workspaces feature](https://docs.npmjs.com/cli/v8/using-npm/workspaces). This allows us to have a single root node_modules that holds all the project dependencies and a root package.json + package-lock.json that has every dependency + version that's used in the repository listed in it.
Every app/function/package (known as workspaces) in the repo also has a package.json file where their dependency list has only the dependencies that the app/function/package in question requires and their versions are denoted as `*` which implies that they're relying on the root package.json for their versioning - this helps us keep versioning consistent across the repo.

**First time installing dependencies**:
- run `npm ci` from the root of the project (this will use the project's package-lock.json file to sort your local node_modules directory and will avoid creating package-lock.json diffs where they're not expected).
Expand Down
4 changes: 2 additions & 2 deletions apps/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-alpine3.20
FROM node:22-alpine3.22
WORKDIR /src/app

ARG GIT_SHA
Expand All @@ -17,7 +17,7 @@ ENV DATABASE_URL=$DATABASE_URL
ENV GIT_SHA=$GIT_SHA

WORKDIR /src/app/apps/api
RUN npx prisma generate
RUN npm run prisma-generate

EXPOSE 3000

Expand Down
1 change: 1 addition & 0 deletions apps/api/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"paths": {
"#app-test": ["./src/server/app-test.js"],
"#api-constants": ["./src/server/applications/constants.js"],
"#database-client": ["./src/database/client/client.js"],
"#config/*": ["./src/server/config/*"],
"#infrastructure/*": ["./src/server/infrastructure/*"],
"#middleware/*": ["./src/server/middleware/*"],
Expand Down
15 changes: 7 additions & 8 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@
"private": true,
"description": "PINS Back Office API App",
"engines": {
"node": ">=20.0.0 <21.0.0"
},
"prisma": {
"schema": "src/database/schema.prisma",
"seed": "node src/database/seed/seed-development.js"
"node": ">=22.0.0 <23.0.0"
},
"type": "module",
"scripts": {
Expand All @@ -32,7 +28,7 @@
"db:seed:prod": "npm run prisma-generate && cross-env NODE_ENV=production node src/database/seed/seed-production.js",
"db:create:gs51": "npm run prisma-generate && node src/database/seed/seed-gs51.js",
"prisma:format": "npx prisma format",
"prisma-generate": "npx prisma generate",
"prisma-generate": "npx prisma generate && tsc --project src/database/client-tsconfig.json",
"tscheck": "npx tsc -p jsconfig.json --maxNodeModuleJsDepth 0"
},
"dependencies": {
Expand All @@ -49,6 +45,7 @@
"@pins/examination-timetable-utils": "*",
"@pins/platform": "*",
"@prisma/instrumentation": "*",
"@prisma/adapter-mssql": "*",
"@prisma/client": "*",
"@supercharge/promise-pool": "*",
"ajv": "*",
Expand Down Expand Up @@ -89,12 +86,14 @@
"rimraf": "*",
"supertest": "*",
"swagger-autogen": "*",
"swagger-typescript-api": "*"
"swagger-typescript-api": "*",
"typescript": "*"
},
"imports": {
"#app-test": "./src/server/app-test.js",
"#api-constants": "./src/server/applications/constants.js",
"#config/*": "./src/server/config/*",
"#database-client": "./src/database/client/client.js",
"#config/*": "./src/server/config/*",
"#infrastructure/*": "./src/server/infrastructure/*",
"#middleware/*": "./src/server/middleware/*",
"#repositories/*": "./src/server/repositories/*",
Expand Down
17 changes: 17 additions & 0 deletions apps/api/prisma.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig } from 'prisma/config';
import path from 'node:path';
import dotenv from 'dotenv';

// load configuration from .env file into process.env
dotenv.config();

export default defineConfig({
schema: path.join('src', 'database', 'schema.prisma'),
migrations: {
path: path.join('src', 'database', 'migrations'),
seed: 'node src/database/seed/seed-development.js'
},
datasource: {
url: process.env.DATABASE_URL || ''
}
});
23 changes: 16 additions & 7 deletions apps/api/setup-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,22 +397,31 @@ class MockPrismaClient {
return Array.isArray(results) ? results : [];
}
});

// see https://www.prisma.io/docs/orm/reference/prisma-client-reference#extends
// return the prisma client instance
$extends = jest.fn().mockImplementation(() => this);
}

const mockPrismaUse = jest.fn().mockResolvedValue();

MockPrismaClient.prototype.$executeRawUnsafe = mockExecuteRawUnsafe;
MockPrismaClient.prototype.$use = mockPrismaUse;

class MockPrisma {}
class MockPrisma {
// see https://www.prisma.io/docs/orm/prisma-client/client-extensions
static defineExtension(func) {
func(new MockPrismaClient());
}
}

jest.unstable_mockModule('@prisma/client', () => ({
jest.unstable_mockModule('#database-client', () => ({
PrismaClient: MockPrismaClient,
Prisma: MockPrisma,
default: {
// PrismaClient: MockPrismaClient,
// Prisma: MockPrisma
}
Prisma: MockPrisma
}));

jest.unstable_mockModule('@prisma/adapter-mssql', () => ({
PrismaMssql: jest.fn()
}));

const mockSendEvents = jest.fn().mockImplementation(sendEvents);
Expand Down
12 changes: 12 additions & 0 deletions apps/api/src/database/client-tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"include": ["./client/client.ts"],
"compilerOptions": {
"baseUrl": "./client",
"esModuleInterop": true,
"moduleResolution": "nodenext",
"checkJs": false,
"target": "esnext",
"module": "NodeNext",
"skipLibCheck": true
}
}
5 changes: 3 additions & 2 deletions apps/api/src/database/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client"
output = "./client"
importFileExtension = "js"
}

datasource db {
provider = "sqlserver"
url = env("DATABASE_URL")
}

/// Case model
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/database/seed/data-gs51.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createRepresentation } from './data-test.js';

/**
*
* @param {import('@prisma/client').PrismaClient} databaseConnector
* @param {import('#database-client').PrismaClient} databaseConnector
*/
export const createGeneralS51Application = async (databaseConnector) => {
try {
Expand Down
10 changes: 5 additions & 5 deletions apps/api/src/database/seed/data-static.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

/**
*
* @typedef {import('@prisma/client').Prisma.SectorCreateInput} SectorCreateInput
* @typedef {import('@prisma/client').Prisma.RegionCreateInput} RegionCreateInput
* @typedef {import('@prisma/client').Prisma.ZoomLevelCreateInput} ZoomLevelCreateInput
* @typedef {import('@prisma/client').Prisma.ExaminationTimetableTypeCreateInput} ExaminationTimetableTypeCreateInput
* @typedef {import('#database-client').Prisma.SectorCreateInput} SectorCreateInput
* @typedef {import('#database-client').Prisma.RegionCreateInput} RegionCreateInput
* @typedef {import('#database-client').Prisma.ZoomLevelCreateInput} ZoomLevelCreateInput
* @typedef {import('#database-client').Prisma.ExaminationTimetableTypeCreateInput} ExaminationTimetableTypeCreateInput
*
* @typedef {{ abbreviation: string, name: string, displayNameEn: string, displayNameCy: string}} SubSectorPartial
* @typedef {{ sectorName: string, subSector: SubSectorPartial}} SubSectorPartialWithSectorName
Expand Down Expand Up @@ -490,7 +490,7 @@ export const examinationTimetableTypes = [
/**
* seed static data into the database. Does not disconnect from the database or handle errors.
*
* @param {import('@prisma/client').PrismaClient} databaseConnector
* @param {import('#database-client').PrismaClient} databaseConnector
*/
export async function seedStaticData(databaseConnector) {
for (const sector of sectors) {
Expand Down
12 changes: 6 additions & 6 deletions apps/api/src/database/seed/data-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function createRepresentation(caseReference, index, isValidStatus = false
/**
*
* @param {number} caseId
* @returns {import('@prisma/client').Prisma.ProjectUpdateCreateManyInput}
* @returns {import('#database-client').Prisma.ProjectUpdateCreateManyInput}
*/
function generateProjectUpdate(caseId) {
const statuses = ['draft', 'published', 'unpublished', 'archived'];
Expand All @@ -98,7 +98,7 @@ function generateProjectUpdate(caseId) {
];
const dates = oneDatePerMonth();
/**
* @type {import('@prisma/client').Prisma.ProjectUpdateCreateManyInput}
* @type {import('#database-client').Prisma.ProjectUpdateCreateManyInput}
*/
const projectUpdate = {
caseId,
Expand All @@ -114,9 +114,9 @@ function generateProjectUpdate(caseId) {
}

/**
* @param {import('@prisma/client').PrismaClient} databaseConnector
* @param {import('#database-client').PrismaClient} databaseConnector
* @param {number} caseId
* @returns {Promise<import('@prisma/client').Prisma.BatchPayload>}
* @returns {Promise<import('#database-client').Prisma.BatchPayload>}
*/
function createProjectUpdates(databaseConnector, caseId) {
const numUpdates = pseudoRandomInt(0, 28);
Expand All @@ -131,7 +131,7 @@ function createProjectUpdates(databaseConnector, caseId) {

/**
*
* @param {import('@prisma/client').PrismaClient} databaseConnector
* @param {import('#database-client').PrismaClient} databaseConnector
* @param {{name: string, abbreviation: string, displayNameEn: string}} subSector
* @param {number} index
*/
Expand Down Expand Up @@ -214,7 +214,7 @@ const createApplication = async (databaseConnector, subSector, index) => {
};

/**
* @param {import('@prisma/client').PrismaClient} databaseConnector
* @param {import('#database-client').PrismaClient} databaseConnector
*/
export async function seedTestData(databaseConnector) {
// now create some sample applications
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/database/seed/seed-clear.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { truncateTable } from '../prisma.truncate.js';
/**
* Seeding function to clear down a database, deletes all cases, documents, records, reference tables, etc
*
* @param {import('@prisma/client').PrismaClient} databaseConnector
* @param {import('#database-client').PrismaClient} databaseConnector
*/
export async function deleteAllRecords(databaseConnector) {
const deleteCases = databaseConnector.case.deleteMany();
Expand Down
14 changes: 11 additions & 3 deletions apps/api/src/database/seed/seed-development.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Set NODE_ENV before importing any modules that depend on it
process.env.NODE_ENV = 'seeding';

import { databaseConnector } from '#utils/database-connector.js';
import { seedStaticData } from './data-static.js';
import { seedTestData } from './data-test.js';
Expand All @@ -11,14 +14,19 @@ import { deleteAllRecords } from './seed-clear.js';
* @returns {Promise<void>}
*/
const seedDevelopment = async () => {
process.env.NODE_ENV = 'seeding';
try {
await deleteAllRecords(databaseConnector);
// Check if database is empty first
const caseCount = await databaseConnector.case.count();

if (caseCount > 0) {
await deleteAllRecords(databaseConnector);
}

await seedStaticData(databaseConnector);
await seedTestData(databaseConnector);
await createGeneralS51Application(databaseConnector);
} catch (error) {
console.error(error);
console.error('Error during seeding:', error);
throw error;
} finally {
await databaseConnector.$disconnect();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ import {
import { validateDocumentVersionMetadataBody } from './document.validators.js';

/**
* @typedef {import('@prisma/client').Document} Document
* @typedef {import('@prisma/client').DocumentVersion} DocumentVersion
* @typedef {import('#database-client').Document} Document
* @typedef {import('#database-client').DocumentVersion} DocumentVersion
* @typedef {import('@pins/applications.api').Schema.DocumentDetails} DocumentDetails
* @typedef {import('@pins/applications.api').Schema.DocumentVersionWithDocument} DocumentVersionWithDocument
* @typedef {import('@pins/applications.api').Api.DocumentToSave} DocumentToSave
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import { getApplicationDocumentsFolderName } from '#utils/mapping/map-document-f
import { handleCreationOfDocumentActivityLogs } from '../../../migration/migrators/nsip-document-migrator.js';

/**
* @typedef {import('@prisma/client').DocumentVersion} DocumentVersion
* @typedef {import('@prisma/client').Document} Document
* @typedef {import('@prisma/client').Document & {documentName: string}} DocumentWithDocumentName
* @typedef {import('@prisma/client').Prisma.DocumentVersionGetPayload<{include: {Document: {include: {folder: {include: {case: {include: {CaseStatus: true}}}}}}}}> } DocumentVersionWithDocumentAndFolder
* @typedef {import('#database-client').DocumentVersion} DocumentVersion
* @typedef {import('#database-client').Document} Document
* @typedef {import('#database-client').Document & {documentName: string}} DocumentWithDocumentName
* @typedef {import('#database-client').Prisma.DocumentVersionGetPayload<{include: {Document: {include: {folder: {include: {case: {include: {CaseStatus: true}}}}}}}}> } DocumentVersionWithDocumentAndFolder
* @typedef {import('@pins/applications.api').Schema.DocumentDetails} DocumentDetails
* @typedef {import('@pins/applications.api').Schema.DocumentVersionWithDocument} DocumentVersionWithDocument
* @typedef {import('@pins/applications.api').Api.DocumentAndBlobInfoManyResponse} DocumentAndBlobInfoManyResponse
Expand Down Expand Up @@ -119,7 +119,7 @@ const getCaseStageMapping = async (folderId) => {
* @param {import('@prisma/client').Prisma.TransactionClient} [tx] - Optional transaction client
* @returns {Promise<{successful: DocumentWithDocumentName[], failed: string[]}>}
*/
const attemptInsertDocuments = async (caseId, documents, isS51, tx = null) => {
const attemptInsertDocuments = async (caseId, documents, isS51, tx) => {
// Use PromisePool to concurrently process the documents with a concurrency of 5.

/**
Expand Down Expand Up @@ -258,7 +258,7 @@ const mapDocumentsToGetBlobStorageProperties = (documents, caseReference, isFrom
const upsertDocumentVersionsMetadataToDatabase = async (
blobStorageDocuments,
privateBlobContainer,
tx = null
tx
) => {
// Generate an array of documents to upsert, with metadata pulled from the blob storage documents
const documentsMetadataToSendToDatabase = blobStorageDocuments.map((documentToUpload) => {
Expand Down Expand Up @@ -297,7 +297,7 @@ const upsertDocumentVersionsMetadataToDatabase = async (
* @param {import('@prisma/client').Prisma.TransactionClient} [tx] - Optional transaction client
* @returns {Promise<{response: DocumentAndBlobInfoManyResponse | null, failedDocuments: string[]}>}}
*/
export const createDocuments = async (documentsToUpload, caseId, isS51, tx = null) => {
export const createDocuments = async (documentsToUpload, caseId, isS51, tx) => {
// Step 1: Retrieve the case object associated with the provided caseId
logger.info(`Retrieving case for caseId ${caseId}...`);
const caseForDocuments = await caseRepository.getById(Number(caseId), { sector: true });
Expand Down Expand Up @@ -359,7 +359,7 @@ export const createDocuments = async (documentsToUpload, caseId, isS51, tx = nul
tx
);

/** @type {Promise<import('@prisma/client').DocumentActivityLog>[]} */
/** @type {Promise<import('#database-client').DocumentActivityLog>[]} */
// TODO: refactor to use createMany instead?
const documentActivityLogs = requestToGetDocumentStorageProperties.map((document) =>
documentActivityLogRepository.create(
Expand Down
Loading
Loading