From 5a284fe6cf5f2ab2965abe401503dceb1037be45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 7 Aug 2025 01:44:15 +0200 Subject: [PATCH] [WIP] Add aggregated address search API endpoint Introduced a new API endpoint `/api/1.0/main/address-reference/aggregated/search` for aggregated address reference search with support for query filtering. Extended repository with `findAggregatedBySearchString` method and updated materialized view `view_chill_main_address_reference`. Added test coverage and API specification details. --- ...ddressReferenceAggregatedApiController.php | 50 +++++++++++++++++++ .../Repository/AddressReferenceRepository.php | 34 +++++++++++++ .../AddressReferenceRepositoryTest.php | 14 ++++++ .../ChillMainBundle/chill.api.specs.yaml | 19 +++++++ .../migrations/Version20250214154310.php | 15 +++--- 5 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Controller/AddressReferenceAggregatedApiController.php diff --git a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAggregatedApiController.php b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAggregatedApiController.php new file mode 100644 index 000000000..9976d939e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAggregatedApiController.php @@ -0,0 +1,50 @@ +security->isGranted('IS_AUTHENTICATED')) { + throw new AccessDeniedHttpException(); + } + + if (!$request->query->has('q')) { + throw new BadRequestHttpException('Parameter "q" is required.'); + } + + $q = trim($request->query->get('q')); + + if ('' === $q) { + throw new BadRequestHttpException('Parameter "q" is required and cannot be empty.'); + } + + $result = $this->addressReferenceRepository->findAggregatedBySearchString($q); + + return new JsonResponse(iterator_to_array($result)); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php index cda989c83..741d99b2c 100644 --- a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php @@ -67,6 +67,40 @@ final readonly class AddressReferenceRepository implements ObjectRepository return $this->repository->findAll(); } + public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable + { + $terms = $this->buildTermsFromSearchString($search); + if ([] === $terms) { + return []; + } + + $connection = $this->entityManager->getConnection(); + $qb = $connection->createQueryBuilder(); + + $qb->select('var.street AS street', 'cmpc.id AS postcode_id', 'cmpc.code AS code', 'cmpc.label AS label', 'jsonb_object_agg(var.address_id, var.streetnumber ORDER BY var.row_number) AS positions') + ->from('view_chill_main_address_reference', 'var') + ->innerJoin('var', 'chill_main_postal_code', 'cmpc', 'cmpc.id = var.postcode_id') + ->groupBy('cmpc.id', 'var.street') + ->setFirstResult($firstResult) + ->setMaxResults($maxResults); + + $paramId = 0; + + foreach ($terms as $k => $term) { + $qb->andWhere('var.address like ?'); + $qb->setParameter(++$paramId, "%{$term}%"); + } + + if (null !== $postalCode) { + $qb->andWhere('var.postcode_id = ?'); + $qb->setParameter(++$paramId, $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode); + } + + $result = $qb->executeQuery(); + + return $result->iterateAssociative(); + } + /** * @return iterable */ diff --git a/src/Bundle/ChillMainBundle/Tests/Repository/AddressReferenceRepositoryTest.php b/src/Bundle/ChillMainBundle/Tests/Repository/AddressReferenceRepositoryTest.php index c70bcd999..f0f500404 100644 --- a/src/Bundle/ChillMainBundle/Tests/Repository/AddressReferenceRepositoryTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Repository/AddressReferenceRepositoryTest.php @@ -56,6 +56,20 @@ class AddressReferenceRepositoryTest extends KernelTestCase self::assertIsInt($actual, $text); } + /** + * @dataProvider generateSearchString + */ + public function testFindAggreggateBySearchString(string $search, int|PostalCode|null $postalCode, string $text, ?array $expected = null): void + { + $actual = static::$repository->findAggregatedBySearchString($search, $postalCode); + + self::assertIsIterable($actual, $text); + + if (null !== $expected) { + self::assertEquals($expected, iterator_to_array($actual)); + } + } + public static function generateSearchString(): iterable { self::bootKernel(); diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index d87a0eb71..9c5f7d84a 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -595,6 +595,25 @@ paths: 401: description: "Unauthorized" + /1.0/main/address-reference/aggregated/search: + get: + tags: + - address + summary: Search for address reference aggregated + parameters: + - name: q + in: query + required: true + description: The search pattern + schema: + type: string + responses: + 200: + description: "ok" + 401: + description: "Unauthorized" + 400: + description: "Bad Request" /1.0/main/postal-code/search.json: get: tags: diff --git a/src/Bundle/ChillMainBundle/migrations/Version20250214154310.php b/src/Bundle/ChillMainBundle/migrations/Version20250214154310.php index 3de3ee36c..d085ce11c 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20250214154310.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20250214154310.php @@ -24,12 +24,15 @@ final class Version20250214154310 extends AbstractMigration public function up(Schema $schema): void { $this->addSql(<<<'SQL' - create materialized view IF NOT EXISTS view_chill_main_address_reference as - SELECT row_number() OVER () AS row_number, - cmar.id AS address_id, - lower(unaccent(cmar.street || ' '::text || cmar.streetnumber || ' '::text || cmpc.code::text || ' '::text || - cmpc.label::text)) AS address, - cmpc.id AS postcode_id + create materialized view public.view_chill_main_address_reference as + SELECT row_number() OVER () AS row_number, + cmar.street AS street, + cmar.streetnumber AS streetnumber, + cmar.id AS address_id, + lower(unaccent( + (((((cmar.street || ' '::text) || cmar.streetnumber) || ' '::text) || cmpc.code::text) || ' '::text) || + cmpc.label::text)) AS address, + cmpc.id AS postcode_id FROM chill_main_address_reference cmar JOIN chill_main_postal_code cmpc ON cmar.postcode_id = cmpc.id WHERE cmar.deletedat IS NULL