diff --git a/Dockerfile b/Dockerfile index 9cc341347..ec5ef64ae 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,7 @@ RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ --mount=type=bind,source=packages/entreprise/package.json,target=packages/entreprise/package.json \ --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ + --mount=type=bind,source=packages/opendatasoft/package.json,target=packages/opendatasoft/package.json \ --mount=type=bind,source=packages/identite/package.json,target=packages/identite/package.json \ --mount=type=bind,source=packages/testing/package.json,target=packages/testing/package.json \ --mount=type=cache,sharing=locked,target=/root/.npm \ @@ -32,6 +33,7 @@ RUN --mount=type=bind,source=package.json,target=package.json \ --mount=type=bind,source=packages/email/package.json,target=packages/email/package.json \ --mount=type=bind,source=packages/entreprise/package.json,target=packages/entreprise/package.json \ --mount=type=bind,source=packages/insee/package.json,target=packages/insee/package.json \ + --mount=type=bind,source=packages/opendatasoft/package.json,target=packages/opendatasoft/package.json \ --mount=type=bind,source=packages/identite/package.json,target=packages/identite/package.json \ --mount=type=bind,source=packages/testing/package.json,target=packages/testing/package.json \ --mount=type=cache,sharing=locked,target=/root/.npm \ diff --git a/package-lock.json b/package-lock.json index 365bb0406..463ed4738 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "packages/email", "packages/entreprise", "packages/insee", + "packages/opendatasoft", "packages/identite", "packages/testing" ], @@ -27,6 +28,7 @@ "@gouvfr-lasuite/proconnect.entreprise": "workspace:*", "@gouvfr-lasuite/proconnect.identite": "workspace:*", "@gouvfr-lasuite/proconnect.insee": "workspace:*", + "@gouvfr-lasuite/proconnect.opendatasoft": "workspace:*", "@gouvfr-lasuite/proconnect.testing": "workspace:*", "@sentry/node": "^9.42.1", "@sentry/profiling-node": "^9.42.1", @@ -1382,6 +1384,10 @@ "resolved": "packages/insee", "link": true }, + "node_modules/@gouvfr-lasuite/proconnect.opendatasoft": { + "resolved": "packages/opendatasoft", + "link": true + }, "node_modules/@gouvfr-lasuite/proconnect.testing": { "resolved": "packages/testing", "link": true @@ -3489,6 +3495,12 @@ "@types/node": "*" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -10737,6 +10749,70 @@ "tsx": "^4.20.3" } }, + "packages/opendatasoft": { + "name": "@gouvfr-lasuite/proconnect.opendatasoft", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@gouvfr-lasuite/proconnect.core": "^0.5.0", + "openapi-fetch": "^0.14.0" + }, + "devDependencies": { + "@gouvfr-lasuite/proconnect.devtools.typescript": "0.0.0", + "@types/node": "^22.14.1", + "openapi-typescript": "^7.6.1", + "tsx": "^4.19.4" + } + }, + "packages/opendatasoft/node_modules/@gouvfr-lasuite/proconnect.core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@gouvfr-lasuite/proconnect.core/-/proconnect.core-0.5.0.tgz", + "integrity": "sha512-dhVcvfkP8jf3sB/hf8V5W8I5aCH4u+wLX2QvSBOcmb9hq1Ejee3cxS21Qp0//+nUQsqTs5Wk0csw+eWDFdqCaw==", + "license": "MIT", + "dependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/lodash-es": "^4.17.12", + "@types/oidc-provider": "^8.5.2", + "@zootools/email-spell-checker": "^1.12.0", + "bcryptjs": "^2.4.3", + "is-disposable-email-domain": "^1.0.7", + "lodash-es": "^4.17.21", + "nanoid": "^5.0.9", + "tld-extract": "^2.1.0" + } + }, + "packages/opendatasoft/node_modules/@types/node": { + "version": "22.17.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.2.tgz", + "integrity": "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "packages/opendatasoft/node_modules/@types/oidc-provider": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.8.1.tgz", + "integrity": "sha512-Yi/OJ7s0CFJ1AWAQrY2EO/zkV9uppLtiGAzrA07lBDveUOvxtYh7GflnHFXcgufVaPxVAjdykizjTYTMNVhdJw==", + "license": "MIT", + "dependencies": { + "@types/keygrip": "*", + "@types/koa": "*", + "@types/node": "*" + } + }, + "packages/opendatasoft/node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "packages/opendatasoft/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "packages/testing": { "name": "@gouvfr-lasuite/proconnect.testing", "version": "0.5.2", diff --git a/package.json b/package.json index e6f3924f9..5ce757ece 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "packages/email", "packages/entreprise", "packages/insee", + "packages/opendatasoft", "packages/identite", "packages/testing" ], @@ -71,6 +72,7 @@ "@gouvfr-lasuite/proconnect.entreprise": "workspace:*", "@gouvfr-lasuite/proconnect.identite": "workspace:*", "@gouvfr-lasuite/proconnect.insee": "workspace:*", + "@gouvfr-lasuite/proconnect.opendatasoft": "workspace:*", "@gouvfr-lasuite/proconnect.testing": "workspace:*", "@sentry/node": "^9.42.1", "@sentry/profiling-node": "^9.42.1", diff --git a/packages/core/package.json b/packages/core/package.json index 3f061be2e..73b0e0fbf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,6 +37,11 @@ "import": "./dist/services/suggestion/index.js", "types": "./dist/services/suggestion/index.d.ts" }, + "./utils/lodash": { + "require": "./dist/utils/lodash/index.cjs", + "import": "./dist/utils/lodash/index.js", + "types": "./dist/utils/lodash/index.d.ts" + }, "./data": { "require": "./dist/data/index.cjs", "import": "./dist/data/index.js", @@ -57,6 +62,9 @@ "services/suggestion": [ "./dist/services/suggestion/index.d.ts" ], + "utils/lodash": [ + "./dist/utils/lodash/index.d.ts" + ], "data": [ "./dist/data/index.d.ts" ] diff --git a/packages/core/src/utils/lodash/index.ts b/packages/core/src/utils/lodash/index.ts new file mode 100644 index 000000000..5970db1fd --- /dev/null +++ b/packages/core/src/utils/lodash/index.ts @@ -0,0 +1,3 @@ +// + +export { isEmpty } from "lodash-es"; diff --git a/packages/opendatasoft/openapi/index.d.ts b/packages/opendatasoft/openapi/index.d.ts new file mode 100644 index 000000000..4e083d123 --- /dev/null +++ b/packages/opendatasoft/openapi/index.d.ts @@ -0,0 +1,1796 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/catalog/datasets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Query catalog datasets + * @description Retrieve available datasets. + */ + get: operations["getDatasets"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/exports": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List export formats + * @description List available export formats + */ + get: operations["listExportFormats"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/exports/{format}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export a catalog + * @description Export a catalog in the desired format. + */ + get: operations["exportDatasets"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/exports/csv": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export a catalog in CSV + * @description Export a catalog in CSV (Comma Separated Values). Specific parameters are described here + */ + get: operations["exportCatalogCSV"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/exports/dcat{dcat_ap_format}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export a catalog in RDF/XML (DCAT) + * @description Export a catalog in RDF/XML described with DCAT (Data Catalog Vocabulary). Specific parameters are described here + */ + get: operations["exportCatalogDCAT"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/facets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List facet values + * @description Enumerate facet values for datasets and returns a list of values for each facet. + * Can be used to implement guided navigation in large result sets. + */ + get: operations["getDatasetsFacets"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/records": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Query dataset records + * @description Perform a query on dataset records. + */ + get: operations["getRecords"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/exports": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List export formats + * @description List available export formats + */ + get: operations["listDatasetExportFormats"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/exports/{format}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export a dataset + * @description Export a dataset in the desired format. + * **Note:** The `group_by` parameter is only available on exports starting with the v2.1 + */ + get: operations["exportRecords"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/exports/csv": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export a dataset in CSV + * @description Export a dataset in CSV (Comma Separated Values). Specific parameters are described here + */ + get: operations["exportRecordsCSV"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/exports/parquet": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export a dataset in Parquet + * @description Export a dataset in Parquet. Specific parameters are described here + */ + get: operations["exportRecordsParquet"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/exports/gpx": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Export a dataset in GPX + * @description Export a dataset in GPX. Specific parameters are described here + */ + get: operations["exportRecordsGPX"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Show dataset information + * @description Returns a list of available endpoints for the specified dataset, with metadata and endpoints. + * + * The response includes the following links: + * * the attachments endpoint + * * the files endpoint + * * the records endpoint + * * the catalog endpoint. + */ + get: operations["getDataset"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/facets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List dataset facets + * @description Enumerates facet values for records and returns a list of values for each facet. + * Can be used to implement guided navigation in large result sets. + * + */ + get: operations["getRecordsFacets"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/attachments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List dataset attachments + * @description Returns a list of all available attachments for a dataset. + * + */ + get: operations["getDatasetAttachments"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/catalog/datasets/{dataset_id}/records/{record_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Read a dataset record + * @description Reads a single dataset record based on its identifier. + * + */ + get: operations["getRecord"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + links: { + /** Format: uri */ + href?: string; + /** @enum {string} */ + rel?: "self" | "first" | "last" | "next" | "dataset" | "catalog"; + }; + dataset: { + _links?: components["schemas"]["links"][]; + dataset_id?: string; + readonly dataset_uid?: string; + attachments?: { + mimetype?: string; + url?: string; + id?: string; + title?: string; + }[]; + has_records?: boolean; + data_visible?: boolean; + /** @description A map of available features for a dataset, with the fields they apply to. + * */ + features?: string[]; + metas?: Record; + fields?: { + name?: string; + label?: string; + type?: string; + annotations?: Record; + description?: string | null; + }[]; + } & { + [key: string]: unknown; + }; + datasets: { + total_count?: number; + _links?: components["schemas"]["links"][]; + results?: components["schemas"]["dataset"][]; + }; + /** @enum {string} */ + "enum-format-datasets": + | "csv" + | "fgb" + | "geojson" + | "gpx" + | "json" + | "jsonl" + | "jsonld" + | "kml" + | "n3" + | "ov2" + | "parquet" + | "rdfxml" + | "shp" + | "turtle" + | "xlsx"; + facet_value_enumeration: { + name?: string; + count?: number; + value?: string; + state?: string; + }; + facet_enumeration: { + name?: string; + facets?: components["schemas"]["facet_value_enumeration"][]; + }; + record: { + _id?: string; + /** Format: dateTime */ + _timestamp?: string; + /** Format: int64 */ + _size?: number; + _links?: components["schemas"]["links"][]; + } & { + [key: string]: Record; + }; + records: { + total_count?: number; + _links?: components["schemas"]["links"][]; + results?: components["schemas"]["record"][]; + }; + attachment: { + href?: string; + metas?: { + "mime-type"?: string; + title?: string; + url?: string; + id?: string; + }; + }; + }; + responses: { + /** @description Bad Request */ + bad_request: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": { + message: string; + error_code: string; + }; + }; + }; + /** @description Too many requests */ + quota: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": { + errorcode: number; + reset_time: string; + limit_time_unit: string; + call_limit: number; + error: string; + }; + }; + }; + }; + parameters: { + /** @description Examples: + * - `select=size` - Example of select, which only return the "size" field. + * - `select=size * 2 as bigger_size` - Example of a complex expression with a label, which returns a new field named "bigger_size" and containing the double of size field value. + * - `select=dataset_id, fields` - Example of a select in catalog ODSQL query to only retrieve dataset_id and schema of datasets. + * + * A select expression can be used to add, remove or change the fields to return. + * An expression can be: + * - a wildcard ('*'): all fields are returned. + * - A field name: only the specified field is returned. + * - An include/exclude function: All fields matching the include or exclude expression are included or excluded. This expression can contain wildcard. + * - A complex expression. The result of the expression is returned. A label can be set for this expression, and in that case, the field will be named after this label. */ + select: string; + /** @description A `where` filter is a text expression performing a simple full-text search that can also include logical operations + * (NOT, AND, OR...) and lots of other functions to perform complex and precise search operations. + * + * For more information, see [Opendatasoft Query Language (ODSQL)]() reference documentation. */ + where: string; + /** @description Example: `order_by=sum(age) desc, name asc` + * + * A comma-separated list of field names or aggregations to sort on, followed by an order (`asc` or `desc`). + * + * Results are sorted in ascending order by default. To sort results in descending order, use the `desc` keyword. */ + order_by: string; + /** @description Number of items to return. + * + * To use with the `offset` parameter to implement pagination. + * + * The maximum possible value depends on whether the query contains a `group_by` clause or not. + * + * For a query **without** a `group_by`: + * - the maximum value for `limit` is 100, + * - `offset+limit` should be less than 10000 + * + * For a query **with** a `group_by`: + * - the maximum value for `limit` is 20000, + * - `offset+limit` should be less than 20000 + * + * **Note:** If you need more results, please use the /exports endpoint. + * */ + limit: number; + /** @description Index of the first item to return (starting at 0). + * + * To use with the `limit` parameter to implement pagination. + * + * **Note:** the maximum value depends on the type of query, see the note on `limit` for the details + * */ + offset: number; + /** @description Example: `refine=modified:2020` - Return only the value `2020` from the `modified` facet. + * + * A facet filter used to limit the result set. + * Using this parameter, you can refine your query to display only the selected facet value in the response. + * + * Refinement uses the following syntax: `refine=:` + * + * For date, and other hierarchical facets, when refining on one value, all second-level values related to that entry will appear in facets enumeration. For example, after refining on the year 2019, the related second-level month will appear. And when refining on August 2019, the third-level day will appear. + * + * **`refine` must not be confused with a `where` filter. Refining with a facet is equivalent to selecting an entry in the left navigation panel.** */ + refine: string; + /** @description Examples: + * - `exclude=city:Paris` - Exclude the value `Paris` from the `city` facet. Facets enumeration will display `Paris` as `excluded` without any count information. + * - `exclude=modified:2019/12` - Exclude the value `2019/12` from the `modified` facet. Facets enumeration will display `2020` as `excluded` without any count information. + * + * A facet filter used to exclude a facet value from the result set. + * Using this parameter, you can filter your query to exclude the selected facet value in the response. + * + * `exclude` uses the following syntax: `exclude=:` + * + * **`exclude` must not be confused with a `where` filter. Excluding a facet value is equivalent to removing an entry in the left navigation panel.** */ + exclude: string; + /** @description A language value. + * + * If specified, the `lang` value override the default language, which is "fr". + * The language is used to format string, for example in the `date_format` function. */ + lang: + | "en" + | "fr" + | "nl" + | "pt" + | "it" + | "ar" + | "de" + | "es" + | "ca" + | "eu" + | "sv"; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone: string; + /** @description Example: `group_by=city_field as city` + * + * A group by expression defines a grouping function for an aggregation. + * It can be: + * - a field name: group result by each value of this field + * - a range function: group result by range + * - a date function: group result by date + * + * It is possible to specify a custom name with the 'as name' notation. */ + group_by: string; + /** @description If set to `true`, this parameter will add HATEOAS links in the response. + * */ + include_links: boolean; + /** @description If set to `true`, this parameter will add application metadata to the response. + * */ + include_app_metas: boolean; + "format-catalog": + | "csv" + | "data.json" + | "dcat" + | "dcat_ap_ch" + | "dcat_ap_de" + | "dcat_ap_se" + | "dcat_ap_sp" + | "dcat_ap_it" + | "dcat_ap_vl" + | "dcat_ap_benap" + | "dublin_core" + | "json" + | "rdf" + | "rss" + | "ttl" + | "xlsx"; + /** @description Number of items to return in export. + * + * Use -1 (default) to retrieve all records + * */ + limit_export: number; + dcat_format: + | "_ap_ch" + | "_ap_de" + | "_ap_se" + | "_ap_sp" + | "_ap_it" + | "_ap_vl" + | "_ap_benap"; + /** @description A facet is a field used for simple filtering (through the `refine` and `exclude` parameters) or exploration (with the `/facets` endpoint). + * + * It can also be a function such as `facet=facet(name="field_name")` which is identical to `facet=field_name`. But this `facet()` function + * can also take some optional arguments such as `disjunctive`, `hierarchical`, `separator`, `sort` and `limit`. + * + * * `disjunctive`: a boolean `true/false`, whether multiple values can be selected for the facet + * * `hierarchical`: a boolean `true/false` if the field is hierarchical. The separator must be given as the argument. + * For instance, you can do `facet=facet(name="filepath", hierarchical=true, separator="/")` to retrieve facets related to this field which might look like `"/home/user/file.txt"` + * * `separator`: a string, e.g. `/`, `-`, `;` + * * `sort`: a string which describes how to sort the facets. Possible arguments are `count` and `-count` for all field types, `alphanum` and `-alphanum` for `date`, `datetime` and `text`, `num` and `-num` for `decimal` and `int` + * * `limit`: an integer to limit the number of results + * */ + facet: string; + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: string; + "format-datasets": components["schemas"]["enum-format-datasets"]; + /** @description If set to `true`, this parameter will make exports output the label of each field rather than its name. + * + * This parameter only makes sense for formats that contain a list of the fields in their output. + * */ + use_labels: boolean; + /** @description If set to `true`, this parameter can compress the output file of a specific export format with GZIP, e.g. `.csv.gzip`. + * */ + compressed: boolean; + /** @description This parameter sets the EPSG code to project shapes into for formats that support geometric features. + * */ + epsg: number; + /** @description Record identifier */ + record_id: string; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + getDatasets: { + parameters: { + query?: { + /** @description Examples: + * - `select=size` - Example of select, which only return the "size" field. + * - `select=size * 2 as bigger_size` - Example of a complex expression with a label, which returns a new field named "bigger_size" and containing the double of size field value. + * - `select=dataset_id, fields` - Example of a select in catalog ODSQL query to only retrieve dataset_id and schema of datasets. + * + * A select expression can be used to add, remove or change the fields to return. + * An expression can be: + * - a wildcard ('*'): all fields are returned. + * - A field name: only the specified field is returned. + * - An include/exclude function: All fields matching the include or exclude expression are included or excluded. This expression can contain wildcard. + * - A complex expression. The result of the expression is returned. A label can be set for this expression, and in that case, the field will be named after this label. */ + select?: components["parameters"]["select"]; + /** @description A `where` filter is a text expression performing a simple full-text search that can also include logical operations + * (NOT, AND, OR...) and lots of other functions to perform complex and precise search operations. + * + * For more information, see [Opendatasoft Query Language (ODSQL)]() reference documentation. */ + where?: components["parameters"]["where"]; + /** @description Example: `order_by=sum(age) desc, name asc` + * + * A comma-separated list of field names or aggregations to sort on, followed by an order (`asc` or `desc`). + * + * Results are sorted in ascending order by default. To sort results in descending order, use the `desc` keyword. */ + order_by?: components["parameters"]["order_by"]; + /** @description Number of items to return. + * + * To use with the `offset` parameter to implement pagination. + * + * The maximum possible value depends on whether the query contains a `group_by` clause or not. + * + * For a query **without** a `group_by`: + * - the maximum value for `limit` is 100, + * - `offset+limit` should be less than 10000 + * + * For a query **with** a `group_by`: + * - the maximum value for `limit` is 20000, + * - `offset+limit` should be less than 20000 + * + * **Note:** If you need more results, please use the /exports endpoint. + * */ + limit?: components["parameters"]["limit"]; + /** @description Index of the first item to return (starting at 0). + * + * To use with the `limit` parameter to implement pagination. + * + * **Note:** the maximum value depends on the type of query, see the note on `limit` for the details + * */ + offset?: components["parameters"]["offset"]; + /** @description Example: `refine=modified:2020` - Return only the value `2020` from the `modified` facet. + * + * A facet filter used to limit the result set. + * Using this parameter, you can refine your query to display only the selected facet value in the response. + * + * Refinement uses the following syntax: `refine=:` + * + * For date, and other hierarchical facets, when refining on one value, all second-level values related to that entry will appear in facets enumeration. For example, after refining on the year 2019, the related second-level month will appear. And when refining on August 2019, the third-level day will appear. + * + * **`refine` must not be confused with a `where` filter. Refining with a facet is equivalent to selecting an entry in the left navigation panel.** */ + refine?: components["parameters"]["refine"]; + /** @description Examples: + * - `exclude=city:Paris` - Exclude the value `Paris` from the `city` facet. Facets enumeration will display `Paris` as `excluded` without any count information. + * - `exclude=modified:2019/12` - Exclude the value `2019/12` from the `modified` facet. Facets enumeration will display `2020` as `excluded` without any count information. + * + * A facet filter used to exclude a facet value from the result set. + * Using this parameter, you can filter your query to exclude the selected facet value in the response. + * + * `exclude` uses the following syntax: `exclude=:` + * + * **`exclude` must not be confused with a `where` filter. Excluding a facet value is equivalent to removing an entry in the left navigation panel.** */ + exclude?: components["parameters"]["exclude"]; + /** @description A language value. + * + * If specified, the `lang` value override the default language, which is "fr". + * The language is used to format string, for example in the `date_format` function. */ + lang?: components["parameters"]["lang"]; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone?: components["parameters"]["timezone"]; + /** @description Example: `group_by=city_field as city` + * + * A group by expression defines a grouping function for an aggregation. + * It can be: + * - a field name: group result by each value of this field + * - a range function: group result by range + * - a date function: group result by date + * + * It is possible to specify a custom name with the 'as name' notation. */ + group_by?: components["parameters"]["group_by"]; + /** @description If set to `true`, this parameter will add HATEOAS links in the response. + * */ + include_links?: components["parameters"]["include_links"]; + /** @description If set to `true`, this parameter will add application metadata to the response. + * */ + include_app_metas?: components["parameters"]["include_app_metas"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of available datasets */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": components["schemas"]["datasets"]; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + listExportFormats: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of available export formats */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": { + links?: components["schemas"]["links"][]; + }; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + exportDatasets: { + parameters: { + query?: { + /** @description Examples: + * - `select=size` - Example of select, which only return the "size" field. + * - `select=size * 2 as bigger_size` - Example of a complex expression with a label, which returns a new field named "bigger_size" and containing the double of size field value. + * - `select=dataset_id, fields` - Example of a select in catalog ODSQL query to only retrieve dataset_id and schema of datasets. + * + * A select expression can be used to add, remove or change the fields to return. + * An expression can be: + * - a wildcard ('*'): all fields are returned. + * - A field name: only the specified field is returned. + * - An include/exclude function: All fields matching the include or exclude expression are included or excluded. This expression can contain wildcard. + * - A complex expression. The result of the expression is returned. A label can be set for this expression, and in that case, the field will be named after this label. */ + select?: components["parameters"]["select"]; + /** @description A `where` filter is a text expression performing a simple full-text search that can also include logical operations + * (NOT, AND, OR...) and lots of other functions to perform complex and precise search operations. + * + * For more information, see [Opendatasoft Query Language (ODSQL)]() reference documentation. */ + where?: components["parameters"]["where"]; + /** @description Example: `order_by=sum(age) desc, name asc` + * + * A comma-separated list of field names or aggregations to sort on, followed by an order (`asc` or `desc`). + * + * Results are sorted in ascending order by default. To sort results in descending order, use the `desc` keyword. */ + order_by?: components["parameters"]["order_by"]; + /** @description Example: `group_by=city_field as city` + * + * A group by expression defines a grouping function for an aggregation. + * It can be: + * - a field name: group result by each value of this field + * - a range function: group result by range + * - a date function: group result by date + * + * It is possible to specify a custom name with the 'as name' notation. */ + group_by?: components["parameters"]["group_by"]; + /** @description Number of items to return in export. + * + * Use -1 (default) to retrieve all records + * */ + limit?: components["parameters"]["limit_export"]; + /** @description Index of the first item to return (starting at 0). + * + * To use with the `limit` parameter to implement pagination. + * + * **Note:** the maximum value depends on the type of query, see the note on `limit` for the details + * */ + offset?: components["parameters"]["offset"]; + /** @description Example: `refine=modified:2020` - Return only the value `2020` from the `modified` facet. + * + * A facet filter used to limit the result set. + * Using this parameter, you can refine your query to display only the selected facet value in the response. + * + * Refinement uses the following syntax: `refine=:` + * + * For date, and other hierarchical facets, when refining on one value, all second-level values related to that entry will appear in facets enumeration. For example, after refining on the year 2019, the related second-level month will appear. And when refining on August 2019, the third-level day will appear. + * + * **`refine` must not be confused with a `where` filter. Refining with a facet is equivalent to selecting an entry in the left navigation panel.** */ + refine?: components["parameters"]["refine"]; + /** @description Examples: + * - `exclude=city:Paris` - Exclude the value `Paris` from the `city` facet. Facets enumeration will display `Paris` as `excluded` without any count information. + * - `exclude=modified:2019/12` - Exclude the value `2019/12` from the `modified` facet. Facets enumeration will display `2020` as `excluded` without any count information. + * + * A facet filter used to exclude a facet value from the result set. + * Using this parameter, you can filter your query to exclude the selected facet value in the response. + * + * `exclude` uses the following syntax: `exclude=:` + * + * **`exclude` must not be confused with a `where` filter. Excluding a facet value is equivalent to removing an entry in the left navigation panel.** */ + exclude?: components["parameters"]["exclude"]; + /** @description A language value. + * + * If specified, the `lang` value override the default language, which is "fr". + * The language is used to format string, for example in the `date_format` function. */ + lang?: components["parameters"]["lang"]; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone?: components["parameters"]["timezone"]; + }; + header?: never; + path: { + format: components["parameters"]["format-catalog"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Return a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + exportCatalogCSV: { + parameters: { + query?: { + /** @description Sets the field delimiter of the CSV export */ + delimiter?: ";" | "," | "\t" | "|"; + /** @description Sets the separator character used for multivalued strings */ + list_separator?: string; + /** @description Set it to true to force quoting all strings, i.e. surrounding all strings with quote characters */ + quote_all?: boolean; + /** @description Set it to true to force the first characters of the CSV file to be a Unicode Byte Order Mask (0xFEFF). It usually makes Excel correctly open the output CSV file without warning. + * **Warning:** the default value of this parameter is `false` in v2.0 and `true` starting with v2.1 */ + with_bom?: boolean; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Return a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + exportCatalogDCAT: { + parameters: { + query?: { + /** @description Sets the datasets exports exposed in the DCAT export. By default, all exports are exposed. */ + include_exports?: components["schemas"]["enum-format-datasets"]; + /** @description If set to `true`, this parameter will make distributions output the label of each field rather than its name. This parameter only applies on distributions that contain a list of the fields in their output (e.g., CSV, XLSX). */ + use_labels_in_exports?: boolean; + }; + header?: never; + path: { + dcat_ap_format: components["parameters"]["dcat_format"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Return a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getDatasetsFacets: { + parameters: { + query?: { + /** @description A facet is a field used for simple filtering (through the `refine` and `exclude` parameters) or exploration (with the `/facets` endpoint). + * + * It can also be a function such as `facet=facet(name="field_name")` which is identical to `facet=field_name`. But this `facet()` function + * can also take some optional arguments such as `disjunctive`, `hierarchical`, `separator`, `sort` and `limit`. + * + * * `disjunctive`: a boolean `true/false`, whether multiple values can be selected for the facet + * * `hierarchical`: a boolean `true/false` if the field is hierarchical. The separator must be given as the argument. + * For instance, you can do `facet=facet(name="filepath", hierarchical=true, separator="/")` to retrieve facets related to this field which might look like `"/home/user/file.txt"` + * * `separator`: a string, e.g. `/`, `-`, `;` + * * `sort`: a string which describes how to sort the facets. Possible arguments are `count` and `-count` for all field types, `alphanum` and `-alphanum` for `date`, `datetime` and `text`, `num` and `-num` for `decimal` and `int` + * * `limit`: an integer to limit the number of results + * */ + facet?: components["parameters"]["facet"]; + /** @description Example: `refine=modified:2020` - Return only the value `2020` from the `modified` facet. + * + * A facet filter used to limit the result set. + * Using this parameter, you can refine your query to display only the selected facet value in the response. + * + * Refinement uses the following syntax: `refine=:` + * + * For date, and other hierarchical facets, when refining on one value, all second-level values related to that entry will appear in facets enumeration. For example, after refining on the year 2019, the related second-level month will appear. And when refining on August 2019, the third-level day will appear. + * + * **`refine` must not be confused with a `where` filter. Refining with a facet is equivalent to selecting an entry in the left navigation panel.** */ + refine?: components["parameters"]["refine"]; + /** @description Examples: + * - `exclude=city:Paris` - Exclude the value `Paris` from the `city` facet. Facets enumeration will display `Paris` as `excluded` without any count information. + * - `exclude=modified:2019/12` - Exclude the value `2019/12` from the `modified` facet. Facets enumeration will display `2020` as `excluded` without any count information. + * + * A facet filter used to exclude a facet value from the result set. + * Using this parameter, you can filter your query to exclude the selected facet value in the response. + * + * `exclude` uses the following syntax: `exclude=:` + * + * **`exclude` must not be confused with a `where` filter. Excluding a facet value is equivalent to removing an entry in the left navigation panel.** */ + exclude?: components["parameters"]["exclude"]; + /** @description A `where` filter is a text expression performing a simple full-text search that can also include logical operations + * (NOT, AND, OR...) and lots of other functions to perform complex and precise search operations. + * + * For more information, see [Opendatasoft Query Language (ODSQL)]() reference documentation. */ + where?: components["parameters"]["where"]; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone?: components["parameters"]["timezone"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description An enumeration of facets */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": { + links?: components["schemas"]["links"][]; + facets?: components["schemas"]["facet_enumeration"][]; + }; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRecords: { + parameters: { + query?: { + /** @description Examples: + * - `select=size` - Example of select, which only return the "size" field. + * - `select=size * 2 as bigger_size` - Example of a complex expression with a label, which returns a new field named "bigger_size" and containing the double of size field value. + * - `select=dataset_id, fields` - Example of a select in catalog ODSQL query to only retrieve dataset_id and schema of datasets. + * + * A select expression can be used to add, remove or change the fields to return. + * An expression can be: + * - a wildcard ('*'): all fields are returned. + * - A field name: only the specified field is returned. + * - An include/exclude function: All fields matching the include or exclude expression are included or excluded. This expression can contain wildcard. + * - A complex expression. The result of the expression is returned. A label can be set for this expression, and in that case, the field will be named after this label. */ + select?: components["parameters"]["select"]; + /** @description A `where` filter is a text expression performing a simple full-text search that can also include logical operations + * (NOT, AND, OR...) and lots of other functions to perform complex and precise search operations. + * + * For more information, see [Opendatasoft Query Language (ODSQL)]() reference documentation. */ + where?: components["parameters"]["where"]; + /** @description Example: `group_by=city_field as city` + * + * A group by expression defines a grouping function for an aggregation. + * It can be: + * - a field name: group result by each value of this field + * - a range function: group result by range + * - a date function: group result by date + * + * It is possible to specify a custom name with the 'as name' notation. */ + group_by?: components["parameters"]["group_by"]; + /** @description Example: `order_by=sum(age) desc, name asc` + * + * A comma-separated list of field names or aggregations to sort on, followed by an order (`asc` or `desc`). + * + * Results are sorted in ascending order by default. To sort results in descending order, use the `desc` keyword. */ + order_by?: components["parameters"]["order_by"]; + /** @description Number of items to return. + * + * To use with the `offset` parameter to implement pagination. + * + * The maximum possible value depends on whether the query contains a `group_by` clause or not. + * + * For a query **without** a `group_by`: + * - the maximum value for `limit` is 100, + * - `offset+limit` should be less than 10000 + * + * For a query **with** a `group_by`: + * - the maximum value for `limit` is 20000, + * - `offset+limit` should be less than 20000 + * + * **Note:** If you need more results, please use the /exports endpoint. + * */ + limit?: components["parameters"]["limit"]; + /** @description Index of the first item to return (starting at 0). + * + * To use with the `limit` parameter to implement pagination. + * + * **Note:** the maximum value depends on the type of query, see the note on `limit` for the details + * */ + offset?: components["parameters"]["offset"]; + /** @description Example: `refine=modified:2020` - Return only the value `2020` from the `modified` facet. + * + * A facet filter used to limit the result set. + * Using this parameter, you can refine your query to display only the selected facet value in the response. + * + * Refinement uses the following syntax: `refine=:` + * + * For date, and other hierarchical facets, when refining on one value, all second-level values related to that entry will appear in facets enumeration. For example, after refining on the year 2019, the related second-level month will appear. And when refining on August 2019, the third-level day will appear. + * + * **`refine` must not be confused with a `where` filter. Refining with a facet is equivalent to selecting an entry in the left navigation panel.** */ + refine?: components["parameters"]["refine"]; + /** @description Examples: + * - `exclude=city:Paris` - Exclude the value `Paris` from the `city` facet. Facets enumeration will display `Paris` as `excluded` without any count information. + * - `exclude=modified:2019/12` - Exclude the value `2019/12` from the `modified` facet. Facets enumeration will display `2020` as `excluded` without any count information. + * + * A facet filter used to exclude a facet value from the result set. + * Using this parameter, you can filter your query to exclude the selected facet value in the response. + * + * `exclude` uses the following syntax: `exclude=:` + * + * **`exclude` must not be confused with a `where` filter. Excluding a facet value is equivalent to removing an entry in the left navigation panel.** */ + exclude?: components["parameters"]["exclude"]; + /** @description A language value. + * + * If specified, the `lang` value override the default language, which is "fr". + * The language is used to format string, for example in the `date_format` function. */ + lang?: components["parameters"]["lang"]; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone?: components["parameters"]["timezone"]; + /** @description If set to `true`, this parameter will add HATEOAS links in the response. + * */ + include_links?: components["parameters"]["include_links"]; + /** @description If set to `true`, this parameter will add application metadata to the response. + * */ + include_app_metas?: components["parameters"]["include_app_metas"]; + }; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Records */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": components["schemas"]["records"]; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + listDatasetExportFormats: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A list of available export formats */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": { + links?: components["schemas"]["links"][]; + }; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + exportRecords: { + parameters: { + query?: { + /** @description Examples: + * - `select=size` - Example of select, which only return the "size" field. + * - `select=size * 2 as bigger_size` - Example of a complex expression with a label, which returns a new field named "bigger_size" and containing the double of size field value. + * - `select=dataset_id, fields` - Example of a select in catalog ODSQL query to only retrieve dataset_id and schema of datasets. + * + * A select expression can be used to add, remove or change the fields to return. + * An expression can be: + * - a wildcard ('*'): all fields are returned. + * - A field name: only the specified field is returned. + * - An include/exclude function: All fields matching the include or exclude expression are included or excluded. This expression can contain wildcard. + * - A complex expression. The result of the expression is returned. A label can be set for this expression, and in that case, the field will be named after this label. */ + select?: components["parameters"]["select"]; + /** @description A `where` filter is a text expression performing a simple full-text search that can also include logical operations + * (NOT, AND, OR...) and lots of other functions to perform complex and precise search operations. + * + * For more information, see [Opendatasoft Query Language (ODSQL)]() reference documentation. */ + where?: components["parameters"]["where"]; + /** @description Example: `order_by=sum(age) desc, name asc` + * + * A comma-separated list of field names or aggregations to sort on, followed by an order (`asc` or `desc`). + * + * Results are sorted in ascending order by default. To sort results in descending order, use the `desc` keyword. */ + order_by?: components["parameters"]["order_by"]; + /** @description Example: `group_by=city_field as city` + * + * A group by expression defines a grouping function for an aggregation. + * It can be: + * - a field name: group result by each value of this field + * - a range function: group result by range + * - a date function: group result by date + * + * It is possible to specify a custom name with the 'as name' notation. */ + group_by?: components["parameters"]["group_by"]; + /** @description Number of items to return in export. + * + * Use -1 (default) to retrieve all records + * */ + limit?: components["parameters"]["limit_export"]; + /** @description Example: `refine=modified:2020` - Return only the value `2020` from the `modified` facet. + * + * A facet filter used to limit the result set. + * Using this parameter, you can refine your query to display only the selected facet value in the response. + * + * Refinement uses the following syntax: `refine=:` + * + * For date, and other hierarchical facets, when refining on one value, all second-level values related to that entry will appear in facets enumeration. For example, after refining on the year 2019, the related second-level month will appear. And when refining on August 2019, the third-level day will appear. + * + * **`refine` must not be confused with a `where` filter. Refining with a facet is equivalent to selecting an entry in the left navigation panel.** */ + refine?: components["parameters"]["refine"]; + /** @description Examples: + * - `exclude=city:Paris` - Exclude the value `Paris` from the `city` facet. Facets enumeration will display `Paris` as `excluded` without any count information. + * - `exclude=modified:2019/12` - Exclude the value `2019/12` from the `modified` facet. Facets enumeration will display `2020` as `excluded` without any count information. + * + * A facet filter used to exclude a facet value from the result set. + * Using this parameter, you can filter your query to exclude the selected facet value in the response. + * + * `exclude` uses the following syntax: `exclude=:` + * + * **`exclude` must not be confused with a `where` filter. Excluding a facet value is equivalent to removing an entry in the left navigation panel.** */ + exclude?: components["parameters"]["exclude"]; + /** @description A language value. + * + * If specified, the `lang` value override the default language, which is "fr". + * The language is used to format string, for example in the `date_format` function. */ + lang?: components["parameters"]["lang"]; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone?: components["parameters"]["timezone"]; + /** @description If set to `true`, this parameter will make exports output the label of each field rather than its name. + * + * This parameter only makes sense for formats that contain a list of the fields in their output. + * */ + use_labels?: components["parameters"]["use_labels"]; + /** @description If set to `true`, this parameter can compress the output file of a specific export format with GZIP, e.g. `.csv.gzip`. + * */ + compressed?: components["parameters"]["compressed"]; + /** @description This parameter sets the EPSG code to project shapes into for formats that support geometric features. + * */ + epsg?: components["parameters"]["epsg"]; + }; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + format: components["parameters"]["format-datasets"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Return a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + exportRecordsCSV: { + parameters: { + query?: { + /** @description Sets the field delimiter of the CSV export */ + delimiter?: ";" | "," | "\t" | "|"; + /** @description Sets the separator character used for multivalued strings */ + list_separator?: string; + /** @description Set it to true to force quoting all strings, i.e. surrounding all strings with quote characters */ + quote_all?: boolean; + /** @description Set it to true to force the first characters of the CSV file to be a Unicode Byte Order Mask (0xFEFF). It usually makes Excel correctly open the output CSV file without warning. + * **Warning:** the default value of this parameter is `false` in v2.0 and `true` starting with v2.1 */ + with_bom?: boolean; + }; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Return a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + exportRecordsParquet: { + parameters: { + query?: { + /** @description Sets the compression parameter for the Parquet export file */ + parquet_compression?: "snappy" | "zstd"; + }; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Return a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + exportRecordsGPX: { + parameters: { + query?: { + /** @description Sets the field that is used as the 'name' attribute in the GPX output */ + name_field?: string; + /** @description Sets the fields to use in the 'description' attribute of the GPX output */ + description_field_list?: string; + /** @description Set it to true to use the `` tag for attributes (as GDAL does). Set it to false to use the `` tag for attributes. + * **Warning:** the default value of this parameter is `false` in v2.0 and `true` starting with v2.1 */ + use_extension?: boolean; + }; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Return a file */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getDataset: { + parameters: { + query?: { + /** @description Examples: + * - `select=size` - Example of select, which only return the "size" field. + * - `select=size * 2 as bigger_size` - Example of a complex expression with a label, which returns a new field named "bigger_size" and containing the double of size field value. + * - `select=dataset_id, fields` - Example of a select in catalog ODSQL query to only retrieve dataset_id and schema of datasets. + * + * A select expression can be used to add, remove or change the fields to return. + * An expression can be: + * - a wildcard ('*'): all fields are returned. + * - A field name: only the specified field is returned. + * - An include/exclude function: All fields matching the include or exclude expression are included or excluded. This expression can contain wildcard. + * - A complex expression. The result of the expression is returned. A label can be set for this expression, and in that case, the field will be named after this label. */ + select?: components["parameters"]["select"]; + /** @description A language value. + * + * If specified, the `lang` value override the default language, which is "fr". + * The language is used to format string, for example in the `date_format` function. */ + lang?: components["parameters"]["lang"]; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone?: components["parameters"]["timezone"]; + /** @description If set to `true`, this parameter will add HATEOAS links in the response. + * */ + include_links?: components["parameters"]["include_links"]; + /** @description If set to `true`, this parameter will add application metadata to the response. + * */ + include_app_metas?: components["parameters"]["include_app_metas"]; + }; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The dataset */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8json": components["schemas"]["dataset"]; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRecordsFacets: { + parameters: { + query?: { + /** @description A `where` filter is a text expression performing a simple full-text search that can also include logical operations + * (NOT, AND, OR...) and lots of other functions to perform complex and precise search operations. + * + * For more information, see [Opendatasoft Query Language (ODSQL)]() reference documentation. */ + where?: components["parameters"]["where"]; + /** @description Example: `refine=modified:2020` - Return only the value `2020` from the `modified` facet. + * + * A facet filter used to limit the result set. + * Using this parameter, you can refine your query to display only the selected facet value in the response. + * + * Refinement uses the following syntax: `refine=:` + * + * For date, and other hierarchical facets, when refining on one value, all second-level values related to that entry will appear in facets enumeration. For example, after refining on the year 2019, the related second-level month will appear. And when refining on August 2019, the third-level day will appear. + * + * **`refine` must not be confused with a `where` filter. Refining with a facet is equivalent to selecting an entry in the left navigation panel.** */ + refine?: components["parameters"]["refine"]; + /** @description Examples: + * - `exclude=city:Paris` - Exclude the value `Paris` from the `city` facet. Facets enumeration will display `Paris` as `excluded` without any count information. + * - `exclude=modified:2019/12` - Exclude the value `2019/12` from the `modified` facet. Facets enumeration will display `2020` as `excluded` without any count information. + * + * A facet filter used to exclude a facet value from the result set. + * Using this parameter, you can filter your query to exclude the selected facet value in the response. + * + * `exclude` uses the following syntax: `exclude=:` + * + * **`exclude` must not be confused with a `where` filter. Excluding a facet value is equivalent to removing an entry in the left navigation panel.** */ + exclude?: components["parameters"]["exclude"]; + /** @description A facet is a field used for simple filtering (through the `refine` and `exclude` parameters) or exploration (with the `/facets` endpoint). + * + * It can also be a function such as `facet=facet(name="field_name")` which is identical to `facet=field_name`. But this `facet()` function + * can also take some optional arguments such as `disjunctive`, `hierarchical`, `separator`, `sort` and `limit`. + * + * * `disjunctive`: a boolean `true/false`, whether multiple values can be selected for the facet + * * `hierarchical`: a boolean `true/false` if the field is hierarchical. The separator must be given as the argument. + * For instance, you can do `facet=facet(name="filepath", hierarchical=true, separator="/")` to retrieve facets related to this field which might look like `"/home/user/file.txt"` + * * `separator`: a string, e.g. `/`, `-`, `;` + * * `sort`: a string which describes how to sort the facets. Possible arguments are `count` and `-count` for all field types, `alphanum` and `-alphanum` for `date`, `datetime` and `text`, `num` and `-num` for `decimal` and `int` + * * `limit`: an integer to limit the number of results + * */ + facet?: components["parameters"]["facet"]; + /** @description A language value. + * + * If specified, the `lang` value override the default language, which is "fr". + * The language is used to format string, for example in the `date_format` function. */ + lang?: components["parameters"]["lang"]; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone?: components["parameters"]["timezone"]; + }; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Facets enumeration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": { + links?: components["schemas"]["links"][]; + facets?: components["schemas"]["facet_enumeration"][]; + }; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getDatasetAttachments: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of all available attachments */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": { + links?: components["schemas"]["links"][]; + attachments?: components["schemas"]["attachment"][]; + }; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + getRecord: { + parameters: { + query?: { + /** @description Examples: + * - `select=size` - Example of select, which only return the "size" field. + * - `select=size * 2 as bigger_size` - Example of a complex expression with a label, which returns a new field named "bigger_size" and containing the double of size field value. + * - `select=dataset_id, fields` - Example of a select in catalog ODSQL query to only retrieve dataset_id and schema of datasets. + * + * A select expression can be used to add, remove or change the fields to return. + * An expression can be: + * - a wildcard ('*'): all fields are returned. + * - A field name: only the specified field is returned. + * - An include/exclude function: All fields matching the include or exclude expression are included or excluded. This expression can contain wildcard. + * - A complex expression. The result of the expression is returned. A label can be set for this expression, and in that case, the field will be named after this label. */ + select?: components["parameters"]["select"]; + /** @description A language value. + * + * If specified, the `lang` value override the default language, which is "fr". + * The language is used to format string, for example in the `date_format` function. */ + lang?: components["parameters"]["lang"]; + /** @description Set the timezone for datetime fields. + * + * Timezone IDs are defined by the [Unicode CLDR project](https://github.com/unicode-org/cldr). The list of timezone IDs is available in [timezone.xml](https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml). */ + timezone?: components["parameters"]["timezone"]; + }; + header?: never; + path: { + /** @description The identifier of the dataset to be queried. + * + * You can find it in the "Information" tab of the dataset page or in the dataset URL, right after `/datasets/`. */ + dataset_id: components["parameters"]["dataset_id"]; + /** @description Record identifier */ + record_id: components["parameters"]["record_id"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description A single record */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json; charset=utf-8": components["schemas"]["record"]; + }; + }; + 400: components["responses"]["bad_request"]; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + 429: components["responses"]["quota"]; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; +} diff --git a/packages/opendatasoft/package.json b/packages/opendatasoft/package.json new file mode 100644 index 000000000..339a20f03 --- /dev/null +++ b/packages/opendatasoft/package.json @@ -0,0 +1,50 @@ +{ + "name": "@gouvfr-lasuite/proconnect.opendatasoft", + "version": "0.0.0", + "homepage": "https://github.com/numerique-gouv/proconnect-identite/tree/master/packages/opendatasoft#readme", + "bugs": "https://github.com/numerique-gouv/proconnect-identite/issues", + "repository": { + "type": "git", + "url": "git+https://github.com/numerique-gouv/proconnect-identite.git", + "directory": "packages/opendatasoft" + }, + "license": "MIT", + "sideEffects": false, + "type": "module", + "imports": { + "#openapi": "./openapi/index.d.ts", + "#src/*": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + } + }, + "exports": { + "./*": { + "types": "./dist/*/index.d.ts", + "default": "./dist/*/index.js" + } + }, + "scripts": { + "build": "tsc --build tsconfig.lib.json", + "check": "npm run build -- --noEmit", + "dev": "npm run build -- --watch --preserveWatchOutput", + "build:openapi": "openapi-typescript https://api-opendatasoft.service-public.fr/api/explore/v2.1/swagger.json -o openapi/index.d.ts", + "dev:test": "tsx --watch --test src/**/*.test.ts", + "postbuild:openapi": "prettier --write openapi/index.d.ts", + "test": "tsx --test --test-reporter=spec src/**/*.test.ts" + }, + "dependencies": { + "@gouvfr-lasuite/proconnect.core": "^0.5.0", + "openapi-fetch": "^0.14.0" + }, + "devDependencies": { + "@gouvfr-lasuite/proconnect.devtools.typescript": "0.0.0", + "@types/node": "^22.14.1", + "openapi-typescript": "^7.6.1", + "tsx": "^4.19.4" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/get-annuaire-service-public-contact-email.test.ts b/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/get-annuaire-service-public-contact-email.test.ts new file mode 100644 index 000000000..1aca2d09b --- /dev/null +++ b/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/get-annuaire-service-public-contact-email.test.ts @@ -0,0 +1,217 @@ +// + +import { + createOpendatasoftOpenApiClient, + type OpendatasoftOpenApiClient, +} from "#src/client"; +import { + ApiOpendatasoftConnectionError, + ApiOpendatasoftNotFoundError, + ApiOpendatasoftUnprocessableEntityError, + type ApiLannuaireAdministrationRecords, + type ApiOpendatasoftResponseBadRequestError, +} from "#src/types"; +import assert from "node:assert/strict"; +import { describe, it, mock } from "node:test"; +import { getAnnuaireServicePublicContactEmailFactory } from "./get-annuaire-service-public-contact-email.js"; + +// + +describe("getAnnuaireServicePublicContactEmail", () => { + it("should return a valid email", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 1, + results: [{ adresse_courriel: "administration@aurillac.fr" }], + } as ApiLannuaireAdministrationRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://api-lannuaire.service-public.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireServicePublicContactEmail = + getAnnuaireServicePublicContactEmailFactory(client); + + assert.equal( + await getAnnuaireServicePublicContactEmail("15014", "15000"), + "administration@aurillac.fr", + ); + }); + + it("should return valid email for two mairies with the same Code Officiel Geographique", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 2, + results: [ + { + adresse_courriel: "venosc@mairie2alpes.fr", + adresse: JSON.stringify([{ code_postal: "38520" }]), + }, + { + adresse_courriel: "accueil@mairie2alpes.fr", + adresse: JSON.stringify([{ code_postal: "38860" }]), + }, + ], + } as ApiLannuaireAdministrationRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://api-lannuaire.service-public.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireServicePublicContactEmail = + getAnnuaireServicePublicContactEmailFactory(client); + + assert.equal( + await getAnnuaireServicePublicContactEmail("38253", "38860"), + "accueil@mairie2alpes.fr", + ); + }); + + it("should throw an error for invalid cog", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 0, + results: [], + } as ApiLannuaireAdministrationRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://api-lannuaire.service-public.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireServicePublicContactEmail = + getAnnuaireServicePublicContactEmailFactory(client); + + await assert.rejects( + getAnnuaireServicePublicContactEmail("00000", "00000"), + ApiOpendatasoftNotFoundError, + ); + }); + + it("❎ fail with null codeOfficielGeographique", async () => { + const getAnnuaireServicePublicContactEmail = + getAnnuaireServicePublicContactEmailFactory( + {} as OpendatasoftOpenApiClient, + ); + await assert.rejects( + getAnnuaireServicePublicContactEmail(null, "00000"), + ApiOpendatasoftNotFoundError, + ); + }); + + it("❎ fail with http error", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + error_code: "ODSQLSyntaxError", + message: "💣", + } as ApiOpendatasoftResponseBadRequestError), + { status: 400 }, + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://api-lannuaire.service-public.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireServicePublicContactEmail = + getAnnuaireServicePublicContactEmailFactory(client); + await assert.rejects( + getAnnuaireServicePublicContactEmail("🗺️", null), + new ApiOpendatasoftConnectionError(""), + ); + }); + + it("❎ fail without postal code", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 2, + results: [{}, {}], + } as ApiLannuaireAdministrationRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://api-lannuaire.service-public.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireServicePublicContactEmail = + getAnnuaireServicePublicContactEmailFactory(client); + + await assert.rejects( + getAnnuaireServicePublicContactEmail("🗺️", null), + new ApiOpendatasoftUnprocessableEntityError( + "Without postal code, we cannot choose a mairie between 2 results.", + ), + ); + }); + + it("❎ fail with no pair found", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 2, + results: [ + { adresse: JSON.stringify([{ code_postal: "🌁" }]) }, + { adresse: JSON.stringify([{ code_postal: "🛝" }]) }, + ], + } as ApiLannuaireAdministrationRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://api-lannuaire.service-public.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireServicePublicContactEmail = + getAnnuaireServicePublicContactEmailFactory(client); + + await assert.rejects( + getAnnuaireServicePublicContactEmail("🗺️", "⛩️"), + new ApiOpendatasoftNotFoundError( + "No pair found for (codeOfficielGeographique: 🗺️, codePostal: ⛩️).", + ), + ); + }); + + it("❎ fail with invalid email address.", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 1, + results: [{ adresse_courriel: "📺" }], + } as ApiLannuaireAdministrationRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://api-lannuaire.service-public.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireServicePublicContactEmail = + getAnnuaireServicePublicContactEmailFactory(client); + + await assert.rejects( + getAnnuaireServicePublicContactEmail("🗺️", null), + new ApiOpendatasoftUnprocessableEntityError( + "📺 is not a valid email address.", + ), + ); + }); +}); diff --git a/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/get-annuaire-service-public-contact-email.ts b/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/get-annuaire-service-public-contact-email.ts new file mode 100644 index 000000000..7619f0e95 --- /dev/null +++ b/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/get-annuaire-service-public-contact-email.ts @@ -0,0 +1,106 @@ +// + +import type { OpendatasoftOpenApiClient } from "#src/client"; +import { + ApiOpendatasoftConnectionError, + ApiOpendatasoftNotFoundError, + ApiOpendatasoftUnprocessableEntityError, + type ApiLannuaireAdministrationRecord, + type SelectedAdministrationRecord, +} from "#src/types"; +import { isEmailValid } from "@gouvfr-lasuite/proconnect.core/security"; +import { isEmpty } from "@gouvfr-lasuite/proconnect.core/utils/lodash"; +import type { FetchOptions } from "openapi-fetch"; + +// + +export function getAnnuaireServicePublicContactEmailFactory( + client: OpendatasoftOpenApiClient, + optionsFn: () => FetchOptions = () => ({}), +) { + return async function getAnnuaireServicePublicContactEmail( + codeOfficielGeographique: string | null, + codePostal: string | null, + ): Promise { + if (isEmpty(codeOfficielGeographique)) + throw new ApiOpendatasoftNotFoundError(); + + const { data, error } = await client.GET( + "/catalog/datasets/{dataset_id}/records", + { + ...optionsFn(), + params: { + path: { + dataset_id: "api-lannuaire-administration", + }, + query: { + select: [ + "adresse_courriel", + "adresse", + "code_insee_commune", + "nom", + ].join(", "), + where: [ + `code_insee_commune LIKE "${codeOfficielGeographique}"`, + `pivot LIKE "mairie"`, + ].join(" and "), + }, + }, + }, + ); + + if (error) throw new ApiOpendatasoftConnectionError("", { cause: error }); + if (!data?.results) + throw new ApiOpendatasoftUnprocessableEntityError("Response malformed"); + + type Administration = SelectedAdministrationRecord & { + adresse: [{ code_postal: string }]; + }; + const features: Administration[] = data.results.map( + (feature: ApiLannuaireAdministrationRecord) => ({ + ...feature, + // HACK(douglasduteil): the API returns a string instead of a JSON object in the adresse field + adresse: JSON.parse(feature.adresse ?? "[]"), + }), + ); + + let feature: Administration | undefined; + + if (features.length === 1) { + feature = features[0]; + } else if (features.length > 1) { + if (!codePostal) { + throw new ApiOpendatasoftUnprocessableEntityError( + `Without postal code, we cannot choose a mairie between ${features.length} results.`, + ); + } + + // Take the first match + feature = features.find( + ({ adresse: [{ code_postal: codePostalMairie }] }) => + codePostalMairie === codePostal, + ); + } + + if (!feature) { + throw new ApiOpendatasoftNotFoundError( + `No pair found for (codeOfficielGeographique: ${codeOfficielGeographique}, codePostal: ${codePostal}).`, + ); + } + + const { adresse_courriel } = feature; + const formattedEmail = adresse_courriel?.toLowerCase().trim(); + + if (!isEmailValid(formattedEmail)) { + throw new ApiOpendatasoftUnprocessableEntityError( + `${formattedEmail} is not a valid email address.`, + ); + } + + return formattedEmail; + }; +} + +export type GetAnnuaireServicePublicContactEmailHandler = ReturnType< + typeof getAnnuaireServicePublicContactEmailFactory +>; diff --git a/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/index.ts b/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/index.ts new file mode 100644 index 000000000..62cc77ed0 --- /dev/null +++ b/packages/opendatasoft/src/api/api-lannuaire.service-public.fr/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./get-annuaire-service-public-contact-email.js"; diff --git a/packages/opendatasoft/src/api/data.education.gouv.fr/get-annuaire-education-nationale-contact-email.test.ts b/packages/opendatasoft/src/api/data.education.gouv.fr/get-annuaire-education-nationale-contact-email.test.ts new file mode 100644 index 000000000..c0a795e12 --- /dev/null +++ b/packages/opendatasoft/src/api/data.education.gouv.fr/get-annuaire-education-nationale-contact-email.test.ts @@ -0,0 +1,136 @@ +// + +import { createOpendatasoftOpenApiClient } from "#src/client"; +import { + ApiOpendatasoftNotFoundError, + ApiOpendatasoftUnprocessableEntityError, + type ApiAnnuaireEducationNationaleRecords, +} from "#src/types"; +import assert from "node:assert/strict"; +import { describe, it, mock } from "node:test"; +import { getAnnuaireEducationNationaleContactEmailFactory } from "./get-annuaire-education-nationale-contact-email.js"; + +describe("getAnnuaireEducationNationaleContactEmail", () => { + it("should return Collège Jeanne d'Arc contact email", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 1, + results: [{ mail: "jeannedarc.millau@gmail.com" }], + } as ApiAnnuaireEducationNationaleRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://data.education.gouv.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireEducationNationaleContactEmail = + getAnnuaireEducationNationaleContactEmailFactory(client); + const contactEmail = + await getAnnuaireEducationNationaleContactEmail("77672253000040"); + assert.equal(contactEmail, "jeannedarc.millau@gmail.com"); + }); + + it("should return Lycée Younoussa Bamana contact email", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 1, + results: [{ mail: "lgt.bamana@ac-mayotte.fr" }], + } as ApiAnnuaireEducationNationaleRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://data.education.gouv.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireEducationNationaleContactEmail = + getAnnuaireEducationNationaleContactEmailFactory(client); + const contactEmail = + await getAnnuaireEducationNationaleContactEmail("20000454700010"); + assert.equal(contactEmail, "lgt.bamana@ac-mayotte.fr"); + }); + + it("should return valid email for a college and a lycee sharing the same SIRET", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 2, + results: [ + { + nom_etablissement: "Collège Jeanne d'Arc", + mail: "jeannedarc.👶@gmail.com", + }, + { + nom_etablissement: "Lycée professionnel privé Jeanne d'Arc", + mail: "jeannedarc.👴@gmail.com", + }, + ], + } as ApiAnnuaireEducationNationaleRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://data.education.gouv.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireEducationNationaleContactEmail = + getAnnuaireEducationNationaleContactEmailFactory(client); + const contactEmail = + await getAnnuaireEducationNationaleContactEmail("77672253000040"); + assert.equal(contactEmail, "jeannedarc.👶@gmail.com"); + }); + + it("❎ fail with no result", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 0, + results: [], + } as ApiAnnuaireEducationNationaleRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://data.education.gouv.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireEducationNationaleContactEmail = + getAnnuaireEducationNationaleContactEmailFactory(client); + await assert.rejects( + getAnnuaireEducationNationaleContactEmail("⛩️"), + new ApiOpendatasoftNotFoundError(), + ); + }); + + it("❎ fail with invalid email address.", async () => { + const fetch = mock.fn(() => { + return Promise.resolve( + new Response( + JSON.stringify({ + total_count: 1, + results: [{ mail: "📺" }], + } as ApiAnnuaireEducationNationaleRecords), + ), + ); + }); + const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://data.education.gouv.fr/api/explore/v2.1", + fetch, + }); + const getAnnuaireEducationNationaleContactEmail = + getAnnuaireEducationNationaleContactEmailFactory(client); + await assert.rejects( + getAnnuaireEducationNationaleContactEmail("⛩️"), + new ApiOpendatasoftUnprocessableEntityError( + "📺 is not a valid email address.", + ), + ); + }); +}); diff --git a/packages/opendatasoft/src/api/data.education.gouv.fr/get-annuaire-education-nationale-contact-email.ts b/packages/opendatasoft/src/api/data.education.gouv.fr/get-annuaire-education-nationale-contact-email.ts new file mode 100644 index 000000000..ec8f59b9a --- /dev/null +++ b/packages/opendatasoft/src/api/data.education.gouv.fr/get-annuaire-education-nationale-contact-email.ts @@ -0,0 +1,67 @@ +// + +import type { OpendatasoftOpenApiClient } from "#src/client/"; +import { + ApiOpendatasoftConnectionError, + ApiOpendatasoftNotFoundError, + ApiOpendatasoftUnprocessableEntityError, + type ApiAnnuaireEducationNationaleRecord, +} from "#src/types"; +import { isEmailValid } from "@gouvfr-lasuite/proconnect.core/security"; +import { isEmpty } from "@gouvfr-lasuite/proconnect.core/utils/lodash"; +import type { FetchOptions } from "openapi-fetch"; + +// + +export function getAnnuaireEducationNationaleContactEmailFactory( + client: OpendatasoftOpenApiClient, + optionsFn: () => FetchOptions = () => ({}), +) { + return async function getAnnuaireEducationNationaleContactEmail( + siret: string | null, + ): Promise { + if (isEmpty(siret)) { + throw new ApiOpendatasoftNotFoundError(); + } + + const { data, error } = await client.GET( + "/catalog/datasets/{dataset_id}/records", + { + ...optionsFn(), + params: { + path: { + dataset_id: "fr-en-annuaire-education", + }, + query: { + select: ["mail", "nom_etablissement"].join(", "), + where: [`siren_siret = "${siret}"`].join(" and "), + }, + }, + }, + ); + + if (error) throw new ApiOpendatasoftConnectionError("", { cause: error }); + const results = data?.results; + + if (!results) + throw new ApiOpendatasoftUnprocessableEntityError("Response malformed", { + cause: data, + }); + + // We take the first établissement as every établissements are sharing the same SIRET. + // We assume the first contact email is OK for every other établissements. + const record = results.at(0) as ApiAnnuaireEducationNationaleRecord; + if (isEmpty(record)) { + throw new ApiOpendatasoftNotFoundError(); + } + + const { mail } = record; + const formattedEmail = mail?.toLowerCase().trim(); + if (!isEmailValid(formattedEmail)) + throw new ApiOpendatasoftUnprocessableEntityError( + `${formattedEmail} is not a valid email address.`, + ); + + return formattedEmail; + }; +} diff --git a/packages/opendatasoft/src/api/data.education.gouv.fr/index.ts b/packages/opendatasoft/src/api/data.education.gouv.fr/index.ts new file mode 100644 index 000000000..91de9800b --- /dev/null +++ b/packages/opendatasoft/src/api/data.education.gouv.fr/index.ts @@ -0,0 +1,3 @@ +// + +export * from "./get-annuaire-education-nationale-contact-email.js"; diff --git a/packages/opendatasoft/src/client/index.ts b/packages/opendatasoft/src/client/index.ts new file mode 100644 index 000000000..48b30fb06 --- /dev/null +++ b/packages/opendatasoft/src/client/index.ts @@ -0,0 +1,16 @@ +// + +import type { paths } from "#openapi"; +import createClient from "openapi-fetch"; + +// + +/** + * Create an Opendatasoft client + * @see https://help.opendatasoft.com/apis/ods-explore-v2/explore_v2.1.html + */ +export const createOpendatasoftOpenApiClient = createClient; + +export type OpendatasoftOpenApiClient = ReturnType< + typeof createOpendatasoftOpenApiClient +>; diff --git a/test/api-annuaire-education-nationale-data/no-result.json b/packages/opendatasoft/src/testing/seed/api-annuaire-education-nationale-data/no-result.json similarity index 100% rename from test/api-annuaire-education-nationale-data/no-result.json rename to packages/opendatasoft/src/testing/seed/api-annuaire-education-nationale-data/no-result.json diff --git a/packages/opendatasoft/src/types/errors.ts b/packages/opendatasoft/src/types/errors.ts new file mode 100644 index 000000000..98d1c2add --- /dev/null +++ b/packages/opendatasoft/src/types/errors.ts @@ -0,0 +1,32 @@ +import type { components } from "#openapi"; + +// +export type ApiOpendatasoftResponseBadRequestError = + components["responses"]["bad_request"]["content"]["application/json; charset=utf-8"]; + +export class ApiOpendatasoftError extends Error { + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + this.name = "ApiOpendatasoftError"; + } +} + +export class ApiOpendatasoftNotFoundError extends Error { + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + this.name = "ApiOpendatasoftNotFoundError"; + } +} +export class ApiOpendatasoftConnectionError extends Error { + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + this.name = "ApiOpendatasoftConnectionError"; + } +} + +export class ApiOpendatasoftUnprocessableEntityError extends Error { + constructor(message?: string, options?: ErrorOptions) { + super(message, options); + this.name = "ApiOpendatasoftUnprocessableEntityError"; + } +} diff --git a/packages/opendatasoft/src/types/index.ts b/packages/opendatasoft/src/types/index.ts new file mode 100644 index 000000000..7302daecd --- /dev/null +++ b/packages/opendatasoft/src/types/index.ts @@ -0,0 +1,4 @@ +// + +export * from "./errors.js"; +export * from "./record.js"; diff --git a/packages/opendatasoft/src/types/record.ts b/packages/opendatasoft/src/types/record.ts new file mode 100644 index 000000000..a529961be --- /dev/null +++ b/packages/opendatasoft/src/types/record.ts @@ -0,0 +1,32 @@ +// + +import type { components } from "#openapi"; + +// + +export type Records = components["schemas"]["records"]; +export type Record = components["schemas"]["record"]; + +// + +export type SelectedAdministrationRecord = { + adresse_courriel?: string; + adresse?: string; + nom?: string; +}; +export type ApiLannuaireAdministrationRecords = Records & { + results: SelectedAdministrationRecord[]; +}; +export type ApiLannuaireAdministrationRecord = + ApiLannuaireAdministrationRecords["results"][0]; + +// + +export type ApiAnnuaireEducationNationaleRecords = Records & { + results: { + nom_etablissement?: string; + mail?: string; + }[]; +}; +export type ApiAnnuaireEducationNationaleRecord = + ApiAnnuaireEducationNationaleRecords["results"][0]; diff --git a/packages/opendatasoft/tsconfig.json b/packages/opendatasoft/tsconfig.json new file mode 100644 index 000000000..53e9dd68f --- /dev/null +++ b/packages/opendatasoft/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "src", + "resolveJsonModule": true, + "types": ["node"] + }, + "extends": "@gouvfr-lasuite/proconnect.devtools.typescript/base/tsconfig.json", + "include": ["openapi", "src"], + "references": [] +} diff --git a/packages/opendatasoft/tsconfig.lib.json b/packages/opendatasoft/tsconfig.lib.json new file mode 100644 index 000000000..3ba354418 --- /dev/null +++ b/packages/opendatasoft/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "exclude": ["src/**/*.test.ts"], + "extends": "./tsconfig.json", + "include": ["src"] +} diff --git a/src/config/errors.ts b/src/config/errors.ts index a6b8b5865..e7a249e6c 100644 --- a/src/config/errors.ts +++ b/src/config/errors.ts @@ -76,33 +76,10 @@ export class InvalidTokenError extends Error {} export class InvalidMagicLinkError extends Error {} -export class ApiAnnuaireError extends Error {} - -export class ApiAnnuaireNotFoundError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message, options); - this.name = "ApiAnnuaireNotFoundError"; - } -} - -export class ApiAnnuaireTooManyResultsError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message, options); - this.name = "ApiAnnuaireTooManyResultsError"; - } -} - -export class ApiAnnuaireInvalidEmailError extends Error { - constructor(message?: string, options?: ErrorOptions) { - super(message, options); - this.name = "ApiAnnuaireInvalidEmailError"; - } -} - -export class ApiAnnuaireConnectionError extends Error { +export class ApiAnnuaireError extends Error { constructor(message?: string, options?: ErrorOptions) { super(message, options); - this.name = "ApiAnnuaireConnectionError"; + this.name = "ApiAnnuaireError"; } } diff --git a/src/connectors/api-annuaire-education-nationale.ts b/src/connectors/api-annuaire-education-nationale.ts index 9f0267506..a15b1f8ac 100644 --- a/src/connectors/api-annuaire-education-nationale.ts +++ b/src/connectors/api-annuaire-education-nationale.ts @@ -1,249 +1,28 @@ -import { isEmailValid } from "@gouvfr-lasuite/proconnect.core/security"; -import axios, { AxiosError, type AxiosResponse } from "axios"; -import { isEmpty, isString } from "lodash-es"; +// + +import { getAnnuaireEducationNationaleContactEmailFactory } from "@gouvfr-lasuite/proconnect.opendatasoft/api/data.education.gouv.fr"; +import { createOpendatasoftOpenApiClient } from "@gouvfr-lasuite/proconnect.opendatasoft/client"; import { FEATURE_USE_ANNUAIRE_EMAILS, HTTP_CLIENT_TIMEOUT, TEST_CONTACT_EMAIL, } from "../config/env"; -import { - ApiAnnuaireConnectionError, - ApiAnnuaireInvalidEmailError, - ApiAnnuaireNotFoundError, -} from "../config/errors"; import { logger } from "../services/log"; -type ApiAnnuaireEducationNationaleReponse = { - total_count: number; - links: { - rel: "self" | "first" | "last"; - // ex: "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records?where=siren_siret%3D19750663700010&limit=10&offset=0&include_app_metas=False&include_links=False" - href: string; - }[]; - records: { - links: { - rel: "self" | "datasets" | "dataset"; - // ex: "https://data.education.gouv.fr/api/v2/catalog/datasets" - href: string; - }[]; - record: { - // ex: '886c9de6d33b66f33ead6ce93deb7a0eac0c3995' - id: string; - // ex: '2023-10-04T12:45:00Z' - timestamp: string; - // ex: 758 - size: number; - fields: { - // ex: '0750663N' - identifiant_de_l_etablissement: string; - // ex: 'Lycée Chaptal' - nom_etablissement: string; - // ex: 'Lycée' - type_etablissement: string; - // ex: 'Public' - statut_public_prive: string; - // ex: '45 boulevard des Batignolles' - adresse_1: string; - // ex: null - adresse_2: string | null; - // ex: null - adresse_3: string | null; - // ex: '75008' - code_postal: string; - // ex: '75100' - code_commune: string; - // ex: 'Paris' - nom_commune: string; - // ex: '075' - code_departement: string; - // ex: '01' - code_academie: string; - // ex: '11' - code_region: string; - // ex: null - ecole_maternelle: string | null; - // ex: null - ecole_elementaire: string | null; - // ex: '1' - voie_generale: "1" | "0"; - // ex: '1' - voie_technologique: "1" | "0"; - // ex: '0' - voie_professionnelle: "1" | "0"; - // ex: '01 45 22 76 95' - telephone: string; - // ex: '01 45 22 85 12' - fax: string; - // ex: 'https://lycee-chaptal.ac-paris.fr' - web: string; - // ex: 'ce.0750663n@ac-paris.fr' - mail: string; - // ex: 1 - restauration: number; - // ex: 1 - hebergement: number; - // ex: 0 - ulis: number; - // ex: '0' - apprentissage: "1" | "0"; - // ex: '0' - segpa: "1" | "0"; - // ex: '0' - section_arts: "1" | "0"; - // ex: '0' - section_cinema: "1" | "0"; - // ex: '0' - section_theatre: "1" | "0"; - // ex: '0' - section_sport: "1" | "0"; - // ex: '0' - section_internationale: "1" | "0"; - // ex: '1' - section_europeenne: "1" | "0"; - // ex: '0' - lycee_agricole: "1" | "0"; - // ex: '0' - lycee_militaire: "1" | "0"; - // ex: '0' - lycee_des_metiers: "1" | "0"; - // ex: '1' - post_bac: "1" | "0"; - // ex: null - appartenance_education_prioritaire: null; - // ex: '1' - greta: "1" | "0"; - // ex: '19750663700010' - siren_siret: string; - // ex: 564 - nombre_d_eleves: number; - // ex: 'https://www.onisep.fr/http/redirection/etablissement/slug/ENS.1865' - fiche_onisep: string; - position: { - lon: number; - lat: number; - }; - // ex: 'SANS OBJET' - type_contrat_prive: string; - // ex: 'Paris' - libelle_departement: string; - // ex: 'Paris' - libelle_academie: string; - // ex: 'Ile-de-France' - libelle_region: string; - // ex: 650107.7 - coordx_origine: number; - // ex: 6864850.5 - coordy_origine: number; - // ex: 'EPSG:2154' - epsg_origine: string; - // ex: null - nom_circonscription: null; - // ex: 48.881738320109136 - latitude: number; - // ex: 2.3196885071009623 - longitude: number; - // ex: 'Numéro de rue' - precision_localisation: string; - // ex: '1965-05-01' - date_ouverture: string; - // ex: '2023-10-04' - date_maj_ligne: string; - // ex: 'OUVERT' - etat: string; - // ex: "MINISTERE DE L'EDUCATION NATIONALE" - ministere_tutelle: string; - // ex: 0 - multi_uai: number; - // ex: 0 - rpi_concentre: number; - // ex: null - rpi_disperse: null; - // ex: 300 - code_nature: number; - // ex: 'LYCEE ENSEIGNT GENERAL ET TECHNOLOGIQUE' - libelle_nature: string; - // ex: '99' - code_type_contrat_prive: string; - // ex: '0752529S' - pial: string; - // ex: null - etablissement_mere: null; - // ex: null - type_rattachement_etablissement_mere: null; - // ex: '01000' - code_zone_animation_pedagogique: string; - // ex: 'BASSIN PARIS' - libelle_zone_animation_pedagogique: string; +// + +const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://data.education.gouv.fr/api/v2.1", +}); + +export const getAnnuaireEducationNationaleContactEmail = + FEATURE_USE_ANNUAIRE_EMAILS + ? getAnnuaireEducationNationaleContactEmailFactory(client, () => ({ + signal: AbortSignal.timeout(HTTP_CLIENT_TIMEOUT), + })) + : () => { + logger.info( + `Test email address ${TEST_CONTACT_EMAIL} was used instead of the real one.`, + ); + return TEST_CONTACT_EMAIL; }; - }; - }[]; -}; - -export const getAnnuaireEducationNationaleContactEmail = async ( - siret: string | null, -): Promise => { - if (isEmpty(siret)) { - throw new ApiAnnuaireNotFoundError(); - } - - let records: ApiAnnuaireEducationNationaleReponse["records"] = []; - try { - const { data }: AxiosResponse = - await axios({ - method: "GET", - url: `https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records?where=siren_siret%3D${siret}`, - headers: { - accept: "application/json", - }, - timeout: HTTP_CLIENT_TIMEOUT, - }); - - records = data.records; - } catch (e) { - if ( - e instanceof AxiosError && - (e.code === "ECONNABORTED" || - e.code === "ERR_BAD_RESPONSE" || - e.code === "EAI_AGAIN") - ) { - throw new ApiAnnuaireConnectionError(); - } - - throw e; - } - - let record: ApiAnnuaireEducationNationaleReponse["records"][0] | undefined; - - // We take the first établissement as every établissements are sharing the same SIRET. - // We assume the first contact email is OK for every other établissements. - record = records[0]; - - if (isEmpty(record)) { - throw new ApiAnnuaireNotFoundError(); - } - - const { - record: { - fields: { mail }, - }, - } = record; - - if (!isString(mail)) { - throw new ApiAnnuaireInvalidEmailError(); - } - - const formattedEmail = mail.toLowerCase().trim(); - - if (!isEmailValid(formattedEmail)) { - throw new ApiAnnuaireInvalidEmailError(); - } - - if (!FEATURE_USE_ANNUAIRE_EMAILS) { - logger.info( - `Test email address ${TEST_CONTACT_EMAIL} was used instead of the real one ${formattedEmail}.`, - ); - return TEST_CONTACT_EMAIL; - } - - return formattedEmail; -}; diff --git a/src/connectors/api-annuaire-service-public.ts b/src/connectors/api-annuaire-service-public.ts index 03087d70d..41d030a91 100644 --- a/src/connectors/api-annuaire-service-public.ts +++ b/src/connectors/api-annuaire-service-public.ts @@ -1,135 +1,27 @@ -import { isEmailValid } from "@gouvfr-lasuite/proconnect.core/security"; -import axios, { AxiosError, type AxiosResponse } from "axios"; -import { isEmpty, isString } from "lodash-es"; +// + +import { getAnnuaireServicePublicContactEmailFactory } from "@gouvfr-lasuite/proconnect.opendatasoft/api/api-lannuaire.service-public.fr"; +import { createOpendatasoftOpenApiClient } from "@gouvfr-lasuite/proconnect.opendatasoft/client"; import { FEATURE_USE_ANNUAIRE_EMAILS, HTTP_CLIENT_TIMEOUT, TEST_CONTACT_EMAIL, } from "../config/env"; -import { - ApiAnnuaireConnectionError, - ApiAnnuaireInvalidEmailError, - ApiAnnuaireNotFoundError, - ApiAnnuaireTooManyResultsError, -} from "../config/errors"; import { logger } from "../services/log"; -// more info at https://api-lannuaire.service-public.fr/api/explore/v2.1/console - -type ApiAnnuaireServicePublicReponse = { - type: "FeatureCollection"; - total_count: number; - results: { - site_internet: { - valeur: string; - }[]; - nom: string; - adresse_courriel?: string; - pivot: { - type_service_local: string; - }[]; - id: string; - telephone: { - valeur: string; - }[]; - code_insee_commune: string; - adresse: { - type_adresse: "Adresse"; - numero_voie: string; - code_postal: string; - nom_commune: string; - longitude: string; - latitude: string; - }[]; - }[]; -}; - -export const getAnnuaireServicePublicContactEmail = async ( - codeOfficielGeographique: string | null, - codePostal: string | null, -): Promise => { - if (isEmpty(codeOfficielGeographique)) { - throw new ApiAnnuaireNotFoundError(); - } - - let features: ApiAnnuaireServicePublicReponse["results"] = []; - try { - const { data }: AxiosResponse = - await axios({ - method: "get", - url: `https://api-lannuaire.service-public.fr/api/explore/v2.1/catalog/datasets/api-lannuaire-administration/records?where=code_insee_commune LIKE "${codeOfficielGeographique}" and pivot LIKE "mairie"`, - headers: { - accept: "application/json", - }, - timeout: HTTP_CLIENT_TIMEOUT, - }); - - features = data.results.map((feature) => ({ - ...feature, - // HACK(douglasduteil): the API returns a string instead of a JSON object in the adresse field - adresse: JSON.parse(feature.adresse as any), - })); - } catch (e) { - if ( - e instanceof AxiosError && - (e.code === "ECONNABORTED" || - e.code === "ERR_BAD_RESPONSE" || - e.code === "EAI_AGAIN") - ) { - throw new ApiAnnuaireConnectionError(undefined, { cause: e }); - } - - throw e; - } - - let feature: ApiAnnuaireServicePublicReponse["results"][0] | undefined; +// - if (features.length === 1) { - feature = features[0]; - } +const client = createOpendatasoftOpenApiClient({ + baseUrl: "https://api-lannuaire.service-public.fr/api/explore/v2.1", +}); - if (features.length > 1) { - if (isEmpty(codePostal)) { - throw new ApiAnnuaireTooManyResultsError( - `Without postal code, we cannot choose a mairie between ${features.length} results.`, +export const getAnnuaireServicePublicContactEmail = FEATURE_USE_ANNUAIRE_EMAILS + ? getAnnuaireServicePublicContactEmailFactory(client, () => ({ + signal: AbortSignal.timeout(HTTP_CLIENT_TIMEOUT), + })) + : () => { + logger.info( + `Test email address ${TEST_CONTACT_EMAIL} was used instead of the real one.`, ); - } - - // Take the first match - feature = features.find( - ({ adresse: [{ code_postal: codePostalMairie }] }) => - codePostalMairie === codePostal, - ); - } - - if (isEmpty(feature)) { - throw new ApiAnnuaireNotFoundError( - `No pair found for (codeOfficielGeographique: ${codeOfficielGeographique}, codePostal: ${codePostal}).`, - ); - } - - const { adresse_courriel } = feature; - - if (!isString(adresse_courriel)) { - throw new ApiAnnuaireInvalidEmailError( - `${adresse_courriel} is not a string.`, - ); - } - - const formattedEmail = adresse_courriel.toLowerCase().trim(); - - if (!isEmailValid(formattedEmail)) { - throw new ApiAnnuaireInvalidEmailError( - `${formattedEmail} is not a valid email address.`, - ); - } - - if (!FEATURE_USE_ANNUAIRE_EMAILS) { - logger.info( - `Test email address ${TEST_CONTACT_EMAIL} was used instead of the real one ${formattedEmail}.`, - ); - return TEST_CONTACT_EMAIL; - } - - return formattedEmail; -}; + return TEST_CONTACT_EMAIL; + }; diff --git a/src/connectors/test.ts b/src/connectors/test.ts deleted file mode 100644 index 791980d86..000000000 --- a/src/connectors/test.ts +++ /dev/null @@ -1,26 +0,0 @@ -type ApiAnnuaireServicePublicReponse = { - total_count: number; - results: { - site_internet: { - valeur: string; - }[]; - nom: string; - adresse_courriel?: string; - pivot: { - type_service_local: string; - }[]; - id: string; - telephone: { - valeur: string; - }[]; - code_insee_commune: string; - adresse: { - type_adresse: "Adresse"; - numero_voie: string; - code_postal: string; - nom_commune: string; - longitude: string; - latitude: string; - }[]; - }[]; -}; diff --git a/src/managers/organization/official-contact-email-verification.ts b/src/managers/organization/official-contact-email-verification.ts index 9d68499bf..5f42fd510 100644 --- a/src/managers/organization/official-contact-email-verification.ts +++ b/src/managers/organization/official-contact-email-verification.ts @@ -77,7 +77,7 @@ export const sendOfficialContactEmailVerificationEmail = async ({ contactEmail = await getAnnuaireEducationNationaleContactEmail(siret); } } catch (error) { - throw new ApiAnnuaireError(); + throw new ApiAnnuaireError("", { cause: error }); } if (!contactEmail) { diff --git a/test/api-annuaire-education-nationale-data/two-etablissements.json b/test/api-annuaire-education-nationale-data/two-etablissements.json deleted file mode 100644 index a7df9af81..000000000 --- a/test/api-annuaire-education-nationale-data/two-etablissements.json +++ /dev/null @@ -1,209 +0,0 @@ -{ - "total_count": 2, - "links": [ - { - "rel": "self", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records?where=siren_siret%3D77672253000040&limit=10&offset=0&include_app_metas=False&include_links=False" - }, - { - "rel": "first", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records?where=siren_siret%3D77672253000040&limit=10&offset=0&include_app_metas=False&include_links=False" - }, - { - "rel": "last", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records?where=siren_siret%3D77672253000040&limit=10&offset=0&include_app_metas=False&include_links=False" - } - ], - "records": [ - { - "links": [ - { - "rel": "self", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records/983019be7e50f73a9ca9654b0b7e5b8c99d17b04" - }, - { - "rel": "datasets", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets" - }, - { - "rel": "dataset", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education" - } - ], - "record": { - "id": "983019be7e50f73a9ca9654b0b7e5b8c99d17b04", - "timestamp": "2023-10-04T12:45:00Z", - "size": 797, - "fields": { - "identifiant_de_l_etablissement": "0121259D", - "nom_etablissement": "Collège Jeanne d'Arc", - "type_etablissement": "Collège", - "statut_public_prive": "Privé", - "adresse_1": "3 place du Mandarous", - "adresse_2": "BP 412", - "adresse_3": null, - "code_postal": "12104", - "code_commune": "12145", - "nom_commune": "Millau", - "code_departement": "012", - "code_academie": "16", - "code_region": "76", - "ecole_maternelle": null, - "ecole_elementaire": null, - "voie_generale": "0", - "voie_technologique": "0", - "voie_professionnelle": "0", - "telephone": "05 65 60 64 00", - "fax": "05 65 60 97 83", - "web": "http://www.jeannedarcmillau.fr/college-millau", - "mail": "jeannedarc.millau@gmail.com", - "restauration": 1, - "hebergement": 1, - "ulis": 1, - "apprentissage": "0", - "segpa": "0", - "section_arts": "0", - "section_cinema": "0", - "section_theatre": "0", - "section_sport": "1", - "section_internationale": "0", - "section_europeenne": "0", - "lycee_agricole": "0", - "lycee_militaire": "0", - "lycee_des_metiers": "0", - "post_bac": "0", - "appartenance_education_prioritaire": null, - "greta": "0", - "siren_siret": "77672253000040", - "nombre_d_eleves": 611, - "fiche_onisep": "https://www.onisep.fr/http/redirection/etablissement/slug/ENS.3893", - "position": { - "lon": 3.0782791213481806, - "lat": 44.099587303871296 - }, - "type_contrat_prive": "CONTRAT D'ASSOCIATION TOUTES CLASSES", - "libelle_departement": "Aveyron", - "libelle_academie": "Toulouse", - "libelle_region": "Occitanie", - "coordx_origine": 706267.5, - "coordy_origine": 6333401.3, - "epsg_origine": "EPSG:2154", - "nom_circonscription": null, - "latitude": 44.099587303871296, - "longitude": 3.0782791213481806, - "precision_localisation": "Numéro de rue", - "date_ouverture": "1980-06-24", - "date_maj_ligne": "2023-10-04", - "etat": "OUVERT", - "ministere_tutelle": "MINISTERE DE L'EDUCATION NATIONALE", - "multi_uai": 0, - "rpi_concentre": 0, - "rpi_disperse": null, - "code_nature": 340, - "libelle_nature": "COLLEGE", - "code_type_contrat_prive": "30", - "pial": "0120878P", - "etablissement_mere": null, - "type_rattachement_etablissement_mere": null, - "code_zone_animation_pedagogique": "16105", - "libelle_zone_animation_pedagogique": "MILLAU / SAINT-AFFRIQUE" - } - } - }, - { - "links": [ - { - "rel": "self", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education/records/730c157e079b71a380dd6b57b107172b19042f13" - }, - { - "rel": "datasets", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets" - }, - { - "rel": "dataset", - "href": "https://data.education.gouv.fr/api/v2/catalog/datasets/fr-en-annuaire-education" - } - ], - "record": { - "id": "730c157e079b71a380dd6b57b107172b19042f13", - "timestamp": "2023-10-04T12:45:00Z", - "size": 812, - "fields": { - "identifiant_de_l_etablissement": "0120105Z", - "nom_etablissement": "Lycée professionnel privé Jeanne d'Arc", - "type_etablissement": "Lycée", - "statut_public_prive": "Privé", - "adresse_1": "3 place du Mandarous", - "adresse_2": "BP 412", - "adresse_3": null, - "code_postal": "12104", - "code_commune": "12145", - "nom_commune": "Millau", - "code_departement": "012", - "code_academie": "16", - "code_region": "76", - "ecole_maternelle": null, - "ecole_elementaire": null, - "voie_generale": "0", - "voie_technologique": "0", - "voie_professionnelle": "1", - "telephone": "05 65 60 64 00", - "fax": "05 65 60 97 83", - "web": "http://www.jeannedarcmillau.fr", - "mail": "jeannedarc.millau@gmail.com", - "restauration": 1, - "hebergement": 1, - "ulis": 0, - "apprentissage": "0", - "segpa": "0", - "section_arts": "0", - "section_cinema": "0", - "section_theatre": "0", - "section_sport": "0", - "section_internationale": "0", - "section_europeenne": "0", - "lycee_agricole": "0", - "lycee_militaire": "0", - "lycee_des_metiers": "0", - "post_bac": "0", - "appartenance_education_prioritaire": null, - "greta": "0", - "siren_siret": "77672253000040", - "nombre_d_eleves": 125, - "fiche_onisep": "https://www.onisep.fr/http/redirection/etablissement/slug/ENS.4543", - "position": { - "lon": 3.078134373296811, - "lat": 44.099683711604264 - }, - "type_contrat_prive": "CONTRAT D'ASSOCIATION TOUTES CLASSES", - "libelle_departement": "Aveyron", - "libelle_academie": "Toulouse", - "libelle_region": "Occitanie", - "coordx_origine": 706255.9, - "coordy_origine": 6333412.0, - "epsg_origine": "EPSG:2154", - "nom_circonscription": null, - "latitude": 44.099683711604264, - "longitude": 3.078134373296811, - "precision_localisation": "Numéro de rue", - "date_ouverture": "1967-12-19", - "date_maj_ligne": "2023-10-04", - "etat": "OUVERT", - "ministere_tutelle": "MINISTERE DE L'EDUCATION NATIONALE", - "multi_uai": 0, - "rpi_concentre": 0, - "rpi_disperse": null, - "code_nature": 320, - "libelle_nature": "LYCEE PROFESSIONNEL", - "code_type_contrat_prive": "30", - "pial": "0120878P", - "etablissement_mere": null, - "type_rattachement_etablissement_mere": null, - "code_zone_animation_pedagogique": "16105", - "libelle_zone_animation_pedagogique": "MILLAU / SAINT-AFFRIQUE" - } - } - } - ] -} diff --git a/test/api-annuaire-education-nationale.test.ts b/test/api-annuaire-education-nationale.test.ts index ab7c7a673..d83429733 100644 --- a/test/api-annuaire-education-nationale.test.ts +++ b/test/api-annuaire-education-nationale.test.ts @@ -3,8 +3,8 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { ApiAnnuaireNotFoundError } from "../src/config/errors"; import { getAnnuaireEducationNationaleContactEmail } from "../src/connectors/api-annuaire-education-nationale"; -import noResult from "./api-annuaire-education-nationale-data/no-result.json"; -import twoEtablissementsData from "./api-annuaire-education-nationale-data/two-etablissements.json"; +// import noResult from "./api-annuaire-education-nationale-data/no-result.json"; +// import twoEtablissementsData from "./api-annuaire-education-nationale-data/two-etablissements.json"; describe("getAnnuaireEducationNationaleContactEmail", () => { it("should throw an error when no result is found", async () => { @@ -12,7 +12,7 @@ describe("getAnnuaireEducationNationaleContactEmail", () => { .get( "/api/v2/catalog/datasets/fr-en-annuaire-education/records?where=siren_siret%3D77672253000024", ) - .reply(200, noResult); + .reply(200, {}); await assert.rejects( getAnnuaireEducationNationaleContactEmail("77672253000024"), ApiAnnuaireNotFoundError, @@ -23,7 +23,7 @@ describe("getAnnuaireEducationNationaleContactEmail", () => { .get( "/api/v2/catalog/datasets/fr-en-annuaire-education/records?where=siren_siret%3D77672253000040", ) - .reply(200, twoEtablissementsData); + .reply(200, {}); assert.equal( await getAnnuaireEducationNationaleContactEmail("77672253000040"), "jeannedarc.millau@gmail.com", diff --git a/test/api-annuaire-service-public-data/invalid-cog.json b/test/api-annuaire-service-public-data/invalid-cog.json deleted file mode 100644 index 4aae00ec6..000000000 --- a/test/api-annuaire-service-public-data/invalid-cog.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "FeatureCollection", - "results": [] -} diff --git a/test/api-annuaire-service-public-data/one-mairie.json b/test/api-annuaire-service-public-data/one-mairie.json deleted file mode 100644 index 663573099..000000000 --- a/test/api-annuaire-service-public-data/one-mairie.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "total_count": 1, - "results": [ - { - "plage_ouverture": [ - { - "nom_jour_debut": "Lundi", - "nom_jour_fin": "Lundi", - "valeur_heure_debut_1": "09:00:00", - "valeur_heure_fin_1": "17:00:00", - "valeur_heure_debut_2": "", - "valeur_heure_fin_2": "", - "commentaire": "Uniquement pour l'accueil et l'état civil. Accueil fermé en juillet et août." - }, - { - "nom_jour_debut": "Mardi", - "nom_jour_fin": "Vendredi", - "valeur_heure_debut_1": "09:00:00", - "valeur_heure_fin_1": "12:00:00", - "valeur_heure_debut_2": "13:30:00", - "valeur_heure_fin_2": "17:00:00", - "commentaire": "" - }, - { - "nom_jour_debut": "Samedi", - "nom_jour_fin": "Samedi", - "valeur_heure_debut_1": "09:00:00", - "valeur_heure_fin_1": "12:00:00", - "valeur_heure_debut_2": "", - "valeur_heure_fin_2": "", - "commentaire": "Uniquement pour l'accueil, l'état civil et le stationnement. Accueil fermé en juillet et août." - } - ], - "site_internet": [ - { - "libelle": "", - "valeur": "https://www.aurillac.fr/" - } - ], - "copyright": "Direction de l'information légale et administrative (Premier ministre)", - "siren": "211500145", - "ancien_code_pivot": "mairie-15014-01", - "reseau_social": [ - { - "valeur": "https://www.facebook.com/mairiedaurillac", - "description": "", - "custom_dico2": "Facebook" - }, - { - "valeur": "https://www.instagram.com/mairiedaurillac/", - "description": "", - "custom_dico2": "Instagram" - }, - { - "valeur": "https://www.youtube.com/user/villeaurillac15", - "description": "", - "custom_dico2": "YouTube" - } - ], - "texte_reference": null, - "partenaire": null, - "telecopie": "04 71 45 46 90", - "nom": "Mairie - Aurillac", - "siret": "21150014500019", - "itm_identifiant": "317955", - "sigle": null, - "affectation_personne": null, - "date_modification": "06/06/2024 15:25:42", - "adresse_courriel": "administration@aurillac.fr", - "service_disponible": null, - "organigramme": null, - "pivot": [ - { - "type_service_local": "mairie", - "code_insee_commune": ["15014"] - } - ], - "partenaire_identifiant": null, - "ancien_identifiant": null, - "id": "36cb4285-18ee-45f8-b781-2bfe3b352986", - "ancien_nom": null, - "commentaire_plage_ouverture": "Uniquement pour l'accueil et l'état civil. Accueil fermé en juillet et août.", - "annuaire": null, - "tchat": null, - "hierarchie": null, - "categorie": "SL", - "sve": null, - "telephone_accessible": [ - { - "valeur": "https://www.acce-o.fr/client/aurillac", - "description": "Du lundi au vendredi de 9h à 17h et le samedi de 9h à 12h. Fermé lundi et samedi en Juillet et Août.", - "custom_dico1": [] - } - ], - "application_mobile": null, - "version_type": "Publiable", - "type_repertoire": null, - "telephone": [ - { - "valeur": "04 71 45 46 46", - "description": "" - } - ], - "version_etat_modification": null, - "date_creation": "18/01/2012 00:00:00", - "partenaire_date_modification": null, - "mission": null, - "formulaire_contact": "https://www.aurillac.fr/contact/", - "version_source": null, - "type_organisme": null, - "code_insee_commune": "15014", - "statut_de_diffusion": "true", - "adresse": "[{\"type_adresse\":\"Adresse\",\"complement1\":\"\",\"complement2\":\"\",\"numero_voie\":\"Place de l'Hôtel-de-Ville\",\"service_distribution\":\"\",\"code_postal\":\"15000\",\"nom_commune\":\"Aurillac\",\"pays\":\"France\",\"continent\":\"Europe\",\"longitude\":\"2.444953\",\"latitude\":\"44.930317\",\"accessibilite\":\"ACC\",\"note_accessibilite\":\"ascenseurs\"},{\"type_adresse\":\"Adresse postale\",\"complement1\":\"\",\"complement2\":\"Hôtel de Ville\",\"numero_voie\":\"Place de l'Hôtel-de-Ville\",\"service_distribution\":\"BP 509\",\"code_postal\":\"15005\",\"nom_commune\":\"Aurillac Cedex\",\"pays\":\"France\",\"continent\":\"Europe\",\"longitude\":\"\",\"latitude\":\"\",\"accessibilite\":\"\",\"note_accessibilite\":\"\"}]", - "url_service_public": "https://lannuaire.service-public.fr/auvergne-rhone-alpes/cantal/36cb4285-18ee-45f8-b781-2bfe3b352986", - "information_complementaire": null, - "date_diffusion": null - } - ] -} diff --git a/test/api-annuaire-service-public-data/two-mairies.json b/test/api-annuaire-service-public-data/two-mairies.json deleted file mode 100644 index 8ef891bca..000000000 --- a/test/api-annuaire-service-public-data/two-mairies.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "total_count": 2, - "results": [ - { - "plage_ouverture": [ - { - "nom_jour_debut": "Lundi", - "nom_jour_fin": "Vendredi", - "valeur_heure_debut_1": "08:30:00", - "valeur_heure_fin_1": "12:00:00", - "valeur_heure_debut_2": "14:00:00", - "valeur_heure_fin_2": "17:00:00", - "commentaire": "" - } - ], - "site_internet": [ - { - "libelle": "", - "valeur": "https://www.mairie2alpes.fr/" - } - ], - "copyright": "Direction de l'information légale et administrative (Premier ministre)", - "siren": "200064434", - "ancien_code_pivot": "mairie-38253-01", - "reseau_social": [ - { - "valeur": "https://www.facebook.com/communelesdeuxalpes", - "description": "", - "custom_dico2": "Facebook" - } - ], - "texte_reference": null, - "partenaire": null, - "telecopie": "04 76 79 57 09", - "nom": "Mairie - Les Deux Alpes", - "siret": "20006443400018", - "itm_identifiant": "406661", - "sigle": null, - "affectation_personne": null, - "date_modification": "28/02/2024 13:28:03", - "adresse_courriel": "accueil@mairie2alpes.fr", - "service_disponible": null, - "organigramme": null, - "pivot": [ - { - "type_service_local": "mairie", - "code_insee_commune": ["38253"] - } - ], - "partenaire_identifiant": null, - "ancien_identifiant": null, - "id": "b6947613-6ec3-4986-b27a-4137ae96425c", - "ancien_nom": null, - "commentaire_plage_ouverture": null, - "annuaire": null, - "tchat": null, - "hierarchie": null, - "categorie": "SL", - "sve": null, - "telephone_accessible": null, - "application_mobile": null, - "version_type": "Publiable", - "type_repertoire": null, - "telephone": [ - { - "valeur": "04 76 79 24 24", - "description": "" - } - ], - "version_etat_modification": null, - "date_creation": "18/01/2012 00:00:00", - "partenaire_date_modification": null, - "mission": null, - "formulaire_contact": null, - "version_source": null, - "type_organisme": null, - "code_insee_commune": "38253", - "statut_de_diffusion": "true", - "adresse": "[{\"type_adresse\":\"Adresse\",\"complement1\":\"\",\"complement2\":\"\",\"numero_voie\":\"48 avenue de la Muzelle\",\"service_distribution\":\"BP 12\",\"code_postal\":\"38860\",\"nom_commune\":\"Les Deux Alpes\",\"pays\":\"France\",\"continent\":\"Europe\",\"longitude\":\"6.12974023819\",\"latitude\":\"45.0360565186\",\"accessibilite\":\"ACC\",\"note_accessibilite\":\"rampe, ascenseur\"}]", - "url_service_public": "https://lannuaire.service-public.fr/auvergne-rhone-alpes/isere/b6947613-6ec3-4986-b27a-4137ae96425c", - "information_complementaire": null, - "date_diffusion": null - }, - { - "plage_ouverture": [ - { - "nom_jour_debut": "Lundi", - "nom_jour_fin": "Vendredi", - "valeur_heure_debut_1": "08:30:00", - "valeur_heure_fin_1": "12:00:00", - "valeur_heure_debut_2": "14:00:00", - "valeur_heure_fin_2": "16:30:00", - "commentaire": "" - } - ], - "site_internet": [ - { - "libelle": "", - "valeur": "https://www.mairie2alpes.fr/" - } - ], - "copyright": "Direction de l'information légale et administrative (Premier ministre)", - "siren": "200064434", - "ancien_code_pivot": "mairie-38253-02", - "reseau_social": [ - { - "valeur": "https://www.facebook.com/communelesdeuxalpes", - "description": "", - "custom_dico2": "Facebook" - } - ], - "texte_reference": null, - "partenaire": null, - "telecopie": null, - "nom": "Mairie déléguée - Venosc", - "siret": "20006443400018", - "itm_identifiant": "315979", - "sigle": null, - "affectation_personne": null, - "date_modification": "28/02/2024 15:50:02", - "adresse_courriel": "venosc@mairie2alpes.fr", - "service_disponible": null, - "organigramme": null, - "pivot": [ - { - "type_service_local": "mairie", - "code_insee_commune": ["38253"] - } - ], - "partenaire_identifiant": null, - "ancien_identifiant": null, - "id": "c93cb8a9-7535-44d6-bf28-36cc3f5fbc74", - "ancien_nom": null, - "commentaire_plage_ouverture": null, - "annuaire": null, - "tchat": null, - "hierarchie": null, - "categorie": "SL", - "sve": null, - "telephone_accessible": null, - "application_mobile": null, - "version_type": "Publiable", - "type_repertoire": null, - "telephone": [ - { - "valeur": "04 76 80 06 75", - "description": "" - } - ], - "version_etat_modification": null, - "date_creation": "18/01/2012 00:00:00", - "partenaire_date_modification": null, - "mission": null, - "formulaire_contact": null, - "version_source": null, - "type_organisme": null, - "code_insee_commune": "38253", - "statut_de_diffusion": "true", - "adresse": "[{\"type_adresse\":\"Adresse\",\"complement1\":\"Venosc\",\"complement2\":\"\",\"numero_voie\":\"5 rue du Câble\",\"service_distribution\":\"\",\"code_postal\":\"38520\",\"nom_commune\":\"Les Deux Alpes\",\"pays\":\"France\",\"continent\":\"Europe\",\"longitude\":\"6.116463\",\"latitude\":\"44.989936\",\"accessibilite\":\"ACC\",\"note_accessibilite\":\"\"}]", - "url_service_public": "https://lannuaire.service-public.fr/auvergne-rhone-alpes/isere/c93cb8a9-7535-44d6-bf28-36cc3f5fbc74", - "information_complementaire": null, - "date_diffusion": null - } - ] -} diff --git a/test/api-annuaire-service-public.test.ts b/test/api-annuaire-service-public.test.ts deleted file mode 100644 index 998cdcb07..000000000 --- a/test/api-annuaire-service-public.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import nock from "nock"; -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import { ApiAnnuaireNotFoundError } from "../src/config/errors"; -import { getAnnuaireServicePublicContactEmail } from "../src/connectors/api-annuaire-service-public"; -import invalidCogData from "./api-annuaire-service-public-data/invalid-cog.json"; -import oneMairieData from "./api-annuaire-service-public-data/one-mairie.json"; -import twoMairiesData from "./api-annuaire-service-public-data/two-mairies.json"; - -describe("getAnnuaireServicePublicContactEmail", () => { - it("should throw an error for invalid cog", async () => { - nock("https://api-lannuaire.service-public.fr") - .get( - `/api/explore/v2.1/catalog/datasets/api-lannuaire-administration/records?where=code_insee_commune LIKE "00000" and pivot LIKE "mairie"`, - ) - .reply(200, invalidCogData); - await assert.rejects( - getAnnuaireServicePublicContactEmail("00000", "00000"), - ApiAnnuaireNotFoundError, - ); - }); - it("should return a valid email", async () => { - nock("https://api-lannuaire.service-public.fr") - .get( - `/api/explore/v2.1/catalog/datasets/api-lannuaire-administration/records?where=code_insee_commune LIKE "15014" and pivot LIKE "mairie"`, - ) - .reply(200, oneMairieData); - assert.equal( - await getAnnuaireServicePublicContactEmail("15014", "15000"), - "administration@aurillac.fr", - ); - }); - it("should return valid email for two mairies with the same Code Officiel Geographique", async () => { - nock("https://api-lannuaire.service-public.fr") - .get( - `/api/explore/v2.1/catalog/datasets/api-lannuaire-administration/records?where=code_insee_commune LIKE "38253" and pivot LIKE "mairie"`, - ) - .reply(200, twoMairiesData); - assert.equal( - await getAnnuaireServicePublicContactEmail("38253", "38860"), - "accueil@mairie2alpes.fr", - ); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 9e7efac96..fbe5e3192 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,6 @@ "module": "Preserve", "moduleResolution": "Bundler", "outDir": "./build", - "resolveJsonModule": true, "types": ["cypress"] }, "exclude": ["packages/*", "build/*", "dist/*"],