diff --git a/api/src/feeds/impl/feeds_api_impl.py b/api/src/feeds/impl/feeds_api_impl.py index 998090152..262390db2 100644 --- a/api/src/feeds/impl/feeds_api_impl.py +++ b/api/src/feeds/impl/feeds_api_impl.py @@ -1,6 +1,7 @@ from datetime import datetime from typing import List, Union, TypeVar +from sqlalchemy import or_ from sqlalchemy import select from sqlalchemy.orm import joinedload from sqlalchemy.orm.query import Query @@ -39,7 +40,6 @@ from feeds_gen.models.gtfs_feed import GtfsFeed from feeds_gen.models.gtfs_rt_feed import GtfsRTFeed from middleware.request_context import is_user_email_restricted -from sqlalchemy import or_ from utils.date_utils import valid_iso_date from utils.location_translation import ( create_location_translation_object, @@ -89,12 +89,15 @@ def get_feeds( status: str, provider: str, producer_url: str, + is_official: bool, ) -> List[BasicFeed]: """Get some (or all) feeds from the Mobility Database.""" feed_filter = FeedFilter( status=status, provider__ilike=provider, producer_url__ilike=producer_url, stable_id=None ) feed_query = feed_filter.filter(Database().get_query_model(Feed)) + if is_official: + feed_query = feed_query.filter(Feed.official) feed_query = feed_query.filter(Feed.data_type != "gbfs") # Filter out GBFS feeds feed_query = feed_query.filter( or_( @@ -221,6 +224,7 @@ def get_gtfs_feeds( dataset_latitudes: str, dataset_longitudes: str, bounding_filter_method: str, + is_official: bool, ) -> List[GtfsFeed]: """Get some (or all) GTFS feeds from the Mobility Database.""" gtfs_feed_filter = GtfsFeedFilter( @@ -258,9 +262,10 @@ def get_gtfs_feeds( *BasicFeedImpl.get_joinedload_options(), ) .order_by(Gtfsfeed.provider, Gtfsfeed.stable_id) - .limit(limit) - .offset(offset) ) + if is_official: + feed_query = feed_query.filter(Feed.official) + feed_query = feed_query.limit(limit).offset(offset) return self._get_response(feed_query, GtfsFeedImpl) def get_gtfs_rt_feed( @@ -311,6 +316,7 @@ def get_gtfs_rt_feeds( country_code: str, subdivision_name: str, municipality: str, + is_official: bool, ) -> List[GtfsRTFeed]: """Get some (or all) GTFS Realtime feeds from the Mobility Database.""" entity_types_list = entity_types.split(",") if entity_types else None @@ -359,9 +365,10 @@ def get_gtfs_rt_feeds( *BasicFeedImpl.get_joinedload_options(), ) .order_by(Gtfsrealtimefeed.provider, Gtfsrealtimefeed.stable_id) - .limit(limit) - .offset(offset) ) + if is_official: + feed_query = feed_query.filter(Feed.official) + feed_query = feed_query.limit(limit).offset(offset) return self._get_response(feed_query, GtfsRTFeedImpl) @staticmethod diff --git a/api/src/feeds/impl/models/basic_feed_impl.py b/api/src/feeds/impl/models/basic_feed_impl.py index 19bce6662..489d0cd2f 100644 --- a/api/src/feeds/impl/models/basic_feed_impl.py +++ b/api/src/feeds/impl/models/basic_feed_impl.py @@ -23,10 +23,14 @@ class Config: def from_orm(cls, feed: Feed | None, _=None) -> BasicFeed | None: if not feed: return None + latest_official_status = None + if len(feed.officialstatushistories) > 0: + latest_official_status = max(feed.officialstatushistories, key=lambda x: x.timestamp).is_official return cls( id=feed.stable_id, data_type=feed.data_type, status=feed.status, + official=latest_official_status, created_at=feed.created_at, external_ids=sorted( [ExternalIdImpl.from_orm(item) for item in feed.externalids], key=lambda x: x.external_id @@ -48,7 +52,12 @@ def from_orm(cls, feed: Feed | None, _=None) -> BasicFeed | None: @staticmethod def get_joinedload_options() -> [_AbstractLoad]: """Returns common joinedload options for feeds queries.""" - return [joinedload(Feed.locations), joinedload(Feed.externalids), joinedload(Feed.redirectingids)] + return [ + joinedload(Feed.locations), + joinedload(Feed.externalids), + joinedload(Feed.redirectingids), + joinedload(Feed.officialstatushistories), + ] class BasicFeedImpl(BaseFeedImpl, BasicFeed): diff --git a/api/src/feeds/impl/models/search_feed_item_result_impl.py b/api/src/feeds/impl/models/search_feed_item_result_impl.py index d0e062bf9..ab3a98700 100644 --- a/api/src/feeds/impl/models/search_feed_item_result_impl.py +++ b/api/src/feeds/impl/models/search_feed_item_result_impl.py @@ -25,6 +25,7 @@ def from_orm_gtfs(cls, feed_search_row): external_ids=feed_search_row.external_ids, provider=feed_search_row.provider, feed_name=feed_search_row.feed_name, + official=feed_search_row.official, note=feed_search_row.note, feed_contact_email=feed_search_row.feed_contact_email, source_info=SourceInfo( @@ -58,6 +59,7 @@ def from_orm_gtfs_rt(cls, feed_search_row): external_ids=feed_search_row.external_ids, provider=feed_search_row.provider, feed_name=feed_search_row.feed_name, + official=feed_search_row.official, note=feed_search_row.note, feed_contact_email=feed_search_row.feed_contact_email, source_info=SourceInfo( diff --git a/api/src/feeds/impl/search_api_impl.py b/api/src/feeds/impl/search_api_impl.py index e8906b13d..2519868d2 100644 --- a/api/src/feeds/impl/search_api_impl.py +++ b/api/src/feeds/impl/search_api_impl.py @@ -31,7 +31,7 @@ def get_parsed_search_tsquery(search_query: str) -> str: return func.plainto_tsquery("english", unaccent(parsed_query)) @staticmethod - def add_search_query_filters(query, search_query, data_type, feed_id, status) -> Query: + def add_search_query_filters(query, search_query, data_type, feed_id, status, is_official) -> Query: """ Add filters to the search query. Filter values are trimmed and converted to lowercase. @@ -53,6 +53,8 @@ def add_search_query_filters(query, search_query, data_type, feed_id, status) -> status_list = [s.strip().lower() for s in status[0].split(",") if s] if status_list: query = query.where(t_feedsearch.c.status.in_([s.strip().lower() for s in status_list])) + if is_official is not None and is_official: + query = query.where(t_feedsearch.c.official == is_official) if search_query and len(search_query.strip()) > 0: query = query.filter( t_feedsearch.c.document.op("@@")(SearchApiImpl.get_parsed_search_tsquery(search_query)) @@ -60,15 +62,23 @@ def add_search_query_filters(query, search_query, data_type, feed_id, status) -> return query @staticmethod - def create_count_search_query(status: List[str], feed_id: str, data_type: str, search_query: str) -> Query: + def create_count_search_query( + status: List[str], + feed_id: str, + data_type: str, + is_official: bool, + search_query: str, + ) -> Query: """ Create a search query for the database. """ query = select(func.count(t_feedsearch.c.feed_id)) - return SearchApiImpl.add_search_query_filters(query, search_query, data_type, feed_id, status) + return SearchApiImpl.add_search_query_filters(query, search_query, data_type, feed_id, status, is_official) @staticmethod - def create_search_query(status: List[str], feed_id: str, data_type: str, search_query: str) -> Query: + def create_search_query( + status: List[str], feed_id: str, data_type: str, is_official: bool, search_query: str + ) -> Query: """ Create a search query for the database. """ @@ -80,7 +90,7 @@ def create_search_query(status: List[str], feed_id: str, data_type: str, search_ rank_expression, *feed_search_columns, ) - query = SearchApiImpl.add_search_query_filters(query, search_query, data_type, feed_id, status) + query = SearchApiImpl.add_search_query_filters(query, search_query, data_type, feed_id, status, is_official) return query.order_by(rank_expression.desc()) def search_feeds( @@ -90,17 +100,18 @@ def search_feeds( status: List[str], feed_id: str, data_type: str, + is_official: bool, search_query: str, ) -> SearchFeeds200Response: """Search feeds using full-text search on feed, location and provider's information.""" - query = self.create_search_query(status, feed_id, data_type, search_query) + query = self.create_search_query(status, feed_id, data_type, is_official, search_query) feed_rows = Database().select( query=query, limit=limit, offset=offset, ) feed_total_count = Database().select( - query=self.create_count_search_query(status, feed_id, data_type, search_query), + query=self.create_count_search_query(status, feed_id, data_type, is_official, search_query), ) if feed_rows is None or feed_total_count is None: return SearchFeeds200Response( diff --git a/api/src/scripts/populate_db_test_data.py b/api/src/scripts/populate_db_test_data.py index c12e0ff0f..5beb7649b 100644 --- a/api/src/scripts/populate_db_test_data.py +++ b/api/src/scripts/populate_db_test_data.py @@ -14,6 +14,7 @@ Feature, t_feedsearch, Location, + Officialstatushistory, ) from scripts.populate_db import set_up_configs, DatabasePopulateHelper from utils.logger import Logger @@ -172,6 +173,14 @@ def populate_test_feeds(self, feeds_data): ) locations.append(location) feed.locations = locations + if "official" in feed_data: + official_status_history = Officialstatushistory( + feed_id=feed.id, + is_official=feed_data["official"], + reviewer_email="dev@test.com", + timestamp=feed_data["created_at"], + ) + feed.officialstatushistories.append(official_status_history) self.db.session.add(feed) logger.info(f"Added feed {feed.stable_id}") diff --git a/api/tests/integration/test_data/extra_test_data.json b/api/tests/integration/test_data/extra_test_data.json index e963c3c6b..68f247782 100644 --- a/api/tests/integration/test_data/extra_test_data.json +++ b/api/tests/integration/test_data/extra_test_data.json @@ -6,6 +6,7 @@ "status": "active", "created_at": "2024-02-08T00:00:00Z", "provider": "BlaBlaCar Bus", + "official": true, "feed_name": "", "note": "", "feed_contact_email": "", @@ -43,6 +44,7 @@ "status": "active", "created_at": "2024-02-08T00:00:00Z", "provider": "BlaBlaCar Bus", + "official": false, "feed_name": "", "note": "", "feed_contact_email": "", @@ -80,6 +82,7 @@ "status": "active", "created_at": "2024-02-08T00:00:00Z", "provider": "BlaBlaCar Bus", + "official": true, "feed_name": "", "note": "", "feed_contact_email": "", diff --git a/api/tests/integration/test_database.py b/api/tests/integration/test_database.py index c819d870a..a30fa595a 100644 --- a/api/tests/integration/test_database.py +++ b/api/tests/integration/test_database.py @@ -100,7 +100,7 @@ def test_bounding_box_disjoint(latitudes, longitudes, method, expected_found, te def test_merge_gtfs_feed(test_database): results = { feed.id: feed - for feed in FeedsApiImpl().get_gtfs_feeds(None, None, None, None, None, None, None, None, None, None) + for feed in FeedsApiImpl().get_gtfs_feeds(None, None, None, None, None, None, None, None, None, None, None) if feed.id in TEST_GTFS_FEED_STABLE_IDS } assert len(results) == len(TEST_GTFS_FEED_STABLE_IDS) diff --git a/api/tests/integration/test_feeds_api.py b/api/tests/integration/test_feeds_api.py index f762a8f8b..0d2e2a6a6 100644 --- a/api/tests/integration/test_feeds_api.py +++ b/api/tests/integration/test_feeds_api.py @@ -246,6 +246,44 @@ def test_feeds_gtfs_rt_id_get(client: TestClient): assert response.status_code == 200 +@pytest.mark.parametrize( + "endpoint", + [ + "/v1/gtfs_feeds", + "/v1/gtfs_rt_feeds", + "/v1/feeds", + ], +) +def test_feeds_filter_by_official(client: TestClient, endpoint): + # 1 - Test with official=false should return all feeds + response_no_filter = client.request( + "GET", + endpoint, + headers=authHeaders, + ) + assert response_no_filter.status_code == 200 + response_no_filter_json = response_no_filter.json() + response_official_false = client.request( + "GET", + endpoint, + headers=authHeaders, + params=[("is_official", "false")], + ) + assert response_official_false.status_code == 200 + response_official_false_json = response_official_false.json() + assert response_no_filter_json == response_official_false_json, "official=false parameter should return all feeds" + # 2 - Test with official=true should return at least one feed + response = client.request( + "GET", + endpoint, + headers=authHeaders, + params=[("is_official", "true")], + ) + assert response.status_code == 200 + json_response = response.json() + assert len(json_response) < len(response_no_filter_json), "Not all feeds are official" + + def test_non_existent_gtfs_rt_feed_get(client: TestClient): """Test case for feeds_gtfs_rt_id_get with a non-existent feed""" response = client.request( diff --git a/api/tests/integration/test_search_api.py b/api/tests/integration/test_search_api.py index 93f2fd15a..7148820f4 100644 --- a/api/tests/integration/test_search_api.py +++ b/api/tests/integration/test_search_api.py @@ -388,3 +388,26 @@ def test_search_feeds_filter_accents(client: TestClient, values: dict): assert len(response_body.results) == len(values["expected_ids"]) assert response_body.total == len(values["expected_ids"]) assert all(result.id in values["expected_ids"] for result in response_body.results) + + +def test_search_filter_by_official_status(client: TestClient): + """ + Retrieve feeds with the official status. + """ + params = [ + ("limit", 100), + ("offset", 0), + ("is_official", "true"), + ] + headers = { + "Authentication": "special-key", + } + response = client.request( + "GET", + "/v1/search", + headers=headers, + params=params, + ) + # Parse the response body into a Python object + response_body = SearchFeeds200Response.parse_obj(response.json()) + assert response_body.total == 2, "There should be 2 official feeds in extra_test_data.json" diff --git a/api/tests/unittest/models/test_search_feed_item_result_impl.py b/api/tests/unittest/models/test_search_feed_item_result_impl.py index 54ef891ce..db699ddae 100644 --- a/api/tests/unittest/models/test_search_feed_item_result_impl.py +++ b/api/tests/unittest/models/test_search_feed_item_result_impl.py @@ -23,6 +23,7 @@ def __init__(self, **kwargs): data_type="gtfs", status="active", feed_name="feed_name", + official=None, note="note", feed_contact_email="feed_contact_email", producer_url="producer_url", diff --git a/docs/DatabaseCatalogAPI.yaml b/docs/DatabaseCatalogAPI.yaml index 467c7dfd5..1ddf2c731 100644 --- a/docs/DatabaseCatalogAPI.yaml +++ b/docs/DatabaseCatalogAPI.yaml @@ -47,6 +47,7 @@ paths: - $ref: "#/components/parameters/status" - $ref: "#/components/parameters/provider" - $ref: "#/components/parameters/producer_url" + - $ref: "#/components/parameters/is_official_query_param" security: - Authentication: [ ] @@ -97,6 +98,7 @@ paths: - $ref: "#/components/parameters/dataset_latitudes" - $ref: "#/components/parameters/dataset_longitudes" - $ref: "#/components/parameters/bounding_filter_method" + - $ref: "#/components/parameters/is_official_query_param" security: - Authentication: [] @@ -123,6 +125,7 @@ paths: - $ref: "#/components/parameters/country_code" - $ref: "#/components/parameters/subdivision_name" - $ref: "#/components/parameters/municipality" + - $ref: "#/components/parameters/is_official_query_param" security: - Authentication: [] responses: @@ -279,6 +282,7 @@ paths: - $ref: "#/components/parameters/statuses" - $ref: "#/components/parameters/feed_id_query_param" - $ref: "#/components/parameters/data_type_query_param" + - $ref: "#/components/parameters/is_official_query_param" - $ref: "#/components/parameters/search_text_query_param" security: - Authentication: [] @@ -352,6 +356,12 @@ components: type: string example: 2023-07-10T22:06:00Z format: date-time + official: + description: > + A boolean value indicating if the feed is official or not. + Official feeds are provided by the transit agency or a trusted source. + type: boolean + example: true external_ids: $ref: "#/components/schemas/ExternalIds" provider: @@ -463,6 +473,12 @@ components: type: string example: 2023-07-10T22:06:00Z format: date-time + official: + description: > + A boolean value indicating if the feed is official or not. + Official feeds are provided by the transit agency or a trusted source. + type: boolean + example: true external_ids: $ref: "#/components/schemas/ExternalIds" provider: @@ -993,6 +1009,15 @@ components: type: boolean default: false + is_official_query_param: + name: is_official + in: query + description: If true, only return official feeds. + required: False + schema: + type: boolean + default: false + limit_query_param: name: limit in: query diff --git a/liquibase/changelog.xml b/liquibase/changelog.xml index 5c86b287c..5b6442927 100644 --- a/liquibase/changelog.xml +++ b/liquibase/changelog.xml @@ -31,4 +31,6 @@ + + \ No newline at end of file diff --git a/liquibase/changes/feat_794.sql b/liquibase/changes/feat_794.sql new file mode 100644 index 000000000..37dfb135e --- /dev/null +++ b/liquibase/changes/feat_794.sql @@ -0,0 +1,173 @@ +-- Dropping the materialized view if it exists as we cannot update it +DROP MATERIALIZED VIEW IF EXISTS FeedSearch; + +CREATE MATERIALIZED VIEW FeedSearch AS +SELECT + -- feed + Feed.stable_id AS feed_stable_id, + Feed.id AS feed_id, + Feed.data_type, + Feed.status, + Feed.feed_name, + Feed.note, + Feed.feed_contact_email, + -- source + Feed.producer_url, + Feed.authentication_info_url, + Feed.authentication_type, + Feed.api_key_parameter_name, + Feed.license_url, + Feed.provider, + Feed.operational_status, + -- official status + Latest_official_status.is_official AS official, + -- latest_dataset + Latest_dataset.id AS latest_dataset_id, + Latest_dataset.hosted_url AS latest_dataset_hosted_url, + Latest_dataset.downloaded_at AS latest_dataset_downloaded_at, + Latest_dataset.bounding_box AS latest_dataset_bounding_box, + Latest_dataset.hash AS latest_dataset_hash, + -- external_ids + ExternalIdJoin.external_ids, + -- redirect_ids + RedirectingIdJoin.redirect_ids, + -- feed gtfs_rt references + FeedReferenceJoin.feed_reference_ids, + -- feed gtfs_rt entities + EntityTypeFeedJoin.entities, + -- locations + FeedLocationJoin.locations, + -- translations + FeedCountryTranslationJoin.translations AS country_translations, + FeedSubdivisionNameTranslationJoin.translations AS subdivision_name_translations, + FeedMunicipalityTranslationJoin.translations AS municipality_translations, + -- full-text searchable document + setweight(to_tsvector('english', coalesce(unaccent(Feed.feed_name), '')), 'C') || + setweight(to_tsvector('english', coalesce(unaccent(Feed.provider), '')), 'C') || + setweight(to_tsvector('english', coalesce(unaccent(( + SELECT string_agg( + coalesce(location->>'country_code', '') || ' ' || + coalesce(location->>'country', '') || ' ' || + coalesce(location->>'subdivision_name', '') || ' ' || + coalesce(location->>'municipality', ''), + ' ' + ) + FROM json_array_elements(FeedLocationJoin.locations) AS location + )), '')), 'A') || + setweight(to_tsvector('english', coalesce(unaccent(( + SELECT string_agg( + coalesce(translation->>'value', ''), + ' ' + ) + FROM json_array_elements(FeedCountryTranslationJoin.translations) AS translation + )), '')), 'A') || + setweight(to_tsvector('english', coalesce(unaccent(( + SELECT string_agg( + coalesce(translation->>'value', ''), + ' ' + ) + FROM json_array_elements(FeedSubdivisionNameTranslationJoin.translations) AS translation + )), '')), 'A') || + setweight(to_tsvector('english', coalesce(unaccent(( + SELECT string_agg( + coalesce(translation->>'value', ''), + ' ' + ) + FROM json_array_elements(FeedMunicipalityTranslationJoin.translations) AS translation + )), '')), 'A') AS document +FROM Feed +LEFT JOIN ( + SELECT * + FROM gtfsdataset + WHERE latest = true +) AS Latest_dataset ON Latest_dataset.feed_id = Feed.id AND Feed.data_type = 'gtfs' +LEFT JOIN ( + SELECT + feed_id, + json_agg(json_build_object('external_id', associated_id, 'source', source)) AS external_ids + FROM externalid + GROUP BY feed_id +) AS ExternalIdJoin ON ExternalIdJoin.feed_id = Feed.id +LEFT JOIN ( + SELECT + gtfs_rt_feed_id, + array_agg(FeedReferenceJoinInnerQuery.stable_id) AS feed_reference_ids + FROM FeedReference + LEFT JOIN Feed AS FeedReferenceJoinInnerQuery ON FeedReferenceJoinInnerQuery.id = FeedReference.gtfs_feed_id + GROUP BY gtfs_rt_feed_id +) AS FeedReferenceJoin ON FeedReferenceJoin.gtfs_rt_feed_id = Feed.id AND Feed.data_type = 'gtfs_rt' +LEFT JOIN ( + SELECT + target_id, + json_agg(json_build_object('target_id', target_id, 'comment', redirect_comment)) AS redirect_ids + FROM RedirectingId + GROUP BY target_id +) AS RedirectingIdJoin ON RedirectingIdJoin.target_id = Feed.id +LEFT JOIN ( + SELECT + LocationFeed.feed_id, + json_agg(json_build_object('country', country, 'country_code', country_code, 'subdivision_name', + subdivision_name, 'municipality', municipality)) AS locations + FROM Location + LEFT JOIN LocationFeed ON LocationFeed.location_id = Location.id + GROUP BY LocationFeed.feed_id +) AS FeedLocationJoin ON FeedLocationJoin.feed_id = Feed.id +LEFT JOIN ( + SELECT DISTINCT ON (feed_id) * + FROM officialstatushistory + ORDER BY feed_id, timestamp DESC +) AS Latest_official_status ON Latest_official_status.feed_id = Feed.id +LEFT JOIN ( + SELECT + LocationFeed.feed_id, + json_agg(json_build_object('value', Translation.value, 'key', Translation.key)) AS translations + FROM Location + LEFT JOIN Translation ON Location.country = Translation.key + LEFT JOIN LocationFeed ON LocationFeed.location_id = Location.id + WHERE Translation.language_code = 'en' + AND Translation.type = 'country' + AND Location.country IS NOT NULL + GROUP BY LocationFeed.feed_id +) AS FeedCountryTranslationJoin ON FeedCountryTranslationJoin.feed_id = Feed.id +LEFT JOIN ( + SELECT + LocationFeed.feed_id, + json_agg(json_build_object('value', Translation.value, 'key', Translation.key)) AS translations + FROM Location + LEFT JOIN Translation ON Location.subdivision_name = Translation.key + LEFT JOIN LocationFeed ON LocationFeed.location_id = Location.id + WHERE Translation.language_code = 'en' + AND Translation.type = 'subdivision_name' + AND Location.subdivision_name IS NOT NULL + GROUP BY LocationFeed.feed_id +) AS FeedSubdivisionNameTranslationJoin ON FeedSubdivisionNameTranslationJoin.feed_id = Feed.id +LEFT JOIN ( + SELECT + LocationFeed.feed_id, + json_agg(json_build_object('value', Translation.value, 'key', Translation.key)) AS translations + FROM Location + LEFT JOIN Translation ON Location.municipality = Translation.key + LEFT JOIN LocationFeed ON LocationFeed.location_id = Location.id + WHERE Translation.language_code = 'en' + AND Translation.type = 'municipality' + AND Location.municipality IS NOT NULL + GROUP BY LocationFeed.feed_id +) AS FeedMunicipalityTranslationJoin ON FeedMunicipalityTranslationJoin.feed_id = Feed.id +LEFT JOIN ( + SELECT + feed_id, + array_agg(entity_name) AS entities + FROM EntityTypeFeed + GROUP BY feed_id +) AS EntityTypeFeedJoin ON EntityTypeFeedJoin.feed_id = Feed.id AND Feed.data_type = 'gtfs_rt' +; + + +-- This index allows concurrent refresh on the materialized view avoiding table locks +CREATE UNIQUE INDEX idx_unique_feed_id ON FeedSearch(feed_id); + +-- Indices for feedsearch view optimization +CREATE INDEX feedsearch_document_idx ON FeedSearch USING GIN(document); +CREATE INDEX feedsearch_feed_stable_id ON FeedSearch(feed_stable_id); +CREATE INDEX feedsearch_data_type ON FeedSearch(data_type); +CREATE INDEX feedsearch_status ON FeedSearch(status); diff --git a/liquibase/changes/feat_794_2.sql b/liquibase/changes/feat_794_2.sql new file mode 100644 index 000000000..a217d4e8f --- /dev/null +++ b/liquibase/changes/feat_794_2.sql @@ -0,0 +1 @@ +ALTER TABLE Feed ADD COLUMN official BOOLEAN DEFAULT NULL; \ No newline at end of file diff --git a/web-app/src/app/services/feeds/types.ts b/web-app/src/app/services/feeds/types.ts index ddfd98b28..6961f0adc 100644 --- a/web-app/src/app/services/feeds/types.ts +++ b/web-app/src/app/services/feeds/types.ts @@ -138,6 +138,12 @@ export interface components { * @example "2023-07-10T22:06:00.000Z" */ created_at?: string; + /** + * @description A boolean value indicating if the feed is official or not. Official feeds are provided by the transit agency or a trusted source. + * + * @example true + */ + official?: boolean; external_ids?: components['schemas']['ExternalIds']; /** * @description A commonly used name for the transit provider included in the feed. @@ -202,6 +208,12 @@ export interface components { * @example "2023-07-10T22:06:00.000Z" */ created_at?: string; + /** + * @description A boolean value indicating if the feed is official or not. Official feeds are provided by the transit agency or a trusted source. + * + * @example true + */ + official?: boolean; external_ids?: components['schemas']['ExternalIds']; /** * @description A commonly used name for the transit provider included in the feed. @@ -495,6 +507,8 @@ export interface components { | 'disjoint'; /** @description If true, only return the latest dataset. */ latest_query_param?: boolean; + /** @description If true, only return official feeds. */ + is_official_query_param?: boolean; /** @description The number of items to be returned. */ limit_query_param?: number; /** @description Offset of the first item to return. */ @@ -545,6 +559,7 @@ export interface operations { status?: components['parameters']['status']; provider?: components['parameters']['provider']; producer_url?: components['parameters']['producer_url']; + is_official?: components['parameters']['is_official_query_param']; }; }; responses: { @@ -586,6 +601,7 @@ export interface operations { dataset_latitudes?: components['parameters']['dataset_latitudes']; dataset_longitudes?: components['parameters']['dataset_longitudes']; bounding_filter_method?: components['parameters']['bounding_filter_method']; + is_official?: components['parameters']['is_official_query_param']; }; }; responses: { @@ -609,6 +625,7 @@ export interface operations { country_code?: components['parameters']['country_code']; subdivision_name?: components['parameters']['subdivision_name']; municipality?: components['parameters']['municipality']; + is_official?: components['parameters']['is_official_query_param']; }; }; responses: { @@ -746,6 +763,7 @@ export interface operations { status?: components['parameters']['statuses']; feed_id?: components['parameters']['feed_id_query_param']; data_type?: components['parameters']['data_type_query_param']; + is_official?: components['parameters']['is_official_query_param']; search_query?: components['parameters']['search_text_query_param']; }; };