Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HBA Release 04 18 2024 #688

Merged
merged 8 commits into from
Apr 18, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Convert all rule keys to lowercase
-- Then correct nameAndDOB in lowercased rule_keys

UPDATE application_flagged_set
SET rule_key = LOWER(rule_key);

UPDATE application_flagged_set
SET rule_key = REPLACE(rule_key, 'nameanddob', 'nameAndDOB');
10 changes: 10 additions & 0 deletions api/src/controllers/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ export class AppController {
return await this.appService.healthCheck();
}

@Get('teapot')
@ApiOperation({
summary: 'Tip me over and pour me out',
operationId: 'teapot',
})
@ApiOkResponse({ type: SuccessDTO })
async teapot(): Promise<SuccessDTO> {
return await this.appService.teapot();
}

@Put('clearTempFiles')
@ApiOperation({
summary: 'Trigger the removal of CSVs job',
Expand Down
4 changes: 4 additions & 0 deletions api/src/enums/user/view-enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum UserViews {
base = 'base',
full = 'full',
}
13 changes: 13 additions & 0 deletions api/src/services/app.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs';
import { join } from 'path';
import {
ImATeapotException,
Inject,
Injectable,
InternalServerErrorException,
Expand Down Expand Up @@ -111,4 +112,16 @@ export class AppService implements OnModuleInit {
});
}
}

// art pulled from: https://www.asciiart.eu/food-and-drinks/coffee-and-tea
async teapot(): Promise<SuccessDTO> {
throw new ImATeapotException(`
;,'
_o_ ;:;'
,-.'---\`.__ ;
((j\`=====',-'
\`-\ /
\`-=-' hjw
`);
}
}
235 changes: 117 additions & 118 deletions api/src/services/application-csv-export.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const typeMap = {
fiveBdrm: 'Five Bedroom',
};

const NUMBER_TO_PAGINATE_BY = 500;

