diff --git a/cvs-nop b/cvs-nop index 2ca75ad1..f1de3909 160000 --- a/cvs-nop +++ b/cvs-nop @@ -1 +1 @@ -Subproject commit 2ca75ad188c7ad11c98bb358eb48f3d27c645568 +Subproject commit f1de39095ab90e1e34d18afa107b1e15cc7b9734 diff --git a/src/models/defects.ts b/src/models/defects.ts index 4ff4873b..91b0b4cd 100644 --- a/src/models/defects.ts +++ b/src/models/defects.ts @@ -1,5 +1,6 @@ import { DynamoDbImage } from '../services/dynamodb-images'; import { Maybe } from './optionals'; +import { Medias, parseMedias } from './medias'; export type DeficiencyCategory = 'advisory' | 'dangerous' | 'major' | 'minor'; @@ -42,6 +43,7 @@ export interface Defect { stdForProhibition?: boolean; prs?: boolean; prohibitionIssued?: boolean; + medias: Medias; } export type CustomDefects = CustomDefect[]; @@ -98,6 +100,7 @@ export const parseDefect = (image: DynamoDbImage): Defect => ({ stdForProhibition: image.getBoolean('stdForProhibition'), prs: image.getBoolean('prs'), prohibitionIssued: image.getBoolean('prohibitionIssued'), + medias: parseMedias(image.getList('media')), }); export const parseDefectAdditionalInformation = ( diff --git a/src/models/medias.ts b/src/models/medias.ts new file mode 100644 index 00000000..7b191e29 --- /dev/null +++ b/src/models/medias.ts @@ -0,0 +1,51 @@ +import { DynamoDbImage } from '../services/dynamodb-images'; + +export type Medias = Media[]; + +export type MediaType = 'failReason' | 'image' | 'video'; + +export interface FailReasonMedia { + path: string; + reason: string; + type: 'failReason'; +} + +export interface FileMedia { + path: string; + type: 'image' | 'video'; +} + +export type Media = FailReasonMedia | FileMedia; + +export const parseMedias = (image?: DynamoDbImage): Medias => { + if (!image) { + return [] as Medias; + } + + const medias: Medias = []; + + for (const key of image.getKeys()) { + medias.push(parseMedia(image.getMap(key)!)); + } + + return medias; +}; + +export const parseMedia = ( + image: DynamoDbImage, +): Media => { + const type = image.getString('type')! as MediaType; + + if (type === 'failReason') { + return { + path: image.getString('path')!, + reason: image.getString('reason')!, + type, + }; + } + + return { + path: image.getString('path')!, + type, + }; +}; diff --git a/src/models/test-results.ts b/src/models/test-results.ts index 18dea05d..4415ae6c 100644 --- a/src/models/test-results.ts +++ b/src/models/test-results.ts @@ -9,6 +9,7 @@ import { import { DynamoDbImage, parseStringArray } from '../services/dynamodb-images'; import { debugLog } from '../services/logger'; import { TestStationTypes } from "@dvsa/cvs-type-definitions/types/v1/enums/testStationType.enum"; +import { Medias, parseMedias } from './medias'; export type TestVersion = 'current' | 'archived'; @@ -62,6 +63,7 @@ export interface TestResult { regnDate?: string; firstUseDate?: string; testTypes?: TestTypes; + medias: Medias; } export const parseTestResults = (image?: DynamoDbImage): TestResults => { @@ -129,4 +131,5 @@ export const parseTestResult = (image: DynamoDbImage): TestResult => ({ regnDate: image.getString('regnDate'), firstUseDate: image.getString('firstUseDate'), testTypes: parseTestTypes(image.getList('testTypes')), + medias: parseMedias(image.getList('media')), }); diff --git a/src/services/sql-execution.ts b/src/services/sql-execution.ts index b32d63e8..3e69e745 100644 --- a/src/services/sql-execution.ts +++ b/src/services/sql-execution.ts @@ -6,6 +6,7 @@ import { generateSelectSql, generatePartialUpsertSql, generateSelectRecordIds, + generateSelectRecordIdsBasedOnWhereIn, generateDeleteBasedOnWhereIn, } from './sql-generation'; @@ -114,3 +115,18 @@ export async function selectRecordIds( connection, ); } + +export async function selectRecordIdsBasedOnWhereIn( + targetTableName: string, + targetColumnName: string, + ids: any[], + connection: Connection, +): Promise { + const values: any[] | undefined = Object.values(ids); + + return executeSql( + generateSelectRecordIdsBasedOnWhereIn(targetTableName, targetColumnName, ids), + values, + connection, + ); +} diff --git a/src/services/sql-generation.ts b/src/services/sql-generation.ts index d1e5a6a2..d11979da 100644 --- a/src/services/sql-generation.ts +++ b/src/services/sql-generation.ts @@ -58,6 +58,14 @@ export const generateSelectRecordIds = ( .map(([key]) => `${key}=?`) .join(' AND ')}`; +export const generateSelectRecordIdsBasedOnWhereIn = ( + targetTableName: string, + targetColumnName: string, + conditionAttributes: any[], +): string => `SELECT id FROM ${targetTableName} WHERE ${targetColumnName} IN (${Object.entries( + conditionAttributes, +).map(() => '?')})`; + const generateUpsertSql = ( tableDetails: TableDetails, updatePlaceholders: string[], diff --git a/src/services/table-details.ts b/src/services/table-details.ts index b9f40dd9..832ad57e 100644 --- a/src/services/table-details.ts +++ b/src/services/table-details.ts @@ -365,6 +365,21 @@ export const TEST_RESULT_TABLE: TableDetails = { ], }; +export const MEDIA_TYPE_TABLE: TableDetails = { + tableName: 'media_type', + columnNames: [ 'type' ], +}; + +export const DEFECT_MEDIA_TABLE: TableDetails = { + tableName: 'defect_media', + columnNames: [ 'test_defect_id', 'path', 'reason', 'media_type_id' ], +}; + +export const TEST_RESULT_MEDIA_TABLE: TableDetails = { + tableName: 'test_result_media', + columnNames: [ 'test_result_id', 'path', 'reason', 'media_type_id' ], +}; + export const allTables = (): TableDetails[] => [ VEHICLE_TABLE, MAKE_MODEL_TABLE, @@ -389,4 +404,7 @@ export const allTables = (): TableDetails[] => [ TEST_DEFECT_TABLE, CUSTOM_DEFECT_TABLE, TEST_RESULT_TABLE, + MEDIA_TYPE_TABLE, + DEFECT_MEDIA_TABLE, + TEST_RESULT_MEDIA_TABLE ]; diff --git a/src/services/test-result-record-conversion.ts b/src/services/test-result-record-conversion.ts index ea3a5e24..b8fe004f 100644 --- a/src/services/test-result-record-conversion.ts +++ b/src/services/test-result-record-conversion.ts @@ -11,12 +11,16 @@ import { import { TestType } from '../models/test-types'; import { CUSTOM_DEFECT_TABLE, + DEFECT_MEDIA_TABLE, DEFECTS_TABLE, FUEL_EMISSION_TABLE, IDENTITY_TABLE, LOCATION_TABLE, + MEDIA_TYPE_TABLE, PREPARER_TABLE, + TableDetails, TEST_DEFECT_TABLE, + TEST_RESULT_MEDIA_TABLE, TEST_RESULT_TABLE, TEST_STATION_TABLE, TEST_TYPE_TABLE, @@ -31,11 +35,13 @@ import { executePartialUpsert, executePartialUpsertIfNotExists, selectRecordIds, + selectRecordIdsBasedOnWhereIn, } from './sql-execution'; import { getConnectionPool } from './connection-pool'; import { EntityConverter } from './entity-conversion'; import { debugLog } from './logger'; import { vinCleanser } from '../utils/cleanser'; +import { Medias } from '../models/medias'; export const testResultsConverter = (): EntityConverter => ({ parseRootImage: parseTestResults, @@ -50,6 +56,9 @@ const upsertTestResults = async (testResults: TestResults): Promise => { return; } + debugLog("test result received for insert"); + debugLog(JSON.stringify(testResults)); + const pool = await getConnectionPool(); debugLog(`Upserting ${testResults.length} test results`); @@ -123,8 +132,67 @@ const upsertTestResults = async (testResults: TestResults): Promise => { testResult.lastUpdatedByName!, ); + if (!process.env.DISABLE_DELETE_ON_UPDATE) { + const existingTestResultIds = await selectRecordIds( + TEST_RESULT_TABLE.tableName, + { vehicle_id: vehicleId, testResultId: testResult.testResultId }, + testResultConnection, + ); + if (existingTestResultIds.rows.length > 0) { + await testResultConnection.beginTransaction(); + + const testResultIds = existingTestResultIds.rows.map( + (row: { id: any }) => row.id, + ); + const existingTestDefectIds = await selectRecordIdsBasedOnWhereIn( + TEST_DEFECT_TABLE.tableName, + 'test_result_id', + testResultIds, + testResultConnection, + ); + const testDefectIds = existingTestDefectIds.rows.map( + (row: { id: any }) => row.id, + ); + + if (testDefectIds.length > 0) { + await deleteBasedOnWhereIn( + DEFECT_MEDIA_TABLE.tableName, + 'test_defect_id', + testDefectIds, + testResultConnection, + ); + } + await deleteBasedOnWhereIn( + CUSTOM_DEFECT_TABLE.tableName, + 'test_result_id', + testResultIds, + testResultConnection, + ); + await deleteBasedOnWhereIn( + TEST_DEFECT_TABLE.tableName, + 'test_result_id', + testResultIds, + testResultConnection, + ); + await deleteBasedOnWhereIn( + TEST_RESULT_MEDIA_TABLE.tableName, + 'test_result_id', + testResultIds, + testResultConnection, + ); + await deleteBasedOnWhereIn( + TEST_RESULT_TABLE.tableName, + 'id', + testResultIds, + testResultConnection, + ); + + await testResultConnection.commit(); + } + } + if (!testResult.testTypes || testResult.testTypes.length < 1) { - await executeFullUpsert( + const response = await executeFullUpsert( TEST_RESULT_TABLE, [ vehicleId, @@ -172,43 +240,16 @@ const upsertTestResults = async (testResults: TestResults): Promise => { ], testResultConnection, ); - // eslint-disable-next-line no-continue - continue; - } - if (!process.env.DISABLE_DELETE_ON_UPDATE) { - const existingTestResultIds = await selectRecordIds( - TEST_RESULT_TABLE.tableName, - { vehicle_id: vehicleId, testResultId: testResult.testResultId }, + const testResultRecordId = response.rows.insertId; + await upsertMedias( testResultConnection, + TEST_RESULT_MEDIA_TABLE, + testResultRecordId, + testResult.medias, ); - if (existingTestResultIds.rows.length > 0) { - await testResultConnection.beginTransaction(); - - const testResultIds = existingTestResultIds.rows.map( - (row: { id: any }) => row.id, - ); - await deleteBasedOnWhereIn( - CUSTOM_DEFECT_TABLE.tableName, - 'test_result_id', - testResultIds, - testResultConnection, - ); - await deleteBasedOnWhereIn( - TEST_DEFECT_TABLE.tableName, - 'test_result_id', - testResultIds, - testResultConnection, - ); - await deleteBasedOnWhereIn( - TEST_RESULT_TABLE.tableName, - 'id', - testResultIds, - testResultConnection, - ); - - await testResultConnection.commit(); - } + // eslint-disable-next-line no-continue + continue; } for (const testType of testResult.testTypes) { @@ -284,6 +325,13 @@ const upsertTestResults = async (testResults: TestResults): Promise => { testType, ); + await upsertMedias( + testResultConnection, + TEST_RESULT_MEDIA_TABLE, + testResultRecordId, + testResult.medias, + ); + await testResultConnection.commit(); } } catch (err) { @@ -566,7 +614,7 @@ const upsertDefects = async ( `upsertTestResults: Upserted defect location (location ID: ${locationId})`, ); - await executePartialUpsert( + const insertTestDefectResponse = await executePartialUpsert( TEST_DEFECT_TABLE, [ testResultId, @@ -579,7 +627,16 @@ const upsertDefects = async ( connection, ); - debugLog('upsertTestResults: Upserted defect test-defect mapping'); + const testDefectId = insertTestDefectResponse.rows.insertId; + + debugLog(`upsertTestResults: Upserted defect test-defect mapping (Test Defect ID: ${testDefectId})`); + + await upsertMedias( + connection, + DEFECT_MEDIA_TABLE, + testDefectId, + defect.medias, + ); } }; @@ -611,3 +668,63 @@ const upsertCustomDefects = async ( ); } }; + +const upsertMediaType = async ( + connection: Connection, + type: string, +): Promise => { + debugLog(`upsertTestResults: Upserting media type (${type})...`); + + const existingMediaTypeIds = await selectRecordIds( + MEDIA_TYPE_TABLE.tableName, + { type }, + connection, + ); + + if (existingMediaTypeIds.rows.length > 0) { + return existingMediaTypeIds.rows[0].id; + } + + const response = await executePartialUpsert(MEDIA_TYPE_TABLE, [type], connection); + + debugLog( + `upsertTestResults: Upserted media type (ID: ${response.rows.insertId})`, + ); + + return response.rows.insertId; +}; + +const upsertMedias = async ( + connection: Connection, + targetTable: TableDetails, + targetId: number, + medias: Medias, +): Promise => { + if (!medias || medias.length < 1) { + debugLog('no media found to insert'); + return; + } + + for (const media of medias) { + debugLog('upsertTestResults: Upserting media type...'); + + const mediaTypeId = await upsertMediaType(connection, media.type); + + debugLog(`upsertTestResults: Upserting ${targetTable.tableName} media...`); + + const response = await executePartialUpsert( + targetTable, + [ + targetId, + media.path, + media.type === 'failReason' ? media.reason : null, + mediaTypeId, + ], + connection, + ); + + debugLog( + `upsertTestResults: Upserted ${targetTable.tableName} media (ID: ${response.rows.insertId})`, + ); + } +}; diff --git a/tests/integration/test-results-conversion-with-media.intTest.ts b/tests/integration/test-results-conversion-with-media.intTest.ts new file mode 100644 index 00000000..73dcd9ac --- /dev/null +++ b/tests/integration/test-results-conversion-with-media.intTest.ts @@ -0,0 +1,246 @@ +/* eslint-disable global-require */ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { StartedTestContainer } from 'testcontainers'; +import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; +import { + destroyConnectionPool, + executeSql, +} from '../../src/services/connection-pool'; +import { exampleContext, useLocalDb } from '../utils'; +import { getContainerizedDatabase } from './cvsbnop-container'; +import { processStreamEvent } from '../../src/functions/process-stream-event'; +import { getConnectionPoolOptions } from '../../src/services/connection-pool-options'; + +useLocalDb(); +jest.setTimeout(60_000); + +describe('convertTestResults() integration tests with media', () => { + let container: StartedTestContainer; + const testResultsJsonWithMedia = JSON.parse( + JSON.stringify( + require('../resources/dynamodb-image-test-results-with-media.json'), + ), + ); + const testResultId = testResultsJsonWithMedia.testResultId.S; + + beforeAll(async () => { + delete process.env.DISABLE_DELETE_ON_UPDATE; + jest.restoreAllMocks(); + + // see README for why this environment variable exists + if (process.env.USE_CONTAINERIZED_DATABASE === '1') { + container = await getContainerizedDatabase(); + } else { + (getConnectionPoolOptions as jest.Mock) = jest.fn().mockResolvedValue({ + host: '127.0.0.1', + port: '3306', + user: 'root', + password: '12345', + database: 'CVSBNOP', + }); + } + }); + + afterAll(async () => { + await destroyConnectionPool(); + if (process.env.USE_CONTAINERIZED_DATABASE === '1' && container) { + await container.stop(); + } + }); + + it('should correctly convert test-result and defect media into Aurora rows', async () => { + await processStreamEvent(buildEvent(testResultsJsonWithMedia), exampleContext(), jest.fn()); + + const testResultSet = await executeSql( + `SELECT id FROM test_result WHERE testResultId = "${testResultId}"`, + ); + expect(testResultSet.rows).toHaveLength(2); + + const testResultMediaSet = await selectTestResultMedia(); + expect(testResultMediaSet.rows).toHaveLength(testResultSet.rows.length); + testResultMediaSet.rows.forEach((row: any) => { + expect(row.path).toBe('test-result-media-path-1.jpg'); + expect(row.reason).toBe('TEST-RESULT-MEDIA-REASON'); + expect(row.type).toBe('failReason'); + }); + + const defectMediaSet = await selectDefectMedia(); + expect(defectMediaSet.rows).toEqual([ + { + path: 'defect-media-path-1.jpg', + reason: 'DEFECT-MEDIA-REASON', + type: 'failReason', + }, + { + path: 'defect-media-path-2.jpg', + reason: 'SECOND-DEFECT-MEDIA-REASON', + type: 'failReason', + }, + ]); + }); + + it('should replace old media rows when the test result is updated', async () => { + const deserializedJson = unmarshall(testResultsJsonWithMedia); + deserializedJson.media = [ + { + path: 'test-result-media-path-2.jpg', + reason: 'UPDATED-TEST-RESULT-MEDIA-REASON', + type: 'failReason', + }, + ]; + deserializedJson.testTypes[0].defects[0].media = [ + { + path: 'defect-media-path-3.jpg', + reason: 'UPDATED-DEFECT-MEDIA-REASON', + type: 'failReason', + }, + { + path: 'defect-media-path-4.jpg', + reason: 'SECOND-UPDATED-DEFECT-MEDIA-REASON', + type: 'failReason', + }, + ]; + + await processStreamEvent(buildEvent(marshall(deserializedJson), 'MODIFY'), exampleContext(), jest.fn()); + + const testResultMediaSet = await selectTestResultMedia(); + expect(testResultMediaSet.rows).toHaveLength(2); + testResultMediaSet.rows.forEach((row: any) => { + expect(row.path).toBe('test-result-media-path-2.jpg'); + expect(row.reason).toBe('UPDATED-TEST-RESULT-MEDIA-REASON'); + expect(row.type).toBe('failReason'); + }); + + const oldTestResultMediaSet = await executeSql( + `SELECT trm.id + FROM test_result_media trm + INNER JOIN test_result tr ON trm.test_result_id = tr.id + WHERE tr.testResultId = "${testResultId}" + AND trm.path = "test-result-media-path-1.jpg"`, + ); + expect(oldTestResultMediaSet.rows).toHaveLength(0); + + const defectMediaSet = await selectDefectMedia(); + expect(defectMediaSet.rows).toEqual([ + { + path: 'defect-media-path-3.jpg', + reason: 'UPDATED-DEFECT-MEDIA-REASON', + type: 'failReason', + }, + { + path: 'defect-media-path-4.jpg', + reason: 'SECOND-UPDATED-DEFECT-MEDIA-REASON', + type: 'failReason', + }, + ]); + + const oldDefectMediaSet = await executeSql( + `SELECT dm.id + FROM defect_media dm + INNER JOIN test_defect td ON dm.test_defect_id = td.id + INNER JOIN test_result tr ON td.test_result_id = tr.id + WHERE tr.testResultId = "${testResultId}" + AND dm.path IN ("defect-media-path-1.jpg", "defect-media-path-2.jpg")`, + ); + expect(oldDefectMediaSet.rows).toHaveLength(0); + }); + + it('should correctly convert image and video media without reasons into Aurora rows', async () => { + const deserializedJson = unmarshall(testResultsJsonWithMedia); + deserializedJson.media = [ + { + path: 'test-result-media-image-1.jpg', + type: 'image', + }, + { + path: 'test-result-media-video-1.mp4', + type: 'video', + }, + ]; + deserializedJson.testTypes[0].defects[0].media = [ + { + path: 'defect-media-image-1.jpg', + type: 'image', + }, + { + path: 'defect-media-video-1.mp4', + type: 'video', + }, + ]; + + await processStreamEvent(buildEvent(marshall(deserializedJson), 'MODIFY'), exampleContext(), jest.fn()); + + const testResultSet = await executeSql( + `SELECT id FROM test_result WHERE testResultId = "${testResultId}"`, + ); + expect(testResultSet.rows).toHaveLength(2); + + const testResultMediaSet = await selectTestResultMedia(); + expect(testResultMediaSet.rows).toHaveLength(testResultSet.rows.length * 2); + testResultMediaSet.rows.forEach((row: any) => { + expect(row.reason).toBeNull(); + }); + expect(testResultMediaSet.rows.filter((row: any) => ( + row.path === 'test-result-media-image-1.jpg' + && row.type === 'image' + ))).toHaveLength(testResultSet.rows.length); + expect(testResultMediaSet.rows.filter((row: any) => ( + row.path === 'test-result-media-video-1.mp4' + && row.type === 'video' + ))).toHaveLength(testResultSet.rows.length); + + const defectMediaSet = await selectDefectMedia(); + expect(defectMediaSet.rows).toEqual([ + { + path: 'defect-media-image-1.jpg', + reason: null, + type: 'image', + }, + { + path: 'defect-media-video-1.mp4', + reason: null, + type: 'video', + }, + ]); + }); + + function buildEvent(newImage: any, eventName = 'INSERT') { + return { + Records: [ + { + body: JSON.stringify({ + eventSourceARN: + 'arn:aws:dynamodb:eu-west-1:1:table/test-results/stream/2020-01-01T00:00:00.000', + eventName, + dynamodb: { + NewImage: newImage, + }, + }), + }, + ], + }; + } + + async function selectTestResultMedia() { + return executeSql( + `SELECT trm.path, trm.reason, mt.type + FROM test_result_media trm + INNER JOIN media_type mt ON trm.media_type_id = mt.id + INNER JOIN test_result tr ON trm.test_result_id = tr.id + WHERE tr.testResultId = "${testResultId}" + ORDER BY tr.testNumber, trm.path`, + ); + } + + async function selectDefectMedia() { + return executeSql( + `SELECT dm.path, dm.reason, mt.type + FROM defect_media dm + INNER JOIN media_type mt ON dm.media_type_id = mt.id + INNER JOIN test_defect td ON dm.test_defect_id = td.id + INNER JOIN test_result tr ON td.test_result_id = tr.id + WHERE tr.testResultId = "${testResultId}" + ORDER BY dm.path`, + ); + } +}); diff --git a/tests/resources/dynamodb-image-test-results-with-media.json b/tests/resources/dynamodb-image-test-results-with-media.json new file mode 100644 index 00000000..1ac51682 --- /dev/null +++ b/tests/resources/dynamodb-image-test-results-with-media.json @@ -0,0 +1,478 @@ +{ + "systemNumber": { + "S": "SYSTEM-NUMBER-MEDIA" + }, + "vrm": { + "S": "VRM-MEDIA" + }, + "trailerId": { + "S": "TRLMEDIA" + }, + "vin": { + "S": "VIN-MEDIA" + }, + "vehicleId": { + "S": "VEHICLE-ID" + }, + "testHistory": { + "L": [] + }, + "testVersion": { + "S": "TEST-VERSION-3" + }, + "reasonForCreation": { + "S": "REASON-FOR-CREATION-3" + }, + "createdAt": { + "S": "2020-01-01T00:00:00.000Z" + }, + "createdByName": { + "S": "CREATED-BY-NAME-3" + }, + "createdById": { + "S": "CREATED-BY-ID-3" + }, + "lastUpdatedAt": { + "S": "2020-01-01T00:00:00.000Z" + }, + "lastUpdatedByName": { + "S": "LAST-UPDATED-BY-NAME-3" + }, + "lastUpdatedById": { + "S": "LAST-UPDATED-BY-ID-3" + }, + "shouldEmailCertificate": { + "S": "SHOULD-EMAIL-CERTIFICATE-3" + }, + "testStationName": { + "S": "TEST-STATION-NAME-3" + }, + "testStationPNumber": { + "S": "P-NUMBER-3" + }, + "testStationType": { + "S": "atf" + }, + "testerName": { + "S": "TESTER-NAME-3" + }, + "testerStaffId": { + "S": "999999998" + }, + "testResultId": { + "S": "TEST-RESULT-ID-MEDIA" + }, + "testerEmailAddress": { + "S": "TESTER-EMAIL-ADDRESS-3" + }, + "testStartTimestamp": { + "S": "2020-01-01T00:00:00.000Z" + }, + "testEndTimestamp": { + "S": "2020-01-01T00:00:00.000Z" + }, + "testStatus": { + "S": "submitted" + }, + "reasonForCancellation": { + "S": "REASON-FOR-CANCELLATION-3" + }, + "vehicleClass": { + "M": { + "code": { + "S": "v" + }, + "description": { + "S": "heavy goods vehicle" + } + } + }, + "vehicleSubclass": { + "L": [ + { + "S": "2" + } + ] + }, + "vehicleType": { + "S": "hgv" + }, + "numberOfSeats": { + "N": "1" + }, + "vehicleConfiguration": { + "S": "rigid" + }, + "odometerReading": { + "N": "1" + }, + "odometerReadingUnits": { + "S": "KILOMETRES" + }, + "preparerId": { + "S": "999999998" + }, + "preparerName": { + "S": "PREPARER-NAME-3" + }, + "numberOfWheelsDriven": { + "N": "1" + }, + "euVehicleCategory": { + "S": "m1" + }, + "countryOfRegistration": { + "S": "COUNTRY-OF-REGISTRATION-3" + }, + "vehicleSize": { + "S": "large" + }, + "noOfAxles": { + "N": "4" + }, + "regnDate": { + "S": "2020-01-01" + }, + "firstUseDate": { + "S": "2020-01-01" + }, + "media": { + "L": [ + { + "M": { + "path": { + "S": "test-result-media-path-1.jpg" + }, + "reason": { + "S": "TEST-RESULT-MEDIA-REASON" + }, + "type": { + "S": "failReason" + } + } + } + ] + }, + "testTypes": { + "L": [ + { + "M": { + "createdAt": { + "S": "2020-01-01T00:00:00.000Z" + }, + "lastUpdatedAt": { + "S": "2020-01-01T00:00:00.000Z" + }, + "deletionFlag": { + "BOOL": true + }, + "testCode": { + "S": "333" + }, + "testTypeClassification": { + "S": "2323232323232323232323" + }, + "testTypeName": { + "S": "TEST-TYPE-NAME" + }, + "name": { + "S": "NAME" + }, + "testTypeId": { + "S": "TEST-TYPE-ID" + }, + "testNumber": { + "S": "TEST-NUMBER" + }, + "certificateNumber": { + "S": "CERTIFICATE-NO" + }, + "secondaryCertificateNumber": { + "S": "2ND-CERTIFICATE-NO" + }, + "certificateLink": { + "S": "CERTIFICATE-LINK" + }, + "testExpiryDate": { + "S": "2020-01-01T00:00:00.000Z" + }, + "testAnniversaryDate": { + "S": "2020-01-01T00:00:00.000Z" + }, + "testTypeStartTimestamp": { + "S": "2020-01-01T00:00:00.000Z" + }, + "testTypeEndTimestamp": { + "S": "2020-01-01T16:54:44.123Z" + }, + "statusUpdatedFlag": { + "BOOL": true + }, + "numberOfSeatbeltsFitted": { + "N": "1" + }, + "lastSeatbeltInstallationCheckDate": { + "S": "2020-01-01" + }, + "seatbeltInstallationCheckDate": { + "BOOL": true + }, + "testResult": { + "S": "fail" + }, + "prohibitionIssued": { + "BOOL": true + }, + "reasonForAbandoning": { + "S": "REASON-FOR-ABANDONING" + }, + "additionalNotesRecorded": { + "S": "ADDITIONAL-NOTES-RECORDED" + }, + "additionalCommentsForAbandon": { + "S": "ADDITIONAL-COMMENTS-FOR-ABANDON" + }, + "modType": { + "M": { + "code": { + "S": "p" + }, + "description": { + "S": "particulate trap" + } + } + }, + "emissionStandard": { + "S": "0.10 g/kWh Euro 3 PM" + }, + "fuelType": { + "S": "diesel" + }, + "particulateTrapFitted": { + "S": "PARTICULATE-TRAP-FITTED" + }, + "particulateTrapSerialNumber": { + "S": "PARTICULATE-TRAP-SERIAL-NUMBER" + }, + "modificationTypeUsed": { + "S": "MODIFICATION-TYPE-USED" + }, + "smokeTestKLimitApplied": { + "S": "SMOKE-TEST-K-LIMIT-APPLIED" + }, + "defects": { + "L": [ + { + "M": { + "imNumber": { + "N": "3" + }, + "imDescription": { + "S": "IM-DESCRIPTION-3" + }, + "additionalInformation": { + "M": { + "location": { + "M": { + "vertical": { + "S": "upper" + }, + "horizontal": { + "S": "inner" + }, + "lateral": { + "S": "nearside" + }, + "longitudinal": { + "S": "front" + }, + "rowNumber": { + "N": "1" + }, + "seatNumber": { + "N": "1" + }, + "axleNumber": { + "N": "1" + } + } + }, + "notes": { + "S": "NOTES" + } + } + }, + "itemNumber": { + "N": "1" + }, + "itemDescription": { + "S": "ITEM-DESCRIPTION-3" + }, + "deficiencyRef": { + "S": "DEFICIENCY-REF-3" + }, + "deficiencyId": { + "S": "a" + }, + "deficiencySubId": { + "S": "mdclxvi" + }, + "deficiencyCategory": { + "S": "advisory" + }, + "deficiencyText": { + "S": "DEFICIENCY-TEXT-3" + }, + "stdForProhibition": { + "BOOL": true + }, + "prs": { + "BOOL": true + }, + "prohibitionIssued": { + "BOOL": true + }, + "media": { + "L": [ + { + "M": { + "path": { + "S": "defect-media-path-1.jpg" + }, + "reason": { + "S": "DEFECT-MEDIA-REASON" + }, + "type": { + "S": "failReason" + } + } + }, + { + "M": { + "path": { + "S": "defect-media-path-2.jpg" + }, + "reason": { + "S": "SECOND-DEFECT-MEDIA-REASON" + }, + "type": { + "S": "failReason" + } + } + } + ] + } + } + } + ] + }, + "customDefects": { + "L": [ + { + "M": { + "referenceNumber": { + "S": "def3" + }, + "defectName": { + "S": "DEFECT-NAME-3" + }, + "defectNotes": { + "S": "DEFECT-NOTES-3" + } + } + } + ] + } + } + }, + { + "M": { + "additionalCommentsForAbandon": { + "NULL": true + }, + "additionalNotesRecorded": { + "S": "No emission plate default 0.70" + }, + "certificateNumber": { + "S": "W123123" + }, + "createdAt": { + "S": "2021-06-21T12:59:08.000Z" + }, + "customDefects": { + "NULL": true + }, + "defects": { + "L": [] + }, + "emissionStandard": { + "NULL": true + }, + "fuelType": { + "NULL": true + }, + "lastUpdatedAt": { + "S": "2021-06-21T12:59:08.000Z" + }, + "modificationTypeUsed": { + "NULL": true + }, + "modType": { + "NULL": true + }, + "name": { + "S": "Annual test" + }, + "particulateTrapFitted": { + "NULL": true + }, + "particulateTrapSerialNumber": { + "NULL": true + }, + "prohibitionIssued": { + "BOOL": false + }, + "reasonForAbandoning": { + "NULL": true + }, + "secondaryCertificateNumber": { + "NULL": true + }, + "smokeTestKLimitApplied": { + "NULL": true + }, + "testAnniversaryDate": { + "S": "2022-06-30T00:00:00.000Z" + }, + "testCode": { + "S": "aav" + }, + "testExpiryDate": { + "S": "2022-06-30T00:00:00.000Z" + }, + "testNumber": { + "S": "W123123" + }, + "testResult": { + "S": "pass" + }, + "testTypeClassification": { + "S": "Annual With Certificate" + }, + "testTypeEndTimestamp": { + "S": "2021-06-21T12:59:07.000Z" + }, + "testTypeId": { + "S": "94" + }, + "testTypeName": { + "S": "Annual test" + }, + "testTypeStartTimestamp": { + "S": "2021-06-21T12:07:22.000Z" + } + } + } + ] + } +} diff --git a/tests/unit/services/sql-execution.unitTest.ts b/tests/unit/services/sql-execution.unitTest.ts index d3b8e260..e8a706e2 100644 --- a/tests/unit/services/sql-execution.unitTest.ts +++ b/tests/unit/services/sql-execution.unitTest.ts @@ -4,6 +4,7 @@ import { executePartialUpsert, executePartialUpsertIfNotExists, selectRecordIds, + selectRecordIdsBasedOnWhereIn, } from '../../../src/services/sql-execution'; import { executeSql } from '../../../src/services/connection-pool'; import { @@ -15,6 +16,7 @@ import { generateFullUpsertSql, generatePartialUpsertSql, generateSelectRecordIds, + generateSelectRecordIdsBasedOnWhereIn, generateSelectSql, } from '../../../src/services/sql-generation'; @@ -141,3 +143,23 @@ describe('selectRecordIds()', () => { expect(executeSql).toHaveBeenCalledWith('SELECT 1', [], undefined); }); }); + +describe('selectRecordIdsBasedOnWhereIn()', () => { + it('should call generateSelectRecordIdsBasedOnWhereIn', () => { + (generateSelectRecordIdsBasedOnWhereIn as jest.Mock) = jest + .fn() + .mockReturnValue('SELECT 1'); + + (executeSql as jest.Mock) = jest.fn().mockResolvedValue({ + rows: [], + fields: [], + }); + + // @ts-expect-error + selectRecordIdsBasedOnWhereIn(CUSTOM_DEFECT_TABLE.tableName, 'id', [], undefined); + + expect(executeSql).toHaveBeenCalledTimes(1); + expect(generateSelectRecordIdsBasedOnWhereIn).toHaveBeenCalledTimes(1); + expect(executeSql).toHaveBeenCalledWith('SELECT 1', [], undefined); + }); +}); diff --git a/tests/unit/services/sql-generation.unitTest.ts b/tests/unit/services/sql-generation.unitTest.ts index a0c5a274..2ecd928d 100644 --- a/tests/unit/services/sql-generation.unitTest.ts +++ b/tests/unit/services/sql-generation.unitTest.ts @@ -3,6 +3,7 @@ import { generateFullUpsertSql, generatePartialUpsertSql, generateSelectRecordIds, + generateSelectRecordIdsBasedOnWhereIn, generateSelectSql, } from '../../../src/services/sql-generation'; @@ -86,3 +87,19 @@ describe('generateSelectRecordIds', () => { expect(result).toEqual(expectedQuery); }); }); + +describe('generateSelectRecordIdsBasedOnWhereIn', () => { + it('should construct a correct Select SQL query, based on WHERE IN clause', () => { + const targetTableName = 'test_defect'; + const targetColumnName = 'test_result_id'; + const ids = [1, 3, 5, 6]; + + const expectedQuery = 'SELECT id FROM test_defect WHERE test_result_id IN (?,?,?,?)'; + const result = generateSelectRecordIdsBasedOnWhereIn( + targetTableName, + targetColumnName, + ids, + ); + expect(result).toEqual(expectedQuery); + }); +});