From 0a4c3ee647431f1258351c54acedf067fffcd497 Mon Sep 17 00:00:00 2001 From: Niels Mokkenstorm Date: Sat, 30 May 2026 12:44:27 +0200 Subject: [PATCH] feat(cvg-82): public Location CRUD resolver + seed Dutch provinces (#70) --- .../graphql/location-connection-args.type.ts | 15 ++ .../location/graphql/location.resolver.ts | 135 ++++++++++++++++ .../location/graphql/location.type.ts | 9 ++ .../vacancies/location/location.module.ts | 16 +- .../seed/seeders/reference-data.seed.ts | 41 +++++ packages/core/src/modules/vacancies/index.ts | 11 +- .../__tests__/location.factory.spec.ts | 41 +++++ .../__tests__/location.service.spec.ts | 148 ++++++++++++++++++ .../vacancies/location/location.dto.ts | 14 ++ .../vacancies/location/location.factory.ts | 25 +++ .../vacancies/location/location.module.ts | 27 +++- .../vacancies/location/location.policy.ts | 19 +++ .../vacancies/location/location.service.ts | 83 +++++++++- 13 files changed, 575 insertions(+), 9 deletions(-) create mode 100644 apps/api/src/modules/vacancies/location/graphql/location-connection-args.type.ts create mode 100644 apps/api/src/modules/vacancies/location/graphql/location.resolver.ts create mode 100644 packages/core/src/modules/vacancies/location/__tests__/location.factory.spec.ts create mode 100644 packages/core/src/modules/vacancies/location/__tests__/location.service.spec.ts create mode 100644 packages/core/src/modules/vacancies/location/location.dto.ts create mode 100644 packages/core/src/modules/vacancies/location/location.factory.ts create mode 100644 packages/core/src/modules/vacancies/location/location.policy.ts diff --git a/apps/api/src/modules/vacancies/location/graphql/location-connection-args.type.ts b/apps/api/src/modules/vacancies/location/graphql/location-connection-args.type.ts new file mode 100644 index 0000000..12c5474 --- /dev/null +++ b/apps/api/src/modules/vacancies/location/graphql/location-connection-args.type.ts @@ -0,0 +1,15 @@ +import { BasePaginationArgs, LocationType } from "@cv/core"; +import { ArgsType, Field, ID } from "@nestjs/graphql"; +import { GraphQLString } from "graphql"; + +@ArgsType() +export class LocationConnectionArgs extends BasePaginationArgs { + @Field(() => LocationType, { nullable: true }) + type?: LocationType | null; + + @Field(() => ID, { nullable: true }) + parentId?: string | null; + + @Field(() => GraphQLString, { nullable: true }) + searchTerm?: string | null; +} diff --git a/apps/api/src/modules/vacancies/location/graphql/location.resolver.ts b/apps/api/src/modules/vacancies/location/graphql/location.resolver.ts new file mode 100644 index 0000000..0e6e24e --- /dev/null +++ b/apps/api/src/modules/vacancies/location/graphql/location.resolver.ts @@ -0,0 +1,135 @@ +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { + AuthorizationService, + ClockService, + User as DomainUser, + Location as LocationEntity, + LocationFactory, + type LocationFilters, + LocationType, + PaginationService, + VacancyLocationService, +} from "@cv/core"; +import { UseGuards } from "@nestjs/common"; +import { Args, ID, Mutation, Query, Resolver } from "@nestjs/graphql"; +import { CurrentUser } from "@/modules/current-user/current-user.decorator"; +import { + LocationConnection, + Location as LocationGraphQL, +} from "./location.type"; +import { LocationConnectionArgs } from "./location-connection-args.type"; + +@Resolver(() => LocationGraphQL) +@UseGuards(JwtAuthGuard, VerifiedScopeGuard) +export class LocationResolver { + constructor( + private readonly locationService: VacancyLocationService, + private readonly locationFactory: LocationFactory, + private readonly paginationService: PaginationService, + private readonly authorizationService: AuthorizationService, + private readonly clock: ClockService, + ) {} + + @Query(() => LocationConnection) + async locations( + @Args() args: LocationConnectionArgs = {}, + ): Promise { + const options = this.paginationService.parsePaginationArgs(args); + const filters: LocationFilters = {}; + if (args.type) { + filters.type = args.type; + } + if (args.parentId !== undefined && args.parentId !== null) { + filters.parentId = args.parentId; + } + if (args.searchTerm) { + filters.searchTerm = args.searchTerm; + } + + const [items, totalCount] = await Promise.all([ + this.locationService.findMany(filters), + this.locationService.count(filters), + ]); + + const result = this.paginationService.buildPaginationResult( + items, + totalCount, + options, + ); + + return LocationConnection.fromPaginationResult(result); + } + + @Query(() => LocationGraphQL, { nullable: true }) + async location( + @CurrentUser() user: DomainUser, + @Args("id", { type: () => ID }) id: string, + ): Promise { + const location = await this.locationService.findById(id); + if (!location) { + return null; + } + await this.authorizationService.canView(user, location, LocationEntity); + return LocationGraphQL.fromDomain(location); + } + + @Mutation(() => LocationGraphQL) + async createLocation( + @CurrentUser() user: DomainUser, + @Args("name") name: string, + @Args("type", { type: () => LocationType }) type: LocationType, + @Args("code", { nullable: true }) code?: string, + @Args("parentId", { type: () => ID, nullable: true }) parentId?: string, + ): Promise { + await this.authorizationService.canCreate(user, LocationEntity); + const dto: { + name: string; + type: LocationType; + code?: string; + parentId?: string; + } = { name, type }; + if (code !== undefined) { + dto.code = code; + } + if (parentId !== undefined) { + dto.parentId = parentId; + } + const entity = this.locationFactory.create(dto); + const saved = await this.locationService.save(entity); + return LocationGraphQL.fromDomain(saved); + } + + @Mutation(() => LocationGraphQL) + async updateLocation( + @CurrentUser() user: DomainUser, + @Args("id", { type: () => ID }) id: string, + @Args("name", { nullable: true }) name?: string, + @Args("code", { nullable: true }) code?: string, + @Args("parentId", { type: () => ID, nullable: true }) parentId?: string, + ): Promise { + const location = await this.locationService.findByIdOrFail(id); + await this.authorizationService.canUpdate(user, location, LocationEntity); + const updated = new LocationEntity( + location.id, + name ?? location.name, + location.type, + location.createdAt, + this.clock.now(), + code !== undefined ? code : location.code, + parentId !== undefined ? parentId : location.parentId, + ); + const saved = await this.locationService.save(updated); + return LocationGraphQL.fromDomain(saved); + } + + @Mutation(() => Boolean) + async deleteLocation( + @CurrentUser() user: DomainUser, + @Args("id", { type: () => ID }) id: string, + ): Promise { + const location = await this.locationService.findByIdOrFail(id); + await this.authorizationService.canDelete(user, location, LocationEntity); + await this.locationService.destroy(location); + return true; + } +} diff --git a/apps/api/src/modules/vacancies/location/graphql/location.type.ts b/apps/api/src/modules/vacancies/location/graphql/location.type.ts index b207346..4eb03cf 100644 --- a/apps/api/src/modules/vacancies/location/graphql/location.type.ts +++ b/apps/api/src/modules/vacancies/location/graphql/location.type.ts @@ -1,6 +1,7 @@ import { Location as LocationDomain, LocationType } from "@cv/core"; import { Field, ID, ObjectType, registerEnumType } from "@nestjs/graphql"; import { BaseGraphQLType } from "@/modules/base/base-graphql-type"; +import { createConnection } from "@/modules/base/connection.factory"; registerEnumType(LocationType, { name: "LocationType" }); @@ -30,3 +31,11 @@ export class Location extends BaseGraphQLType { return new Location(domain); } } + +export const { Connection: LocationConnection, Edge: LocationEdge } = + createConnection(Location, (domain) => + Location.fromDomain(domain), + ); + +export type LocationConnection = InstanceType; +export type LocationEdge = InstanceType; diff --git a/apps/api/src/modules/vacancies/location/location.module.ts b/apps/api/src/modules/vacancies/location/location.module.ts index 24b00c1..04bf12f 100644 --- a/apps/api/src/modules/vacancies/location/location.module.ts +++ b/apps/api/src/modules/vacancies/location/location.module.ts @@ -1,16 +1,30 @@ import { + AuthorizationModule, + BaseModule, DatabaseModule, + LocationFactory, LocationMapper, + LocationPolicy, VacancyLocationService, } from "@cv/core"; import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "@/modules/authentication/authentication.module"; +import { LocationResolver } from "./graphql/location.resolver"; import { LocationDataLoaderService } from "./location.dataloader"; @Module({ - imports: [DatabaseModule], + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + ], providers: [ VacancyLocationService, LocationMapper, + LocationFactory, + LocationPolicy, + LocationResolver, LocationDataLoaderService, ], exports: [VacancyLocationService, LocationDataLoaderService], diff --git a/packages/core/src/modules/database/seed/seeders/reference-data.seed.ts b/packages/core/src/modules/database/seed/seeders/reference-data.seed.ts index 172355a..e0c4414 100644 --- a/packages/core/src/modules/database/seed/seeders/reference-data.seed.ts +++ b/packages/core/src/modules/database/seed/seeders/reference-data.seed.ts @@ -22,9 +22,50 @@ export class ReferenceDataSeedService implements Seeder { this.ensureApplicationStatuses(prisma), this.ensureRateTypes(prisma), this.ensureContractTypes(prisma), + this.ensureDutchProvinces(prisma), ]); } + /** + * Seed the 12 Dutch provinces as PROVINCE-type locations with stable + * `nl_*` codes. Callers (seeders, scripts, importers from freelance.nl + * payloads carrying ISO-3166-2 province codes) can resolve via + * `findFirst({ where: { type: PROVINCE, code: 'nl_NB' } })`. + */ + async ensureDutchProvinces( + prisma: PrismaService = this.prisma, + ): Promise> { + const provinces: Array<{ code: string; name: string }> = [ + { code: "nl_DR", name: "Drenthe" }, + { code: "nl_FL", name: "Flevoland" }, + { code: "nl_FR", name: "Friesland" }, + { code: "nl_GE", name: "Gelderland" }, + { code: "nl_GR", name: "Groningen" }, + { code: "nl_LI", name: "Limburg" }, + { code: "nl_NB", name: "Noord-Brabant" }, + { code: "nl_NH", name: "Noord-Holland" }, + { code: "nl_OV", name: "Overijssel" }, + { code: "nl_UT", name: "Utrecht" }, + { code: "nl_ZE", name: "Zeeland" }, + { code: "nl_ZH", name: "Zuid-Holland" }, + ]; + + return Promise.all( + provinces.map(async ({ code, name }) => { + const existing = await prisma.location.findUnique({ + where: { type_code: { type: "PROVINCE", code } }, + }); + if (existing) { + return { id: existing.id }; + } + const created = await prisma.location.create({ + data: { name, type: "PROVINCE", code }, + }); + return { id: created.id }; + }), + ); + } + async ensureSkills( prisma: PrismaService = this.prisma, ): Promise> { diff --git a/packages/core/src/modules/vacancies/index.ts b/packages/core/src/modules/vacancies/index.ts index fb63e91..c2c8709 100644 --- a/packages/core/src/modules/vacancies/index.ts +++ b/packages/core/src/modules/vacancies/index.ts @@ -13,11 +13,20 @@ export { JobTypeMapper } from "./job-type/job-type.mapper"; export { JobTypeModule } from "./job-type/job-type.module"; export { JobTypeService } from "./job-type/job-type.service"; export { LocationDataLoaderService } from "./location/location.dataloader"; +export type { + CreateLocationDto, + UpdateLocationDto, +} from "./location/location.dto"; export { Location, LocationType } from "./location/location.entity"; +export { LocationFactory } from "./location/location.factory"; export { LocationMapper } from "./location/location.mapper"; export { LocationModule } from "./location/location.module"; +export { LocationPolicy } from "./location/location.policy"; // Renamed to disambiguate from auth's LocationService (geolocation lookup). -export { LocationService as VacancyLocationService } from "./location/location.service"; +export { + type LocationFilters, + LocationService as VacancyLocationService, +} from "./location/location.service"; export { RateTypeDataLoaderService } from "./rate-type/rate-type.dataloader"; export { RateType } from "./rate-type/rate-type.entity"; export { RateTypeMapper } from "./rate-type/rate-type.mapper"; diff --git a/packages/core/src/modules/vacancies/location/__tests__/location.factory.spec.ts b/packages/core/src/modules/vacancies/location/__tests__/location.factory.spec.ts new file mode 100644 index 0000000..dff2922 --- /dev/null +++ b/packages/core/src/modules/vacancies/location/__tests__/location.factory.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { FrozenClockService } from "../../../../shared/testing"; +import { LocationType } from "../location.entity"; +import { LocationFactory } from "../location.factory"; + +const fixedClock = new FrozenClockService(new Date("2026-01-01T00:00:00Z")); + +const fakeUuidFactory = (id: string) => + ({ generate: () => id }) as never; + +describe("LocationFactory.create", () => { + it("builds a Location with generated id and frozen timestamps", () => { + const factory = new LocationFactory(fakeUuidFactory("loc-1"), fixedClock); + + const location = factory.create({ + name: "Noord-Brabant", + type: LocationType.PROVINCE, + code: "nl_NB", + }); + + expect(location.id).toBe("loc-1"); + expect(location.name).toBe("Noord-Brabant"); + expect(location.type).toBe(LocationType.PROVINCE); + expect(location.code).toBe("nl_NB"); + expect(location.parentId).toBeUndefined(); + expect(location.createdAt).toEqual(new Date("2026-01-01T00:00:00Z")); + expect(location.updatedAt).toEqual(new Date("2026-01-01T00:00:00Z")); + }); + + it("propagates parentId when supplied", () => { + const factory = new LocationFactory(fakeUuidFactory("loc-2"), fixedClock); + + const location = factory.create({ + name: "Amsterdam", + type: LocationType.CITY, + parentId: "prov-1", + }); + + expect(location.parentId).toBe("prov-1"); + }); +}); diff --git a/packages/core/src/modules/vacancies/location/__tests__/location.service.spec.ts b/packages/core/src/modules/vacancies/location/__tests__/location.service.spec.ts new file mode 100644 index 0000000..fee8a39 --- /dev/null +++ b/packages/core/src/modules/vacancies/location/__tests__/location.service.spec.ts @@ -0,0 +1,148 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { MAX_PAGINATION_TAKE } from "../../../../shared"; +import { LocationType } from "../location.entity"; +import { LocationService } from "../location.service"; + +const buildMockPrisma = () => ({ + location: { + findUnique: vi.fn(), + findMany: vi.fn(), + count: vi.fn(), + upsert: vi.fn(), + delete: vi.fn(), + }, +}); + +const buildMockMapper = () => ({ + toDomain: vi.fn((row: unknown) => row), + mapToDomain: vi.fn((rows: unknown[]) => rows), +}); + +type MockPrisma = ReturnType; +type MockMapper = ReturnType; + +const buildService = () => { + const prisma = buildMockPrisma(); + const mapper = buildMockMapper(); + prisma.location.findMany.mockResolvedValue([]); + prisma.location.count.mockResolvedValue(0); + const service = new LocationService(prisma as never, mapper as never); + return { service, prisma, mapper }; +}; + +describe("LocationService.findMany", () => { + let prisma: MockPrisma; + let service: LocationService; + + beforeEach(() => { + ({ prisma, service } = buildService()); + }); + + const getFindManyArgs = () => { + const call = prisma.location.findMany.mock.calls[0]?.[0]; + if (!call) { + throw new Error("expected prisma.location.findMany to be called"); + } + return call as { take?: number; where?: Record }; + }; + + it("caps `take` at MAX_PAGINATION_TAKE", async () => { + await service.findMany(); + expect(getFindManyArgs().take).toBe(MAX_PAGINATION_TAKE); + }); + + it("filters by type when provided", async () => { + await service.findMany({ type: LocationType.PROVINCE }); + expect(getFindManyArgs().where).toEqual({ type: LocationType.PROVINCE }); + }); + + it("filters by parentId (including null for root nodes)", async () => { + await service.findMany({ parentId: null }); + expect(getFindManyArgs().where).toEqual({ parentId: null }); + }); + + it("filters by case-insensitive name contains for searchTerm", async () => { + await service.findMany({ searchTerm: "noord" }); + expect(getFindManyArgs().where).toEqual({ + name: { contains: "noord", mode: "insensitive" }, + }); + }); + + it("filters by id array", async () => { + await service.findMany({ id: ["a", "b"] }); + expect(getFindManyArgs().where).toEqual({ id: { in: ["a", "b"] } }); + }); + + it("combines filters", async () => { + await service.findMany({ + type: LocationType.CITY, + parentId: "prov-1", + searchTerm: "amst", + }); + expect(getFindManyArgs().where).toEqual({ + type: LocationType.CITY, + parentId: "prov-1", + name: { contains: "amst", mode: "insensitive" }, + }); + }); +}); + +describe("LocationService.save", () => { + it("upserts with non-null code / parentId on update branch", async () => { + const { service, prisma, mapper } = buildService(); + const entity = { + id: "loc-1", + name: "Utrecht", + type: LocationType.CITY, + code: "utr", + parentId: "prov-1", + createdAt: new Date(), + updatedAt: new Date(), + } as never; + prisma.location.upsert.mockResolvedValue(entity); + mapper.toDomain.mockReturnValue(entity); + + await service.save(entity); + + const upsertArgs = prisma.location.upsert.mock.calls[0]?.[0] as { + where: unknown; + create: unknown; + update: { name: string; code: string | null; parentId: string | null }; + }; + expect(upsertArgs.where).toEqual({ id: "loc-1" }); + expect(upsertArgs.update.name).toBe("Utrecht"); + expect(upsertArgs.update.code).toBe("utr"); + expect(upsertArgs.update.parentId).toBe("prov-1"); + }); + + it("coerces undefined code / parentId to null on update branch", async () => { + const { service, prisma, mapper } = buildService(); + const entity = { + id: "loc-2", + name: "Drenthe", + type: LocationType.PROVINCE, + createdAt: new Date(), + updatedAt: new Date(), + } as never; + prisma.location.upsert.mockResolvedValue(entity); + mapper.toDomain.mockReturnValue(entity); + + await service.save(entity); + + const upsertArgs = prisma.location.upsert.mock.calls[0]?.[0] as { + update: { code: string | null; parentId: string | null }; + }; + expect(upsertArgs.update.code).toBeNull(); + expect(upsertArgs.update.parentId).toBeNull(); + }); +}); + +describe("LocationService.destroy", () => { + it("deletes by id", async () => { + const { service, prisma } = buildService(); + await service.destroy({ id: "loc-1" } as never); + expect(prisma.location.delete).toHaveBeenCalledWith({ + where: { id: "loc-1" }, + }); + }); +}); diff --git a/packages/core/src/modules/vacancies/location/location.dto.ts b/packages/core/src/modules/vacancies/location/location.dto.ts new file mode 100644 index 0000000..5fe60be --- /dev/null +++ b/packages/core/src/modules/vacancies/location/location.dto.ts @@ -0,0 +1,14 @@ +import type { LocationType } from "./location.entity"; + +export interface CreateLocationDto { + name: string; + type: LocationType; + code?: string; + parentId?: string; +} + +export interface UpdateLocationDto { + name?: string; + code?: string; + parentId?: string; +} diff --git a/packages/core/src/modules/vacancies/location/location.factory.ts b/packages/core/src/modules/vacancies/location/location.factory.ts new file mode 100644 index 0000000..7cd42ac --- /dev/null +++ b/packages/core/src/modules/vacancies/location/location.factory.ts @@ -0,0 +1,25 @@ +import { Injectable } from "@nestjs/common"; +import { ClockService, Factory, UuidFactoryService } from "../../../shared"; +import { CreateLocationDto } from "./location.dto"; +import { Location } from "./location.entity"; + +@Injectable() +export class LocationFactory implements Factory { + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateLocationDto): Location { + const now = this.clock.now(); + return new Location( + this.uuidFactory.generate(), + dto.name, + dto.type, + now, + now, + dto.code, + dto.parentId, + ); + } +} diff --git a/packages/core/src/modules/vacancies/location/location.module.ts b/packages/core/src/modules/vacancies/location/location.module.ts index a99ec4d..53f5414 100644 --- a/packages/core/src/modules/vacancies/location/location.module.ts +++ b/packages/core/src/modules/vacancies/location/location.module.ts @@ -1,12 +1,33 @@ import { Module } from "@nestjs/common"; +import { BaseModule } from "../../../shared"; +import { AuthorizationModule } from "../../auth"; +import { AuthenticationModule } from "../../authentication/authentication.module"; import { DatabaseModule } from "../../database"; import { LocationDataLoaderService } from "./location.dataloader"; +import { LocationFactory } from "./location.factory"; import { LocationMapper } from "./location.mapper"; +import { LocationPolicy } from "./location.policy"; import { LocationService } from "./location.service"; @Module({ - imports: [DatabaseModule], - providers: [LocationService, LocationMapper, LocationDataLoaderService], - exports: [LocationService, LocationDataLoaderService], + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + ], + providers: [ + LocationService, + LocationMapper, + LocationFactory, + LocationPolicy, + LocationDataLoaderService, + ], + exports: [ + LocationService, + LocationMapper, + LocationFactory, + LocationDataLoaderService, + ], }) export class LocationModule {} diff --git a/packages/core/src/modules/vacancies/location/location.policy.ts b/packages/core/src/modules/vacancies/location/location.policy.ts new file mode 100644 index 0000000..04cd802 --- /dev/null +++ b/packages/core/src/modules/vacancies/location/location.policy.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@nestjs/common"; +import { Policy, PublicResourcePolicy } from "../../auth"; +import { Location } from "./location.entity"; + +@Injectable() +@Policy(Location) +export class LocationPolicy extends PublicResourcePolicy { + override create(): boolean { + return true; + } + + override update(): boolean { + return true; + } + + override delete(): boolean { + return true; + } +} diff --git a/packages/core/src/modules/vacancies/location/location.service.ts b/packages/core/src/modules/vacancies/location/location.service.ts index 25149d5..7aa3935 100644 --- a/packages/core/src/modules/vacancies/location/location.service.ts +++ b/packages/core/src/modules/vacancies/location/location.service.ts @@ -1,8 +1,17 @@ import { Injectable } from "@nestjs/common"; +import { Prisma } from "@prisma/client"; +import { assertIdBatchSize, MAX_PAGINATION_TAKE } from "../../../shared"; import { PrismaService } from "../../database"; -import { Location } from "./location.entity"; +import { Location, LocationType } from "./location.entity"; import { LocationMapper } from "./location.mapper"; +export interface LocationFilters { + id?: string[]; + type?: LocationType; + parentId?: string | null; + searchTerm?: string; +} + @Injectable() export class LocationService { constructor( @@ -15,9 +24,75 @@ export class LocationService { return this.mapper.toDomain(location); } - async findMany(filter?: { id?: string[] }): Promise { - const where = filter?.id ? { id: { in: filter.id } } : {}; - const items = await this.prisma.location.findMany({ where }); + async findByIdOrFail(id: string): Promise { + const location = await this.findById(id); + if (!location) { + throw new Error(`Location with id ${id} not found`); + } + return location; + } + + async findMany(filter: LocationFilters = {}): Promise { + const items = await this.prisma.location.findMany({ + where: this.buildWhere(filter), + orderBy: { name: "asc" }, + take: MAX_PAGINATION_TAKE, + }); return this.mapper.mapToDomain(items); } + + async count(filter: LocationFilters = {}): Promise { + return this.prisma.location.count({ where: this.buildWhere(filter) }); + } + + async save(entity: Location): Promise { + const data = this.buildPrismaData(entity); + const prismaEntity = await this.prisma.location.upsert({ + where: { id: entity.id }, + create: { id: entity.id, ...data }, + update: { + name: data.name, + code: data.code ?? null, + parentId: data.parentId ?? null, + }, + }); + const mapped = this.mapper.toDomain(prismaEntity); + if (mapped === null) { + throw new Error("Failed to map Location entity after save"); + } + return mapped; + } + + async destroy(entity: Location): Promise { + await this.prisma.location.delete({ where: { id: entity.id } }); + } + + private buildWhere(filter: LocationFilters): Prisma.LocationWhereInput { + const where: Prisma.LocationWhereInput = {}; + if (filter.id) { + assertIdBatchSize(filter.id, "Location"); + where.id = { in: filter.id }; + } + if (filter.type !== undefined) { + where.type = filter.type; + } + if (filter.parentId !== undefined) { + where.parentId = filter.parentId; + } + if (filter.searchTerm) { + where.name = { contains: filter.searchTerm, mode: "insensitive" }; + } + return where; + } + + private buildPrismaData( + entity: Location, + ): Prisma.LocationUncheckedCreateInput { + return { + name: entity.name, + type: entity.type, + code: entity.code ?? null, + parentId: entity.parentId ?? null, + }; + } } -- 2.51.2