diff --git a/.github/workflows/typescript-generator-check.yml b/.github/workflows/typescript-generator-check.yml new file mode 100644 index 000000000..576d5c357 --- /dev/null +++ b/.github/workflows/typescript-generator-check.yml @@ -0,0 +1,50 @@ +name: Verify TypeScript Types Generation +on: + pull_request: + branches: + - main + paths: + - "docs/DatabaseCatalogAPI.yaml" + +env: + NODE_VERSION: "18" + +jobs: + generate-and-compare: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'yarn' + cache-dependency-path: 'web-app/yarn.lock' + + - name: Cache Yarn dependencies + uses: actions/cache@v4 + id: yarn-cache + with: + path: | + **/node_modules + **/.eslintcache + key: ${{ runner.os }}-yarn-${{ hashFiles('web-app/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Install dependencies + working-directory: web-app + run: yarn install --frozen-lockfile --prefer-offline + + - name: Generate TypeScript types + working-directory: web-app + run: yarn generate:api-types:output + env: + OUTPUT_PATH_TYPES: src/app/services/feeds/generated/types.ts + + - name: Compare TypeScript types with existing types + working-directory: web-app + run: diff src/app/services/feeds/generated/types.ts src/app/services/feeds/types.ts || (echo "Types are different!" && exit 1) diff --git a/api/.openapi-generator/FILES b/api/.openapi-generator/FILES index d29d2b923..ed072b1a2 100644 --- a/api/.openapi-generator/FILES +++ b/api/.openapi-generator/FILES @@ -23,8 +23,8 @@ src/feeds_gen/models/latest_dataset_validation_report.py src/feeds_gen/models/location.py src/feeds_gen/models/metadata.py src/feeds_gen/models/redirect.py +src/feeds_gen/models/search_feed_item_result.py src/feeds_gen/models/search_feeds200_response.py -src/feeds_gen/models/search_feeds200_response_results_inner.py src/feeds_gen/models/source_info.py src/feeds_gen/models/validation_report.py src/feeds_gen/security_api.py diff --git a/api/src/feeds/impl/models/search_feeds200_response_results_inner_impl.py b/api/src/feeds/impl/models/search_feed_item_result_impl.py similarity index 93% rename from api/src/feeds/impl/models/search_feeds200_response_results_inner_impl.py rename to api/src/feeds/impl/models/search_feed_item_result_impl.py index 622540c09..c0b218c95 100644 --- a/api/src/feeds/impl/models/search_feeds200_response_results_inner_impl.py +++ b/api/src/feeds/impl/models/search_feed_item_result_impl.py @@ -1,10 +1,10 @@ from feeds_gen.models.latest_dataset import LatestDataset -from feeds_gen.models.search_feeds200_response_results_inner import SearchFeeds200ResponseResultsInner +from feeds_gen.models.search_feed_item_result import SearchFeedItemResult from feeds_gen.models.source_info import SourceInfo -class SearchFeeds200ResponseResultsInnerImpl(SearchFeeds200ResponseResultsInner): - """Implementation of the `SearchFeeds200ResponseResultsInner` model. +class SearchFeedItemResultImpl(SearchFeedItemResult): + """Implementation of the `SearchFeedItemResult` model. This class converts a SQLAlchemy row object to a Pydantic model instance taking in consideration the data type. """ diff --git a/api/src/feeds/impl/search_api_impl.py b/api/src/feeds/impl/search_api_impl.py index 55097b61f..270520e76 100644 --- a/api/src/feeds/impl/search_api_impl.py +++ b/api/src/feeds/impl/search_api_impl.py @@ -3,7 +3,7 @@ from database.database import Database from database_gen.sqlacodegen_models import t_feedsearch -from feeds.impl.models.search_feeds200_response_results_inner_impl import SearchFeeds200ResponseResultsInnerImpl +from feeds.impl.models.search_feed_item_result_impl import SearchFeedItemResultImpl from feeds_gen.apis.search_api_base import BaseSearchApi from feeds_gen.models.search_feeds200_response import SearchFeeds200Response @@ -92,7 +92,7 @@ def search_feeds( total=0, ) - results = list(map(lambda feed: SearchFeeds200ResponseResultsInnerImpl.from_orm(feed), feed_rows)) + results = list(map(lambda feed: SearchFeedItemResultImpl.from_orm(feed), feed_rows)) return SearchFeeds200Response( results=results, total=feed_total_count[0][0] if feed_total_count and feed_total_count[0] else 0, diff --git a/api/tests/unittest/models/test_search_feeds200_response_results_inner_impl.py b/api/tests/unittest/models/test_search_feed_item_result_impl.py similarity index 85% rename from api/tests/unittest/models/test_search_feeds200_response_results_inner_impl.py rename to api/tests/unittest/models/test_search_feed_item_result_impl.py index f4b702e86..729a13e8c 100644 --- a/api/tests/unittest/models/test_search_feeds200_response_results_inner_impl.py +++ b/api/tests/unittest/models/test_search_feed_item_result_impl.py @@ -3,7 +3,7 @@ import copy from faker import Faker -from feeds.impl.models.search_feeds200_response_results_inner_impl import SearchFeeds200ResponseResultsInnerImpl +from feeds.impl.models.search_feed_item_result_impl import SearchFeedItemResultImpl from feeds_gen.models.latest_dataset import LatestDataset from feeds_gen.models.source_info import SourceInfo @@ -50,9 +50,9 @@ class TestSearchFeeds200ResponseResultsInnerImpl(unittest.TestCase): def test_from_orm_gtfs(self): item = copy.deepcopy(search_item) item.data_type = "gtfs" - result = SearchFeeds200ResponseResultsInnerImpl.from_orm_gtfs(item) + result = SearchFeedItemResultImpl.from_orm_gtfs(item) assert result.data_type == "gtfs" - expected = SearchFeeds200ResponseResultsInnerImpl( + expected = SearchFeedItemResultImpl( id=item.feed_stable_id, data_type=item.data_type, status=item.status, @@ -82,9 +82,9 @@ def test_from_orm_gtfs(self): def test_from_orm_gtfs_rt(self): item = copy.deepcopy(search_item) item.data_type = "gtfs_rt" - result = SearchFeeds200ResponseResultsInnerImpl.from_orm_gtfs_rt(item) + result = SearchFeedItemResultImpl.from_orm_gtfs_rt(item) assert result.data_type == "gtfs_rt" - expected = SearchFeeds200ResponseResultsInnerImpl( + expected = SearchFeedItemResultImpl( id=item.feed_stable_id, data_type=item.data_type, status=item.status, @@ -110,17 +110,17 @@ def test_from_orm_gtfs_rt(self): def test_from_orm(self): item = copy.deepcopy(search_item) item.data_type = "gtfs" - result = SearchFeeds200ResponseResultsInnerImpl.from_orm(item) + result = SearchFeedItemResultImpl.from_orm(item) assert result.data_type == "gtfs" item = copy.deepcopy(search_item) item.data_type = "gtfs_rt" - result = SearchFeeds200ResponseResultsInnerImpl.from_orm(item) + result = SearchFeedItemResultImpl.from_orm(item) assert result.data_type == "gtfs_rt" - assert SearchFeeds200ResponseResultsInnerImpl.from_orm(None) is None + assert SearchFeedItemResultImpl.from_orm(None) is None with pytest.raises(ValueError): item = copy.deepcopy(search_item) item.data_type = "unknown" - SearchFeeds200ResponseResultsInnerImpl.from_orm(item) + SearchFeedItemResultImpl.from_orm(item) diff --git a/docs/DatabaseCatalogAPI.yaml b/docs/DatabaseCatalogAPI.yaml index d04e384d4..7f902246b 100644 --- a/docs/DatabaseCatalogAPI.yaml +++ b/docs/DatabaseCatalogAPI.yaml @@ -296,10 +296,7 @@ paths: results: type: array items: - allOf: - - $ref: '#/components/schemas/GtfsFeed' - - $ref: '#/components/schemas/GtfsRTFeed' - + $ref: "#/components/schemas/SearchFeedItemResult" components: schemas: @@ -316,6 +313,11 @@ components: example: Redirected because of a change of URL. BasicFeed: type: object + discriminator: + propertyName: data_type + mapping: + gtfs: '#/components/schemas/GtfsFeed' + gtfs_rt: '#/components/schemas/GtfsRTFeed' properties: id: description: Unique identifier used as a key for the feeds table. @@ -350,7 +352,6 @@ components: type: string example: 2023-07-10T22:06:00Z format: date-time - external_ids: $ref: "#/components/schemas/ExternalIds" provider: @@ -418,6 +419,103 @@ components: locations: $ref: "#/components/schemas/Locations" + SearchFeedItemResult: + # The following schema is used to represent the search results for feeds. + # The schema is a union of all the possible types(BasicFeed, GtfsFeed and GtfsRTFeed) of feeds that can be returned. + # This union is not based on its original types due to the limitations of openapi-generator. + # For the same reason it's not defined as anyOf, but as a single object with all the possible properties. + type: object + required: + - id + - data_type + - status + properties: + id: + description: Unique identifier used as a key for the feeds table. + type: string + example: mdb-1210 + data_type: + type: string + enum: + - gtfs + - gtfs_rt + example: gtfs +# Have to put the enum inline because of a bug in openapi-generator +# $ref: "#/components/schemas/DataType" + status: + description: > + Describes status of the Feed. Should be one of + * `active` Feed should be used in public trip planners. + * `deprecated` Feed is explicitly deprecated and should not be used in public trip planners. + * `inactive` Feed hasn't been recently updated and should be used at risk of providing outdated information. + * `development` Feed is being used for development purposes and should not be used in public trip planners. + type: string + enum: + - active + - deprecated + - inactive + - development + example: deprecated +# Have to put the enum inline because of a bug in openapi-generator +# $ref: "#/components/schemas/FeedStatus" + created_at: + description: The date and time the feed was added to the database, in ISO 8601 date-time format. + type: string + example: 2023-07-10T22:06:00Z + format: date-time + external_ids: + $ref: "#/components/schemas/ExternalIds" + provider: + description: A commonly used name for the transit provider included in the feed. + type: string + example: Los Angeles Department of Transportation (LADOT, DASH, Commuter Express) + feed_name: + description: > + An optional description of the data feed, e.g to specify if the data feed is an aggregate of + multiple providers, or which network is represented by the feed. + type: string + example: Bus + note: + description: A note to clarify complex use cases for consumers. + type: string + feed_contact_email: + description: Use to contact the feed producer. + type: string + example: someEmail@ladotbus.com + source_info: + $ref: "#/components/schemas/SourceInfo" + redirects: + type: array + items: + $ref: "#/components/schemas/Redirect" + locations: + $ref: "#/components/schemas/Locations" + latest_dataset: + $ref: "#/components/schemas/LatestDataset" + entity_types: + type: array + items: + type: string + enum: + - vp + - tu + - sa + example: vp + description: > + The type of realtime entry: + * vp - vehicle positions + * tu - trip updates + * sa - service alerts +# Have to put the enum inline because of a bug in openapi-generator +# $ref: "#/components/schemas/EntityTypes" + feed_references: + description: + A list of the GTFS feeds that the real time source is associated with, represented by their MDB source IDs. + type: array + items: + type: string + example: "mdb-20" + BasicFeeds: type: array items: diff --git a/web-app/package.json b/web-app/package.json index dc2951d9c..0a13a5c5a 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -61,7 +61,8 @@ "lint:fix": "eslint 'src/app/**/*.{js,ts,tsx}' --fix", "cypress:run": "cypress run", "cypress:open": "cypress open", - "generate:api-types": "npx openapi-typescript ../docs/DatabaseCatalogAPI.yaml -o src/app/services/feeds/types.ts && eslint src/app/services/feeds/types.ts --fix" + "generate:api-types:output": "npx openapi-typescript ../docs/DatabaseCatalogAPI.yaml -o $OUTPUT_PATH_TYPES && eslint $OUTPUT_PATH_TYPES --fix", + "generate:api-types": "OUTPUT_PATH_TYPES=src/app/services/feeds/types.ts npm run generate:api-types:output" }, "eslintConfig": { "extends": [ diff --git a/web-app/src/app/services/feeds/types.ts b/web-app/src/app/services/feeds/types.ts index 1ac9fa70f..63fbd2453 100644 --- a/web-app/src/app/services/feeds/types.ts +++ b/web-app/src/app/services/feeds/types.ts @@ -52,6 +52,15 @@ export interface paths { }; }; }; + '/v1/gtfs_feeds/{id}/gtfs_rt_feeds': { + /** @description Get a list of GTFS Realtime related to a GTFS feed. */ + get: operations['getGtfsFeedGtfsRtFeeds']; + parameters: { + path: { + id: components['parameters']['feed_id_path_param']; + }; + }; + }; '/v1/datasets/gtfs/{id}': { /** @description Get the specified dataset from the Mobility Database. */ get: operations['getDatasetGtfs']; @@ -123,6 +132,12 @@ export interface components { * @enum {string} */ status?: 'active' | 'deprecated' | 'inactive' | 'development'; + /** + * Format: date-time + * @description The date and time the feed was added to the database, in ISO 8601 date-time format. + * @example "2023-07-10T22:06:00.000Z" + */ + created_at?: string; external_ids?: components['schemas']['ExternalIds']; /** * @description A commonly used name for the transit provider included in the feed. @@ -147,22 +162,73 @@ export interface components { }; GtfsFeed: { data_type: 'gtfs'; - } & components['schemas']['BasicFeed'] & { - data_type: 'gtfs'; + } & Omit & { locations?: components['schemas']['Locations']; - } & { - data_type: 'gtfs'; latest_dataset?: components['schemas']['LatestDataset']; }; GtfsRTFeed: { data_type: 'gtfs_rt'; - } & components['schemas']['BasicFeed'] & { - data_type: 'gtfs_rt'; + } & Omit & { entity_types?: Array<'vp' | 'tu' | 'sa'>; /** @description A list of the GTFS feeds that the real time source is associated with, represented by their MDB source IDs. */ feed_references?: string[]; locations?: components['schemas']['Locations']; }; + SearchFeedItemResult: { + /** + * @description Unique identifier used as a key for the feeds table. + * @example mdb-1210 + */ + id: string; + /** + * @example gtfs + * @enum {string} + */ + data_type: 'gtfs' | 'gtfs_rt'; + /** + * @description Describes status of the Feed. Should be one of + * * `active` Feed should be used in public trip planners. + * * `deprecated` Feed is explicitly deprecated and should not be used in public trip planners. + * * `inactive` Feed hasn't been recently updated and should be used at risk of providing outdated information. + * * `development` Feed is being used for development purposes and should not be used in public trip planners. + * + * @example deprecated + * @enum {string} + */ + status: 'active' | 'deprecated' | 'inactive' | 'development'; + /** + * Format: date-time + * @description The date and time the feed was added to the database, in ISO 8601 date-time format. + * @example "2023-07-10T22:06:00.000Z" + */ + created_at?: string; + external_ids?: components['schemas']['ExternalIds']; + /** + * @description A commonly used name for the transit provider included in the feed. + * @example Los Angeles Department of Transportation (LADOT, DASH, Commuter Express) + */ + provider?: string; + /** + * @description An optional description of the data feed, e.g to specify if the data feed is an aggregate of multiple providers, or which network is represented by the feed. + * + * @example Bus + */ + feed_name?: string; + /** @description A note to clarify complex use cases for consumers. */ + note?: string; + /** + * @description Use to contact the feed producer. + * @example someEmail@ladotbus.com + */ + feed_contact_email?: string; + source_info?: components['schemas']['SourceInfo']; + redirects?: Array; + locations?: components['schemas']['Locations']; + latest_dataset?: components['schemas']['LatestDataset']; + entity_types?: Array<'vp' | 'tu' | 'sa'>; + /** @description A list of the GTFS feeds that the real time source is associated with, represented by their MDB source IDs. */ + feed_references?: string[]; + }; BasicFeeds: Array; GtfsFeeds: Array; GtfsRTFeeds: Array; @@ -602,6 +668,22 @@ export interface operations { }; }; }; + /** @description Get a list of GTFS Realtime related to a GTFS feed. */ + getGtfsFeedGtfsRtFeeds: { + parameters: { + path: { + id: components['parameters']['feed_id_path_param']; + }; + }; + responses: { + /** @description Successful pull of the GTFS Realtime feeds info related to a GTFS feed. */ + 200: { + content: { + 'application/json': components['schemas']['GtfsRTFeeds']; + }; + }; + }; + }; /** @description Get the specified dataset from the Mobility Database. */ getDatasetGtfs: { parameters: { @@ -667,10 +749,7 @@ export interface operations { 'application/json': { /** @description The total number of matching entities found regardless the limit and offset parameters. */ total?: number; - results?: Array< - | components['schemas']['GtfsFeed'] - | components['schemas']['GtfsRTFeed'] - >; + results?: Array; }; }; }; diff --git a/web-app/yarn.lock b/web-app/yarn.lock index 2956f5936..0b136e1b2 100644 --- a/web-app/yarn.lock +++ b/web-app/yarn.lock @@ -10763,15 +10763,15 @@ openapi-typescript-helpers@^0.0.7: integrity sha512-7nwlAtdA1fULipibFRBWE/rnF114q6ejRYzNvhdA/x+qTWAZhXGLc/368dlwMlyJDvCQMCnADjpzb5BS5ZmNSA== openapi-typescript@^6.7.5: - version "6.7.5" - resolved "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-6.7.5.tgz" - integrity sha512-ZD6dgSZi0u1QCP55g8/2yS5hNJfIpgqsSGHLxxdOjvY7eIrXzj271FJEQw33VwsZ6RCtO/NOuhxa7GBWmEudyA== + version "6.7.6" + resolved "https://registry.yarnpkg.com/openapi-typescript/-/openapi-typescript-6.7.6.tgz#4f387199203bd7bfb94545cbc613751b52e3fa37" + integrity sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw== dependencies: ansi-colors "^4.1.3" fast-glob "^3.3.2" js-yaml "^4.1.0" supports-color "^9.4.0" - undici "^5.28.2" + undici "^5.28.4" yargs-parser "^21.1.1" openapi3-ts@^3.1.1: @@ -13145,16 +13145,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -13237,14 +13228,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13944,9 +13928,9 @@ undici-types@~5.26.4: resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici@^5.28.2: +undici@^5.28.4: version "5.28.4" - resolved "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== dependencies: "@fastify/busboy" "^2.0.0" @@ -14676,7 +14660,7 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -14694,15 +14678,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"