@Injectable()
export class ApplicationCsvExporterService
implements CsvExporterServiceInterface
Expand Down Expand Up @@ -94,6 +96,11 @@ export class ApplicationCsvExporterService
const applications = await this.prisma.applications.findMany({
select: {
id: true,
householdMember: {
select: {
id: true,
},
},
},
where: {
listingId: queryParams.listingId,
Expand All @@ -107,10 +114,13 @@ export class ApplicationCsvExporterService
queryParams.listingId,
);

// get maxHouseholdMembers or associated to the selected applications
const maxHouseholdMembers = await this.maxHouseholdMembers(
applications.map((application) => application.id),
);
// get maxHouseholdMembers associated to the selected applications
let maxHouseholdMembers = 0;
applications.forEach((app) => {
if (app.householdMember?.length > maxHouseholdMembers) {
maxHouseholdMembers = app.householdMember.length;
}
});

const csvHeaders = await this.getCsvHeaders(
maxHouseholdMembers,
Expand Down Expand Up @@ -138,131 +148,120 @@ export class ApplicationCsvExporterService
.join(',') + '\n',
);

for (let i = 0; i < applications.length / 1000 + 1; i++) {
// grab applications 1k at a time
const paginatedApplications =
await this.prisma.applications.findMany({
include: {
...view.csv,
demographics: queryParams.includeDemographics
? {
select: {
id: true,
createdAt: true,
updatedAt: true,
ethnicity: true,
gender: true,
sexualOrientation: true,
howDidYouHear: true,
race: true,
},
const promiseArray: Promise<string>[] = [];
for (let i = 0; i < applications.length; i += NUMBER_TO_PAGINATE_BY) {
promiseArray.push(
new Promise(async (resolve) => {
// grab applications NUMBER_TO_PAGINATE_BY at a time
const paginatedApplications =
await this.prisma.applications.findMany({
include: {
...view.csv,
demographics: queryParams.includeDemographics
? {
select: {
id: true,
createdAt: true,
updatedAt: true,
ethnicity: true,
gender: true,
sexualOrientation: true,
howDidYouHear: true,
race: true,
},
}
: false,
},
where: {
listingId: queryParams.listingId,
deletedAt: null,
},
skip: i,
take: NUMBER_TO_PAGINATE_BY,
});

let row = '';
paginatedApplications.forEach((app) => {
let preferences: ApplicationMultiselectQuestion[];
csvHeaders.forEach((header, index) => {
let multiselectQuestionValue = false;
let parsePreference = false;
let value = header.path.split('.').reduce((acc, curr) => {
// return preference/program as value for the format function to accept
if (multiselectQuestionValue) {
return acc;
}

if (parsePreference) {
// curr should equal the preference id we're pulling from
if (!preferences) {
preferences =
app.preferences as unknown as ApplicationMultiselectQuestion[];
}
parsePreference = false;
// there aren't typically many preferences, but if there, then a object map should be created and used
const preference = preferences.find(
(preference) =>
preference.multiselectQuestionId === curr,
);
multiselectQuestionValue = true;
return preference;
}

// sets parsePreference to true, for the next iteration
if (curr === 'preferences') {
parsePreference = true;
}

if (acc === null || acc === undefined) {
return '';
}

// handles working with arrays, e.g. householdMember.0.firstName
if (!isNaN(Number(curr))) {
const index = Number(curr);
return acc[index];
}
: false,
},
where: {
listingId: queryParams.listingId,
deletedAt: null,
},
skip: i * 1000,
take: 1000,
});

// now loop over applications and write them to file
paginatedApplications.forEach((app) => {
let row = '';
let preferences: ApplicationMultiselectQuestion[];
csvHeaders.forEach((header, index) => {
let multiselectQuestionValue = false;
let parsePreference = false;
let value = header.path.split('.').reduce((acc, curr) => {
// return preference/program as value for the format function to accept
if (multiselectQuestionValue) {
return acc;
}

if (parsePreference) {
// curr should equal the preference id we're pulling from
if (!preferences) {
preferences =
app.preferences as unknown as ApplicationMultiselectQuestion[];
return acc[curr];
}, app);
value =
value === undefined ? '' : value === null ? '' : value;
if (header.format) {
value = header.format(value);
}
parsePreference = false;
// there aren't typically many preferences, but if there, then a object map should be created and used
const preference = preferences.find(
(preference) => preference.multiselectQuestionId === curr,
);
multiselectQuestionValue = true;
return preference;
}

// sets parsePreference to true, for the next iteration
if (curr === 'preferences') {
parsePreference = true;
}

if (acc === null || acc === undefined) {
return '';
}

// handles working with arrays, e.g. householdMember.0.firstName
if (!isNaN(Number(curr))) {
const index = Number(curr);
return acc[index];
}

return acc[curr];
}, app);
value = value === undefined ? '' : value === null ? '' : value;
if (header.format) {
value = header.format(value);
}

row += value ? `"${value.toString().replace(/"/g, `""`)}"` : '';
if (index < csvHeaders.length - 1) {
row += ',';
}
});

try {
writableStream.write(row + '\n');
} catch (e) {
console.log('writeStream write error = ', e);
writableStream.once('drain', () => {
console.log('drain buffer');
writableStream.write(row + '\n');
row += value
? `"${value.toString().replace(/"/g, `""`)}"`
: '';
if (index < csvHeaders.length - 1) {
row += ',';
}
});
row += '\n';
});
}
});
resolve(row);
}),
);
}
const resolvedArray = await Promise.all(promiseArray);
// now loop over batched row data and write them to file
resolvedArray.forEach((row) => {
try {
writableStream.write(row);
} catch (e) {
console.log('writeStream write error = ', e);
writableStream.once('drain', () => {
console.log('drain buffer');
writableStream.write(row + '\n');
});
}
});
writableStream.end();
});
});
}

async maxHouseholdMembers(applicationIds: string[]): Promise<number> {
const maxHouseholdMembersRes = await this.prisma.householdMember.groupBy({
by: ['applicationId'],
_count: {
applicationId: true,
},
where: {
OR: applicationIds.map((id) => {
return { applicationId: id };
}),
},
orderBy: {
_count: {
applicationId: 'desc',
},
},
take: 1,
});

return maxHouseholdMembersRes && maxHouseholdMembersRes.length
? maxHouseholdMembersRes[0]._count.applicationId
: 0;
}

getHouseholdCsvHeaders(maxHouseholdMembers: number): CsvHeader[] {
const headers = [];
for (let i = 0; i < maxHouseholdMembers; i++) {
Expand Down
8 changes: 6 additions & 2 deletions api/src/services/application-flagged-set.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,8 +649,8 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
return `${listingId}-email-${application.applicant.emailAddress}`;
} else {
return (
`${listingId}-nameAndDOB-${application.applicant.firstName}-${application.applicant.lastName}-${application.applicant.birthMonth}-` +
`${application.applicant.birthDay}-${application.applicant.birthYear}`
`${listingId}-nameAndDOB-${application.applicant.firstName.toLowerCase()}-${application.applicant.lastName.toLowerCase()}` +
`-${application.applicant.birthMonth}-${application.applicant.birthDay}-${application.applicant.birthYear}`
);
}
}
Expand Down Expand Up @@ -748,6 +748,7 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
some: {
firstName: {
in: firstNames,
mode: 'insensitive',
},
},
},
Expand All @@ -756,6 +757,7 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
applicant: {
firstName: {
in: firstNames,
mode: 'insensitive',
},
},
},
Expand All @@ -768,6 +770,7 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
some: {
lastName: {
in: lastNames,
mode: 'insensitive',
},
},
},
Expand All @@ -776,6 +779,7 @@ export class ApplicationFlaggedSetService implements OnModuleInit {
applicant: {
lastName: {
in: lastNames,
mode: 'insensitive',
},
},
},
Expand Down
Loading
Loading