Skip to content

Commit

Permalink
Merge pull request #60 from ti-broish/violations-filters
Browse files Browse the repository at this point in the history
Филтри за сигналите в преброителния център
  • Loading branch information
hkdobrev authored Jun 27, 2021
2 parents c421caf + a750ddf commit 2631f3a
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 42 deletions.
49 changes: 49 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,54 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/ban-types': [
'error',
{
types: {
String: {
message: 'Use string instead',
fixWith: 'string',
},
Boolean: {
message: 'Use boolean instead',
fixWith: 'boolean',
},
Number: {
message: 'Use number instead',
fixWith: 'number',
},
Symbol: {
message: 'Use symbol instead',
fixWith: 'symbol',
},

Function: {
message: [
'The `Function` type accepts any function-like value.',
'It provides no type safety when calling the function, which can be a common source of bugs.',
'It also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.',
'If you are expecting the function to accept certain arguments, you should explicitly define the function shape.',
].join('\n'),
},

// object typing
Object: {
message: [
'The `Object` type actually means "any non-nullish value", so it is marginally better than `unknown`.',
'- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.',
'- If you want a type meaning "any value", you probably want `unknown` instead.',
].join('\n'),
},
'{}': {
message: [
'`{}` actually means "any non-nullish value".',
'- If you want a type meaning "any object", you probably want `Record<string, unknown>` instead.',
'- If you want a type meaning "any value", you probably want `unknown` instead.',
].join('\n'),
},
object: false,
},
},
],
},
};
3 changes: 2 additions & 1 deletion src/i18n/bg/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"BROADCAST_PENDING": "В изчакване",
"BROADCAST_PROCESSING": "Обработва се",
"BROADCAST_PUBLISHED": "Публикувано",
"BROADCAST_DISCARDED": "Отхвърлено"
"BROADCAST_DISCARDED": "Отхвърлено",
"OBJECT_ACCEPTED": "Прието"
}
3 changes: 2 additions & 1 deletion src/i18n/en/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"BROADCAST_PENDING": "Pending",
"BROADCAST_PROCESSING": "Processing",
"BROADCAST_PUBLISHED": "Published",
"BROADCAST_DISCARDED": "Discarded"
"BROADCAST_DISCARDED": "Discarded",
"OBJECT_ACCEPTED": "Accepted"
}
18 changes: 18 additions & 0 deletions src/migrations/1624337501383-AddTownsCountryMunicipalityIndices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddTownsCountryMunicipalityIndices1624337501383
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create index "towns_country_id_key" on "towns" ("country_id");
create index "towns_municipality_id_key" on "towns" ("country_id");
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
drop index "towns_municipality_id_key";
drop index "towns_country_id_key";
`);
}
}
2 changes: 1 addition & 1 deletion src/sections/api/town-exists.constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class IsTownExistsConstraint implements ValidatorConstraintInterface {
}

export function IsTownExists(validationOptions?: ValidationOptions) {
return function (town: TownDto, propertyName: string) {
return function (town: TownDto | object, propertyName: string) {
registerDecorator({
target: town.constructor,
propertyName: propertyName,
Expand Down
2 changes: 1 addition & 1 deletion src/sections/entities/town.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class Town {
readonly id: number;

@Column()
readonly code: number;
code: number;

@Column()
readonly name: string;
Expand Down
1 change: 1 addition & 0 deletions src/sections/sections.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { CityRegionsRepository } from './entities/cityRegions.repository';
MunicipalitiesRepository,
CountriesRepository,
CityRegionsRepository,
TownsRepository,
],
controllers: [
SectionsController,
Expand Down
2 changes: 1 addition & 1 deletion src/users/api/organization-exists.constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class IsOrganizationExistsConstraint
}

export function IsOrganizationExists(validationOptions?: ValidationOptions) {
return function (object: OrganizationDto, propertyName: string) {
return function (object: OrganizationDto | object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
Expand Down
6 changes: 3 additions & 3 deletions src/users/api/user-exists.constraint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export class IsUserExistsConstraint implements ValidatorConstraintInterface {
}

defaultMessage?(): string {
return `User with $property "$value" does not exist!`;
return `User with ID "$value" does not exist!`;
}
}

export function IsUserExists(validationOptions?: ValidationOptions) {
return function (user: UserDto, propertyName: string) {
return function (target: UserDto | object, propertyName: string) {
registerDecorator({
target: user.constructor,
target: target.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
Expand Down
33 changes: 33 additions & 0 deletions src/utils/ulid-constraint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common';
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';

@ValidatorConstraint({ async: true })
@Injectable()
export class IsULIDConstraint implements ValidatorConstraintInterface {
async validate(id?: string): Promise<boolean> {
return typeof id === 'string' && /[A-Z0-9]{26}/.test(id);
}

defaultMessage?(context: ValidationArguments): string {
console.debug(context.object);
return `${context.property} identifier is not an ULID`;
}
}

export function IsULID(validationOptions?: ValidationOptions) {
return function (entity: object, propertyName: string) {
registerDecorator({
target: entity.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsULIDConstraint,
});
};
}
4 changes: 3 additions & 1 deletion src/violations/api/violation.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export class ViolationDto {
@Expose({ groups: ['read', 'create'] })
@Type(() => TownDto)
@Transform(
({ value: id }) => plainToClass(TownDto, { id }, { groups: ['create'] }),
({ value: id }) =>
plainToClass(TownDto, { code: id }, { groups: ['create'] }),
{ groups: ['create'] },
)
@IsNotEmpty({ groups: ['create'] })
Expand Down Expand Up @@ -111,6 +112,7 @@ export class ViolationDto {
groups: ['create'],
},
);
violation.town.code = this.town.id;

let sortPosition = 1;
violation.pictures = (violation.pictures || []).map(
Expand Down
55 changes: 47 additions & 8 deletions src/violations/api/violations-filters.dto.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,62 @@
import { Optional } from '@nestjs/common';
import { Type } from 'class-transformer';
import {
IsBooleanString,
IsInt,
IsNumberString,
IsOptional,
Length,
} from 'class-validator';
import { IsTownExists } from 'src/sections/api/town-exists.constraint';
import { IsOrganizationExists } from 'src/users/api/organization-exists.constraint';
import { IsUserExists } from 'src/users/api/user-exists.constraint';
import { PageDTO } from 'src/utils/page.dto';
import { IsULID } from 'src/utils/ulid-constraint';
import { ViolationStatus } from '../entities/violation.entity';

export class ViolationsFilters extends PageDTO {
@Optional()
@IsOptional()
@IsULID()
@IsUserExists()
assignee: string;

@Optional()
@IsOptional()
@Length(1, 9)
section: string;

@Optional()
@IsOptional()
status: ViolationStatus;

@Optional()
author: string;
@IsOptional()
@IsNumberString()
@Length(2, 2)
electionRegion: string;

@Optional()
@IsOptional()
@IsNumberString()
@Length(2, 2)
municipality: string;

@IsOptional()
@IsNumberString()
@Length(2, 2)
country: string;

@IsOptional()
@IsTownExists()
town: number;

@Optional()
@IsOptional()
@IsNumberString()
@Length(2, 2)
cityRegion: string;

@IsOptional()
@IsInt()
@Type(() => Number)
@IsOrganizationExists()
organization: number;

@IsOptional()
@IsBooleanString()
published: boolean;
}
2 changes: 1 addition & 1 deletion src/violations/api/violations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class ViolationsController {
@Get()
@HttpCode(200)
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: Ability) => ability.can(Action.Read, Violation))
@CheckPolicies((ability: Ability) => ability.can(Action.Manage, Violation))
@UsePipes(new ValidationPipe({ transform: true }))
async index(
@Query() query: ViolationsFilters,
Expand Down
71 changes: 47 additions & 24 deletions src/violations/entities/violations.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { User } from '../../users/entities';
import { Violation } from './violation.entity';
import { ViolationUpdateType } from './violation-update.entity';
import { ViolationsFilters } from '../api/violations-filters.dto';
import { TownsRepository } from 'src/sections/entities/towns.repository';

@Injectable()
export class ViolationsRepository {
constructor(
@InjectRepository(Violation) private readonly repo: Repository<Violation>,
private readonly townsRepo: TownsRepository,
) {}

findOneOrFail(id: string): Promise<Violation> {
Expand All @@ -32,11 +34,14 @@ export class ViolationsRepository {
): SelectQueryBuilder<Violation> {
const qb = this.repo.createQueryBuilder('violation');

qb.leftJoinAndSelect('violation.section', 'section');
qb.innerJoinAndSelect('violation.town', 'town');
qb.innerJoinAndSelect('violation.updates', 'update');
qb.innerJoinAndSelect('update.actor', 'actor');
qb.innerJoinAndSelect('actor.organization', 'organization');
qb.innerJoinAndSelect('violation.updates', 'update_send');
qb.andWhere('update_send.type = :update', {
update: ViolationUpdateType.SEND,
});
qb.innerJoinAndSelect('update_send.actor', 'sender');
qb.innerJoinAndSelect('sender.organization', 'organization');
qb.innerJoinAndSelect('violation.updates', 'updates');
qb.leftJoinAndSelect('violation.pictures', 'picture');

if (filters.assignee) {
Expand All @@ -47,44 +52,62 @@ export class ViolationsRepository {
}

if (filters.section) {
qb.innerJoinAndSelect('violation.section', 'section');
qb.andWhere('section.id LIKE :section', {
section: `${filters.section}%`,
});
} else {
qb.leftJoinAndSelect('violation.section', 'section');
}

if (filters.status) {
qb.andWhere('violation.status = :status', { status: filters.status });
}

if (filters.town) {
qb.andWhere('town.code = :town', { town: filters.town });
}

if (filters.author || filters.organization) {
qb.innerJoin('violation.updates', 'update_send');
qb.andWhere('update_send.type = :update', {
update: ViolationUpdateType.SEND,
if (filters.electionRegion) {
qb.innerJoin('town.municipality', 'municipality');
qb.innerJoin('municipality.electionRegions', 'electionRegions');
qb.andWhere('electionRegions.code = :electionRegion', {
electionRegion: filters.electionRegion,
});

if (filters.author) {
qb.andWhere('update_send.actor_id = :author', {
author: filters.author,
if (filters.municipality) {
qb.andWhere('municipality.code = :municipality', {
municipality: filters.municipality,
});
}

if (filters.organization) {
qb.innerJoin('update_send.actor', 'sender');
qb.innerJoin('sender.organization', 'organization');
qb.andWhere('organization.id = :organization', {
organization: filters.organization,
if (filters.country) {
qb.innerJoin('town.country', 'country');
qb.andWhere('country.code = :country', { country: filters.country });
}

if (filters.town) {
qb.andWhere('town.code = :town', {
town: filters.town,
});
}
}

if (filters.status) {
qb.andWhere('violation.status = :status', { status: filters.status });
}

if (filters.published) {
qb.andWhere('violation.isPublished = :published', {
published: filters.published,
});
}

if (filters.organization) {
qb.andWhere('organization.id = :organization', {
organization: filters.organization,
});
}

return qb;
}

async save(violation: Violation): Promise<Violation> {
if (violation.town && !violation.town.id && violation.town.code) {
violation.town = await this.townsRepo.findOneByCode(violation.town.code);
}
await this.repo.save(violation);

return this.findOneOrFail(violation.id);
Expand Down
Loading

0 comments on commit 2631f3a

Please sign in to comment.