From 557fc1ff2a4be4fdcb50d0b26d99c452d2f89edb Mon Sep 17 00:00:00 2001
From: Lau
Date: Tue, 30 Jan 2024 14:10:04 -0800
Subject: [PATCH 01/28] initial commit
---
express-api/Dockerfile | 4 +-
express-api/src/constants/index.ts | 2 +
express-api/src/constants/urls.ts | 6 ++
.../src/services/geocoder/geocoderService.ts | 76 ++++++++++++++++++
.../src/services/geocoder/geocoderUtils.ts | 77 +++++++++++++++++++
.../geocoder/interfaces/IAddressModel.ts | 10 +++
.../services/geocoder/interfaces/ICrsModel.ts | 4 +
.../geocoder/interfaces/IFaultModel.ts | 5 ++
.../interfaces/IFeatureCollectionModel.ts | 23 ++++++
.../geocoder/interfaces/IFeatureModel.ts | 8 ++
.../geocoder/interfaces/IGeometryModel.ts | 7 ++
.../geocoder/interfaces/IPropertyModel.ts | 35 +++++++++
.../interfaces/ISitePidsResponseModel.ts | 4 +
13 files changed, 259 insertions(+), 2 deletions(-)
create mode 100644 express-api/src/constants/urls.ts
create mode 100644 express-api/src/services/geocoder/geocoderService.ts
create mode 100644 express-api/src/services/geocoder/geocoderUtils.ts
create mode 100644 express-api/src/services/geocoder/interfaces/IAddressModel.ts
create mode 100644 express-api/src/services/geocoder/interfaces/ICrsModel.ts
create mode 100644 express-api/src/services/geocoder/interfaces/IFaultModel.ts
create mode 100644 express-api/src/services/geocoder/interfaces/IFeatureCollectionModel.ts
create mode 100644 express-api/src/services/geocoder/interfaces/IFeatureModel.ts
create mode 100644 express-api/src/services/geocoder/interfaces/IGeometryModel.ts
create mode 100644 express-api/src/services/geocoder/interfaces/IPropertyModel.ts
create mode 100644 express-api/src/services/geocoder/interfaces/ISitePidsResponseModel.ts
diff --git a/express-api/Dockerfile b/express-api/Dockerfile
index 9117d124f..1b06e2b18 100644
--- a/express-api/Dockerfile
+++ b/express-api/Dockerfile
@@ -2,7 +2,7 @@
# Base #
#############################################
# Use an official Node.js runtime as a base image
-FROM node:18.17.1-bullseye-slim as base
+FROM node:21.6-bullseye-slim as base
# Set the working directory in the container
WORKDIR /express-api
@@ -25,7 +25,7 @@ RUN npm run build
#############################################
# Prod Build #
#############################################
-FROM node:18.17.1-bullseye-slim as Prod
+FROM node:21.6-bullseye-slim as Prod
# Set the working directory to /express-api
WORKDIR /express-api
diff --git a/express-api/src/constants/index.ts b/express-api/src/constants/index.ts
index eb2b3d681..47c111f17 100644
--- a/express-api/src/constants/index.ts
+++ b/express-api/src/constants/index.ts
@@ -1,8 +1,10 @@
import networking from '@/constants/networking';
import switches from '@/constants/switches';
+import urls from '@/constants/urls';
const constants = {
...networking,
...switches,
+ ...urls,
};
export default constants;
diff --git a/express-api/src/constants/urls.ts b/express-api/src/constants/urls.ts
new file mode 100644
index 000000000..88aa170e2
--- /dev/null
+++ b/express-api/src/constants/urls.ts
@@ -0,0 +1,6 @@
+const urls = {
+ GEOCODER: {
+ HOSTURI: 'https://geocoder.api.gov.bc.ca',
+ },
+};
+export default urls;
diff --git a/express-api/src/services/geocoder/geocoderService.ts b/express-api/src/services/geocoder/geocoderService.ts
new file mode 100644
index 000000000..1858744f9
--- /dev/null
+++ b/express-api/src/services/geocoder/geocoderService.ts
@@ -0,0 +1,76 @@
+import { IAddressModel } from '@/services/geocoder/interfaces/IAddressModel';
+//import { Request, Response } from 'express';
+import { IFeatureCollectionModel } from '@/services/geocoder/interfaces/IFeatureCollectionModel';
+import constants from '@/constants';
+import { IFeatureModel } from '@/services/geocoder/interfaces/IFeatureModel';
+import { getLongitude, getLatitude, getAddress1 } from './geocoderUtils';
+
+const generateUrl = (endpoint: string, outputFormat = 'json'): string => {
+ const host = constants.GEOCODER.HOSTURI;
+ return `${host}${endpoint.replace('{outputFormat}', outputFormat)}`;
+};
+
+// const getSiteAddresses = async (
+// address: string,
+// outputFormat = 'json',
+// ): Promise => {
+// const parameters = {
+// AddressString: address,
+// };
+// return getSiteAddressesAsync(parameters, outputFormat);
+// };
+
+const mapFeatureToAddress = (feature: IFeatureModel): IAddressModel => {
+ return {
+ siteId: feature.properties.siteID,
+ fullAddress: feature.properties.fullAddress,
+ address1: getAddress1(feature.properties),
+ administrativeArea: feature.properties.localityName,
+ provinceCode: feature.properties.provinceCode,
+ longitude: getLongitude(feature.geometry),
+ latitude: getLatitude(feature.geometry),
+ score: feature.properties.score,
+ };
+};
+
+export const getSiteAddressesAsync = async (): Promise =>
+ //parameters: AddressesParameters,
+ {
+ // const queryString = new URLSearchParams(parameters).toString();
+ try {
+ const url = constants.GEOCODER.HOSTURI + '/addresses.json?addressString=525%20Superior';
+ const response = await fetch(url, {
+ headers: {
+ apiKey: process.env.GEOCODER__KEY,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch data');
+ }
+
+ const responseData = await response.json();
+ const featureCollection: IFeatureCollectionModel = responseData;
+ const addressInformation: IAddressModel = mapFeatureToAddress(featureCollection.features[0]);
+ console.log('generateUrl', generateUrl('addresses.json?addressString=525%20Superior'));
+ console.log('test', addressInformation);
+ return addressInformation;
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ throw error;
+ }
+ };
+
+// const getPids = async (siteId: string, outputFormat = 'json'): Promise => {
+// const endpoint = options.parcels.pidsUrl.replace('{siteId}', siteId);
+// const url = generateUrl(endpoint, outputFormat);
+// const response = await fetch(url);
+// return await response.json();
+// };
+
+export const GeocoderService = {
+ generateUrl,
+ // getSiteAddresses,
+ getSiteAddressesAsync,
+ // getPids,
+};
diff --git a/express-api/src/services/geocoder/geocoderUtils.ts b/express-api/src/services/geocoder/geocoderUtils.ts
new file mode 100644
index 000000000..71f02f506
--- /dev/null
+++ b/express-api/src/services/geocoder/geocoderUtils.ts
@@ -0,0 +1,77 @@
+/**
+ * geocoderUtils.ts - Utility functions for working with geocoding data
+ */
+
+import { IPropertyModel } from '@/services/geocoder/interfaces/IPropertyModel';
+import { IGeometryModel } from '@/services/geocoder/interfaces/IGeometryModel';
+import { IFeatureModel } from '@/services/geocoder/interfaces/IFeatureModel';
+import { IAddressModel } from '@/services/geocoder/interfaces/IAddressModel';
+
+/**
+ * Constructs address string based on property data.
+ * @param properties - The property data.
+ * @returns The constructed address string.
+ */
+export const getAddress1 = (properties: IPropertyModel): string => {
+ const address = [];
+
+ if (properties.civicNumber) address.push(properties.civicNumber);
+
+ if (properties.isStreetTypePrefix && properties.streetType) address.push(properties.streetType);
+
+ if (properties.isStreetDirectionPrefix && properties.streetDirection)
+ address.push(properties.streetDirection);
+
+ if (properties.streetName) address.push(properties.streetName);
+
+ if (properties.streetQualifier) address.push(properties.streetQualifier);
+
+ if (!properties.isStreetDirectionPrefix && properties.streetDirection)
+ address.push(properties.streetDirection);
+
+ if (!properties.isStreetTypePrefix && properties.streetType) address.push(properties.streetType);
+
+ return address.join(' ');
+};
+
+/**
+ * Retrieves latitude from geometry data.
+ * @param geometry - The geometry data.
+ * @returns The latitude value.
+ */
+export const getLatitude = (geometry: IGeometryModel): number => {
+ if (geometry.coordinates && geometry.coordinates.length === 2) {
+ return geometry.coordinates[1];
+ }
+ return 0;
+};
+
+/**
+ * Retrieves longitude from geometry data.
+ * @param geometry - The geometry data.
+ * @returns The longitude value.
+ */
+export const getLongitude = (geometry: IGeometryModel): number => {
+ if (geometry.coordinates && geometry.coordinates.length === 2) {
+ return geometry.coordinates[0];
+ }
+ return 0;
+};
+
+/**
+ * Maps feature data to address data.
+ * @param feature - The feature data.
+ * @returns The mapped address data.
+ */
+export const mapFeatureToAddress = (feature: IFeatureModel): IAddressModel => {
+ return {
+ siteId: feature.properties.siteID,
+ fullAddress: feature.properties.fullAddress,
+ address1: getAddress1(feature.properties),
+ administrativeArea: feature.properties.localityName,
+ provinceCode: feature.properties.provinceCode,
+ longitude: getLongitude(feature.geometry),
+ latitude: getLatitude(feature.geometry),
+ score: feature.properties.score,
+ };
+};
diff --git a/express-api/src/services/geocoder/interfaces/IAddressModel.ts b/express-api/src/services/geocoder/interfaces/IAddressModel.ts
new file mode 100644
index 000000000..bd072b3b2
--- /dev/null
+++ b/express-api/src/services/geocoder/interfaces/IAddressModel.ts
@@ -0,0 +1,10 @@
+export interface IAddressModel {
+ siteId: string;
+ fullAddress: string;
+ address1: string;
+ administrativeArea: string;
+ provinceCode: string;
+ latitude: number;
+ longitude: number;
+ score: number;
+}
diff --git a/express-api/src/services/geocoder/interfaces/ICrsModel.ts b/express-api/src/services/geocoder/interfaces/ICrsModel.ts
new file mode 100644
index 000000000..63648753f
--- /dev/null
+++ b/express-api/src/services/geocoder/interfaces/ICrsModel.ts
@@ -0,0 +1,4 @@
+export interface ICrsModel {
+ type: string;
+ properties: { [key: string]: unknown };
+}
diff --git a/express-api/src/services/geocoder/interfaces/IFaultModel.ts b/express-api/src/services/geocoder/interfaces/IFaultModel.ts
new file mode 100644
index 000000000..4a87c21d1
--- /dev/null
+++ b/express-api/src/services/geocoder/interfaces/IFaultModel.ts
@@ -0,0 +1,5 @@
+export interface IFaultModel {
+ element: string;
+ fault: string;
+ penalty: number;
+}
diff --git a/express-api/src/services/geocoder/interfaces/IFeatureCollectionModel.ts b/express-api/src/services/geocoder/interfaces/IFeatureCollectionModel.ts
new file mode 100644
index 000000000..5ef1ca1d2
--- /dev/null
+++ b/express-api/src/services/geocoder/interfaces/IFeatureCollectionModel.ts
@@ -0,0 +1,23 @@
+import { ICrsModel } from '@/services/geocoder/interfaces/ICrsModel';
+import { IFeatureModel } from '@/services/geocoder/interfaces/IFeatureModel';
+
+export interface IFeatureCollectionModel {
+ type: string;
+ queryAddress: string;
+ searchTimestamp: string;
+ executionTime: number;
+ version: string;
+ baseDataDate: string;
+ crs: ICrsModel;
+ interpolation: string;
+ echo: string;
+ locationDescripture: string;
+ setback: number;
+ minScore: number;
+ maxResults: number;
+ disclaimer: string;
+ privacyStatement: string;
+ copyrightNotice: string;
+ copyrightLicense: string;
+ features: IFeatureModel[];
+}
diff --git a/express-api/src/services/geocoder/interfaces/IFeatureModel.ts b/express-api/src/services/geocoder/interfaces/IFeatureModel.ts
new file mode 100644
index 000000000..acb08bedb
--- /dev/null
+++ b/express-api/src/services/geocoder/interfaces/IFeatureModel.ts
@@ -0,0 +1,8 @@
+import { IGeometryModel } from '@/services/geocoder/interfaces/IGeometryModel';
+import { IPropertyModel } from '@/services/geocoder/interfaces/IPropertyModel';
+
+export interface IFeatureModel {
+ type: string;
+ geometry: IGeometryModel;
+ properties: IPropertyModel;
+}
diff --git a/express-api/src/services/geocoder/interfaces/IGeometryModel.ts b/express-api/src/services/geocoder/interfaces/IGeometryModel.ts
new file mode 100644
index 000000000..2d4b9ebc2
--- /dev/null
+++ b/express-api/src/services/geocoder/interfaces/IGeometryModel.ts
@@ -0,0 +1,7 @@
+import { ICrsModel } from '@/services/geocoder/interfaces/ICrsModel';
+
+export interface IGeometryModel {
+ type: string;
+ crs: ICrsModel;
+ coordinates: number[];
+}
diff --git a/express-api/src/services/geocoder/interfaces/IPropertyModel.ts b/express-api/src/services/geocoder/interfaces/IPropertyModel.ts
new file mode 100644
index 000000000..1572bb125
--- /dev/null
+++ b/express-api/src/services/geocoder/interfaces/IPropertyModel.ts
@@ -0,0 +1,35 @@
+import { IFaultModel } from '@/services/geocoder/interfaces/IFaultModel';
+
+export interface IPropertyModel {
+ fullAddress: string;
+ score: number;
+ matchPrecision: string;
+ precisionPoints: number;
+ faults: IFaultModel[];
+ siteName: string;
+ unitDesignator: string;
+ unitNumber: string;
+ unitNumberSuffix: string;
+ civicNumber: string;
+ civicNumberSuffix: string;
+ streetName: string;
+ streetType: string;
+ isStreetTypePrefix: boolean;
+ streetDirection: string;
+ isStreetDirectionPrefix: boolean;
+ streetQualifier: string;
+ localityName: string;
+ localityType: string;
+ electoralArea: string;
+ provinceCode: string;
+ locationPositionalAccuracy: string;
+ locationDescriptor: string;
+ siteID: string;
+ blockID: string;
+ fullSiteDescriptor: string;
+ accessNotes: string;
+ siteStatus: string;
+ siteRetireDate: string;
+ changeDate: string;
+ isOfficial: string;
+}
diff --git a/express-api/src/services/geocoder/interfaces/ISitePidsResponseModel.ts b/express-api/src/services/geocoder/interfaces/ISitePidsResponseModel.ts
new file mode 100644
index 000000000..0125e41e8
--- /dev/null
+++ b/express-api/src/services/geocoder/interfaces/ISitePidsResponseModel.ts
@@ -0,0 +1,4 @@
+export interface ISitePidsResponseModel {
+ siteID: string;
+ pids: string;
+}
From ec385d0e176752de1c00123878c8a3da92e8eb41 Mon Sep 17 00:00:00 2001
From: Lau
Date: Tue, 30 Jan 2024 14:17:08 -0800
Subject: [PATCH 02/28] Updating From column for project reports to nullable
because first record is always null
---
express-api/src/typeorm/Entities/ProjectReports.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/express-api/src/typeorm/Entities/ProjectReports.ts b/express-api/src/typeorm/Entities/ProjectReports.ts
index 7509a48d3..cf8b77209 100644
--- a/express-api/src/typeorm/Entities/ProjectReports.ts
+++ b/express-api/src/typeorm/Entities/ProjectReports.ts
@@ -13,7 +13,7 @@ export class ProjectReports extends BaseEntity {
@Column({ type: 'character varying', length: 250, nullable: true })
Name: string;
- @Column('timestamp')
+ @Column({ type: 'timestamp', nullable: true })
From: Date;
@Column('timestamp')
From 6e1a0c54fad8f02da9f07540581d0856ffc2c174 Mon Sep 17 00:00:00 2001
From: Lau
Date: Tue, 30 Jan 2024 16:25:03 -0800
Subject: [PATCH 03/28] refining the service
---
.../src/services/geocoder/geocoderService.ts | 22 ++++++++++---------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/express-api/src/services/geocoder/geocoderService.ts b/express-api/src/services/geocoder/geocoderService.ts
index 1858744f9..0d9134a12 100644
--- a/express-api/src/services/geocoder/geocoderService.ts
+++ b/express-api/src/services/geocoder/geocoderService.ts
@@ -5,11 +5,6 @@ import constants from '@/constants';
import { IFeatureModel } from '@/services/geocoder/interfaces/IFeatureModel';
import { getLongitude, getLatitude, getAddress1 } from './geocoderUtils';
-const generateUrl = (endpoint: string, outputFormat = 'json'): string => {
- const host = constants.GEOCODER.HOSTURI;
- return `${host}${endpoint.replace('{outputFormat}', outputFormat)}`;
-};
-
// const getSiteAddresses = async (
// address: string,
// outputFormat = 'json',
@@ -33,13 +28,16 @@ const mapFeatureToAddress = (feature: IFeatureModel): IAddressModel => {
};
};
-export const getSiteAddressesAsync = async (): Promise =>
+export const getSiteAddressesAsync = async (address: string): Promise =>
//parameters: AddressesParameters,
{
// const queryString = new URLSearchParams(parameters).toString();
+ const encodedAddress = encodeURIComponent(address);
+ const url = new URL('/addresses.json', constants.GEOCODER.HOSTURI);
+ url.searchParams.append('addressString', encodedAddress);
+
try {
- const url = constants.GEOCODER.HOSTURI + '/addresses.json?addressString=525%20Superior';
- const response = await fetch(url, {
+ const response = await fetch(url.toString(), {
headers: {
apiKey: process.env.GEOCODER__KEY,
},
@@ -52,7 +50,7 @@ export const getSiteAddressesAsync = async (): Promise =>
const responseData = await response.json();
const featureCollection: IFeatureCollectionModel = responseData;
const addressInformation: IAddressModel = mapFeatureToAddress(featureCollection.features[0]);
- console.log('generateUrl', generateUrl('addresses.json?addressString=525%20Superior'));
+ console.log(url.toString());
console.log('test', addressInformation);
return addressInformation;
} catch (error) {
@@ -61,6 +59,11 @@ export const getSiteAddressesAsync = async (): Promise =>
}
};
+///
+/// Make a request to Data BC Geocoder for PIDs that belong to the specified 'siteId'.
+///
+/// The site identifier for a parcel.
+/// An array of PIDs for the supplied 'siteId'.
// const getPids = async (siteId: string, outputFormat = 'json'): Promise => {
// const endpoint = options.parcels.pidsUrl.replace('{siteId}', siteId);
// const url = generateUrl(endpoint, outputFormat);
@@ -69,7 +72,6 @@ export const getSiteAddressesAsync = async (): Promise =>
// };
export const GeocoderService = {
- generateUrl,
// getSiteAddresses,
getSiteAddressesAsync,
// getPids,
From a5d0c64d5cce852842109a0c61e688c3406d7623 Mon Sep 17 00:00:00 2001
From: GrahamS-Quartech <112989452+GrahamS-Quartech@users.noreply.github.com>
Date: Thu, 1 Feb 2024 16:55:10 -0800
Subject: [PATCH 04/28] PIMS-669: User & Roles Services (#2037)
---
express-api/package.json | 3 +-
express-api/src/appDataSource.ts | 4 +
express-api/src/constants/roles.ts | 2 +-
.../admin/roles/rolesController.ts | 75 ++---
.../controllers/admin/roles/rolesSchema.ts | 11 +
.../admin/users/usersController.ts | 106 +++++--
.../controllers/admin/users/usersSchema.ts | 19 ++
.../src/controllers/users/usersController.ts | 108 ++++---
express-api/src/routes/adminRouter.ts | 6 +-
express-api/src/routes/usersRouter.ts | 2 -
.../src/services/admin/rolesServices.ts | 63 +++++
.../src/services/admin/usersServices.ts | 101 +++++++
.../src/services/keycloak/keycloakService.ts | 149 +++++++++-
.../src/services/users/usersServices.ts | 206 ++++++++++++++
.../src/typeorm/Entities/AccessRequests.ts | 5 +-
express-api/src/typeorm/Entities/Agencies.ts | 17 +-
express-api/src/typeorm/Entities/Claims.ts | 23 --
.../src/typeorm/Entities/NotificationQueue.ts | 2 +-
.../Entities/ProjectAgencyResponses.ts | 2 +-
express-api/src/typeorm/Entities/Projects.ts | 2 +-
.../src/typeorm/Entities/RoleClaims.ts | 17 --
express-api/src/typeorm/Entities/Roles.ts | 29 --
.../src/typeorm/Entities/UserAgencies.ts | 18 --
express-api/src/typeorm/Entities/UserRoles.ts | 18 --
express-api/src/typeorm/Entities/Users.ts | 80 ------
.../typeorm/Entities/Users_Roles_Claims.ts | 201 +++++++++++++
.../Entities/abstractEntities/BaseEntity.ts | 2 +-
.../utilities/customErrors/ErrorWithCode.ts | 8 +
express-api/tests/testUtils/factories.ts | 89 ++++++
.../admin/roles/rolesController.test.ts | 132 ++++-----
.../admin/users/usersController.test.ts | 264 ++++++++----------
.../controllers/users/usersController.test.ts | 155 +++++-----
.../unit/services/admin/rolesServices.test.ts | 62 ++++
.../unit/services/admin/usersServices.test.ts | 89 ++++++
.../unit/services/users/usersServices.test.ts | 180 ++++++++++++
35 files changed, 1628 insertions(+), 622 deletions(-)
create mode 100644 express-api/src/controllers/admin/roles/rolesSchema.ts
create mode 100644 express-api/src/controllers/admin/users/usersSchema.ts
create mode 100644 express-api/src/services/admin/rolesServices.ts
create mode 100644 express-api/src/services/admin/usersServices.ts
create mode 100644 express-api/src/services/users/usersServices.ts
delete mode 100644 express-api/src/typeorm/Entities/Claims.ts
delete mode 100644 express-api/src/typeorm/Entities/RoleClaims.ts
delete mode 100644 express-api/src/typeorm/Entities/Roles.ts
delete mode 100644 express-api/src/typeorm/Entities/UserAgencies.ts
delete mode 100644 express-api/src/typeorm/Entities/UserRoles.ts
delete mode 100644 express-api/src/typeorm/Entities/Users.ts
create mode 100644 express-api/src/typeorm/Entities/Users_Roles_Claims.ts
create mode 100644 express-api/src/utilities/customErrors/ErrorWithCode.ts
create mode 100644 express-api/tests/unit/services/admin/rolesServices.test.ts
create mode 100644 express-api/tests/unit/services/admin/usersServices.test.ts
create mode 100644 express-api/tests/unit/services/users/usersServices.test.ts
diff --git a/express-api/package.json b/express-api/package.json
index 4a3889294..f07c51dff 100644
--- a/express-api/package.json
+++ b/express-api/package.json
@@ -14,7 +14,8 @@
"coverage": "jest --coverage",
"test:unit": "jest --testPathPattern=/tests/unit",
"test:integration": "jest --testPathPattern=/tests/integration",
- "swagger": "node ./src/swagger/swagger.mjs"
+ "swagger": "node ./src/swagger/swagger.mjs",
+ "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm"
},
"author": "",
"license": "ISC",
diff --git a/express-api/src/appDataSource.ts b/express-api/src/appDataSource.ts
index 10d05080b..7b9c00b41 100644
--- a/express-api/src/appDataSource.ts
+++ b/express-api/src/appDataSource.ts
@@ -1,5 +1,9 @@
import { DataSource } from 'typeorm';
import { CustomWinstonLogger } from '@/typeorm/utilities/CustomWinstonLogger';
+import dotenv from 'dotenv';
+import { resolve } from 'path';
+
+dotenv.config({ path: resolve(__dirname, '../../.env') });
const {
POSTGRES_USER,
diff --git a/express-api/src/constants/roles.ts b/express-api/src/constants/roles.ts
index 88e7ba6c9..edc2bc079 100644
--- a/express-api/src/constants/roles.ts
+++ b/express-api/src/constants/roles.ts
@@ -1,3 +1,3 @@
export enum Roles {
- ADMIN = 'Admin',
+ ADMIN = 'admin',
}
diff --git a/express-api/src/controllers/admin/roles/rolesController.ts b/express-api/src/controllers/admin/roles/rolesController.ts
index 77bc76b45..40426540c 100644
--- a/express-api/src/controllers/admin/roles/rolesController.ts
+++ b/express-api/src/controllers/admin/roles/rolesController.ts
@@ -1,5 +1,7 @@
import { Request, Response } from 'express';
-import { stubResponse } from '@/utilities/stubResponse';
+import rolesServices from '@/services/admin/rolesServices';
+import { RolesFilter, RolesFilterSchema } from '@/controllers/admin/roles/rolesSchema';
+import { UUID } from 'crypto';
/**
* @description Gets a paged list of roles.
@@ -15,9 +17,13 @@ export const getRoles = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ const filter = RolesFilterSchema.safeParse(req.query);
+ if (filter.success) {
+ const roles = await rolesServices.getRoles(filter.data as RolesFilter); //await rolesServices.getRoles(filter.data as RolesFilter);
+ return res.status(200).send(roles);
+ } else {
+ return res.status(400).send('Could not parse filter.');
+ }
};
/**
@@ -34,9 +40,12 @@ export const addRole = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ try {
+ const role = await rolesServices.addRole(req.body);
+ return res.status(201).send(role);
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
/**
@@ -53,9 +62,13 @@ export const getRoleById = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ const id = req.params.id;
+ const role = rolesServices.getRoleById(id as UUID);
+ if (!role) {
+ return res.status(404);
+ } else {
+ return res.status(200).send(role);
+ }
};
/**
@@ -73,8 +86,13 @@ export const updateRoleById = async (req: Request, res: Response) => {
}]
*/
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ const id = req.params.id;
+ if (id != req.body.Id) {
+ return res.status(400).send('Request param id did not match request body id.');
+ } else {
+ const role = await rolesServices.updateRole(req.body);
+ return res.status(200).send(role);
+ }
};
/**
@@ -91,26 +109,15 @@ export const deleteRoleById = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
-};
-
-/**
- * @description Gets a single role that matches a name.
- * @param {Request} req Incoming request
- * @param {Response} res Outgoing response
- * @returns {Response} A 200 status and the role data.
- */
-export const getRoleByName = async (req: Request, res: Response) => {
- /**
- * #swagger.tags = ['Roles - Admin']
- * #swagger.description = 'Gets a role that matches the supplied name.'
- * #swagger.security = [{
- "bearerAuth": []
- }]
- */
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ try {
+ const id = req.params.id;
+ if (id != req.body.Id) {
+ return res.status(400).send('Request param id did not match request body id.');
+ } else {
+ const role = await rolesServices.removeRole(req.body);
+ return res.status(200).send(role);
+ }
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
diff --git a/express-api/src/controllers/admin/roles/rolesSchema.ts b/express-api/src/controllers/admin/roles/rolesSchema.ts
new file mode 100644
index 000000000..b54163157
--- /dev/null
+++ b/express-api/src/controllers/admin/roles/rolesSchema.ts
@@ -0,0 +1,11 @@
+import { UUID } from 'crypto';
+import { z } from 'zod';
+
+export const RolesFilterSchema = z.object({
+ page: z.coerce.number().optional(),
+ quantity: z.coerce.number().optional(),
+ name: z.string().optional(),
+ id: z.string().uuid().optional(),
+});
+
+export type RolesFilter = z.infer & { id?: UUID };
diff --git a/express-api/src/controllers/admin/users/usersController.ts b/express-api/src/controllers/admin/users/usersController.ts
index 0e4f8c398..539b3b62d 100644
--- a/express-api/src/controllers/admin/users/usersController.ts
+++ b/express-api/src/controllers/admin/users/usersController.ts
@@ -1,5 +1,8 @@
import { Request, Response } from 'express';
import { stubResponse } from '@/utilities/stubResponse';
+import userServices from '@/services/admin/usersServices';
+import { UserFilteringSchema, UserFiltering } from '@/controllers/admin/users/usersSchema';
+import { z } from 'zod';
/**
* @description Gets a paged list of users.
@@ -16,8 +19,13 @@ export const getUsers = async (req: Request, res: Response) => {
}]
*/
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ const filter = UserFilteringSchema.safeParse(req.query);
+ if (filter.success) {
+ const users = await userServices.getUsers(filter.data as UserFiltering);
+ return res.status(200).send(users);
+ } else {
+ return res.status(400).send('Failed to parse filter query.');
+ }
};
/**
@@ -34,9 +42,12 @@ export const addUser = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ try {
+ const user = await userServices.addUser(req.body);
+ return res.status(201).send(user);
+ } catch (e) {
+ return res.status(400).send(e.message);
+ }
};
/**
@@ -53,9 +64,18 @@ export const getUserById = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ const id = req.params.id;
+ const uuid = z.string().uuid().safeParse(id);
+ if (uuid.success) {
+ const user = await userServices.getUserById(uuid.data);
+ if (user) {
+ return res.status(200).send(user);
+ } else {
+ return res.status(404);
+ }
+ } else {
+ return res.status(400).send('Could not parse UUID.');
+ }
};
/**
@@ -72,9 +92,16 @@ export const updateUserById = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ const id = z.string().uuid().parse(req.params.id);
+ if (id != req.body.Id) {
+ return res.status(400).send('The param ID does not match the request body.');
+ }
+ try {
+ const user = await userServices.updateUser(req.body);
+ return res.status(200).send(user);
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
/**
@@ -92,20 +119,28 @@ export const deleteUserById = async (req: Request, res: Response) => {
}]
*/
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ const id = z.string().uuid().parse(req.params.id);
+ if (id != req.body.Id) {
+ return res.status(400).send('The param ID does not match the request body.');
+ }
+ try {
+ const user = await userServices.deleteUser(req.body);
+ return res.status(200).send(user);
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
/**
- * @description Gets a paged list of users based on filter.
+ * @description Gets a paged list of users within the user's agency.
* @param {Request} req Incoming request
* @param {Response} res Outgoing response
* @returns {Response} A 200 status with a list of users.
*/
-export const getUsersByFilter = async (req: Request, res: Response) => {
+export const getUsersSameAgency = async (req: Request, res: Response) => {
/**
* #swagger.tags = ['Users - Admin']
- * #swagger.description = 'Gets a paged list of users based on supplied filter.'
+ * #swagger.description = 'Gets a paged list of users within the user's agency.'
* #swagger.security = [{
"bearerAuth": []
}]
@@ -116,22 +151,25 @@ export const getUsersByFilter = async (req: Request, res: Response) => {
};
/**
- * @description Gets a paged list of users within the user's agency.
+ * @description Gets all roles of a user based on their name.
* @param {Request} req Incoming request
* @param {Response} res Outgoing response
- * @returns {Response} A 200 status with a list of users.
+ * @returns {Response} A 200 status with a list of the user's roles.
*/
-export const getUsersSameAgency = async (req: Request, res: Response) => {
+export const getAllRoles = async (req: Request, res: Response) => {
/**
* #swagger.tags = ['Users - Admin']
- * #swagger.description = 'Gets a paged list of users within the user's agency.'
+ * #swagger.description = 'Gets a list of roles assigned to a user.'
* #swagger.security = [{
"bearerAuth": []
}]
*/
-
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+ const username = req.params.username;
+ if (!username) {
+ return res.status(400).send('Username was empty.');
+ }
+ const roles = await userServices.getKeycloakUserRoles(username);
+ return res.status(200).send(roles);
};
/**
@@ -148,11 +186,29 @@ export const getUserRolesByName = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
+ const username = req.params.username;
+ if (!username) {
+ return res.status(400).send('Username was empty.');
+ }
+ const roles = await userServices.getKeycloakUserRoles(username);
+ return res.status(200).send(roles);
+};
- // TODO: Replace stub response with controller logic
- return stubResponse(res);
+export const updateUserRolesByName = async (req: Request, res: Response) => {
+ const username = req.params.username;
+ const roles = z.string().array().safeParse(req.body);
+ if (!roles.success) {
+ return res.status(400).send('Request body was wrong format.');
+ }
+ if (!username) {
+ return res.status(400).send('Username was empty.');
+ }
+ const updatedRoles = await userServices.updateKeycloakUserRoles(username, roles.data);
+ return res.status(200).send(updatedRoles);
};
+// Leaving these two below here for now but I think that we can just consolidate them into the above function instead.
+
/**
* @description Adds a role to a user based on their name.
* @param {Request} req Incoming request
diff --git a/express-api/src/controllers/admin/users/usersSchema.ts b/express-api/src/controllers/admin/users/usersSchema.ts
new file mode 100644
index 000000000..0cdc85449
--- /dev/null
+++ b/express-api/src/controllers/admin/users/usersSchema.ts
@@ -0,0 +1,19 @@
+import { UUID } from 'crypto';
+import { z } from 'zod';
+
+export const UserFilteringSchema = z.object({
+ page: z.coerce.number().optional(),
+ quantity: z.coerce.number().optional(),
+ username: z.string().optional(),
+ displayName: z.string().optional(),
+ lastName: z.string().optional(),
+ firstName: z.string().optional(),
+ email: z.string().optional(),
+ agency: z.string().optional(),
+ role: z.string().optional(),
+ position: z.string().optional(),
+ id: z.string().uuid().optional(),
+ isDisabled: z.boolean().optional(),
+});
+
+export type UserFiltering = z.infer & { id?: UUID }; //Kinda hacky, but the type expected in typeorm is more strict than what zod infers here.
diff --git a/express-api/src/controllers/users/usersController.ts b/express-api/src/controllers/users/usersController.ts
index 5097578a6..f6357c082 100644
--- a/express-api/src/controllers/users/usersController.ts
+++ b/express-api/src/controllers/users/usersController.ts
@@ -1,6 +1,6 @@
-import { stubResponse } from '../../utilities/stubResponse';
+import userServices from '@/services/users/usersServices';
import { Request, Response } from 'express';
-
+import { KeycloakUser } from '@bcgov/citz-imb-kc-express';
/**
* @description Redirects user to the keycloak user info endpoint.
* @param {Request} req Incoming request.
@@ -15,7 +15,28 @@ export const getUserInfo = async (req: Request, res: Response) => {
"bearerAuth" : []
}]
*/
- return stubResponse(res);
+ const decodeJWT = (jwt: string) => {
+ try {
+ return JSON.parse(Buffer.from(jwt, 'base64').toString('ascii'));
+ } catch {
+ throw new Error('Invalid input in decodeJWT()');
+ }
+ };
+
+ if (!req.token) return res.status(400).send('No access token');
+ const [header, payload] = req.token.split('.');
+ if (!header || !payload) return res.status(400).send('Bad token format.');
+
+ const info = {
+ header: decodeJWT(header),
+ payload: decodeJWT(payload),
+ };
+
+ if (info) {
+ return res.status(200).send(info.payload);
+ } else {
+ return res.status(400).send('No keycloak user authenticated.');
+ }
};
/**
@@ -32,7 +53,16 @@ export const getUserAccessRequestLatest = async (req: Request, res: Response) =>
"bearerAuth" : []
}]
*/
- return stubResponse(res);
+ const user = req?.user as KeycloakUser;
+ try {
+ const result = await userServices.getAccessRequest(user);
+ if (!result) {
+ return res.status(204).send('No access request was found.');
+ }
+ return res.status(200).send(result);
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
/**
@@ -49,7 +79,13 @@ export const submitUserAccessRequest = async (req: Request, res: Response) => {
"bearerAuth" : []
}]
*/
- return stubResponse(res);
+ const user = req?.user as KeycloakUser;
+ try {
+ const result = await userServices.addAccessRequest(req.body, user);
+ return res.status(200).send(result);
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
/**
@@ -66,7 +102,17 @@ export const getUserAccessRequestById = async (req: Request, res: Response) => {
"bearerAuth" : []
}]
*/
- return stubResponse(res);
+ const user = req?.user as KeycloakUser;
+ const requestId = Number(req.params?.reqeustId);
+ try {
+ const result = await userServices.getAccessRequestById(requestId, user);
+ if (!result) {
+ return res.status(404);
+ }
+ return res.status(200).send(result);
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
/**
@@ -83,7 +129,13 @@ export const updateUserAccessRequest = async (req: Request, res: Response) => {
"bearerAuth" : []
}]
*/
- return stubResponse(res);
+ const user = req?.user as KeycloakUser;
+ try {
+ const result = await userServices.updateAccessRequest(req.body, user);
+ return res.status(200).send(result);
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
/**
@@ -103,39 +155,11 @@ export const getUserAgencies = async (req: Request, res: Response) => {
"bearerAuth" : []
}]
*/
- return stubResponse(res);
-};
-
-/**
- * @description Exports user as CSV or Excel file.
- * @param {Request} req Incoming request.
- * @param {Response} res Outgoing response.
- * @returns {Response} A 200 status with the CSV or Excel file in the response body.
- */
-export const getUserReport = async (req: Request, res: Response) => {
- /**
- * #swagger.tags = ['Users']
- * #swagger.description = 'Exports users as CSV or Excel file. Include 'Accept' header to request the appropriate expor - ["text/csv", "application/application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]'
- * #swagger.security = [{
- "bearerAuth" : []
- }]
- */
- return stubResponse(res);
-};
-
-/**
- * @description Filters user report based on criteria provided in the request body.
- * @param {Request} req Incoming request.
- * @param {Response} res Outgoing response.
- * @returns {Response} A 200 status with
- */
-export const filterUserReport = async (req: Request, res: Response) => {
- /**
- * #swagger.tags = ['Users']
- * #swagger.description = 'Exports users as CSV or Excel file. Include 'Accept' header to request the appropriate expor - ["text/csv", "application/application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]'
- * #swagger.security = [{
- "bearerAuth" : []
- }]
- */
- return stubResponse(res);
+ const user = String(req.params?.username);
+ try {
+ const result = await userServices.getAgencies(user);
+ return res.status(200).send(result);
+ } catch (e) {
+ return res.status(e?.code ?? 400).send(e?.message);
+ }
};
diff --git a/express-api/src/routes/adminRouter.ts b/express-api/src/routes/adminRouter.ts
index c2ddf60a9..d74dcfbd1 100644
--- a/express-api/src/routes/adminRouter.ts
+++ b/express-api/src/routes/adminRouter.ts
@@ -24,7 +24,6 @@ const {
addRole,
deleteRoleById,
getRoleById,
- getRoleByName,
getRoles,
updateRoleById,
deleteAccessRequest,
@@ -36,7 +35,6 @@ const {
getUserById,
getUserRolesByName,
getUsers,
- getUsersByFilter,
getUsersSameAgency,
updateUserById,
} = controllers.admin;
@@ -80,13 +78,11 @@ router
.put(updateRoleById) // TODO: should put be a patch?
.delete(deleteRoleById);
-router.route(`/roles/name/:name`).get(getRoleByName);
+// router.route(`/roles/name/:name`).get(getRoleByName);
// Endpoints for Admin Users
router.route(`/users`).get(getUsers).post(addUser);
-router.route(`/users/filter`).post(getUsersByFilter); // TODO: GET with query strings instead?
-
router.route(`/users/my/agency`).post(getUsersSameAgency); // TODO: Should this just be generic: get users from an agency?
router
diff --git a/express-api/src/routes/usersRouter.ts b/express-api/src/routes/usersRouter.ts
index b28c0997c..daaf165bf 100644
--- a/express-api/src/routes/usersRouter.ts
+++ b/express-api/src/routes/usersRouter.ts
@@ -9,7 +9,5 @@ router.route(`/access/requests`).post(controllers.submitUserAccessRequest);
router.route(`/access/requests/:requestId`).get(controllers.getUserAccessRequestById);
router.route(`/access/requests/:requestId`).put(controllers.updateUserAccessRequest);
router.route(`/agencies/:username`).get(controllers.getUserAgencies);
-router.route(`/reports/users`).get(controllers.getUserReport);
-router.route(`/reports/users/filter`).post(controllers.filterUserReport);
export default router;
diff --git a/express-api/src/services/admin/rolesServices.ts b/express-api/src/services/admin/rolesServices.ts
new file mode 100644
index 000000000..ccfc61713
--- /dev/null
+++ b/express-api/src/services/admin/rolesServices.ts
@@ -0,0 +1,63 @@
+import { AppDataSource } from '@/appDataSource';
+import { RolesFilter } from '../../controllers/admin/roles/rolesSchema';
+import { DeepPartial } from 'typeorm';
+import { Roles } from '@/typeorm/Entities/Users_Roles_Claims';
+import { UUID } from 'crypto';
+import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
+
+const getRoles = async (filter: RolesFilter) => {
+ const roles = AppDataSource.getRepository(Roles).find({
+ relations: {
+ RoleClaims: { Claim: true },
+ },
+ where: {
+ Name: filter.name,
+ Id: filter.id,
+ },
+ skip: (filter.page ?? 0) * (filter.quantity ?? 0),
+ take: filter.quantity,
+ });
+ return roles;
+};
+
+const getRoleById = async (roleId: UUID) => {
+ return AppDataSource.getRepository(Roles).findOne({
+ where: { Id: roleId },
+ });
+};
+
+const addRole = async (role: Roles) => {
+ const existing = await getRoleById(role.Id);
+ if (existing) {
+ throw new ErrorWithCode('Role already exists', 409);
+ }
+ const retRole = AppDataSource.getRepository(Roles).save(role);
+ return retRole;
+};
+
+const updateRole = async (role: DeepPartial) => {
+ const retRole = await AppDataSource.getRepository(Roles).update(role.Id, role);
+ if (!retRole.affected) {
+ throw new ErrorWithCode('Role was not found.', 404);
+ }
+ return retRole.generatedMaps[0];
+};
+
+const removeRole = async (role: Roles) => {
+ const existing = await getRoleById(role.Id);
+ if (!existing) {
+ throw new ErrorWithCode('Role was not found.', 404);
+ }
+ const retRole = AppDataSource.getRepository(Roles).remove(role);
+ return retRole;
+};
+
+const rolesServices = {
+ getRoles,
+ getRoleById,
+ addRole,
+ updateRole,
+ removeRole,
+};
+
+export default rolesServices;
diff --git a/express-api/src/services/admin/usersServices.ts b/express-api/src/services/admin/usersServices.ts
new file mode 100644
index 000000000..24a312381
--- /dev/null
+++ b/express-api/src/services/admin/usersServices.ts
@@ -0,0 +1,101 @@
+import { AppDataSource } from '@/appDataSource';
+import { Users } from '@/typeorm/Entities/Users_Roles_Claims';
+import { UserFiltering } from '../../controllers/admin/users/usersSchema';
+import KeycloakService from '../keycloak/keycloakService';
+import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
+import { UUID } from 'crypto';
+
+const getUsers = async (filter: UserFiltering) => {
+ const users = await AppDataSource.getRepository(Users).find({
+ relations: {
+ Agency: true,
+ },
+ where: {
+ Id: filter.id,
+ Username: filter.username,
+ DisplayName: filter.displayName,
+ LastName: filter.lastName,
+ Email: filter.email,
+ Agency: {
+ Name: filter.agency,
+ },
+ UserRoles: {
+ Role: {
+ Name: filter.role,
+ },
+ },
+ IsDisabled: filter.isDisabled,
+ Position: filter.position,
+ },
+ take: filter.quantity,
+ skip: (filter.page ?? 0) * (filter.quantity ?? 0),
+ });
+ return users;
+};
+
+const getUserById = async (id: string) => {
+ return AppDataSource.getRepository(Users).findOne({
+ relations: {
+ Agency: true,
+ UserRoles: { Role: true },
+ },
+ where: {
+ Id: id as UUID,
+ },
+ });
+};
+
+const addUser = async (user: Users) => {
+ const resource = await AppDataSource.getRepository(Users).findOne({ where: { Id: user.Id } });
+ if (resource) {
+ throw new ErrorWithCode('Resource already exists.', 409);
+ }
+ const retUser = await AppDataSource.getRepository(Users).save(user);
+ return retUser;
+};
+
+const updateUser = async (user: Users) => {
+ const resource = await AppDataSource.getRepository(Users).findOne({ where: { Id: user.Id } });
+ if (!resource) {
+ throw new ErrorWithCode('Resource does not exist.', 404);
+ }
+ const retUser = await AppDataSource.getRepository(Users).update(user.Id, user);
+ return retUser.generatedMaps[0];
+};
+
+const deleteUser = async (user: Users) => {
+ const resource = await AppDataSource.getRepository(Users).findOne({ where: { Id: user.Id } });
+ if (!resource) {
+ throw new ErrorWithCode('Resource does not exist.', 404);
+ }
+ const retUser = await AppDataSource.getRepository(Users).remove(user);
+ return retUser;
+};
+
+const getKeycloakRoles = async () => {
+ const roles = await KeycloakService.getKeycloakRoles();
+ return roles.map((a) => a.name);
+};
+
+const getKeycloakUserRoles = async (username: string) => {
+ const keycloakRoles = await KeycloakService.getKeycloakUserRoles(username);
+ return keycloakRoles.map((a) => a.name);
+};
+
+const updateKeycloakUserRoles = async (username: string, roleNames: string[]) => {
+ const keycloakRoles = await KeycloakService.updateKeycloakUserRoles(username, roleNames);
+ return keycloakRoles.map((a) => a.name);
+};
+
+const userServices = {
+ getUsers,
+ addUser,
+ updateUser,
+ deleteUser,
+ getKeycloakRoles,
+ getKeycloakUserRoles,
+ updateKeycloakUserRoles,
+ getUserById,
+};
+
+export default userServices;
diff --git a/express-api/src/services/keycloak/keycloakService.ts b/express-api/src/services/keycloak/keycloakService.ts
index 7afb97bbb..0edaee157 100644
--- a/express-api/src/services/keycloak/keycloakService.ts
+++ b/express-api/src/services/keycloak/keycloakService.ts
@@ -20,6 +20,12 @@ import {
unassignUserRole,
IDIRUserQuery,
} from '@bcgov/citz-imb-kc-css-api';
+import rolesServices from '../admin/rolesServices';
+import { randomUUID } from 'crypto';
+import { AppDataSource } from '@/appDataSource';
+import { DeepPartial, In, Not } from 'typeorm';
+import userServices from '../admin/usersServices';
+import { Users, Roles } from '@/typeorm/Entities/Users_Roles_Claims';
/**
* @description Sync keycloak roles into PIMS roles.
@@ -31,6 +37,50 @@ const syncKeycloakRoles = async () => {
// If role is in PIMS, update it
// If not in PIMS, add it
// If PIMS has roles that aren't in Keycloak, remove them.
+ const roles = await KeycloakService.getKeycloakRoles();
+ for (const role of roles) {
+ const internalRole = await rolesServices.getRoles({ name: role.name });
+
+ if (internalRole.length == 0) {
+ const newRole: Roles = {
+ Id: randomUUID(),
+ Name: role.name,
+ IsDisabled: false,
+ SortOrder: 0,
+ KeycloakGroupId: '',
+ Description: '',
+ IsPublic: false,
+ CreatedById: undefined,
+ CreatedOn: undefined,
+ UpdatedById: undefined,
+ UpdatedOn: undefined,
+ UserRoles: [],
+ RoleClaims: [],
+ };
+ rolesServices.addRole(newRole);
+ } else {
+ const overwriteRole: DeepPartial = {
+ Id: internalRole[0].Id,
+ Name: role.name,
+ IsDisabled: false,
+ SortOrder: 0,
+ KeycloakGroupId: '',
+ Description: '',
+ IsPublic: false,
+ CreatedById: undefined,
+ CreatedOn: undefined,
+ UpdatedById: undefined,
+ UpdatedOn: undefined,
+ };
+ rolesServices.updateRole(overwriteRole);
+ }
+
+ await AppDataSource.getRepository(Roles).delete({
+ Name: Not(In(roles.map((a) => a.name))),
+ });
+
+ return roles;
+ }
};
/**
@@ -96,7 +146,7 @@ const updateKeycloakRole = async (roleName: string, newRoleName: string) => {
};
// TODO: Complete when user and role services are complete.
-const syncKeycloakUser = async () => {
+const syncKeycloakUser = async (keycloakGuid: string) => {
// Does user exist in Keycloak?
// Get their existing roles.
// Does user exist in PIMS
@@ -104,6 +154,74 @@ const syncKeycloakUser = async () => {
// Update the roles in PIMS to match their Keycloak roles
// If they don't exist in PIMS...
// Add user and assign their roles
+ const kuser = await KeycloakService.getKeycloakUser(keycloakGuid);
+ const kroles = await KeycloakService.getKeycloakUserRoles(kuser.username);
+ const internalUser = await userServices.getUsers({ username: kuser.username });
+
+ for (const krole of kroles) {
+ const internalRole = await rolesServices.getRoles({ name: krole.name });
+ if (internalRole.length == 0) {
+ const newRole: Roles = {
+ Id: randomUUID(),
+ Name: krole.name,
+ IsDisabled: false,
+ SortOrder: 0,
+ KeycloakGroupId: '',
+ Description: '',
+ IsPublic: false,
+ UserRoles: [],
+ RoleClaims: [],
+ CreatedById: undefined,
+ CreatedOn: undefined,
+ UpdatedById: undefined,
+ UpdatedOn: undefined,
+ };
+ await rolesServices.addRole(newRole);
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const newUsersRoles = await AppDataSource.getRepository(Roles).find({
+ where: { Name: In(kroles.map((a) => a.name)) },
+ });
+
+ if (internalUser.length == 0) {
+ const newUser: Users = {
+ Id: randomUUID(),
+ CreatedById: undefined,
+ CreatedOn: undefined,
+ UpdatedById: undefined,
+ UpdatedOn: undefined,
+ Username: kuser.username,
+ DisplayName: kuser.attributes.display_name[0],
+ FirstName: kuser.firstName,
+ MiddleName: '',
+ LastName: kuser.lastName,
+ Email: kuser.email,
+ Position: '',
+ IsDisabled: false,
+ EmailVerified: false,
+ IsSystem: false,
+ Note: '',
+ LastLogin: new Date(),
+ ApprovedById: undefined,
+ ApprovedOn: undefined,
+ KeycloakUserId: keycloakGuid,
+ UserRoles: [],
+ Agency: undefined,
+ AgencyId: undefined,
+ };
+ return await userServices.addUser(newUser);
+ } else {
+ // internalUser[0].UserRoles = newUsersRoles.map((a) => ({
+ // UserId: internalUser[0].Id,
+ // RoleId: a.Id,
+ // User: internalUser[0],
+ // Role: a,
+ // }));
+ // return await userServices.updateUser(internalUser[0]);
+ return;
+ }
};
/**
@@ -141,29 +259,31 @@ const getKeycloakUser = async (guid: string) => {
}
};
-/**
- * @description Updates a user's roles in Keycloak.
- * @param {string} username The user's username.
- * @param {string[]} roles A list of roles that the user should have.
- * @returns {IKeycloakRole[]} A list of Keycloak roles.
- * @throws If the user does not exist.
- */
-const updateKeycloakUserRoles = async (username: string, roles: string[]) => {
+const getKeycloakUserRoles = async (username: string) => {
const existingRolesResponse: IKeycloakRolesResponse | IKeycloakErrorResponse =
await getUserRoles(username);
- // Did that user exist? If not, it will be of type IKeycloakErrorResponse.
if (!keycloakUserRolesSchema.safeParse(existingRolesResponse).success) {
- const message = `keycloakService.updateKeycloakUserRoles: ${
+ const message = `keycloakService.getKeycloakUser: ${
(existingRolesResponse as IKeycloakErrorResponse).message
}`;
logger.warn(message);
throw new Error(message);
}
+ return (existingRolesResponse as IKeycloakRolesResponse).data;
+};
+
+/**
+ * @description Updates a user's roles in Keycloak.
+ * @param {string} username The user's username.
+ * @param {string[]} roles A list of roles that the user should have.
+ * @returns {IKeycloakRole[]} A list of Keycloak roles.
+ * @throws If the user does not exist.
+ */
+const updateKeycloakUserRoles = async (username: string, roles: string[]) => {
+ const existingRolesResponse = await getKeycloakUserRoles(username);
// User is found in Keycloak.
- const existingRoles: string[] = (existingRolesResponse as IKeycloakRolesResponse).data.map(
- (role) => role.name,
- );
+ const existingRoles: string[] = existingRolesResponse.map((role) => role.name);
// Find roles that are in Keycloak but are not in new user info.
const rolesToRemove = existingRoles.filter((existingRole) => !roles.includes(existingRole));
@@ -183,6 +303,7 @@ const updateKeycloakUserRoles = async (username: string, roles: string[]) => {
};
const KeycloakService = {
+ getKeycloakUserRoles,
syncKeycloakRoles,
getKeycloakRole,
getKeycloakRoles,
diff --git a/express-api/src/services/users/usersServices.ts b/express-api/src/services/users/usersServices.ts
new file mode 100644
index 000000000..46aa49f3a
--- /dev/null
+++ b/express-api/src/services/users/usersServices.ts
@@ -0,0 +1,206 @@
+import { Users } from '@/typeorm/Entities/Users_Roles_Claims';
+import { AppDataSource } from '@/appDataSource';
+import { KeycloakBCeIDUser, KeycloakIdirUser, KeycloakUser } from '@bcgov/citz-imb-kc-express';
+import { z } from 'zod';
+import { AccessRequests } from '@/typeorm/Entities/AccessRequests';
+import { In } from 'typeorm';
+import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
+import { Agencies } from '@/typeorm/Entities/Agencies';
+
+interface NormalizedKeycloakUser {
+ given_name: string;
+ family_name: string;
+ username: string;
+ guid: string;
+}
+
+const getUser = async (nameOrGuid: string): Promise => {
+ const userGuid = z.string().uuid().safeParse(nameOrGuid);
+ if (userGuid.success) {
+ return AppDataSource.getRepository(Users).findOneBy({
+ KeycloakUserId: userGuid.data,
+ });
+ } else {
+ return AppDataSource.getRepository(Users).findOneBy({
+ Username: nameOrGuid,
+ });
+ }
+};
+
+const normalizeKeycloakUser = (kcUser: KeycloakUser): NormalizedKeycloakUser => {
+ const provider = kcUser.identity_provider;
+ const username = kcUser.preferred_username;
+ const normalizeUuid = (keycloakUuid: string) =>
+ keycloakUuid.toLowerCase().replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/g, '$1-$2-$3-$4-$5');
+ let user;
+ switch (provider) {
+ case 'idir':
+ user = kcUser as KeycloakIdirUser;
+ return {
+ given_name: user.given_name,
+ family_name: user.family_name,
+ username: username,
+ guid: normalizeUuid(user.idir_user_guid),
+ };
+ case 'bceidbasic':
+ user = kcUser as KeycloakBCeIDUser;
+ return {
+ given_name: '',
+ family_name: '',
+ username: username,
+ guid: normalizeUuid(user.bceid_user_guid),
+ };
+ default:
+ throw new Error();
+ }
+};
+
+const getUserFromKeycloak = async (kcUser: KeycloakUser) => {
+ const normalized = normalizeKeycloakUser(kcUser);
+ return getUser(normalized.guid ?? normalized.username);
+};
+
+const activateUser = async (kcUser: KeycloakUser) => {
+ const normalizedUser = normalizeKeycloakUser(kcUser);
+ const internalUser = await getUser(kcUser.preferred_username);
+ if (!internalUser) {
+ const { given_name, family_name, username, guid } = normalizedUser;
+ AppDataSource.getRepository(Users).insert({
+ Username: username,
+ FirstName: given_name,
+ LastName: family_name,
+ KeycloakUserId: guid,
+ });
+ } else {
+ internalUser.LastLogin = new Date();
+ internalUser.KeycloakUserId = normalizedUser.guid;
+ AppDataSource.getRepository(Users).update({ Id: internalUser.Id }, internalUser);
+ }
+};
+
+const getAccessRequest = async (kcUser: KeycloakUser) => {
+ const internalUser = await getUserFromKeycloak(kcUser);
+ const accessRequest = AppDataSource.getRepository(AccessRequests)
+ .createQueryBuilder('AccessRequests')
+ .leftJoinAndSelect('AccessRequests.AgencyId', 'Agencies')
+ .leftJoinAndSelect('AccessRequests.RoleId', 'Roles')
+ .leftJoinAndSelect('AccessRequests.UserId', 'Users')
+ .where('AccessRequests.UserId = :userId', { userId: internalUser.Id })
+ .andWhere('AccessRequests.Status = :status', { status: 0 })
+ .orderBy('AccessRequests.CreatedOn', 'DESC')
+ .getOne();
+ return accessRequest;
+};
+
+const getAccessRequestById = async (requestId: number, kcUser: KeycloakUser) => {
+ const accessRequest = await AppDataSource.getRepository(AccessRequests)
+ .createQueryBuilder('AccessRequests')
+ .leftJoinAndSelect('AccessRequests.AgencyId', 'Agencies')
+ .leftJoinAndSelect('AccessRequests.RoleId', 'Roles')
+ .leftJoinAndSelect('AccessRequests.UserId', 'Users')
+ .where('AccessRequests.Id = :requestId', { requestId: requestId })
+ .getOne();
+ const internalUser = await getUserFromKeycloak(kcUser);
+ if (accessRequest && accessRequest.UserId.Id != internalUser.Id)
+ throw new Error('Not authorized.');
+ return accessRequest;
+};
+
+const deleteAccessRequest = async (accessRequest: AccessRequests) => {
+ const existing = await AppDataSource.getRepository(AccessRequests).findOne({
+ where: { Id: accessRequest.Id },
+ });
+ if (!existing) {
+ throw new ErrorWithCode('No access request found', 404);
+ }
+ const deletedRequest = AppDataSource.getRepository(AccessRequests).remove(accessRequest);
+ return deletedRequest;
+};
+
+const addAccessRequest = async (accessRequest: AccessRequests, kcUser: KeycloakUser) => {
+ if (accessRequest == null || accessRequest.AgencyId == null || accessRequest.RoleId == null) {
+ throw new Error('Null argument.');
+ }
+ const internalUser = await getUserFromKeycloak(kcUser);
+ accessRequest.UserId = internalUser;
+ internalUser.Position = accessRequest.UserId.Position;
+
+ //Iterating through agencies and roles no longer necessary here?
+
+ return AppDataSource.getRepository(AccessRequests).insert(accessRequest);
+};
+
+const updateAccessRequest = async (updateRequest: AccessRequests, kcUser: KeycloakUser) => {
+ if (updateRequest == null || updateRequest.AgencyId == null || updateRequest.RoleId == null)
+ throw new Error('Null argument.');
+
+ const internalUser = await getUserFromKeycloak(kcUser);
+
+ if (updateRequest.UserId.Id != internalUser.Id) throw new Error('Not authorized.');
+
+ const result = await AppDataSource.getRepository(AccessRequests).update(
+ { Id: updateRequest.Id },
+ updateRequest,
+ );
+ if (!result.affected) {
+ throw new ErrorWithCode('Resource not found.', 404);
+ }
+ return result.generatedMaps[0];
+};
+
+const getAgencies = async (username: string) => {
+ const user = await getUser(username);
+ const userAgencies = await AppDataSource.getRepository(Users).findOneOrFail({
+ relations: {
+ Agency: true,
+ },
+ where: {
+ Id: user.Id,
+ },
+ });
+ const agencyId = userAgencies.Agency.Id;
+ const children = await AppDataSource.getRepository(Agencies).find({
+ where: {
+ ParentId: { Id: agencyId },
+ },
+ });
+ // .createQueryBuilder('Agencies')
+ // .where('Agencies.ParentId IN (:...ids)', { ids: agencies })
+ // .getMany();
+ return [agencyId, ...children.map((c) => c.Id)];
+};
+
+const getAdministrators = async (agencyIds: string[]) => {
+ const admins = await AppDataSource.getRepository(Users).find({
+ relations: {
+ UserRoles: { Role: { RoleClaims: { Claim: true } } },
+ Agency: true,
+ },
+ where: {
+ Agency: In(agencyIds),
+ UserRoles: { Role: { RoleClaims: { Claim: { Name: 'System Admin' } } } },
+ },
+ });
+ // .createQueryBuilder('Users')
+ // .leftJoinAndSelect('Users.Roles', 'Roles')
+ // .leftJoinAndSelect('Roles.Claims', 'Claims')
+ // .leftJoinAndSelect('Users.Agencies', 'Agencies')
+ // .where('Agencies.Id IN (:...agencyIds)', { agencyIds: agencyIds })
+ // .andWhere('Claims.Name = :systemAdmin', { systemAdmin: 'System Admin' })
+ // .getMany();
+
+ return admins;
+};
+
+const userServices = {
+ activateUser,
+ getAccessRequest,
+ getAccessRequestById,
+ deleteAccessRequest,
+ addAccessRequest,
+ updateAccessRequest,
+ getAgencies,
+ getAdministrators,
+};
+
+export default userServices;
diff --git a/express-api/src/typeorm/Entities/AccessRequests.ts b/express-api/src/typeorm/Entities/AccessRequests.ts
index 571a011ac..281f71859 100644
--- a/express-api/src/typeorm/Entities/AccessRequests.ts
+++ b/express-api/src/typeorm/Entities/AccessRequests.ts
@@ -1,8 +1,7 @@
-import { Agencies } from '@/typeorm/Entities/Agencies';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Roles } from '@/typeorm/Entities/Roles';
-import { Users } from '@/typeorm/Entities/Users';
+import { Users, Roles } from '@/typeorm/Entities/Users_Roles_Claims';
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, Index, JoinColumn } from 'typeorm';
+import { Agencies } from './Agencies';
@Entity()
export class AccessRequests extends BaseEntity {
diff --git a/express-api/src/typeorm/Entities/Agencies.ts b/express-api/src/typeorm/Entities/Agencies.ts
index fa045d33e..55d658eaf 100644
--- a/express-api/src/typeorm/Entities/Agencies.ts
+++ b/express-api/src/typeorm/Entities/Agencies.ts
@@ -1,5 +1,15 @@
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Entity, Column, ManyToOne, Index, JoinColumn, PrimaryColumn } from 'typeorm';
+import {
+ Entity,
+ Index,
+ PrimaryColumn,
+ Column,
+ ManyToOne,
+ JoinColumn,
+ OneToMany,
+ Relation,
+} from 'typeorm';
+import { Users } from './Users_Roles_Claims';
+import { BaseEntity } from './abstractEntities/BaseEntity';
@Entity()
@Index(['ParentId', 'IsDisabled', 'Id', 'Name', 'SortOrder']) // I'm not sure this index is needed. How often do we search by this group?
@@ -35,4 +45,7 @@ export class Agencies extends BaseEntity {
@Column({ type: 'character varying', length: 250, nullable: true })
CCEmail: string;
+
+ @OneToMany(() => Users, (users) => users.Agency)
+ Users: Relation[];
}
diff --git a/express-api/src/typeorm/Entities/Claims.ts b/express-api/src/typeorm/Entities/Claims.ts
deleted file mode 100644
index d037e7ee1..000000000
--- a/express-api/src/typeorm/Entities/Claims.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { UUID } from 'crypto';
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Entity, Column, Index, PrimaryColumn } from 'typeorm';
-
-@Entity()
-@Index(['IsDisabled', 'Name'])
-export class Claims extends BaseEntity {
- @PrimaryColumn({ type: 'uuid' })
- Id: UUID;
-
- @Column({ type: 'character varying', length: 150 })
- @Index({ unique: true })
- Name: string;
-
- @Column('uuid', { nullable: true })
- KeycloakRoleId: string;
-
- @Column('text', { nullable: true })
- Description: string;
-
- @Column('bit')
- IsDisabled: boolean;
-}
diff --git a/express-api/src/typeorm/Entities/NotificationQueue.ts b/express-api/src/typeorm/Entities/NotificationQueue.ts
index be8b55916..c1246665c 100644
--- a/express-api/src/typeorm/Entities/NotificationQueue.ts
+++ b/express-api/src/typeorm/Entities/NotificationQueue.ts
@@ -2,8 +2,8 @@ import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { UUID } from 'crypto';
import { Projects } from '@/typeorm/Entities/Projects';
-import { Agencies } from '@/typeorm/Entities/Agencies';
import { NotificationTemplates } from '@/typeorm/Entities/NotificationTemplates';
+import { Agencies } from './Agencies';
@Entity()
@Index(['Status', 'SendOn', 'Subject'])
diff --git a/express-api/src/typeorm/Entities/ProjectAgencyResponses.ts b/express-api/src/typeorm/Entities/ProjectAgencyResponses.ts
index a87815c38..9bfd8bb0a 100644
--- a/express-api/src/typeorm/Entities/ProjectAgencyResponses.ts
+++ b/express-api/src/typeorm/Entities/ProjectAgencyResponses.ts
@@ -1,8 +1,8 @@
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { Projects } from '@/typeorm/Entities/Projects';
-import { Agencies } from '@/typeorm/Entities/Agencies';
import { NotificationQueue } from '@/typeorm/Entities/NotificationQueue';
+import { Agencies } from './Agencies';
@Entity()
export class ProjectAgencyResponses extends BaseEntity {
diff --git a/express-api/src/typeorm/Entities/Projects.ts b/express-api/src/typeorm/Entities/Projects.ts
index 7ade60345..1dcf0f90d 100644
--- a/express-api/src/typeorm/Entities/Projects.ts
+++ b/express-api/src/typeorm/Entities/Projects.ts
@@ -3,8 +3,8 @@ import { ProjectStatus } from '@/typeorm/Entities/ProjectStatus';
import { Workflows } from '@/typeorm/Entities/Workflows';
import { TierLevels } from '@/typeorm/Entities/TierLevels';
import { ProjectRisks } from '@/typeorm/Entities/ProjectRisks';
-import { Agencies } from '@/typeorm/Entities/Agencies';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+import { Agencies } from './Agencies';
@Entity()
@Index(['Assessed', 'NetBook', 'Market', 'ReportedFiscalYear', 'ActualFiscalYear'])
diff --git a/express-api/src/typeorm/Entities/RoleClaims.ts b/express-api/src/typeorm/Entities/RoleClaims.ts
deleted file mode 100644
index bba38ebb8..000000000
--- a/express-api/src/typeorm/Entities/RoleClaims.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Roles } from '@/typeorm/Entities/Roles';
-import { Claims } from '@/typeorm/Entities/Claims';
-import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
-
-@Entity()
-export class RoleClaims extends BaseEntity {
- @ManyToOne(() => Roles, (Role) => Role.Id)
- @JoinColumn({ name: 'RoleId' })
- @PrimaryColumn('uuid')
- RoleId: Roles;
-
- @ManyToOne(() => Claims, (Claim) => Claim.Id)
- @JoinColumn({ name: 'ClaimId' })
- @PrimaryColumn('uuid')
- ClaimId: Claims;
-}
diff --git a/express-api/src/typeorm/Entities/Roles.ts b/express-api/src/typeorm/Entities/Roles.ts
deleted file mode 100644
index 536cedaf5..000000000
--- a/express-api/src/typeorm/Entities/Roles.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { UUID } from 'crypto';
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Entity, PrimaryColumn, Column, Index } from 'typeorm';
-
-@Entity()
-@Index(['IsDisabled', 'Name'])
-export class Roles extends BaseEntity {
- @PrimaryColumn({ type: 'uuid' })
- Id: UUID;
-
- @Column({ type: 'character varying', length: 100 })
- @Index({ unique: true })
- Name: string;
-
- @Column('bit')
- IsDisabled: boolean;
-
- @Column('int')
- SortOrder: number;
-
- @Column('uuid', { nullable: true })
- KeycloakGroupId: string;
-
- @Column('text', { nullable: true })
- Description: string;
-
- @Column('bit')
- IsPublic: boolean;
-}
diff --git a/express-api/src/typeorm/Entities/UserAgencies.ts b/express-api/src/typeorm/Entities/UserAgencies.ts
deleted file mode 100644
index 2726ca56f..000000000
--- a/express-api/src/typeorm/Entities/UserAgencies.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Users } from '@/typeorm/Entities/Users'; // Adjust the path based on your project structure
-import { Agencies } from '@/typeorm/Entities/Agencies'; // Adjust the path based on your project structure
-import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
-import { User } from '@/controllers/users/usersSchema';
-
-@Entity()
-export class UserAgencies extends BaseEntity {
- @ManyToOne(() => Users, (User) => User.Id)
- @JoinColumn({ name: 'UserId' })
- @PrimaryColumn('uuid')
- UserId: User;
-
- @ManyToOne(() => Agencies, (Agency) => Agency.Id)
- @JoinColumn({ name: 'AgencyId' })
- @PrimaryColumn('character varying')
- AgencyId: Agencies;
-}
diff --git a/express-api/src/typeorm/Entities/UserRoles.ts b/express-api/src/typeorm/Entities/UserRoles.ts
deleted file mode 100644
index 88c406291..000000000
--- a/express-api/src/typeorm/Entities/UserRoles.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Roles } from '@/typeorm/Entities/Roles';
-import { Users } from '@/typeorm/Entities/Users';
-import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
-import { User } from '@/controllers/users/usersSchema';
-
-@Entity()
-export class UserRoles extends BaseEntity {
- @ManyToOne(() => Users, (User) => User.Id)
- @JoinColumn({ name: 'UserId' })
- @PrimaryColumn('uuid')
- UserId: User;
-
- @ManyToOne(() => Roles, (Role) => Role.Id)
- @JoinColumn({ name: 'RoleId' })
- @PrimaryColumn('int')
- RoleId: Roles;
-}
diff --git a/express-api/src/typeorm/Entities/Users.ts b/express-api/src/typeorm/Entities/Users.ts
deleted file mode 100644
index 1ae72c0a4..000000000
--- a/express-api/src/typeorm/Entities/Users.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { UUID } from 'crypto';
-import {
- Entity,
- Column,
- CreateDateColumn,
- ManyToOne,
- Index,
- JoinColumn,
- PrimaryColumn,
-} from 'typeorm';
-
-// This class cannot extend BaseEntity. It creates a circular reference that TypeORM can't handle.
-@Entity()
-export class Users {
- @PrimaryColumn({ type: 'uuid' })
- Id: UUID;
-
- @ManyToOne(() => Users, (User) => User.Id)
- @JoinColumn({ name: 'CreatedById' })
- CreatedById: Users;
-
- @CreateDateColumn()
- CreatedOn: Date;
-
- @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
- @JoinColumn({ name: 'UpdatedById' })
- UpdatedById: Users;
-
- @Column({ type: 'timestamp', nullable: true })
- UpdatedOn: Date;
-
- @Column({ type: 'character varying', length: 25 })
- @Index({ unique: true })
- Username: string;
-
- @Column({ type: 'character varying', length: 100 })
- DisplayName: string;
-
- @Column({ type: 'character varying', length: 100 })
- FirstName: string;
-
- @Column({ type: 'character varying', length: 100, nullable: true })
- MiddleName: string;
-
- @Column({ type: 'character varying', length: 100 })
- LastName: string;
-
- @Column({ type: 'character varying', length: 100 })
- @Index({ unique: true })
- Email: string;
-
- @Column({ type: 'character varying', length: 100, nullable: true })
- Position: string;
-
- @Column('bit')
- IsDisabled: boolean;
-
- @Column('bit')
- EmailVerified: boolean;
-
- @Column('bit')
- IsSystem: boolean;
-
- @Column({ type: 'character varying', length: 1000, nullable: true })
- Note: string;
-
- @Column({ type: 'timestamp', nullable: true })
- LastLogin: Date;
-
- @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
- @JoinColumn({ name: 'ApprovedById' })
- ApprovedById: Users;
-
- @Column({ type: 'timestamp', nullable: true })
- ApprovedOn: Date;
-
- @Column({ type: 'uuid', nullable: true })
- @Index({ unique: true })
- KeycloakUserId: string;
-}
diff --git a/express-api/src/typeorm/Entities/Users_Roles_Claims.ts b/express-api/src/typeorm/Entities/Users_Roles_Claims.ts
new file mode 100644
index 000000000..1ff30378b
--- /dev/null
+++ b/express-api/src/typeorm/Entities/Users_Roles_Claims.ts
@@ -0,0 +1,201 @@
+import { UUID } from 'crypto';
+import {
+ Entity,
+ Column,
+ CreateDateColumn,
+ ManyToOne,
+ Index,
+ JoinColumn,
+ PrimaryColumn,
+ OneToMany,
+ Relation,
+} from 'typeorm';
+import { Agencies } from './Agencies';
+
+@Entity()
+export class Users {
+ @PrimaryColumn({ type: 'uuid' })
+ Id: UUID;
+
+ @ManyToOne(() => Users, (User) => User.Id)
+ @JoinColumn({ name: 'CreatedById' })
+ CreatedById: Users;
+
+ @CreateDateColumn()
+ CreatedOn: Date;
+
+ @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
+ @JoinColumn({ name: 'UpdatedById' })
+ UpdatedById: Users;
+
+ @Column({ type: 'timestamp', nullable: true })
+ UpdatedOn: Date;
+
+ @Column({ type: 'character varying', length: 25 })
+ @Index({ unique: true })
+ Username: string;
+
+ @Column({ type: 'character varying', length: 100 })
+ DisplayName: string;
+
+ @Column({ type: 'character varying', length: 100 })
+ FirstName: string;
+
+ @Column({ type: 'character varying', length: 100, nullable: true })
+ MiddleName: string;
+
+ @Column({ type: 'character varying', length: 100 })
+ LastName: string;
+
+ @Column({ type: 'character varying', length: 100 })
+ @Index({ unique: true })
+ Email: string;
+
+ @Column({ type: 'character varying', length: 100, nullable: true })
+ Position: string;
+
+ @Column('bit')
+ IsDisabled: boolean;
+
+ @Column('bit')
+ EmailVerified: boolean;
+
+ @Column('bit')
+ IsSystem: boolean;
+
+ @Column({ type: 'character varying', length: 1000, nullable: true })
+ Note: string;
+
+ @Column({ type: 'timestamp', nullable: true })
+ LastLogin: Date;
+
+ @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
+ @JoinColumn({ name: 'ApprovedById' })
+ ApprovedById: Users;
+
+ @Column({ type: 'timestamp', nullable: true })
+ ApprovedOn: Date;
+
+ @Column({ type: 'uuid', nullable: true })
+ @Index({ unique: true })
+ KeycloakUserId: string;
+
+ @Column({ name: 'AgencyId', type: 'varchar', length: 6 })
+ AgencyId: string;
+
+ @ManyToOne(() => Agencies, (agency) => agency.Users)
+ @JoinColumn({ name: 'AgencyId' })
+ Agency: Relation;
+
+ @OneToMany(() => UserRoles, (userRole) => userRole.User, { cascade: true })
+ UserRoles: UserRoles[];
+}
+
+//This is copied from the BaseEntity in its own file. Obviously duplication is not ideal, but I doubt this will be getting changed much so should be acceptable.
+//Can't just import it at the top since it depends on Users.
+abstract class BaseEntity {
+ @ManyToOne(() => Users, (User) => User.Id)
+ @JoinColumn({ name: 'CreatedById' })
+ @Index()
+ CreatedById: Users;
+
+ @CreateDateColumn()
+ CreatedOn: Date;
+
+ @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
+ @JoinColumn({ name: 'UpdatedById' })
+ @Index()
+ UpdatedById: Users;
+
+ @Column({ type: 'timestamp', nullable: true })
+ UpdatedOn: Date;
+}
+
+@Entity()
+@Index(['IsDisabled', 'Name'])
+export class Roles extends BaseEntity {
+ @PrimaryColumn({ type: 'uuid' })
+ Id: UUID;
+
+ @Column({ type: 'character varying', length: 100 })
+ @Index({ unique: true })
+ Name: string;
+
+ @Column('bit')
+ IsDisabled: boolean;
+
+ @Column('int')
+ SortOrder: number;
+
+ @Column('uuid', { nullable: true })
+ KeycloakGroupId: string;
+
+ @Column('text', { nullable: true })
+ Description: string;
+
+ @Column('bit')
+ IsPublic: boolean;
+
+ @OneToMany(() => UserRoles, (userRole) => userRole.Role)
+ UserRoles: UserRoles[];
+
+ @OneToMany(() => RoleClaims, (roleClaim) => roleClaim.Role)
+ RoleClaims: RoleClaims[];
+}
+
+@Entity()
+@Index(['IsDisabled', 'Name'])
+export class Claims extends BaseEntity {
+ @PrimaryColumn({ type: 'uuid' })
+ Id: UUID;
+
+ @Column({ type: 'character varying', length: 150 })
+ @Index({ unique: true })
+ Name: string;
+
+ @Column('uuid', { nullable: true })
+ KeycloakRoleId: string;
+
+ @Column('text', { nullable: true })
+ Description: string;
+
+ @Column('bit')
+ IsDisabled: boolean;
+
+ @OneToMany(() => RoleClaims, (roleClaim) => roleClaim.Claim)
+ RoleClaims: RoleClaims[];
+}
+
+@Entity()
+export class RoleClaims extends BaseEntity {
+ @PrimaryColumn()
+ RoleId: string;
+
+ @PrimaryColumn()
+ ClaimId: string;
+
+ @ManyToOne(() => Roles, (Role) => Role.Id)
+ @JoinColumn({ name: 'RoleId', referencedColumnName: 'Id' })
+ Role: Roles;
+
+ @ManyToOne(() => Claims, (Claim) => Claim.Id)
+ @JoinColumn({ name: 'ClaimId', referencedColumnName: 'Id' })
+ Claim: Claims;
+}
+
+@Entity()
+export class UserRoles extends BaseEntity {
+ @PrimaryColumn()
+ RoleId: string;
+
+ @PrimaryColumn()
+ UserId: string;
+
+ @ManyToOne(() => Users, (User) => User.UserRoles)
+ @JoinColumn({ name: 'UserId', referencedColumnName: 'Id' })
+ User: Users;
+
+ @ManyToOne(() => Roles, (Role) => Role.UserRoles)
+ @JoinColumn({ name: 'RoleId', referencedColumnName: 'Id' })
+ Role: Roles;
+}
diff --git a/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts b/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
index e0b4aad3f..bb8ae898f 100644
--- a/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
+++ b/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
@@ -1,4 +1,4 @@
-import { Users } from '@/typeorm/Entities/Users';
+import { Users } from '@/typeorm/Entities/Users_Roles_Claims';
import { Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm';
export abstract class BaseEntity {
diff --git a/express-api/src/utilities/customErrors/ErrorWithCode.ts b/express-api/src/utilities/customErrors/ErrorWithCode.ts
new file mode 100644
index 000000000..2e84bd71c
--- /dev/null
+++ b/express-api/src/utilities/customErrors/ErrorWithCode.ts
@@ -0,0 +1,8 @@
+export class ErrorWithCode extends Error {
+ public code: number;
+
+ constructor(message: string, code?: number) {
+ super(message);
+ this.code = code;
+ }
+}
diff --git a/express-api/tests/testUtils/factories.ts b/express-api/tests/testUtils/factories.ts
index bc2805a46..652897bdd 100644
--- a/express-api/tests/testUtils/factories.ts
+++ b/express-api/tests/testUtils/factories.ts
@@ -1,4 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+import { AccessRequests } from '@/typeorm/Entities/AccessRequests';
+import { Agencies } from '@/typeorm/Entities/Agencies';
+import { Users, Roles as RolesEntity } from '@/typeorm/Entities/Users_Roles_Claims';
+import { faker } from '@faker-js/faker';
+import { UUID } from 'crypto';
import { Request, Response } from 'express';
export class MockRes {
@@ -66,3 +71,87 @@ export const getRequestHandlerMocks = () => {
return { mockReq, mockRes /*mockNext*/ };
};
+
+export const produceUser = (): Users => {
+ const id = faker.string.uuid() as UUID;
+ return {
+ CreatedOn: faker.date.anytime(),
+ UpdatedOn: faker.date.anytime(),
+ UpdatedById: undefined,
+ CreatedById: undefined,
+ Id: id,
+ DisplayName: faker.company.name(),
+ FirstName: faker.person.firstName(),
+ MiddleName: faker.person.middleName(),
+ LastName: faker.person.lastName(),
+ Email: faker.internet.email(),
+ Username: faker.internet.userName(),
+ Position: 'Tester',
+ IsDisabled: false,
+ EmailVerified: false,
+ IsSystem: false,
+ Note: '',
+ LastLogin: faker.date.anytime(),
+ ApprovedById: undefined,
+ ApprovedOn: undefined,
+ KeycloakUserId: faker.string.uuid() as UUID,
+ UserRoles: [],
+ Agency: produceAgency(id),
+ AgencyId: undefined,
+ };
+};
+
+export const produceRequest = (): AccessRequests => {
+ const request: AccessRequests = {
+ Id: faker.number.int(),
+ UserId: produceUser(),
+ Note: 'test',
+ Status: 0,
+ RoleId: undefined,
+ AgencyId: produceAgency(),
+ CreatedById: undefined,
+ CreatedOn: faker.date.anytime(),
+ UpdatedById: undefined,
+ UpdatedOn: faker.date.anytime(),
+ };
+ return request;
+};
+
+export const produceAgency = (userId?: string): Agencies => {
+ const agency: Agencies = {
+ Id: userId ?? faker.string.numeric(6),
+ Name: faker.company.name(),
+ IsDisabled: false,
+ SortOrder: 0,
+ Description: '',
+ ParentId: undefined,
+ Email: faker.internet.email(),
+ SendEmail: false,
+ AddressTo: '',
+ CCEmail: faker.internet.email(),
+ CreatedById: undefined,
+ CreatedOn: new Date(),
+ UpdatedById: undefined,
+ UpdatedOn: new Date(),
+ Users: [],
+ };
+ return agency;
+};
+
+export const produceRole = (): RolesEntity => {
+ return {
+ CreatedOn: faker.date.anytime(),
+ UpdatedOn: faker.date.anytime(),
+ UpdatedById: undefined,
+ CreatedById: undefined,
+ Id: faker.string.uuid() as UUID,
+ Name: faker.company.name(),
+ IsDisabled: false,
+ Description: '',
+ SortOrder: 0,
+ KeycloakGroupId: faker.string.uuid() as UUID,
+ IsPublic: false,
+ UserRoles: [],
+ RoleClaims: [],
+ };
+};
diff --git a/express-api/tests/unit/controllers/admin/roles/rolesController.test.ts b/express-api/tests/unit/controllers/admin/roles/rolesController.test.ts
index 48941023d..a65e07937 100644
--- a/express-api/tests/unit/controllers/admin/roles/rolesController.test.ts
+++ b/express-api/tests/unit/controllers/admin/roles/rolesController.test.ts
@@ -1,138 +1,110 @@
import { Request, Response } from 'express';
import controllers from '@/controllers';
-import { MockReq, MockRes, getRequestHandlerMocks } from '../../../../testUtils/factories';
-import { Roles } from '@/constants/roles';
-import { faker } from '@faker-js/faker';
-import { UUID } from 'crypto';
-import { IRole } from '@/controllers/admin/roles/IRole';
+import {
+ MockReq,
+ MockRes,
+ getRequestHandlerMocks,
+ produceRole,
+} from '../../../../testUtils/factories';
+import { Roles as RolesEntity } from '@/typeorm/Entities/Users_Roles_Claims';
+import { Roles as RolesConstant } from '@/constants/roles';
let mockRequest: Request & MockReq, mockResponse: Response & MockRes;
-const { addRole, getRoleById, getRoleByName, getRoles, deleteRoleById, updateRoleById } =
- controllers.admin;
+const { addRole, getRoleById, getRoles, deleteRoleById, updateRoleById } = controllers.admin;
-const mockRole: IRole = {
- createdOn: faker.date.anytime().toLocaleString(),
- updatedOn: faker.date.anytime().toLocaleString(),
- updatedById: faker.string.uuid() as UUID,
- createdById: faker.string.uuid() as UUID,
- id: faker.string.uuid() as UUID,
- name: faker.company.name(),
- isDisabled: false,
- description: '',
- type: '',
- sortOrder: 0,
- isVisible: true,
-};
+const _getRoles = jest.fn().mockImplementation(() => [produceRole()]);
+const _addRole = jest.fn().mockImplementation((role) => role);
+const _updateRole = jest.fn().mockImplementation((role) => role);
+const _deleteRole = jest.fn().mockImplementation((role) => role);
+const _getRole = jest.fn().mockImplementation(() => produceRole());
+
+jest.mock('@/services/admin/rolesServices', () => ({
+ getRoles: () => _getRoles(),
+ addRole: (role: RolesEntity) => _addRole(role),
+ getRoleById: () => _getRole(),
+ updateRole: (role: RolesEntity) => _updateRole(role),
+ removeRole: (role: RolesEntity) => _deleteRole(role),
+}));
describe('UNIT - Roles Admin', () => {
beforeEach(() => {
const { mockReq, mockRes } = getRequestHandlerMocks();
mockRequest = mockReq;
- mockRequest.setUser({ client_roles: [Roles.ADMIN] });
+ mockRequest.setUser({ client_roles: [RolesConstant.ADMIN] });
mockResponse = mockRes;
});
describe('Controller getRoles', () => {
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
+ it('should return status 200 and a list of roles', async () => {
await getRoles(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ expect(mockResponse.statusValue).toBe(200);
+ expect(Array.isArray(mockResponse.sendValue)).toBe(true);
});
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and a list of roles', async () => {
+ it('should return status 200 and a filtered roles', async () => {
+ mockRequest.body.filter = { name: 'big name' };
+ const role = produceRole();
+ role.Name = 'big name';
await getRoles(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(Array.isArray(mockResponse.sendValue)).toBe(true);
});
});
describe('Controller addRole', () => {
+ const role = produceRole();
beforeEach(() => {
- mockRequest.body = mockRole;
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await addRole(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ mockRequest.body = role;
});
- // TODO: enable other tests when controller is complete
- xit('should return status 201 and the new role', async () => {
+ it('should return status 201 and the new role', async () => {
await addRole(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(201);
+ expect(role.Id).toBe(mockResponse.sendValue.Id);
});
});
describe('Controller getRoleById', () => {
+ const role = produceRole();
beforeEach(() => {
- mockRequest.params.id = `${mockRole.id}`;
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await getRoleById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ _getRole.mockImplementationOnce(() => role);
+ mockRequest.params.id = role.Id;
});
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and the role info', async () => {
+ it('should return status 200 and the role info', async () => {
await getRoleById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
- expect(mockResponse.jsonValue.name).toBe('new name');
- });
- });
-
- describe('Controller getRoleByName', () => {
- beforeEach(() => {
- mockRequest.params.name = `${mockRole.name}`;
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await getRoleByName(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and the role info', async () => {
- await getRoleByName(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(200);
- expect(mockResponse.jsonValue.name).toBe('new name');
+ expect(mockResponse.sendValue.Id).toBe(role.Id);
});
});
describe('Controller updateRoleById', () => {
+ const role = produceRole();
beforeEach(() => {
- mockRequest.params.id = `${mockRole.id}`;
- mockRequest.body = { ...mockRole, name: 'new name' };
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await updateRoleById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ role.Name = 'new name';
+ mockRequest.params.id = role.Id;
+ mockRequest.body = role;
});
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and the updated role', async () => {
+ it('should return status 200 and the updated role', async () => {
await updateRoleById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
- expect(mockResponse.jsonValue.name).toBe('new name');
+ expect(mockResponse.sendValue.Name).toBe('new name');
});
});
describe('Controller deleteRoleById', () => {
+ const role = produceRole();
beforeEach(() => {
- mockRequest.params.id = `${mockRole.id}`;
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await deleteRoleById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ mockRequest.params.id = role.Id;
});
- // TODO: enable other tests when controller is complete
- xit('should return status 204', async () => {
+ it('should return status 204', async () => {
+ mockRequest.body = role;
await deleteRoleById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(204);
+ expect(mockResponse.statusValue).toBe(200);
+ expect(mockResponse.sendValue.Id).toBe(role.Id);
});
});
});
diff --git a/express-api/tests/unit/controllers/admin/users/usersController.test.ts b/express-api/tests/unit/controllers/admin/users/usersController.test.ts
index d357cfa90..367d7fd70 100644
--- a/express-api/tests/unit/controllers/admin/users/usersController.test.ts
+++ b/express-api/tests/unit/controllers/admin/users/usersController.test.ts
@@ -1,40 +1,62 @@
import { Request, Response } from 'express';
import controllers from '@/controllers';
-import { MockReq, MockRes, getRequestHandlerMocks } from '../../../../testUtils/factories';
+import {
+ MockReq,
+ MockRes,
+ getRequestHandlerMocks,
+ produceUser,
+} from '../../../../testUtils/factories';
import { Roles } from '@/constants/roles';
-import { faker } from '@faker-js/faker';
-import { UUID } from 'crypto';
-import { IUser } from '@/controllers/admin/users/IUser';
+import { UserFiltering } from '@/controllers/admin/users/usersSchema';
let mockRequest: Request & MockReq, mockResponse: Response & MockRes;
const {
addUser,
- addUserRoleByName,
+ //addUserRoleByName,
getUserById,
getUserRolesByName,
getUsers,
- getUsersByFilter,
- getUsersSameAgency,
+ //getUsersSameAgency,
deleteUserById,
- deleteUserRoleByName,
+ //deleteUserRoleByName,
updateUserById,
} = controllers.admin;
-const mockUser: IUser = {
- createdOn: faker.date.anytime().toLocaleString(),
- updatedOn: faker.date.anytime().toLocaleString(),
- updatedById: faker.string.uuid() as UUID,
- createdById: faker.string.uuid() as UUID,
- id: faker.string.uuid() as UUID,
- displayName: faker.company.name(),
- firstName: faker.person.firstName(),
- middleName: faker.person.middleName(),
- lastName: faker.person.lastName(),
- email: faker.internet.email(),
- username: faker.internet.userName(),
- position: 'Tester',
-};
+const _getUsers = jest.fn().mockImplementation(() => {
+ return [produceUser()];
+});
+const _addUser = jest.fn().mockImplementation((user) => {
+ return user;
+});
+const _updateUser = jest.fn().mockImplementation((user) => {
+ return user;
+});
+const _deleteUser = jest.fn().mockImplementation((user) => {
+ return user;
+});
+const _getRoles = jest.fn().mockImplementation(() => {
+ return ['admin', 'test'];
+});
+const _getUserRoles = jest.fn().mockImplementation(() => {
+ return ['admin'];
+});
+const _updateUserRoles = jest.fn().mockImplementation((username, roles) => {
+ return [roles];
+});
+const _getUserById = jest.fn().mockImplementation(() => produceUser());
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+jest.mock('@/services/admin/usersServices', () => ({
+ getUsers: () => _getUsers(),
+ addUser: () => _addUser(),
+ updateUser: () => _updateUser(),
+ deleteUser: () => _deleteUser(),
+ getKeycloakRoles: () => _getRoles(),
+ getKeycloakUserRoles: () => _getUserRoles(),
+ updateKeycloakUserRoles: () => _updateUserRoles(),
+ getUserById: () => _getUserById(),
+}));
describe('UNIT - Users Admin', () => {
beforeEach(() => {
@@ -45,181 +67,139 @@ describe('UNIT - Users Admin', () => {
});
describe('Controller getUsers', () => {
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await getUsers(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and a list of users', async () => {
+ it('should return status 200 and a list of users', async () => {
await getUsers(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(Array.isArray(mockResponse.sendValue)).toBe(true);
});
- });
- describe('Controller getUsersByFilter', () => {
- beforeEach(() => {
+ it('should return a list of users based off the filter', async () => {
mockRequest.body = {
- page: 0,
- quantity: 0,
position: 'Tester',
- sort: [''],
- };
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await getUsersByFilter(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and a list of users', async () => {
- await getUsersByFilter(mockRequest, mockResponse);
+ } as UserFiltering;
+ await getUsers(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(Array.isArray(mockResponse.sendValue)).toBe(true);
+ expect(mockResponse.sendValue.length === 1);
});
});
- describe('Controller getUsersSameAgency', () => {
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await getUsersSameAgency(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
+ // describe('Controller getUsersSameAgency', () => {
+ // // TODO: remove stub test when controller is complete
+ // it('should return the stub response of 501', async () => {
+ // await getUsersSameAgency(mockRequest, mockResponse);
+ // expect(mockResponse.statusValue).toBe(501);
+ // });
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and a list of users', async () => {
- await getUsersSameAgency(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(200);
- });
- });
+ // // TODO: enable other tests when controller is complete
+ // xit('should return status 200 and a list of users', async () => {
+ // await getUsersSameAgency(mockRequest, mockResponse);
+ // expect(mockResponse.statusValue).toBe(200);
+ // });
+ // });
describe('Controller addUser', () => {
+ const user = produceUser();
beforeEach(() => {
- mockRequest.body = mockUser;
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await addUser(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ _addUser.mockImplementationOnce(() => user);
+ mockRequest.body = user;
});
- // TODO: enable other tests when controller is complete
- xit('should return status 201 and the new user', async () => {
+ it('should return status 201 and the new user', async () => {
await addUser(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(201);
+ expect(mockResponse.sendValue.Id).toBe(mockRequest.body.Id);
});
});
describe('Controller getUserById', () => {
+ const user = produceUser();
beforeEach(() => {
- mockRequest.params.id = `${mockUser.id}`;
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await getUserById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ _getUserById.mockImplementationOnce(() => user);
});
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and the user info', async () => {
+ it('should return status 200 and the user info', async () => {
+ mockRequest.params.id = user.Id;
await getUserById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
- expect(mockResponse.jsonValue.name).toBe('new name');
+ expect(mockResponse.sendValue.Id).toBe(user.Id);
});
});
describe('Controller updateUserById', () => {
+ const user = produceUser();
+ user.Email = 'newEmail@gov.bc.ca';
beforeEach(() => {
- mockRequest.params.id = `${mockUser.id}`;
- mockRequest.body = { ...mockUser, email: 'newEmail@gov.bc.ca' };
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await updateUserById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ _updateUser.mockImplementationOnce(() => user);
+ mockRequest.params.id = user.Id;
+ mockRequest.body = { ...user, Email: 'newEmail@gov.bc.ca' };
});
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and the updated user', async () => {
+ it('should return status 200 and the updated user', async () => {
await updateUserById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
- expect(mockResponse.jsonValue.email).toBe('newEmail@gov.bc.ca');
+ expect(mockResponse.sendValue.Email).toBe('newEmail@gov.bc.ca');
});
});
describe('Controller deleteUserById', () => {
+ const user = produceUser();
beforeEach(() => {
- mockRequest.params.id = `${mockUser.id}`;
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await deleteUserById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ _deleteUser.mockImplementationOnce(() => user);
+ mockRequest.params.id = user.Id;
+ mockRequest.body = user;
});
- // TODO: enable other tests when controller is complete
- xit('should return status 204', async () => {
+ it('should return status 200', async () => {
await deleteUserById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(204);
+ expect(mockResponse.statusValue).toBe(200);
+ expect(mockResponse.sendValue.Id).toBe(user.Id);
});
});
describe('Controller getUserRolesByName', () => {
beforeEach(() => {
- mockRequest.params.username = `${mockUser.username}`;
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await getUserRolesByName(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
+ mockRequest.params.username = 'test';
});
- // TODO: enable other tests when controller is complete
- xit('should return status 200 and a list of their roles', async () => {
+ it('should return status 200 and a list of their roles', async () => {
await getUserRolesByName(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
// Only role is Admin
- expect(mockResponse.jsonValue.roles).toHaveLength(1);
- expect(mockResponse.jsonValue.roles.at(0)).toBe('Admin');
- });
- });
-
- describe('Controller addUserRoleByName', () => {
- beforeEach(() => {
- mockRequest.params.username = `${mockUser.username}`;
- mockRequest.body = 'new role';
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await addUserRoleByName(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- // TODO: enable other tests when controller is complete
- xit('should return status 201 and the updated user with that role', async () => {
- await addUserRoleByName(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(201);
- expect(mockResponse.jsonValue.roles).toContain('new role');
+ expect(mockResponse.sendValue).toHaveLength(1);
+ expect(mockResponse.sendValue.at(0)).toBe('admin');
});
});
- describe('Controller deleteUserRoleByName', () => {
- beforeEach(() => {
- mockRequest.params.username = `${mockUser.username}`;
- mockRequest.body = 'new role';
- });
- // TODO: remove stub test when controller is complete
- it('should return the stub response of 501', async () => {
- await deleteUserRoleByName(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- // TODO: enable other tests when controller is complete
- xit('should return status 204 and updated user without role', async () => {
- await deleteUserRoleByName(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(204);
- expect(mockResponse.jsonValue.roles).not.toContain('new role');
- });
- });
+ // describe('Controller addUserRoleByName', () => {
+ // beforeEach(() => {
+ // mockRequest.params.username = `test`;
+ // mockRequest.body = 'new role';
+ // });
+
+ // it('should return status 201 and the updated user with that role', async () => {
+ // await addUserRoleByName(mockRequest, mockResponse);
+ // expect(mockResponse.statusValue).toBe(201);
+ // expect(mockResponse.sendValue).toContain('new role');
+ // });
+ // });
+
+ // describe('Controller deleteUserRoleByName', () => {
+ // beforeEach(() => {
+ // mockRequest.params.username = `${mockUser.Username}`;
+ // mockRequest.body = 'new role';
+ // });
+ // // TODO: remove stub test when controller is complete
+ // it('should return the stub response of 501', async () => {
+ // await deleteUserRoleByName(mockRequest, mockResponse);
+ // expect(mockResponse.statusValue).toBe(501);
+ // });
+
+ // // TODO: enable other tests when controller is complete
+ // xit('should return status 204 and updated user without role', async () => {
+ // await deleteUserRoleByName(mockRequest, mockResponse);
+ // expect(mockResponse.statusValue).toBe(204);
+ // expect(mockResponse.jsonValue.roles).not.toContain('new role');
+ // });
+ // });
});
diff --git a/express-api/tests/unit/controllers/users/usersController.test.ts b/express-api/tests/unit/controllers/users/usersController.test.ts
index 65313caf5..ae519964a 100644
--- a/express-api/tests/unit/controllers/users/usersController.test.ts
+++ b/express-api/tests/unit/controllers/users/usersController.test.ts
@@ -1,6 +1,37 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
import { Request, Response } from 'express';
import controllers from '@/controllers';
-import { MockReq, MockRes, getRequestHandlerMocks } from '../../../testUtils/factories';
+import {
+ MockReq,
+ MockRes,
+ getRequestHandlerMocks,
+ produceRequest,
+} from '../../../testUtils/factories';
+import { IKeycloakUser } from '@/services/keycloak/IKeycloakUser';
+import { AccessRequests } from '@/typeorm/Entities/AccessRequests';
+import { faker } from '@faker-js/faker';
+import { KeycloakUser } from '@bcgov/citz-imb-kc-express';
+
+const _activateUser = jest.fn();
+const _getAccessRequest = jest.fn().mockImplementation(() => produceRequest());
+const _getAccessRequestById = jest.fn().mockImplementation(() => produceRequest());
+const _deleteAccessRequest = jest.fn().mockImplementation((req) => req);
+const _addAccessRequest = jest.fn().mockImplementation((req) => req);
+const _updateAccessRequest = jest.fn().mockImplementation((req) => req);
+const _getAgencies = jest.fn().mockImplementation(() => ['1', '2', '3']);
+const _getAdministrators = jest.fn();
+
+jest.mock('@/services/users/usersServices', () => ({
+ activateUser: () => _activateUser(),
+ getAccessRequest: () => _getAccessRequest(),
+ getAccessRequestById: () => _getAccessRequestById(),
+ deleteAccessRequest: (request: AccessRequests) => _deleteAccessRequest(request),
+ addAccessRequest: (request: AccessRequests, _kc: KeycloakUser) => _addAccessRequest(request),
+ updateAccessRequest: (request: AccessRequests, _kc: KeycloakUser) =>
+ _updateAccessRequest(request),
+ getAgencies: () => _getAgencies(),
+ getAdministrators: () => _getAdministrators(),
+}));
describe('UNIT - Testing controllers for users routes.', () => {
let mockRequest: Request & MockReq, mockResponse: Response & MockRes;
@@ -12,47 +43,44 @@ describe('UNIT - Testing controllers for users routes.', () => {
});
describe('GET /users/info ', () => {
- it('should return stub response 501', async () => {
- await controllers.getUserInfo(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- xit('should return status 200 and keycloak info', async () => {
+ it('should return status 200 and keycloak info', async () => {
+ const header = { a: faker.string.alphanumeric() };
+ const payload = { b: faker.string.alphanumeric() };
+ mockRequest.token = btoa(JSON.stringify(header)) + '.' + btoa(JSON.stringify(payload));
await controllers.getUserInfo(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(mockResponse.sendValue.b).toBe(payload.b);
});
});
describe('GET /users/access/requests', () => {
- it('should return stub response 501', async () => {
- await controllers.getUserAccessRequestLatest(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- xit('should return status 200 and an access request', async () => {
+ it('should return status 200 and an access request', async () => {
await controllers.getUserAccessRequestLatest(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(mockResponse.sendValue.Id).toBeDefined();
});
- xit('should return status 204 if no requests', async () => {
+ it('should return status 204 if no requests', async () => {
+ _getAccessRequest.mockImplementationOnce(() => null);
await controllers.getUserAccessRequestLatest(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(204);
+ expect(mockResponse.sendValue.Id).toBeUndefined();
});
});
describe('GET /users/access/requests/:requestId', () => {
- it('should return stub response 501', async () => {
- await controllers.getUserAccessRequestById(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
+ const request = produceRequest();
- xit('should return status 200 and an access request', async () => {
- mockRequest.params.requestId = '1';
+ it('should return status 200 and an access request', async () => {
+ _getAccessRequestById.mockImplementationOnce(() => request);
+ mockRequest.params.requestId = String(request.Id);
await controllers.getUserAccessRequestById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(mockResponse.sendValue.Id).toBe(request.Id);
});
- xit('should return status 404 if no request found', async () => {
+ it('should return status 404 if no request found', async () => {
+ _getAccessRequestById.mockImplementationOnce(() => null);
mockRequest.params.requestId = '-1';
await controllers.getUserAccessRequestById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(404);
@@ -60,96 +88,59 @@ describe('UNIT - Testing controllers for users routes.', () => {
});
describe('PUT /users/access/requests/:requestId', () => {
- it('should return stub response 501', async () => {
- await controllers.updateUserAccessRequest(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- xit('should return status 200 and an access request', async () => {
+ it('should return status 200 and an access request', async () => {
+ const request = produceRequest();
mockRequest.params.requestId = '1';
- mockRequest.body = {};
+ mockRequest.body = request;
await controllers.updateUserAccessRequest(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(mockResponse.sendValue.Id).toBe(request.Id);
});
- xit('should return status 400 if malformed', async () => {
+ it('should return status 400 if malformed', async () => {
mockRequest.params.requestId = '1';
mockRequest.body = {};
+ _updateAccessRequest.mockImplementationOnce(() => {
+ throw Error();
+ });
await controllers.updateUserAccessRequest(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(400);
});
});
describe('POST /users/access/requests', () => {
- it('should return stub response 501', async () => {
- await controllers.submitUserAccessRequest(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- xit('should return status 201 and an access request', async () => {
- mockRequest.body = {};
+ it('should return status 201 and an access request', async () => {
+ const request = produceRequest();
+ mockRequest.body = request;
await controllers.submitUserAccessRequest(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(mockResponse.sendValue.Id).toBe(request.Id);
});
- xit('should return status 400 if malformed', async () => {
+ it('should return status 400 if malformed', async () => {
mockRequest.body = {};
+ _addAccessRequest.mockImplementationOnce(() => {
+ throw Error();
+ });
await controllers.submitUserAccessRequest(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(400);
});
});
describe('GET /users/agencies/:username', () => {
- it('should return stub response 501', async () => {
- await controllers.submitUserAccessRequest(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- xit('should return status 200 and an int array', async () => {
+ it('should return status 200 and an int array', async () => {
mockRequest.params.username = 'john';
- await controllers.submitUserAccessRequest(mockRequest, mockResponse);
+ await controllers.getUserAgencies(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
+ expect(Array.isArray(mockResponse.sendValue));
});
- xit('should return status 404 if no user exists', async () => {
+ it('should return status 400 if no user exists', async () => {
mockRequest.params.username = '11111';
- await controllers.submitUserAccessRequest(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(404);
- });
- });
-
- describe('GET /reports/users', () => {
- it('should return stub response 501', async () => {
- await controllers.getUserReport(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- xit('should return status 200 and csv data', async () => {
- await controllers.getUserReport(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(200);
- });
-
- xit('should return status 400 if malformed', async () => {
- await controllers.getUserReport(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(400);
- });
- });
-
- describe('POST /reports/users/filter', () => {
- it('should return stub response 501', async () => {
- await controllers.filterUserReport(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(501);
- });
-
- xit('should return status 200 and csv data', async () => {
- mockRequest.body = {};
- await controllers.filterUserReport(mockRequest, mockResponse);
- expect(mockResponse.statusValue).toBe(200);
- });
-
- xit('should return status 400 if malformed', async () => {
- mockRequest.body = {};
- await controllers.filterUserReport(mockRequest, mockResponse);
+ _getAgencies.mockImplementationOnce(() => {
+ throw Error();
+ });
+ await controllers.getUserAgencies(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(400);
});
});
diff --git a/express-api/tests/unit/services/admin/rolesServices.test.ts b/express-api/tests/unit/services/admin/rolesServices.test.ts
new file mode 100644
index 000000000..b9297f6d7
--- /dev/null
+++ b/express-api/tests/unit/services/admin/rolesServices.test.ts
@@ -0,0 +1,62 @@
+import { AppDataSource } from '@/appDataSource';
+import rolesServices from '@/services/admin/rolesServices';
+import { Roles } from '@/typeorm/Entities/Users_Roles_Claims';
+import { produceRole } from 'tests/testUtils/factories';
+import { DeepPartial } from 'typeorm';
+
+const _rolesFind = jest
+ .spyOn(AppDataSource.getRepository(Roles), 'find')
+ .mockImplementation(async () => [produceRole()]);
+const _rolesSave = jest
+ .spyOn(AppDataSource.getRepository(Roles), 'save')
+ .mockImplementation(async (role: DeepPartial & Roles) => role);
+const _rolesUpdate = jest
+ .spyOn(AppDataSource.getRepository(Roles), 'update')
+ .mockImplementation(async (id, role) => ({ raw: {}, generatedMaps: [role], affected: 1 }));
+const _rolesRemove = jest
+ .spyOn(AppDataSource.getRepository(Roles), 'remove')
+ .mockImplementation(async (role) => role);
+
+const _roleFindOne = jest
+ .spyOn(AppDataSource.getRepository(Roles), 'findOne')
+ .mockImplementation(async () => produceRole());
+
+describe('UNIT - Admin roles services', () => {
+ beforeEach(() => jest.clearAllMocks());
+
+ describe('getRoles', () => {
+ it('should get all roles', async () => {
+ const roles = await rolesServices.getRoles({});
+ expect(_rolesFind).toHaveBeenCalledTimes(1);
+ expect(Array.isArray(roles)).toBe(true);
+ });
+ });
+
+ describe('addRole', () => {
+ it('should save a role and return it', async () => {
+ _roleFindOne.mockResolvedValueOnce(null);
+ const role = produceRole();
+ const ret = await rolesServices.addRole(role);
+ expect(_rolesSave).toHaveBeenCalledTimes(1);
+ expect(role.Id).toBe(ret.Id);
+ });
+ });
+
+ describe('updateRole', () => {
+ it('should update a role and return it', async () => {
+ const role = produceRole();
+ const ret = await rolesServices.updateRole(role);
+ expect(_rolesUpdate).toHaveBeenCalledTimes(1);
+ expect(ret.Id).toBe(role.Id);
+ });
+ });
+
+ describe('removeRole', () => {
+ it('remove a role and return it', async () => {
+ const role = produceRole();
+ const ret = await rolesServices.removeRole(role);
+ expect(_rolesRemove).toHaveBeenCalledTimes(1);
+ expect(ret.Id).toBe(role.Id);
+ });
+ });
+});
diff --git a/express-api/tests/unit/services/admin/usersServices.test.ts b/express-api/tests/unit/services/admin/usersServices.test.ts
new file mode 100644
index 000000000..f514b8a17
--- /dev/null
+++ b/express-api/tests/unit/services/admin/usersServices.test.ts
@@ -0,0 +1,89 @@
+import { AppDataSource } from '@/appDataSource';
+import userServices from '@/services/admin/usersServices';
+import { IKeycloakRole } from '@/services/keycloak/IKeycloakRole';
+import { Users } from '@/typeorm/Entities/Users_Roles_Claims';
+import { produceUser } from 'tests/testUtils/factories';
+import { DeepPartial } from 'typeorm';
+import { z } from 'zod';
+
+const _usersFind = jest
+ .spyOn(AppDataSource.getRepository(Users), 'find')
+ .mockImplementation(async () => [produceUser()]);
+
+const _usersFindOne = jest
+ .spyOn(AppDataSource.getRepository(Users), 'findOne')
+ .mockImplementation(async () => produceUser());
+
+const _usersSave = jest
+ .spyOn(AppDataSource.getRepository(Users), 'save')
+ .mockImplementation(async (user: DeepPartial & Users) => user);
+
+const _usersUpdate = jest
+ .spyOn(AppDataSource.getRepository(Users), 'update')
+ .mockImplementation(async (id, user) => ({ generatedMaps: [user], raw: {} }));
+
+const _usersRemove = jest
+ .spyOn(AppDataSource.getRepository(Users), 'remove')
+ .mockImplementation(async (user) => user);
+
+jest.mock('@/services/keycloak/keycloakService', () => ({
+ getKeycloakRoles: (): IKeycloakRole[] => [{ name: 'abc' }],
+ getKeycloakUserRoles: (): IKeycloakRole[] => [{ name: 'abc' }],
+ updateKeycloakUserRoles: (username: string, roles: string[]): IKeycloakRole[] =>
+ roles.map((a) => ({ name: a })),
+}));
+
+describe('UNIT - admin user services', () => {
+ describe('getUsers', () => {
+ it('should get a list of all users', async () => {
+ const users = await userServices.getUsers({});
+ expect(_usersFind).toHaveBeenCalledTimes(1);
+ expect(Array.isArray(users)).toBe(true);
+ });
+ });
+ describe('addUser', () => {
+ it('should insert and return the added user', async () => {
+ const user = produceUser();
+ _usersFindOne.mockResolvedValueOnce(null);
+ const retUser = await userServices.addUser(user);
+ expect(_usersSave).toHaveBeenCalledTimes(1);
+ expect(user.Id).toBe(retUser.Id);
+ });
+ });
+ describe('updateUser', () => {
+ it('should update and return the added user', async () => {
+ const user = produceUser();
+ const retUser = await userServices.updateUser(user);
+ expect(_usersUpdate).toHaveBeenCalledTimes(1);
+ expect(user.Id).toBe(retUser.Id);
+ });
+ });
+ describe('deleteUser', () => {
+ it('should delete and return the deleted user', async () => {
+ const user = produceUser();
+ const retUser = await userServices.deleteUser(user);
+ expect(_usersRemove).toHaveBeenCalledTimes(1);
+ expect(user.Id).toBe(retUser.Id);
+ });
+ });
+ describe('getRoles', () => {
+ it('should get names of roles in keycloak', async () => {
+ const roles = await userServices.getKeycloakRoles();
+ expect(z.string().array().safeParse(roles).success).toBe(true);
+ });
+ });
+ describe('getUserRoles', () => {
+ it('should get names of users roles in keycloak', async () => {
+ const roles = await userServices.getKeycloakUserRoles('test');
+ expect(z.string().array().safeParse(roles).success).toBe(true);
+ });
+ });
+ describe('updateUserRoles', () => {
+ it('should update (put style) users roles in keycloak', async () => {
+ const newRoles = ['admin', 'test'];
+ const roles = await userServices.updateKeycloakUserRoles('test', newRoles);
+ expect(z.string().array().safeParse(roles).success).toBe(true);
+ newRoles.forEach((a, i) => expect(a).toBe(newRoles[i]));
+ });
+ });
+});
diff --git a/express-api/tests/unit/services/users/usersServices.test.ts b/express-api/tests/unit/services/users/usersServices.test.ts
new file mode 100644
index 000000000..91858dd2b
--- /dev/null
+++ b/express-api/tests/unit/services/users/usersServices.test.ts
@@ -0,0 +1,180 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import { AppDataSource } from '@/appDataSource';
+import userServices from '@/services/users/usersServices';
+import { AccessRequests } from '@/typeorm/Entities/AccessRequests';
+import { Agencies } from '@/typeorm/Entities/Agencies';
+import { Users } from '@/typeorm/Entities/Users_Roles_Claims';
+import { KeycloakUser } from '@bcgov/citz-imb-kc-express';
+import { produceAgency, produceRequest, produceUser } from 'tests/testUtils/factories';
+
+const _usersFindOneBy = jest
+ .spyOn(AppDataSource.getRepository(Users), 'findOneBy')
+ .mockImplementation(async (_where) => produceUser());
+
+const _usersUpdate = jest
+ .spyOn(AppDataSource.getRepository(Users), 'update')
+ .mockImplementation(async (_where) => ({ raw: {}, generatedMaps: [] }));
+
+const _usersInsert = jest
+ .spyOn(AppDataSource.getRepository(Users), 'insert')
+ .mockImplementation(async (_where) => ({ raw: {}, generatedMaps: [], identifiers: [] }));
+
+const _requestInsert = jest
+ .spyOn(AppDataSource.getRepository(AccessRequests), 'insert')
+ .mockImplementation(async (req) => ({ raw: {}, generatedMaps: [{ ...req }], identifiers: [] }));
+
+const _requestUpdate = jest
+ .spyOn(AppDataSource.getRepository(AccessRequests), 'update')
+ .mockImplementation(async (id, req) => ({
+ raw: {},
+ generatedMaps: [req],
+ identifiers: [],
+ affected: 1,
+ }));
+
+const _requestRemove = jest
+ .spyOn(AppDataSource.getRepository(AccessRequests), 'remove')
+ .mockImplementation(async (req) => req);
+const _requestQueryGetOne = jest.fn().mockImplementation(() => produceRequest());
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const _requestsCreateQueryBuilder: any = {
+ select: () => _requestsCreateQueryBuilder,
+ leftJoinAndSelect: () => _requestsCreateQueryBuilder,
+ where: () => _requestsCreateQueryBuilder,
+ andWhere: () => _requestsCreateQueryBuilder,
+ orderBy: () => _requestsCreateQueryBuilder,
+ getOne: () => _requestQueryGetOne(),
+};
+
+jest
+ .spyOn(AppDataSource.getRepository(AccessRequests), 'createQueryBuilder')
+ .mockImplementation(() => _requestsCreateQueryBuilder);
+
+jest
+ .spyOn(AppDataSource.getRepository(Users), 'find')
+ .mockImplementation(async () => [produceUser()]);
+
+jest
+ .spyOn(AppDataSource.getRepository(Users), 'findOneOrFail')
+ .mockImplementation(async () => produceUser());
+
+jest
+ .spyOn(AppDataSource.getRepository(AccessRequests), 'findOne')
+ .mockImplementation(async () => produceRequest());
+
+jest
+ .spyOn(AppDataSource.getRepository(AccessRequests), 'findOneOrFail')
+ .mockImplementation(async () => produceRequest());
+
+jest
+ .spyOn(AppDataSource.getRepository(Agencies), 'findOne')
+ .mockImplementation(async () => produceAgency());
+
+jest
+ .spyOn(AppDataSource.getRepository(Agencies), 'findOneOrFail')
+ .mockImplementation(async () => produceAgency());
+
+jest
+ .spyOn(AppDataSource.getRepository(Agencies), 'find')
+ .mockImplementation(async () => [produceAgency()]);
+
+describe('UNIT - User services', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const kcUser: KeycloakUser = {
+ preferred_username: 'test',
+ email: 'test@gov.bc.ca',
+ display_name: 'test',
+ identity_provider: 'idir',
+ idir_user_guid: 'test',
+ idir_username: 'test',
+ given_name: 'test',
+ family_name: 'test',
+ };
+
+ describe('activateUser', () => {
+ it('updates a user based off the kc username', async () => {
+ const found = produceUser();
+ found.Username = 'test';
+ _usersFindOneBy.mockResolvedValueOnce(found);
+ const user = await userServices.activateUser(kcUser);
+ expect(_usersFindOneBy).toHaveBeenCalledTimes(1);
+ expect(_usersUpdate).toHaveBeenCalledTimes(1);
+ });
+ it('adds a new user based off the kc username', async () => {
+ _usersFindOneBy.mockResolvedValueOnce(null);
+ const user = await userServices.activateUser(kcUser);
+ expect(_usersFindOneBy).toHaveBeenCalledTimes(1);
+ expect(_usersInsert).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('getAccessRequest', () => {
+ it('should get the latest accessRequest', async () => {
+ const request = await userServices.getAccessRequest(kcUser);
+ expect(AppDataSource.getRepository(AccessRequests).createQueryBuilder).toHaveBeenCalledTimes(
+ 1,
+ );
+ });
+ });
+
+ describe('getAccessRequestById', () => {
+ it('should get the accessRequest at the id specified', async () => {
+ const user = produceUser();
+ const req = produceRequest();
+ req.UserId = user;
+ _usersFindOneBy.mockResolvedValueOnce(user);
+ _requestQueryGetOne.mockImplementationOnce(() => req);
+ const request = await userServices.getAccessRequestById(req.Id, kcUser);
+ });
+ });
+
+ describe('deleteAccessRequest', () => {
+ it('should return a deleted access request', async () => {
+ const req = await userServices.deleteAccessRequest(produceRequest());
+ expect(_requestRemove).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('addAccessRequest', () => {
+ it('should add and return an access request', async () => {
+ const request = produceRequest();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ request.AgencyId = {} as any;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ request.RoleId = {} as any;
+ const req = await userServices.addAccessRequest(request, kcUser);
+ expect(_requestInsert).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('updateAccessRequest', () => {
+ it('should update and return the access request', async () => {
+ const req = produceRequest();
+ _usersFindOneBy.mockResolvedValueOnce(req.UserId);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ req.RoleId = {} as any;
+ const request = await userServices.updateAccessRequest(req, kcUser);
+ expect(_requestUpdate).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('getAgencies', () => {
+ it('should return an array of agency ids', async () => {
+ const agencies = await userServices.getAgencies('test');
+ expect(AppDataSource.getRepository(Users).findOneOrFail).toHaveBeenCalledTimes(1);
+ expect(AppDataSource.getRepository(Agencies).find).toHaveBeenCalledTimes(1);
+ expect(Array.isArray(agencies)).toBe(true);
+ });
+ });
+
+ describe('getAdministrators', () => {
+ it('should return users that have administrative role in the given agencies', async () => {
+ const admins = await userServices.getAdministrators(['123', '456']);
+ expect(AppDataSource.getRepository(Users).find).toHaveBeenCalledTimes(1);
+ expect(Array.isArray(admins)).toBe(true);
+ });
+ });
+});
From 59db332ccfcc43aa707c9f66008d53713428568f Mon Sep 17 00:00:00 2001
From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Date: Fri, 2 Feb 2024 10:35:17 -0800
Subject: [PATCH 05/28] PIMS-545 Transfer Keycloak Users-Roles (#2166)
---
tools/keycloakRoleMapping/.env-template | 5 ++
tools/keycloakRoleMapping/.gitignore | 1 +
tools/keycloakRoleMapping/README.md | 24 +++++++
tools/keycloakRoleMapping/package.json | 20 ++++++
.../keycloakRoleMapping/src/extractRoleMap.ts | 71 +++++++++++++++++++
.../keycloakRoleMapping/src/importRoleMap.ts | 42 +++++++++++
.../src/roleMappingConversion.ts | 12 ++++
tools/keycloakRoleMapping/tsconfig.json | 15 ++++
8 files changed, 190 insertions(+)
create mode 100644 tools/keycloakRoleMapping/.env-template
create mode 100644 tools/keycloakRoleMapping/.gitignore
create mode 100644 tools/keycloakRoleMapping/README.md
create mode 100644 tools/keycloakRoleMapping/package.json
create mode 100644 tools/keycloakRoleMapping/src/extractRoleMap.ts
create mode 100644 tools/keycloakRoleMapping/src/importRoleMap.ts
create mode 100644 tools/keycloakRoleMapping/src/roleMappingConversion.ts
create mode 100644 tools/keycloakRoleMapping/tsconfig.json
diff --git a/tools/keycloakRoleMapping/.env-template b/tools/keycloakRoleMapping/.env-template
new file mode 100644
index 000000000..f8da0d733
--- /dev/null
+++ b/tools/keycloakRoleMapping/.env-template
@@ -0,0 +1,5 @@
+CSS_API_CLIENT_ID= # Keycloak CSS API Service Account client_id
+CSS_API_CLIENT_SECRET= # Keycloak CSS API Service Account client_secret
+
+SSO_INTEGRATION_ID= # Current integration ID. Change between extract and inject commands.
+SSO_ENVIRONMENT= # 'dev', 'test' or 'prod'. Default is 'dev'.
diff --git a/tools/keycloakRoleMapping/.gitignore b/tools/keycloakRoleMapping/.gitignore
new file mode 100644
index 000000000..02b976ffe
--- /dev/null
+++ b/tools/keycloakRoleMapping/.gitignore
@@ -0,0 +1 @@
+extractResults.json
diff --git a/tools/keycloakRoleMapping/README.md b/tools/keycloakRoleMapping/README.md
new file mode 100644
index 000000000..6b8473e87
--- /dev/null
+++ b/tools/keycloakRoleMapping/README.md
@@ -0,0 +1,24 @@
+# Keycloak Roles Transfer Scripts
+
+## Purpose
+
+During the PIMS modernization project, the multiple Keycloak integrations were reworked into one singular one, and the names of roles were also changed from their original values.
+
+These scripts were created to transfer the existing roles and re-map them to the new roles for each user.
+
+## Instructions
+
+### Setup
+
+1. Node must be installed on your local system for this to work.
+2. Use the command `npm i` from this directory to install the necessary dependencies.
+3. Create a `.env` file using the `.env-template` file as an example. These keys should be available through the [Keycloak dashboard](https://bcgov.github.io/sso-requests).
+
+### Commands
+
+- `npm run extract`: Takes all users and roles from specified integration and saves a JSON file in this directory with their mappings.
+- `npm run import`: Uses the JSON file saved in the extract command to transform and apply the old roles to new roles, applying them to relevant users.
+
+### Notes
+
+- Make sure to switch the integration information in your `.env` between extract and import commands.
diff --git a/tools/keycloakRoleMapping/package.json b/tools/keycloakRoleMapping/package.json
new file mode 100644
index 000000000..d2ec4b595
--- /dev/null
+++ b/tools/keycloakRoleMapping/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "keycloak-role-mapping-tool",
+ "version": "1.0.0",
+ "description": "Used to extract roles from existing Keycloak integrations and remap them onto new ones.",
+ "scripts": {
+ "extract": "ts-node ./src/extractRoleMap.ts",
+ "import": "ts-node ./src/importRoleMap.ts",
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Dylan Barkowsky",
+ "license": "ISC",
+ "dependencies": {
+ "@bcgov/citz-imb-kc-css-api": "https://github.com/bcgov/citz-imb-kc-css-api/releases/download/v1.3.4/bcgov-citz-imb-kc-css-api-1.3.4.tgz"
+ },
+ "devDependencies": {
+ "@types/node": "20.11.16",
+ "ts-node": "10.9.2",
+ "typescript": "5.3.3"
+ }
+}
diff --git a/tools/keycloakRoleMapping/src/extractRoleMap.ts b/tools/keycloakRoleMapping/src/extractRoleMap.ts
new file mode 100644
index 000000000..43064ef79
--- /dev/null
+++ b/tools/keycloakRoleMapping/src/extractRoleMap.ts
@@ -0,0 +1,71 @@
+import { getRoles, getUsersWithRole } from '@bcgov/citz-imb-kc-css-api';
+import fs from 'fs';
+
+interface IRole {
+ name: string;
+ composite: boolean;
+}
+
+interface IRolesResponse {
+ data: IRole[];
+}
+
+interface IUser {
+ email: string;
+ firstName: string;
+ lastName: string;
+ username: string;
+}
+
+// Gets list of roles from integration
+const getRoleList = async () => {
+ const result: IRolesResponse = await getRoles();
+ // Filter out base claims
+ return result.data.filter(role => role.composite);
+}
+
+// Create the user/roles object
+const createUserRolesObject = async () => {
+ const roleUsers: Record = {};
+
+ // Get roles
+ const roleList = await getRoleList();
+
+ // Get all users for each role
+ // Runs all calls in parallel
+ await Promise.all(roleList.map(async (role) => {
+ const users = await getUsersWithRole(role.name);
+ roleUsers[role.name] = users.data;
+ }))
+
+ // Convert data to an object of users with a list of all their current roles
+ const usersWithRoles: Record = {};
+ Object.keys(roleUsers).forEach(role => {
+ // For each user with that role
+ roleUsers[role].forEach((user: IUser) => {
+ // Does this already exist in usersWithRoles?
+ if (usersWithRoles[user.username]) {
+ // Just add this role to the list
+ usersWithRoles[user.username].push(role);
+ } else {
+ usersWithRoles[user.username] = [role];
+ }
+ })
+ })
+ return usersWithRoles;
+}
+
+// Save the roles to a file
+const saveResultToFile = async () => {
+ const result = await createUserRolesObject();
+ fs.writeFile('extractResults.json', JSON.stringify(result, null, 2), (err) => {
+ if (err) {
+ console.error('Error writing file', err);
+ } else {
+ console.log('Successfully wrote file: extractResults.json');
+ }
+ });
+}
+
+// Call the saving file function
+saveResultToFile();
diff --git a/tools/keycloakRoleMapping/src/importRoleMap.ts b/tools/keycloakRoleMapping/src/importRoleMap.ts
new file mode 100644
index 000000000..6715722c7
--- /dev/null
+++ b/tools/keycloakRoleMapping/src/importRoleMap.ts
@@ -0,0 +1,42 @@
+import { assignUserRoles } from '@bcgov/citz-imb-kc-css-api';
+import fs from 'fs';
+import data from '../extractResults.json';
+import { getMappedRole } from './roleMappingConversion';
+
+// Seemingly needed so it identifies keys as strings
+const typedData = data as Record;
+
+const importRoles = async () => {
+ const usernames: string[] = Object.keys(typedData);
+
+ await Promise.all(usernames.map(async (username) => {
+ // Get old roles
+ const oldRoles = typedData[username];
+ // Map old roles to new roles
+ // and convert to Set to remove duplicates
+ const newRoles: Set = new Set(oldRoles.map((role: string) => getMappedRole(role)))
+ try {
+ // If there is more than one new role, we have to choose the most permissive
+ // Logic should work if there's only one role as well.
+ switch (true) {
+ case newRoles.has('admin'):
+ await assignUserRoles(username, ['admin']);
+ break;
+ case newRoles.has('auditor'):
+ await assignUserRoles(username, ['auditor']);
+ break;
+ case newRoles.has('general user'):
+ await assignUserRoles(username, ['general user']);
+ break;
+ default:
+ break;
+ }
+
+ } catch (e) {
+ console.error(e);
+ }
+ }))
+ console.log('Finished successfully! Please check Keycloak integration to confirm.')
+}
+
+importRoles();
diff --git a/tools/keycloakRoleMapping/src/roleMappingConversion.ts b/tools/keycloakRoleMapping/src/roleMappingConversion.ts
new file mode 100644
index 000000000..5fa60191c
--- /dev/null
+++ b/tools/keycloakRoleMapping/src/roleMappingConversion.ts
@@ -0,0 +1,12 @@
+export const getMappedRole = (oldRole: string) => {
+ switch (true) {
+ case ['Minister Assistant', 'Minister', 'Assistant Deputy', 'Executive Director', 'View Only Properties'].includes(oldRole):
+ return 'auditor';
+ case ['Manager', 'Agency Administrator', 'Real Estate Analyst', 'Real Estate Manager'].includes(oldRole):
+ return 'general user';
+ case ['System Administrator', 'SRES', 'SRES Financial Reporter', 'SRES Financial', 'SRES Financial Manager'].includes(oldRole):
+ return 'admin';
+ default:
+ throw new Error(`No mapped role found for original role: ${oldRole}`);
+ }
+}
diff --git a/tools/keycloakRoleMapping/tsconfig.json b/tools/keycloakRoleMapping/tsconfig.json
new file mode 100644
index 000000000..3722ba692
--- /dev/null
+++ b/tools/keycloakRoleMapping/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "esModuleInterop": true,
+ "target": "es2022",
+ "noImplicitAny": true,
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "outDir": "dist",
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "resolveJsonModule": true
+ },
+ "include": ["./**/*"]
+}
From c26631efc585969cc07cf46140101bd022e6c4a6 Mon Sep 17 00:00:00 2001
From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Date: Fri, 2 Feb 2024 16:38:17 -0800
Subject: [PATCH 06/28] PIMS-408 Establish Migration Options (#2164)
---
express-api/package.json | 3 +-
.../admin/users/usersController.ts | 2 +
.../src/services/keycloak/keycloakService.ts | 56 ++++---
.../src/services/users/usersServices.ts | 2 +
.../helperScripts/migrationScript.sh | 51 +++++++
.../admin/roles/rolesController.test.ts | 49 +++++-
.../admin/users/usersController.test.ts | 140 +++++++++++++++++-
.../controllers/users/usersController.test.ts | 37 ++++-
.../unit/services/admin/usersServices.test.ts | 18 +++
.../services/keycloak/keycloakService.test.ts | 2 +-
.../unit/services/users/usersServices.test.ts | 63 ++++++++
11 files changed, 383 insertions(+), 40 deletions(-)
create mode 100644 express-api/src/typeorm/utilities/helperScripts/migrationScript.sh
rename express-api/{src => tests/unit}/services/keycloak/keycloakService.test.ts (98%)
diff --git a/express-api/package.json b/express-api/package.json
index f07c51dff..cd15b218d 100644
--- a/express-api/package.json
+++ b/express-api/package.json
@@ -15,7 +15,8 @@
"test:unit": "jest --testPathPattern=/tests/unit",
"test:integration": "jest --testPathPattern=/tests/integration",
"swagger": "node ./src/swagger/swagger.mjs",
- "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/.bin/typeorm"
+ "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli",
+ "migration": "sh ./src/typeorm/utilities/helperScripts/migrationScript.sh"
},
"author": "",
"license": "ISC",
diff --git a/express-api/src/controllers/admin/users/usersController.ts b/express-api/src/controllers/admin/users/usersController.ts
index 539b3b62d..8869ec17c 100644
--- a/express-api/src/controllers/admin/users/usersController.ts
+++ b/express-api/src/controllers/admin/users/usersController.ts
@@ -92,6 +92,7 @@ export const updateUserById = async (req: Request, res: Response) => {
"bearerAuth": []
}]
*/
+ // TODO: This schema check should not throw an uncaught error when failing. Handle properly.
const id = z.string().uuid().parse(req.params.id);
if (id != req.body.Id) {
return res.status(400).send('The param ID does not match the request body.');
@@ -119,6 +120,7 @@ export const deleteUserById = async (req: Request, res: Response) => {
}]
*/
+ // TODO: This schema check should not throw an uncaught error when failing. Handle properly.
const id = z.string().uuid().parse(req.params.id);
if (id != req.body.Id) {
return res.status(400).send('The param ID does not match the request body.');
diff --git a/express-api/src/services/keycloak/keycloakService.ts b/express-api/src/services/keycloak/keycloakService.ts
index 0edaee157..af4cea3e5 100644
--- a/express-api/src/services/keycloak/keycloakService.ts
+++ b/express-api/src/services/keycloak/keycloakService.ts
@@ -20,17 +20,16 @@ import {
unassignUserRole,
IDIRUserQuery,
} from '@bcgov/citz-imb-kc-css-api';
-import rolesServices from '../admin/rolesServices';
+import rolesServices from '@/services/admin/rolesServices';
import { randomUUID } from 'crypto';
import { AppDataSource } from '@/appDataSource';
import { DeepPartial, In, Not } from 'typeorm';
-import userServices from '../admin/usersServices';
+import userServices from '@/services/admin/usersServices';
import { Users, Roles } from '@/typeorm/Entities/Users_Roles_Claims';
/**
* @description Sync keycloak roles into PIMS roles.
*/
-// TODO: Complete when role service is complete.
const syncKeycloakRoles = async () => {
// Gets roles from keycloak
// For each role
@@ -145,7 +144,6 @@ const updateKeycloakRole = async (roleName: string, newRoleName: string) => {
return role;
};
-// TODO: Complete when user and role services are complete.
const syncKeycloakUser = async (keycloakGuid: string) => {
// Does user exist in Keycloak?
// Get their existing roles.
@@ -259,11 +257,17 @@ const getKeycloakUser = async (guid: string) => {
}
};
+/**
+ * @description Retrieves a Keycloak user's roles.
+ * @param {string} username The user's username.
+ * @returns {IKeycloakRole[]} A list of the user's roles.
+ * @throws If the user is not found.
+ */
const getKeycloakUserRoles = async (username: string) => {
const existingRolesResponse: IKeycloakRolesResponse | IKeycloakErrorResponse =
await getUserRoles(username);
if (!keycloakUserRolesSchema.safeParse(existingRolesResponse).success) {
- const message = `keycloakService.getKeycloakUser: ${
+ const message = `keycloakService.getKeycloakUserRoles: ${
(existingRolesResponse as IKeycloakErrorResponse).message
}`;
logger.warn(message);
@@ -276,30 +280,38 @@ const getKeycloakUserRoles = async (username: string) => {
* @description Updates a user's roles in Keycloak.
* @param {string} username The user's username.
* @param {string[]} roles A list of roles that the user should have.
- * @returns {IKeycloakRole[]} A list of Keycloak roles.
+ * @returns {IKeycloakRole[]} A list of the updated Keycloak roles.
* @throws If the user does not exist.
*/
const updateKeycloakUserRoles = async (username: string, roles: string[]) => {
- const existingRolesResponse = await getKeycloakUserRoles(username);
+ try {
+ const existingRolesResponse = await getKeycloakUserRoles(username);
- // User is found in Keycloak.
- const existingRoles: string[] = existingRolesResponse.map((role) => role.name);
+ // User is found in Keycloak.
+ const existingRoles: string[] = existingRolesResponse.map((role) => role.name);
- // Find roles that are in Keycloak but are not in new user info.
- const rolesToRemove = existingRoles.filter((existingRole) => !roles.includes(existingRole));
- // Remove old roles
- // No call to remove all as list, so have to loop.
- rolesToRemove.forEach(async (role) => {
- await unassignUserRole(username, role);
- });
+ // Find roles that are in Keycloak but are not in new user info.
+ const rolesToRemove = existingRoles.filter((existingRole) => !roles.includes(existingRole));
+ // Remove old roles
+ // No call to remove all as list, so have to loop.
+ rolesToRemove.forEach(async (role) => {
+ await unassignUserRole(username, role);
+ });
- // Find new roles that aren't in Keycloak already.
- const rolesToAdd = roles.filter((newRole) => !existingRoles.includes(newRole));
- // Add new roles
- const updatedRoles: IKeycloakRolesResponse = await assignUserRoles(username, rolesToAdd);
+ // Find new roles that aren't in Keycloak already.
+ const rolesToAdd = roles.filter((newRole) => !existingRoles.includes(newRole));
+ // Add new roles
+ const updatedRoles: IKeycloakRolesResponse = await assignUserRoles(username, rolesToAdd);
- // Return updated list of roles
- return updatedRoles.data;
+ // Return updated list of roles
+ return updatedRoles.data;
+ } catch (e: unknown) {
+ const message = `keycloakService.updateKeycloakUserRoles: ${
+ (e as IKeycloakErrorResponse).message
+ }`;
+ logger.warn(message);
+ throw new Error(message);
+ }
};
const KeycloakService = {
diff --git a/express-api/src/services/users/usersServices.ts b/express-api/src/services/users/usersServices.ts
index 46aa49f3a..4977587b2 100644
--- a/express-api/src/services/users/usersServices.ts
+++ b/express-api/src/services/users/usersServices.ts
@@ -193,6 +193,7 @@ const getAdministrators = async (agencyIds: string[]) => {
};
const userServices = {
+ getUser,
activateUser,
getAccessRequest,
getAccessRequestById,
@@ -201,6 +202,7 @@ const userServices = {
updateAccessRequest,
getAgencies,
getAdministrators,
+ normalizeKeycloakUser,
};
export default userServices;
diff --git a/express-api/src/typeorm/utilities/helperScripts/migrationScript.sh b/express-api/src/typeorm/utilities/helperScripts/migrationScript.sh
new file mode 100644
index 000000000..8410b4b36
--- /dev/null
+++ b/express-api/src/typeorm/utilities/helperScripts/migrationScript.sh
@@ -0,0 +1,51 @@
+# Check if arguments were forgotten.
+if [ $# -eq 0 ]
+then
+ echo "
+ No arguments detected. Valid commands are as follows:
+ - npm run migration create
+ - npm run migration generate
+ - npm run migration run
+ - npm run migration revert"
+ exit 1
+fi
+
+# Colours for text output.
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+# If the recommended name argument was missing, run commands with default name.
+function missing_name {
+ echo "${RED}WARNING: Missing argument for migration name.${NC} Using default name 'migration'.";
+ if [ $1 -eq "create" ]
+ then
+ npm run typeorm -- migration:$1 ./src/typeorm/migrations/migration
+ else
+ npm run typeorm -- migration:$1 -d ./src/appDataSource.ts ./src/typeorm/migrations/migration
+ fi
+ exit 0
+}
+
+# Switch statement decides migration action.
+case $1 in
+ "create")
+ if [ -z "$2"]
+ then
+ missing_name $1
+ fi
+ npm run typeorm -- migration:create ./src/typeorm/migrations/$2
+ ;;
+ "generate")
+ if [ -z "$2"]
+ then
+ missing_name $1
+ fi
+ npm run typeorm -- migration:generate -d ./src/appDataSource.ts ./src/typeorm/migrations/$2
+ ;;
+ "run")
+ npm run typeorm -- migration:run -d ./src/appDataSource.ts
+ ;;
+ "revert")
+ npm run typeorm -- migration:revert -d ./src/appDataSource.ts
+ ;;
+esac
diff --git a/express-api/tests/unit/controllers/admin/roles/rolesController.test.ts b/express-api/tests/unit/controllers/admin/roles/rolesController.test.ts
index a65e07937..612e64540 100644
--- a/express-api/tests/unit/controllers/admin/roles/rolesController.test.ts
+++ b/express-api/tests/unit/controllers/admin/roles/rolesController.test.ts
@@ -50,6 +50,15 @@ describe('UNIT - Roles Admin', () => {
expect(mockResponse.statusValue).toBe(200);
expect(Array.isArray(mockResponse.sendValue)).toBe(true);
});
+
+ it('should return status 400 if incorrect query params are sent', async () => {
+ mockRequest.query = {
+ page: 'not good',
+ };
+ await getRoles(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Could not parse filter.');
+ });
});
describe('Controller addRole', () => {
@@ -63,12 +72,20 @@ describe('UNIT - Roles Admin', () => {
expect(mockResponse.statusValue).toBe(201);
expect(role.Id).toBe(mockResponse.sendValue.Id);
});
+
+ it('should return a 400 status code if adding a user is unsuccessful', async () => {
+ _addRole.mockImplementationOnce((role) => {
+ throw new Error(role.name);
+ });
+ await addRole(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ });
});
describe('Controller getRoleById', () => {
const role = produceRole();
beforeEach(() => {
- _getRole.mockImplementationOnce(() => role);
+ _getRole.mockImplementation(() => role);
mockRequest.params.id = role.Id;
});
@@ -77,6 +94,12 @@ describe('UNIT - Roles Admin', () => {
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.Id).toBe(role.Id);
});
+
+ it('should return status 404 if the role cannot be found', async () => {
+ _getRole.mockImplementationOnce(() => undefined);
+ await getRoleById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(404);
+ });
});
describe('Controller updateRoleById', () => {
@@ -92,19 +115,41 @@ describe('UNIT - Roles Admin', () => {
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.Name).toBe('new name');
});
+
+ it('should return status 400 if body id and param id do not match', async () => {
+ mockRequest.params.id = '9999';
+ await updateRoleById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Request param id did not match request body id.');
+ });
});
describe('Controller deleteRoleById', () => {
const role = produceRole();
beforeEach(() => {
mockRequest.params.id = role.Id;
+ mockRequest.body = role;
});
it('should return status 204', async () => {
- mockRequest.body = role;
await deleteRoleById(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.Id).toBe(role.Id);
});
+
+ it('should return status 400 if body id and param id do not match', async () => {
+ mockRequest.params.id = '9999';
+ await deleteRoleById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Request param id did not match request body id.');
+ });
+
+ it('should return status 400 if the rolesService.removeRole throws an error', async () => {
+ _deleteRole.mockImplementationOnce((role) => {
+ throw new Error(role.name);
+ });
+ await deleteRoleById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ });
});
});
diff --git a/express-api/tests/unit/controllers/admin/users/usersController.test.ts b/express-api/tests/unit/controllers/admin/users/usersController.test.ts
index 367d7fd70..8d11fd259 100644
--- a/express-api/tests/unit/controllers/admin/users/usersController.test.ts
+++ b/express-api/tests/unit/controllers/admin/users/usersController.test.ts
@@ -7,7 +7,9 @@ import {
produceUser,
} from '../../../../testUtils/factories';
import { Roles } from '@/constants/roles';
-import { UserFiltering } from '@/controllers/admin/users/usersSchema';
+import { faker } from '@faker-js/faker';
+import { UUID } from 'crypto';
+import { updateUserRolesByName } from '@/controllers/admin/users/usersController';
let mockRequest: Request & MockReq, mockResponse: Response & MockRes;
@@ -16,6 +18,7 @@ const {
//addUserRoleByName,
getUserById,
getUserRolesByName,
+ getAllRoles,
getUsers,
//getUsersSameAgency,
deleteUserById,
@@ -42,7 +45,7 @@ const _getUserRoles = jest.fn().mockImplementation(() => {
return ['admin'];
});
const _updateUserRoles = jest.fn().mockImplementation((username, roles) => {
- return [roles];
+ return roles;
});
const _getUserById = jest.fn().mockImplementation(() => produceUser());
@@ -74,14 +77,23 @@ describe('UNIT - Users Admin', () => {
});
it('should return a list of users based off the filter', async () => {
- mockRequest.body = {
+ mockRequest.query = {
position: 'Tester',
- } as UserFiltering;
+ };
await getUsers(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
expect(Array.isArray(mockResponse.sendValue)).toBe(true);
expect(mockResponse.sendValue.length === 1);
});
+
+ it('should return status 400 when given a filter that fails to parse', async () => {
+ mockRequest.query = {
+ page: 'hi',
+ };
+ await getUsers(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Failed to parse filter query.');
+ });
});
// describe('Controller getUsersSameAgency', () => {
@@ -101,7 +113,7 @@ describe('UNIT - Users Admin', () => {
describe('Controller addUser', () => {
const user = produceUser();
beforeEach(() => {
- _addUser.mockImplementationOnce(() => user);
+ _addUser.mockImplementation(() => user);
mockRequest.body = user;
});
@@ -110,12 +122,20 @@ describe('UNIT - Users Admin', () => {
expect(mockResponse.statusValue).toBe(201);
expect(mockResponse.sendValue.Id).toBe(mockRequest.body.Id);
});
+
+ it('should return status 400 when the userService.addUser throws an error', async () => {
+ _addUser.mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await addUser(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ });
});
describe('Controller getUserById', () => {
const user = produceUser();
beforeEach(() => {
- _getUserById.mockImplementationOnce(() => user);
+ _getUserById.mockImplementation(() => user);
});
it('should return status 200 and the user info', async () => {
@@ -124,13 +144,27 @@ describe('UNIT - Users Admin', () => {
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.Id).toBe(user.Id);
});
+
+ it('should return status 400 if the uuid cannot be parsed', async () => {
+ mockRequest.params.id = 'hello';
+ await getUserById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Could not parse UUID.');
+ });
+
+ it('should return status 404 userService.getUserById does not find a user', async () => {
+ mockRequest.params.id = user.Id;
+ _getUserById.mockImplementationOnce(() => undefined);
+ await getUserById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(404);
+ });
});
describe('Controller updateUserById', () => {
const user = produceUser();
user.Email = 'newEmail@gov.bc.ca';
beforeEach(() => {
- _updateUser.mockImplementationOnce(() => user);
+ _updateUser.mockImplementation(() => user);
mockRequest.params.id = user.Id;
mockRequest.body = { ...user, Email: 'newEmail@gov.bc.ca' };
});
@@ -140,12 +174,27 @@ describe('UNIT - Users Admin', () => {
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.Email).toBe('newEmail@gov.bc.ca');
});
+
+ it('should return status 400 if the param ID does not match the body ID', async () => {
+ mockRequest.params.id = faker.string.uuid() as UUID;
+ await updateUserById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('The param ID does not match the request body.');
+ });
+
+ it('should return status 400 if userService.updateUser throws an error', async () => {
+ _updateUser.mockImplementation(() => {
+ throw new Error();
+ });
+ await updateUserById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ });
});
describe('Controller deleteUserById', () => {
const user = produceUser();
beforeEach(() => {
- _deleteUser.mockImplementationOnce(() => user);
+ _deleteUser.mockImplementation(() => user);
mockRequest.params.id = user.Id;
mockRequest.body = user;
});
@@ -155,6 +204,21 @@ describe('UNIT - Users Admin', () => {
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.Id).toBe(user.Id);
});
+
+ it('should return status 400 if the param ID does not match the body ID', async () => {
+ mockRequest.params.id = faker.string.uuid() as UUID;
+ await deleteUserById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('The param ID does not match the request body.');
+ });
+
+ it('should return status 400 if userService.deleteUser throws an error', async () => {
+ _deleteUser.mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await deleteUserById(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ });
});
describe('Controller getUserRolesByName', () => {
@@ -169,6 +233,66 @@ describe('UNIT - Users Admin', () => {
expect(mockResponse.sendValue).toHaveLength(1);
expect(mockResponse.sendValue.at(0)).toBe('admin');
});
+
+ it('should return status 400 if params.username is not provided', async () => {
+ mockRequest.params = {};
+ await getUserRolesByName(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Username was empty.');
+ });
+ });
+
+ describe('Controller getAllRoles', () => {
+ const user = produceUser();
+ beforeEach(() => {
+ mockRequest.params = {
+ username: user.Username,
+ };
+ });
+ it('should return status 200 and a list of roles assigned to a user', async () => {
+ await getAllRoles(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(200);
+ expect(mockResponse.sendValue.at(0)).toBe('admin');
+ });
+
+ it('should return status 400 when no username is provided', async () => {
+ mockRequest.params = {};
+ await getAllRoles(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Username was empty.');
+ });
+ });
+
+ describe('Controller updateUserRolesByName', () => {
+ const user = produceUser();
+ beforeEach(() => {
+ _updateUserRoles.mockImplementation((username, roles) => roles);
+ mockRequest.params = {
+ username: user.Username,
+ };
+ mockRequest.body = ['admin', 'auditor'];
+ });
+ it('should return status 200 and a list of updated roles', async () => {
+ await updateUserRolesByName(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(200);
+ // TODO: Check the return value. It's currently undefined for some reason.
+ });
+
+ it('should return status 400 if the request body was not parsed successfully', async () => {
+ mockRequest.body = {
+ notGood: true,
+ };
+ await updateUserRolesByName(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Request body was wrong format.');
+ });
+
+ it('should return 400 if params.username was not provided', async () => {
+ mockRequest.params = {};
+ await updateUserRolesByName(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ expect(mockResponse.sendValue).toBe('Username was empty.');
+ });
});
// describe('Controller addUserRoleByName', () => {
diff --git a/express-api/tests/unit/controllers/users/usersController.test.ts b/express-api/tests/unit/controllers/users/usersController.test.ts
index ae519964a..a841efa30 100644
--- a/express-api/tests/unit/controllers/users/usersController.test.ts
+++ b/express-api/tests/unit/controllers/users/usersController.test.ts
@@ -42,7 +42,7 @@ describe('UNIT - Testing controllers for users routes.', () => {
mockResponse = mockRes;
});
- describe('GET /users/info ', () => {
+ describe('getUserInfo', () => {
it('should return status 200 and keycloak info', async () => {
const header = { a: faker.string.alphanumeric() };
const payload = { b: faker.string.alphanumeric() };
@@ -51,9 +51,26 @@ describe('UNIT - Testing controllers for users routes.', () => {
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue.b).toBe(payload.b);
});
+
+ it('should return 400 when an invalid JWT is sent', async () => {
+ mockRequest.token = 'hello';
+ await controllers.getUserInfo(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ });
+
+ it('should return 400 when no JWT is sent', async () => {
+ await controllers.getUserInfo(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ });
+
+ // FIXME: I don't think we should be throwing errors from controllers
+ // it('should throw an error when either side of the jwt cannot be parsed', () => {
+ // mockRequest.token = 'hello.goodbye';
+ // expect(() => {controllers.getUserInfo(mockRequest, mockResponse)}).toThrow();
+ // })
});
- describe('GET /users/access/requests', () => {
+ describe('getUserAccessRequestLatest', () => {
it('should return status 200 and an access request', async () => {
await controllers.getUserAccessRequestLatest(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
@@ -66,9 +83,17 @@ describe('UNIT - Testing controllers for users routes.', () => {
expect(mockResponse.statusValue).toBe(204);
expect(mockResponse.sendValue.Id).toBeUndefined();
});
+
+ it('should return status 400 if userService.getAccessRequest throws an error', async () => {
+ _getAccessRequest.mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await controllers.getUserAccessRequestLatest(mockRequest, mockResponse);
+ expect(mockResponse.statusValue).toBe(400);
+ });
});
- describe('GET /users/access/requests/:requestId', () => {
+ describe('getUserAccessRequestById', () => {
const request = produceRequest();
it('should return status 200 and an access request', async () => {
@@ -87,7 +112,7 @@ describe('UNIT - Testing controllers for users routes.', () => {
});
});
- describe('PUT /users/access/requests/:requestId', () => {
+ describe('updateUserAccessRequest', () => {
it('should return status 200 and an access request', async () => {
const request = produceRequest();
mockRequest.params.requestId = '1';
@@ -108,7 +133,7 @@ describe('UNIT - Testing controllers for users routes.', () => {
});
});
- describe('POST /users/access/requests', () => {
+ describe('submitUserAccessRequest', () => {
it('should return status 201 and an access request', async () => {
const request = produceRequest();
mockRequest.body = request;
@@ -127,7 +152,7 @@ describe('UNIT - Testing controllers for users routes.', () => {
});
});
- describe('GET /users/agencies/:username', () => {
+ describe('getUserAgencies', () => {
it('should return status 200 and an int array', async () => {
mockRequest.params.username = 'john';
await controllers.getUserAgencies(mockRequest, mockResponse);
diff --git a/express-api/tests/unit/services/admin/usersServices.test.ts b/express-api/tests/unit/services/admin/usersServices.test.ts
index f514b8a17..84f04ab67 100644
--- a/express-api/tests/unit/services/admin/usersServices.test.ts
+++ b/express-api/tests/unit/services/admin/usersServices.test.ts
@@ -49,6 +49,12 @@ describe('UNIT - admin user services', () => {
expect(_usersSave).toHaveBeenCalledTimes(1);
expect(user.Id).toBe(retUser.Id);
});
+
+ it('should throw an error if the user already exists', async () => {
+ const user = produceUser();
+ _usersFindOne.mockResolvedValueOnce(user);
+ expect(async () => await userServices.addUser(user)).rejects.toThrow();
+ });
});
describe('updateUser', () => {
it('should update and return the added user', async () => {
@@ -57,6 +63,12 @@ describe('UNIT - admin user services', () => {
expect(_usersUpdate).toHaveBeenCalledTimes(1);
expect(user.Id).toBe(retUser.Id);
});
+
+ it('should throw an error if the user does not exist', () => {
+ const user = produceUser();
+ _usersFindOne.mockResolvedValueOnce(undefined);
+ expect(async () => await userServices.updateUser(user)).rejects.toThrow();
+ });
});
describe('deleteUser', () => {
it('should delete and return the deleted user', async () => {
@@ -65,6 +77,12 @@ describe('UNIT - admin user services', () => {
expect(_usersRemove).toHaveBeenCalledTimes(1);
expect(user.Id).toBe(retUser.Id);
});
+
+ it('should throw an error if the user does not exist', () => {
+ const user = produceUser();
+ _usersFindOne.mockResolvedValueOnce(undefined);
+ expect(async () => await userServices.deleteUser(user)).rejects.toThrow();
+ });
});
describe('getRoles', () => {
it('should get names of roles in keycloak', async () => {
diff --git a/express-api/src/services/keycloak/keycloakService.test.ts b/express-api/tests/unit/services/keycloak/keycloakService.test.ts
similarity index 98%
rename from express-api/src/services/keycloak/keycloakService.test.ts
rename to express-api/tests/unit/services/keycloak/keycloakService.test.ts
index 798772012..764b35d4b 100644
--- a/express-api/src/services/keycloak/keycloakService.test.ts
+++ b/express-api/tests/unit/services/keycloak/keycloakService.test.ts
@@ -1,5 +1,5 @@
import { IKeycloakUser } from '@/services/keycloak/IKeycloakUser';
-import KeycloakService from './keycloakService';
+import KeycloakService from '../../../../src/services/keycloak/keycloakService';
import {
getRoles,
getRole,
diff --git a/express-api/tests/unit/services/users/usersServices.test.ts b/express-api/tests/unit/services/users/usersServices.test.ts
index 91858dd2b..e39ebb71e 100644
--- a/express-api/tests/unit/services/users/usersServices.test.ts
+++ b/express-api/tests/unit/services/users/usersServices.test.ts
@@ -94,6 +94,25 @@ describe('UNIT - User services', () => {
family_name: 'test',
};
+ describe('getUser', () => {
+ const user = produceUser();
+ it('should return a user when called with a UUID', async () => {
+ jest
+ .spyOn(AppDataSource.getRepository(Users), 'findOneBy')
+ .mockImplementationOnce(async () => user);
+ const result = await userServices.getUser(user.Id);
+ expect(result.FirstName).toBe(user.FirstName);
+ });
+
+ it('should return a user when called with a username', async () => {
+ jest
+ .spyOn(AppDataSource.getRepository(Users), 'findOneBy')
+ .mockImplementationOnce(async () => user);
+ const result = await userServices.getUser(user.Username);
+ expect(result.FirstName).toBe(user.FirstName);
+ });
+ });
+
describe('activateUser', () => {
it('updates a user based off the kc username', async () => {
const found = produceUser();
@@ -136,6 +155,15 @@ describe('UNIT - User services', () => {
const req = await userServices.deleteAccessRequest(produceRequest());
expect(_requestRemove).toHaveBeenCalledTimes(1);
});
+
+ it('should throw an error if the access request does not exist', () => {
+ jest
+ .spyOn(AppDataSource.getRepository(AccessRequests), 'findOne')
+ .mockImplementationOnce(() => undefined);
+ expect(
+ async () => await userServices.deleteAccessRequest(produceRequest()),
+ ).rejects.toThrow();
+ });
});
describe('addAccessRequest', () => {
@@ -148,6 +176,10 @@ describe('UNIT - User services', () => {
const req = await userServices.addAccessRequest(request, kcUser);
expect(_requestInsert).toHaveBeenCalledTimes(1);
});
+
+ it('should throw an error if the provided access request is null', () => {
+ expect(async () => await userServices.addAccessRequest(null, kcUser)).rejects.toThrow();
+ });
});
describe('updateAccessRequest', () => {
@@ -159,6 +191,10 @@ describe('UNIT - User services', () => {
const request = await userServices.updateAccessRequest(req, kcUser);
expect(_requestUpdate).toHaveBeenCalledTimes(1);
});
+
+ it('should throw an error if the provided access request is null', () => {
+ expect(async () => await userServices.updateAccessRequest(null, kcUser)).rejects.toThrow();
+ });
});
describe('getAgencies', () => {
@@ -177,4 +213,31 @@ describe('UNIT - User services', () => {
expect(Array.isArray(admins)).toBe(true);
});
});
+
+ describe('normalizeKeycloakUser', () => {
+ it('should return a normalized user from IDIR', () => {
+ const result = userServices.normalizeKeycloakUser(kcUser);
+ expect(result.given_name).toBe(kcUser.given_name);
+ expect(result.family_name).toBe(kcUser.family_name);
+ expect(result.username).toBe(kcUser.preferred_username);
+ });
+
+ // TODO: This function looks like it should handle BCeID users, but the param type doesn't fit
+ // It should also allow business and basic bceid users, not just basic
+ // it('should return a normalized BCeID user', () => {
+ // const bceidUser = {
+ // identity_provider: 'bciedbasic',
+ // preferred_username: 'Test',
+ // bceid_user_guid: '00000000000000000000000000000000',
+ // bceid_username: 'test',
+ // display_name: 'Test',
+ // email: 'test@gov.bc.ca'
+ // }
+ // const result = userServices.normalizeKeycloakUser(bceidUser);
+ // expect(result.given_name).toBe('');
+ // expect(result.family_name).toBe('');
+ // expect(result.username).toBe(bceidUser.preferred_username);
+ // expect(result.guid).toBe('00000000-0000-0000-0000-000000000000')
+ // })
+ });
});
From 0baf563036dc3dae2a6f12721e3c0727d0b5974f Mon Sep 17 00:00:00 2001
From: TaylorFries <78506153+TaylorFries@users.noreply.github.com>
Date: Tue, 6 Feb 2024 14:50:58 -0800
Subject: [PATCH 07/28] Sprint 14 Updating express-api minor NPM Dependencies
(#2170)
---
express-api/package.json | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/express-api/package.json b/express-api/package.json
index cd15b218d..f548683dc 100644
--- a/express-api/package.json
+++ b/express-api/package.json
@@ -28,33 +28,33 @@
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"cors": "2.8.5",
- "dotenv": "16.3.1",
+ "dotenv": "16.4.1",
"express": "4.18.2",
"express-rate-limit": "7.1.1",
"morgan": "1.10.0",
"pg": "8.11.3",
- "reflect-metadata": "0.1.14",
+ "reflect-metadata": "0.2.1",
"swagger-ui-express": "5.0.0",
"typeorm": "0.3.17",
"winston": "3.11.0",
"zod": "3.22.4"
},
"devDependencies": {
- "@faker-js/faker": "8.3.1",
+ "@faker-js/faker": "8.4.0",
"@types/compression": "1.7.4",
"@types/cookie-parser": "1.4.5",
"@types/cors": "2.8.15",
"@types/express": "4.17.20",
"@types/jest": "29.5.10",
"@types/morgan": "1.9.9",
- "@types/node": "20.8.7",
+ "@types/node": "20.11.13",
"@types/supertest": "2.0.16",
"@types/swagger-ui-express": "4.1.6",
- "@typescript-eslint/eslint-plugin": "6.12.0",
- "@typescript-eslint/parser": "6.12.0",
- "eslint": "8.54.0",
- "eslint-config-prettier": "9.0.0",
- "eslint-plugin-prettier": "5.0.1",
+ "@typescript-eslint/eslint-plugin": "6.20.0",
+ "@typescript-eslint/parser": "6.20.0",
+ "eslint": "8.56.0",
+ "eslint-config-prettier": "9.1.0",
+ "eslint-plugin-prettier": "5.1.3",
"jest": "29.7.0",
"nodemon": "3.0.1",
"prettier": "3.1.0",
@@ -64,6 +64,6 @@
"ts-node": "10.9.1",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
- "typescript": "5.2.2"
+ "typescript": "5.3.3"
}
}
From 3d3c10545b88d8de3281d862fc674bd15a530945 Mon Sep 17 00:00:00 2001
From: TaylorFries <78506153+TaylorFries@users.noreply.github.com>
Date: Tue, 6 Feb 2024 14:59:07 -0800
Subject: [PATCH 08/28] Sprint 14 Updating Frontend NPM Packages (minor
updates) (#2169)
---
frontend/package.json | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/frontend/package.json b/frontend/package.json
index 6f2910d28..e1c75fee8 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -47,7 +47,7 @@
"polylabel": "1.1.0",
"react": "18.2.0",
"react-app-polyfill": "3.0.0",
- "react-bootstrap": "2.9.0",
+ "react-bootstrap": "2.10.0",
"react-bootstrap-typeahead": "5.2.2",
"react-click-away-listener": "2.2.3",
"react-datepicker": "4.25.0",
@@ -71,13 +71,13 @@
"redux-logger": "3.0.6",
"redux-thunk": "2.4.2",
"retry-axios": "3.1.3",
- "sass": "1.69.0",
+ "sass": "1.70.0",
"styled-components": "6.0.7",
"supercluster": "8.0.1",
"tiles-in-bbox": "1.0.2",
"vite": "5.0.5",
"vite-plugin-svgr": "4.2.0",
- "vite-tsconfig-paths": "4.2.1",
+ "vite-tsconfig-paths": "4.3.1",
"yup": "1.3.1"
},
"devDependencies": {
@@ -85,8 +85,8 @@
"@babel/preset-typescript": "7.23.0",
"@cfaester/enzyme-adapter-react-18": "0.7.1",
"@testing-library/dom": "9.3.1",
- "@testing-library/jest-dom": "6.2.1",
- "@testing-library/react": "14.1.0",
+ "@testing-library/jest-dom": "6.3.0",
+ "@testing-library/react": "14.2.0",
"@testing-library/react-hooks": "8.0.1",
"@testing-library/user-event": "14.5.0",
"@types/crypto-js": "4.2.1",
@@ -116,13 +116,13 @@
"@types/styled-components": "5.1.26",
"@types/supercluster": "7.1.0",
"@types/yup": "0.32.0",
- "@typescript-eslint/eslint-plugin": "6.18.1",
- "@typescript-eslint/parser": "6.18.1",
+ "@typescript-eslint/eslint-plugin": "6.19.1",
+ "@typescript-eslint/parser": "6.19.1",
"axios-mock-adapter": "1.22.0",
"babel-jest": "29.7.0",
"cross-env": "7.0.3",
"cypress": "13.6.0",
- "dotenv": "16.3.1",
+ "dotenv": "16.4.1",
"enzyme": "3.11.0",
"enzyme-to-json": "3.6.2",
"eslint": "8.56.0",
From 4904225261272b668abc3c0dcd6de0aad6c852b2 Mon Sep 17 00:00:00 2001
From: GrahamS-Quartech <112989452+GrahamS-Quartech@users.noreply.github.com>
Date: Tue, 6 Feb 2024 15:22:37 -0800
Subject: [PATCH 09/28] PIMS-1260: React API Hooks (#2167)
Co-authored-by: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
---
express-api/src/typeorm/Entities/Agencies.ts | 2 +-
.../typeorm/Entities/Users_Roles_Claims.ts | 4 +-
.../Entities/abstractEntities/BaseEntity.ts | 12 +--
react-app/src/App.tsx | 25 ++++-
react-app/src/components/table/DataTable.tsx | 2 +-
react-app/src/contexts/authContext.tsx | 32 +++++++
react-app/src/contexts/configContext.tsx | 33 +++++++
react-app/src/guards/AuthRouteGuard.tsx | 21 +++++
react-app/src/hooks/api/useUsersApi.ts | 13 +++
react-app/src/hooks/useAsync.ts | 50 ++++++++++
react-app/src/hooks/useDataLoader.ts | 60 ++++++++++++
react-app/src/hooks/useFetch.ts | 94 +++++++++++++++++++
react-app/src/hooks/usePimsApi.ts | 21 +++++
react-app/src/pages/DevZone.tsx | 71 +++++++++++---
react-app/vite.config.ts | 7 +-
15 files changed, 418 insertions(+), 29 deletions(-)
create mode 100644 react-app/src/contexts/authContext.tsx
create mode 100644 react-app/src/contexts/configContext.tsx
create mode 100644 react-app/src/guards/AuthRouteGuard.tsx
create mode 100644 react-app/src/hooks/api/useUsersApi.ts
create mode 100644 react-app/src/hooks/useAsync.ts
create mode 100644 react-app/src/hooks/useDataLoader.ts
create mode 100644 react-app/src/hooks/useFetch.ts
create mode 100644 react-app/src/hooks/usePimsApi.ts
diff --git a/express-api/src/typeorm/Entities/Agencies.ts b/express-api/src/typeorm/Entities/Agencies.ts
index 55d658eaf..11b03cb8f 100644
--- a/express-api/src/typeorm/Entities/Agencies.ts
+++ b/express-api/src/typeorm/Entities/Agencies.ts
@@ -8,8 +8,8 @@ import {
OneToMany,
Relation,
} from 'typeorm';
-import { Users } from './Users_Roles_Claims';
import { BaseEntity } from './abstractEntities/BaseEntity';
+import { Users } from './Users_Roles_Claims';
@Entity()
@Index(['ParentId', 'IsDisabled', 'Id', 'Name', 'SortOrder']) // I'm not sure this index is needed. How often do we search by this group?
diff --git a/express-api/src/typeorm/Entities/Users_Roles_Claims.ts b/express-api/src/typeorm/Entities/Users_Roles_Claims.ts
index 1ff30378b..adc4f41f8 100644
--- a/express-api/src/typeorm/Entities/Users_Roles_Claims.ts
+++ b/express-api/src/typeorm/Entities/Users_Roles_Claims.ts
@@ -80,10 +80,10 @@ export class Users {
@Index({ unique: true })
KeycloakUserId: string;
- @Column({ name: 'AgencyId', type: 'varchar', length: 6 })
+ @Column({ name: 'AgencyId', type: 'varchar', length: 6, nullable: true })
AgencyId: string;
- @ManyToOne(() => Agencies, (agency) => agency.Users)
+ @ManyToOne(() => Agencies, (agency) => agency.Users, { nullable: true })
@JoinColumn({ name: 'AgencyId' })
Agency: Relation;
diff --git a/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts b/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
index bb8ae898f..d58f45246 100644
--- a/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
+++ b/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
@@ -1,19 +1,19 @@
-import { Users } from '@/typeorm/Entities/Users_Roles_Claims';
-import { Column, CreateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm';
+import type { Users } from '@/typeorm/Entities/Users_Roles_Claims';
+import { Column, CreateDateColumn, ManyToOne, JoinColumn, Index, Relation } from 'typeorm';
export abstract class BaseEntity {
- @ManyToOne(() => Users, (User) => User.Id)
+ @ManyToOne('Users', 'Users.Id')
@JoinColumn({ name: 'CreatedById' })
@Index()
- CreatedById: Users;
+ CreatedById: Relation;
@CreateDateColumn()
CreatedOn: Date;
- @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
+ @ManyToOne('Users', 'Users.Id', { nullable: true })
@JoinColumn({ name: 'UpdatedById' })
@Index()
- UpdatedById: Users;
+ UpdatedById: Relation;
@Column({ type: 'timestamp', nullable: true })
UpdatedOn: Date;
diff --git a/react-app/src/App.tsx b/react-app/src/App.tsx
index 2c3ccb085..1fdd9a825 100644
--- a/react-app/src/App.tsx
+++ b/react-app/src/App.tsx
@@ -5,13 +5,30 @@ import '@/App.css';
import { ThemeProvider } from '@emotion/react';
import appTheme from './themes/appTheme';
import Dev from './pages/DevZone';
+import { ConfigContextProvider } from './contexts/configContext';
+import AuthContextProvider from './contexts/authContext';
+import AuthRouteGuard from './guards/AuthRouteGuard';
+import BaseLayout from './components/layout/BaseLayout';
const Router = () => {
return (
-
- } />
- } />
-
+
+
+
+ } />
+
+
+
+
+
+ }
+ />
+
+
+
);
};
diff --git a/react-app/src/components/table/DataTable.tsx b/react-app/src/components/table/DataTable.tsx
index b4275969f..956d4e56e 100644
--- a/react-app/src/components/table/DataTable.tsx
+++ b/react-app/src/components/table/DataTable.tsx
@@ -21,7 +21,7 @@ type RenderCellParams = GridRenderCellParams {
return (
-
+ No rows to display.
);
diff --git a/react-app/src/contexts/authContext.tsx b/react-app/src/contexts/authContext.tsx
new file mode 100644
index 000000000..b8ab1e7e4
--- /dev/null
+++ b/react-app/src/contexts/authContext.tsx
@@ -0,0 +1,32 @@
+import { AuthService, useKeycloak } from '@bcgov/citz-imb-kc-react';
+import React, { createContext } from 'react';
+export interface IAuthState {
+ keycloak: AuthService;
+}
+export const AuthContext = createContext(undefined);
+
+/**
+ * Provides access to user and authentication (keycloak) data about the logged in user.
+ *
+ * @param {*} props
+ * @return {*}
+ */
+export const AuthContextProvider: React.FC = (props) => {
+ const keycloak = useKeycloak();
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const pimsUser = undefined;
+ // We could later add in a hook to give us user information from this context as well, thus allowing us to obtain
+ // both the keycloak authentication state and our internal user state in the same spot.
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export default AuthContextProvider;
diff --git a/react-app/src/contexts/configContext.tsx b/react-app/src/contexts/configContext.tsx
new file mode 100644
index 000000000..085c414ba
--- /dev/null
+++ b/react-app/src/contexts/configContext.tsx
@@ -0,0 +1,33 @@
+import React, { createContext, useEffect, useState } from 'react';
+
+export interface IConfig {
+ API_HOST: string;
+ NODE_ENV: string;
+}
+
+export const ConfigContext = createContext({
+ API_HOST: '',
+ NODE_ENV: '',
+});
+
+const getConfig = (): IConfig => {
+ return {
+ API_HOST: '/api/v2',
+ NODE_ENV: import.meta.env.MODE,
+ };
+};
+
+/**
+ * Retrieves information from the environment and exposes it within this context.
+ * @param props
+ * @returns
+ */
+export const ConfigContextProvider: React.FC = (props) => {
+ const [config, setConfig] = useState();
+ useEffect(() => {
+ if (!config) {
+ setConfig(getConfig());
+ }
+ }, [config]);
+ return {props.children};
+};
diff --git a/react-app/src/guards/AuthRouteGuard.tsx b/react-app/src/guards/AuthRouteGuard.tsx
new file mode 100644
index 000000000..18b97761e
--- /dev/null
+++ b/react-app/src/guards/AuthRouteGuard.tsx
@@ -0,0 +1,21 @@
+import { AuthContext } from '@/contexts/authContext';
+import { CircularProgress } from '@mui/material';
+import { PropsWithChildren, useContext } from 'react';
+import React from 'react';
+
+/**
+ * AuthRouteGuard - Use this to wrap any component you don't want being rendered until the keycloak authentication state has resolved.
+ * @param props
+ * @returns
+ */
+const AuthRouteGuard = (props: PropsWithChildren) => {
+ const authStateContext = useContext(AuthContext);
+
+ if (!authStateContext.keycloak.isAuthenticated) {
+ return ;
+ }
+
+ return <>{props.children}>;
+};
+
+export default AuthRouteGuard;
diff --git a/react-app/src/hooks/api/useUsersApi.ts b/react-app/src/hooks/api/useUsersApi.ts
new file mode 100644
index 000000000..4df407bd0
--- /dev/null
+++ b/react-app/src/hooks/api/useUsersApi.ts
@@ -0,0 +1,13 @@
+import { IFetch } from '../useFetch';
+
+const useUsersApi = (absoluteFetch: IFetch) => {
+ const getLatestAccessRequest = async () => {
+ const { parsedBody } = await absoluteFetch.get(`/users/access/requests`);
+ return parsedBody;
+ };
+ return {
+ getLatestAccessRequest,
+ };
+};
+
+export default useUsersApi;
diff --git a/react-app/src/hooks/useAsync.ts b/react-app/src/hooks/useAsync.ts
new file mode 100644
index 000000000..9aa6fa2ac
--- /dev/null
+++ b/react-app/src/hooks/useAsync.ts
@@ -0,0 +1,50 @@
+import { useRef } from 'react';
+
+export type AsyncFunction = (
+ ...args: AFArgs
+) => Promise;
+
+/**
+ * useAsync - Wraps an asyncronous function in a hook.
+ * This is useful when trying to make an asyncronous call inside a component that may be duplicated
+ * if it gets re-rendered.
+ *
+ * @param asyncFunction Any arbitray async call.
+ * @returns Promise returned by resolving the async call.
+ */
+const useAsync = (
+ asyncFunction: AsyncFunction,
+): AsyncFunction => {
+ const promiseResponse = useRef>();
+ const isPending = useRef(false);
+ const asyncFunctionWrapper: AsyncFunction = async (...args) => {
+ //This evaluation is the crux of it. If we already have a reference to the async function
+ //and isPending is true, then we already called this and we can just return the reference to the current
+ //async call instead of making a new one.
+ if (promiseResponse.current && isPending.current) {
+ return promiseResponse.current;
+ }
+
+ isPending.current = true;
+
+ //An example of one of few instances where there is probably not a real equivalent using await.
+ promiseResponse.current = asyncFunction(...args).then(
+ (response: AFResponse) => {
+ isPending.current = false;
+
+ return response;
+ },
+ (error) => {
+ isPending.current = false;
+
+ throw error;
+ },
+ );
+
+ return promiseResponse.current;
+ };
+
+ return asyncFunctionWrapper;
+};
+
+export default useAsync;
diff --git a/react-app/src/hooks/useDataLoader.ts b/react-app/src/hooks/useDataLoader.ts
new file mode 100644
index 000000000..2260ddf5c
--- /dev/null
+++ b/react-app/src/hooks/useDataLoader.ts
@@ -0,0 +1,60 @@
+import useAsync, { AsyncFunction } from './useAsync';
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+/**
+ * useDataLoader - Hook that helps you retrieve data from api calls.
+ * Plug in an async api call into the first argument. Then use refresh to make the call, and data to access the response body.
+ *
+ * @param dataFetcher An async api call that returns some data in the response body.
+ * @param errorHandler Handle any errors thrown using this.
+ * @returns {AFResponse} data - async function response
+ * @returns {Function} refreshData - makes the api call
+ * @returns {boolean} isLoading - monitor request status
+ * @returns {Error} error - thrown by making the call
+ */
+const useDataLoader = (
+ dataFetcher: AsyncFunction,
+ errorHandler: (error: AFError) => void,
+) => {
+ //We have this little useEffect here to avoid touching state if this hook suddenly gets unmounted.
+ //React doesn't like it when this happens.
+ const mountedRef = useRef(false);
+ useEffect(() => {
+ mountedRef.current = true;
+
+ return () => {
+ mountedRef.current = false;
+ };
+ }, []);
+ const isMounted = useCallback(() => mountedRef.current, [mountedRef]);
+
+ const [error, setError] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+ const [data, setData] = useState();
+ const getData = useAsync(dataFetcher);
+ const refreshData = async (...args: AFArgs) => {
+ setIsLoading(true);
+ setError(undefined);
+ try {
+ const response = await getData(...args);
+ if (!isMounted) {
+ return;
+ }
+ setData(response);
+ } catch (e) {
+ if (!isMounted) {
+ return;
+ }
+ setError(error);
+ errorHandler?.(error);
+ } finally {
+ if (isMounted) {
+ setIsLoading(false);
+ }
+ }
+ };
+
+ return { refreshData, isLoading, data, error };
+};
+
+export default useDataLoader;
diff --git a/react-app/src/hooks/useFetch.ts b/react-app/src/hooks/useFetch.ts
new file mode 100644
index 000000000..b628c9075
--- /dev/null
+++ b/react-app/src/hooks/useFetch.ts
@@ -0,0 +1,94 @@
+import { useKeycloak } from '@bcgov/citz-imb-kc-react';
+import { useMemo } from 'react';
+
+export type FetchResponse = Response & { parsedBody?: Record };
+export type FetchType = (url: string, params?: RequestInit) => Promise;
+export interface IFetch {
+ get: (url: string, params?: Record) => Promise;
+ put: (url: string, body?: any) => Promise;
+ patch: (url: string, body?: any) => Promise;
+ del: (url: string, body?: any) => Promise;
+ post: (url: string, body?: any) => Promise;
+}
+
+/**
+ * useFetch - hook serving as a wrapper over the native fetch implementation of node.
+ * You can use this pretty similarly to a certain popular library, the baseUrl can be set to avoid typing in the root path all the time,
+ * the authorization header is automatically set, and the request and response bodies are automatically encoded/decoded into JSON.
+ *
+ * @param baseUrl
+ * @returns
+ */
+const useFetch = (baseUrl?: string) => {
+ const keycloak = useKeycloak();
+
+ return useMemo(() => {
+ const absoluteFetch = async (url: string, params?: RequestInit): Promise => {
+ let response: Response;
+
+ params = {
+ ...params,
+ headers: {
+ Authorization: keycloak.getAuthorizationHeaderValue(),
+ },
+ };
+
+ if (params && params.body) {
+ params.body = JSON.stringify(params.body);
+ }
+
+ if (url.startsWith('/')) {
+ response = await fetch(baseUrl + url, params);
+ } else {
+ response = await fetch(url, params);
+ }
+ const text = await response.text();
+ if (text.length) {
+ let parsedBody: any | undefined;
+ try {
+ parsedBody = JSON.parse(text);
+ } catch {
+ parsedBody = text;
+ }
+ return { ...response, parsedBody: parsedBody };
+ } else {
+ return response;
+ }
+ };
+ const buildQueryParms = (params: Record): string => {
+ if (!params) {
+ return '';
+ }
+ const q = Object.entries(params)
+ .map(([k, value]) => {
+ return `${k}=${encodeURIComponent(value)}`;
+ })
+ .join('&');
+ return `?${q}`;
+ };
+ const get = (url: string, params?: Record) => {
+ return absoluteFetch(url + buildQueryParms(params), { method: 'GET' });
+ };
+ const post = (url: string, body: any) => {
+ return absoluteFetch(url, { method: 'POST', body: body });
+ };
+ const put = (url: string, body: any) => {
+ return absoluteFetch(url, { method: 'PUT', body: body });
+ };
+ const patch = (url: string, body: any) => {
+ return absoluteFetch(url, { method: 'PATCH', body: body });
+ };
+ const del = (url: string, body: any) => {
+ return absoluteFetch(url, { method: 'DELETE', body: body });
+ };
+ return {
+ get,
+ patch,
+ put,
+ post,
+ del,
+ };
+ }, [baseUrl, keycloak]);
+};
+
+export default useFetch;
diff --git a/react-app/src/hooks/usePimsApi.ts b/react-app/src/hooks/usePimsApi.ts
new file mode 100644
index 000000000..b8f8c0686
--- /dev/null
+++ b/react-app/src/hooks/usePimsApi.ts
@@ -0,0 +1,21 @@
+import { ConfigContext } from '@/contexts/configContext';
+import { useContext } from 'react';
+import useFetch from './useFetch';
+import useUsersApi from './api/useUsersApi';
+
+/**
+ * usePimsApi - This stores all the sub-hooks we need to make calls to our API and helps manage authentication state for them.
+ * @returns
+ */
+const usePimsApi = () => {
+ const config = useContext(ConfigContext);
+ const fetch = useFetch(config?.API_HOST);
+
+ const users = useUsersApi(fetch);
+
+ return {
+ users,
+ };
+};
+
+export default usePimsApi;
diff --git a/react-app/src/pages/DevZone.tsx b/react-app/src/pages/DevZone.tsx
index e15999e98..d1065af74 100644
--- a/react-app/src/pages/DevZone.tsx
+++ b/react-app/src/pages/DevZone.tsx
@@ -1,18 +1,29 @@
/* eslint-disable no-console */
//Simple component testing area.
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import { CustomDataGrid, DataGridFloatingMenu } from '@/components/table/DataTable';
-import { Box, Chip, Paper } from '@mui/material';
+import { Box, Button, Chip, Paper, Typography } from '@mui/material';
import { GridColDef } from '@mui/x-data-grid';
-import BaseLayout from '@/components/layout/BaseLayout';
import { mdiCheckCircle, mdiCloseThick } from '@mdi/js';
+import usePimsApi from '@/hooks/usePimsApi';
+import useDataLoader from '@/hooks/useDataLoader';
const Dev = () => {
- const colorMap = {
- Pending: 'warning',
- Active: 'success',
- Hold: 'error',
- };
+ const { users } = usePimsApi();
+ const {
+ data: realData,
+ refreshData: refreshRealData,
+ isLoading: realDataLoading,
+ } = useDataLoader(users.getLatestAccessRequest, () => {});
+
+ const {
+ data: fakeData,
+ refreshData: refreshFakeData,
+ isLoading: fakeDataLoading,
+ } = useDataLoader(
+ async () => rows,
+ () => {},
+ );
const rows = [
{ UserId: 0, FirstName: 'Graham', LastName: 'Stewart', Status: 'Active', Date: '2023-04-02' },
@@ -27,6 +38,25 @@ const Dev = () => {
{ UserId: 9, FirstName: 'Daniel', LastName: 'Miller', Status: 'Hold', Date: '2023-04-06' },
];
+ const [dataRows, setDataRows] = useState([]);
+ useEffect(() => {
+ if (fakeData) {
+ setDataRows(fakeData);
+ }
+ }, [fakeData]);
+
+ useEffect(() => {
+ if (!realData) {
+ refreshRealData();
+ }
+ }, [realData]);
+
+ const colorMap = {
+ Pending: 'warning',
+ Active: 'success',
+ Hold: 'error',
+ };
+
const columns: GridColDef[] = [
{
field: 'FirstName',
@@ -80,13 +110,26 @@ const Dev = () => {
];
return (
-
-
-
- row.UserId} columns={columns} rows={rows} />
-
+
+
+
+ {realDataLoading ? (
+ Real API Data loading....
+ ) : (
+ {JSON.stringify(realData, null, 2)}
+ )}
-
+
+
+ row.UserId}
+ columns={columns}
+ rows={dataRows}
+ loading={fakeDataLoading}
+ />
+
+
);
};
diff --git a/react-app/vite.config.ts b/react-app/vite.config.ts
index 4af1ed526..9cbf6b80a 100644
--- a/react-app/vite.config.ts
+++ b/react-app/vite.config.ts
@@ -21,7 +21,12 @@ export default () => {
'/api': {
target: target,
changeOrigin: true,
- rewrite: (path) => path.replace(/^\/api/, ''),
+ rewrite: (path) => {
+ if (path.includes('auth')) {
+ return path.replace(/^\/api/, '');
+ }
+ return path;
+ },
},
},
},
From 5baad42a690723fbf4b8bd31debccf4f2a2931b6 Mon Sep 17 00:00:00 2001
From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Date: Tue, 6 Feb 2024 15:30:34 -0800
Subject: [PATCH 10/28] PIMS-1201 Update react-app Dependencies (#2174)
---
react-app/package.json | 34 +++++++++++++++++-----------------
1 file changed, 17 insertions(+), 17 deletions(-)
diff --git a/react-app/package.json b/react-app/package.json
index 6852ba5a1..76514a0ae 100644
--- a/react-app/package.json
+++ b/react-app/package.json
@@ -14,29 +14,29 @@
"snapshots": "jest --updateSnapshot"
},
"dependencies": {
- "@bcgov/bc-sans": "^2.1.0",
+ "@bcgov/bc-sans": "2.1.0",
"@bcgov/citz-imb-kc-react": "https://github.com/bcgov/citz-imb-kc-react/releases/download/v1.1.0/bcgov-citz-imb-kc-react-1.1.0.tgz",
- "@emotion/react": "^11.11.3",
- "@emotion/styled": "^11.11.0",
- "@mdi/js": "^7.4.47",
- "@mdi/react": "^1.6.1",
- "@mui/material": "^5.15.3",
- "@mui/x-data-grid": "^6.18.7",
+ "@emotion/react": "11.11.3",
+ "@emotion/styled": "11.11.0",
+ "@mdi/js": "7.4.47",
+ "@mdi/react": "1.6.1",
+ "@mui/material": "5.15.6",
+ "@mui/x-data-grid": "6.19.2",
"react": "18.2.0",
"react-dom": "18.2.0",
- "react-hook-form": "7.49.3",
- "react-router-dom": "6.21.1"
+ "react-hook-form": "7.50.1",
+ "react-router-dom": "6.22.0"
},
"devDependencies": {
"@babel/preset-env": "7.23.8",
"@babel/preset-react": "7.23.3",
- "@testing-library/jest-dom": "6.2.0",
- "@testing-library/react": "14.1.2",
+ "@testing-library/jest-dom": "6.3.0",
+ "@testing-library/react": "14.2.0",
"@types/jest": "29.5.11",
"@types/react": "18.2.43",
"@types/react-dom": "18.2.17",
- "@typescript-eslint/eslint-plugin": "6.17.0",
- "@typescript-eslint/parser": "6.17.0",
+ "@typescript-eslint/eslint-plugin": "6.19.1",
+ "@typescript-eslint/parser": "6.19.1",
"@vitejs/plugin-react": "4.2.1",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
@@ -44,12 +44,12 @@
"eslint-plugin-react": "7.33.2",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
- "prettier": "3.1.1",
+ "prettier": "3.2.4",
"react-test-renderer": "18.2.0",
"ts-jest": "29.1.1",
"ts-node": "10.9.2",
- "typescript": "5.2.2",
- "vite": "5.0.8",
- "vite-tsconfig-paths": "4.2.3"
+ "typescript": "5.3.3",
+ "vite": "5.0.12",
+ "vite-tsconfig-paths": "4.3.1"
}
}
From 88fb798f8afd2cf900b4c4c09b278401d325b184 Mon Sep 17 00:00:00 2001
From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Date: Wed, 7 Feb 2024 08:55:24 -0800
Subject: [PATCH 11/28] PIMS-668 Users Table (#2148)
---
react-app/.eslintrc.cjs | 2 +-
react-app/package.json | 4 +-
react-app/src/App.tsx | 13 +
react-app/src/components/layout/Header.tsx | 2 +-
.../src/components/table/KeywordSearch.tsx | 112 ++++++
react-app/src/hooks/api/useUsersApi.ts | 6 +
react-app/src/hooks/useDataLoader.ts | 2 +-
react-app/src/interfaces/IUser.ts | 27 ++
react-app/src/pages/UsersTable.tsx | 372 ++++++++++++++++++
react-app/src/themes/appTheme.ts | 20 +
react-app/src/utilities/downloadExcelFile.ts | 54 +++
11 files changed, 610 insertions(+), 4 deletions(-)
create mode 100644 react-app/src/components/table/KeywordSearch.tsx
create mode 100644 react-app/src/interfaces/IUser.ts
create mode 100644 react-app/src/pages/UsersTable.tsx
create mode 100644 react-app/src/utilities/downloadExcelFile.ts
diff --git a/react-app/.eslintrc.cjs b/react-app/.eslintrc.cjs
index b340830c9..85609e6eb 100644
--- a/react-app/.eslintrc.cjs
+++ b/react-app/.eslintrc.cjs
@@ -36,7 +36,7 @@ module.exports = {
'no-prototype-builtins': 'off',
'react/prop-types': 'off',
'react/display-name': 'off',
- 'no-console': 'error',
+ 'no-console': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-inferrable-types': 'off', // ie. const val: number = 4;
'@typescript-eslint/no-empty-function': 'off', // ie. {}
diff --git a/react-app/package.json b/react-app/package.json
index 76514a0ae..edf2c4b5e 100644
--- a/react-app/package.json
+++ b/react-app/package.json
@@ -15,13 +15,15 @@
},
"dependencies": {
"@bcgov/bc-sans": "2.1.0",
- "@bcgov/citz-imb-kc-react": "https://github.com/bcgov/citz-imb-kc-react/releases/download/v1.1.0/bcgov-citz-imb-kc-react-1.1.0.tgz",
+ "@bcgov/citz-imb-kc-react": "https://github.com/bcgov/citz-imb-kc-react/releases/download/v1.4.0/bcgov-citz-imb-kc-react-1.4.0.tgz",
"@emotion/react": "11.11.3",
"@emotion/styled": "11.11.0",
+ "@mui/icons-material": "5.15.6",
"@mdi/js": "7.4.47",
"@mdi/react": "1.6.1",
"@mui/material": "5.15.6",
"@mui/x-data-grid": "6.19.2",
+ "node-xlsx": "0.23.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "7.50.1",
diff --git a/react-app/src/App.tsx b/react-app/src/App.tsx
index 1fdd9a825..64f0777bc 100644
--- a/react-app/src/App.tsx
+++ b/react-app/src/App.tsx
@@ -9,6 +9,7 @@ import { ConfigContextProvider } from './contexts/configContext';
import AuthContextProvider from './contexts/authContext';
import AuthRouteGuard from './guards/AuthRouteGuard';
import BaseLayout from './components/layout/BaseLayout';
+import UsersTable from '@/pages/UsersTable';
const Router = () => {
return (
@@ -26,6 +27,18 @@ const Router = () => {
}
/>
+
+
+
+
+
+
+ }
+ />
+
diff --git a/react-app/src/components/layout/Header.tsx b/react-app/src/components/layout/Header.tsx
index 7ee4d62c7..18fe84f7a 100644
--- a/react-app/src/components/layout/Header.tsx
+++ b/react-app/src/components/layout/Header.tsx
@@ -78,7 +78,7 @@ const Header: React.FC = () => {
Disposal Inventory
-
+
Users
>
diff --git a/react-app/src/components/table/KeywordSearch.tsx b/react-app/src/components/table/KeywordSearch.tsx
new file mode 100644
index 000000000..bd4afee75
--- /dev/null
+++ b/react-app/src/components/table/KeywordSearch.tsx
@@ -0,0 +1,112 @@
+import { SxProps, TextField, InputAdornment, useTheme } from '@mui/material';
+import React, { Dispatch, SetStateAction, useState } from 'react';
+import SearchIcon from '@mui/icons-material/Search';
+import CloseIcon from '@mui/icons-material/Close';
+
+/**
+ * @interface
+ * @description Props for the KeywordSearch field.
+ * @prop {Function} onChange (Optional) Function to run when value of field changes.
+ * @prop {[string, Dispatch>]} optionalExternalState (Optional) An external state getter and setter for field value. Component also contains internal state if that suffices.
+ */
+interface IKeywordSearchProps {
+ onChange?: Function;
+ optionalExternalState?: [string, Dispatch>];
+}
+
+/**
+ * @description Input field that is a search icon when minimized but can be expanded upon click.
+ * @param props Properties passed to the component.
+ */
+const KeywordSearch = (props: IKeywordSearchProps) => {
+ const { onChange, optionalExternalState } = props;
+ const [isOpen, setIsOpen] = useState(false);
+ const [fieldContents, setFieldContents] = optionalExternalState
+ ? optionalExternalState
+ : useState('');
+ const theme = useTheme();
+
+ // Style shared when both open and closed
+ const commonStyle: SxProps = {
+ fontSize: theme.typography.fontWeightBold,
+ fontFamily: theme.typography.fontFamily,
+ padding: '5px',
+ marginBottom: '1px',
+ boxSizing: 'content-box',
+ borderRadius: '5px',
+ };
+
+ // Style when open
+ const openStyle: SxProps = {
+ ...commonStyle,
+ width: '240px',
+ transition: 'width 0.3s ease-in, border 1s',
+ border: `1.5px solid ${theme.palette.grey[400]}`,
+ '&:focus-within': {
+ border: '1.5px solid black',
+ },
+ };
+
+ // Style when closed
+ const closedStyle: SxProps = {
+ ...commonStyle,
+ width: '32px',
+ transition: 'width 0.3s ease-in, border 1s',
+ border: '1.5px solid transparent',
+ '&:hover': {
+ cursor: 'default',
+ },
+ };
+ return (
+ {
+ setFieldContents(e.target.value);
+ if (onChange) onChange(e.target.value);
+ }}
+ InputProps={{
+ disableUnderline: true,
+ sx: { cursor: 'default' },
+ endAdornment: (
+
+ {fieldContents ? (
+ {
+ // Clear text and filter
+ setFieldContents('');
+ if (onChange) onChange('');
+ document.getElementById('keyword-search').focus();
+ }}
+ sx={{
+ '&:hover': {
+ cursor: 'pointer',
+ },
+ }}
+ />
+ ) : (
+ {
+ setIsOpen(!isOpen);
+ document.getElementById('keyword-search').focus();
+ }}
+ sx={{
+ '&:hover': {
+ cursor: 'pointer',
+ },
+ }}
+ />
+ )}
+
+ ),
+ }}
+ />
+ );
+};
+
+export default KeywordSearch;
diff --git a/react-app/src/hooks/api/useUsersApi.ts b/react-app/src/hooks/api/useUsersApi.ts
index 4df407bd0..7348cf1bd 100644
--- a/react-app/src/hooks/api/useUsersApi.ts
+++ b/react-app/src/hooks/api/useUsersApi.ts
@@ -5,8 +5,14 @@ const useUsersApi = (absoluteFetch: IFetch) => {
const { parsedBody } = await absoluteFetch.get(`/users/access/requests`);
return parsedBody;
};
+
+ const getAllUsers = async () => {
+ const { parsedBody } = await absoluteFetch.get('/admin/users');
+ return parsedBody;
+ };
return {
getLatestAccessRequest,
+ getAllUsers,
};
};
diff --git a/react-app/src/hooks/useDataLoader.ts b/react-app/src/hooks/useDataLoader.ts
index 2260ddf5c..5bb637cbe 100644
--- a/react-app/src/hooks/useDataLoader.ts
+++ b/react-app/src/hooks/useDataLoader.ts
@@ -14,7 +14,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
*/
const useDataLoader = (
dataFetcher: AsyncFunction,
- errorHandler: (error: AFError) => void,
+ errorHandler: (error: AFError) => void = () => {},
) => {
//We have this little useEffect here to avoid touching state if this hook suddenly gets unmounted.
//React doesn't like it when this happens.
diff --git a/react-app/src/interfaces/IUser.ts b/react-app/src/interfaces/IUser.ts
new file mode 100644
index 000000000..9164e0012
--- /dev/null
+++ b/react-app/src/interfaces/IUser.ts
@@ -0,0 +1,27 @@
+import { UUID } from 'crypto';
+
+/**
+ * @interface
+ * @description Defines the user object returned from the API.
+ */
+export interface IUser {
+ createdOn: string;
+ updatedOn: string;
+ updatedByName: string;
+ updatedByEmail: string;
+ id: UUID;
+ keycloakid: UUID;
+ username: string;
+ position: string;
+ displayName: string;
+ firstName: string;
+ middleName: string;
+ lastName: string;
+ email: string;
+ isDisabled: true;
+ emailVerified: true;
+ note: string;
+ lastLogin: string;
+ agency: string;
+ roles: string; // TODO: Are Agency and Roles going to be singular or multiple?
+}
diff --git a/react-app/src/pages/UsersTable.tsx b/react-app/src/pages/UsersTable.tsx
new file mode 100644
index 000000000..fa909a4b4
--- /dev/null
+++ b/react-app/src/pages/UsersTable.tsx
@@ -0,0 +1,372 @@
+import { CustomDataGrid } from '@/components/table/DataTable';
+import {
+ Box,
+ Chip,
+ Paper,
+ SxProps,
+ Typography,
+ debounce,
+ useTheme,
+ IconButton,
+ Select,
+ ListSubheader,
+ MenuItem,
+ Tooltip,
+} from '@mui/material';
+import { GridColDef, gridFilteredSortedRowEntriesSelector, useGridApiRef } from '@mui/x-data-grid';
+import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react';
+import { useKeycloak } from '@bcgov/citz-imb-kc-react';
+import FilterAltOffIcon from '@mui/icons-material/FilterAltOff';
+import KeywordSearch from '@/components/table/KeywordSearch';
+import { IUser } from '@/interfaces/IUser';
+import AddIcon from '@mui/icons-material/Add';
+import DownloadIcon from '@mui/icons-material/Download';
+import { downloadExcelFile } from '@/utilities/downloadExcelFile';
+import useDataLoader from '@/hooks/useDataLoader';
+import usePimsApi from '@/hooks/usePimsApi';
+
+const CustomMenuItem = (props: PropsWithChildren & { value: string }) => {
+ const theme = useTheme();
+ return (
+
+ );
+};
+
+const CustomListSubheader = (props: PropsWithChildren) => {
+ const theme = useTheme();
+ return (
+
+ {props.children}
+
+ );
+};
+
+const UsersTable = () => {
+ // States and contexts
+ const [users, setUsers] = useState([]);
+ const [rowCount, setRowCount] = useState(0);
+ const [keywordSearchContents, setKeywordSearchContents] = useState('');
+ const [selectValue, setSelectValue] = useState('All Users');
+ const [gridFilterItems, setGridFilterItems] = useState([]);
+ const { state } = useKeycloak();
+ const theme = useTheme();
+ const tableApiRef = useGridApiRef(); // Ref to MUI DataGrid
+
+ // Getting data from API
+ const usersApi = usePimsApi();
+ const { data, refreshData, isLoading, error } = useDataLoader(usersApi.users.getAllUsers);
+
+ useEffect(() => {
+ if (error) {
+ console.error(error);
+ }
+ if (data) {
+ setUsers(data as IUser[]);
+ } else {
+ refreshData();
+ }
+ }, [state, data]);
+
+ // Determines colours of chips
+ const colorMap = {
+ Pending: 'info',
+ Active: 'success',
+ Hold: 'warning',
+ };
+
+ // Sets quickfilter value of DataGrid. newValue is a string input.
+ const updateSearchValue = useMemo(() => {
+ return debounce((newValue) => {
+ tableApiRef.current.setQuickFilterValues(newValue.split(' ').filter((word) => word !== ''));
+ }, 100);
+ }, [tableApiRef]);
+
+ // Converts dates to the MMM DD, YYYY locale
+ const dateFormatter = (params) =>
+ params.value
+ ? new Intl.DateTimeFormat('en-US', {
+ month: 'short',
+ day: '2-digit',
+ year: 'numeric',
+ }).format(new Date(params.value))
+ : undefined;
+
+ // Sets the preset filter based on the select input
+ const selectPresetFilter = (value: string) => {
+ // Clear the quick search contents
+ setKeywordSearchContents('');
+ switch (value) {
+ case 'All Users':
+ tableApiRef.current.setFilterModel({ items: [] });
+ break;
+ // All Status filters
+ case 'Active':
+ case 'Pending':
+ case 'Hold':
+ tableApiRef.current.setFilterModel({
+ items: [
+ {
+ value,
+ operator: 'contains',
+ field: 'Status',
+ },
+ ],
+ });
+ break;
+ // All Role filters
+ case 'User':
+ case 'Admin':
+ tableApiRef.current.setFilterModel({
+ items: [
+ {
+ value,
+ operator: 'contains',
+ field: 'Role',
+ },
+ ],
+ });
+ break;
+ }
+ };
+
+ // Defines the columns used in the table.
+ const columns: GridColDef[] = [
+ {
+ field: 'FirstName',
+ headerName: 'First Name',
+ flex: 1,
+ minWidth: 125,
+ },
+ {
+ field: 'LastName',
+ headerName: 'Last Name',
+ flex: 1,
+ minWidth: 125,
+ },
+ {
+ field: 'Status',
+ headerName: 'Status',
+ renderCell: (params) => {
+ if (!params.value) return <>>;
+ return (
+
+ );
+ },
+ maxWidth: 100,
+ },
+ {
+ field: 'Email',
+ headerName: 'Email Address',
+ minWidth: 150,
+ flex: 1,
+ },
+ {
+ field: 'Username',
+ headerName: 'IDIR/BCeID',
+ minWidth: 150,
+ flex: 1,
+ },
+ {
+ field: 'Agency',
+ headerName: 'Agency',
+ minWidth: 125,
+ flex: 1,
+ },
+ {
+ field: 'Position',
+ headerName: 'Position',
+ minWidth: 150,
+ flex: 1,
+ },
+ {
+ field: 'Role',
+ headerName: 'Role',
+ minWidth: 100,
+ flex: 1,
+ },
+ {
+ field: 'CreatedOn',
+ headerName: 'Created',
+ minWidth: 120,
+ valueFormatter: dateFormatter,
+ type: 'date',
+ },
+ {
+ field: 'LastLogin',
+ headerName: 'Last Login',
+ minWidth: 120,
+ valueFormatter: dateFormatter,
+ type: 'date',
+ },
+ ];
+
+ return (
+
+
+
+
+
+ Users Overview ({rowCount ?? 0} users)
+
+ {keywordSearchContents || gridFilterItems.length > 0 ? (
+
+ {
+ // Set both DataGrid and Keyword search back to blanks
+ tableApiRef.current.setFilterModel({ items: [] });
+ setKeywordSearchContents('');
+ // Set select field back to default
+ setSelectValue('All Users');
+ }}
+ >
+
+
+
+ ) : (
+ <>>
+ )}
+
+ *': {
+ // Applies to all children
+ margin: '0 2px',
+ },
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ {
+ downloadExcelFile({
+ data: gridFilteredSortedRowEntriesSelector(tableApiRef),
+ tableName: 'UsersTable',
+ filterName: selectValue,
+ includeDate: true,
+ });
+ }}
+ >
+
+
+
+
+
+
+ row.Id}
+ columns={columns}
+ rows={users}
+ loading={isLoading}
+ onStateChange={(e) => {
+ // Keep track of row count separately
+ setRowCount(Object.values(e.filter.filteredRowsLookup).filter((value) => value).length);
+ }}
+ onFilterModelChange={(e) => {
+ // Get the filter items from MUI, filter out blanks, set state
+ setGridFilterItems(e.items.filter((item) => item.value));
+ }}
+ apiRef={tableApiRef}
+ initialState={{
+ pagination: { paginationModel: { pageSize: 10 } },
+ sorting: {
+ sortModel: [{ field: 'created', sort: 'desc' }],
+ },
+ }}
+ pageSizeOptions={[10, 20, 30, 100]} // DataGrid max is 100
+ disableRowSelectionOnClick
+ sx={{
+ minHeight: '200px',
+ overflow: 'scroll',
+ // Neutralize the hover colour (causing a flash)
+ '& .MuiDataGrid-row.Mui-hovered': {
+ backgroundColor: 'transparent',
+ },
+ // Take out the hover colour
+ '& .MuiDataGrid-row:hover': {
+ backgroundColor: 'transparent',
+ },
+ '& .MuiDataGrid-cell:focus-within': {
+ outline: 'none',
+ },
+ }}
+ slots={{ toolbar: KeywordSearch }}
+ />
+
+
+ );
+};
+
+export default UsersTable;
diff --git a/react-app/src/themes/appTheme.ts b/react-app/src/themes/appTheme.ts
index 834655c09..f214806e8 100644
--- a/react-app/src/themes/appTheme.ts
+++ b/react-app/src/themes/appTheme.ts
@@ -62,6 +62,7 @@ const appTheme = createTheme({
},
},
typography: {
+ fontWeightMedium: 400,
fontFamily: ['BC Sans', 'Verdana', 'Arial', 'sans-serif'].join(','),
h1: {
fontSize: '2.25rem',
@@ -108,6 +109,16 @@ const appTheme = createTheme({
},
},
},
+ MuiSelect: {
+ styleOverrides: {
+ root: {
+ boxShadow: 'none',
+ ':hover': {
+ boxShadow: 'none',
+ },
+ },
+ },
+ },
MuiLink: {
styleOverrides: {
root: {
@@ -134,6 +145,15 @@ const appTheme = createTheme({
},
},
},
+ MuiIconButton: {
+ styleOverrides: {
+ root: {
+ ':hover': {
+ backgroundColor: '#f8f8f8',
+ },
+ },
+ },
+ },
},
});
diff --git a/react-app/src/utilities/downloadExcelFile.ts b/react-app/src/utilities/downloadExcelFile.ts
new file mode 100644
index 000000000..03b5a0ec8
--- /dev/null
+++ b/react-app/src/utilities/downloadExcelFile.ts
@@ -0,0 +1,54 @@
+import { GridRowId, GridValidRowModel } from '@mui/x-data-grid';
+import xlsx from 'node-xlsx';
+
+/**
+ * @interface
+ * @description Properties expected for downloadExcelFile function.
+ */
+export interface IExcelDownloadProps {
+ tableName: string;
+ data: {
+ id: GridRowId;
+ model: GridValidRowModel;
+ }[];
+ filterName?: string;
+ includeDate?: boolean;
+}
+
+/**
+ * @description Uses exported rows from MUI DataGrid to build and download an Excel file.
+ * @param {IExcelDownloadProps} props
+ */
+export const downloadExcelFile = (props: IExcelDownloadProps) => {
+ const { tableName, data, filterName, includeDate } = props;
+ // No point exporting if there are no data
+ if (data.length > 0) {
+ // Extract column headers
+ const columnHeaders = Object.keys(data.at(0).model);
+ // Build xlsx file as bit array buffer
+ const bitArray = xlsx.build([
+ {
+ name: tableName,
+ data: [columnHeaders, ...data.map((row) => Object.values(row.model))],
+ options: {}, // Required even if empty
+ },
+ ]);
+ // Combine into one string of data
+ const binaryString = bitArray.reduce((acc, cur) => (acc += String.fromCharCode(cur)), '');
+ // Convert data into file
+ const file = window.btoa(binaryString);
+ const url = `data:application/xlsx;base64,${file}`;
+ // Create file name
+ const fileName = `${includeDate ? new Date().toISOString().substring(0, 10) : ''}_${tableName}${
+ '-' + filterName || ''
+ }`;
+ // Download file
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${fileName}.xlsx`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ window.URL.revokeObjectURL(url);
+ }
+};
From c6ce5314227c849db8664b366a4d9d266a247183 Mon Sep 17 00:00:00 2001
From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Date: Mon, 12 Feb 2024 09:34:00 -0800
Subject: [PATCH 12/28] PIMS-1275 Update react-datepicker to 6.1.0 (frontend)
(#2180)
---
frontend/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/package.json b/frontend/package.json
index e1c75fee8..3986fd80b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -50,7 +50,7 @@
"react-bootstrap": "2.10.0",
"react-bootstrap-typeahead": "5.2.2",
"react-click-away-listener": "2.2.3",
- "react-datepicker": "4.25.0",
+ "react-datepicker": "6.1.0",
"react-dom": "18.2.0",
"react-draggable": "4.4.5",
"react-error-boundary": "4.0.11",
From 238744bac08c4183a9336c7f665a923ff980646c Mon Sep 17 00:00:00 2001
From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Date: Mon, 12 Feb 2024 09:45:57 -0800
Subject: [PATCH 13/28] PIMS-1298 Update react-router-dom to 6.22.0 (frontend)
(#2179)
---
frontend/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/package.json b/frontend/package.json
index 3986fd80b..3ef0ce7f7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -60,7 +60,7 @@
"react-redux": "8.1.2",
"react-redux-loading-bar": "5.0.4",
"react-resize-detector": "10.0.1",
- "react-router-dom": "6.21.0",
+ "react-router-dom": "6.22.0",
"react-scripts": "5.0.1",
"react-simple-tree-menu": "1.1.18",
"react-table": "7.8.0",
From ff6f3e71650772096f15529b49bef3aa701aafa7 Mon Sep 17 00:00:00 2001
From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Date: Mon, 12 Feb 2024 10:30:57 -0800
Subject: [PATCH 14/28] PIMS-1287 Updated @types/supertest to 6.0.2
(express-api) (#2178)
---
express-api/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/express-api/package.json b/express-api/package.json
index f548683dc..1e2689b4d 100644
--- a/express-api/package.json
+++ b/express-api/package.json
@@ -48,7 +48,7 @@
"@types/jest": "29.5.10",
"@types/morgan": "1.9.9",
"@types/node": "20.11.13",
- "@types/supertest": "2.0.16",
+ "@types/supertest": "6.0.2",
"@types/swagger-ui-express": "4.1.6",
"@typescript-eslint/eslint-plugin": "6.20.0",
"@typescript-eslint/parser": "6.20.0",
From 43796c8c5abf883bbf6e708ea2252f1b3af8cee1 Mon Sep 17 00:00:00 2001
From: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Date: Mon, 12 Feb 2024 10:32:54 -0800
Subject: [PATCH 15/28] PIMS-1236 Updated prettier to 3.2.4 (express-api)
(#2177)
---
express-api/.prettierignore | 1 +
express-api/package.json | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/express-api/.prettierignore b/express-api/.prettierignore
index 58ee09054..acc66db64 100644
--- a/express-api/.prettierignore
+++ b/express-api/.prettierignore
@@ -2,3 +2,4 @@ coverage/
dist/
node_modules/
swagger-output.json
+tsconfig.json
diff --git a/express-api/package.json b/express-api/package.json
index 1e2689b4d..6e4276e40 100644
--- a/express-api/package.json
+++ b/express-api/package.json
@@ -57,7 +57,7 @@
"eslint-plugin-prettier": "5.1.3",
"jest": "29.7.0",
"nodemon": "3.0.1",
- "prettier": "3.1.0",
+ "prettier": "3.2.4",
"supertest": "6.3.3",
"swagger-autogen": "2.23.7",
"ts-jest": "29.1.1",
From dd1f409fbbe7e4054d308c99997a69a6b30270cb Mon Sep 17 00:00:00 2001
From: Sharala-Perumal <80914899+Sharala-Perumal@users.noreply.github.com>
Date: Mon, 12 Feb 2024 14:18:32 -0800
Subject: [PATCH 16/28] PIMS-407: Seed Data in Postgres (#2165)
Co-authored-by: dbarkowsky
Co-authored-by: Dylan Barkowsky <37922247+dbarkowsky@users.noreply.github.com>
Co-authored-by: Graham Stewart
---
express-api/package.json | 1 +
express-api/src/appDataSource.ts | 4 +-
express-api/src/constants/roles.ts | 4 +-
.../src/services/admin/rolesServices.ts | 21 +-
.../src/services/admin/usersServices.ts | 33 +-
.../src/services/keycloak/keycloakService.ts | 31 +-
.../src/services/users/usersServices.ts | 66 +-
.../src/typeorm/Entities/AccessRequest.ts | 44 +
.../src/typeorm/Entities/AccessRequests.ts | 30 -
...strativeAreas.ts => AdministrativeArea.ts} | 30 +-
.../Entities/{Agencies.ts => Agency.ts} | 29 +-
.../Entities/{Buildings.ts => Building.ts} | 34 +-
...onTypes.ts => BuildingConstructionType.ts} | 4 +-
.../typeorm/Entities/BuildingEvaluation.ts | 14 +
.../typeorm/Entities/BuildingEvaluations.ts | 12 -
.../src/typeorm/Entities/BuildingFiscal.ts | 14 +
.../src/typeorm/Entities/BuildingFiscals.ts | 12 -
...cupantTypes.ts => BuildingOccupantType.ts} | 4 +-
...inateUses.ts => BuildingPredominateUse.ts} | 4 +-
.../{EvaluationKeys.ts => EvaluationKey.ts} | 2 +-
.../Entities/{FiscalKeys.ts => FiscalKey.ts} | 2 +-
.../src/typeorm/Entities/NotificationQueue.ts | 30 +-
...onTemplates.ts => NotificationTemplate.ts} | 4 +-
.../Entities/{Parcels.ts => Parcel.ts} | 13 +-
.../src/typeorm/Entities/ParcelBuilding.ts | 21 +
.../src/typeorm/Entities/ParcelBuildings.ts | 17 -
...rcelEvaluations.ts => ParcelEvaluation.ts} | 12 +-
.../src/typeorm/Entities/ParcelFiscal.ts | 14 +
.../src/typeorm/Entities/ParcelFiscals.ts | 12 -
.../Entities/{Projects.ts => Project.ts} | 48 +-
...yResponses.ts => ProjectAgencyResponse.ts} | 30 +-
.../{ProjectNotes.ts => ProjectNote.ts} | 12 +-
.../{ProjectNumbers.ts => ProjectNumber.ts} | 2 +-
.../src/typeorm/Entities/ProjectProperties.ts | 29 -
.../src/typeorm/Entities/ProjectProperty.ts | 45 +
.../src/typeorm/Entities/ProjectReport.ts | 30 +
.../src/typeorm/Entities/ProjectReports.ts | 24 -
.../{ProjectRisks.ts => ProjectRisk.ts} | 4 +-
...ProjectSnapshots.ts => ProjectSnapshot.ts} | 13 +-
.../src/typeorm/Entities/ProjectStatus.ts | 10 +-
.../typeorm/Entities/ProjectStatusHistory.ts | 29 +-
...ations.ts => ProjectStatusNotification.ts} | 26 +-
.../Entities/ProjectStatusTransition.ts | 47 +
.../Entities/ProjectStatusTransitions.ts | 37 -
.../src/typeorm/Entities/ProjectTask.ts | 39 +
.../src/typeorm/Entities/ProjectTasks.ts | 33 -
.../{ReportTypes.ts => ProjectType.ts} | 4 +-
...fications.ts => PropertyClassification.ts} | 6 +-
.../{PropertyTypes.ts => PropertyType.ts} | 4 +-
.../Entities/{Provinces.ts => Province.ts} | 2 +-
...gionalDistricts.ts => RegionalDistrict.ts} | 9 +-
.../{ProjectTypes.ts => ReportType.ts} | 4 +-
express-api/src/typeorm/Entities/Role.ts | 33 +
.../src/typeorm/Entities/Subdivisions.ts | 16 -
.../typeorm/Entities/{Tasks.ts => Task.ts} | 12 +-
.../Entities/{TierLevels.ts => TierLevel.ts} | 4 +-
express-api/src/typeorm/Entities/User.ts | 79 +
.../typeorm/Entities/Users_Roles_Claims.ts | 201 --
.../Entities/{Workflows.ts => Workflow.ts} | 8 +-
.../typeorm/Entities/WorkflowProjectStatus.ts | 23 +-
.../Entities/abstractEntities/BaseEntity.ts | 17 +-
.../Entities/abstractEntities/Evaluation.ts | 12 +-
.../Entities/abstractEntities/Fiscal.ts | 14 +-
.../Entities/abstractEntities/Property.ts | 44 +-
.../Migrations/1707763864013-CreateTables.ts | 1763 +++++++++++++++++
.../Migrations/1707763864014-SeedData.ts | 127 ++
.../Migrations/Seeds/AdministrativeAreas.sql | 356 ++++
.../src/typeorm/Migrations/Seeds/Agencies.sql | 180 ++
.../Seeds/BuildingConstructionTypes.sql | 6 +
.../Seeds/BuildingOccupantTypes.sql | 4 +
.../Seeds/BuildingPredominateUses.sql | 55 +
.../Migrations/Seeds/EvaluationKeys.sql | 4 +
.../typeorm/Migrations/Seeds/FiscalKeys.sql | 3 +
.../Seeds/NotificationTemplates.sql | 821 ++++++++
.../typeorm/Migrations/Seeds/ProjectRisks.sql | 4 +
.../Migrations/Seeds/ProjectStatus.sql | 27 +
.../Seeds/ProjectStatusNotifications.sql | 19 +
.../Seeds/ProjectStatusTransitions.sql | 88 +
.../typeorm/Migrations/Seeds/ProjectTypes.sql | 2 +
.../Seeds/PropertyClassifications.sql | 8 +
.../Migrations/Seeds/PropertyTypes.sql | 4 +
.../typeorm/Migrations/Seeds/Provinces.sql | 3 +
.../Migrations/Seeds/RegionalDistricts.sql | 32 +
.../typeorm/Migrations/Seeds/ReportTypes.sql | 4 +
.../src/typeorm/Migrations/Seeds/Roles.sql | 5 +
.../src/typeorm/Migrations/Seeds/Tasks.sql | 15 +
.../typeorm/Migrations/Seeds/TierLevels.sql | 5 +
.../src/typeorm/Migrations/Seeds/Users.sql | 2 +
.../Seeds/WorkflowProjectStatus.sql | 41 +
.../typeorm/Migrations/Seeds/Workflows.sql | 7 +
express-api/tests/testUtils/factories.ts | 48 +-
.../admin/roles/rolesController.test.ts | 2 +-
.../controllers/users/usersController.test.ts | 9 +-
.../unit/services/admin/rolesServices.test.ts | 14 +-
.../unit/services/admin/usersServices.test.ts | 14 +-
.../unit/services/users/usersServices.test.ts | 53 +-
96 files changed, 4444 insertions(+), 749 deletions(-)
create mode 100644 express-api/src/typeorm/Entities/AccessRequest.ts
delete mode 100644 express-api/src/typeorm/Entities/AccessRequests.ts
rename express-api/src/typeorm/Entities/{AdministrativeAreas.ts => AdministrativeArea.ts} (50%)
rename express-api/src/typeorm/Entities/{Agencies.ts => Agency.ts} (58%)
rename express-api/src/typeorm/Entities/{Buildings.ts => Building.ts} (55%)
rename express-api/src/typeorm/Entities/{BuildingConstructionTypes.ts => BuildingConstructionType.ts} (83%)
create mode 100644 express-api/src/typeorm/Entities/BuildingEvaluation.ts
delete mode 100644 express-api/src/typeorm/Entities/BuildingEvaluations.ts
create mode 100644 express-api/src/typeorm/Entities/BuildingFiscal.ts
delete mode 100644 express-api/src/typeorm/Entities/BuildingFiscals.ts
rename express-api/src/typeorm/Entities/{BuildingOccupantTypes.ts => BuildingOccupantType.ts} (84%)
rename express-api/src/typeorm/Entities/{BuildingPredominateUses.ts => BuildingPredominateUse.ts} (84%)
rename express-api/src/typeorm/Entities/{EvaluationKeys.ts => EvaluationKey.ts} (88%)
rename express-api/src/typeorm/Entities/{FiscalKeys.ts => FiscalKey.ts} (89%)
rename express-api/src/typeorm/Entities/{NotificationTemplates.ts => NotificationTemplate.ts} (94%)
rename express-api/src/typeorm/Entities/{Parcels.ts => Parcel.ts} (63%)
create mode 100644 express-api/src/typeorm/Entities/ParcelBuilding.ts
delete mode 100644 express-api/src/typeorm/Entities/ParcelBuildings.ts
rename express-api/src/typeorm/Entities/{ParcelEvaluations.ts => ParcelEvaluation.ts} (58%)
create mode 100644 express-api/src/typeorm/Entities/ParcelFiscal.ts
delete mode 100644 express-api/src/typeorm/Entities/ParcelFiscals.ts
rename express-api/src/typeorm/Entities/{Projects.ts => Project.ts} (63%)
rename express-api/src/typeorm/Entities/{ProjectAgencyResponses.ts => ProjectAgencyResponse.ts} (55%)
rename express-api/src/typeorm/Entities/{ProjectNotes.ts => ProjectNote.ts} (58%)
rename express-api/src/typeorm/Entities/{ProjectNumbers.ts => ProjectNumber.ts} (79%)
delete mode 100644 express-api/src/typeorm/Entities/ProjectProperties.ts
create mode 100644 express-api/src/typeorm/Entities/ProjectProperty.ts
create mode 100644 express-api/src/typeorm/Entities/ProjectReport.ts
delete mode 100644 express-api/src/typeorm/Entities/ProjectReports.ts
rename express-api/src/typeorm/Entities/{ProjectRisks.ts => ProjectRisk.ts} (89%)
rename express-api/src/typeorm/Entities/{ProjectSnapshots.ts => ProjectSnapshot.ts} (71%)
rename express-api/src/typeorm/Entities/{ProjectStatusNotifications.ts => ProjectStatusNotification.ts} (54%)
create mode 100644 express-api/src/typeorm/Entities/ProjectStatusTransition.ts
delete mode 100644 express-api/src/typeorm/Entities/ProjectStatusTransitions.ts
create mode 100644 express-api/src/typeorm/Entities/ProjectTask.ts
delete mode 100644 express-api/src/typeorm/Entities/ProjectTasks.ts
rename express-api/src/typeorm/Entities/{ReportTypes.ts => ProjectType.ts} (87%)
rename express-api/src/typeorm/Entities/{PropertyClassifications.ts => PropertyClassification.ts} (80%)
rename express-api/src/typeorm/Entities/{PropertyTypes.ts => PropertyType.ts} (85%)
rename express-api/src/typeorm/Entities/{Provinces.ts => Province.ts} (88%)
rename express-api/src/typeorm/Entities/{RegionalDistricts.ts => RegionalDistrict.ts} (61%)
rename express-api/src/typeorm/Entities/{ProjectTypes.ts => ReportType.ts} (87%)
create mode 100644 express-api/src/typeorm/Entities/Role.ts
delete mode 100644 express-api/src/typeorm/Entities/Subdivisions.ts
rename express-api/src/typeorm/Entities/{Tasks.ts => Task.ts} (78%)
rename express-api/src/typeorm/Entities/{TierLevels.ts => TierLevel.ts} (87%)
create mode 100644 express-api/src/typeorm/Entities/User.ts
delete mode 100644 express-api/src/typeorm/Entities/Users_Roles_Claims.ts
rename express-api/src/typeorm/Entities/{Workflows.ts => Workflow.ts} (74%)
create mode 100644 express-api/src/typeorm/Migrations/1707763864013-CreateTables.ts
create mode 100644 express-api/src/typeorm/Migrations/1707763864014-SeedData.ts
create mode 100644 express-api/src/typeorm/Migrations/Seeds/AdministrativeAreas.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/Agencies.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/BuildingConstructionTypes.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/BuildingOccupantTypes.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/BuildingPredominateUses.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/EvaluationKeys.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/FiscalKeys.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/NotificationTemplates.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/ProjectRisks.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/ProjectStatus.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/ProjectStatusNotifications.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/ProjectStatusTransitions.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/ProjectTypes.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/PropertyClassifications.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/PropertyTypes.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/Provinces.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/RegionalDistricts.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/ReportTypes.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/Roles.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/Tasks.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/TierLevels.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/Users.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/WorkflowProjectStatus.sql
create mode 100644 express-api/src/typeorm/Migrations/Seeds/Workflows.sql
diff --git a/express-api/package.json b/express-api/package.json
index 6e4276e40..244353ed1 100644
--- a/express-api/package.json
+++ b/express-api/package.json
@@ -32,6 +32,7 @@
"express": "4.18.2",
"express-rate-limit": "7.1.1",
"morgan": "1.10.0",
+ "node-sql-reader": "0.1.3",
"pg": "8.11.3",
"reflect-metadata": "0.2.1",
"swagger-ui-express": "5.0.0",
diff --git a/express-api/src/appDataSource.ts b/express-api/src/appDataSource.ts
index 7b9c00b41..8b97bf34f 100644
--- a/express-api/src/appDataSource.ts
+++ b/express-api/src/appDataSource.ts
@@ -21,11 +21,11 @@ export const AppDataSource = new DataSource({
username: POSTGRES_USER,
password: POSTGRES_PASSWORD,
database: POSTGRES_DB,
- synchronize: true,
+ synchronize: false,
migrationsRun: false,
logging: true,
logger: new CustomWinstonLogger(true),
entities: ['./src/typeorm/Entities/*.ts'],
- migrations: ['./src/typeorm/migrations/seed/*.ts', './src/typeorm/migrations/*.ts'],
+ migrations: ['./src/typeorm/Migrations/Seeds/*.ts', './src/typeorm/Migrations/*.ts'],
subscribers: [],
});
diff --git a/express-api/src/constants/roles.ts b/express-api/src/constants/roles.ts
index edc2bc079..a7cb42136 100644
--- a/express-api/src/constants/roles.ts
+++ b/express-api/src/constants/roles.ts
@@ -1,3 +1,5 @@
export enum Roles {
- ADMIN = 'admin',
+ ADMIN = 'Administrator',
+ GENERAL_USER = 'General User',
+ AUDITOR = 'Auditor',
}
diff --git a/express-api/src/services/admin/rolesServices.ts b/express-api/src/services/admin/rolesServices.ts
index ccfc61713..13ae745aa 100644
--- a/express-api/src/services/admin/rolesServices.ts
+++ b/express-api/src/services/admin/rolesServices.ts
@@ -1,15 +1,12 @@
import { AppDataSource } from '@/appDataSource';
import { RolesFilter } from '../../controllers/admin/roles/rolesSchema';
import { DeepPartial } from 'typeorm';
-import { Roles } from '@/typeorm/Entities/Users_Roles_Claims';
+import { Role } from '@/typeorm/Entities/Role';
import { UUID } from 'crypto';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
const getRoles = async (filter: RolesFilter) => {
- const roles = AppDataSource.getRepository(Roles).find({
- relations: {
- RoleClaims: { Claim: true },
- },
+ const roles = AppDataSource.getRepository(Role).find({
where: {
Name: filter.name,
Id: filter.id,
@@ -21,34 +18,34 @@ const getRoles = async (filter: RolesFilter) => {
};
const getRoleById = async (roleId: UUID) => {
- return AppDataSource.getRepository(Roles).findOne({
+ return AppDataSource.getRepository(Role).findOne({
where: { Id: roleId },
});
};
-const addRole = async (role: Roles) => {
+const addRole = async (role: Role) => {
const existing = await getRoleById(role.Id);
if (existing) {
throw new ErrorWithCode('Role already exists', 409);
}
- const retRole = AppDataSource.getRepository(Roles).save(role);
+ const retRole = AppDataSource.getRepository(Role).save(role);
return retRole;
};
-const updateRole = async (role: DeepPartial) => {
- const retRole = await AppDataSource.getRepository(Roles).update(role.Id, role);
+const updateRole = async (role: DeepPartial) => {
+ const retRole = await AppDataSource.getRepository(Role).update(role.Id, role);
if (!retRole.affected) {
throw new ErrorWithCode('Role was not found.', 404);
}
return retRole.generatedMaps[0];
};
-const removeRole = async (role: Roles) => {
+const removeRole = async (role: Role) => {
const existing = await getRoleById(role.Id);
if (!existing) {
throw new ErrorWithCode('Role was not found.', 404);
}
- const retRole = AppDataSource.getRepository(Roles).remove(role);
+ const retRole = AppDataSource.getRepository(Role).remove(role);
return retRole;
};
diff --git a/express-api/src/services/admin/usersServices.ts b/express-api/src/services/admin/usersServices.ts
index 24a312381..f1fa2e402 100644
--- a/express-api/src/services/admin/usersServices.ts
+++ b/express-api/src/services/admin/usersServices.ts
@@ -1,14 +1,15 @@
import { AppDataSource } from '@/appDataSource';
-import { Users } from '@/typeorm/Entities/Users_Roles_Claims';
+import { User } from '@/typeorm/Entities/User';
import { UserFiltering } from '../../controllers/admin/users/usersSchema';
import KeycloakService from '../keycloak/keycloakService';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import { UUID } from 'crypto';
const getUsers = async (filter: UserFiltering) => {
- const users = await AppDataSource.getRepository(Users).find({
+ const users = await AppDataSource.getRepository(User).find({
relations: {
Agency: true,
+ Role: true,
},
where: {
Id: filter.id,
@@ -19,10 +20,8 @@ const getUsers = async (filter: UserFiltering) => {
Agency: {
Name: filter.agency,
},
- UserRoles: {
- Role: {
- Name: filter.role,
- },
+ Role: {
+ Name: filter.role,
},
IsDisabled: filter.isDisabled,
Position: filter.position,
@@ -34,10 +33,10 @@ const getUsers = async (filter: UserFiltering) => {
};
const getUserById = async (id: string) => {
- return AppDataSource.getRepository(Users).findOne({
+ return AppDataSource.getRepository(User).findOne({
relations: {
Agency: true,
- UserRoles: { Role: true },
+ Role: true,
},
where: {
Id: id as UUID,
@@ -45,30 +44,30 @@ const getUserById = async (id: string) => {
});
};
-const addUser = async (user: Users) => {
- const resource = await AppDataSource.getRepository(Users).findOne({ where: { Id: user.Id } });
+const addUser = async (user: User) => {
+ const resource = await AppDataSource.getRepository(User).findOne({ where: { Id: user.Id } });
if (resource) {
throw new ErrorWithCode('Resource already exists.', 409);
}
- const retUser = await AppDataSource.getRepository(Users).save(user);
+ const retUser = await AppDataSource.getRepository(User).save(user);
return retUser;
};
-const updateUser = async (user: Users) => {
- const resource = await AppDataSource.getRepository(Users).findOne({ where: { Id: user.Id } });
+const updateUser = async (user: User) => {
+ const resource = await AppDataSource.getRepository(User).findOne({ where: { Id: user.Id } });
if (!resource) {
throw new ErrorWithCode('Resource does not exist.', 404);
}
- const retUser = await AppDataSource.getRepository(Users).update(user.Id, user);
+ const retUser = await AppDataSource.getRepository(User).update(user.Id, user);
return retUser.generatedMaps[0];
};
-const deleteUser = async (user: Users) => {
- const resource = await AppDataSource.getRepository(Users).findOne({ where: { Id: user.Id } });
+const deleteUser = async (user: User) => {
+ const resource = await AppDataSource.getRepository(User).findOne({ where: { Id: user.Id } });
if (!resource) {
throw new ErrorWithCode('Resource does not exist.', 404);
}
- const retUser = await AppDataSource.getRepository(Users).remove(user);
+ const retUser = await AppDataSource.getRepository(User).remove(user);
return retUser;
};
diff --git a/express-api/src/services/keycloak/keycloakService.ts b/express-api/src/services/keycloak/keycloakService.ts
index af4cea3e5..5e3221d5a 100644
--- a/express-api/src/services/keycloak/keycloakService.ts
+++ b/express-api/src/services/keycloak/keycloakService.ts
@@ -25,7 +25,8 @@ import { randomUUID } from 'crypto';
import { AppDataSource } from '@/appDataSource';
import { DeepPartial, In, Not } from 'typeorm';
import userServices from '@/services/admin/usersServices';
-import { Users, Roles } from '@/typeorm/Entities/Users_Roles_Claims';
+import { User } from '@/typeorm/Entities/User';
+import { Role } from '@/typeorm/Entities/Role';
/**
* @description Sync keycloak roles into PIMS roles.
@@ -41,7 +42,7 @@ const syncKeycloakRoles = async () => {
const internalRole = await rolesServices.getRoles({ name: role.name });
if (internalRole.length == 0) {
- const newRole: Roles = {
+ const newRole: Role = {
Id: randomUUID(),
Name: role.name,
IsDisabled: false,
@@ -50,15 +51,16 @@ const syncKeycloakRoles = async () => {
Description: '',
IsPublic: false,
CreatedById: undefined,
+ CreatedBy: undefined,
CreatedOn: undefined,
UpdatedById: undefined,
+ UpdatedBy: undefined,
UpdatedOn: undefined,
- UserRoles: [],
- RoleClaims: [],
+ Users: [],
};
rolesServices.addRole(newRole);
} else {
- const overwriteRole: DeepPartial = {
+ const overwriteRole: DeepPartial = {
Id: internalRole[0].Id,
Name: role.name,
IsDisabled: false,
@@ -74,7 +76,7 @@ const syncKeycloakRoles = async () => {
rolesServices.updateRole(overwriteRole);
}
- await AppDataSource.getRepository(Roles).delete({
+ await AppDataSource.getRepository(Role).delete({
Name: Not(In(roles.map((a) => a.name))),
});
@@ -159,7 +161,7 @@ const syncKeycloakUser = async (keycloakGuid: string) => {
for (const krole of kroles) {
const internalRole = await rolesServices.getRoles({ name: krole.name });
if (internalRole.length == 0) {
- const newRole: Roles = {
+ const newRole: Role = {
Id: randomUUID(),
Name: krole.name,
IsDisabled: false,
@@ -167,11 +169,12 @@ const syncKeycloakUser = async (keycloakGuid: string) => {
KeycloakGroupId: '',
Description: '',
IsPublic: false,
- UserRoles: [],
- RoleClaims: [],
+ Users: [],
CreatedById: undefined,
+ CreatedBy: undefined,
CreatedOn: undefined,
UpdatedById: undefined,
+ UpdatedBy: undefined,
UpdatedOn: undefined,
};
await rolesServices.addRole(newRole);
@@ -179,16 +182,18 @@ const syncKeycloakUser = async (keycloakGuid: string) => {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const newUsersRoles = await AppDataSource.getRepository(Roles).find({
+ const newUsersRoles = await AppDataSource.getRepository(Role).find({
where: { Name: In(kroles.map((a) => a.name)) },
});
if (internalUser.length == 0) {
- const newUser: Users = {
+ const newUser: User = {
Id: randomUUID(),
CreatedById: undefined,
+ CreatedBy: undefined,
CreatedOn: undefined,
UpdatedById: undefined,
+ UpdatedBy: undefined,
UpdatedOn: undefined,
Username: kuser.username,
DisplayName: kuser.attributes.display_name[0],
@@ -203,9 +208,11 @@ const syncKeycloakUser = async (keycloakGuid: string) => {
Note: '',
LastLogin: new Date(),
ApprovedById: undefined,
+ ApprovedBy: undefined,
ApprovedOn: undefined,
KeycloakUserId: keycloakGuid,
- UserRoles: [],
+ Role: undefined,
+ RoleId: undefined,
Agency: undefined,
AgencyId: undefined,
};
diff --git a/express-api/src/services/users/usersServices.ts b/express-api/src/services/users/usersServices.ts
index 4977587b2..6d64ef0b7 100644
--- a/express-api/src/services/users/usersServices.ts
+++ b/express-api/src/services/users/usersServices.ts
@@ -1,11 +1,11 @@
-import { Users } from '@/typeorm/Entities/Users_Roles_Claims';
+import { User } from '@/typeorm/Entities/User';
import { AppDataSource } from '@/appDataSource';
import { KeycloakBCeIDUser, KeycloakIdirUser, KeycloakUser } from '@bcgov/citz-imb-kc-express';
import { z } from 'zod';
-import { AccessRequests } from '@/typeorm/Entities/AccessRequests';
+import { AccessRequest } from '@/typeorm/Entities/AccessRequest';
import { In } from 'typeorm';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
-import { Agencies } from '@/typeorm/Entities/Agencies';
+import { Agency } from '@/typeorm/Entities/Agency';
interface NormalizedKeycloakUser {
given_name: string;
@@ -14,14 +14,14 @@ interface NormalizedKeycloakUser {
guid: string;
}
-const getUser = async (nameOrGuid: string): Promise => {
+const getUser = async (nameOrGuid: string): Promise => {
const userGuid = z.string().uuid().safeParse(nameOrGuid);
if (userGuid.success) {
- return AppDataSource.getRepository(Users).findOneBy({
+ return AppDataSource.getRepository(User).findOneBy({
KeycloakUserId: userGuid.data,
});
} else {
- return AppDataSource.getRepository(Users).findOneBy({
+ return AppDataSource.getRepository(User).findOneBy({
Username: nameOrGuid,
});
}
@@ -65,7 +65,7 @@ const activateUser = async (kcUser: KeycloakUser) => {
const internalUser = await getUser(kcUser.preferred_username);
if (!internalUser) {
const { given_name, family_name, username, guid } = normalizedUser;
- AppDataSource.getRepository(Users).insert({
+ AppDataSource.getRepository(User).insert({
Username: username,
FirstName: given_name,
LastName: family_name,
@@ -74,13 +74,13 @@ const activateUser = async (kcUser: KeycloakUser) => {
} else {
internalUser.LastLogin = new Date();
internalUser.KeycloakUserId = normalizedUser.guid;
- AppDataSource.getRepository(Users).update({ Id: internalUser.Id }, internalUser);
+ AppDataSource.getRepository(User).update({ Id: internalUser.Id }, internalUser);
}
};
const getAccessRequest = async (kcUser: KeycloakUser) => {
const internalUser = await getUserFromKeycloak(kcUser);
- const accessRequest = AppDataSource.getRepository(AccessRequests)
+ const accessRequest = AppDataSource.getRepository(AccessRequest)
.createQueryBuilder('AccessRequests')
.leftJoinAndSelect('AccessRequests.AgencyId', 'Agencies')
.leftJoinAndSelect('AccessRequests.RoleId', 'Roles')
@@ -93,7 +93,7 @@ const getAccessRequest = async (kcUser: KeycloakUser) => {
};
const getAccessRequestById = async (requestId: number, kcUser: KeycloakUser) => {
- const accessRequest = await AppDataSource.getRepository(AccessRequests)
+ const accessRequest = await AppDataSource.getRepository(AccessRequest)
.createQueryBuilder('AccessRequests')
.leftJoinAndSelect('AccessRequests.AgencyId', 'Agencies')
.leftJoinAndSelect('AccessRequests.RoleId', 'Roles')
@@ -101,44 +101,43 @@ const getAccessRequestById = async (requestId: number, kcUser: KeycloakUser) =>
.where('AccessRequests.Id = :requestId', { requestId: requestId })
.getOne();
const internalUser = await getUserFromKeycloak(kcUser);
- if (accessRequest && accessRequest.UserId.Id != internalUser.Id)
- throw new Error('Not authorized.');
+ if (accessRequest && accessRequest.UserId != internalUser.Id) throw new Error('Not authorized.');
return accessRequest;
};
-const deleteAccessRequest = async (accessRequest: AccessRequests) => {
- const existing = await AppDataSource.getRepository(AccessRequests).findOne({
+const deleteAccessRequest = async (accessRequest: AccessRequest) => {
+ const existing = await AppDataSource.getRepository(AccessRequest).findOne({
where: { Id: accessRequest.Id },
});
if (!existing) {
throw new ErrorWithCode('No access request found', 404);
}
- const deletedRequest = AppDataSource.getRepository(AccessRequests).remove(accessRequest);
+ const deletedRequest = AppDataSource.getRepository(AccessRequest).remove(accessRequest);
return deletedRequest;
};
-const addAccessRequest = async (accessRequest: AccessRequests, kcUser: KeycloakUser) => {
+const addAccessRequest = async (accessRequest: AccessRequest, kcUser: KeycloakUser) => {
if (accessRequest == null || accessRequest.AgencyId == null || accessRequest.RoleId == null) {
throw new Error('Null argument.');
}
const internalUser = await getUserFromKeycloak(kcUser);
- accessRequest.UserId = internalUser;
- internalUser.Position = accessRequest.UserId.Position;
+ accessRequest.User = internalUser;
+ internalUser.Position = accessRequest.User.Position;
//Iterating through agencies and roles no longer necessary here?
- return AppDataSource.getRepository(AccessRequests).insert(accessRequest);
+ return AppDataSource.getRepository(AccessRequest).insert(accessRequest);
};
-const updateAccessRequest = async (updateRequest: AccessRequests, kcUser: KeycloakUser) => {
+const updateAccessRequest = async (updateRequest: AccessRequest, kcUser: KeycloakUser) => {
if (updateRequest == null || updateRequest.AgencyId == null || updateRequest.RoleId == null)
throw new Error('Null argument.');
const internalUser = await getUserFromKeycloak(kcUser);
- if (updateRequest.UserId.Id != internalUser.Id) throw new Error('Not authorized.');
+ if (updateRequest.UserId != internalUser.Id) throw new Error('Not authorized.');
- const result = await AppDataSource.getRepository(AccessRequests).update(
+ const result = await AppDataSource.getRepository(AccessRequest).update(
{ Id: updateRequest.Id },
updateRequest,
);
@@ -150,7 +149,7 @@ const updateAccessRequest = async (updateRequest: AccessRequests, kcUser: Keyclo
const getAgencies = async (username: string) => {
const user = await getUser(username);
- const userAgencies = await AppDataSource.getRepository(Users).findOneOrFail({
+ const userAgencies = await AppDataSource.getRepository(User).findOneOrFail({
relations: {
Agency: true,
},
@@ -159,36 +158,25 @@ const getAgencies = async (username: string) => {
},
});
const agencyId = userAgencies.Agency.Id;
- const children = await AppDataSource.getRepository(Agencies).find({
+ const children = await AppDataSource.getRepository(Agency).find({
where: {
- ParentId: { Id: agencyId },
+ ParentId: agencyId,
},
});
- // .createQueryBuilder('Agencies')
- // .where('Agencies.ParentId IN (:...ids)', { ids: agencies })
- // .getMany();
return [agencyId, ...children.map((c) => c.Id)];
};
const getAdministrators = async (agencyIds: string[]) => {
- const admins = await AppDataSource.getRepository(Users).find({
+ const admins = await AppDataSource.getRepository(User).find({
relations: {
- UserRoles: { Role: { RoleClaims: { Claim: true } } },
+ Role: true,
Agency: true,
},
where: {
Agency: In(agencyIds),
- UserRoles: { Role: { RoleClaims: { Claim: { Name: 'System Admin' } } } },
+ Role: { Name: 'System Admin' },
},
});
- // .createQueryBuilder('Users')
- // .leftJoinAndSelect('Users.Roles', 'Roles')
- // .leftJoinAndSelect('Roles.Claims', 'Claims')
- // .leftJoinAndSelect('Users.Agencies', 'Agencies')
- // .where('Agencies.Id IN (:...agencyIds)', { agencyIds: agencyIds })
- // .andWhere('Claims.Name = :systemAdmin', { systemAdmin: 'System Admin' })
- // .getMany();
-
return admins;
};
diff --git a/express-api/src/typeorm/Entities/AccessRequest.ts b/express-api/src/typeorm/Entities/AccessRequest.ts
new file mode 100644
index 000000000..2c3bacac6
--- /dev/null
+++ b/express-api/src/typeorm/Entities/AccessRequest.ts
@@ -0,0 +1,44 @@
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+import { User } from '@/typeorm/Entities/User';
+import { Role } from '@/typeorm/Entities/Role';
+import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, Index, JoinColumn } from 'typeorm';
+import { Agency } from './Agency';
+import { UUID } from 'crypto';
+
+@Entity()
+export class AccessRequest extends BaseEntity {
+ @PrimaryGeneratedColumn()
+ Id: number;
+
+ // User Relations
+ @Column({ name: 'UserId', type: 'uuid' })
+ UserId: UUID;
+
+ @ManyToOne(() => User, (User) => User.Id)
+ @JoinColumn({ name: 'UserId' })
+ @Index()
+ User: User;
+
+ @Column({ type: 'character varying', length: 1000, nullable: true })
+ Note: string;
+
+ @Column({ type: 'int' })
+ @Index()
+ Status: number;
+
+ // Role Relations
+ @Column({ name: 'RoleId', type: 'uuid' })
+ RoleId: UUID;
+
+ @ManyToOne(() => Role, (Role) => Role.Id)
+ @JoinColumn({ name: 'RoleId' })
+ Role: Role;
+
+ // Agency Relations
+ @Column({ name: 'AgencyId', type: 'int' })
+ AgencyId: number;
+
+ @ManyToOne(() => Agency, (Agency) => Agency.Id)
+ @JoinColumn({ name: 'AgencyId' })
+ Agency: Agency;
+}
diff --git a/express-api/src/typeorm/Entities/AccessRequests.ts b/express-api/src/typeorm/Entities/AccessRequests.ts
deleted file mode 100644
index 281f71859..000000000
--- a/express-api/src/typeorm/Entities/AccessRequests.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Users, Roles } from '@/typeorm/Entities/Users_Roles_Claims';
-import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, Index, JoinColumn } from 'typeorm';
-import { Agencies } from './Agencies';
-
-@Entity()
-export class AccessRequests extends BaseEntity {
- @PrimaryGeneratedColumn()
- Id: number;
-
- @ManyToOne(() => Users, (User) => User.Id)
- @JoinColumn({ name: 'UserId' })
- @Index()
- UserId: Users;
-
- @Column({ type: 'character varying', length: 1000, nullable: true })
- Note: string;
-
- @Column({ type: 'int' })
- @Index()
- Status: number;
-
- @ManyToOne(() => Roles, (Role) => Role.Id)
- @JoinColumn({ name: 'RoleId' })
- RoleId: Roles;
-
- @ManyToOne(() => Agencies, (Agency) => Agency.Id)
- @JoinColumn({ name: 'AgencyId' })
- AgencyId: Agencies;
-}
diff --git a/express-api/src/typeorm/Entities/AdministrativeAreas.ts b/express-api/src/typeorm/Entities/AdministrativeArea.ts
similarity index 50%
rename from express-api/src/typeorm/Entities/AdministrativeAreas.ts
rename to express-api/src/typeorm/Entities/AdministrativeArea.ts
index 0588c241b..d8d69ec6d 100644
--- a/express-api/src/typeorm/Entities/AdministrativeAreas.ts
+++ b/express-api/src/typeorm/Entities/AdministrativeArea.ts
@@ -7,14 +7,14 @@ import {
JoinColumn,
Unique,
} from 'typeorm';
-import { RegionalDistricts } from './RegionalDistricts';
+import { RegionalDistrict } from './RegionalDistrict';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Provinces } from '@/typeorm/Entities/Provinces';
+import { Province } from '@/typeorm/Entities/Province';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
@Unique('Unique_Name_RegionalDistrict', ['Name', 'RegionalDistrictId'])
-export class AdministrativeAreas extends BaseEntity {
+export class AdministrativeArea extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -22,25 +22,27 @@ export class AdministrativeAreas extends BaseEntity {
@Index()
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
SortOrder: number;
- @Column({ type: 'character varying', length: 100, nullable: true })
- Abbreviation: string;
-
- @Column({ type: 'character varying', length: 50, nullable: true })
- BoundaryType: string;
+ // Regional District Relations
+ @Column({ name: 'RegionalDistrictId', type: 'int' })
+ @Index()
+ RegionalDistrictId: number;
- @ManyToOne(() => RegionalDistricts, (RegionalDistrict) => RegionalDistrict.Id)
+ @ManyToOne(() => RegionalDistrict, (RegionalDistrict) => RegionalDistrict.Id)
@JoinColumn({ name: 'RegionalDistrictId' })
+ RegionalDistrict: RegionalDistrict;
+
+ // Province Relations
+ @Column({ name: 'ProvinceId', type: 'character varying', length: 2 })
@Index()
- RegionalDistrictId: RegionalDistricts;
+ ProvinceId: string;
- @ManyToOne(() => Provinces, (Province) => Province.Id)
+ @ManyToOne(() => Province, (Province) => Province.Id)
@JoinColumn({ name: 'ProvinceId' })
- @Index()
- ProvinceId: Provinces;
+ Province: Province;
}
diff --git a/express-api/src/typeorm/Entities/Agencies.ts b/express-api/src/typeorm/Entities/Agency.ts
similarity index 58%
rename from express-api/src/typeorm/Entities/Agencies.ts
rename to express-api/src/typeorm/Entities/Agency.ts
index 11b03cb8f..b516eac3b 100644
--- a/express-api/src/typeorm/Entities/Agencies.ts
+++ b/express-api/src/typeorm/Entities/Agency.ts
@@ -8,36 +8,43 @@ import {
OneToMany,
Relation,
} from 'typeorm';
-import { BaseEntity } from './abstractEntities/BaseEntity';
-import { Users } from './Users_Roles_Claims';
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+import { User } from '@/typeorm/Entities/User';
@Entity()
@Index(['ParentId', 'IsDisabled', 'Id', 'Name', 'SortOrder']) // I'm not sure this index is needed. How often do we search by this group?
-export class Agencies extends BaseEntity {
- @PrimaryColumn({ type: 'character varying', length: 6 })
- Id: string;
+export class Agency extends BaseEntity {
+ @PrimaryColumn({ type: 'int' })
+ Id: number;
@Column({ type: 'character varying', length: 150 })
Name: string;
- @Column({ type: 'bit' })
+ @Column({ type: 'boolean' })
IsDisabled: boolean;
@Column({ type: 'int' })
SortOrder: number;
+ @Column({ type: 'character varying', length: 6 })
+ Code: string;
+
@Column({ type: 'character varying', length: 500, nullable: true })
Description: string;
- @ManyToOne(() => Agencies, (agency) => agency.Id)
+ // Parent Agency Relations
+ @Column({ name: 'ParentId', type: 'int', nullable: true })
+ ParentId: number;
+
+ @ManyToOne(() => Agency, (agency) => agency.Id)
@JoinColumn({ name: 'ParentId' })
@Index()
- ParentId: Agencies;
+ Parent: Agency;
@Column({ type: 'character varying', length: 150, nullable: true })
Email: string;
- @Column({ type: 'bit' })
+ @Column({ type: 'boolean' })
SendEmail: boolean;
@Column({ type: 'character varying', length: 100, nullable: true })
@@ -46,6 +53,6 @@ export class Agencies extends BaseEntity {
@Column({ type: 'character varying', length: 250, nullable: true })
CCEmail: string;
- @OneToMany(() => Users, (users) => users.Agency)
- Users: Relation[];
+ @OneToMany(() => User, (user) => user.AgencyId)
+ Users: Relation[];
}
diff --git a/express-api/src/typeorm/Entities/Buildings.ts b/express-api/src/typeorm/Entities/Building.ts
similarity index 55%
rename from express-api/src/typeorm/Entities/Buildings.ts
rename to express-api/src/typeorm/Entities/Building.ts
index a1487dcee..5ae079f10 100644
--- a/express-api/src/typeorm/Entities/Buildings.ts
+++ b/express-api/src/typeorm/Entities/Building.ts
@@ -1,6 +1,6 @@
-import { BuildingConstructionTypes } from '@/typeorm/Entities/BuildingConstructionTypes';
-import { BuildingOccupantTypes } from '@/typeorm/Entities/BuildingOccupantTypes';
-import { BuildingPredominateUses } from '@/typeorm/Entities/BuildingPredominateUses';
+import { BuildingConstructionType } from '@/typeorm/Entities/BuildingConstructionType';
+import { BuildingOccupantType } from '@/typeorm/Entities/BuildingOccupantType';
+import { BuildingPredominateUse } from '@/typeorm/Entities/BuildingPredominateUse';
import { Entity, Column, ManyToOne, Index, JoinColumn } from 'typeorm';
import { Property } from '@/typeorm/Entities/abstractEntities/Property';
@@ -9,19 +9,27 @@ import { Property } from '@/typeorm/Entities/abstractEntities/Property';
// Can Buildings and Parcels share a base Properties entity?
@Entity()
-export class Buildings extends Property {
- @ManyToOne(() => BuildingConstructionTypes, (ConstructionType) => ConstructionType.Id)
+export class Building extends Property {
+ // Construction Type Relations
+ @Column({ name: 'BuildingConstructionTypeId', type: 'int' })
+ BuildingConstructionTypeId: number;
+
+ @ManyToOne(() => BuildingConstructionType, (ConstructionType) => ConstructionType.Id)
@JoinColumn({ name: 'BuildingConstructionTypeId' })
@Index()
- BuildingConstructionTypeId: BuildingConstructionTypes;
+ BuildingConstructionType: BuildingConstructionType;
@Column({ type: 'int' })
BuildingFloorCount: number;
- @ManyToOne(() => BuildingPredominateUses, (PredominateUse) => PredominateUse.Id)
+ // Predominate Use Relations
+ @Column({ name: 'BuildingPredominateUseId', type: 'int' })
+ BuildingPredominateUseId: number;
+
+ @ManyToOne(() => BuildingPredominateUse, (PredominateUse) => PredominateUse.Id)
@JoinColumn({ name: 'BuildingPredominateUseId' })
@Index()
- BuildingPredominateUseId: BuildingPredominateUses;
+ BuildingPredominateUse: BuildingPredominateUse;
@Column({ type: 'character varying', length: 450 })
BuildingTenancy: string;
@@ -29,10 +37,14 @@ export class Buildings extends Property {
@Column({ type: 'real' })
RentableArea: number;
- @ManyToOne(() => BuildingOccupantTypes, (OccupantType) => OccupantType.Id)
+ // Occupant Type Relations
+ @Column({ name: 'BuildingOccupantTypeId', type: 'int' })
+ BuildingOccupantTypeId: number;
+
+ @ManyToOne(() => BuildingOccupantType, (OccupantType) => OccupantType.Id)
@JoinColumn({ name: 'BuildingOccupantTypeId' })
@Index()
- BuildingOccupantTypeId: BuildingOccupantTypes;
+ BuildingOccupantType: BuildingOccupantType;
@Column({ type: 'timestamp', nullable: true })
LeaseExpiry: Date;
@@ -40,7 +52,7 @@ export class Buildings extends Property {
@Column({ type: 'character varying', length: 100, nullable: true })
OccupantName: string;
- @Column({ type: 'bit' })
+ @Column({ type: 'boolean' })
TransferLeaseOnSale: boolean;
@Column({ type: 'timestamp', nullable: true })
diff --git a/express-api/src/typeorm/Entities/BuildingConstructionTypes.ts b/express-api/src/typeorm/Entities/BuildingConstructionType.ts
similarity index 83%
rename from express-api/src/typeorm/Entities/BuildingConstructionTypes.ts
rename to express-api/src/typeorm/Entities/BuildingConstructionType.ts
index 816aba500..3277bcace 100644
--- a/express-api/src/typeorm/Entities/BuildingConstructionTypes.ts
+++ b/express-api/src/typeorm/Entities/BuildingConstructionType.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
-export class BuildingConstructionTypes extends BaseEntity {
+export class BuildingConstructionType extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -11,7 +11,7 @@ export class BuildingConstructionTypes extends BaseEntity {
@Index({ unique: true })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/BuildingEvaluation.ts b/express-api/src/typeorm/Entities/BuildingEvaluation.ts
new file mode 100644
index 000000000..1a57cd28b
--- /dev/null
+++ b/express-api/src/typeorm/Entities/BuildingEvaluation.ts
@@ -0,0 +1,14 @@
+import { Building } from '@/typeorm/Entities/Building';
+import { Evaluation } from '@/typeorm/Entities/abstractEntities/Evaluation';
+import { Entity, Index, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
+
+@Entity()
+@Index(['BuildingId', 'EvaluationKey'])
+export class BuildingEvaluation extends Evaluation {
+ @PrimaryColumn({ name: 'BuildingId', type: 'int' })
+ BuildingId: number;
+
+ @ManyToOne(() => Building, (Building) => Building.Id)
+ @JoinColumn({ name: 'BuildingId' })
+ Building: Building;
+}
diff --git a/express-api/src/typeorm/Entities/BuildingEvaluations.ts b/express-api/src/typeorm/Entities/BuildingEvaluations.ts
deleted file mode 100644
index 987b64cea..000000000
--- a/express-api/src/typeorm/Entities/BuildingEvaluations.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Buildings } from '@/typeorm/Entities/Buildings';
-import { Evaluation } from '@/typeorm/Entities/abstractEntities/Evaluation';
-import { Entity, Index, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
-
-@Entity()
-@Index(['BuildingId', 'EvaluationKey'])
-export class BuildingEvaluations extends Evaluation {
- @ManyToOne(() => Buildings, (Building) => Building.Id)
- @JoinColumn({ name: 'BuildingId' })
- @PrimaryColumn()
- BuildingId: Buildings;
-}
diff --git a/express-api/src/typeorm/Entities/BuildingFiscal.ts b/express-api/src/typeorm/Entities/BuildingFiscal.ts
new file mode 100644
index 000000000..896c92023
--- /dev/null
+++ b/express-api/src/typeorm/Entities/BuildingFiscal.ts
@@ -0,0 +1,14 @@
+import { Entity, Index, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { Fiscal } from '@/typeorm/Entities/abstractEntities/Fiscal';
+import { Building } from '@/typeorm/Entities/Building';
+
+@Entity()
+export class BuildingFiscal extends Fiscal {
+ @PrimaryColumn({ name: 'BuildingId', type: 'int' })
+ @Index()
+ BuildingId: number;
+
+ @ManyToOne(() => Building, (Building) => Building.Id)
+ @JoinColumn({ name: 'BuildingId' })
+ Building: Building;
+}
diff --git a/express-api/src/typeorm/Entities/BuildingFiscals.ts b/express-api/src/typeorm/Entities/BuildingFiscals.ts
deleted file mode 100644
index e9ea19cbc..000000000
--- a/express-api/src/typeorm/Entities/BuildingFiscals.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Entity, Index, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
-import { Fiscal } from '@/typeorm/Entities/abstractEntities/Fiscal';
-import { Buildings } from '@/typeorm/Entities/Buildings';
-
-@Entity()
-export class BuildingFiscals extends Fiscal {
- @ManyToOne(() => Buildings, (Building) => Building.Id)
- @JoinColumn({ name: 'BuildingId' })
- @PrimaryColumn()
- @Index()
- BuildingId: Buildings;
-}
diff --git a/express-api/src/typeorm/Entities/BuildingOccupantTypes.ts b/express-api/src/typeorm/Entities/BuildingOccupantType.ts
similarity index 84%
rename from express-api/src/typeorm/Entities/BuildingOccupantTypes.ts
rename to express-api/src/typeorm/Entities/BuildingOccupantType.ts
index b40b58ed4..f3f1ce15a 100644
--- a/express-api/src/typeorm/Entities/BuildingOccupantTypes.ts
+++ b/express-api/src/typeorm/Entities/BuildingOccupantType.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
-export class BuildingOccupantTypes extends BaseEntity {
+export class BuildingOccupantType extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -11,7 +11,7 @@ export class BuildingOccupantTypes extends BaseEntity {
@Index({ unique: true })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/BuildingPredominateUses.ts b/express-api/src/typeorm/Entities/BuildingPredominateUse.ts
similarity index 84%
rename from express-api/src/typeorm/Entities/BuildingPredominateUses.ts
rename to express-api/src/typeorm/Entities/BuildingPredominateUse.ts
index 66dab329d..0d43d2978 100644
--- a/express-api/src/typeorm/Entities/BuildingPredominateUses.ts
+++ b/express-api/src/typeorm/Entities/BuildingPredominateUse.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
-export class BuildingPredominateUses extends BaseEntity {
+export class BuildingPredominateUse extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -11,7 +11,7 @@ export class BuildingPredominateUses extends BaseEntity {
@Index({ unique: true })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/EvaluationKeys.ts b/express-api/src/typeorm/Entities/EvaluationKey.ts
similarity index 88%
rename from express-api/src/typeorm/Entities/EvaluationKeys.ts
rename to express-api/src/typeorm/Entities/EvaluationKey.ts
index 86d97367c..8b15452f5 100644
--- a/express-api/src/typeorm/Entities/EvaluationKeys.ts
+++ b/express-api/src/typeorm/Entities/EvaluationKey.ts
@@ -2,7 +2,7 @@ import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Entity, Column, Index, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
-export class EvaluationKeys extends BaseEntity {
+export class EvaluationKey extends BaseEntity {
@PrimaryGeneratedColumn({ type: 'int' })
Id: number;
diff --git a/express-api/src/typeorm/Entities/FiscalKeys.ts b/express-api/src/typeorm/Entities/FiscalKey.ts
similarity index 89%
rename from express-api/src/typeorm/Entities/FiscalKeys.ts
rename to express-api/src/typeorm/Entities/FiscalKey.ts
index 0b2ded21b..885d33bbb 100644
--- a/express-api/src/typeorm/Entities/FiscalKeys.ts
+++ b/express-api/src/typeorm/Entities/FiscalKey.ts
@@ -2,7 +2,7 @@ import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Entity, Column, Index, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
-export class FiscalKeys extends BaseEntity {
+export class FiscalKey extends BaseEntity {
@PrimaryGeneratedColumn({ type: 'int' })
Id: number;
diff --git a/express-api/src/typeorm/Entities/NotificationQueue.ts b/express-api/src/typeorm/Entities/NotificationQueue.ts
index c1246665c..14f0bb1d2 100644
--- a/express-api/src/typeorm/Entities/NotificationQueue.ts
+++ b/express-api/src/typeorm/Entities/NotificationQueue.ts
@@ -1,9 +1,9 @@
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { UUID } from 'crypto';
-import { Projects } from '@/typeorm/Entities/Projects';
-import { NotificationTemplates } from '@/typeorm/Entities/NotificationTemplates';
-import { Agencies } from './Agencies';
+import { Project } from '@/typeorm/Entities/Project';
+import { NotificationTemplate } from '@/typeorm/Entities/NotificationTemplate';
+import { Agency } from './Agency';
@Entity()
@Index(['Status', 'SendOn', 'Subject'])
@@ -50,19 +50,31 @@ export class NotificationQueue extends BaseEntity {
@Column({ type: 'character varying', length: 50, nullable: true })
Tag: string;
- @ManyToOne(() => Projects, (Project) => Project.Id)
+ // Project Relation
+ @Column({ name: 'ProjectId', type: 'int' })
+ ProjectId: number;
+
+ @ManyToOne(() => Project, (Project) => Project.Id)
@JoinColumn({ name: 'ProjectId' })
- ProjectId: Projects;
+ Project: Project;
+
+ // Agency Relation
+ @Column({ name: 'ToAgencyId', type: 'int' })
+ ToAgencyId: number;
- @ManyToOne(() => Agencies, (Agency) => Agency.Id)
+ @ManyToOne(() => Agency, (Agency) => Agency.Id)
@JoinColumn({ name: 'ToAgencyId' })
@Index()
- ToAgencyId: Agencies;
+ ToAgency: Agency;
+
+ // Template Relation
+ @Column({ name: 'TemplateId', type: 'int' })
+ TemplateId: number;
- @ManyToOne(() => NotificationTemplates, (Template) => Template.Id)
+ @ManyToOne(() => NotificationTemplate, (Template) => Template.Id)
@JoinColumn({ name: 'TemplateId' })
@Index()
- TemplateId: NotificationTemplates;
+ Template: NotificationTemplate;
@Column({ type: 'uuid' })
ChesMessageId: UUID;
diff --git a/express-api/src/typeorm/Entities/NotificationTemplates.ts b/express-api/src/typeorm/Entities/NotificationTemplate.ts
similarity index 94%
rename from express-api/src/typeorm/Entities/NotificationTemplates.ts
rename to express-api/src/typeorm/Entities/NotificationTemplate.ts
index 531aaed16..b2e6fb857 100644
--- a/express-api/src/typeorm/Entities/NotificationTemplates.ts
+++ b/express-api/src/typeorm/Entities/NotificationTemplate.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Tag'])
-export class NotificationTemplates extends BaseEntity {
+export class NotificationTemplate extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -41,7 +41,7 @@ export class NotificationTemplates extends BaseEntity {
@Column({ type: 'text', nullable: true })
Body: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column({ type: 'character varying', length: 50, nullable: true })
diff --git a/express-api/src/typeorm/Entities/Parcels.ts b/express-api/src/typeorm/Entities/Parcel.ts
similarity index 63%
rename from express-api/src/typeorm/Entities/Parcels.ts
rename to express-api/src/typeorm/Entities/Parcel.ts
index 9b09d3e62..d7dae9290 100644
--- a/express-api/src/typeorm/Entities/Parcels.ts
+++ b/express-api/src/typeorm/Entities/Parcel.ts
@@ -1,9 +1,9 @@
-import { Entity, Column, Index } from 'typeorm';
+import { Entity, Column, Index, ManyToOne, JoinColumn } from 'typeorm';
import { Property } from '@/typeorm/Entities/abstractEntities/Property';
@Entity()
@Index(['PID', 'PIN'], { unique: true })
-export class Parcels extends Property {
+export class Parcel extends Property {
@Column({ type: 'int' })
PID: number;
@@ -22,6 +22,13 @@ export class Parcels extends Property {
@Column({ type: 'character varying', length: 50, nullable: true })
ZoningPotential: string;
- @Column({ type: 'bit' })
+ @Column({ type: 'boolean' })
NotOwned: boolean;
+
+ @Column({ name: 'ParentParcelId', type: 'int', nullable: true })
+ ParentParcelId: number;
+
+ @ManyToOne(() => Parcel, (Parcel) => Parcel.Id)
+ @JoinColumn({ name: 'ParentParcelId' })
+ ParentParcel: Parcel;
}
diff --git a/express-api/src/typeorm/Entities/ParcelBuilding.ts b/express-api/src/typeorm/Entities/ParcelBuilding.ts
new file mode 100644
index 000000000..ec520c448
--- /dev/null
+++ b/express-api/src/typeorm/Entities/ParcelBuilding.ts
@@ -0,0 +1,21 @@
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
+import { Parcel } from '@/typeorm/Entities/Parcel';
+import { Building } from '@/typeorm/Entities/Building';
+
+@Entity()
+export class ParcelBuilding extends BaseEntity {
+ @PrimaryColumn({ name: 'ParcelId', type: 'int' })
+ ParcelId: number;
+
+ @ManyToOne(() => Parcel, (Parcel) => Parcel.Id)
+ @JoinColumn({ name: 'ParcelId' })
+ Parcel: Parcel;
+
+ @PrimaryColumn({ name: 'BuildingId', type: 'int' })
+ BuildingId: number;
+
+ @ManyToOne(() => Building, (Building) => Building.Id)
+ @JoinColumn({ name: 'BuildingId' })
+ Building: Building;
+}
diff --git a/express-api/src/typeorm/Entities/ParcelBuildings.ts b/express-api/src/typeorm/Entities/ParcelBuildings.ts
deleted file mode 100644
index 4297e20cf..000000000
--- a/express-api/src/typeorm/Entities/ParcelBuildings.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
-import { Parcels } from '@/typeorm/Entities/Parcels';
-import { Buildings } from '@/typeorm/Entities/Buildings';
-
-@Entity()
-export class ParcelBuildings extends BaseEntity {
- @ManyToOne(() => Parcels, (Parcel) => Parcel.Id)
- @JoinColumn({ name: 'ParcelId' })
- @PrimaryColumn('int')
- ParcelId: Parcels;
-
- @ManyToOne(() => Buildings, (Building) => Building.Id)
- @JoinColumn({ name: 'BuildingId' })
- @PrimaryColumn('int')
- BuildingId: Buildings;
-}
diff --git a/express-api/src/typeorm/Entities/ParcelEvaluations.ts b/express-api/src/typeorm/Entities/ParcelEvaluation.ts
similarity index 58%
rename from express-api/src/typeorm/Entities/ParcelEvaluations.ts
rename to express-api/src/typeorm/Entities/ParcelEvaluation.ts
index da71d3af9..0938f23d4 100644
--- a/express-api/src/typeorm/Entities/ParcelEvaluations.ts
+++ b/express-api/src/typeorm/Entities/ParcelEvaluation.ts
@@ -1,14 +1,16 @@
-import { Parcels } from '@/typeorm/Entities/Parcels';
+import { Parcel } from '@/typeorm/Entities/Parcel';
import { Evaluation } from '@/typeorm/Entities/abstractEntities/Evaluation';
import { Entity, Column, Index, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
@Entity()
@Index(['ParcelId', 'EvaluationKey'])
-export class ParcelEvaluations extends Evaluation {
- @ManyToOne(() => Parcels, (Parcel) => Parcel.Id)
+export class ParcelEvaluation extends Evaluation {
+ @PrimaryColumn({ name: 'ParcelId', type: 'int' })
+ ParcelId: number;
+
+ @ManyToOne(() => Parcel, (Parcel) => Parcel.Id)
@JoinColumn({ name: 'ParcelId' })
- @PrimaryColumn()
- ParcelId: Parcels;
+ Parcel: Parcel;
@Column({ type: 'character varying', length: 150, nullable: true })
Firm: string;
diff --git a/express-api/src/typeorm/Entities/ParcelFiscal.ts b/express-api/src/typeorm/Entities/ParcelFiscal.ts
new file mode 100644
index 000000000..22777b6bd
--- /dev/null
+++ b/express-api/src/typeorm/Entities/ParcelFiscal.ts
@@ -0,0 +1,14 @@
+import { Entity, Index, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { Fiscal } from '@/typeorm/Entities/abstractEntities/Fiscal';
+import { Parcel } from '@/typeorm/Entities/Parcel';
+
+@Entity()
+export class ParcelFiscal extends Fiscal {
+ @PrimaryColumn({ name: 'ParcelId', type: 'int' })
+ @Index()
+ ParcelId: number;
+
+ @ManyToOne(() => Parcel, (Parcel) => Parcel.Id)
+ @JoinColumn({ name: 'ParcelId' })
+ Parcel: Parcel;
+}
diff --git a/express-api/src/typeorm/Entities/ParcelFiscals.ts b/express-api/src/typeorm/Entities/ParcelFiscals.ts
deleted file mode 100644
index 950e341be..000000000
--- a/express-api/src/typeorm/Entities/ParcelFiscals.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Entity, Index, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
-import { Fiscal } from '@/typeorm/Entities/abstractEntities/Fiscal';
-import { Parcels } from '@/typeorm/Entities/Parcels';
-
-@Entity()
-export class ParcelFiscals extends Fiscal {
- @ManyToOne(() => Parcels, (Parcel) => Parcel.Id)
- @JoinColumn({ name: 'ParcelId' })
- @PrimaryColumn()
- @Index()
- ParcelId: Parcels;
-}
diff --git a/express-api/src/typeorm/Entities/Projects.ts b/express-api/src/typeorm/Entities/Project.ts
similarity index 63%
rename from express-api/src/typeorm/Entities/Projects.ts
rename to express-api/src/typeorm/Entities/Project.ts
index 1dcf0f90d..95b6ee11e 100644
--- a/express-api/src/typeorm/Entities/Projects.ts
+++ b/express-api/src/typeorm/Entities/Project.ts
@@ -1,15 +1,15 @@
import { Entity, Column, Index, JoinColumn, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
import { ProjectStatus } from '@/typeorm/Entities/ProjectStatus';
-import { Workflows } from '@/typeorm/Entities/Workflows';
-import { TierLevels } from '@/typeorm/Entities/TierLevels';
-import { ProjectRisks } from '@/typeorm/Entities/ProjectRisks';
+import { Workflow } from '@/typeorm/Entities/Workflow';
+import { TierLevel } from '@/typeorm/Entities/TierLevel';
+import { ProjectRisk } from '@/typeorm/Entities/ProjectRisk';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Agencies } from './Agencies';
+import { Agency } from './Agency';
@Entity()
@Index(['Assessed', 'NetBook', 'Market', 'ReportedFiscalYear', 'ActualFiscalYear'])
@Index(['StatusId', 'TierLevelId', 'AgencyId'])
-export class Projects extends BaseEntity {
+export class Project extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -65,28 +65,48 @@ export class Projects extends BaseEntity {
@Column('int')
ProjectType: number;
- @ManyToOne(() => Workflows, (Workflow) => Workflow.Id)
+ // Workflow Relation
+ @Column({ name: 'WorkflowId', type: 'int' })
+ WorkflowId: number;
+
+ @ManyToOne(() => Workflow, (Workflow) => Workflow.Id)
@JoinColumn({ name: 'WorkflowId' })
@Index()
- WorkflowId: Workflows;
+ Workflow: Workflow;
+
+ // Agency Relation
+ @Column({ name: 'AgencyId', type: 'int' })
+ AgencyId: number;
- @ManyToOne(() => Agencies, (Agency) => Agency.Id)
+ @ManyToOne(() => Agency, (Agency) => Agency.Id)
@JoinColumn({ name: 'AgencyId' })
@Index()
- AgencyId: Agencies;
+ Agency: Agency;
+
+ // Tier Level Relation
+ @Column({ name: 'TierLevelId', type: 'int' })
+ TierLevelId: number;
- @ManyToOne(() => TierLevels, (TierLevel) => TierLevel.Id)
+ @ManyToOne(() => TierLevel, (TierLevel) => TierLevel.Id)
@JoinColumn({ name: 'TierLevelId' })
@Index()
- TierLevelId: TierLevels;
+ TierLevel: TierLevel;
+
+ // Status Relation
+ @Column({ name: 'StatusId', type: 'int' })
+ StatusId: number;
@ManyToOne(() => ProjectStatus, (Status) => Status.Id)
@JoinColumn({ name: 'StatusId' })
@Index()
- StatusId: ProjectStatus;
+ Status: ProjectStatus;
+
+ // Risk Relation
+ @Column({ name: 'RiskId', type: 'int' })
+ RiskId: number;
- @ManyToOne(() => ProjectRisks, (Risk) => Risk.Id)
+ @ManyToOne(() => ProjectRisk, (Risk) => Risk.Id)
@JoinColumn({ name: 'RiskId' })
@Index()
- RiskId: ProjectRisks;
+ Risk: ProjectRisk;
}
diff --git a/express-api/src/typeorm/Entities/ProjectAgencyResponses.ts b/express-api/src/typeorm/Entities/ProjectAgencyResponse.ts
similarity index 55%
rename from express-api/src/typeorm/Entities/ProjectAgencyResponses.ts
rename to express-api/src/typeorm/Entities/ProjectAgencyResponse.ts
index 9bfd8bb0a..224f5cf8a 100644
--- a/express-api/src/typeorm/Entities/ProjectAgencyResponses.ts
+++ b/express-api/src/typeorm/Entities/ProjectAgencyResponse.ts
@@ -1,28 +1,38 @@
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
-import { Projects } from '@/typeorm/Entities/Projects';
+import { Project } from '@/typeorm/Entities/Project';
import { NotificationQueue } from '@/typeorm/Entities/NotificationQueue';
-import { Agencies } from './Agencies';
+import { Agency } from './Agency';
@Entity()
-export class ProjectAgencyResponses extends BaseEntity {
- @ManyToOne(() => Projects, (Project) => Project.Id)
+export class ProjectAgencyResponse extends BaseEntity {
+ // Project Relation
+ @PrimaryColumn({ name: 'ProjectId', type: 'int' })
+ ProjectId: number;
+
+ @ManyToOne(() => Project, (Project) => Project.Id)
@JoinColumn({ name: 'ProjectId' })
- @PrimaryColumn('int')
- ProjectId: Projects;
+ Project: Project;
+
+ // Agency Relation
+ @PrimaryColumn({ name: 'AgencyId', type: 'int' })
+ AgencyId: number;
- @ManyToOne(() => Agencies, (Agency) => Agency.Id)
+ @ManyToOne(() => Agency, (Agency) => Agency.Id)
@JoinColumn({ name: 'AgencyId' })
- @PrimaryColumn('int')
- AgencyId: Agencies;
+ Agency: Agency;
@Column({ type: 'money' })
OfferAmount: number;
+ // Notification Relation
+ @Column({ name: 'NotificationId', type: 'int' })
+ NotificationId: number;
+
@ManyToOne(() => NotificationQueue, (Notification) => Notification.Id, { nullable: true })
@JoinColumn({ name: 'NotificationId' })
@Index()
- NotificationId: NotificationQueue;
+ Notification: NotificationQueue;
// What is this field?
@Column({ type: 'int' })
diff --git a/express-api/src/typeorm/Entities/ProjectNotes.ts b/express-api/src/typeorm/Entities/ProjectNote.ts
similarity index 58%
rename from express-api/src/typeorm/Entities/ProjectNotes.ts
rename to express-api/src/typeorm/Entities/ProjectNote.ts
index dfefd8c59..f95a200ef 100644
--- a/express-api/src/typeorm/Entities/ProjectNotes.ts
+++ b/express-api/src/typeorm/Entities/ProjectNote.ts
@@ -1,16 +1,20 @@
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne, JoinColumn } from 'typeorm';
-import { Projects } from '@/typeorm/Entities/Projects';
+import { Project } from '@/typeorm/Entities/Project';
@Entity()
@Index(['ProjectId', 'NoteType'])
-export class ProjectNotes extends BaseEntity {
+export class ProjectNote extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
- @ManyToOne(() => Projects, (Project) => Project.Id)
+ // Project Relation
+ @Column({ name: 'ProjectId', type: 'int' })
+ ProjectId: number;
+
+ @ManyToOne(() => Project, (Project) => Project.Id)
@JoinColumn({ name: 'ProjectId' })
- ProjectId: Projects;
+ Project: Project;
@Column('int')
NoteType: number;
diff --git a/express-api/src/typeorm/Entities/ProjectNumbers.ts b/express-api/src/typeorm/Entities/ProjectNumber.ts
similarity index 79%
rename from express-api/src/typeorm/Entities/ProjectNumbers.ts
rename to express-api/src/typeorm/Entities/ProjectNumber.ts
index c69510213..8913c56bc 100644
--- a/express-api/src/typeorm/Entities/ProjectNumbers.ts
+++ b/express-api/src/typeorm/Entities/ProjectNumber.ts
@@ -2,7 +2,7 @@ import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
-export class ProjectNumbers extends BaseEntity {
+export class ProjectNumber extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
}
diff --git a/express-api/src/typeorm/Entities/ProjectProperties.ts b/express-api/src/typeorm/Entities/ProjectProperties.ts
deleted file mode 100644
index 1e86e748c..000000000
--- a/express-api/src/typeorm/Entities/ProjectProperties.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, Index } from 'typeorm';
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Projects } from '@/typeorm/Entities/Projects';
-import { Buildings } from '@/typeorm/Entities/Buildings';
-import { Parcels } from '@/typeorm/Entities/Parcels';
-import { PropertyTypes } from './PropertyTypes';
-
-@Entity()
-@Index(['ProjectId', 'PropertyType', 'ParcelId', 'BuildingId'])
-export class ProjectProperties extends BaseEntity {
- @PrimaryGeneratedColumn()
- Id: number;
-
- @ManyToOne(() => Projects, (Project) => Project.Id)
- @JoinColumn({ name: 'ProjectId' })
- ProjectId: Projects;
-
- @ManyToOne(() => PropertyTypes, (PropertyType) => PropertyType.Id)
- @JoinColumn({ name: 'PropertyType' })
- PropertyType: number;
-
- @ManyToOne(() => Parcels, (Parcel) => Parcel.Id)
- @JoinColumn({ name: 'ParcelId' })
- ParcelId: Parcels;
-
- @ManyToOne(() => Buildings, (Building) => Building.Id)
- @JoinColumn({ name: 'BuildingId' })
- BuildingId: Buildings;
-}
diff --git a/express-api/src/typeorm/Entities/ProjectProperty.ts b/express-api/src/typeorm/Entities/ProjectProperty.ts
new file mode 100644
index 000000000..c24e5d69c
--- /dev/null
+++ b/express-api/src/typeorm/Entities/ProjectProperty.ts
@@ -0,0 +1,45 @@
+import { Entity, PrimaryGeneratedColumn, ManyToOne, JoinColumn, Index, Column } from 'typeorm';
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+import { Project } from '@/typeorm/Entities/Project';
+import { Building } from '@/typeorm/Entities/Building';
+import { Parcel } from '@/typeorm/Entities/Parcel';
+import { PropertyType } from './PropertyType';
+
+@Entity()
+@Index(['ProjectId', 'PropertyType', 'ParcelId', 'BuildingId'])
+export class ProjectProperty extends BaseEntity {
+ @PrimaryGeneratedColumn()
+ Id: number;
+
+ // Project Relation
+ @Column({ name: 'ProjectId', type: 'int' })
+ ProjectId: number;
+
+ @ManyToOne(() => Project, (Project) => Project.Id)
+ @JoinColumn({ name: 'ProjectId' })
+ Project: Project;
+
+ // Property Type Relation
+ @Column({ name: 'PropertyTypeId', type: 'int' })
+ PropertyTypeId: number;
+
+ @ManyToOne(() => PropertyType, (PropertyType) => PropertyType.Id)
+ @JoinColumn({ name: 'PropertyTypeId' })
+ PropertyType: PropertyType;
+
+ // Parcel Relation
+ @Column({ name: 'ParcelId', type: 'int' })
+ ParcelId: number;
+
+ @ManyToOne(() => Parcel, (Parcel) => Parcel.Id)
+ @JoinColumn({ name: 'ParcelId' })
+ Parcel: Parcel;
+
+ // Building Relation
+ @Column({ name: 'BuildingId', type: 'int' })
+ BuildingId: number;
+
+ @ManyToOne(() => Building, (Building) => Building.Id)
+ @JoinColumn({ name: 'BuildingId' })
+ Building: Building;
+}
diff --git a/express-api/src/typeorm/Entities/ProjectReport.ts b/express-api/src/typeorm/Entities/ProjectReport.ts
new file mode 100644
index 000000000..f7f6c7687
--- /dev/null
+++ b/express-api/src/typeorm/Entities/ProjectReport.ts
@@ -0,0 +1,30 @@
+import { ReportType } from '@/typeorm/Entities/ReportType';
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+import { Entity, PrimaryGeneratedColumn, Column, Index, JoinColumn, ManyToOne } from 'typeorm';
+
+@Entity()
+@Index(['Id', 'To', 'From', 'IsFinal'])
+export class ProjectReport extends BaseEntity {
+ @PrimaryGeneratedColumn()
+ Id: number;
+
+ @Column('boolean')
+ IsFinal: boolean;
+
+ @Column({ type: 'character varying', length: 250, nullable: true })
+ Name: string;
+
+ @Column('timestamp')
+ From: Date;
+
+ @Column('timestamp')
+ To: Date;
+
+ // Report Type Relation
+ @Column({ name: 'ReportTypeId', type: 'int' })
+ ReportTypeId: number;
+
+ @ManyToOne(() => ReportType, (ReportType) => ReportType.Id)
+ @JoinColumn({ name: 'ReportTypeId' })
+ ReportType: ReportType;
+}
diff --git a/express-api/src/typeorm/Entities/ProjectReports.ts b/express-api/src/typeorm/Entities/ProjectReports.ts
deleted file mode 100644
index cf8b77209..000000000
--- a/express-api/src/typeorm/Entities/ProjectReports.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
-
-@Entity()
-@Index(['Id', 'To', 'From', 'IsFinal'])
-export class ProjectReports extends BaseEntity {
- @PrimaryGeneratedColumn()
- Id: number;
-
- @Column('bit')
- IsFinal: boolean;
-
- @Column({ type: 'character varying', length: 250, nullable: true })
- Name: string;
-
- @Column({ type: 'timestamp', nullable: true })
- From: Date;
-
- @Column('timestamp')
- To: Date;
-
- @Column('int')
- ReportType: number;
-}
diff --git a/express-api/src/typeorm/Entities/ProjectRisks.ts b/express-api/src/typeorm/Entities/ProjectRisk.ts
similarity index 89%
rename from express-api/src/typeorm/Entities/ProjectRisks.ts
rename to express-api/src/typeorm/Entities/ProjectRisk.ts
index 4a86b31a8..0a03aa63b 100644
--- a/express-api/src/typeorm/Entities/ProjectRisks.ts
+++ b/express-api/src/typeorm/Entities/ProjectRisk.ts
@@ -3,14 +3,14 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Code', 'Name', 'SortOrder'])
-export class ProjectRisks extends BaseEntity {
+export class ProjectRisk extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@Column({ type: 'character varying', length: 150 })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/ProjectSnapshots.ts b/express-api/src/typeorm/Entities/ProjectSnapshot.ts
similarity index 71%
rename from express-api/src/typeorm/Entities/ProjectSnapshots.ts
rename to express-api/src/typeorm/Entities/ProjectSnapshot.ts
index d13381c42..7cd144cd4 100644
--- a/express-api/src/typeorm/Entities/ProjectSnapshots.ts
+++ b/express-api/src/typeorm/Entities/ProjectSnapshot.ts
@@ -8,19 +8,22 @@ import {
PrimaryColumn,
PrimaryGeneratedColumn,
} from 'typeorm';
-import { Projects } from '@/typeorm/Entities/Projects';
+import { Project } from '@/typeorm/Entities/Project';
@Entity()
@Index(['ProjectId', 'SnapshotOn'])
-export class ProjectSnapshots extends BaseEntity {
+export class ProjectSnapshot extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
- @ManyToOne(() => Projects, (Project) => Project.Id)
+ // Project Relation
+ @PrimaryColumn({ name: 'ProjectId', type: 'int' })
+ ProjectId: number;
+
+ @ManyToOne(() => Project, (Project) => Project.Id)
@JoinColumn({ name: 'ProjectId' })
- @PrimaryColumn()
@Index()
- ProjectId: Projects;
+ Project: Project;
@Column('money', { nullable: true })
NetBook: number;
diff --git a/express-api/src/typeorm/Entities/ProjectStatus.ts b/express-api/src/typeorm/Entities/ProjectStatus.ts
index 5a7c4057e..cd6836cb1 100644
--- a/express-api/src/typeorm/Entities/ProjectStatus.ts
+++ b/express-api/src/typeorm/Entities/ProjectStatus.ts
@@ -1,16 +1,16 @@
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
+import { Entity, PrimaryColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'Code', 'SortOrder'])
export class ProjectStatus extends BaseEntity {
- @PrimaryGeneratedColumn()
+ @PrimaryColumn()
Id: number;
@Column({ type: 'character varying', length: 150 })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
@@ -26,10 +26,10 @@ export class ProjectStatus extends BaseEntity {
@Column('text', { nullable: true })
Description: string;
- @Column('bit')
+ @Column('boolean')
IsMilestone: boolean;
- @Column('bit')
+ @Column('boolean')
IsTerminal: boolean;
@Column({ type: 'character varying', length: 150 })
diff --git a/express-api/src/typeorm/Entities/ProjectStatusHistory.ts b/express-api/src/typeorm/Entities/ProjectStatusHistory.ts
index 1ebdf9a25..83ac5459c 100644
--- a/express-api/src/typeorm/Entities/ProjectStatusHistory.ts
+++ b/express-api/src/typeorm/Entities/ProjectStatusHistory.ts
@@ -7,30 +7,39 @@ import {
PrimaryColumn,
} from 'typeorm';
import { ProjectStatus } from '@/typeorm/Entities/ProjectStatus';
-import { Workflows } from '@/typeorm/Entities/Workflows';
+import { Workflow } from '@/typeorm/Entities/Workflow';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Projects } from '@/typeorm/Entities/Projects';
+import { Project } from '@/typeorm/Entities/Project';
@Entity()
export class ProjectStatusHistory extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
- @ManyToOne(() => Projects, (Project) => Project.Id)
+ // Project Relation
+ @PrimaryColumn({ name: 'ProjectId', type: 'int' })
+ ProjectId: number;
+
+ @ManyToOne(() => Project, (Project) => Project.Id)
@JoinColumn({ name: 'ProjectId' })
- @PrimaryColumn()
@Index()
- ProjectId: Projects;
+ Project: Project;
+
+ // Workflow Relation
+ @PrimaryColumn({ name: 'WorkflowId', type: 'int' })
+ WorkflowId: number;
- @ManyToOne(() => Workflows, (Workflow) => Workflow.Id)
+ @ManyToOne(() => Workflow, (Workflow) => Workflow.Id)
@JoinColumn({ name: 'WorkflowId' })
- @PrimaryColumn()
@Index()
- WorkflowId: Workflows;
+ Workflow: Workflow;
+
+ // Status Relation
+ @PrimaryColumn({ name: 'StatusId', type: 'int' })
+ StatusId: number;
@ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id)
@JoinColumn({ name: 'StatusId' })
- @PrimaryColumn()
@Index()
- StatusId: ProjectStatus;
+ Status: ProjectStatus;
}
diff --git a/express-api/src/typeorm/Entities/ProjectStatusNotifications.ts b/express-api/src/typeorm/Entities/ProjectStatusNotification.ts
similarity index 54%
rename from express-api/src/typeorm/Entities/ProjectStatusNotifications.ts
rename to express-api/src/typeorm/Entities/ProjectStatusNotification.ts
index e29fc64bc..f73281829 100644
--- a/express-api/src/typeorm/Entities/ProjectStatusNotifications.ts
+++ b/express-api/src/typeorm/Entities/ProjectStatusNotification.ts
@@ -1,27 +1,39 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne, JoinColumn } from 'typeorm';
import { ProjectStatus } from './ProjectStatus';
-import { NotificationTemplates } from './NotificationTemplates';
+import { NotificationTemplate } from './NotificationTemplate';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
@Entity()
@Index(['FromStatusId', 'ToStatusId'])
-export class ProjectStatusNotifications extends BaseEntity {
+export class ProjectStatusNotification extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
- @ManyToOne(() => NotificationTemplates, (Template) => Template.Id)
- @JoinColumn({ name: 'TemplateId' })
+ // Template Relation
+ @Column({ name: 'TemplateId', type: 'int' })
@Index()
- TemplateId: NotificationTemplates;
+ TemplateId: number;
+
+ @ManyToOne(() => NotificationTemplate, (Template) => Template.Id)
+ @JoinColumn({ name: 'TemplateId' })
+ Template: NotificationTemplate;
+
+ // From Status Relation
+ @Column({ name: 'FromStatusId', type: 'int', nullable: true })
+ FromStatusId: number;
@ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id)
@JoinColumn({ name: 'FromStatusId' })
- FromStatusId: ProjectStatus;
+ FromStatus: ProjectStatus;
+
+ // To Status Relation
+ @Column({ name: 'ToStatusId', type: 'int' })
+ ToStatusId: number;
@ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id)
@JoinColumn({ name: 'ToStatusId' })
@Index()
- ToStatusId: ProjectStatus;
+ ToStatus: ProjectStatus;
@Column('int')
Priority: number;
diff --git a/express-api/src/typeorm/Entities/ProjectStatusTransition.ts b/express-api/src/typeorm/Entities/ProjectStatusTransition.ts
new file mode 100644
index 000000000..9767ba41e
--- /dev/null
+++ b/express-api/src/typeorm/Entities/ProjectStatusTransition.ts
@@ -0,0 +1,47 @@
+import { Entity, Column, Index, ManyToOne, JoinColumn, Relation, PrimaryColumn } from 'typeorm';
+import { Workflow } from './Workflow';
+import { ProjectStatus } from './ProjectStatus';
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+
+@Entity()
+@Index(['ToWorkflowId', 'ToStatusId'])
+@Index(['FromWorkflowId', 'FromStatusId'])
+export class ProjectStatusTransition extends BaseEntity {
+ // From Workflow Relation
+ @PrimaryColumn({ name: 'FromWorkflowId', type: 'int' })
+ FromWorkflowId: number;
+
+ @ManyToOne(() => Workflow, (Workflow) => Workflow.Id)
+ @JoinColumn({ name: 'FromWorkflowId' })
+ FromWorkflow: Relation;
+
+ // From Status Relation
+ @PrimaryColumn({ name: 'FromStatusId', type: 'int' })
+ FromStatusId: number;
+
+ @ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id)
+ @JoinColumn({ name: 'FromStatusId' })
+ FromStatus: Relation;
+
+ // To Workflow Relation
+ @PrimaryColumn({ name: 'ToWorkflowId', type: 'int' })
+ ToWorkflowId: number;
+
+ @ManyToOne(() => Workflow, (Workflow) => Workflow.Id)
+ @JoinColumn({ name: 'ToWorkflowId' })
+ ToWorkflow: Relation;
+
+ // To Status Relation
+ @PrimaryColumn({ name: 'ToStatusId', type: 'int' })
+ ToStatusId: number;
+
+ @ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id)
+ @JoinColumn({ name: 'ToStatusId' })
+ ToStatus: Relation;
+
+ @Column({ type: 'character varying', length: 100 })
+ Action: string;
+
+ @Column('boolean')
+ ValidateTasks: boolean;
+}
diff --git a/express-api/src/typeorm/Entities/ProjectStatusTransitions.ts b/express-api/src/typeorm/Entities/ProjectStatusTransitions.ts
deleted file mode 100644
index 82bfbd3c5..000000000
--- a/express-api/src/typeorm/Entities/ProjectStatusTransitions.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Entity, Column, Index, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
-import { ProjectStatus } from './ProjectStatus';
-import { Workflows } from './Workflows';
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-
-@Entity()
-@Index(['ToWorkflowId', 'ToStatusId'])
-export class ProjectStatusTransitions extends BaseEntity {
- @ManyToOne(() => Workflows, (Workflow) => Workflow.Id)
- @JoinColumn({ name: 'FromWorkflowId' })
- @PrimaryColumn()
- @Index()
- FromWorkflowId: Workflows;
-
- @ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id)
- @JoinColumn({ name: 'FromStatusId' })
- @PrimaryColumn()
- FromStatusId: ProjectStatus;
-
- @ManyToOne(() => Workflows, (Workflow) => Workflow.Id)
- @JoinColumn({ name: 'ToWorkflowId' })
- @PrimaryColumn()
- @Index()
- ToWorkflowId: Workflows;
-
- @ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id)
- @JoinColumn({ name: 'ToStatusId' })
- @PrimaryColumn()
- @Index()
- ToStatusId: ProjectStatus;
-
- @Column({ type: 'character varying', length: 100 })
- Action: string;
-
- @Column('bit')
- ValidateTasks: boolean;
-}
diff --git a/express-api/src/typeorm/Entities/ProjectTask.ts b/express-api/src/typeorm/Entities/ProjectTask.ts
new file mode 100644
index 000000000..ed5135b58
--- /dev/null
+++ b/express-api/src/typeorm/Entities/ProjectTask.ts
@@ -0,0 +1,39 @@
+import {
+ Entity,
+ Column,
+ CreateDateColumn,
+ Index,
+ ManyToOne,
+ PrimaryColumn,
+ JoinColumn,
+} from 'typeorm';
+import { Project } from './Project';
+import { Task } from './Task';
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+
+@Entity()
+@Index(['ProjectId', 'TaskId', 'IsCompleted', 'CompletedOn'])
+export class ProjectTask extends BaseEntity {
+ // Project Relation
+ @PrimaryColumn({ name: 'ProjectId', type: 'int' })
+ ProjectId: number;
+
+ @ManyToOne(() => Project, (Project) => Project.Id)
+ @JoinColumn({ name: 'ProjectId' })
+ Project: Project;
+
+ // Task Relation
+ @PrimaryColumn({ name: 'TaskId', type: 'int' })
+ @Index()
+ TaskId: number;
+
+ @ManyToOne(() => Task, (Task) => Task.Id)
+ @JoinColumn({ name: 'TaskId' })
+ Task: Task;
+
+ @Column('boolean')
+ IsCompleted: boolean;
+
+ @CreateDateColumn({ nullable: true })
+ CompletedOn: Date;
+}
diff --git a/express-api/src/typeorm/Entities/ProjectTasks.ts b/express-api/src/typeorm/Entities/ProjectTasks.ts
deleted file mode 100644
index ee7704ac8..000000000
--- a/express-api/src/typeorm/Entities/ProjectTasks.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import {
- Entity,
- Column,
- CreateDateColumn,
- Index,
- ManyToOne,
- PrimaryColumn,
- JoinColumn,
-} from 'typeorm';
-import { Projects } from './Projects';
-import { Tasks } from './Tasks';
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-
-@Entity()
-@Index(['ProjectId', 'TaskId', 'IsCompleted', 'CompletedOn'])
-export class ProjectTasks extends BaseEntity {
- @ManyToOne(() => Projects, (Project) => Project.Id)
- @JoinColumn({ name: 'ProjectId' })
- @PrimaryColumn()
- ProjectId: Projects;
-
- @ManyToOne(() => Tasks, (Task) => Task.Id)
- @JoinColumn({ name: 'TaskId' })
- @PrimaryColumn()
- @Index()
- TaskId: Tasks;
-
- @Column('bit')
- IsCompleted: boolean;
-
- @CreateDateColumn({ nullable: true })
- CompletedOn: Date;
-}
diff --git a/express-api/src/typeorm/Entities/ReportTypes.ts b/express-api/src/typeorm/Entities/ProjectType.ts
similarity index 87%
rename from express-api/src/typeorm/Entities/ReportTypes.ts
rename to express-api/src/typeorm/Entities/ProjectType.ts
index ca0511cad..15f5d4937 100644
--- a/express-api/src/typeorm/Entities/ReportTypes.ts
+++ b/express-api/src/typeorm/Entities/ProjectType.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
-export class ReportTypes extends BaseEntity {
+export class ProjectType extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -14,7 +14,7 @@ export class ReportTypes extends BaseEntity {
@Column('text', { nullable: true })
Description: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/PropertyClassifications.ts b/express-api/src/typeorm/Entities/PropertyClassification.ts
similarity index 80%
rename from express-api/src/typeorm/Entities/PropertyClassifications.ts
rename to express-api/src/typeorm/Entities/PropertyClassification.ts
index a04f215a2..c4212da0f 100644
--- a/express-api/src/typeorm/Entities/PropertyClassifications.ts
+++ b/express-api/src/typeorm/Entities/PropertyClassification.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name'])
-export class PropertyClassifications extends BaseEntity {
+export class PropertyClassification extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -11,12 +11,12 @@ export class PropertyClassifications extends BaseEntity {
@Index({ unique: true })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
SortOrder: number;
- @Column('bit')
+ @Column('boolean')
IsVisible: boolean;
}
diff --git a/express-api/src/typeorm/Entities/PropertyTypes.ts b/express-api/src/typeorm/Entities/PropertyType.ts
similarity index 85%
rename from express-api/src/typeorm/Entities/PropertyTypes.ts
rename to express-api/src/typeorm/Entities/PropertyType.ts
index 21be88c2b..add2730c3 100644
--- a/express-api/src/typeorm/Entities/PropertyTypes.ts
+++ b/express-api/src/typeorm/Entities/PropertyType.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
-export class PropertyTypes extends BaseEntity {
+export class PropertyType extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -11,7 +11,7 @@ export class PropertyTypes extends BaseEntity {
@Index({ unique: true })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/Provinces.ts b/express-api/src/typeorm/Entities/Province.ts
similarity index 88%
rename from express-api/src/typeorm/Entities/Provinces.ts
rename to express-api/src/typeorm/Entities/Province.ts
index 3386aaf2f..3562f35f3 100644
--- a/express-api/src/typeorm/Entities/Provinces.ts
+++ b/express-api/src/typeorm/Entities/Province.ts
@@ -2,7 +2,7 @@ import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Entity, Column, Index, PrimaryColumn } from 'typeorm';
@Entity()
-export class Provinces extends BaseEntity {
+export class Province extends BaseEntity {
@PrimaryColumn({ type: 'character varying', length: 2 })
Id: string;
diff --git a/express-api/src/typeorm/Entities/RegionalDistricts.ts b/express-api/src/typeorm/Entities/RegionalDistrict.ts
similarity index 61%
rename from express-api/src/typeorm/Entities/RegionalDistricts.ts
rename to express-api/src/typeorm/Entities/RegionalDistrict.ts
index 3850a84b0..4a30d7bcb 100644
--- a/express-api/src/typeorm/Entities/RegionalDistricts.ts
+++ b/express-api/src/typeorm/Entities/RegionalDistrict.ts
@@ -2,10 +2,13 @@ import { Entity, PrimaryColumn, Column, Index } from 'typeorm';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
@Entity()
-export class RegionalDistricts extends BaseEntity {
- @PrimaryColumn({ type: 'character varying', length: 4 })
+export class RegionalDistrict extends BaseEntity {
+ @PrimaryColumn({ type: 'int' })
@Index({ unique: true })
- Id: string;
+ Id: number;
+
+ @Column({ type: 'character varying', length: 5 })
+ Abbreviation: string;
@Column({ type: 'character varying', length: 250 })
@Index({ unique: true })
diff --git a/express-api/src/typeorm/Entities/ProjectTypes.ts b/express-api/src/typeorm/Entities/ReportType.ts
similarity index 87%
rename from express-api/src/typeorm/Entities/ProjectTypes.ts
rename to express-api/src/typeorm/Entities/ReportType.ts
index 3e4d7f620..d5a4b1dab 100644
--- a/express-api/src/typeorm/Entities/ProjectTypes.ts
+++ b/express-api/src/typeorm/Entities/ReportType.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
-export class ProjectTypes extends BaseEntity {
+export class ReportType extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -14,7 +14,7 @@ export class ProjectTypes extends BaseEntity {
@Column('text', { nullable: true })
Description: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/Role.ts b/express-api/src/typeorm/Entities/Role.ts
new file mode 100644
index 000000000..058c4cce6
--- /dev/null
+++ b/express-api/src/typeorm/Entities/Role.ts
@@ -0,0 +1,33 @@
+import { UUID } from 'crypto';
+import { Entity, Column, Index, PrimaryColumn, OneToMany, Relation } from 'typeorm';
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+import { User } from '@/typeorm/Entities/User';
+
+@Entity()
+@Index(['IsDisabled', 'Name'])
+export class Role extends BaseEntity {
+ @PrimaryColumn({ type: 'uuid' })
+ Id: UUID;
+
+ @Column({ type: 'character varying', length: 100 })
+ @Index({ unique: true })
+ Name: string;
+
+ @Column('boolean')
+ IsDisabled: boolean;
+
+ @Column('int')
+ SortOrder: number;
+
+ @Column('uuid', { nullable: true })
+ KeycloakGroupId: string;
+
+ @Column('text', { nullable: true })
+ Description: string;
+
+ @Column('boolean')
+ IsPublic: boolean;
+
+ @OneToMany(() => User, (user) => user.Role)
+ Users: Relation[];
+}
diff --git a/express-api/src/typeorm/Entities/Subdivisions.ts b/express-api/src/typeorm/Entities/Subdivisions.ts
deleted file mode 100644
index e3541bb24..000000000
--- a/express-api/src/typeorm/Entities/Subdivisions.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Parcels } from '@/typeorm/Entities/Parcels';
-import { Entity, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
-
-@Entity()
-export class Subdivisions extends BaseEntity {
- @ManyToOne(() => Parcels, (Parcel) => Parcel.Id)
- @JoinColumn({ name: 'ParentId' })
- @PrimaryColumn('int')
- ParentId: number;
-
- @ManyToOne(() => Parcels, (Parcel) => Parcel.Id)
- @JoinColumn({ name: 'SubdivisionId' })
- @PrimaryColumn('int')
- SubdivisionId: number;
-}
diff --git a/express-api/src/typeorm/Entities/Tasks.ts b/express-api/src/typeorm/Entities/Task.ts
similarity index 78%
rename from express-api/src/typeorm/Entities/Tasks.ts
rename to express-api/src/typeorm/Entities/Task.ts
index 1b5f36c7c..3a3a5bf75 100644
--- a/express-api/src/typeorm/Entities/Tasks.ts
+++ b/express-api/src/typeorm/Entities/Task.ts
@@ -4,14 +4,14 @@ import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
@Entity()
@Index(['IsDisabled', 'IsOptional', 'Name', 'SortOrder'])
-export class Tasks extends BaseEntity {
+export class Task extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@Column({ type: 'character varying', length: 150 })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
@@ -20,10 +20,14 @@ export class Tasks extends BaseEntity {
@Column('text', { nullable: true })
Description: string;
- @Column('bit')
+ @Column('boolean')
IsOptional: boolean;
+ // Status Relation
+ @Column({ name: 'StatusId', type: 'int' })
+ StatusId: number;
+
@ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id, { nullable: true })
@JoinColumn({ name: 'StatusId' })
- StatusId: ProjectStatus;
+ Status: ProjectStatus;
}
diff --git a/express-api/src/typeorm/Entities/TierLevels.ts b/express-api/src/typeorm/Entities/TierLevel.ts
similarity index 87%
rename from express-api/src/typeorm/Entities/TierLevels.ts
rename to express-api/src/typeorm/Entities/TierLevel.ts
index 57cc20ca0..e4d75d797 100644
--- a/express-api/src/typeorm/Entities/TierLevels.ts
+++ b/express-api/src/typeorm/Entities/TierLevel.ts
@@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
-export class TierLevels extends BaseEntity {
+export class TierLevel extends BaseEntity {
@PrimaryGeneratedColumn()
Id: number;
@@ -11,7 +11,7 @@ export class TierLevels extends BaseEntity {
@Index({ unique: true })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/User.ts b/express-api/src/typeorm/Entities/User.ts
new file mode 100644
index 000000000..20765e9ea
--- /dev/null
+++ b/express-api/src/typeorm/Entities/User.ts
@@ -0,0 +1,79 @@
+import { UUID } from 'crypto';
+import { Entity, Column, ManyToOne, Index, JoinColumn, PrimaryColumn, Relation } from 'typeorm';
+import { Agency } from '@/typeorm/Entities/Agency';
+import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
+import { Role } from '@/typeorm/Entities/Role';
+
+@Entity()
+export class User extends BaseEntity {
+ @PrimaryColumn({ type: 'uuid' })
+ Id: UUID;
+
+ @Column({ type: 'character varying', length: 25 })
+ @Index({ unique: true })
+ Username: string;
+
+ @Column({ type: 'character varying', length: 100 })
+ DisplayName: string;
+
+ @Column({ type: 'character varying', length: 100 })
+ FirstName: string;
+
+ @Column({ type: 'character varying', length: 100, nullable: true })
+ MiddleName: string;
+
+ @Column({ type: 'character varying', length: 100 })
+ LastName: string;
+
+ @Column({ type: 'character varying', length: 100 })
+ @Index({ unique: true })
+ Email: string;
+
+ @Column({ type: 'character varying', length: 100, nullable: true })
+ Position: string;
+
+ @Column('boolean')
+ IsDisabled: boolean;
+
+ @Column('boolean')
+ EmailVerified: boolean;
+
+ @Column('boolean')
+ IsSystem: boolean;
+
+ @Column({ type: 'character varying', length: 1000, nullable: true })
+ Note: string;
+
+ @Column({ type: 'timestamp', nullable: true })
+ LastLogin: Date;
+
+ @Column({ name: 'ApprovedById', type: 'uuid', nullable: true })
+ ApprovedById: UUID;
+
+ @ManyToOne(() => User, (User) => User.Id, { nullable: true })
+ @JoinColumn({ name: 'ApprovedById' })
+ ApprovedBy: User;
+
+ @Column({ type: 'timestamp', nullable: true })
+ ApprovedOn: Date;
+
+ @Column({ type: 'uuid', nullable: true })
+ @Index({ unique: true })
+ KeycloakUserId: string;
+
+ // Agency Relations
+ @Column({ name: 'AgencyId', type: 'int', nullable: true })
+ AgencyId: number;
+
+ @ManyToOne(() => Agency, (agency) => agency.Users, { nullable: true })
+ @JoinColumn({ name: 'AgencyId' })
+ Agency: Relation;
+
+ // Role Relations
+ @Column({ name: 'RoleId', type: 'uuid', nullable: true })
+ RoleId: UUID;
+
+ @ManyToOne(() => Role, (role) => role.Users, { nullable: true })
+ @JoinColumn({ name: 'RoleId' })
+ Role: Relation;
+}
diff --git a/express-api/src/typeorm/Entities/Users_Roles_Claims.ts b/express-api/src/typeorm/Entities/Users_Roles_Claims.ts
deleted file mode 100644
index adc4f41f8..000000000
--- a/express-api/src/typeorm/Entities/Users_Roles_Claims.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import { UUID } from 'crypto';
-import {
- Entity,
- Column,
- CreateDateColumn,
- ManyToOne,
- Index,
- JoinColumn,
- PrimaryColumn,
- OneToMany,
- Relation,
-} from 'typeorm';
-import { Agencies } from './Agencies';
-
-@Entity()
-export class Users {
- @PrimaryColumn({ type: 'uuid' })
- Id: UUID;
-
- @ManyToOne(() => Users, (User) => User.Id)
- @JoinColumn({ name: 'CreatedById' })
- CreatedById: Users;
-
- @CreateDateColumn()
- CreatedOn: Date;
-
- @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
- @JoinColumn({ name: 'UpdatedById' })
- UpdatedById: Users;
-
- @Column({ type: 'timestamp', nullable: true })
- UpdatedOn: Date;
-
- @Column({ type: 'character varying', length: 25 })
- @Index({ unique: true })
- Username: string;
-
- @Column({ type: 'character varying', length: 100 })
- DisplayName: string;
-
- @Column({ type: 'character varying', length: 100 })
- FirstName: string;
-
- @Column({ type: 'character varying', length: 100, nullable: true })
- MiddleName: string;
-
- @Column({ type: 'character varying', length: 100 })
- LastName: string;
-
- @Column({ type: 'character varying', length: 100 })
- @Index({ unique: true })
- Email: string;
-
- @Column({ type: 'character varying', length: 100, nullable: true })
- Position: string;
-
- @Column('bit')
- IsDisabled: boolean;
-
- @Column('bit')
- EmailVerified: boolean;
-
- @Column('bit')
- IsSystem: boolean;
-
- @Column({ type: 'character varying', length: 1000, nullable: true })
- Note: string;
-
- @Column({ type: 'timestamp', nullable: true })
- LastLogin: Date;
-
- @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
- @JoinColumn({ name: 'ApprovedById' })
- ApprovedById: Users;
-
- @Column({ type: 'timestamp', nullable: true })
- ApprovedOn: Date;
-
- @Column({ type: 'uuid', nullable: true })
- @Index({ unique: true })
- KeycloakUserId: string;
-
- @Column({ name: 'AgencyId', type: 'varchar', length: 6, nullable: true })
- AgencyId: string;
-
- @ManyToOne(() => Agencies, (agency) => agency.Users, { nullable: true })
- @JoinColumn({ name: 'AgencyId' })
- Agency: Relation;
-
- @OneToMany(() => UserRoles, (userRole) => userRole.User, { cascade: true })
- UserRoles: UserRoles[];
-}
-
-//This is copied from the BaseEntity in its own file. Obviously duplication is not ideal, but I doubt this will be getting changed much so should be acceptable.
-//Can't just import it at the top since it depends on Users.
-abstract class BaseEntity {
- @ManyToOne(() => Users, (User) => User.Id)
- @JoinColumn({ name: 'CreatedById' })
- @Index()
- CreatedById: Users;
-
- @CreateDateColumn()
- CreatedOn: Date;
-
- @ManyToOne(() => Users, (User) => User.Id, { nullable: true })
- @JoinColumn({ name: 'UpdatedById' })
- @Index()
- UpdatedById: Users;
-
- @Column({ type: 'timestamp', nullable: true })
- UpdatedOn: Date;
-}
-
-@Entity()
-@Index(['IsDisabled', 'Name'])
-export class Roles extends BaseEntity {
- @PrimaryColumn({ type: 'uuid' })
- Id: UUID;
-
- @Column({ type: 'character varying', length: 100 })
- @Index({ unique: true })
- Name: string;
-
- @Column('bit')
- IsDisabled: boolean;
-
- @Column('int')
- SortOrder: number;
-
- @Column('uuid', { nullable: true })
- KeycloakGroupId: string;
-
- @Column('text', { nullable: true })
- Description: string;
-
- @Column('bit')
- IsPublic: boolean;
-
- @OneToMany(() => UserRoles, (userRole) => userRole.Role)
- UserRoles: UserRoles[];
-
- @OneToMany(() => RoleClaims, (roleClaim) => roleClaim.Role)
- RoleClaims: RoleClaims[];
-}
-
-@Entity()
-@Index(['IsDisabled', 'Name'])
-export class Claims extends BaseEntity {
- @PrimaryColumn({ type: 'uuid' })
- Id: UUID;
-
- @Column({ type: 'character varying', length: 150 })
- @Index({ unique: true })
- Name: string;
-
- @Column('uuid', { nullable: true })
- KeycloakRoleId: string;
-
- @Column('text', { nullable: true })
- Description: string;
-
- @Column('bit')
- IsDisabled: boolean;
-
- @OneToMany(() => RoleClaims, (roleClaim) => roleClaim.Claim)
- RoleClaims: RoleClaims[];
-}
-
-@Entity()
-export class RoleClaims extends BaseEntity {
- @PrimaryColumn()
- RoleId: string;
-
- @PrimaryColumn()
- ClaimId: string;
-
- @ManyToOne(() => Roles, (Role) => Role.Id)
- @JoinColumn({ name: 'RoleId', referencedColumnName: 'Id' })
- Role: Roles;
-
- @ManyToOne(() => Claims, (Claim) => Claim.Id)
- @JoinColumn({ name: 'ClaimId', referencedColumnName: 'Id' })
- Claim: Claims;
-}
-
-@Entity()
-export class UserRoles extends BaseEntity {
- @PrimaryColumn()
- RoleId: string;
-
- @PrimaryColumn()
- UserId: string;
-
- @ManyToOne(() => Users, (User) => User.UserRoles)
- @JoinColumn({ name: 'UserId', referencedColumnName: 'Id' })
- User: Users;
-
- @ManyToOne(() => Roles, (Role) => Role.UserRoles)
- @JoinColumn({ name: 'RoleId', referencedColumnName: 'Id' })
- Role: Roles;
-}
diff --git a/express-api/src/typeorm/Entities/Workflows.ts b/express-api/src/typeorm/Entities/Workflow.ts
similarity index 74%
rename from express-api/src/typeorm/Entities/Workflows.ts
rename to express-api/src/typeorm/Entities/Workflow.ts
index 4026d27e0..5cc8c1f33 100644
--- a/express-api/src/typeorm/Entities/Workflows.ts
+++ b/express-api/src/typeorm/Entities/Workflow.ts
@@ -1,17 +1,17 @@
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
-import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
+import { Entity, PrimaryColumn, Column, Index } from 'typeorm';
@Entity()
@Index(['IsDisabled', 'Name', 'SortOrder'])
-export class Workflows extends BaseEntity {
- @PrimaryGeneratedColumn()
+export class Workflow extends BaseEntity {
+ @PrimaryColumn()
Id: number;
@Column({ type: 'character varying', length: 150 })
@Index({ unique: true })
Name: string;
- @Column('bit')
+ @Column('boolean')
IsDisabled: boolean;
@Column('int')
diff --git a/express-api/src/typeorm/Entities/WorkflowProjectStatus.ts b/express-api/src/typeorm/Entities/WorkflowProjectStatus.ts
index fbfa24378..d213ac7ed 100644
--- a/express-api/src/typeorm/Entities/WorkflowProjectStatus.ts
+++ b/express-api/src/typeorm/Entities/WorkflowProjectStatus.ts
@@ -1,24 +1,31 @@
import { Entity, Column, Index, ManyToOne, JoinColumn, PrimaryColumn } from 'typeorm';
-import { Workflows } from '@/typeorm/Entities/Workflows';
+import { Workflow } from '@/typeorm/Entities/Workflow';
import { ProjectStatus } from '@/typeorm/Entities/ProjectStatus';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
@Entity()
-@Index(['StatusId'])
export class WorkflowProjectStatus extends BaseEntity {
- @ManyToOne(() => Workflows, (Workflow) => Workflow.Id)
+ // Workflow Relation
+ @PrimaryColumn({ name: 'WorkflowId', type: 'int' })
+ @Index()
+ WorkflowId: number;
+
+ @ManyToOne(() => Workflow, (Workflow) => Workflow.Id)
@JoinColumn({ name: 'WorkflowId' })
- @PrimaryColumn()
- WorkflowId: Workflows;
+ Workflow: Workflow;
+
+ // Status Relation
+ @PrimaryColumn({ name: 'StatusId', type: 'int' })
+ @Index()
+ StatusId: number;
@ManyToOne(() => ProjectStatus, (ProjectStatus) => ProjectStatus.Id)
@JoinColumn({ name: 'StatusId' })
- @PrimaryColumn()
- StatusId: ProjectStatus;
+ Status: ProjectStatus;
@Column('int')
SortOrder: number;
- @Column('bit')
+ @Column('boolean')
IsOptional: boolean;
}
diff --git a/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts b/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
index d58f45246..2fbc9ba67 100644
--- a/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
+++ b/express-api/src/typeorm/Entities/abstractEntities/BaseEntity.ts
@@ -1,19 +1,26 @@
-import type { Users } from '@/typeorm/Entities/Users_Roles_Claims';
+import type { User } from '@/typeorm/Entities/User';
+import { UUID } from 'crypto';
import { Column, CreateDateColumn, ManyToOne, JoinColumn, Index, Relation } from 'typeorm';
export abstract class BaseEntity {
- @ManyToOne('Users', 'Users.Id')
+ @Column({ name: 'CreatedById' })
+ CreatedById: UUID;
+
+ @ManyToOne('User', 'User.Id')
@JoinColumn({ name: 'CreatedById' })
@Index()
- CreatedById: Relation;
+ CreatedBy: Relation;
@CreateDateColumn()
CreatedOn: Date;
- @ManyToOne('Users', 'Users.Id', { nullable: true })
+ @Column({ name: 'UpdatedById', nullable: true })
+ UpdatedById: UUID;
+
+ @ManyToOne('User', 'User.Id', { nullable: true })
@JoinColumn({ name: 'UpdatedById' })
@Index()
- UpdatedById: Relation;
+ UpdatedBy: Relation;
@Column({ type: 'timestamp', nullable: true })
UpdatedOn: Date;
diff --git a/express-api/src/typeorm/Entities/abstractEntities/Evaluation.ts b/express-api/src/typeorm/Entities/abstractEntities/Evaluation.ts
index 5f877d3b2..c67048136 100644
--- a/express-api/src/typeorm/Entities/abstractEntities/Evaluation.ts
+++ b/express-api/src/typeorm/Entities/abstractEntities/Evaluation.ts
@@ -1,4 +1,4 @@
-import { EvaluationKeys } from '@/typeorm/Entities/EvaluationKeys';
+import { EvaluationKey } from '@/typeorm/Entities/EvaluationKey';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Column, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
@@ -6,10 +6,12 @@ export abstract class Evaluation extends BaseEntity {
@PrimaryColumn('timestamp')
Date: Date;
- @PrimaryColumn()
- @ManyToOne(() => EvaluationKeys, (EvaluationKey) => EvaluationKey.Id)
- @JoinColumn({ name: 'EvaluationKey' })
- EvaluationKey: EvaluationKeys;
+ @PrimaryColumn({ name: 'EvaluationKeyId', type: 'int' })
+ EvaluationKeyId: number;
+
+ @ManyToOne(() => EvaluationKey, (EvaluationKey) => EvaluationKey.Id)
+ @JoinColumn({ name: 'EvaluationKeyId' })
+ EvaluationKey: EvaluationKey;
@Column({ type: 'money' })
Value: number;
diff --git a/express-api/src/typeorm/Entities/abstractEntities/Fiscal.ts b/express-api/src/typeorm/Entities/abstractEntities/Fiscal.ts
index f8ef47c71..4c4dd33cb 100644
--- a/express-api/src/typeorm/Entities/abstractEntities/Fiscal.ts
+++ b/express-api/src/typeorm/Entities/abstractEntities/Fiscal.ts
@@ -1,17 +1,19 @@
-import { FiscalKeys } from '@/typeorm/Entities/FiscalKeys';
+import { FiscalKey } from '@/typeorm/Entities/FiscalKey';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Entity, Column, Index, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
@Entity()
-@Index(['FiscalYear', 'FiscalKey', 'Value'])
+@Index(['FiscalYear', 'FiscalKeyId', 'Value'])
export class Fiscal extends BaseEntity {
@PrimaryColumn('int')
FiscalYear: number;
- @PrimaryColumn()
- @ManyToOne(() => FiscalKeys, (FiscalKey) => FiscalKey.Id)
- @JoinColumn({ name: 'FiscalKey' })
- FiscalKey: FiscalKeys;
+ @PrimaryColumn({ name: 'FiscalKeyId', type: 'int' })
+ FiscalKeyId: number;
+
+ @ManyToOne(() => FiscalKey, (FiscalKey) => FiscalKey.Id)
+ @JoinColumn({ name: 'FiscalKeyId' })
+ FiscalKey: FiscalKey;
@Column('money')
Value: number;
diff --git a/express-api/src/typeorm/Entities/abstractEntities/Property.ts b/express-api/src/typeorm/Entities/abstractEntities/Property.ts
index 709a8829e..5fadd2eff 100644
--- a/express-api/src/typeorm/Entities/abstractEntities/Property.ts
+++ b/express-api/src/typeorm/Entities/abstractEntities/Property.ts
@@ -1,7 +1,7 @@
-import { AdministrativeAreas } from '@/typeorm/Entities/AdministrativeAreas';
-import { Agencies } from '@/typeorm/Entities/Agencies';
-import { PropertyClassifications } from '@/typeorm/Entities/PropertyClassifications';
-import { PropertyTypes } from '@/typeorm/Entities/PropertyTypes';
+import { AdministrativeArea } from '@/typeorm/Entities/AdministrativeArea';
+import { Agency } from '@/typeorm/Entities/Agency';
+import { PropertyClassification } from '@/typeorm/Entities/PropertyClassification';
+import { PropertyType } from '@/typeorm/Entities/PropertyType';
import { BaseEntity } from '@/typeorm/Entities/abstractEntities/BaseEntity';
import { Column, ManyToOne, JoinColumn, Index, PrimaryGeneratedColumn, Point } from 'typeorm';
@@ -15,25 +15,37 @@ export abstract class Property extends BaseEntity {
@Column({ type: 'character varying', length: 2000, nullable: true })
Description: string;
- @ManyToOne(() => PropertyClassifications, (Classification) => Classification.Id)
+ // Classification Relations
+ @Column({ name: 'ClassificationId', type: 'int' })
+ ClassificationId: number;
+
+ @ManyToOne(() => PropertyClassification, (Classification) => Classification.Id)
@JoinColumn({ name: 'ClassificationId' })
@Index()
- ClassificationId: PropertyClassifications;
+ Classification: PropertyClassification;
+
+ // Agency Relations
+ @Column({ name: 'AgencyId', type: 'int', nullable: true })
+ AgencyId: number;
- @ManyToOne(() => Agencies, (Agency) => Agency.Id, { nullable: true })
+ @ManyToOne(() => Agency, (Agency) => Agency.Id, { nullable: true })
@JoinColumn({ name: 'AgencyId' })
@Index()
- AgencyId: Agencies;
+ Agency: Agency;
- @ManyToOne(() => AdministrativeAreas, (AdminArea) => AdminArea.Id)
+ // Administrative Area Relations
+ @Column({ name: 'AdministrativeAreaId', type: 'int' })
+ AdministrativeAreaId: number;
+
+ @ManyToOne(() => AdministrativeArea, (AdminArea) => AdminArea.Id)
@JoinColumn({ name: 'AdministrativeAreaId' })
@Index()
- AdministrativeAreaId: AdministrativeAreas;
+ AdministrativeArea: AdministrativeArea;
- @Column({ type: 'bit' })
+ @Column({ type: 'boolean' })
IsSensitive: boolean;
- @Column({ type: 'bit' })
+ @Column({ type: 'boolean' })
IsVisibleToOtherAgencies: boolean;
@Column({ type: 'point' }) // We need PostGIS before we can use type geography, using point for now, but maybe this is ok?
@@ -42,10 +54,14 @@ export abstract class Property extends BaseEntity {
@Column({ type: 'character varying', length: 2000, nullable: true })
ProjectNumbers: string;
- @ManyToOne(() => PropertyTypes, (PropertyType) => PropertyType.Id)
+ // Property Type Relations
+ @Column({ name: 'PropertyTypeId', type: 'int' })
+ PropertyTypeId: number;
+
+ @ManyToOne(() => PropertyType, (PropertyType) => PropertyType.Id)
@JoinColumn({ name: 'PropertyTypeId' })
@Index()
- PropertyTypeId: PropertyTypes;
+ PropertyType: PropertyType;
@Column({ type: 'character varying', length: 150, nullable: true })
@Index()
diff --git a/express-api/src/typeorm/Migrations/1707763864013-CreateTables.ts b/express-api/src/typeorm/Migrations/1707763864013-CreateTables.ts
new file mode 100644
index 000000000..08bdaefa5
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/1707763864013-CreateTables.ts
@@ -0,0 +1,1763 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class CreateTables1707763864013 implements MigrationInterface {
+ name = 'CreateTables1707763864013';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `CREATE TABLE "agency" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" integer NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "Code" character varying(6) NOT NULL, "Description" character varying(500), "ParentId" integer, "Email" character varying(150), "SendEmail" boolean NOT NULL, "AddressTo" character varying(100), "CCEmail" character varying(250), CONSTRAINT "PK_b782cb3c20bab9b6eff7a661ad6" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_57fd74ea1b8a1b0d85e1c7ad20" ON "agency" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_4087b9d814a4a90af734b6dd3a" ON "agency" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_89544ece79ad52653c8fce606b" ON "agency" ("ParentId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_5e16d9d279223ff5fe988f6a3b" ON "agency" ("ParentId", "IsDisabled", "Id", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "role" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" uuid NOT NULL, "Name" character varying(100) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "KeycloakGroupId" uuid, "Description" text, "IsPublic" boolean NOT NULL, CONSTRAINT "PK_ab3dbbb04afe867d22e43aacad5" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_051c6dcae604d9286f20cc2d76" ON "role" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_0a24ac662369d88db55649f3e5" ON "role" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_65aaedd70b9d60594dddcc36b2" ON "role" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_882f8f0de8c784abe421f17cd4" ON "role" ("IsDisabled", "Name") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "user" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" uuid NOT NULL, "Username" character varying(25) NOT NULL, "DisplayName" character varying(100) NOT NULL, "FirstName" character varying(100) NOT NULL, "MiddleName" character varying(100), "LastName" character varying(100) NOT NULL, "Email" character varying(100) NOT NULL, "Position" character varying(100), "IsDisabled" boolean NOT NULL, "EmailVerified" boolean NOT NULL, "IsSystem" boolean NOT NULL, "Note" character varying(1000), "LastLogin" TIMESTAMP, "ApprovedById" uuid, "ApprovedOn" TIMESTAMP, "KeycloakUserId" uuid, "AgencyId" integer, "RoleId" uuid, CONSTRAINT "PK_1e4be10b13490bd87f4cc30c142" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_061257d343976f0dd80167c79e" ON "user" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_cdcb63fdec2cdf48ea4589557d" ON "user" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_b000857089edf6cae23b9bc9b8" ON "user" ("Username") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_b7eee57d84fb7ed872e660197f" ON "user" ("Email") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_c01351e0689032ad8995861393" ON "user" ("KeycloakUserId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "access_request" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "UserId" uuid NOT NULL, "Note" character varying(1000), "Status" integer NOT NULL, "RoleId" uuid NOT NULL, "AgencyId" integer NOT NULL, CONSTRAINT "PK_e655a6c3c0132a8b756aaa44c2a" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_c3e2e1bb170870974db5ce848f" ON "access_request" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_0f567e907073e1bf5af072410c" ON "access_request" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_352664edaa51c51a3f62d1cd8a" ON "access_request" ("UserId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_25dccf2207a003f5ecce8a33c7" ON "access_request" ("Status") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "building_construction_type" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, CONSTRAINT "PK_dc31406e12839c288095a312f15" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_3bf46797647574d5aeb8122a5a" ON "building_construction_type" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6744ccd2014fd4e75709410415" ON "building_construction_type" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_1fe605fd95d54c7a3d8fb8ea63" ON "building_construction_type" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_7eb84eaff27e41814425e258bf" ON "building_construction_type" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "building_occupant_type" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, CONSTRAINT "PK_c75762463433058aa9795469f08" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_09f8f34216ccfffd99add29359" ON "building_occupant_type" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_c40ce91f4fff8b30bc116e66d5" ON "building_occupant_type" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_4068747f6c9171d4106950eed9" ON "building_occupant_type" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d4305f752c6248063c083a27f4" ON "building_occupant_type" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "building_predominate_use" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, CONSTRAINT "PK_c144048a0ef4e9d0c6174d12593" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_a6adc6a5b01c4024621c52b1a2" ON "building_predominate_use" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_088d32f15fd51a4eee07cdde28" ON "building_predominate_use" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_8d85670f0a3bb38094f1db9023" ON "building_predominate_use" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_ae7808ddc529eb5f08dac71874" ON "building_predominate_use" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "regional_district" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" integer NOT NULL, "Abbreviation" character varying(5) NOT NULL, "Name" character varying(250) NOT NULL, CONSTRAINT "PK_ed5100402d64f5a45c30e724fe4" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_336d5c7c816dc621dbc820cc2a" ON "regional_district" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d8f11c563f58d60c57c96b7b83" ON "regional_district" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_ed5100402d64f5a45c30e724fe" ON "regional_district" ("Id") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_388afb0f03dd0bc943949c24e8" ON "regional_district" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "province" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" character varying(2) NOT NULL, "Name" character varying(100) NOT NULL, CONSTRAINT "PK_b3ca2985c24eae4071940805437" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_ff339e05eab922546e009f6cc7" ON "province" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_9d396303fe83e044d69531b128" ON "province" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_b54101e216cdd5a65212744139" ON "province" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "administrative_area" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "RegionalDistrictId" integer NOT NULL, "ProvinceId" character varying(2) NOT NULL, CONSTRAINT "Unique_Name_RegionalDistrict" UNIQUE ("Name", "RegionalDistrictId"), CONSTRAINT "PK_bf3794e302bec7911342a790f50" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8d89023b6b9a759bee9ba30d08" ON "administrative_area" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_68d342e36be4fc4f78d1d943c1" ON "administrative_area" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_a5b6c7d4abbf4a76127c1b3494" ON "administrative_area" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_09ecbd0495a18dd9c5648e6ede" ON "administrative_area" ("RegionalDistrictId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_af5f23834c3bd813da5cd195da" ON "administrative_area" ("ProvinceId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_a3c788ce5a2d7ee0708b63755c" ON "administrative_area" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "property_classification" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "IsVisible" boolean NOT NULL, CONSTRAINT "PK_d069abd1b928cc373f16db18084" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_0995f092c645d5412712204fa0" ON "property_classification" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_806993c03d99558f423ec01bbb" ON "property_classification" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_1a24aef900f50e1c3abfe53164" ON "property_classification" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_fe1614080ee22db893bdfbdeba" ON "property_classification" ("IsDisabled", "Name") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "property_type" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, CONSTRAINT "PK_42738d19919401b9fe08ccdd4e2" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_719b4bbf3b93a649f5ef90d968" ON "property_type" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6d34de8157955f540f198dbc61" ON "property_type" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_cd7711c358add04632676cd4cf" ON "property_type" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d1d9ae93b704c63e33ed6ebbfc" ON "property_type" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "building" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(250), "Description" character varying(2000), "ClassificationId" integer NOT NULL, "AgencyId" integer, "AdministrativeAreaId" integer NOT NULL, "IsSensitive" boolean NOT NULL, "IsVisibleToOtherAgencies" boolean NOT NULL, "Location" point NOT NULL, "ProjectNumbers" character varying(2000), "PropertyTypeId" integer NOT NULL, "Address1" character varying(150), "Address2" character varying(150), "Postal" character varying(6), "SiteId" character varying, "BuildingConstructionTypeId" integer NOT NULL, "BuildingFloorCount" integer NOT NULL, "BuildingPredominateUseId" integer NOT NULL, "BuildingTenancy" character varying(450) NOT NULL, "RentableArea" real NOT NULL, "BuildingOccupantTypeId" integer NOT NULL, "LeaseExpiry" TIMESTAMP, "OccupantName" character varying(100), "TransferLeaseOnSale" boolean NOT NULL, "BuildingTenancyUpdatedOn" TIMESTAMP, "EncumbranceReason" character varying(500), "LeasedLandMetadata" character varying(2000), "TotalArea" real NOT NULL, CONSTRAINT "PK_b2aab00b122c9bbf60ad12e1750" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_1db6127f236406fbd224404a56" ON "building" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_ecbf2cc6d83ab02e2495f354f7" ON "building" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6bec46322ae9b4fdc1712ab65e" ON "building" ("ClassificationId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_660f6a82c58715ccb3e939bec9" ON "building" ("AgencyId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_e1c368b69fe6d55fb899d4b09f" ON "building" ("AdministrativeAreaId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6774c5af0daf3a6f910a4aa042" ON "building" ("PropertyTypeId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_b3f209b9a4ee05a9eb9ec58fe5" ON "building" ("Address1") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8ae155b8e2213e901180f54ba4" ON "building" ("BuildingConstructionTypeId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_a5b3ee7013349be96c40cb9cfd" ON "building" ("BuildingPredominateUseId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_0dc5a69385e011b026cf9349a2" ON "building" ("BuildingOccupantTypeId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "evaluation_key" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "Description" text, CONSTRAINT "PK_704bb2dafbc4b3136fecad4b083" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_f96c0877bd6385eac3ba1f2c78" ON "evaluation_key" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_839b7cb82690afe5a15c035b08" ON "evaluation_key" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_bc8c2c506f8cc061b0e3380eb1" ON "evaluation_key" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "building_evaluation" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Date" TIMESTAMP NOT NULL, "EvaluationKeyId" integer NOT NULL, "Value" money NOT NULL, "Note" character varying(500), "BuildingId" integer NOT NULL, CONSTRAINT "PK_861291d0fcbce13f6d2490bef2e" PRIMARY KEY ("Date", "EvaluationKeyId", "BuildingId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_56aeba3df852659e336883e93d" ON "building_evaluation" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6da6cc15cd660f17f8d5f4b872" ON "building_evaluation" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_9c6b73e6e1191c475083a1d5fd" ON "building_evaluation" ("BuildingId", "EvaluationKeyId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "fiscal_key" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "Description" text, CONSTRAINT "PK_191f9983601710a803b6f23df4b" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_448cedce8da97154c0238de097" ON "fiscal_key" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_98a4cb83be360a5a1a284d58e4" ON "fiscal_key" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_fe77a224958124b56f0258febb" ON "fiscal_key" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "building_fiscal" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "FiscalYear" integer NOT NULL, "FiscalKeyId" integer NOT NULL, "Value" money NOT NULL, "Note" text, "EffectiveDate" TIMESTAMP, "BuildingId" integer NOT NULL, CONSTRAINT "PK_a7e294fbf4517f9de59ec34ece1" PRIMARY KEY ("FiscalYear", "FiscalKeyId", "BuildingId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_caf56f353cb0d8ae6bd750a0c4" ON "building_fiscal" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_e4449ca75afd996e3044832bb0" ON "building_fiscal" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_1c1a58bed256b7b2a5f15509a3" ON "building_fiscal" ("FiscalYear", "FiscalKeyId", "Value") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_fab667986554eb11aac2b87caa" ON "building_fiscal" ("BuildingId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_status" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" integer NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "Code" character varying(10) NOT NULL, "GroupName" character varying(150), "Description" text, "IsMilestone" boolean NOT NULL, "IsTerminal" boolean NOT NULL, "Route" character varying(150) NOT NULL, CONSTRAINT "PK_9c7c93e46ce16c137c062b26e85" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_dfa98ba50e790bc7b17248fa20" ON "project_status" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_5ece525a0b4612005a76d0110e" ON "project_status" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_d42b1950f600f2ba10a1073c37" ON "project_status" ("Code") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_88b12d599bddadf5cf7ed7ca6c" ON "project_status" ("IsDisabled", "Name", "Code", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "workflow" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" integer NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "Code" character varying(20) NOT NULL, "Description" text, CONSTRAINT "PK_7f2e96be61bf3a880ce026ba746" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_120d2981d695242d3126b2ecf3" ON "workflow" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_7cbf3bb5d2b807ac35197acc7e" ON "workflow" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_fdbbf5ddd085c931b2b9a597c8" ON "workflow" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_1ee68d46c647bbd30af0655406" ON "workflow" ("Code") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_68176849c7a540b890086c2482" ON "workflow" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "tier_level" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "Description" text, CONSTRAINT "PK_6ffd34233549c06caf7f2bc3b97" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6a3f38c658f937940d6e744e2b" ON "tier_level" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d81d7fa7dd028492b040b99f8a" ON "tier_level" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_55ff7ee85ea69072a47da1d158" ON "tier_level" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_485d32d02c154922086bf9f604" ON "tier_level" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_risk" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "Code" character varying(10) NOT NULL, "Description" text, CONSTRAINT "PK_2580496ee836d459722c75722ff" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_826e688fee211771c9b0f5e3a2" ON "project_risk" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d2429392781e2bbd3e707d5cbb" ON "project_risk" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_8e81515c1554b7fe9540a80e9d" ON "project_risk" ("Code") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_0bf9c3491d0f50a52d4874a5b4" ON "project_risk" ("IsDisabled", "Code", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "ProjectNumber" character varying(25) NOT NULL, "Name" character varying(100) NOT NULL, "Manager" character varying(150), "ReportedFiscalYear" integer NOT NULL, "ActualFiscalYear" integer NOT NULL, "Description" text, "Metadata" text, "SubmittedOn" TIMESTAMP, "ApprovedOn" TIMESTAMP, "DeniedOn" TIMESTAMP, "CancelledOn" TIMESTAMP, "CompletedOn" TIMESTAMP, "NetBook" money, "Market" money, "Assessed" money, "Appraised" money, "ProjectType" integer NOT NULL, "WorkflowId" integer NOT NULL, "AgencyId" integer NOT NULL, "TierLevelId" integer NOT NULL, "StatusId" integer NOT NULL, "RiskId" integer NOT NULL, CONSTRAINT "PK_7f2c2f1af4879a4af2dabc43b59" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_92edd522e47ae4b0c32fd5ec79" ON "project" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_eebf4f4b257d3c54b29cc8612a" ON "project" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_af9dfc9e21f85cf3d6e7b3f364" ON "project" ("ProjectNumber") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_36d8a4897455038c86fe864b56" ON "project" ("WorkflowId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_7687de6e695cb7f4bd9c2c9f75" ON "project" ("AgencyId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_c8a2d7000e930610fcff4518b5" ON "project" ("TierLevelId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8e5ce48d7c645fe89c026dc183" ON "project" ("StatusId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8d8b0999e9c0eca839ebc9f044" ON "project" ("RiskId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_92ae926b277011146b34e7c63d" ON "project" ("StatusId", "TierLevelId", "AgencyId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_62bf88fe0fbad2490568a3464e" ON "project" ("Assessed", "NetBook", "Market", "ReportedFiscalYear", "ActualFiscalYear") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "notification_template" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(100) NOT NULL, "Description" text, "To" character varying(500), "Cc" character varying(500), "Bcc" character varying(500), "Audience" character varying(50) NOT NULL, "Encoding" character varying(50) NOT NULL, "BodyType" character varying(50) NOT NULL, "Priority" character varying(50) NOT NULL, "Subject" character varying(200) NOT NULL, "Body" text, "IsDisabled" boolean NOT NULL, "Tag" character varying(50), CONSTRAINT "PK_2f34a8ce654a891a88b508ef329" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_c0604eb0c437f557e473e80f83" ON "notification_template" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_1dd63279a1fc0216a61f2d4cc5" ON "notification_template" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_6b2b9d8f3db40276ccf3751645" ON "notification_template" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_3393387f688ba9899d39f93f5f" ON "notification_template" ("IsDisabled", "Tag") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "notification_queue" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Key" uuid NOT NULL, "Status" integer NOT NULL, "Priority" character varying(50) NOT NULL, "Encoding" character varying(50) NOT NULL, "SendOn" TIMESTAMP NOT NULL, "To" character varying(500), "Subject" character varying(200) NOT NULL, "BodyType" character varying(50) NOT NULL, "Body" text NOT NULL, "Bcc" character varying(500), "Cc" character varying(500), "Tag" character varying(50), "ProjectId" integer NOT NULL, "ToAgencyId" integer NOT NULL, "TemplateId" integer NOT NULL, "ChesMessageId" uuid NOT NULL, "ChesTransactionId" uuid NOT NULL, CONSTRAINT "PK_0fbce80fc625ae6af8f2c05f18a" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_ada44a71f0c630ba3a6360dfd7" ON "notification_queue" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_68870a847392c566965ee40b00" ON "notification_queue" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_b1a1fb67dbbe84787e9836745d" ON "notification_queue" ("Key") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_705c9558182adb03383285e867" ON "notification_queue" ("ToAgencyId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_1f4fd4ee805c533caaf891556c" ON "notification_queue" ("TemplateId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_359a7aedcd7b3d97fda4bfbc83" ON "notification_queue" ("ProjectId", "TemplateId", "ToAgencyId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d201b60436282e54dc2f4a5900" ON "notification_queue" ("Status", "SendOn", "Subject") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "parcel" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(250), "Description" character varying(2000), "ClassificationId" integer NOT NULL, "AgencyId" integer, "AdministrativeAreaId" integer NOT NULL, "IsSensitive" boolean NOT NULL, "IsVisibleToOtherAgencies" boolean NOT NULL, "Location" point NOT NULL, "ProjectNumbers" character varying(2000), "PropertyTypeId" integer NOT NULL, "Address1" character varying(150), "Address2" character varying(150), "Postal" character varying(6), "SiteId" character varying, "PID" integer NOT NULL, "PIN" integer, "LandArea" real, "LandLegalDescription" character varying(500), "Zoning" character varying(50), "ZoningPotential" character varying(50), "NotOwned" boolean NOT NULL, "ParentParcelId" integer, CONSTRAINT "PK_a1c1300daf57406ab4f1bf44485" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_a287500cc1c1e800e47308bfbb" ON "parcel" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8d0744d5e33373639336bb921d" ON "parcel" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8c7de899d6823ce867d84e9a5a" ON "parcel" ("ClassificationId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_24abde90040d45d682843e5c6f" ON "parcel" ("AgencyId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_1afa8aa2564a147b63a239ba7b" ON "parcel" ("AdministrativeAreaId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_ff2eb544d938698a090740bf4f" ON "parcel" ("PropertyTypeId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_37cd8e04f7b073c98d969df0dc" ON "parcel" ("Address1") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_f9c9b5645952970fe04184d7f3" ON "parcel" ("PID", "PIN") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "parcel_building" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "ParcelId" integer NOT NULL, "BuildingId" integer NOT NULL, CONSTRAINT "PK_92ef502c853d0bcbc5aee50b5dd" PRIMARY KEY ("ParcelId", "BuildingId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_7c24503d777690b738ce323bce" ON "parcel_building" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_774b5c510271eee240e596d980" ON "parcel_building" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "parcel_fiscal" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "FiscalYear" integer NOT NULL, "FiscalKeyId" integer NOT NULL, "Value" money NOT NULL, "Note" text, "EffectiveDate" TIMESTAMP, "ParcelId" integer NOT NULL, CONSTRAINT "PK_5bf971f7c71f9d25832fd5bd2be" PRIMARY KEY ("FiscalYear", "FiscalKeyId", "ParcelId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_87fc04dbccafd0b564a2b214fa" ON "parcel_fiscal" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_137c389c35d117bd3b57bb3758" ON "parcel_fiscal" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_18f77928db510eb6963cb69976" ON "parcel_fiscal" ("FiscalYear", "FiscalKeyId", "Value") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_02f45f04c7279bb629c1c7eb57" ON "parcel_fiscal" ("ParcelId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_note" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "ProjectId" integer NOT NULL, "NoteType" integer NOT NULL, "Note" text NOT NULL, CONSTRAINT "PK_a0bbe1971e80c671f06aa55d818" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_e4854a8bb51b552c0374014a15" ON "project_note" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_26e9b340be9a220d82ac75aba8" ON "project_note" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_590ffa664889a289918f618e0a" ON "project_note" ("ProjectId", "NoteType") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_number" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, CONSTRAINT "PK_50eaef0eef63fe2e5e899aad357" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_f480fcefbb14d128759fec0651" ON "project_number" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_16ea8abe722621a10ac15db084" ON "project_number" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "report_type" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(20) NOT NULL, "Description" text, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, CONSTRAINT "PK_2d1fa50743a4da4d2ece0802cbe" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_dc099fee4ada0ae3dc7fda3774" ON "report_type" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_afbac675a610832f932c2df659" ON "report_type" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_2236bb453b025c144a12dd42e2" ON "report_type" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_214853469dd5c1b77e0ab3b0b7" ON "report_type" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_report" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "IsFinal" boolean NOT NULL, "Name" character varying(250), "From" TIMESTAMP NOT NULL, "To" TIMESTAMP NOT NULL, "ReportTypeId" integer NOT NULL, CONSTRAINT "PK_4868796169d109a4adea7d8ecc6" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8a9eaf80d836df0b31de6a02eb" ON "project_report" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_cc8f6da014c702e1205ff5e7fc" ON "project_report" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_c32ecc1c2fd813140954bdfcc0" ON "project_report" ("Id", "To", "From", "IsFinal") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_property" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "ProjectId" integer NOT NULL, "PropertyTypeId" integer NOT NULL, "ParcelId" integer NOT NULL, "BuildingId" integer NOT NULL, CONSTRAINT "PK_1696fd8354a197fc5456a88e580" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_bd0411b25b82012ecee5bc250e" ON "project_property" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_db82efd607917822803ea51c31" ON "project_property" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_a8615c36638af5cbe405f58fca" ON "project_property" ("ProjectId", "PropertyTypeId", "ParcelId", "BuildingId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_status_history" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "ProjectId" integer NOT NULL, "WorkflowId" integer NOT NULL, "StatusId" integer NOT NULL, CONSTRAINT "PK_8f45e0fdfdc74760fb330d846f5" PRIMARY KEY ("Id", "ProjectId", "WorkflowId", "StatusId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_7b2b0eec46d15f7972b8adba09" ON "project_status_history" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_f3032f4f9c067dd90d51806594" ON "project_status_history" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_f759d96ffccada1ec97f9db817" ON "project_status_history" ("ProjectId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_b30c21a3bb1cadbf654d2e860a" ON "project_status_history" ("WorkflowId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_01f0894feba7f37f8185731d9f" ON "project_status_history" ("StatusId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_agency_response" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "ProjectId" integer NOT NULL, "AgencyId" integer NOT NULL, "OfferAmount" money NOT NULL, "NotificationId" integer NOT NULL, "Response" integer NOT NULL, "ReceivedOn" TIMESTAMP, "Note" character varying(2000), CONSTRAINT "PK_646db5471098979a3611b431222" PRIMARY KEY ("ProjectId", "AgencyId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_2676ffafe4d84043a69b290cfe" ON "project_agency_response" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d04beec26b2444d9cde212811c" ON "project_agency_response" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_14fa612dd1f3ac554faa62f7ff" ON "project_agency_response" ("NotificationId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_status_transition" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "FromWorkflowId" integer NOT NULL, "FromStatusId" integer NOT NULL, "ToWorkflowId" integer NOT NULL, "ToStatusId" integer NOT NULL, "Action" character varying(100) NOT NULL, "ValidateTasks" boolean NOT NULL, CONSTRAINT "PK_3c62ad86eeeaed7e0a172a48ab0" PRIMARY KEY ("FromWorkflowId", "FromStatusId", "ToWorkflowId", "ToStatusId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_1011dc0123d14c06a9bddcdc2e" ON "project_status_transition" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_206a2108332afbff74c914a07e" ON "project_status_transition" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_10d5e3b7c4f3412bf6ba82f1fb" ON "project_status_transition" ("FromWorkflowId", "FromStatusId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_b9dda79f47cc16dd6f95685a11" ON "project_status_transition" ("ToWorkflowId", "ToStatusId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_status_notification" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "TemplateId" integer NOT NULL, "FromStatusId" integer, "ToStatusId" integer NOT NULL, "Priority" integer NOT NULL, "Delay" integer NOT NULL, "DelayDays" integer NOT NULL, CONSTRAINT "PK_491d6520b436682e61f61e019aa" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_deaf61da2a9bfe0cc7fd3e1397" ON "project_status_notification" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_f23b704bea34701da9938654dd" ON "project_status_notification" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_c42bdbdc9aa6ff04dca365d437" ON "project_status_notification" ("TemplateId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d4c0c9f1bfe8cd1bb26d85f9b6" ON "project_status_notification" ("ToStatusId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_5eb37214b16fa6d7416ca18440" ON "project_status_notification" ("FromStatusId", "ToStatusId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "task" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(150) NOT NULL, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, "Description" text, "IsOptional" boolean NOT NULL, "StatusId" integer NOT NULL, CONSTRAINT "PK_50bde4df67295bf27cd0b7abe99" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_4ea2ad32ccbacc339a4fb1d4db" ON "task" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_78544bfd6310d7e2dc71d57b56" ON "task" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_8d226bbfd3fb32ef2f3ee6f130" ON "task" ("IsDisabled", "IsOptional", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_task" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "ProjectId" integer NOT NULL, "TaskId" integer NOT NULL, "IsCompleted" boolean NOT NULL, "CompletedOn" TIMESTAMP DEFAULT now(), CONSTRAINT "PK_4bb5ea7cc99bc65db2362bd47d0" PRIMARY KEY ("ProjectId", "TaskId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_3d19de58ad17f53726333c89fb" ON "project_task" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_de4567ac4ab366bb6635ea0d77" ON "project_task" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_32343aaa24f49813b1b8403da8" ON "project_task" ("TaskId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_6d31cdb891c6f707147fcec806" ON "project_task" ("ProjectId", "TaskId", "IsCompleted", "CompletedOn") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_snapshot" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "ProjectId" integer NOT NULL, "NetBook" money, "Market" money, "Assessed" money, "Appraised" money, "SnapshotOn" TIMESTAMP NOT NULL, "Metadata" text, CONSTRAINT "PK_caa45e3416441b86e0de14332fa" PRIMARY KEY ("Id", "ProjectId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_72344b23c60662302ef496fd01" ON "project_snapshot" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_7f49593821acb73e3f3f5c75f1" ON "project_snapshot" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d2b08c043730267d30bfe38fee" ON "project_snapshot" ("ProjectId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_30c723e97dd7fd9577c216e4a3" ON "project_snapshot" ("ProjectId", "SnapshotOn") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "parcel_evaluation" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Date" TIMESTAMP NOT NULL, "EvaluationKeyId" integer NOT NULL, "Value" money NOT NULL, "Note" character varying(500), "ParcelId" integer NOT NULL, "Firm" character varying(150), CONSTRAINT "PK_555a6a78040868faa2527607eff" PRIMARY KEY ("Date", "EvaluationKeyId", "ParcelId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_dd245945881a8d0428a37bed61" ON "parcel_evaluation" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_bb8dd863a2b8f363514051792c" ON "parcel_evaluation" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_116fc313f83129cb2266a36f41" ON "parcel_evaluation" ("ParcelId", "EvaluationKeyId") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "project_type" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "Id" SERIAL NOT NULL, "Name" character varying(20) NOT NULL, "Description" text, "IsDisabled" boolean NOT NULL, "SortOrder" integer NOT NULL, CONSTRAINT "PK_98761b1910c29a664aa37287268" PRIMARY KEY ("Id"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_f440e962cc2653a3cd830b60b4" ON "project_type" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_566c876e0679480e2a99fa83e4" ON "project_type" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE UNIQUE INDEX "IDX_9758554732342b0a696a971085" ON "project_type" ("Name") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_c5f2f52407347e0b7a54996821" ON "project_type" ("IsDisabled", "Name", "SortOrder") `,
+ );
+ await queryRunner.query(
+ `CREATE TABLE "workflow_project_status" ("CreatedById" uuid NOT NULL, "CreatedOn" TIMESTAMP NOT NULL DEFAULT now(), "UpdatedById" uuid, "UpdatedOn" TIMESTAMP, "WorkflowId" integer NOT NULL, "StatusId" integer NOT NULL, "SortOrder" integer NOT NULL, "IsOptional" boolean NOT NULL, CONSTRAINT "PK_a7b19095278ab398e21555f9b1e" PRIMARY KEY ("WorkflowId", "StatusId"))`,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_9a1574cf9168ed1f4a1c4eaa8d" ON "workflow_project_status" ("CreatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_d67fa6bf65e80a14dc7e0c6f8f" ON "workflow_project_status" ("UpdatedById") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_9fc2b51bf70dfc6969f9b04934" ON "workflow_project_status" ("WorkflowId") `,
+ );
+ await queryRunner.query(
+ `CREATE INDEX "IDX_2050f19a4c73eb00e7778612d3" ON "workflow_project_status" ("StatusId") `,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "agency" ADD CONSTRAINT "FK_57fd74ea1b8a1b0d85e1c7ad206" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "agency" ADD CONSTRAINT "FK_4087b9d814a4a90af734b6dd3a8" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "agency" ADD CONSTRAINT "FK_89544ece79ad52653c8fce606bd" FOREIGN KEY ("ParentId") REFERENCES "agency"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "role" ADD CONSTRAINT "FK_051c6dcae604d9286f20cc2d76d" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "role" ADD CONSTRAINT "FK_0a24ac662369d88db55649f3e5a" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user" ADD CONSTRAINT "FK_061257d343976f0dd80167c79ee" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user" ADD CONSTRAINT "FK_cdcb63fdec2cdf48ea4589557dc" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user" ADD CONSTRAINT "FK_5c8d47a21865d7a468c4f60ede5" FOREIGN KEY ("ApprovedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user" ADD CONSTRAINT "FK_b56b7c6b8530bf7369d9bdbd19b" FOREIGN KEY ("AgencyId") REFERENCES "agency"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "user" ADD CONSTRAINT "FK_27f6f4b16a19bf5384ac8a11ca1" FOREIGN KEY ("RoleId") REFERENCES "role"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" ADD CONSTRAINT "FK_c3e2e1bb170870974db5ce848f8" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" ADD CONSTRAINT "FK_0f567e907073e1bf5af072410c2" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" ADD CONSTRAINT "FK_352664edaa51c51a3f62d1cd8a6" FOREIGN KEY ("UserId") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" ADD CONSTRAINT "FK_952464b169add4b0f13a4401ef9" FOREIGN KEY ("RoleId") REFERENCES "role"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" ADD CONSTRAINT "FK_f8eab68887703533b9ec5656b64" FOREIGN KEY ("AgencyId") REFERENCES "agency"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_construction_type" ADD CONSTRAINT "FK_3bf46797647574d5aeb8122a5a5" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_construction_type" ADD CONSTRAINT "FK_6744ccd2014fd4e757094104150" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_occupant_type" ADD CONSTRAINT "FK_09f8f34216ccfffd99add293598" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_occupant_type" ADD CONSTRAINT "FK_c40ce91f4fff8b30bc116e66d54" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_predominate_use" ADD CONSTRAINT "FK_a6adc6a5b01c4024621c52b1a27" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_predominate_use" ADD CONSTRAINT "FK_088d32f15fd51a4eee07cdde285" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "regional_district" ADD CONSTRAINT "FK_336d5c7c816dc621dbc820cc2a7" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "regional_district" ADD CONSTRAINT "FK_d8f11c563f58d60c57c96b7b830" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "province" ADD CONSTRAINT "FK_ff339e05eab922546e009f6cc7f" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "province" ADD CONSTRAINT "FK_9d396303fe83e044d69531b1283" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "administrative_area" ADD CONSTRAINT "FK_8d89023b6b9a759bee9ba30d085" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "administrative_area" ADD CONSTRAINT "FK_68d342e36be4fc4f78d1d943c1c" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "administrative_area" ADD CONSTRAINT "FK_09ecbd0495a18dd9c5648e6ede3" FOREIGN KEY ("RegionalDistrictId") REFERENCES "regional_district"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "administrative_area" ADD CONSTRAINT "FK_af5f23834c3bd813da5cd195da7" FOREIGN KEY ("ProvinceId") REFERENCES "province"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "property_classification" ADD CONSTRAINT "FK_0995f092c645d5412712204fa0c" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "property_classification" ADD CONSTRAINT "FK_806993c03d99558f423ec01bbb2" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "property_type" ADD CONSTRAINT "FK_719b4bbf3b93a649f5ef90d968b" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "property_type" ADD CONSTRAINT "FK_6d34de8157955f540f198dbc61d" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_1db6127f236406fbd224404a568" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_ecbf2cc6d83ab02e2495f354f75" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_6bec46322ae9b4fdc1712ab65e5" FOREIGN KEY ("ClassificationId") REFERENCES "property_classification"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_660f6a82c58715ccb3e939bec9d" FOREIGN KEY ("AgencyId") REFERENCES "agency"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_e1c368b69fe6d55fb899d4b09f3" FOREIGN KEY ("AdministrativeAreaId") REFERENCES "administrative_area"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_6774c5af0daf3a6f910a4aa042c" FOREIGN KEY ("PropertyTypeId") REFERENCES "property_type"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_8ae155b8e2213e901180f54ba4e" FOREIGN KEY ("BuildingConstructionTypeId") REFERENCES "building_construction_type"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_a5b3ee7013349be96c40cb9cfd2" FOREIGN KEY ("BuildingPredominateUseId") REFERENCES "building_predominate_use"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" ADD CONSTRAINT "FK_0dc5a69385e011b026cf9349a2d" FOREIGN KEY ("BuildingOccupantTypeId") REFERENCES "building_occupant_type"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "evaluation_key" ADD CONSTRAINT "FK_f96c0877bd6385eac3ba1f2c78d" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "evaluation_key" ADD CONSTRAINT "FK_839b7cb82690afe5a15c035b089" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_evaluation" ADD CONSTRAINT "FK_56aeba3df852659e336883e93d0" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_evaluation" ADD CONSTRAINT "FK_6da6cc15cd660f17f8d5f4b8727" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_evaluation" ADD CONSTRAINT "FK_eeecf4e694ba6dff606c907b2a4" FOREIGN KEY ("EvaluationKeyId") REFERENCES "evaluation_key"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_evaluation" ADD CONSTRAINT "FK_3e9ac5c77dbf16ff2d5a3e028c4" FOREIGN KEY ("BuildingId") REFERENCES "building"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "fiscal_key" ADD CONSTRAINT "FK_448cedce8da97154c0238de097d" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "fiscal_key" ADD CONSTRAINT "FK_98a4cb83be360a5a1a284d58e4a" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_fiscal" ADD CONSTRAINT "FK_caf56f353cb0d8ae6bd750a0c4f" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_fiscal" ADD CONSTRAINT "FK_e4449ca75afd996e3044832bb02" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_fiscal" ADD CONSTRAINT "FK_c0a52b1dea77ff6d3c704fcc473" FOREIGN KEY ("FiscalKeyId") REFERENCES "fiscal_key"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_fiscal" ADD CONSTRAINT "FK_fab667986554eb11aac2b87caa4" FOREIGN KEY ("BuildingId") REFERENCES "building"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status" ADD CONSTRAINT "FK_dfa98ba50e790bc7b17248fa204" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status" ADD CONSTRAINT "FK_5ece525a0b4612005a76d0110e3" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow" ADD CONSTRAINT "FK_120d2981d695242d3126b2ecf31" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow" ADD CONSTRAINT "FK_7cbf3bb5d2b807ac35197acc7eb" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "tier_level" ADD CONSTRAINT "FK_6a3f38c658f937940d6e744e2bb" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "tier_level" ADD CONSTRAINT "FK_d81d7fa7dd028492b040b99f8aa" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_risk" ADD CONSTRAINT "FK_826e688fee211771c9b0f5e3a2c" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_risk" ADD CONSTRAINT "FK_d2429392781e2bbd3e707d5cbb2" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" ADD CONSTRAINT "FK_92edd522e47ae4b0c32fd5ec795" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" ADD CONSTRAINT "FK_eebf4f4b257d3c54b29cc8612a5" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" ADD CONSTRAINT "FK_36d8a4897455038c86fe864b563" FOREIGN KEY ("WorkflowId") REFERENCES "workflow"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" ADD CONSTRAINT "FK_7687de6e695cb7f4bd9c2c9f75d" FOREIGN KEY ("AgencyId") REFERENCES "agency"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" ADD CONSTRAINT "FK_c8a2d7000e930610fcff4518b5d" FOREIGN KEY ("TierLevelId") REFERENCES "tier_level"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" ADD CONSTRAINT "FK_8e5ce48d7c645fe89c026dc1835" FOREIGN KEY ("StatusId") REFERENCES "project_status"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" ADD CONSTRAINT "FK_8d8b0999e9c0eca839ebc9f044c" FOREIGN KEY ("RiskId") REFERENCES "project_risk"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_template" ADD CONSTRAINT "FK_c0604eb0c437f557e473e80f830" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_template" ADD CONSTRAINT "FK_1dd63279a1fc0216a61f2d4cc5f" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" ADD CONSTRAINT "FK_ada44a71f0c630ba3a6360dfd79" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" ADD CONSTRAINT "FK_68870a847392c566965ee40b008" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" ADD CONSTRAINT "FK_5d7fe381cc5ac14b88f911c81c7" FOREIGN KEY ("ProjectId") REFERENCES "project"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" ADD CONSTRAINT "FK_705c9558182adb03383285e8674" FOREIGN KEY ("ToAgencyId") REFERENCES "agency"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" ADD CONSTRAINT "FK_1f4fd4ee805c533caaf891556c9" FOREIGN KEY ("TemplateId") REFERENCES "notification_template"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" ADD CONSTRAINT "FK_a287500cc1c1e800e47308bfbb0" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" ADD CONSTRAINT "FK_8d0744d5e33373639336bb921d0" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" ADD CONSTRAINT "FK_8c7de899d6823ce867d84e9a5ac" FOREIGN KEY ("ClassificationId") REFERENCES "property_classification"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" ADD CONSTRAINT "FK_24abde90040d45d682843e5c6ff" FOREIGN KEY ("AgencyId") REFERENCES "agency"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" ADD CONSTRAINT "FK_1afa8aa2564a147b63a239ba7b5" FOREIGN KEY ("AdministrativeAreaId") REFERENCES "administrative_area"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" ADD CONSTRAINT "FK_ff2eb544d938698a090740bf4fd" FOREIGN KEY ("PropertyTypeId") REFERENCES "property_type"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" ADD CONSTRAINT "FK_5b4217d1382b7617cd4e70ec6b4" FOREIGN KEY ("ParentParcelId") REFERENCES "parcel"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_building" ADD CONSTRAINT "FK_7c24503d777690b738ce323bce0" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_building" ADD CONSTRAINT "FK_774b5c510271eee240e596d9808" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_building" ADD CONSTRAINT "FK_e7be5fdaaf9a64fe00018fe5dd5" FOREIGN KEY ("ParcelId") REFERENCES "parcel"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_building" ADD CONSTRAINT "FK_e37f8c948a2f75bc7e2c5e510ec" FOREIGN KEY ("BuildingId") REFERENCES "building"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_fiscal" ADD CONSTRAINT "FK_87fc04dbccafd0b564a2b214fad" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_fiscal" ADD CONSTRAINT "FK_137c389c35d117bd3b57bb37585" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_fiscal" ADD CONSTRAINT "FK_a7b9e967f857dbc908deab2f5f1" FOREIGN KEY ("FiscalKeyId") REFERENCES "fiscal_key"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_fiscal" ADD CONSTRAINT "FK_02f45f04c7279bb629c1c7eb573" FOREIGN KEY ("ParcelId") REFERENCES "parcel"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_note" ADD CONSTRAINT "FK_e4854a8bb51b552c0374014a158" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_note" ADD CONSTRAINT "FK_26e9b340be9a220d82ac75aba89" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_note" ADD CONSTRAINT "FK_92d54e65849fc820dd9deb639cc" FOREIGN KEY ("ProjectId") REFERENCES "project"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_number" ADD CONSTRAINT "FK_f480fcefbb14d128759fec06512" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_number" ADD CONSTRAINT "FK_16ea8abe722621a10ac15db0846" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "report_type" ADD CONSTRAINT "FK_dc099fee4ada0ae3dc7fda3774e" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "report_type" ADD CONSTRAINT "FK_afbac675a610832f932c2df6597" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_report" ADD CONSTRAINT "FK_8a9eaf80d836df0b31de6a02eba" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_report" ADD CONSTRAINT "FK_cc8f6da014c702e1205ff5e7fce" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_report" ADD CONSTRAINT "FK_e74f1589521cb0f15f9c771dbc3" FOREIGN KEY ("ReportTypeId") REFERENCES "report_type"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" ADD CONSTRAINT "FK_bd0411b25b82012ecee5bc250e6" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" ADD CONSTRAINT "FK_db82efd607917822803ea51c31c" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" ADD CONSTRAINT "FK_c59f0cbb17fe28d04ad0c7774c5" FOREIGN KEY ("ProjectId") REFERENCES "project"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" ADD CONSTRAINT "FK_cf8d98786c1247322b0072817b9" FOREIGN KEY ("PropertyTypeId") REFERENCES "property_type"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" ADD CONSTRAINT "FK_a580df432f0e0c521225fc3327b" FOREIGN KEY ("ParcelId") REFERENCES "parcel"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" ADD CONSTRAINT "FK_de366a792fc0aef4f8718e42d9c" FOREIGN KEY ("BuildingId") REFERENCES "building"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" ADD CONSTRAINT "FK_7b2b0eec46d15f7972b8adba092" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" ADD CONSTRAINT "FK_f3032f4f9c067dd90d51806594a" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" ADD CONSTRAINT "FK_f759d96ffccada1ec97f9db817d" FOREIGN KEY ("ProjectId") REFERENCES "project"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" ADD CONSTRAINT "FK_b30c21a3bb1cadbf654d2e860a1" FOREIGN KEY ("WorkflowId") REFERENCES "workflow"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" ADD CONSTRAINT "FK_01f0894feba7f37f8185731d9fb" FOREIGN KEY ("StatusId") REFERENCES "project_status"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" ADD CONSTRAINT "FK_2676ffafe4d84043a69b290cfef" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" ADD CONSTRAINT "FK_d04beec26b2444d9cde212811c7" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" ADD CONSTRAINT "FK_d8543ae1a734af9d06bef9a42be" FOREIGN KEY ("ProjectId") REFERENCES "project"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" ADD CONSTRAINT "FK_a7d3d7245bbe7886ca54f791940" FOREIGN KEY ("AgencyId") REFERENCES "agency"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" ADD CONSTRAINT "FK_14fa612dd1f3ac554faa62f7ff9" FOREIGN KEY ("NotificationId") REFERENCES "notification_queue"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" ADD CONSTRAINT "FK_1011dc0123d14c06a9bddcdc2e9" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" ADD CONSTRAINT "FK_206a2108332afbff74c914a07ef" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" ADD CONSTRAINT "FK_debb730c841395857d2ce6fc4e8" FOREIGN KEY ("FromWorkflowId") REFERENCES "workflow"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" ADD CONSTRAINT "FK_c8badedb6cba0265b076a2eb0f8" FOREIGN KEY ("FromStatusId") REFERENCES "project_status"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" ADD CONSTRAINT "FK_72d2cc0ef534734b4822456e4ef" FOREIGN KEY ("ToWorkflowId") REFERENCES "workflow"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" ADD CONSTRAINT "FK_1fc9664e25d9592bc6f59604197" FOREIGN KEY ("ToStatusId") REFERENCES "project_status"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" ADD CONSTRAINT "FK_deaf61da2a9bfe0cc7fd3e13971" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" ADD CONSTRAINT "FK_f23b704bea34701da9938654dde" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" ADD CONSTRAINT "FK_c42bdbdc9aa6ff04dca365d437e" FOREIGN KEY ("TemplateId") REFERENCES "notification_template"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" ADD CONSTRAINT "FK_028f154ba333fae04bd0d1809e7" FOREIGN KEY ("FromStatusId") REFERENCES "project_status"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" ADD CONSTRAINT "FK_d4c0c9f1bfe8cd1bb26d85f9b6d" FOREIGN KEY ("ToStatusId") REFERENCES "project_status"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "task" ADD CONSTRAINT "FK_4ea2ad32ccbacc339a4fb1d4db8" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "task" ADD CONSTRAINT "FK_78544bfd6310d7e2dc71d57b565" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "task" ADD CONSTRAINT "FK_34feaa99b96dc1b9a12cd75b45e" FOREIGN KEY ("StatusId") REFERENCES "project_status"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_task" ADD CONSTRAINT "FK_3d19de58ad17f53726333c89fb1" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_task" ADD CONSTRAINT "FK_de4567ac4ab366bb6635ea0d77a" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_task" ADD CONSTRAINT "FK_a5819864ef8931ee93ae00cc620" FOREIGN KEY ("ProjectId") REFERENCES "project"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_task" ADD CONSTRAINT "FK_32343aaa24f49813b1b8403da87" FOREIGN KEY ("TaskId") REFERENCES "task"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_snapshot" ADD CONSTRAINT "FK_72344b23c60662302ef496fd013" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_snapshot" ADD CONSTRAINT "FK_7f49593821acb73e3f3f5c75f17" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_snapshot" ADD CONSTRAINT "FK_d2b08c043730267d30bfe38feeb" FOREIGN KEY ("ProjectId") REFERENCES "project"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_evaluation" ADD CONSTRAINT "FK_dd245945881a8d0428a37bed615" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_evaluation" ADD CONSTRAINT "FK_bb8dd863a2b8f363514051792c5" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_evaluation" ADD CONSTRAINT "FK_a95bea21af2bf47c50ef9e97306" FOREIGN KEY ("EvaluationKeyId") REFERENCES "evaluation_key"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_evaluation" ADD CONSTRAINT "FK_88484fcafeb6f3ece533c9bd959" FOREIGN KEY ("ParcelId") REFERENCES "parcel"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_type" ADD CONSTRAINT "FK_f440e962cc2653a3cd830b60b46" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_type" ADD CONSTRAINT "FK_566c876e0679480e2a99fa83e4a" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow_project_status" ADD CONSTRAINT "FK_9a1574cf9168ed1f4a1c4eaa8d7" FOREIGN KEY ("CreatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow_project_status" ADD CONSTRAINT "FK_d67fa6bf65e80a14dc7e0c6f8ff" FOREIGN KEY ("UpdatedById") REFERENCES "user"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow_project_status" ADD CONSTRAINT "FK_9fc2b51bf70dfc6969f9b049343" FOREIGN KEY ("WorkflowId") REFERENCES "workflow"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow_project_status" ADD CONSTRAINT "FK_2050f19a4c73eb00e7778612d37" FOREIGN KEY ("StatusId") REFERENCES "project_status"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "workflow_project_status" DROP CONSTRAINT "FK_2050f19a4c73eb00e7778612d37"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow_project_status" DROP CONSTRAINT "FK_9fc2b51bf70dfc6969f9b049343"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow_project_status" DROP CONSTRAINT "FK_d67fa6bf65e80a14dc7e0c6f8ff"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow_project_status" DROP CONSTRAINT "FK_9a1574cf9168ed1f4a1c4eaa8d7"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_type" DROP CONSTRAINT "FK_566c876e0679480e2a99fa83e4a"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_type" DROP CONSTRAINT "FK_f440e962cc2653a3cd830b60b46"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_evaluation" DROP CONSTRAINT "FK_88484fcafeb6f3ece533c9bd959"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_evaluation" DROP CONSTRAINT "FK_a95bea21af2bf47c50ef9e97306"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_evaluation" DROP CONSTRAINT "FK_bb8dd863a2b8f363514051792c5"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_evaluation" DROP CONSTRAINT "FK_dd245945881a8d0428a37bed615"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_snapshot" DROP CONSTRAINT "FK_d2b08c043730267d30bfe38feeb"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_snapshot" DROP CONSTRAINT "FK_7f49593821acb73e3f3f5c75f17"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_snapshot" DROP CONSTRAINT "FK_72344b23c60662302ef496fd013"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_task" DROP CONSTRAINT "FK_32343aaa24f49813b1b8403da87"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_task" DROP CONSTRAINT "FK_a5819864ef8931ee93ae00cc620"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_task" DROP CONSTRAINT "FK_de4567ac4ab366bb6635ea0d77a"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_task" DROP CONSTRAINT "FK_3d19de58ad17f53726333c89fb1"`,
+ );
+ await queryRunner.query(`ALTER TABLE "task" DROP CONSTRAINT "FK_34feaa99b96dc1b9a12cd75b45e"`);
+ await queryRunner.query(`ALTER TABLE "task" DROP CONSTRAINT "FK_78544bfd6310d7e2dc71d57b565"`);
+ await queryRunner.query(`ALTER TABLE "task" DROP CONSTRAINT "FK_4ea2ad32ccbacc339a4fb1d4db8"`);
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" DROP CONSTRAINT "FK_d4c0c9f1bfe8cd1bb26d85f9b6d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" DROP CONSTRAINT "FK_028f154ba333fae04bd0d1809e7"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" DROP CONSTRAINT "FK_c42bdbdc9aa6ff04dca365d437e"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" DROP CONSTRAINT "FK_f23b704bea34701da9938654dde"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_notification" DROP CONSTRAINT "FK_deaf61da2a9bfe0cc7fd3e13971"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" DROP CONSTRAINT "FK_1fc9664e25d9592bc6f59604197"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" DROP CONSTRAINT "FK_72d2cc0ef534734b4822456e4ef"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" DROP CONSTRAINT "FK_c8badedb6cba0265b076a2eb0f8"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" DROP CONSTRAINT "FK_debb730c841395857d2ce6fc4e8"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" DROP CONSTRAINT "FK_206a2108332afbff74c914a07ef"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_transition" DROP CONSTRAINT "FK_1011dc0123d14c06a9bddcdc2e9"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" DROP CONSTRAINT "FK_14fa612dd1f3ac554faa62f7ff9"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" DROP CONSTRAINT "FK_a7d3d7245bbe7886ca54f791940"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" DROP CONSTRAINT "FK_d8543ae1a734af9d06bef9a42be"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" DROP CONSTRAINT "FK_d04beec26b2444d9cde212811c7"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_agency_response" DROP CONSTRAINT "FK_2676ffafe4d84043a69b290cfef"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" DROP CONSTRAINT "FK_01f0894feba7f37f8185731d9fb"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" DROP CONSTRAINT "FK_b30c21a3bb1cadbf654d2e860a1"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" DROP CONSTRAINT "FK_f759d96ffccada1ec97f9db817d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" DROP CONSTRAINT "FK_f3032f4f9c067dd90d51806594a"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status_history" DROP CONSTRAINT "FK_7b2b0eec46d15f7972b8adba092"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" DROP CONSTRAINT "FK_de366a792fc0aef4f8718e42d9c"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" DROP CONSTRAINT "FK_a580df432f0e0c521225fc3327b"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" DROP CONSTRAINT "FK_cf8d98786c1247322b0072817b9"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" DROP CONSTRAINT "FK_c59f0cbb17fe28d04ad0c7774c5"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" DROP CONSTRAINT "FK_db82efd607917822803ea51c31c"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_property" DROP CONSTRAINT "FK_bd0411b25b82012ecee5bc250e6"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_report" DROP CONSTRAINT "FK_e74f1589521cb0f15f9c771dbc3"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_report" DROP CONSTRAINT "FK_cc8f6da014c702e1205ff5e7fce"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_report" DROP CONSTRAINT "FK_8a9eaf80d836df0b31de6a02eba"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "report_type" DROP CONSTRAINT "FK_afbac675a610832f932c2df6597"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "report_type" DROP CONSTRAINT "FK_dc099fee4ada0ae3dc7fda3774e"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_number" DROP CONSTRAINT "FK_16ea8abe722621a10ac15db0846"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_number" DROP CONSTRAINT "FK_f480fcefbb14d128759fec06512"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_note" DROP CONSTRAINT "FK_92d54e65849fc820dd9deb639cc"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_note" DROP CONSTRAINT "FK_26e9b340be9a220d82ac75aba89"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_note" DROP CONSTRAINT "FK_e4854a8bb51b552c0374014a158"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_fiscal" DROP CONSTRAINT "FK_02f45f04c7279bb629c1c7eb573"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_fiscal" DROP CONSTRAINT "FK_a7b9e967f857dbc908deab2f5f1"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_fiscal" DROP CONSTRAINT "FK_137c389c35d117bd3b57bb37585"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_fiscal" DROP CONSTRAINT "FK_87fc04dbccafd0b564a2b214fad"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_building" DROP CONSTRAINT "FK_e37f8c948a2f75bc7e2c5e510ec"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_building" DROP CONSTRAINT "FK_e7be5fdaaf9a64fe00018fe5dd5"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_building" DROP CONSTRAINT "FK_774b5c510271eee240e596d9808"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel_building" DROP CONSTRAINT "FK_7c24503d777690b738ce323bce0"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" DROP CONSTRAINT "FK_5b4217d1382b7617cd4e70ec6b4"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" DROP CONSTRAINT "FK_ff2eb544d938698a090740bf4fd"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" DROP CONSTRAINT "FK_1afa8aa2564a147b63a239ba7b5"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" DROP CONSTRAINT "FK_24abde90040d45d682843e5c6ff"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" DROP CONSTRAINT "FK_8c7de899d6823ce867d84e9a5ac"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" DROP CONSTRAINT "FK_8d0744d5e33373639336bb921d0"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "parcel" DROP CONSTRAINT "FK_a287500cc1c1e800e47308bfbb0"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" DROP CONSTRAINT "FK_1f4fd4ee805c533caaf891556c9"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" DROP CONSTRAINT "FK_705c9558182adb03383285e8674"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" DROP CONSTRAINT "FK_5d7fe381cc5ac14b88f911c81c7"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" DROP CONSTRAINT "FK_68870a847392c566965ee40b008"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_queue" DROP CONSTRAINT "FK_ada44a71f0c630ba3a6360dfd79"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_template" DROP CONSTRAINT "FK_1dd63279a1fc0216a61f2d4cc5f"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "notification_template" DROP CONSTRAINT "FK_c0604eb0c437f557e473e80f830"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" DROP CONSTRAINT "FK_8d8b0999e9c0eca839ebc9f044c"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" DROP CONSTRAINT "FK_8e5ce48d7c645fe89c026dc1835"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" DROP CONSTRAINT "FK_c8a2d7000e930610fcff4518b5d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" DROP CONSTRAINT "FK_7687de6e695cb7f4bd9c2c9f75d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" DROP CONSTRAINT "FK_36d8a4897455038c86fe864b563"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" DROP CONSTRAINT "FK_eebf4f4b257d3c54b29cc8612a5"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project" DROP CONSTRAINT "FK_92edd522e47ae4b0c32fd5ec795"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_risk" DROP CONSTRAINT "FK_d2429392781e2bbd3e707d5cbb2"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_risk" DROP CONSTRAINT "FK_826e688fee211771c9b0f5e3a2c"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "tier_level" DROP CONSTRAINT "FK_d81d7fa7dd028492b040b99f8aa"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "tier_level" DROP CONSTRAINT "FK_6a3f38c658f937940d6e744e2bb"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow" DROP CONSTRAINT "FK_7cbf3bb5d2b807ac35197acc7eb"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "workflow" DROP CONSTRAINT "FK_120d2981d695242d3126b2ecf31"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status" DROP CONSTRAINT "FK_5ece525a0b4612005a76d0110e3"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "project_status" DROP CONSTRAINT "FK_dfa98ba50e790bc7b17248fa204"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_fiscal" DROP CONSTRAINT "FK_fab667986554eb11aac2b87caa4"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_fiscal" DROP CONSTRAINT "FK_c0a52b1dea77ff6d3c704fcc473"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_fiscal" DROP CONSTRAINT "FK_e4449ca75afd996e3044832bb02"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_fiscal" DROP CONSTRAINT "FK_caf56f353cb0d8ae6bd750a0c4f"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "fiscal_key" DROP CONSTRAINT "FK_98a4cb83be360a5a1a284d58e4a"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "fiscal_key" DROP CONSTRAINT "FK_448cedce8da97154c0238de097d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_evaluation" DROP CONSTRAINT "FK_3e9ac5c77dbf16ff2d5a3e028c4"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_evaluation" DROP CONSTRAINT "FK_eeecf4e694ba6dff606c907b2a4"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_evaluation" DROP CONSTRAINT "FK_6da6cc15cd660f17f8d5f4b8727"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_evaluation" DROP CONSTRAINT "FK_56aeba3df852659e336883e93d0"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "evaluation_key" DROP CONSTRAINT "FK_839b7cb82690afe5a15c035b089"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "evaluation_key" DROP CONSTRAINT "FK_f96c0877bd6385eac3ba1f2c78d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_0dc5a69385e011b026cf9349a2d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_a5b3ee7013349be96c40cb9cfd2"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_8ae155b8e2213e901180f54ba4e"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_6774c5af0daf3a6f910a4aa042c"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_e1c368b69fe6d55fb899d4b09f3"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_660f6a82c58715ccb3e939bec9d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_6bec46322ae9b4fdc1712ab65e5"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_ecbf2cc6d83ab02e2495f354f75"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building" DROP CONSTRAINT "FK_1db6127f236406fbd224404a568"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "property_type" DROP CONSTRAINT "FK_6d34de8157955f540f198dbc61d"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "property_type" DROP CONSTRAINT "FK_719b4bbf3b93a649f5ef90d968b"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "property_classification" DROP CONSTRAINT "FK_806993c03d99558f423ec01bbb2"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "property_classification" DROP CONSTRAINT "FK_0995f092c645d5412712204fa0c"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "administrative_area" DROP CONSTRAINT "FK_af5f23834c3bd813da5cd195da7"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "administrative_area" DROP CONSTRAINT "FK_09ecbd0495a18dd9c5648e6ede3"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "administrative_area" DROP CONSTRAINT "FK_68d342e36be4fc4f78d1d943c1c"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "administrative_area" DROP CONSTRAINT "FK_8d89023b6b9a759bee9ba30d085"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "province" DROP CONSTRAINT "FK_9d396303fe83e044d69531b1283"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "province" DROP CONSTRAINT "FK_ff339e05eab922546e009f6cc7f"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "regional_district" DROP CONSTRAINT "FK_d8f11c563f58d60c57c96b7b830"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "regional_district" DROP CONSTRAINT "FK_336d5c7c816dc621dbc820cc2a7"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_predominate_use" DROP CONSTRAINT "FK_088d32f15fd51a4eee07cdde285"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_predominate_use" DROP CONSTRAINT "FK_a6adc6a5b01c4024621c52b1a27"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_occupant_type" DROP CONSTRAINT "FK_c40ce91f4fff8b30bc116e66d54"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_occupant_type" DROP CONSTRAINT "FK_09f8f34216ccfffd99add293598"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_construction_type" DROP CONSTRAINT "FK_6744ccd2014fd4e757094104150"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "building_construction_type" DROP CONSTRAINT "FK_3bf46797647574d5aeb8122a5a5"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" DROP CONSTRAINT "FK_f8eab68887703533b9ec5656b64"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" DROP CONSTRAINT "FK_952464b169add4b0f13a4401ef9"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" DROP CONSTRAINT "FK_352664edaa51c51a3f62d1cd8a6"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" DROP CONSTRAINT "FK_0f567e907073e1bf5af072410c2"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "access_request" DROP CONSTRAINT "FK_c3e2e1bb170870974db5ce848f8"`,
+ );
+ await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_27f6f4b16a19bf5384ac8a11ca1"`);
+ await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_b56b7c6b8530bf7369d9bdbd19b"`);
+ await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_5c8d47a21865d7a468c4f60ede5"`);
+ await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_cdcb63fdec2cdf48ea4589557dc"`);
+ await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_061257d343976f0dd80167c79ee"`);
+ await queryRunner.query(`ALTER TABLE "role" DROP CONSTRAINT "FK_0a24ac662369d88db55649f3e5a"`);
+ await queryRunner.query(`ALTER TABLE "role" DROP CONSTRAINT "FK_051c6dcae604d9286f20cc2d76d"`);
+ await queryRunner.query(
+ `ALTER TABLE "agency" DROP CONSTRAINT "FK_89544ece79ad52653c8fce606bd"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "agency" DROP CONSTRAINT "FK_4087b9d814a4a90af734b6dd3a8"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "agency" DROP CONSTRAINT "FK_57fd74ea1b8a1b0d85e1c7ad206"`,
+ );
+ await queryRunner.query(`DROP INDEX "public"."IDX_2050f19a4c73eb00e7778612d3"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_9fc2b51bf70dfc6969f9b04934"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d67fa6bf65e80a14dc7e0c6f8f"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_9a1574cf9168ed1f4a1c4eaa8d"`);
+ await queryRunner.query(`DROP TABLE "workflow_project_status"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c5f2f52407347e0b7a54996821"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_9758554732342b0a696a971085"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_566c876e0679480e2a99fa83e4"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_f440e962cc2653a3cd830b60b4"`);
+ await queryRunner.query(`DROP TABLE "project_type"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_116fc313f83129cb2266a36f41"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_bb8dd863a2b8f363514051792c"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_dd245945881a8d0428a37bed61"`);
+ await queryRunner.query(`DROP TABLE "parcel_evaluation"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_30c723e97dd7fd9577c216e4a3"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d2b08c043730267d30bfe38fee"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_7f49593821acb73e3f3f5c75f1"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_72344b23c60662302ef496fd01"`);
+ await queryRunner.query(`DROP TABLE "project_snapshot"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_6d31cdb891c6f707147fcec806"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_32343aaa24f49813b1b8403da8"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_de4567ac4ab366bb6635ea0d77"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_3d19de58ad17f53726333c89fb"`);
+ await queryRunner.query(`DROP TABLE "project_task"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8d226bbfd3fb32ef2f3ee6f130"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_78544bfd6310d7e2dc71d57b56"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_4ea2ad32ccbacc339a4fb1d4db"`);
+ await queryRunner.query(`DROP TABLE "task"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_5eb37214b16fa6d7416ca18440"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d4c0c9f1bfe8cd1bb26d85f9b6"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c42bdbdc9aa6ff04dca365d437"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_f23b704bea34701da9938654dd"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_deaf61da2a9bfe0cc7fd3e1397"`);
+ await queryRunner.query(`DROP TABLE "project_status_notification"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_b9dda79f47cc16dd6f95685a11"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_10d5e3b7c4f3412bf6ba82f1fb"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_206a2108332afbff74c914a07e"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1011dc0123d14c06a9bddcdc2e"`);
+ await queryRunner.query(`DROP TABLE "project_status_transition"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_14fa612dd1f3ac554faa62f7ff"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d04beec26b2444d9cde212811c"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_2676ffafe4d84043a69b290cfe"`);
+ await queryRunner.query(`DROP TABLE "project_agency_response"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_01f0894feba7f37f8185731d9f"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_b30c21a3bb1cadbf654d2e860a"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_f759d96ffccada1ec97f9db817"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_f3032f4f9c067dd90d51806594"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_7b2b0eec46d15f7972b8adba09"`);
+ await queryRunner.query(`DROP TABLE "project_status_history"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_a8615c36638af5cbe405f58fca"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_db82efd607917822803ea51c31"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_bd0411b25b82012ecee5bc250e"`);
+ await queryRunner.query(`DROP TABLE "project_property"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c32ecc1c2fd813140954bdfcc0"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_cc8f6da014c702e1205ff5e7fc"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8a9eaf80d836df0b31de6a02eb"`);
+ await queryRunner.query(`DROP TABLE "project_report"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_214853469dd5c1b77e0ab3b0b7"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_2236bb453b025c144a12dd42e2"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_afbac675a610832f932c2df659"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_dc099fee4ada0ae3dc7fda3774"`);
+ await queryRunner.query(`DROP TABLE "report_type"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_16ea8abe722621a10ac15db084"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_f480fcefbb14d128759fec0651"`);
+ await queryRunner.query(`DROP TABLE "project_number"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_590ffa664889a289918f618e0a"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_26e9b340be9a220d82ac75aba8"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_e4854a8bb51b552c0374014a15"`);
+ await queryRunner.query(`DROP TABLE "project_note"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_02f45f04c7279bb629c1c7eb57"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_18f77928db510eb6963cb69976"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_137c389c35d117bd3b57bb3758"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_87fc04dbccafd0b564a2b214fa"`);
+ await queryRunner.query(`DROP TABLE "parcel_fiscal"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_774b5c510271eee240e596d980"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_7c24503d777690b738ce323bce"`);
+ await queryRunner.query(`DROP TABLE "parcel_building"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_f9c9b5645952970fe04184d7f3"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_37cd8e04f7b073c98d969df0dc"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ff2eb544d938698a090740bf4f"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1afa8aa2564a147b63a239ba7b"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_24abde90040d45d682843e5c6f"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8c7de899d6823ce867d84e9a5a"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8d0744d5e33373639336bb921d"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_a287500cc1c1e800e47308bfbb"`);
+ await queryRunner.query(`DROP TABLE "parcel"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d201b60436282e54dc2f4a5900"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_359a7aedcd7b3d97fda4bfbc83"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1f4fd4ee805c533caaf891556c"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_705c9558182adb03383285e867"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_b1a1fb67dbbe84787e9836745d"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_68870a847392c566965ee40b00"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ada44a71f0c630ba3a6360dfd7"`);
+ await queryRunner.query(`DROP TABLE "notification_queue"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_3393387f688ba9899d39f93f5f"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_6b2b9d8f3db40276ccf3751645"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1dd63279a1fc0216a61f2d4cc5"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c0604eb0c437f557e473e80f83"`);
+ await queryRunner.query(`DROP TABLE "notification_template"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_62bf88fe0fbad2490568a3464e"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_92ae926b277011146b34e7c63d"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8d8b0999e9c0eca839ebc9f044"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8e5ce48d7c645fe89c026dc183"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c8a2d7000e930610fcff4518b5"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_7687de6e695cb7f4bd9c2c9f75"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_36d8a4897455038c86fe864b56"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_af9dfc9e21f85cf3d6e7b3f364"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_eebf4f4b257d3c54b29cc8612a"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_92edd522e47ae4b0c32fd5ec79"`);
+ await queryRunner.query(`DROP TABLE "project"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_0bf9c3491d0f50a52d4874a5b4"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8e81515c1554b7fe9540a80e9d"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d2429392781e2bbd3e707d5cbb"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_826e688fee211771c9b0f5e3a2"`);
+ await queryRunner.query(`DROP TABLE "project_risk"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_485d32d02c154922086bf9f604"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_55ff7ee85ea69072a47da1d158"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d81d7fa7dd028492b040b99f8a"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_6a3f38c658f937940d6e744e2b"`);
+ await queryRunner.query(`DROP TABLE "tier_level"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_68176849c7a540b890086c2482"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1ee68d46c647bbd30af0655406"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_fdbbf5ddd085c931b2b9a597c8"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_7cbf3bb5d2b807ac35197acc7e"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_120d2981d695242d3126b2ecf3"`);
+ await queryRunner.query(`DROP TABLE "workflow"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_88b12d599bddadf5cf7ed7ca6c"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d42b1950f600f2ba10a1073c37"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_5ece525a0b4612005a76d0110e"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_dfa98ba50e790bc7b17248fa20"`);
+ await queryRunner.query(`DROP TABLE "project_status"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_fab667986554eb11aac2b87caa"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1c1a58bed256b7b2a5f15509a3"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_e4449ca75afd996e3044832bb0"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_caf56f353cb0d8ae6bd750a0c4"`);
+ await queryRunner.query(`DROP TABLE "building_fiscal"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_fe77a224958124b56f0258febb"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_98a4cb83be360a5a1a284d58e4"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_448cedce8da97154c0238de097"`);
+ await queryRunner.query(`DROP TABLE "fiscal_key"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_9c6b73e6e1191c475083a1d5fd"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_6da6cc15cd660f17f8d5f4b872"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_56aeba3df852659e336883e93d"`);
+ await queryRunner.query(`DROP TABLE "building_evaluation"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_bc8c2c506f8cc061b0e3380eb1"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_839b7cb82690afe5a15c035b08"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_f96c0877bd6385eac3ba1f2c78"`);
+ await queryRunner.query(`DROP TABLE "evaluation_key"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_0dc5a69385e011b026cf9349a2"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_a5b3ee7013349be96c40cb9cfd"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8ae155b8e2213e901180f54ba4"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_b3f209b9a4ee05a9eb9ec58fe5"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_6774c5af0daf3a6f910a4aa042"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_e1c368b69fe6d55fb899d4b09f"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_660f6a82c58715ccb3e939bec9"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_6bec46322ae9b4fdc1712ab65e"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ecbf2cc6d83ab02e2495f354f7"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1db6127f236406fbd224404a56"`);
+ await queryRunner.query(`DROP TABLE "building"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d1d9ae93b704c63e33ed6ebbfc"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_cd7711c358add04632676cd4cf"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_6d34de8157955f540f198dbc61"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_719b4bbf3b93a649f5ef90d968"`);
+ await queryRunner.query(`DROP TABLE "property_type"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_fe1614080ee22db893bdfbdeba"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1a24aef900f50e1c3abfe53164"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_806993c03d99558f423ec01bbb"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_0995f092c645d5412712204fa0"`);
+ await queryRunner.query(`DROP TABLE "property_classification"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_a3c788ce5a2d7ee0708b63755c"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_af5f23834c3bd813da5cd195da"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_09ecbd0495a18dd9c5648e6ede"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_a5b6c7d4abbf4a76127c1b3494"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_68d342e36be4fc4f78d1d943c1"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8d89023b6b9a759bee9ba30d08"`);
+ await queryRunner.query(`DROP TABLE "administrative_area"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_b54101e216cdd5a65212744139"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_9d396303fe83e044d69531b128"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ff339e05eab922546e009f6cc7"`);
+ await queryRunner.query(`DROP TABLE "province"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_388afb0f03dd0bc943949c24e8"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ed5100402d64f5a45c30e724fe"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d8f11c563f58d60c57c96b7b83"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_336d5c7c816dc621dbc820cc2a"`);
+ await queryRunner.query(`DROP TABLE "regional_district"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_ae7808ddc529eb5f08dac71874"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_8d85670f0a3bb38094f1db9023"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_088d32f15fd51a4eee07cdde28"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_a6adc6a5b01c4024621c52b1a2"`);
+ await queryRunner.query(`DROP TABLE "building_predominate_use"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_d4305f752c6248063c083a27f4"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_4068747f6c9171d4106950eed9"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c40ce91f4fff8b30bc116e66d5"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_09f8f34216ccfffd99add29359"`);
+ await queryRunner.query(`DROP TABLE "building_occupant_type"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_7eb84eaff27e41814425e258bf"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_1fe605fd95d54c7a3d8fb8ea63"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_6744ccd2014fd4e75709410415"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_3bf46797647574d5aeb8122a5a"`);
+ await queryRunner.query(`DROP TABLE "building_construction_type"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_25dccf2207a003f5ecce8a33c7"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_352664edaa51c51a3f62d1cd8a"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_0f567e907073e1bf5af072410c"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c3e2e1bb170870974db5ce848f"`);
+ await queryRunner.query(`DROP TABLE "access_request"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_c01351e0689032ad8995861393"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_b7eee57d84fb7ed872e660197f"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_b000857089edf6cae23b9bc9b8"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_cdcb63fdec2cdf48ea4589557d"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_061257d343976f0dd80167c79e"`);
+ await queryRunner.query(`DROP TABLE "user"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_882f8f0de8c784abe421f17cd4"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_65aaedd70b9d60594dddcc36b2"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_0a24ac662369d88db55649f3e5"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_051c6dcae604d9286f20cc2d76"`);
+ await queryRunner.query(`DROP TABLE "role"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_5e16d9d279223ff5fe988f6a3b"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_89544ece79ad52653c8fce606b"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_4087b9d814a4a90af734b6dd3a"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_57fd74ea1b8a1b0d85e1c7ad20"`);
+ await queryRunner.query(`DROP TABLE "agency"`);
+ }
+}
diff --git a/express-api/src/typeorm/Migrations/1707763864014-SeedData.ts b/express-api/src/typeorm/Migrations/1707763864014-SeedData.ts
new file mode 100644
index 000000000..360469caf
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/1707763864014-SeedData.ts
@@ -0,0 +1,127 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+import { SqlReader } from 'node-sql-reader';
+import * as path from 'path';
+
+export class SeedData1707763864014 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ const sqlFilePath = path.resolve(__dirname, 'Seeds');
+
+ const Users = SqlReader.readSqlFile(path.join(sqlFilePath, 'Users.sql'));
+ await queryRunner.query(Users.toString());
+
+ const Agencies = SqlReader.readSqlFile(path.join(sqlFilePath, 'Agencies.sql'));
+ await queryRunner.query(Agencies.toString());
+
+ const Provinces = SqlReader.readSqlFile(path.join(sqlFilePath, 'Provinces.sql'));
+ await queryRunner.query(Provinces.toString());
+
+ const BuildingOccupantTypes = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'BuildingOccupantTypes.sql'),
+ );
+ await queryRunner.query(BuildingOccupantTypes.toString());
+
+ const BuildingPredominateUses = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'BuildingPredominateUses.sql'),
+ );
+ await queryRunner.query(BuildingPredominateUses.toString());
+
+ const NotificationTemplates = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'NotificationTemplates.sql'),
+ );
+ await queryRunner.query(NotificationTemplates.toString());
+
+ const ProjectRisks = SqlReader.readSqlFile(path.join(sqlFilePath, 'ProjectRisks.sql'));
+ await queryRunner.query(ProjectRisks.toString());
+
+ const ProjectStatus = SqlReader.readSqlFile(path.join(sqlFilePath, 'ProjectStatus.sql'));
+ await queryRunner.query(ProjectStatus.toString());
+
+ const TierLevels = SqlReader.readSqlFile(path.join(sqlFilePath, 'TierLevels.sql'));
+ await queryRunner.query(TierLevels.toString());
+
+ const Workflows = SqlReader.readSqlFile(path.join(sqlFilePath, 'Workflows.sql'));
+ await queryRunner.query(Workflows.toString());
+
+ const WorkflowProjectStatus = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'WorkflowProjectStatus.sql'),
+ );
+ await queryRunner.query(WorkflowProjectStatus.toString());
+
+ const PropertyClassifications = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'PropertyClassifications.sql'),
+ );
+ await queryRunner.query(PropertyClassifications.toString());
+
+ const PropertyTypes = SqlReader.readSqlFile(path.join(sqlFilePath, 'PropertyTypes.sql'));
+ await queryRunner.query(PropertyTypes.toString());
+
+ const BuildingConstructionTypes = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'BuildingConstructionTypes.sql'),
+ );
+ await queryRunner.query(BuildingConstructionTypes.toString());
+
+ const Tasks = SqlReader.readSqlFile(path.join(sqlFilePath, 'Tasks.sql'));
+ await queryRunner.query(Tasks.toString());
+
+ const ProjectStatusNotifications = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'ProjectStatusNotifications.sql'),
+ );
+ await queryRunner.query(ProjectStatusNotifications.toString());
+
+ const ProjectStatusTransitions = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'ProjectStatusTransitions.sql'),
+ );
+ await queryRunner.query(ProjectStatusTransitions.toString());
+
+ const Roles = SqlReader.readSqlFile(path.join(sqlFilePath, 'Roles.sql'));
+ await queryRunner.query(Roles.toString());
+
+ const ReportTypes = SqlReader.readSqlFile(path.join(sqlFilePath, 'ReportTypes.sql'));
+ await queryRunner.query(ReportTypes.toString());
+
+ const RegionalDistricts = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'RegionalDistricts.sql'),
+ );
+ await queryRunner.query(RegionalDistricts.toString());
+
+ const AdministrativeAreas = SqlReader.readSqlFile(
+ path.join(sqlFilePath, 'AdministrativeAreas.sql'),
+ );
+ await queryRunner.query(AdministrativeAreas.toString());
+
+ const FiscalKeys = SqlReader.readSqlFile(path.join(sqlFilePath, 'FiscalKeys.sql'));
+ await queryRunner.query(FiscalKeys.toString());
+
+ const EvaluationKeys = SqlReader.readSqlFile(path.join(sqlFilePath, 'EvaluationKeys.sql'));
+ await queryRunner.query(EvaluationKeys.toString());
+
+ const ProjectTypes = SqlReader.readSqlFile(path.join(sqlFilePath, 'ProjectTypes.sql'));
+ await queryRunner.query(ProjectTypes.toString());
+ }
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query('TRUNCATE TABLE project_type cascade');
+ await queryRunner.query('TRUNCATE TABLE evaluation_key cascade');
+ await queryRunner.query('TRUNCATE TABLE fiscal_key cascade');
+ await queryRunner.query('TRUNCATE TABLE administrative_area cascade');
+ await queryRunner.query('TRUNCATE TABLE report_type cascade');
+ await queryRunner.query('TRUNCATE TABLE project_status_transition cascade');
+ await queryRunner.query('TRUNCATE TABLE project_status_notification cascade');
+ await queryRunner.query('TRUNCATE TABLE workflow_project_status cascade');
+ await queryRunner.query('TRUNCATE TABLE notification_template cascade');
+ await queryRunner.query('TRUNCATE TABLE building_construction_type cascade');
+ await queryRunner.query('TRUNCATE TABLE building_occupant_type cascade');
+ await queryRunner.query('TRUNCATE TABLE building_predominate_use cascade');
+ await queryRunner.query('TRUNCATE TABLE property_classification cascade');
+ await queryRunner.query('TRUNCATE TABLE property_type cascade');
+ await queryRunner.query('TRUNCATE TABLE tier_level cascade');
+ await queryRunner.query('TRUNCATE TABLE agency cascade');
+ await queryRunner.query('TRUNCATE TABLE role cascade');
+ await queryRunner.query('TRUNCATE TABLE province cascade');
+ await queryRunner.query('TRUNCATE TABLE project_risk cascade');
+ await queryRunner.query('TRUNCATE TABLE workflow cascade');
+ await queryRunner.query('TRUNCATE TABLE project_status cascade');
+ await queryRunner.query('TRUNCATE TABLE task cascade');
+ await queryRunner.query('TRUNCATE TABLE regional_district cascade');
+ await queryRunner.query('TRUNCATE TABLE "user" cascade'); // Seems to only work with quotes.
+ }
+}
diff --git a/express-api/src/typeorm/Migrations/Seeds/AdministrativeAreas.sql b/express-api/src/typeorm/Migrations/Seeds/AdministrativeAreas.sql
new file mode 100644
index 000000000..f94bde07b
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/Seeds/AdministrativeAreas.sql
@@ -0,0 +1,356 @@
+INSERT INTO "administrative_area" ("Id","CreatedById","CreatedOn","Name","IsDisabled","SortOrder","RegionalDistrictId","ProvinceId") VALUES
+ (2, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', '100 Mile House', false, 0, 2, 'BC'),
+(3, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', '150 Mile House', false, 0, 2, 'BC'),
+(4, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Abbotsford', false, 0, 8, 'BC'),
+(5, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Agassiz', false, 0, 8, 'BC'),
+(6, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Ahousaht', false, 0, 18, 'BC'),
+(7, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Albert Canyon', false, 0, 14, 'BC'),
+(8, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Aldergrove', false, 0, 9, 'BC'),
+(9, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Alert Bay', false, 0, 23, 'BC'),
+(10, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Alexis Creek', false, 0, 2, 'BC'),
+(11, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Aleza Lake', false, 0, 3, 'BC'),
+(12, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Allison Pass', false, 0, 10, 'BC'),
+(13, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Anahim Lake', false, 0, 2, 'BC'),
+(14, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Armstrong', false, 0, 17, 'BC'),
+(15, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Ashcroft', false, 0, 12, 'BC'),
+(16, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Athalmere', false, 0, 15, 'BC'),
+(17, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Atlin', false, 0, 28, 'BC'),
+(18, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Avola', false, 0, 12, 'BC'),
+(19, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Baldonnel', false, 0, 5, 'BC'),
+(20, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Balfour', false, 0, 13, 'BC'),
+(21, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bamfield', false, 0, 18, 'BC'),
+(22, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Barnston Island', false, 0, 9, 'BC'),
+(23, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Barriere', false, 0, 12, 'BC'),
+(24, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bear Lake', false, 0, 3, 'BC'),
+(25, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Beaverdell', false, 0, 16, 'BC'),
+(26, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bella Bella', false, 0, 20, 'BC'),
+(27, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bella Coola', false, 0, 20, 'BC'),
+(28, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Big Bar Creek', false, 0, 12, 'BC'),
+(29, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Birch Island', false, 0, 12, 'BC'),
+(30, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Blue River', false, 0, 12, 'BC'),
+(31, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bob Quinn Lake', false, 0, 4, 'BC'),
+(32, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Boston Bar', false, 0, 8, 'BC'),
+(33, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Boswell', false, 0, 13, 'BC'),
+(34, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bowen Island', false, 0, 9, 'BC'),
+(35, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bradner', false, 0, 8, 'BC'),
+(36, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bridge Lake', false, 0, 2, 'BC'),
+(37, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Buick Creek', false, 0, 5, 'BC'),
+(38, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Burnaby', false, 0, 9, 'BC'),
+(39, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Burns Lake', false, 0, 1, 'BC'),
+(40, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cache', false, 0, 3, 'BC'),
+(41, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cache Creek', false, 0, 12, 'BC'),
+(42, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Campbell River', false, 0, 27, 'BC'),
+(43, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cassiar', false, 0, 28, 'BC'),
+(44, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cassidy', false, 0, 24, 'BC'),
+(45, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Castlegar', false, 0, 13, 'BC'),
+(46, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cecil Lake', false, 0, 5, 'BC'),
+(47, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Central Saanich', false, 0, 19, 'BC'),
+(48, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Charlie Lake', false, 0, 5, 'BC'),
+(49, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Chase', false, 0, 12, 'BC'),
+(50, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Chemainus', false, 0, 22, 'BC'),
+(51, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cherryville', false, 0, 17, 'BC'),
+(52, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Chetwynd', false, 0, 5, 'BC'),
+(53, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Chief Lake', false, 0, 3, 'BC'),
+(54, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Chilliwack', false, 0, 8, 'BC'),
+(55, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Clayhurst', false, 0, 5, 'BC'),
+(56, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Clearbrook', false, 0, 8, 'BC'),
+(57, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Clearwater', false, 0, 12, 'BC'),
+(58, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Clinton', false, 0, 12, 'BC'),
+(59, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cloverdale', false, 0, 9, 'BC'),
+(60, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Coalmont', false, 0, 10, 'BC'),
+(61, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cobble Hill', false, 0, 22, 'BC'),
+(63, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Colwood', false, 0, 19, 'BC'),
+(64, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Comox', false, 0, 21, 'BC'),
+(65, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Coquitlam', false, 0, 9, 'BC'),
+(66, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cortes Island', false, 0, 27, 'BC'),
+(67, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Courtenay', false, 0, 21, 'BC'),
+(68, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cowichan Bay', false, 0, 22, 'BC'),
+(69, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cowichan Lake', false, 0, 22, 'BC'),
+(70, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cranbrook', false, 0, 15, 'BC'),
+(71, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Crawford Bay', false, 0, 13, 'BC'),
+(72, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Creston', false, 0, 13, 'BC'),
+(73, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cultus Lake', false, 0, 8, 'BC'),
+(74, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Cumberland', false, 0, 21, 'BC'),
+(75, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Dawson Creek', false, 0, 5, 'BC'),
+(76, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Dease Lake', false, 0, 4, 'BC'),
+(77, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Decker Lake', false, 0, 1, 'BC'),
+(78, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Delta', false, 0, 9, 'BC'),
+(79, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Denman Island', false, 0, 21, 'BC'),
+(80, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lake Country', false, 0, 7, 'BC'),
+(81, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Duncan', false, 0, 22, 'BC'),
+(82, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Edgewood', false, 0, 13, 'BC'),
+(83, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Elkford', false, 0, 15, 'BC'),
+(84, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Elko', false, 0, 15, 'BC'),
+(85, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Enderby', false, 0, 17, 'BC'),
+(86, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Errington', false, 0, 24, 'BC'),
+(87, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Esquimalt', false, 0, 19, 'BC'),
+(88, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fairharbour', false, 0, 27, 'BC'),
+(89, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Falkland', false, 0, 14, 'BC'),
+(90, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fauquier', false, 0, 13, 'BC'),
+(91, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fernie', false, 0, 15, 'BC'),
+(92, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Field', false, 0, 14, 'BC'),
+(93, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fort Fraser', false, 0, 1, 'BC'),
+(94, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fort Nelson', false, 0, 29, 'BC'),
+(95, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fort St. James', false, 0, 1, 'BC'),
+(96, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fort St. John', false, 0, 5, 'BC'),
+(97, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fort Ware', false, 0, 5, 'BC'),
+(98, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Francois Lake', false, 0, 1, 'BC'),
+(99, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fraser Lake', false, 0, 1, 'BC'),
+(100, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Fruitvale', false, 0, 16, 'BC'),
+(101, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Gabriola Island', false, 0, 24, 'BC'),
+(102, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Galiano Island', false, 0, 19, 'BC'),
+(103, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Ganges', false, 0, 19, 'BC'),
+(104, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Garibaldi Highlands', false, 0, 11, 'BC'),
+(105, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Gibsons', false, 0, 26, 'BC'),
+(106, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Gillies Bay', false, 0, 25, 'BC'),
+(107, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Gold River', false, 0, 27, 'BC'),
+(108, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Goldbridge', false, 0, 11, 'BC'),
+(109, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Golden', false, 0, 14, 'BC'),
+(110, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Good Hope', false, 0, 20, 'BC'),
+(111, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Grand Forks', false, 0, 16, 'BC'),
+(112, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Granisle', false, 0, 1, 'BC'),
+(113, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Grassy Plains', false, 0, 1, 'BC'),
+(114, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Greenwood', false, 0, 16, 'BC'),
+(115, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hagensborg', false, 0, 20, 'BC'),
+(116, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hansard Creek', false, 0, 3, 'BC'),
+(117, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Harrison Hot Springs', false, 0, 8, 'BC'),
+(118, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hartley Bay', false, 0, 6, 'BC'),
+(119, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hazelton', false, 0, 4, 'BC'),
+(120, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hixon', false, 0, 3, 'BC'),
+(121, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Holberg', false, 0, 23, 'BC'),
+(122, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Honeymoon Creek', false, 0, 5, 'BC'),
+(123, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hope', false, 0, 8, 'BC'),
+(124, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hornby Island', false, 0, 21, 'BC'),
+(125, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Horsefly', false, 0, 2, 'BC'),
+(126, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Houston', false, 0, 1, 'BC'),
+(127, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hudson''s Hope', false, 0, 5, 'BC'),
+(128, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Hutda Lake', false, 0, 3, 'BC'),
+(129, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Invermere', false, 0, 15, 'BC'),
+(130, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Jade City', false, 0, 28, 'BC'),
+(131, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Jaffray', false, 0, 15, 'BC'),
+(132, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Jordan River', false, 0, 14, 'BC'),
+(134, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kaleden', false, 0, 10, 'BC'),
+(135, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kamloops', false, 0, 12, 'BC'),
+(136, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kaslo', false, 0, 13, 'BC'),
+(137, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kelowna', false, 0, 7, 'BC'),
+(138, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Keremeos', false, 0, 10, 'BC'),
+(139, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kimberley', false, 0, 15, 'BC'),
+(140, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kincolith', false, 0, 4, 'BC'),
+(141, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kiskatinaw', false, 0, 5, 'BC'),
+(142, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kitimat', false, 0, 4, 'BC'),
+(143, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kitkatla', false, 0, 6, 'BC'),
+(144, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kitwanga', false, 0, 4, 'BC'),
+(145, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Klemtu', false, 0, 4, 'BC'),
+(146, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kyuquot', false, 0, 27, 'BC'),
+(147, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Ladner', false, 0, 9, 'BC'),
+(148, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Ladysmith', false, 0, 22, 'BC'),
+(149, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Langford', false, 0, 19, 'BC'),
+(150, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Langley', false, 0, 9, 'BC'),
+(151, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lantzville', false, 0, 24, 'BC'),
+(152, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lardeau', false, 0, 13, 'BC'),
+(153, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lasqueti Island', false, 0, 25, 'BC'),
+(154, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Likely', false, 0, 2, 'BC'),
+(155, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lillooet', false, 0, 11, 'BC'),
+(156, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lions Bay', false, 0, 9, 'BC'),
+(157, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Little Fort', false, 0, 12, 'BC'),
+(158, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Logan Lake', false, 0, 12, 'BC'),
+(159, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Loon Creek', false, 0, 9, 'BC'),
+(161, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lumby', false, 0, 17, 'BC'),
+(162, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lytton', false, 0, 12, 'BC'),
+(163, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mackenzie', false, 0, 3, 'BC'),
+(164, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Madeira Park', false, 0, 26, 'BC'),
+(165, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mansons Landing', false, 0, 27, 'BC'),
+(166, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Maple Ridge', false, 0, 9, 'BC'),
+(167, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Marysville', false, 0, 15, 'BC'),
+(168, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Masset', false, 0, 6, 'BC'),
+(169, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Matsqui', false, 0, 8, 'BC'),
+(170, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mayne Island', false, 0, 19, 'BC'),
+(171, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mcbride', false, 0, 3, 'BC'),
+(172, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mcleese Lake', false, 0, 2, 'BC'),
+(173, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mclure', false, 0, 12, 'BC'),
+(174, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Meadow Creek', false, 0, 13, 'BC'),
+(175, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Merritt', false, 0, 12, 'BC'),
+(176, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Metchosin', false, 0, 19, 'BC'),
+(177, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Meziadin Lake', false, 0, 4, 'BC'),
+(178, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Midway', false, 0, 16, 'BC'),
+(179, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mill Bay', false, 0, 22, 'BC'),
+(180, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mission', false, 0, 8, 'BC'),
+(181, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Montney', false, 0, 5, 'BC'),
+(182, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Montrose', false, 0, 16, 'BC'),
+(183, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mount Lemoray', false, 0, 5, 'BC'),
+(184, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Nakusp', false, 0, 13, 'BC'),
+(185, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Nanaimo', false, 0, 24, 'BC'),
+(186, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Nanoose Bay', false, 0, 24, 'BC'),
+(187, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Nazko', false, 0, 2, 'BC'),
+(188, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Nelson', false, 0, 13, 'BC'),
+(189, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'New Aiyansh', false, 0, 4, 'BC'),
+(190, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'New Denver', false, 0, 13, 'BC'),
+(191, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'New Hazelton', false, 0, 4, 'BC'),
+(192, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'New Westminster', false, 0, 9, 'BC'),
+(193, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'North Delta', false, 0, 9, 'BC'),
+(194, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'North Pender Island', false, 0, 19, 'BC'),
+(195, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'North Saanich', false, 0, 19, 'BC'),
+(196, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'North Vancouver', false, 0, 9, 'BC'),
+(197, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Oak Bay', false, 0, 19, 'BC'),
+(198, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Okanagan Falls', false, 0, 10, 'BC'),
+(199, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Oliver', false, 0, 10, 'BC'),
+(200, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Osoyoos', false, 0, 10, 'BC'),
+(201, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Oyama', false, 0, 7, 'BC'),
+(202, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Parksville', false, 0, 24, 'BC'),
+(203, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Peachland', false, 0, 7, 'BC'),
+(204, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Pemberton', false, 0, 11, 'BC'),
+(205, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Pender Island', false, 0, 19, 'BC'),
+(206, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Penticton', false, 0, 10, 'BC'),
+(207, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Pink Mountain', false, 0, 5, 'BC'),
+(208, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Pitt Meadows', false, 0, 9, 'BC'),
+(209, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Simpson', false, 0, 6, 'BC'),
+(210, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Alberni', false, 0, 18, 'BC'),
+(211, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Alice', false, 0, 23, 'BC'),
+(212, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Clements', false, 0, 6, 'BC'),
+(213, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Coquitlam', false, 0, 9, 'BC'),
+(214, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Hardy', false, 0, 23, 'BC'),
+(215, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Mcneill', false, 0, 23, 'BC'),
+(216, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Moody', false, 0, 9, 'BC'),
+(217, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Renfrew', false, 0, 19, 'BC'),
+(218, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Pouce Coupe', false, 0, 5, 'BC'),
+(219, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Powell River', false, 0, 25, 'BC'),
+(220, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Prince George', false, 0, 3, 'BC'),
+(221, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Prince Rupert', false, 0, 6, 'BC'),
+(222, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Princeton', false, 0, 10, 'BC'),
+(223, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Progress', false, 0, 5, 'BC'),
+(224, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Purden Lake', false, 0, 3, 'BC'),
+(225, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Quadra Island', false, 0, 27, 'BC'),
+(226, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Qualicum Bay', false, 0, 24, 'BC'),
+(227, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Qualicum Beach', false, 0, 24, 'BC'),
+(228, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Queen Charlotte', false, 0, 6, 'BC'),
+(229, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Quesnel', false, 0, 2, 'BC'),
+(230, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Rayleigh Corr Inst', false, 0, 12, 'BC'),
+(231, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Revelstoke', false, 0, 14, 'BC'),
+(232, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Richmond', false, 0, 9, 'BC'),
+(233, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Riondel', false, 0, 13, 'BC'),
+(234, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Riske Creek', false, 0, 2, 'BC'),
+(235, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Roberts Creek', false, 0, 26, 'BC'),
+(236, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Rock Creek', false, 0, 16, 'BC'),
+(237, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Rolla', false, 0, 5, 'BC'),
+(238, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Rose Prairie', false, 0, 5, 'BC'),
+(239, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Rosedale', false, 0, 8, 'BC'),
+(240, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Rossland', false, 0, 16, 'BC'),
+(241, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Rutland', false, 0, 7, 'BC'),
+(242, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Saanich', false, 0, 19, 'BC'),
+(243, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Saanichton', false, 0, 19, 'BC'),
+(244, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Salmo', false, 0, 13, 'BC'),
+(245, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Salmon Arm', false, 0, 14, 'BC'),
+(249, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Salt Spring Island', false, 0, 19, 'BC'),
+(250, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Salvus', false, 0, 4, 'BC'),
+(251, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sandspit', false, 0, 6, 'BC'),
+(252, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sardis', false, 0, 8, 'BC'),
+(253, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Saturna Island', false, 0, 19, 'BC'),
+(254, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Savona', false, 0, 12, 'BC'),
+(255, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sayward', false, 0, 27, 'BC'),
+(256, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Scotch Creek', false, 0, 14, 'BC'),
+(257, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sechelt', false, 0, 26, 'BC'),
+(258, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Seton Portage', false, 0, 11, 'BC'),
+(259, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Shawnigan Lake', false, 0, 22, 'BC'),
+(260, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Shelter Bay', false, 0, 14, 'BC'),
+(261, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sicamous', false, 0, 14, 'BC'),
+(262, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sidney', false, 0, 19, 'BC'),
+(263, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Silverton', false, 0, 13, 'BC'),
+(264, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Skidegate', false, 0, 6, 'BC'),
+(265, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Slim Creek', false, 0, 3, 'BC'),
+(266, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Smithers', false, 0, 1, 'BC'),
+(267, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Soda Creek', false, 0, 2, 'BC'),
+(268, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sointula', false, 0, 23, 'BC'),
+(269, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sooke', false, 0, 19, 'BC'),
+(270, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Southbank', false, 0, 1, 'BC'),
+(271, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sparwood', false, 0, 15, 'BC'),
+(272, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Squamish', false, 0, 11, 'BC'),
+(273, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Stewart', false, 0, 4, 'BC'),
+(274, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Summerland', false, 0, 10, 'BC'),
+(275, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Surrey', false, 0, 9, 'BC'),
+(276, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tachie', false, 0, 1, 'BC'),
+(277, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tahsis', false, 0, 27, 'BC'),
+(278, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Takla', false, 0, 1, 'BC'),
+(279, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tatla Lake', false, 0, 2, 'BC'),
+(280, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tatogga Lake', false, 0, 4, 'BC'),
+(281, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Taylor', false, 0, 5, 'BC'),
+(282, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Telegraph Creek', false, 0, 4, 'BC'),
+(283, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Telkwa', false, 0, 1, 'BC'),
+(284, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Terrace', false, 0, 4, 'BC'),
+(285, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tete Jaune', false, 0, 3, 'BC'),
+(286, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Texada Island', false, 0, 25, 'BC'),
+(287, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Thetis Island', false, 0, 22, 'BC'),
+(288, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Toad River', false, 0, 29, 'BC'),
+(289, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tofino', false, 0, 18, 'BC'),
+(290, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Trail', false, 0, 16, 'BC'),
+(291, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Trout Lake', false, 0, 14, 'BC'),
+(292, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tsay Keh Dene Old Goldstream Creek', false, 0, 5, 'BC'),
+(293, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tumbler Ridge', false, 0, 5, 'BC'),
+(294, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Ucluelet', false, 0, 18, 'BC'),
+(295, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Usk', false, 0, 4, 'BC'),
+(296, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Valemount', false, 0, 3, 'BC'),
+(297, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Vancouver', false, 0, 9, 'BC'),
+(298, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Vanderhoof', false, 0, 1, 'BC'),
+(299, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Vernon', false, 0, 17, 'BC'),
+(300, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Victoria', false, 0, 19, 'BC'),
+(301, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'View Royal', false, 0, 19, 'BC'),
+(302, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Waglisla', false, 0, 20, 'BC'),
+(303, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Wardner', false, 0, 15, 'BC'),
+(304, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Wells', false, 0, 2, 'BC'),
+(305, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'West Kelowna', false, 0, 7, 'BC'),
+(306, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'West Vancouver', false, 0, 9, 'BC'),
+(307, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Whistler', false, 0, 11, 'BC'),
+(308, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'White Rock', false, 0, 9, 'BC'),
+(309, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Williams Lake', false, 0, 2, 'BC'),
+(310, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Winlaw', false, 0, 13, 'BC'),
+(311, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Woss', false, 0, 23, 'BC'),
+(312, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Yahk', false, 0, 13, 'BC'),
+(313, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Zeballos', false, 0, 27, 'BC'),
+(314, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lower Nicola', false, 0, 12, 'BC'),
+(315, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Tsawwassen', false, 0, 9, 'BC'),
+(318, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Spallumcheen', false, 0, 17, 'BC'),
+(347, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Port Edward', false, 0, 6, 'BC'),
+(348, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Highlands', false, 0, 19, 'BC'),
+(380, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Slocan', false, 0, 13, 'BC'),
+(425, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Belcarra', false, 0, 9, 'BC'),
+(432, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Anmore', false, 0, 9, 'BC'),
+(438, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Jumbo Glacier Mountain Resort Municipality', false, 0, 15, 'BC'),
+(439, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Kent', false, 0, 8, 'BC'),
+(442, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lake Cowichan', false, 0, 22, 'BC'),
+(448, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Warfield', false, 0, 16, 'BC'),
+(450, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'North Cowichan', false, 0, 22, 'BC'),
+(460, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Radium Hot Springs', false, 0, 15, 'BC'),
+(462, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Northern Rockies Regional Municipality', false, 0, 29, 'BC'),
+(464, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Sun Peaks Mountain Resort Municipality', false, 0, 12, 'BC'),
+(466, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Coldstream', false, 0, 17, 'BC'),
+(471, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Canal Flats', false, 0, 15, 'BC'),
+(476, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Valdes Island', false, 0, 22, 'BC'),
+(477, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Stopper Islands', false, 0, 18, 'BC'),
+(478, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Big Bay in Stuart Island', false, 0, 27, 'BC'),
+(479, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Black Creek', false, 0, 21, 'BC'),
+(480, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Bouchie Lake', false, 0, 2, 'BC'),
+(481, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Brookmere', false, 0, 12, 'BC'),
+(482, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Coal Harbour', false, 0, 23, 'BC'),
+(483, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Coombs', false, 0, 24, 'BC'),
+(484, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Crofton', false, 0, 24, 'BC'),
+(485, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'East Sooke', false, 0, 19, 'BC'),
+(486, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Giscome', false, 0, 3, 'BC'),
+(487, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Granite Bay', false, 0, 27, 'BC'),
+(488, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Halfway River', false, 0, 5, 'BC'),
+(489, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Lake Errock', false, 0, 8, 'BC'),
+(490, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Miworth', false, 0, 3, 'BC'),
+(491, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Moberly Lake', false, 0, 5, 'BC'),
+(492, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Mudge Island', false, 0, 24, 'BC'),
+(493, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Pineview', false, 0, 3, 'BC'),
+(494, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Quathiaski Cove', false, 0, 27, 'BC'),
+(495, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Quatsino', false, 0, 23, 'BC'),
+(496, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Red Rock', false, 0, 3, 'BC'),
+(497, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Rock Bay in Sayward', false, 0, 27, 'BC'),
+(498, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Salmon Valley', false, 0, 3, 'BC'),
+(499, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Stoner', false, 0, 3, 'BC'),
+(500, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Surge Narrows', false, 0, 27, 'BC'),
+(501, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Thornhill', false, 0, 4, 'BC'),
+(502, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Two Mile', false, 0, 4, 'BC'),
+(503, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'West Lake', false, 0, 3, 'BC'),
+(504, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Whaletown', false, 0, 27, 'BC'),
+(505, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Willow River', false, 0, 3, 'BC'),
+(506, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:40.2466667', 'Wonowon', false, 0, 5, 'BC');
diff --git a/express-api/src/typeorm/Migrations/Seeds/Agencies.sql b/express-api/src/typeorm/Migrations/Seeds/Agencies.sql
new file mode 100644
index 000000000..8a3f26c13
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/Seeds/Agencies.sql
@@ -0,0 +1,180 @@
+INSERT INTO "agency"("Id","CreatedById","CreatedOn","UpdatedById","UpdatedOn","Name","IsDisabled","SortOrder","Code","Description","ParentId","Email","SendEmail","AddressTo","CCEmail") VALUES
+ (1,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Advanced Education & Skills Training',false,0,N'AEST',N'',NULL,N'kevin.brewster@gov.bc.ca',true,N'Kevin',NULL),
+ (2,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Citizens'' Services',false,0,N'CITZ',N'',NULL,N'dean.skinner@gov.bc.ca',true,N'Dean',NULL),
+ (4,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Education',false,0,N'EDUC',N'',NULL,N'reg.bawa@gov.bc.ca',true,N'Reg',NULL),
+ (5,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Finance',false,0,N'FIN',N'',NULL,N'teri.spaven@gov.bc.ca',true,N'Teri',NULL),
+ (6,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Ministry of Forests, Lands, Natural Resources',false,0,N'FLNR',NULL,NULL,N'trish.dohan@gov.bc.ca',true,N'ADM CSNR and EFO for FLNR',NULL),
+ (7,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Health',false,0,N'HLTH',N'',NULL,N'philip.twyford@gov.bc.ca',true,N'Philip',NULL),
+ (8,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Ministry of Municipal Affairs & Housing',false,0,N'MAH',NULL,NULL,N'david.curtis@gov.bc.ca',true,N'ADM & EFO Management Services Division',NULL),
+ (9,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Transportation & Infrastructure',false,0,N'TRAN',N'',NULL,N'nancy.bain@gov.bc.ca',true,N'Nancy',NULL),
+ (10,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Energy, Mines & Petroleum Resources',false,0,N'EMPR',NULL,NULL,N'wes.boyd@gov.bc.ca',true,N'ADM CSNR and EFO to MIRR, AGRI EMPR ENV',NULL),
+ (11,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Ministry of Attorney General',false,0,N'MAG',NULL,NULL,N'tracy.campbell@gov.bc.ca',true,N'ADM & EFO CMSB AG and PSSG',NULL),
+ (12,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Jobs, Economic Development & Competitiveness',false,0,N'JEDC',NULL,NULL,N'joanna.white@gov.bc.ca',true,N'A/ADM & EFO Management Services Division',NULL),
+ (13,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Ministry of Tourism, Arts and Culture',false,0,N'MTAC',NULL,NULL,N'salman.azam@gov.bc.ca',true,N'ADM & EFO Management Services Division',NULL),
+ (14,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Social Development & Poverty Reduction',false,0,N'SDPR',N'',NULL,N'jonathan.dube@gov.bc.ca',true,N'Jonathan',NULL),
+ (15,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Mental Health & Addictions',false,0,N'MMHA',N'',NULL,N'dara.landry@gov.bc.ca',true,N'Dara',NULL),
+ (16,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'Ministry of Children and Family Development',false,0,N'MCFD',NULL,NULL,N'rob.byers@gov.bc.ca',true,N'AMD & EFO',NULL),
+ (17,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:29.3833333',NULL,NULL,N'BC Public Service Agency',false,0,N'BCPSA',N'',NULL,N'bruce.richmond@gov.bc.ca',true,N'Bruce',NULL),
+ (20,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5000000',NULL,NULL,N'Fraser Health Authority',false,0,N'FHA',NULL,7,NULL,false,NULL,NULL),
+ (21,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5000000',NULL,NULL,N'Interior Health Authority',false,0,N'IHA',NULL,7,NULL,false,NULL,NULL),
+ (22,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5000000',NULL,NULL,N'Northern Health Authority',false,0,N'NHA',NULL,7,NULL,false,NULL,NULL),
+ (23,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5000000',NULL,NULL,N'Provincial Health Services Authority',false,0,N'PHSA',NULL,7,NULL,false,NULL,NULL),
+ (24,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5000000',NULL,NULL,N'Vancouver Coastal Health Authority',false,0,N'VCHA',NULL,7,NULL,false,NULL,NULL),
+ (25,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5000000',NULL,NULL,N'Vancouver Island Health Authority',false,0,N'VIHA',NULL,7,NULL,false,NULL,NULL),
+ (30,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5033333',NULL,NULL,N'BC Hydro',false,0,N'BCH',NULL,10,NULL,false,NULL,NULL),
+ (40,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5066667',NULL,NULL,N'BC Housing',false,0,N'BCH',NULL,8,NULL,false,NULL,NULL),
+ (41,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5066667',NULL,NULL,N'BC Assessment',false,0,N'BCA',NULL,8,NULL,false,NULL,NULL),
+ (42,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5066667',NULL,NULL,N'Provincial Rental Housing Corporation',false,0,N'PRHC',NULL,8,NULL,false,NULL,NULL),
+ (50,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5100000',NULL,NULL,N'Insurance Coporation of BC',false,0,N'ICBC',NULL,11,NULL,false,NULL,NULL),
+ (51,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5100000',NULL,NULL,N'BC Liquor Distribution Branch',false,0,N'LDB',NULL,11,NULL,false,NULL,NULL),
+ (70,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5100000',NULL,NULL,N'Capital Management Branch',false,0,N'CMB',NULL,4,NULL,false,NULL,NULL),
+ (80,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'British Colubmia Institute of Technology',false,0,N'BCIT',NULL,1,NULL,false,NULL,NULL),
+ (81,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Camosun College',false,0,N'CAMC',NULL,1,NULL,false,NULL,NULL),
+ (82,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Capilano University',false,0,N'CAPU',NULL,1,NULL,false,NULL,NULL),
+ (83,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'College of New Caledonia',false,0,N'CNC',NULL,1,NULL,false,NULL,NULL),
+ (84,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'College of the Rockies',false,0,N'CROCK',NULL,1,NULL,false,NULL,NULL),
+ (85,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Douglas College',false,0,N'DC',NULL,1,NULL,false,NULL,NULL),
+ (86,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Emily Carr University of Art and Design',false,0,N'ECUAD',NULL,1,NULL,false,NULL,NULL),
+ (87,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Justice Institute of BC',false,0,N'JIBC',NULL,1,NULL,false,NULL,NULL),
+ (88,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Kwantlen Polytechnic',false,0,N'KP',NULL,1,NULL,false,NULL,NULL),
+ (89,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Langara College',false,0,N'LC',NULL,1,NULL,false,NULL,NULL),
+ (90,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Nichola Valley Institute of Technology',false,0,N'NVIT',NULL,1,NULL,false,NULL,NULL),
+ (91,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Northern Lights College',false,0,N'NLC',NULL,1,NULL,false,NULL,NULL),
+ (92,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Coast Mountain College',false,0,N'CMC',NULL,1,NULL,false,NULL,NULL),
+ (93,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Okanagan College',false,0,N'OC',NULL,1,NULL,false,NULL,NULL),
+ (94,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Selkirk College',false,0,N'SC',NULL,1,NULL,false,NULL,NULL),
+ (95,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Simon Fraser University',false,0,N'SFU',NULL,1,NULL,false,NULL,NULL),
+ (96,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Thompson Rivers University',false,0,N'TRU',NULL,1,NULL,false,NULL,NULL),
+ (97,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'University of BC',false,0,N'UBC',NULL,1,NULL,false,NULL,NULL),
+ (98,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'University of the Fraser Valley',false,0,N'UFV',NULL,1,NULL,false,NULL,NULL),
+ (99,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'University of Northern BC',false,0,N'UNBC',NULL,1,NULL,false,NULL,NULL),
+ (100,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'University of Victoria',false,0,N'UVIC',NULL,1,NULL,false,NULL,NULL),
+ (101,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Vancouver Community College',false,0,N'VCC',NULL,1,NULL,false,NULL,NULL),
+ (102,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5133333',NULL,NULL,N'Vancouver Island University',false,0,N'VIU',NULL,1,NULL,false,NULL,NULL),
+ (110,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5166667',NULL,NULL,N'Real Property Division',false,0,N'RPD',NULL,2,NULL,false,NULL,NULL),
+ (120,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5166667',NULL,NULL,N'Crown Land Opportunities',false,0,N'CLO',NULL,6,NULL,false,NULL,NULL),
+ (131,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5166667',NULL,NULL,N'BC Transit',false,0,N'BCT',NULL,9,NULL,false,NULL,NULL),
+ (132,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5166667',NULL,NULL,N'BC Transportation Financing Authority',false,0,N'BCTFA',NULL,9,NULL,false,NULL,NULL),
+ (140,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5166667',NULL,NULL,N'BC Pavillion Corporation',false,0,N'PAVCO',NULL,13,NULL,false,NULL,NULL),
+ (150,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 6 Rocky Mountain',false,0,N'SD 06',N'',4,NULL,false,NULL,NULL),
+ (151,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 19 Revelstoke',false,0,N'SD 19',N'',4,NULL,false,NULL,NULL),
+ (152,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 20 Kootenay-Columbia',false,0,N'SD 20',N'',4,NULL,false,NULL,NULL),
+ (153,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 22 Vernon',false,0,N'SD 22',N'',4,NULL,false,NULL,NULL),
+ (154,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 23 Central Okanagan',false,0,N'SD 23',N'',4,NULL,false,NULL,NULL),
+ (155,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 28 Quesnel',false,0,N'SD 28',N'',4,NULL,false,NULL,NULL),
+ (156,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 33 Chilliwack',false,0,N'SD 33',N'',4,NULL,false,NULL,NULL),
+ (157,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 35 Langley',false,0,N'SD 35',N'',4,NULL,false,NULL,NULL),
+ (158,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 36 Surrey',false,0,N'SD 36',N'',4,NULL,false,NULL,NULL),
+ (159,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 37 Delta',false,0,N'SD 37',N'',4,NULL,false,NULL,NULL),
+ (160,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 38 Richmond',false,0,N'SD 38',N'',4,NULL,false,NULL,NULL),
+ (161,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 40 New Westminster',false,0,N'SD 40',N'',4,NULL,false,NULL,NULL),
+ (162,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 41 Burnaby',false,0,N'SD 41',N'',4,NULL,false,NULL,NULL),
+ (163,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 42 Maple Ridge-Pitt Meadows',false,0,N'SD 42',N'',4,NULL,false,NULL,NULL),
+ (164,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 43 Coquitlam',false,0,N'SD 43',N'',4,NULL,false,NULL,NULL),
+ (165,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 44 North Vancouver',false,0,N'SD 44',N'',4,NULL,false,NULL,NULL),
+ (166,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 47 Powell River',false,0,N'SD 47',N'',4,NULL,false,NULL,NULL),
+ (167,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 62 Sooke',false,0,N'SD 62',N'',4,NULL,false,NULL,NULL),
+ (168,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 63 Saanich',false,0,N'SD 63',N'',4,NULL,false,NULL,NULL),
+ (169,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 68 Nanaimo-Ladysmith',false,0,N'SD 68',N'',4,NULL,false,NULL,NULL),
+ (170,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 70 Alberni',false,0,N'SD 70',N'',4,NULL,false,NULL,NULL),
+ (171,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 71 Comox Valley',false,0,N'SD 71',N'',4,NULL,false,NULL,NULL),
+ (172,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 72 Campbell River',false,0,N'SD 72',N'',4,NULL,false,NULL,NULL),
+ (173,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 73 Kamloops/Thompson',false,0,N'SD 73',N'',4,NULL,false,NULL,NULL),
+ (174,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 75 Mission',false,0,N'SD 75',N'',4,NULL,false,NULL,NULL),
+ (175,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 78 Fraser-Cascade',false,0,N'SD 78',N'',4,NULL,false,NULL,NULL),
+ (176,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 79 Cowichan Valley',false,0,N'SD 79',N'',4,NULL,false,NULL,NULL),
+ (177,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 83 North Okanagan-Shuswap',false,0,N'SD 83',N'',4,NULL,false,NULL,NULL),
+ (178,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:40.9166667',NULL,NULL,N'School District 91 Nechako Lakes',false,0,N'SD 91',N'',4,NULL,false,NULL,NULL),
+ (179,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 7 Nelson',true,0,N'SD 07',N'Changed to 8 Kootenay Lake',4,NULL,false,NULL,NULL),
+ (180,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 8 Kootenay Lake',false,0,N'SD 08',N'',4,NULL,false,NULL,NULL),
+ (181,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 9 Castlegar',true,0,N'SD 09',N'Changed to 20 Kootenay-Columbia',4,NULL,false,NULL,NULL),
+ (182,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 10 Arrow Lakes',false,0,N'SD 10',N'',4,NULL,false,NULL,NULL),
+ (183,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 11 Trail',true,0,N'SD 11',N'Changed to 20 Kootenay-Columbia',4,NULL,false,NULL,NULL),
+ (184,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 12 Grand Forks',true,0,N'SD 12',N'Changed to 51 Boundary',4,NULL,false,NULL,NULL),
+ (185,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 13 Kettle Valley',true,0,N'SD 13',N'Changed to 51 Boundary',4,NULL,false,NULL,NULL),
+ (186,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 14 Southern Okanagan',true,0,N'SD 14',N'Changed to 53 Okanagan Similkameen',4,NULL,false,NULL,NULL),
+ (187,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 15 Penticton',true,0,N'SD 15',N'Changed to 67 Okanagan Skaha',4,NULL,false,NULL,NULL),
+ (188,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 16 Keremeos',true,0,N'SD 16',N'Changed to 53 Okanagan Similkameen',4,NULL,false,NULL,NULL),
+ (189,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 17 Princeton',true,0,N'SD 17',N'Changed to 58 Nicola-Similkameen',4,NULL,false,NULL,NULL),
+ (190,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 18 Golden',true,0,N'SD 18',N'Changed to 6 Rocky Mountain',4,NULL,false,NULL,NULL),
+ (191,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 1 Fernie',true,0,N'SD 01',N'Changed to 5 Southeast Kootenay',4,NULL,false,NULL,NULL),
+ (192,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 2 Cranbrook',true,0,N'SD 02',N'Changed to 5 Southeast Kootenay',4,NULL,false,NULL,NULL),
+ (193,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 3 Kimberley',true,0,N'SD 03',N'Changed to 6 Rocky Mountain',4,NULL,false,NULL,NULL),
+ (194,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 4 Windermere',true,0,N'SD 04',N'Changed to 6 Rocky Mountain',4,NULL,false,NULL,NULL),
+ (195,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 5 Southeast Kootenay',false,0,N'SD 05',N'',4,NULL,false,NULL,NULL),
+ (196,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 24 Kamloops',true,0,N'SD 24',N'Changed to 73 Kamloops/Thompson',4,NULL,false,NULL,NULL),
+ (197,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 25',false,0,N'SD 25',N'',4,NULL,false,NULL,NULL),
+ (198,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 26 North Thompson',true,0,N'SD 26',N'Changed to 73 Kamloops/Thompson',4,NULL,false,NULL,NULL),
+ (199,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 27 Cariboo-Chilcotin',false,0,N'SD 27',N'',4,NULL,false,NULL,NULL),
+ (200,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 29 Lillooet',true,0,N'SD 29',N'Changed to 74 Gold Trail',4,NULL,false,NULL,NULL),
+ (201,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 30 South Cariboo',true,0,N'SD 30',N'Changed to 74 Gold Trail',4,NULL,false,NULL,NULL),
+ (202,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 31 Merritt',true,0,N'SD 31',N'Changed to 58 Nicola-Similkameen',4,NULL,false,NULL,NULL),
+ (203,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 32 Hope',true,0,N'SD 32',N'Changed to 78 Fraser-Cascade',4,NULL,false,NULL,NULL),
+ (204,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 34 Abbotsford',false,0,N'SD 34',N'',4,NULL,false,NULL,NULL),
+ (205,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 39 Vancouver',false,0,N'SD 39',N'',4,NULL,false,NULL,NULL),
+ (206,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 21 Armstrong-Spallumcheen',true,0,N'SD 21',N'Changed to 83 North Okanagan-Shuswap',4,NULL,false,NULL,NULL),
+ (207,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 45 West Vancouver',false,0,N'SD 45',N'',4,NULL,false,NULL,NULL),
+ (208,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 46 Sunshine Coast',false,0,N'SD 46',N'',4,NULL,false,NULL,NULL),
+ (209,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 48 Sea to Sky',false,0,N'SD 48',N'',4,NULL,false,NULL,NULL),
+ (210,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 49 Central Coast',false,0,N'SD 49',N'',4,NULL,false,NULL,NULL),
+ (211,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 50 Haida Gwaii',false,0,N'SD 50',N'',4,NULL,false,NULL,NULL),
+ (212,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 51 Boundary',false,0,N'SD 51',N'',4,NULL,false,NULL,NULL),
+ (213,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 52 Prince Rupert',false,0,N'SD 52',N'',4,NULL,false,NULL,NULL),
+ (214,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 53 Okanagan Similkameen',false,0,N'SD 53',N'',4,NULL,false,NULL,NULL),
+ (215,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 54 Bulkley Valley',false,0,N'SD 54',N'',4,NULL,false,NULL,NULL),
+ (216,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 55 Burns Lake',true,0,N'SD 55',N'Changed to 91 Nechako Lakes',4,NULL,false,NULL,NULL),
+ (217,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 56 Nechako',true,0,N'SD 56',N'Changed to 91 Nechako Lakes',4,NULL,false,NULL,NULL),
+ (218,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 57 Prince George',false,0,N'SD 57',N'',4,NULL,false,NULL,NULL),
+ (219,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 58 Nicola-Similkameen',false,0,N'SD 58',N'',4,NULL,false,NULL,NULL),
+ (220,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 59 Peace River South',false,0,N'SD 59',N'',4,NULL,false,NULL,NULL),
+ (221,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 60 Peace River North',false,0,N'SD 60',N'',4,NULL,false,NULL,NULL),
+ (222,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 61 Greater Victoria',false,0,N'SD 61',N'',4,NULL,false,NULL,NULL),
+ (223,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 64 Gulf Islands',false,0,N'SD 64',N'',4,NULL,false,NULL,NULL),
+ (224,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 65 Cowichan',true,0,N'SD 65',N'Changed to 79 Cowichan Valley',4,NULL,false,NULL,NULL),
+ (225,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 66 Lake Cowichan',true,0,N'SD 66',N'Changed to 79 Cowichan Valley',4,NULL,false,NULL,NULL),
+ (226,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 67 Okanagan Skaha',false,0,N'SD 67',N'',4,NULL,false,NULL,NULL),
+ (227,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 69 Qualicum',false,0,N'SD 69',N'',4,NULL,false,NULL,NULL),
+ (228,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 76 Agassiz-Harrison',true,0,N'SD 76',N'Changed to 78 Fraser-Cascade',4,NULL,false,NULL,NULL),
+ (229,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 77 Summerland',true,0,N'SD 77',N'Changed to 67 Okanagan Skaha',4,NULL,false,NULL,NULL),
+ (230,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 80 Kitimat',true,0,N'SD 80',N'Changed to 82 Coast Mountains',4,NULL,false,NULL,NULL),
+ (231,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 81 Fort Nelson',false,0,N'SD 81',N'',4,NULL,false,NULL,NULL),
+ (232,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 82 Coast Mountains',false,0,N'SD 82',N'',4,NULL,false,NULL,NULL),
+ (233,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 84 Vancouver Island West',false,0,N'SD 84',N'',4,NULL,false,NULL,NULL),
+ (234,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 85 Vancouver Island North',false,0,N'SD 85',N'',4,NULL,false,NULL,NULL),
+ (235,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 86 Creston-Kaslo',true,0,N'SD 86',N'Changed to 8 Kootenay Lake',4,NULL,false,NULL,NULL),
+ (236,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 87 Stikine',false,0,N'SD 87',N'',4,NULL,false,NULL,NULL),
+ (237,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 88 Terrace',true,0,N'SD 88',N'Changed to 82 Coast Mountains',4,NULL,false,NULL,NULL),
+ (238,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 89 Shuswap',true,0,N'SD 89',N'Changed to 83 North Okanagan-Shuswap',4,NULL,false,NULL,NULL),
+ (239,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 90',false,0,N'SD 90',N'',4,NULL,false,NULL,NULL),
+ (240,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 92 Nisga''a',false,0,N'SD 92',N'',4,NULL,false,NULL,NULL),
+ (241,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 93 Conseil scolaire francophone',false,0,N'SD 93',N'',4,NULL,false,NULL,NULL),
+ (242,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 94',false,0,N'SD 94',N'',4,NULL,false,NULL,NULL),
+ (243,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 95',false,0,N'SD 95',N'',4,NULL,false,NULL,NULL),
+ (244,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 96',false,0,N'SD 96',N'',4,NULL,false,NULL,NULL),
+ (245,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 97',false,0,N'SD 97',N'',4,NULL,false,NULL,NULL),
+ (246,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 98',false,0,N'SD 98',N'',4,NULL,false,NULL,NULL),
+ (247,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 99',false,0,N'SD 99',N'',4,NULL,false,NULL,NULL),
+ (248,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 100',false,0,N'SD 100',N'',4,NULL,false,NULL,NULL),
+ (249,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:47.5733333',NULL,NULL,N'School District 74 Gold Trail',false,0,N'SD 74',N'',4,NULL,false,NULL,NULL),
+ (250,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Agriculture, Food and Fisheries',false,0,N'AGRI',N'',NULL,N'wes.boyd@gov.bc.ca',true,N'Wes',NULL),
+ (251,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Attorney General',false,0,N'AG',N'',NULL,N'tracy.campbell@gov.bc.ca',true,N'Tracy',NULL),
+ (252,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Children & Family Development',false,0,N'MCF',N'',NULL,N'rob.byers@gov.bc.ca',true,N'Rob',NULL),
+ (253,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Energy, Mines and Low Carbon Innovation',false,0,N'EMPR',N'',NULL,N'wes.boyd@gov.bc.ca',true,N'Wes',NULL),
+ (254,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Environment & Climate Change Strategy',false,0,N'ENV',N'',NULL,N'wes.boyd@gov.bc.ca',true,N'Wes',NULL),
+ (255,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Forests, Lands, Natural Resource Operations & Rural Development',false,0,N'FLNRD',N'',NULL,N'trish.dohan@gov.bc.ca',true,N'Trish',NULL),
+ (256,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Indigenous Relations & Reconciliation',false,0,N'IRR',N'',NULL,N'wes.boyd@gov.bc.ca',true,N'Wes',NULL),
+ (257,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Jobs, Economic Recovery and Innovation',false,0,N'JERI',N'',NULL,N'joanna.white@gov.bc.ca',true,N'Joanna',NULL),
+ (258,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Labour',false,0,N'LBR',N'',NULL,N'joanna.white@gov.bc.ca',true,N'Joanna',NULL),
+ (259,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Municipal Affairs',false,0,N'MUNI',N'',NULL,N'david.curtis@gov.bc.ca',true,N'David',NULL),
+ (260,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Public Safety & Solicitor General & Emergency B.C.',false,0,N'PSSG',N'',NULL,N'tracy.campbell@gov.bc.ca',true,N'Tracy',NULL),
+ (261,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:50.8300000',NULL,NULL,N'Tourism, Arts, Culture and Sport',false,0,N'TACS',N'',NULL,N'joanna.white@gov.bc.ca',true,N'Joanna',NULL),
+ (262,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:25.4208339',NULL,NULL,N'Northwest Community College',false,0,N'NCC',NULL,1,NULL,true,NULL,NULL),
+ (263,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:30.6337046',NULL,NULL,N'Emily Carr University of Art & Design',false,0,N'ECUOA&',NULL,1,NULL,true,NULL,NULL),
+ (264,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:32.2189301',NULL,NULL,N'Kwantlen Polytechnic University',false,0,N'KPU',NULL,1,NULL,true,NULL,NULL),
+ (265,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:38.5403798',NULL,NULL,N'North Island College',false,0,N'NIC',NULL,1,NULL,true,NULL,NULL),
+ (266,'00000000-0000-0000-0000-000000000000','2023-01-17 18:04:32.1983530',NULL,NULL,N'Capital Planning Branch',false,0,N'CPB',NULL,4,NULL,true,NULL,NULL),
+ (267,'00000000-0000-0000-0000-000000000000','2023-01-17 18:07:47.0280727',NULL,NULL,N'LNG, Crown Land Opportunities and Restoration',false,0,N'LCLOAR',NULL,6,NULL,true,NULL,NULL),
+ (268,'00000000-0000-0000-0000-000000000000','2023-01-17 18:11:16.2168252',NULL,NULL,N'Transportation Investment Corporation',false,0,N'TIC',NULL,9,NULL,true,NULL,NULL),
+ (269,'00000000-0000-0000-0000-000000000000','2023-01-17 18:11:16.9530326',NULL,NULL,N'Properties & Land Management Branch',false,0,N'P&LMB',NULL,9,NULL,true,NULL,NULL),
+ (270,'00000000-0000-0000-0000-000000000000','2023-01-17 18:11:23.1283101',NULL,NULL,N'Insurance Corporation of BC',false,0,N'ICBC',NULL,9,NULL,true,NULL,NULL),
+ (1268,'00000000-0000-0000-0000-000000000000','2023-11-08 18:59:11.7020315',NULL,NULL,N'British Columbia Institute of Technology',false,0,N'BCIOT',NULL,1,NULL,true,NULL,NULL);
\ No newline at end of file
diff --git a/express-api/src/typeorm/Migrations/Seeds/BuildingConstructionTypes.sql b/express-api/src/typeorm/Migrations/Seeds/BuildingConstructionTypes.sql
new file mode 100644
index 000000000..cd84f5d61
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/Seeds/BuildingConstructionTypes.sql
@@ -0,0 +1,6 @@
+INSERT INTO "building_construction_type" ("Id","CreatedById","CreatedOn","UpdatedById","UpdatedOn","Name","IsDisabled","SortOrder") VALUES
+ (0,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5933333',NULL,NULL,N'Concrete',false,0),
+ (1,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5933333',NULL,NULL,N'Masonry',false,0),
+ (2,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5933333',NULL,NULL,N'Mixed',false,0),
+ (3,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5933333',NULL,NULL,N'Steel',false,0),
+ (4,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.5933333',NULL,NULL,N'Wood',false,0);
diff --git a/express-api/src/typeorm/Migrations/Seeds/BuildingOccupantTypes.sql b/express-api/src/typeorm/Migrations/Seeds/BuildingOccupantTypes.sql
new file mode 100644
index 000000000..aeeaa0706
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/Seeds/BuildingOccupantTypes.sql
@@ -0,0 +1,4 @@
+INSERT INTO "building_occupant_type" ("Id","CreatedById","CreatedOn","UpdatedById","UpdatedOn","Name","IsDisabled","SortOrder") VALUES
+ (0,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6033333',NULL,NULL,N'Leased',false,0),
+ (1,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6033333',NULL,NULL,N'Occupied By Owning Ministry',false,0),
+ (2,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6033333',NULL,NULL,N'Unoccupied',false,0);
diff --git a/express-api/src/typeorm/Migrations/Seeds/BuildingPredominateUses.sql b/express-api/src/typeorm/Migrations/Seeds/BuildingPredominateUses.sql
new file mode 100644
index 000000000..4e68e81e0
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/Seeds/BuildingPredominateUses.sql
@@ -0,0 +1,55 @@
+INSERT INTO "building_predominate_use" ("Id","CreatedById","CreatedOn","UpdatedById","UpdatedOn","Name","IsDisabled","SortOrder") VALUES
+ (0,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Religious',false,0),
+ (1,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Research & Development Facility',false,0),
+ (2,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Residential Detached',false,0),
+ (3,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Residential Multi',false,0),
+ (4,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Retail',false,0),
+ (5,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Senior Housing (Assisted Living / Skilled Nursing)',false,0),
+ (6,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Shelters / Orphanages / Children’s Homes / Halfway Homes',false,0),
+ (7,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Social Assistance Housing',false,0),
+ (8,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Storage',false,0),
+ (9,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Storage Vehicle',false,0),
+ (10,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Trailer Office',false,0),
+ (11,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Trailer Other',false,0),
+ (13,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Transportation (Airport / Rail / Bus station)',false,0),
+ (15,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Warehouse',false,0),
+ (16,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.6166667',NULL,NULL,N'Weigh Station',false,0),
+ (17,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:44.5466667',NULL,NULL,N'Marina',false,0),
+ (18,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:24.7491538',NULL,NULL,N'University/College',false,0),
+ (19,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:25.3050964',NULL,NULL,N'Dormitory/Residence Halls',false,0),
+ (20,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:28.1865330',NULL,NULL,N'Child Care',false,0),
+ (21,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:28.8726764',NULL,NULL,N'Maintenance',false,0),
+ (22,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:30.2011863',NULL,NULL,N'Mixed Use',false,0),
+ (23,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:31.1432825',NULL,NULL,N'Training Centre',false,0),
+ (24,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:38.2219136',NULL,NULL,N'Recreation',false,0),
+ (25,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:38.2983213',NULL,NULL,N'Library',false,0),
+ (26,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:49.5223861',NULL,NULL,N'Office',false,0),
+ (27,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:50.0348829',NULL,NULL,N'Site Service',false,0),
+ (28,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:50.7027666',NULL,NULL,N'Distribution Centre',false,0),
+ (29,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:51.3012011',NULL,NULL,N'Industrial',false,0),
+ (30,'00000000-0000-0000-0000-000000000000','2023-01-17 18:01:51.5474517',NULL,NULL,N'Laboratory',false,0),
+ (31,'00000000-0000-0000-0000-000000000000','2023-01-17 18:02:00.2963618',NULL,NULL,N'Kiosk',false,0),
+ (32,'00000000-0000-0000-0000-000000000000','2023-01-17 18:02:01.8872394',NULL,NULL,N'Food Services',false,0),
+ (33,'00000000-0000-0000-0000-000000000000','2023-01-17 18:02:03.1687303',NULL,NULL,N'Hall',false,0),
+ (34,'00000000-0000-0000-0000-000000000000','2023-01-17 18:02:03.6435498',NULL,NULL,N'Museum',false,0),
+ (35,'00000000-0000-0000-0000-000000000000','2023-01-17 18:02:29.1705336',NULL,NULL,N'Parking',false,0),
+ (36,'00000000-0000-0000-0000-000000000000','2023-01-17 18:02:30.6208403',NULL,NULL,N'Community/Recreation Centre',false,0),
+ (37,'00000000-0000-0000-0000-000000000000','2023-01-17 18:02:39.0376952',NULL,NULL,N'Hospital',false,0),
+ (38,'00000000-0000-0000-0000-000000000000','2023-01-17 18:02:58.5941650',NULL,NULL,N'Transportation (Airport/Rail/Bus station)',false,0),
+ (39,'00000000-0000-0000-0000-000000000000','2023-01-17 18:04:32.2567083',NULL,NULL,N'K-12 School',false,0),
+ (40,'00000000-0000-0000-0000-000000000000','2023-01-17 18:07:55.1264667',NULL,NULL,N'Health Unit',false,0),
+ (41,'00000000-0000-0000-0000-000000000000','2023-01-17 18:07:56.0627658',NULL,NULL,N'Senior Housing (Assisted Living/Skilled Nursing)',false,0),
+ (42,'00000000-0000-0000-0000-000000000000','2023-01-17 18:08:00.9567361',NULL,NULL,N'Rehabilitation Centre',false,0),
+ (43,'00000000-0000-0000-0000-000000000000','2023-01-17 18:08:06.1961346',NULL,NULL,N'Clinic',false,0),
+ (44,'00000000-0000-0000-0000-000000000000','2023-01-17 18:08:16.4420526',NULL,NULL,N'Ambulance',false,0),
+ (45,'00000000-0000-0000-0000-000000000000','2023-01-17 18:08:43.4715034',NULL,NULL,N'Acute Care',false,0),
+ (46,'00000000-0000-0000-0000-000000000000','2023-01-17 18:08:48.0833994',NULL,NULL,N'Behavioral Care',false,0),
+ (47,'00000000-0000-0000-0000-000000000000','2023-01-17 18:09:03.2648384',NULL,NULL,N'Jail/Prison',false,0),
+ (48,'00000000-0000-0000-0000-000000000000','2023-01-17 18:09:03.5189282',NULL,NULL,N'Courthouse',false,0),
+ (49,'00000000-0000-0000-0000-000000000000','2023-01-17 18:09:08.7075725',NULL,NULL,N'Mobile Home',false,0),
+ (50,'00000000-0000-0000-0000-000000000000','2023-01-17 18:09:52.3408321',NULL,NULL,N'Animal Shelter',false,0),
+ (51,'00000000-0000-0000-0000-000000000000','2023-01-17 18:10:32.6877359',NULL,NULL,N'Farm Storage',false,0),
+ (52,'00000000-0000-0000-0000-000000000000','2023-01-17 18:11:23.2100479',NULL,NULL,N'Commercial',false,0),
+ (53,'00000000-0000-0000-0000-000000000000','2023-01-17 18:11:32.5876069',NULL,NULL,N'Convention Centre',false,0),
+ (54,'00000000-0000-0000-0000-000000000000','2023-11-08 18:59:11.7340836',NULL,NULL,N'University / College',false,0),
+ (55,'00000000-0000-0000-0000-000000000000','2023-11-08 18:59:12.3995835',NULL,NULL,N'Dormitory / Residence Halls',false,0);
diff --git a/express-api/src/typeorm/Migrations/Seeds/EvaluationKeys.sql b/express-api/src/typeorm/Migrations/Seeds/EvaluationKeys.sql
new file mode 100644
index 000000000..f1f6f2d67
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/Seeds/EvaluationKeys.sql
@@ -0,0 +1,4 @@
+INSERT INTO "evaluation_key" ("Id","CreatedById","CreatedOn","UpdatedById","UpdatedOn","Name","Description") VALUES
+ (1, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:34.7500000',NULL, NULL, N'Assessed', NULL),
+ (2, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:34.7500000',NULL, NULL, N'Appraised', NULL),
+ (3, '00000000-0000-0000-0000-000000000000', '2023-01-17 17:58:34.7500000',NULL, NULL, N'Improvements', NULL);
diff --git a/express-api/src/typeorm/Migrations/Seeds/FiscalKeys.sql b/express-api/src/typeorm/Migrations/Seeds/FiscalKeys.sql
new file mode 100644
index 000000000..9a5950729
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/Seeds/FiscalKeys.sql
@@ -0,0 +1,3 @@
+INSERT INTO "fiscal_key" ("Id","CreatedById","CreatedOn","UpdatedById","UpdatedOn","Name", "Description") VALUES
+ (1,'00000000-0000-0000-0000-000000000000','2023-01-06 16:58:34.7500000',NULL,NULL,N'NetBook',NULL),
+ (2,'00000000-0000-0000-0000-000000000000','2023-01-06 16:58:34.7500000',NULL,NULL,N'Market',NULL);
diff --git a/express-api/src/typeorm/Migrations/Seeds/NotificationTemplates.sql b/express-api/src/typeorm/Migrations/Seeds/NotificationTemplates.sql
new file mode 100644
index 000000000..46c5f399f
--- /dev/null
+++ b/express-api/src/typeorm/Migrations/Seeds/NotificationTemplates.sql
@@ -0,0 +1,821 @@
+INSERT INTO "notification_template" ("Id","CreatedById","CreatedOn","UpdatedById","UpdatedOn","Name","Description","To","Cc","Bcc","Audience","Encoding","BodyType","Priority","Subject","Body","IsDisabled","Tag") VALUES
+ (1,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'New Disposal Project Submitted',N'Inform SRES a new project has been submitted for assessment.',N'RealPropertyDivision.Disposals@gov.bc.ca',N'',N'',N'Default',N'Utf8',N'Html',N'High',N'New Disposal Project Submitted - @Model.Project.ProjectNumber','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
Good afternoon,
+
This email is to advise that the following properties have been submitted to the Surplus Property Program to be reviewed as surplus by the current holder of the property and is requesting your review:
Your project @Model.Project.ProjectNumber has been cancelled. Signin to PIMS to review the reason.
+
Sincerely Real Property Division
+
+',false,N'SPP'),
+ (4,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'Disposal Project Approved for ERP',N'Inform owning agency their project has been approved and properties will be added to ERP.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ProjectOwner',N'Utf8',N'Html',N'Normal',N'Disposal Project Approved for ERP - @Model.Project.ProjectNumber','
+@using System.Linq
+@using Pims.Dal.Entities
+
+@Model.Environment.Title
+
+
Good morning / Good afternoon,
+
Your project @Model.Project.ProjectNumber has been approved. Signin to PIMS to review the progress.
+
+',false,N'ERP'),
+ (5,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'New Properties on ERP',N'Inform agencies of new properties added to ERP.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ParentAgencies',N'Utf8',N'Html',N'High',N'ACTION REQUIRED - Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
Please forward this notification to any SUCH Sector Organization or BPS Entity that your ministry is responsible for to ensure any interest from Ministries or Agencies in the properties is identified.
+
Should there be no interest in the property detailed below from your Ministry or any SUCH Sector Organization or BPS Entity that your Ministry is responsible for, please respond to confirm.
+
This email is to advise that the following property has been identified as surplus by the current holder of the property and is available for redeployment if there is a need by your Ministry, SUCH Sector Organization or BPS Entity:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
+ }
+
+
+
Your Ministry, SUCH Sector Organization or BPS Entity is provided with 90 days from this notification to submit a Business Case to SRES expressing your interest in acquiring the surplus property. Reminder notifications will be sent at both 30 days and 60 days from this initial notification.
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (6,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'30 day ERP notification - Owning Agency',N'ERP 30 expiration notification to inform owning agency of time remaining in ERP',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ProjectOwner',N'Utf8',N'Html',N'Normal',N'Notification of Surplus Real Property - 30 Day Reminder Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is a 30 Day Reminder Notification as detailed in the initial Notification of Surplus Real Property below.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
This email is to advise that the following property have been identified as surplus is available for redeployment:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (7,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'60 day ERP notification - Owning Agency',N'ERP 60 expiration notification to inform owning agency of time remaining in ERP',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ProjectOwner',N'Utf8',N'Html',N'Normal',N'Notification of Surplus Real Property - 60 Day Reminder Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is a 60 Day Reminder Notification as detailed in the initial Notification of Surplus Real Property below.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
This email is to advise that the following property have been identified as surplus is available for redeployment:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (8,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'90 day ERP notification - Owning Agency',N'ERP 90 expiration notification to inform owning agency ERP is complete.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ProjectOwner',N'Utf8',N'Html',N'Normal',N'Notification of Surplus Real Property - Completion of 90 Day Enhanced Referral Period for Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is the 90 Day Completion Reminder Notification as detailed in the initial Notification of Surplus Real Property below.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
This email is to advise that the following property have been identified as surplus is available for redeployment:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (9,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'30 day ERP notification - Parent Agencies',N'ERP 30 notification to inform agencies or properties available in ERP.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ParentAgencies',N'Utf8',N'Html',N'High',N'ACTION REQUIRED - Notification of Surplus Real Property - 30 Day Reminder Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is a 30 Day Reminder Notification as detailed in the initial Notification of Surplus Real Property below.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
Please forward this notification to any SUCH Sector Organization or BPS Entity that your ministry is responsible for to ensure any interest from Ministries or Agencies in the properties is identified.
+
Should there be no interest in the property detailed below from your Ministry or any SUCH Sector Organization or BPS Entity that your Ministry is responsible for, please respond to confirm.
+
This email is to advise that the following property has been identified as surplus by the current holder of the property and is available for redeployment if there is a need by your Ministry, SUCH Sector Organization or BPS Entity:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
+ }
+
+
+
Your Ministry, SUCH Sector Organization or BPS Entity is provided with 60 days from this notification to submit a Business Case to SRES expressing your interest in acquiring the surplus property. An additional reminder notification will be sent 30 days before this internal listing expires.
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (10,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'60 day ERP notification - Parent Agencies',N'ERP 60 notification to inform agencies or properties available in ERP.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ParentAgencies',N'Utf8',N'Html',N'High',N'ACTION REQUIRED - Notification of Surplus Real Property - 60 Day Reminder Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is a 60 Day Reminder Notification as detailed in the initial Notification of Surplus Real Property below.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
Please forward this notification to any SUCH Sector Organization or BPS Entity that your ministry is responsible for to ensure any interest from Ministries or Agencies in the properties is identified.
+
Should there be no interest in the property detailed below from your Ministry or any SUCH Sector Organization or BPS Entity that your Ministry is responsible for, please respond to confirm.
+
This email is to advise that the following property has been identified as surplus by the current holder of the property and is available for redeployment if there is a need by your Ministry, SUCH Sector Organization or BPS Entity:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
+ }
+
+
+
Your Ministry, SUCH Sector Organization or BPS Entity is provided with 30 days from this notification to submit a Business Case to SRES expressing your interest in acquiring the surplus property.
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (11,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'90 day ERP notification - Parent Agencies',N'ERP 90 expiration notification to inform agencies.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ParentAgencies',N'Utf8',N'Html',N'High',N'ACTION REQUIRED - Notification of Surplus Real Property - Completion of 90 Day Enhanced Referral Period for Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is to provide confirmation that the 90 Day Enhanced Referral Period as detailed in the initial Notification of Surplus Real Property below is now complete.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
Please forward this notification to any SUCH Sector Organization or BPS Entity that your ministry is responsible for to ensure any interest from Ministries or Agencies in the properties is identified.
+
Should there be no interest in the property detailed below from your Ministry or any SUCH Sector Organization or BPS Entity that your Ministry is responsible for, please respond to confirm.
+
This email is to advise that the following property has been identified as surplus by the current holder of the property and is available for redeployment if there is a need by your Ministry, SUCH Sector Organization or BPS Entity:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
+ }
+
+
+
An Enhanced Referral Notification Letter will be sent to the owning Ministry / Agency at the end of the week advising of next steps in the process.
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (12,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'30 day ERP notification - Purchasing Agencies',N'ERP 30 notification to inform purchasing agencies to submit business case.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'WatchingAgencies',N'Utf8',N'Html',N'Normal',N'ACTION REQUIRED - Notification of Surplus Real Property - 30 Day Reminder Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is a 30 Day Reminder Notification as detailed in the initial Notification of Surplus Real Property below.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
This email is to advise that the following property has been identified as surplus by the current holder of the property and is available for redeployment if there is a need by your Ministry, SUCH Sector Organization or BPS Entity:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
+ }
+
+
+
Your Ministry, SUCH Sector Organization or BPS Entity is provided with 60 days from this notification to submit a Business Case to SRES expressing your interest in acquiring the surplus property.
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (13,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'60 day ERP notification - Purchasing Agencies',N'ERP 60 notification to inform purchasing agencies to submit business case.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'WatchingAgencies',N'Utf8',N'Html',N'Normal',N'ACTION REQUIRED - Notification of Surplus Real Property - 60 Day Reminder Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is a 60 Day Reminder Notification as detailed in the initial Notification of Surplus Real Property below.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
This email is to advise that the following property has been identified as surplus by the current holder of the property and is available for redeployment if there is a need by your Ministry, SUCH Sector Organization or BPS Entity:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
+ }
+
+
+
Your Ministry, SUCH Sector Organization or BPS Entity is provided with 30 days from this notification to submit a Business Case to SRES expressing your interest in acquiring the surplus property.
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (14,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'90 day ERP notification - Purchasing Agencies',N'ERP 90 expiration notification to inform purchasing agencies to submit business case.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'WatchingAgencies',N'Utf8',N'Html',N'Normal',N'ACTION REQUIRED - Notification of Surplus Real Property - Completion of 90 Day Enhanced Referral Period for Notification of Surplus Real Property','
+@using System.Linq
+@using Pims.Dal.Entities
+@using System.Globalization
+
+@Model.Environment.Title
+
@(Model.ToAgency.AddressTo ?? "Good morning / Good afternoon"),
+
This email is to provide confirmation that the 90 Day Enhanced Referral Period as detailed in the initial Notification of Surplus Real Property below is now complete.
+
As detailed in the Surplus Properties Program Process Manual, the Strategic Real Estate Services Branch (SRES) has committed to proactively notifying all other Ministries, SUCH Sector Organization and Broader Public Sector (BPS) Entities of the availability of a new surplus property.
+
This email is to advise that the following property has been identified as surplus by the current holder of the property and is available for redeployment if there is a need by your Ministry, SUCH Sector Organization or BPS Entity:
+
@Model.Project.ProjectNumber :
+
+
+ @foreach (var property in Model.Project.Properties)
+ {
+
+ @if (property.PropertyType == PropertyTypes.Land)
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Parcel.Address.ToString()
+ Site Description: @property.Parcel.Name
+ Site Size: @property.Parcel.LandArea ha
+ Zoned: @property.Parcel.Zoning
+ PID: @property.Parcel.ParcelIdentity
+ Legal: @property.Parcel.LandLegalDescription
+ Current Holder of the Property: @property.Parcel.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+ else
+ {
+ var appraised = @Model.Project.Appraised;
+ var assessed = @Model.Project.Assessed;
+
+
+ Site Address: @property.Building.Address.ToString()
+ Site Description: @property.Building.Name
+ Rentable Area: @property.Building.RentableArea Sq. M
+ Building Floors: @property.Building.BuildingFloorCount
+ Predominate Use: @property.Building.BuildingPredominateUse.Name
+ Tenancy: @property.Building.BuildingTenancy
+ Current Holder of the Property: @property.Building.Agency.Name
+ @if (appraised != null && appraised.Value > 0)
+ {
+ Appraised Value: @appraised.Value.ToString("C", new CultureInfo("en-US"))
+ }
+ else if (assessed != null && assessed.Value > 0)
+ {
+ Assessed Value: @assessed.Value.ToString("C", new CultureInfo("en-US"))
+ }
+
+ }
+
+ }
+
+
+
Your Ministry, SUCH Sector Organization or BPS Entity is provided with until the end of this week from this notification to submit a Business Case to SRES expressing your interest in acquiring the surplus property.
+
An Enhanced Referral Notification Letter will be sent to the owning Ministry / Agency at the end of the week advising of next steps in the process.
If you have any questions regarding this matter, please contact Chris Seltenrich A/Executive Director of the Strategic Real Estate Services Branch at 778-698-3195.
+
Thank you.
+
Strategic Real Estate Services, Real Property Division
',false,N'ERP'),
+ (15,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'Access Request',N'A new authenticated user has requested access.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'Default',N'Utf8',N'Html',N'High',N'PIMS - Access Request','
+@Model.Environment.Title
+
Dear Administrator,
@Model.AccessRequest.User.FirstName @Model.AccessRequest.User.LastName has submitted an access request to PIMS.
Signin and review their request.
',false,N'Access Request'),
+ (16,'00000000-0000-0000-0000-000000000000','2023-01-17 17:58:34.7500000',NULL,NULL,N'Project Shared Note Changed',N'The shared note has been updated and the owning agency should be notified.',N'',N'',N'RealPropertyDivision.Disposals@gov.bc.ca',N'ProjectOwner',N'Utf8',N'Html',N'High',N'PIMS - Project Note Updated - @Model.Project.ProjectNumber','
+@using System.Linq
+@using Pims.Dal.Entities
+@Model.Environment.Title
+