Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
15ae283
WIP - public search with OpenSearch
landonshumway-ia Feb 27, 2026
683d5c6
Move public search logic into separate file
landonshumway-ia Mar 2, 2026
d3c83d3
Add additional cursor tests to verify pagination
landonshumway-ia Mar 3, 2026
9eb8be9
Add mock DB setup for remaining search tests
landonshumway-ia Mar 3, 2026
771aa6b
Update CDK tests
landonshumway-ia Mar 3, 2026
d0b23be
Remove complex pagination logic
landonshumway-ia Mar 26, 2026
e41a7d2
Add support for sorting results by familyName or dateOfUpdate
landonshumway-ia Mar 27, 2026
17bd140
Cleanup response schema validation
landonshumway-ia Mar 27, 2026
c0d9e73
PR feedback - set license number max length to match api spec
landonshumway-ia Mar 27, 2026
8216615
Set all public request schemas to limit license number to 100 chars
landonshumway-ia Mar 27, 2026
c00141f
Remove previous public search implementation
landonshumway-ia Mar 27, 2026
8c9c69b
Add public license response schema for cosm public lookup
landonshumway-ia Mar 27, 2026
ab36ddd
linter/formatting
landonshumway-ia Mar 27, 2026
430e17e
Add licenseType to top level public search response
landonshumway-ia Mar 27, 2026
a4e52fb
Add public license api model to response schema
landonshumway-ia Mar 27, 2026
15486e2
Remove aa and history fields from privilege public response model
landonshumway-ia Mar 27, 2026
3278730
Update API specs to latest
landonshumway-ia Mar 27, 2026
de20e40
Update API specs to latest
landonshumway-ia Mar 27, 2026
c0f4c69
remove provider id access pattern from public query
landonshumway-ia Mar 27, 2026
2cd2840
Add ability to reset indexes when populating documents
landonshumway-ia Mar 31, 2026
32557d0
Grant populate provider documents lambda permission to reset
landonshumway-ia Mar 31, 2026
bca7ab5
Check for index under alias name
landonshumway-ia Apr 1, 2026
dddacd0
linter/comments
landonshumway-ia Apr 1, 2026
520fa83
Remove search filter constraints
landonshumway-ia Apr 1, 2026
096f2e5
PR feedback - set timeout
landonshumway-ia Apr 1, 2026
2628c42
Prevent reset functionality in prod environment
landonshumway-ia Apr 1, 2026
76cb22f
Update cosmetology python requirements to latest
landonshumway-ia Apr 1, 2026
d068ac9
formatting
landonshumway-ia Apr 1, 2026
f370eee
PR feedback - cleanup tests/comments
landonshumway-ia Apr 9, 2026
aebcc40
fix example in mock license script
landonshumway-ia Apr 9, 2026
1c4d5c7
Remove irrelevant query provider smoke test
landonshumway-ia Apr 9, 2026
9824eef
PR feedback - validate path parameter
landonshumway-ia Apr 9, 2026
7f00cf0
PR feedback sort by inner license record
landonshumway-ia Apr 9, 2026
1c78620
formatting
landonshumway-ia Apr 9, 2026
f839d5a
Update nodemailer dep to latest
landonshumway-ia Apr 9, 2026
8c773ba
PR feedback - check missing jurisdiction with schema validation
landonshumway-ia Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/cosmetology-app/bin/run_python_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'lambdas/python/disaster-recovery',
'lambdas/python/migration',
'lambdas/python/provider-data-v1',
'lambdas/python/search',
'lambdas/python/staff-user-pre-token',
'lambdas/python/staff-users',
'.', # CDK tests
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ruff: noqa: N801, N815, ARG002 invalid-name unused-argument
from marshmallow import ValidationError, validates_schema
from marshmallow import ValidationError, post_load, validates_schema
from marshmallow.fields import Integer, List, Nested, Raw, String
from marshmallow.validate import Length, Range, Regexp

Expand Down Expand Up @@ -208,6 +208,19 @@ class ProviderPublicResponseSchema(ForgivingSchema):
# Note the lack of `licenses` here: we do not return license data for public endpoints


class PublicLicenseSearchResponseSchema(ForgivingSchema):
"""
License object fields returned by the public query providers endpoint (OpenSearch-backed).
Jurisdiction is renamed to licenseJurisdiction for parity with JCC implementation.
Comment thread
jlkravitz marked this conversation as resolved.
Outdated
"""

providerId = Raw(required=True, allow_none=False)
givenName = String(required=True, allow_none=False, validate=Length(1, 100))
familyName = String(required=True, allow_none=False, validate=Length(1, 100))
licenseJurisdiction = String(required=True, allow_none=False)
compact = Compact(required=True, allow_none=False)
licenseNumber = String(required=True, allow_none=False, validate=Length(1, 100))
Comment thread
landonshumway-ia marked this conversation as resolved.

class QueryProvidersRequestSchema(CCRequestSchema):
"""
Schema for query providers requests.
Expand All @@ -228,14 +241,15 @@ class QuerySchema(CCRequestSchema):
jurisdiction = Jurisdiction(required=False, allow_none=False)
givenName = String(required=False, allow_none=False, validate=Length(min=1, max=100))
familyName = String(required=False, allow_none=False, validate=Length(min=1, max=100))
licenseNumber = String(required=False, allow_none=False, validate=Length(min=1, max=100))
Comment thread
coderabbitai[bot] marked this conversation as resolved.

class PaginationSchema(ForgivingSchema):
"""
Nested schema for the pagination object within the request.
"""

lastKey = String(required=False, allow_none=False, validate=Length(min=1, max=1024))
pageSize = Integer(required=False, allow_none=False)
pageSize = Integer(required=False, allow_none=False, validate=Range(min=5, max=100))

class SortingSchema(ForgivingSchema):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,26 @@ def test_general_response_schema_does_not_include_date_of_birth_in_licenses(self
self.assertNotIn('dateOfBirth', result['licenses'][0])


class TestQueryProvidersRequestSchema(TstLambdas):
"""QueryProvidersRequestSchema.QuerySchema licenseNumber length matches API Gateway model (max 100)."""

def test_query_license_number_accepts_100_chars(self):
from cc_common.data_model.schema.provider.api import QueryProvidersRequestSchema

ln = 'x' * 100
body = {'query': {'licenseNumber': ln, 'jurisdiction': 'oh'}}
loaded = QueryProvidersRequestSchema().load(body)
self.assertEqual(ln, loaded['query']['licenseNumber'])

def test_query_license_number_rejects_over_100_chars(self):
from cc_common.data_model.schema.provider.api import QueryProvidersRequestSchema

body = {'query': {'licenseNumber': 'x' * 101, 'jurisdiction': 'oh'}}
with self.assertRaises(ValidationError) as ctx:
QueryProvidersRequestSchema().load(body)
self.assertIn('licenseNumber', ctx.exception.messages['query'])


class TestProviderRecordSchema(TstLambdas):
def test_serde(self):
"""Test round-trip deserialization/serialization"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,108 +8,6 @@
from . import get_provider_information


@api_handler
def public_query_providers(event: dict, context: LambdaContext): # noqa: ARG001 unused-argument
"""Query providers data
:param event: Standard API Gateway event, API schema documented in the CDK ApiStack
:param LambdaContext context:
"""
compact = event['pathParameters']['compact']

# Parse and validate the request body using the schema to strip whitespace
try:
schema = QueryProvidersRequestSchema()
body = schema.loads(event['body'])
except ValidationError as e:
logger.warning('Invalid request body', errors=e.messages)
raise CCInvalidRequestException(f'Invalid request: {e.messages}') from e

query = body.get('query', {})
if 'providerId' in query.keys():
provider_id = query['providerId']
query = {'providerId': provider_id}
resp = config.data_client.get_provider(
compact=compact,
provider_id=provider_id,
pagination=body.get('pagination'),
detail=False,
)
resp['query'] = query

else:
if 'givenName' in query.keys() and 'familyName' not in query.keys():
raise CCInvalidRequestException('familyName is required if givenName is provided')
provider_name = None
if 'familyName' in query.keys():
provider_name = (query.get('familyName'), query.get('givenName'))

jurisdiction = query.get('jurisdiction')

if jurisdiction:
# If the request is for a jurisdiction that is not live yet in the compact,
# we return empty results to reduce the number of queries made
# otherwise the request will search through the entire data set
if not config.compact_configuration_client.is_jurisdiction_live_in_compact(compact, jurisdiction):
logger.info(
'Jurisdiction is not live in compact, returning empty results',
compact=compact,
jurisdiction=jurisdiction,
)
return {
'query': query,
'sorting': body.get('sorting', {}),
'providers': [],
'pagination': body.get('pagination', {}),
}

sorting = body.get('sorting', {})
sorting_key = sorting.get('key')

sort_direction = sorting.get('direction', 'ascending')
scan_forward = sort_direction == 'ascending'

match sorting_key:
case None | 'familyName':
resp = {
'query': query,
'sorting': {'key': 'familyName', 'direction': sort_direction},
**config.data_client.get_providers_sorted_by_family_name(
compact=compact,
jurisdiction=jurisdiction,
provider_name=provider_name,
scan_forward=scan_forward,
pagination=body.get('pagination'),
),
}
case 'dateOfUpdate':
if provider_name is not None:
raise CCInvalidRequestException(
'givenName and familyName are not supported for sorting by dateOfUpdate',
)
resp = {
'query': query,
'sorting': {'key': 'dateOfUpdate', 'direction': sort_direction},
**config.data_client.get_providers_sorted_by_updated(
compact=compact,
jurisdiction=jurisdiction,
scan_forward=scan_forward,
pagination=body.get('pagination'),
),
}
case _:
# This shouldn't happen unless our api validation gets misconfigured
raise CCInvalidRequestException(f"Invalid sort key: '{sorting_key}'")
# Convert generic field to more specific one for this API and sanitize data
unsanitized_providers = resp.pop('items', [])
# for the public query endpoint, we only return publicly available data
public_schema = ProviderPublicResponseSchema()
sanitized_providers = [public_schema.load(provider) for provider in unsanitized_providers]

resp['providers'] = sanitized_providers

return resp


@api_handler
def public_get_provider(event: dict, context: LambdaContext): # noqa: ARG001 unused-argument
"""Return one provider's data
Expand Down
Loading
Loading