diff --git a/.gitignore b/.gitignore index 606515f..8fd8b6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Temp folder +tmp + # Logs logs *.log @@ -111,3 +114,5 @@ dist # Jest jest_html_reporters.html reports + +config/local*.json diff --git a/README.md b/README.md index aa757fb..03f0855 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,47 @@ -# Map Colonies typescript service template - ----------------------------------- - -This is a basic repo template for building new MapColonies web services in Typescript. - -> [!IMPORTANT] -> To regenerate the types on openapi change run the command `npm run generate:openapi-types`. - -> [!WARNING] -> After creating a new repo based on this template, you should delete the CODEOWNERS file. +# dem-gateway +Gateway for DEM resources manipulation ## Development -When in development you should use the command `npm run start:dev`. The main benefits are that it enables offline mode for the config package, and source map support for NodeJS errors. - -### Template Features: - -- eslint configuration by [@map-colonies/eslint-config](https://github.com/MapColonies/eslint-config) - -- prettier configuration by [@map-colonies/prettier-config](https://github.com/MapColonies/prettier-config) - -- jest - -- .nvmrc - -- Multi stage production-ready Dockerfile - -- commitlint - -- git hooks - -- logging by [@map-colonies/js-logger](https://github.com/MapColonies/js-logger) - -- OpenAPI request validation - -- config load with [node-config](https://www.npmjs.com/package/node-config) - -- Tracing and metrics by [@map-colonies/telemetry](https://github.com/MapColonies/telemetry) -- github templates - -- bug report +When in development you should use the command `npm run start:dev`. The main benefits are that it enables offline mode for the config package, and source map support for NodeJS errors. -- feature request +### Adding a New Handler -- pull request +Before all, check if existing handlers can fulfill your need. For example, [`gdal handler`](src/info//fileHandlers/gdal.ts) can handle most of raster file formats. -- github actions +Add a new file handler under `src/info/fileHandlers`. The file should contain a class implementing the `FileHandler` interface. +Verify that OpenAPI spec supports the file format associated with the new handler. OpenAPI validates input file formats through a RegEx pattern. -- on pull_request +Add a new or record into the [default.json](config/default.json) under `application.supportedFormatsMap` in the form of: -- LGTM +```json +{ + ... + "application": { + ... + "supportedFormatsMap": { + ... + "formatName": "drivername" + }, + ... + } +} -- test +``` +This configuration is used to map common format names into a given file handler internal name. For example, the gdal handler maps formats into gdal's internal driver name. -- lint +Finally, since DI is utilized a new record for the new handler should be added in [containerConfig.ts](src/containerConfig.ts): -- snyk +```javascript +const dependencies: InjectionObject[] = [ + ... + { token: 'FileHandler', provider: { useClass: NewHandler } } +]; +``` ## API + Checkout the OpenAPI spec [here](/openapi3.yaml) ## Installation @@ -74,33 +57,25 @@ npm install Clone the project ```bash - git clone https://link-to-project - ``` Go to the project directory ```bash - cd my-project - ``` Install dependencies ```bash - npm install - ``` Start the server ```bash - npm run start - ``` ## Running Tests @@ -108,17 +83,17 @@ npm run start To run tests, run the following command ```bash - npm run test - ``` To only run unit tests: + ```bash npm run test:unit ``` To only run integration tests: + ```bash npm run test:integration ``` diff --git a/catalog-info.yaml b/catalog-info.yaml index 55a51de..a65b352 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -1,17 +1,17 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: ts-server-boilerplate - description: A boilerplate github repo for a REST API service in NodeJS for MapColonies + name: dem-gateway + description: Gateway for DEM resources manipulation annotations: - github.com/project-slug: MapColonies/ts-server-boilerplate + github.com/project-slug: MapColonies/dem-gateway tags: - nodejs - typescript - expressjs - - boilerplate + - dem spec: type: service lifecycle: production - owner: DevInfra - system: boilerplate + owner: DEM + system: dem-ingestion diff --git a/config/default.json b/config/default.json index d20b239..78baebe 100644 --- a/config/default.json +++ b/config/default.json @@ -12,8 +12,10 @@ }, "shared": {}, "logger": { - "level": "info", - "prettyPrint": false + "prettyPrint": false, + "opentelemetryOptions": { + "enabled": false + } } }, "server": { @@ -29,5 +31,33 @@ "options": null } } + }, + "storageExplorer": { + "sourceDir": "", + "displayNameDir": "layerSources", + "validFileExtensions": ["tif"] + }, + "jobnik": { + "baseUrl": "http://localhost:3000" + }, + "application": { + "validation": { + "blockSize": 256, + "compression": "LZW", + "resolutionDegree": { "min": 0.00000009060870470168063430786, "max": 0.08982 }, + "resolutionMeter": { "min": 0.01, "max": 10000 }, + "supportedSrsIds": [ + 4326, 32601, 32602, 32603, 32604, 32605, 32606, 32607, 32608, 32609, 32610, 32611, 32612, 32613, 32614, 32615, 32616, 32617, 32618, 32619, + 32620, 32621, 32622, 32623, 32624, 32625, 32626, 32627, 32628, 32629, 32630, 32631, 32632, 32633, 32634, 32635, 32636, 32637, 32638, 32639, + 32640, 32641, 32642, 32643, 32644, 32645, 32646, 32647, 32648, 32649, 32650, 32651, 32652, 32653, 32654, 32655, 32656, 32657, 32658, 32659, + 32660, 32701, 32702, 32703, 32704, 32705, 32706, 32707, 32708, 32709, 32710, 32711, 32712, 32713, 32714, 32715, 32716, 32717, 32718, 32719, + 32720, 32721, 32722, 32723, 32724, 32725, 32726, 32727, 32728, 32729, 32730, 32731, 32732, 32733, 32734, 32735, 32736, 32737, 32738, 32739, + 32740, 32741, 32742, 32743, 32744, 32745, 32746, 32747, 32748, 32749, 32750, 32751, 32752, 32753, 32754, 32755, 32756, 32757, 32758, 32759, + 32760 + ] + }, + "supportedFormatsMap": { + "geotiff": "gtiff" + } } } diff --git a/config/test.json b/config/test.json index 0967ef4..f362cfa 100644 --- a/config/test.json +++ b/config/test.json @@ -1 +1,5 @@ -{} +{ + "storageExplorer": { + "sourceDir": "/" + } +} diff --git a/helm/Chart.yaml b/helm/Chart.yaml index b1f2c0f..a3e9833 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: ts-server-boilerplate -description: A Helm chart for ts-server-boilerplate service +name: dem-gateway +description: A Helm chart for dem-gateway service type: application version: 1.0.0 appVersion: 1.0.0 diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index da7d776..6ea2118 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "ts-server-boilerplate.name" -}} +{{- define "dem-gateway.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} @@ -10,7 +10,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "ts-server-boilerplate.fullname" -}} +{{- define "dem-gateway.fullname" -}} {{- if .Values.fullnameOverride -}} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} {{- else -}} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "ts-server-boilerplate.chart" -}} +{{- define "dem-gateway.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "ts-server-boilerplate.labels" -}} -helm.sh/chart: {{ include "ts-server-boilerplate.chart" . }} -{{ include "ts-server-boilerplate.selectorLabels" . }} +{{- define "dem-gateway.labels" -}} +helm.sh/chart: {{ include "dem-gateway.chart" . }} +{{ include "dem-gateway.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -46,15 +46,15 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Returns the tag of the chart. */}} -{{- define "ts-server-boilerplate.tag" -}} +{{- define "dem-gateway.tag" -}} {{- default (printf "v%s" .Chart.AppVersion) .Values.image.tag }} {{- end }} {{/* Selector labels */}} -{{- define "ts-server-boilerplate.selectorLabels" -}} -app.kubernetes.io/name: {{ include "ts-server-boilerplate.name" . }} +{{- define "dem-gateway.selectorLabels" -}} +app.kubernetes.io/name: {{ include "dem-gateway.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{ include "mclabels.selectorLabels" . }} {{- end }} @@ -62,7 +62,7 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{/* Returns the cloud provider name from global if exists or from the chart's values, defaults to minikube */}} -{{- define "ts-server-boilerplate.cloudProviderFlavor" -}} +{{- define "dem-gateway.cloudProviderFlavor" -}} {{- if .Values.global.cloudProvider.flavor }} {{- .Values.global.cloudProvider.flavor -}} {{- else if .Values.cloudProvider -}} @@ -75,7 +75,7 @@ Returns the cloud provider name from global if exists or from the chart's values {{/* Returns the cloud provider docker registry url from global if exists or from the chart's values */}} -{{- define "ts-server-boilerplate.cloudProviderDockerRegistryUrl" -}} +{{- define "dem-gateway.cloudProviderDockerRegistryUrl" -}} {{- if .Values.global.cloudProvider.dockerRegistryUrl }} {{- printf "%s/" .Values.global.cloudProvider.dockerRegistryUrl -}} {{- else if .Values.cloudProvider.dockerRegistryUrl -}} @@ -87,7 +87,7 @@ Returns the cloud provider docker registry url from global if exists or from the {{/* Returns the cloud provider image pull secret name from global if exists or from the chart's values */}} -{{- define "ts-server-boilerplate.cloudProviderImagePullSecretName" -}} +{{- define "dem-gateway.cloudProviderImagePullSecretName" -}} {{- if .Values.global.cloudProvider.imagePullSecretName }} {{- .Values.global.cloudProvider.imagePullSecretName -}} {{- else if .Values.cloudProvider.imagePullSecretName -}} @@ -98,7 +98,7 @@ Returns the cloud provider image pull secret name from global if exists or from {{/* Returns the tracing url from global if exists or from the chart's values */}} -{{- define "ts-server-boilerplate.tracingUrl" -}} +{{- define "dem-gateway.tracingUrl" -}} {{- if .Values.global.tracing.url }} {{- .Values.global.tracing.url -}} {{- else if .Values.cloudProvider -}} @@ -109,7 +109,7 @@ Returns the tracing url from global if exists or from the chart's values {{/* Returns the tracing url from global if exists or from the chart's values */}} -{{- define "ts-server-boilerplate.metricsUrl" -}} +{{- define "dem-gateway.metricsUrl" -}} {{- if .Values.global.metrics.url }} {{- .Values.global.metrics.url -}} {{- else -}} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 1ccbbc5..59b2158 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -1,10 +1,10 @@ -{{- $tracingUrl := include "ts-server-boilerplate.tracingUrl" . -}} -{{- $metricsUrl := include "ts-server-boilerplate.metricsUrl" . -}} +{{- $tracingUrl := include "dem-gateway.tracingUrl" . -}} +{{- $metricsUrl := include "dem-gateway.metricsUrl" . -}} {{- if .Values.enabled -}} apiVersion: v1 kind: ConfigMap metadata: - name: {{ printf "%s-configmap" (include "ts-server-boilerplate.fullname" .) }} + name: {{ printf "%s-configmap" (include "dem-gateway.fullname" .) }} data: REQUEST_PAYLOAD_LIMIT: {{ .Values.env.requestPayloadLimit | quote }} RESPONSE_COMPRESSION_ENABLED: {{ .Values.env.responseCompressionEnabled | quote }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 276c464..9a01c6f 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -1,19 +1,19 @@ {{- $releaseName := .Release.Name -}} -{{- $chartName := include "ts-server-boilerplate.name" . -}} -{{- $cloudProviderFlavor := include "ts-server-boilerplate.cloudProviderFlavor" . -}} -{{- $cloudProviderDockerRegistryUrl := include "ts-server-boilerplate.cloudProviderDockerRegistryUrl" . -}} -{{- $cloudProviderImagePullSecretName := include "ts-server-boilerplate.cloudProviderImagePullSecretName" . -}} -{{- $imageTag := include "ts-server-boilerplate.tag" . -}} +{{- $chartName := include "dem-gateway.name" . -}} +{{- $cloudProviderFlavor := include "dem-gateway.cloudProviderFlavor" . -}} +{{- $cloudProviderDockerRegistryUrl := include "dem-gateway.cloudProviderDockerRegistryUrl" . -}} +{{- $cloudProviderImagePullSecretName := include "dem-gateway.cloudProviderImagePullSecretName" . -}} +{{- $imageTag := include "dem-gateway.tag" . -}} {{- if .Values.enabled -}} apiVersion: apps/v1 kind: Deployment metadata: - name: {{ printf "%s-deployment" (include "ts-server-boilerplate.fullname" .) }} + name: {{ printf "%s-deployment" (include "dem-gateway.fullname" .) }} labels: app: {{ $chartName }} component: {{ $chartName }} release: {{ $releaseName }} - {{- include "ts-server-boilerplate.labels" . | nindent 4 }} + {{- include "dem-gateway.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} @@ -22,14 +22,14 @@ spec: app: {{ $chartName }} release: {{ $releaseName }} run: {{ $releaseName }}-{{ $chartName }} - {{- include "ts-server-boilerplate.selectorLabels" . | nindent 6 }} + {{- include "dem-gateway.selectorLabels" . | nindent 6 }} template: metadata: labels: app: {{ $chartName }} release: {{ $releaseName }} run: {{ $releaseName }}-{{ $chartName }} - {{- include "ts-server-boilerplate.labels" . | nindent 8 }} + {{- include "dem-gateway.labels" . | nindent 8 }} annotations: {{ include "mclabels.annotations" . | nindent 8 }} {{- if .Values.resetOnConfigChange }} @@ -50,10 +50,10 @@ spec: imagePullPolicy: {{ .pullPolicy | default "IfNotPresent" }} {{- end }} {{- if .Values.command }} - command: + command: {{- toYaml .Values.command | nindent 12 }} {{- if .Values.args }} - args: + args: {{- toYaml .Values.args | nindent 12 }} {{- end }} {{- end }} @@ -67,20 +67,24 @@ spec: {{ toYaml .Values.extraVolumeMounts | nindent 12 }} {{- end }} env: + - name: K8S_POD_UID + valueFrom: + fieldRef: + fieldPath: metadata.uid - name: SERVER_PORT value: {{ .Values.env.targetPort | quote }} {{- if .Values.caSecretName }} - name: REQUESTS_CA_BUNDLE value: {{ printf "%s/%s" .Values.caPath .Values.caKey | quote }} - name: NODE_EXTRA_CA_CERTS - value: {{ printf "[%s/%s]" .Values.caPath .Values.caKey | quote }} + value: {{ printf "%s/%s" .Values.caPath .Values.caKey | quote }} {{- end }} {{- if .Values.extraEnvVars }} {{- toYaml .Values.extraEnvVars | nindent 12 }} - {{- end }} + {{- end }} envFrom: - configMapRef: - name: {{ printf "%s-configmap" (include "ts-server-boilerplate.fullname" .) }} + name: {{ printf "%s-configmap" (include "dem-gateway.fullname" .) }} ports: - name: http containerPort: {{ .Values.env.targetPort }} @@ -102,7 +106,7 @@ spec: httpGet: path: {{ .Values.readinessProbe.path }} port: {{ .Values.env.targetPort }} - {{- end }} + {{- end }} {{- if .Values.resources.enabled }} resources: {{- toYaml .Values.resources.value | nindent 12 }} @@ -113,7 +117,7 @@ spec: volumes: - name: nginx-config configMap: - name: 'nginx-extra-configmap' + name: 'nginx-extra-configmap' {{- if .Values.caSecretName }} - name: root-ca secret: @@ -121,5 +125,5 @@ spec: {{- end }} {{- if .Values.extraVolumes -}} {{ tpl (toYaml .Values.extraVolumes) . | nindent 8 }} - {{- end }} + {{- end }} {{- end -}} diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml index 94f92f8..86de0dc 100644 --- a/helm/templates/ingress.yaml +++ b/helm/templates/ingress.yaml @@ -1,5 +1,5 @@ {{- $releaseName := .Release.Name -}} -{{- $chartName := include "ts-server-boilerplate.name" . -}} +{{- $chartName := include "dem-gateway.name" . -}} {{- if and (.Values.enabled) (.Values.ingress.enabled) -}} apiVersion: networking.k8s.io/v1 kind: Ingress @@ -27,7 +27,7 @@ spec: pathType: Prefix backend: service: - name: {{ printf "%s-service" (include "ts-server-boilerplate.fullname" .) }} + name: {{ printf "%s-service" (include "dem-gateway.fullname" .) }} port: number: {{ .Values.env.port }} host: {{ .Values.ingress.host | quote }} diff --git a/helm/templates/route.yaml b/helm/templates/route.yaml index ab73b56..696b76c 100644 --- a/helm/templates/route.yaml +++ b/helm/templates/route.yaml @@ -1,6 +1,6 @@ {{- $releaseName := .Release.Name -}} -{{- $chartName := include "ts-server-boilerplate.name" . -}} -{{- $cloudProviderFlavor := include "ts-server-boilerplate.cloudProviderFlavor" . -}} +{{- $chartName := include "dem-gateway.name" . -}} +{{- $cloudProviderFlavor := include "dem-gateway.cloudProviderFlavor" . -}} {{- if and (and (.Values.enabled) (eq $cloudProviderFlavor "openshift")) (.Values.route.enabled) -}} apiVersion: route.openshift.io/v1 kind: Route @@ -20,7 +20,7 @@ spec: path: {{ .Values.route.path | default "/" }} to: kind: Service - name: {{ printf "%s-service" (include "ts-server-boilerplate.fullname" .) }} + name: {{ printf "%s-service" (include "dem-gateway.fullname" .) }} {{- if .Values.route.tls.enabled }} tls: termination: {{ .Values.route.tls.termination | quote }} diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml index be44a66..b62bbc8 100644 --- a/helm/templates/service.yaml +++ b/helm/templates/service.yaml @@ -1,16 +1,16 @@ {{- $releaseName := .Release.Name -}} -{{- $chartName := include "ts-server-boilerplate.name" . -}} -{{- $cloudProviderFlavor := include "ts-server-boilerplate.cloudProviderFlavor" . -}} +{{- $chartName := include "dem-gateway.name" . -}} +{{- $cloudProviderFlavor := include "dem-gateway.cloudProviderFlavor" . -}} {{- if .Values.enabled -}} apiVersion: v1 kind: Service metadata: - name: {{ printf "%s-service" (include "ts-server-boilerplate.fullname" .) }} + name: {{ printf "%s-service" (include "dem-gateway.fullname" .) }} labels: app: {{ $chartName }} component: {{ $chartName }} release: {{ $releaseName }} - {{- include "ts-server-boilerplate.labels" . | nindent 4 }} + {{- include "dem-gateway.labels" . | nindent 4 }} spec: {{- if eq $cloudProviderFlavor "minikube" }} type: NodePort @@ -27,5 +27,5 @@ spec: app: {{ $chartName }} release: {{ $releaseName }} run: {{ $releaseName }}-{{ $chartName }} - {{- include "ts-server-boilerplate.selectorLabels" . | nindent 4 }} + {{- include "dem-gateway.selectorLabels" . | nindent 4 }} {{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 11b81f6..749432e 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -5,8 +5,9 @@ global: mclabels: component: backend - partOf: boilerplates - owner: common + partOf: core + owner: dem + gisDomain: dem prometheus: enabled: true @@ -21,7 +22,7 @@ fullnameOverride: "" configManagement: offlineMode: false - name: 'service-name' + name: 'dem-gateway' version: 'latest' serverUrl: 'http://localhost:8080/api' @@ -64,7 +65,7 @@ caPath: '/usr/local/share/ca-certificates' caKey: 'ca.crt' image: - repository: ts-server-boilerplate + repository: dem-gateway # If commented, appVersion will be taken. See: _helpers.tpl # tag: 'latest' pullPolicy: IfNotPresent diff --git a/openapi3.yaml b/openapi3.yaml index 0adee2b..b5ced22 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -31,7 +31,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DemSuccessfulResponse' + $ref: '#/components/schemas/DemResponse' 400: description: Bad Request content: @@ -81,7 +81,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DemSuccessfulResponse' + $ref: '#/components/schemas/DemResponse' 400: description: Bad Request content: @@ -130,7 +130,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DemSuccessfulResponse' + $ref: '#/components/schemas/DemResponse' 400: description: Bad Request content: @@ -280,25 +280,17 @@ components: $ref: '#/components/schemas/DemMetadata' inputFiles: $ref: '#/components/schemas/InputFiles' - DemType: - type: string - description: DEM type - minLength: 0 GeoTiffDataType: type: string description: GeoTiff supported data types enum: - - Byte - - UInt16 + - Int8 - Int16 - - UInt32 - Int32 + - Int64 + - Float16 - Float32 - Float64 - - CInt16 - - CInt32 - - CFloat32 - - CFloat64 DemFilePath: type: string pattern: ^(\/?[\w-]+)(\/[\w-]+)*\/[\wא-ת\.-]+\.(tif)$ @@ -334,7 +326,7 @@ components: $ref: '#/components/schemas/ProducerName' productSubType: $ref: '#/components/schemas/ProductSubType' - DemSuccessfulResponse: + DemResponse: type: object unevaluatedProperties: false required: @@ -382,6 +374,8 @@ components: message: type: string minLength: 1 + stacktrace: + type: string GeoidModel: type: string description: Earth's geoid model @@ -391,15 +385,15 @@ components: description: Common properties of regular grids required: - areaOrPoint - - resolutionDegrees + - resolutionDegree - resolutionMeter - srsId - srsName properties: areaOrPoint: $ref: '#/components/schemas/AreaOrPoint' - resolutionDegrees: - $ref: '#/components/schemas/ResolutionDegrees' + resolutionDegree: + $ref: '#/components/schemas/ResolutionDegree' resolutionMeter: $ref: '#/components/schemas/ResolutionMeter' srsId: @@ -416,38 +410,26 @@ components: demFilePath: $ref: '#/components/schemas/DemFilePath' InfoResponse: - type: object description: Info response body - unevaluatedProperties: false - required: - - demType - discriminator: - propertyName: demType - oneOf: - - allOf: - - $ref: '#/components/schemas/InfoGeoTiff' + $ref: '#/components/schemas/InfoGeoTiff' InfoGeoTiff: + type: object description: Info properties of GeoTiff + unevaluatedProperties: false allOf: - $ref: '#/components/schemas/InfoCommonRegularGridProperties' - type: object required: - - demType - dataType - noDataValue properties: - demType: - allOf: - - $ref: '#/components/schemas/DemType' - - type: string - enum: - - geotiff dataType: $ref: '#/components/schemas/GeoTiffDataType' noDataValue: $ref: '#/components/schemas/NoDataValue' InputFiles: type: object + description: Input files unevaluatedProperties: false required: - demFilePath @@ -474,8 +456,11 @@ components: description: Metadata shape file path example: /path/to/ShapeMetadata.shp NoDataValue: - type: number - description: No data value of DEM + oneOf: + - type: number + description: No data value of DEM + - type: string + const: NaN Status: type: string default: UNPUBLISHED @@ -521,7 +506,7 @@ components: minLength: 1 minItems: 1 description: List of layer's regions - ResolutionDegrees: + ResolutionDegree: type: number description: DEM resolution in degrees exclusiveMinimum: 0 diff --git a/package-lock.json b/package-lock.json index 6224714..2204de1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,56 +1,69 @@ { - "name": "service-name", + "name": "dem-gateway", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "service-name", + "name": "dem-gateway", "version": "1.0.0", "license": "ISC", "dependencies": { "@godaddy/terminus": "^4.12.1", "@map-colonies/config": "^4.0.1", "@map-colonies/error-express-handler": "^4.0.0", - "@map-colonies/express-access-log-middleware": "^4.0.0", - "@map-colonies/js-logger": "^4.0.0", + "@map-colonies/error-types": "^1.3.1", + "@map-colonies/express-access-log-middleware": "^4.1.0", + "@map-colonies/js-logger": "^5.0.0", "@map-colonies/openapi-express-viewer": "^5.0.0", "@map-colonies/prometheus": "^1.0.0", "@map-colonies/read-pkg": "^1.0.0", - "@map-colonies/schemas": "^1.17.0", + "@map-colonies/schemas": "https://ghatmpstorage.blob.core.windows.net/npm-packages/schemas-ba913c633a12f913c8c68774d46036fa5c2ab978.tgz", "@map-colonies/tracing": "^1.0.0", "@map-colonies/tracing-utils": "^1.0.0", "@opentelemetry/api": "^1.9.0", "compression": "^1.8.0", + "epsg-index": "^2.0.0", "express": "^4.21.2", "express-openapi-validator": "^5.6.2", + "gdal-async": "^3.12.2", + "geographiclib-geodesic": "^2.2.0", "http-status-codes": "^2.3.0", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", - "tsyringe": "^4.8.0" + "tsyringe": "^4.8.0", + "zod": "^4.3.6" }, "devDependencies": { "@commitlint/cli": "^20.4.1", + "@faker-js/faker": "^10.1.0", "@map-colonies/commitlint-config": "^2.0.0", "@map-colonies/eslint-config": "^7.2.0", "@map-colonies/openapi-helpers": "^5.1.0", "@map-colonies/prettier-config": "^1.0.0", "@map-colonies/tsconfig": "^2.0.0", + "@readme/openapi-parser": "^5.5.0", "@redocly/cli": "^2.16.0", "@types/compression": "^1.7.5", "@types/express": "^4.17.21", + "@types/lodash": "^4.17.23", "@types/multer": "^1.4.12", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.8", "@vitest/coverage-v8": "^4.0.18", "@vitest/eslint-plugin": "^1.6.9", "@vitest/ui": "^4.0.18", + "ajv": "^8.18.0", + "ajv-formats": "^3.0.1", "copyfiles": "^2.4.1", "cross-env": "^10.1.0", "eslint": "^9.39.2", "eslint-plugin-jest": "^28.11.0", "husky": "^9.1.7", + "jest-extended": "^7.0.0", "jest-openapi": "^0.14.2", + "lodash": "^4.17.23", + "openapi-typescript": "^7.13.0", "prettier": "^3.8.1", "pretty-quick": "^4.2.2", "rimraf": "^6.1.2", @@ -58,7 +71,8 @@ "ts-jest": "^29.2.6", "tsc-alias": "^1.8.11", "typescript": "^5.9.3", - "vitest": "^4.0.18" + "vitest": "^4.0.18", + "zod-schema-faker": "^2.1.0" } }, "node_modules/@ampproject/remapping": { @@ -639,30 +653,6 @@ "node": ">=v18" } }, - "node_modules/@commitlint/config-validator/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@commitlint/ensure": { "version": "20.4.1", "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.4.1.tgz", @@ -1550,6 +1540,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -1563,6 +1570,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/js": { "version": "9.39.2", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", @@ -1608,14 +1622,32 @@ "license": "MIT" }, "node_modules/@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz", + "integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], "license": "MIT", "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.2.tgz", + "integrity": "sha512-Lm/ZLhDZcBECta3TmCQSngiQykFdfw+QtI1/GYMsZd4l3nG+P8WLB16XuS7WaBGLQ+9E+cOcWQsth9cayuGt8g==", + "license": "MIT", + "dependencies": { + "retry": "^0.13.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/@godaddy/terminus": { @@ -1765,6 +1797,18 @@ "node": ">=18" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1885,6 +1929,16 @@ } } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -1942,6 +1996,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/globals": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", @@ -2238,43 +2302,6 @@ "type-fest": "^2.3.2" } }, - "node_modules/@map-colonies/config/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@map-colonies/config/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@map-colonies/config/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "node_modules/@map-colonies/config/node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -2307,6 +2334,26 @@ "node": ">=24" } }, + "node_modules/@map-colonies/error-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@map-colonies/error-types/-/error-types-1.3.1.tgz", + "integrity": "sha512-ZcXiCYcjk4SBhAxO6JGJZ9cmiCInBULpisrnTViPsdxtfk+1a6XG/sKXop5U5se6xQZ77L43ZEUhiwvE7FsaPA==", + "license": "ISC", + "dependencies": { + "@map-colonies/error-express-handler": "^2.0.0", + "express": "^4.17.1", + "http-status-codes": "^2.1.4" + } + }, + "node_modules/@map-colonies/error-types/node_modules/@map-colonies/error-express-handler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@map-colonies/error-express-handler/-/error-express-handler-2.1.0.tgz", + "integrity": "sha512-8qcyePq5JVrbEw7rioZ7nQfYavVw8OiFGwfAJolDmq045ppm82IEKBFMgzLC4p4dbRj+wDzwcuRkcv5yGE0IZA==", + "license": "ISC", + "dependencies": { + "http-status-codes": "^2.1.4" + } + }, "node_modules/@map-colonies/eslint-config": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@map-colonies/eslint-config/-/eslint-config-7.2.0.tgz", @@ -2366,9 +2413,9 @@ } }, "node_modules/@map-colonies/express-access-log-middleware": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@map-colonies/express-access-log-middleware/-/express-access-log-middleware-4.0.0.tgz", - "integrity": "sha512-29tnRz5JGRGOMGFjzkqAOuV3rpjIAXkKT7hdHLejFO3vLfDNulpJk1ST0D/uMtX03kOM6/jhscYAvdbVAtKGYQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@map-colonies/express-access-log-middleware/-/express-access-log-middleware-4.1.0.tgz", + "integrity": "sha512-qtuKfNOE9WmEkLD175SI26xO+X8mloxlTE8JrR/gypaqIa8+XdGYNynDKrQK+83t5vMfToMLXhTzmGtT8HgXAg==", "dependencies": { "@opentelemetry/semantic-conventions": "^1.38.0", "http-status-codes": "^2.3.0", @@ -2388,11 +2435,14 @@ } }, "node_modules/@map-colonies/js-logger": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@map-colonies/js-logger/-/js-logger-4.0.0.tgz", - "integrity": "sha512-MjnHPXb5rWYZ7GAqkxn8X/y3AovazTCng5uayq1YoGb90dse5zjt5yfLqULd2WNNmT74ytRM7wlSheD9VjFkKA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@map-colonies/js-logger/-/js-logger-5.0.0.tgz", + "integrity": "sha512-kPEJwJo+bzZprq6Q91+ozJQpP2sPvgjfUF2BMxuXQXXmfY+USU2XcZJdfU4dowV5E275XhFTG9nQT2gJZvdQbg==", "dependencies": { - "@map-colonies/read-pkg": "^1.0.0", + "@map-colonies/read-pkg": "^2.0.0", + "@opentelemetry/resource-detector-container": "^0.8.4", + "@opentelemetry/resources": "^2.6.0", + "@opentelemetry/semantic-conventions": "^1.40.0", "pino": "^10.1.0", "pino-caller": "^4.0.0", "pino-opentelemetry-transport": "^2.0.0", @@ -2402,6 +2452,41 @@ "node": ">=24" } }, + "node_modules/@map-colonies/js-logger/node_modules/@map-colonies/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@map-colonies/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-rMASq2JeRuaWYGqPN68+FwQa4ZSVJBFpttRypUX6RflZK0svZNNCQSWleqz9iJGw5oVDRILNpAb4HRHm/7rszA==", + "dependencies": { + "type-fest": "^5.0.0" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@map-colonies/js-logger/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@map-colonies/js-logger/node_modules/type-fest": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@map-colonies/openapi-express-viewer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@map-colonies/openapi-express-viewer/-/openapi-express-viewer-5.0.0.tgz", @@ -2609,9 +2694,9 @@ } }, "node_modules/@map-colonies/schemas": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@map-colonies/schemas/-/schemas-1.17.0.tgz", - "integrity": "sha512-lfp27EkpXM2zlHDHtVNkjScbRZDjcn4HHfYtl8tD2lOPMPoR55wTRBsh5OhUllbLbv7O2+youA1w3Ny+iUlW9w==", + "version": "1.18.0", + "resolved": "https://ghatmpstorage.blob.core.windows.net/npm-packages/schemas-ba913c633a12f913c8c68774d46036fa5c2ab978.tgz", + "integrity": "sha512-YwTz15mf+Kp0co+UgHzMnQ0SOmoKWLU+u8DpcvOuhy5Yf6l7028K3d+BsJfN035XDjzqTDQ/BmpWgHkFuAjVRQ==", "license": "MIT", "peer": true }, @@ -2766,22 +2851,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@map-colonies/tracing/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@map-colonies/tracing/node_modules/@opentelemetry/exporter-logs-otlp-grpc": { "version": "0.208.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-grpc/-/exporter-logs-otlp-grpc-0.208.0.tgz", @@ -4300,22 +4369,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@map-colonies/tracing/node_modules/@opentelemetry/resource-detector-container": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.8.2.tgz", - "integrity": "sha512-8oT0tUO+QS8Tz7u0YQZKoZOpS+LIgS4FnLjWSCPyXPOgKuOeOK5Xe0sd0ulkAGPN4yKr7toNYNVkBeaC/HlmFQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^2.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, "node_modules/@map-colonies/tracing/node_modules/@opentelemetry/resource-detector-gcp": { "version": "0.44.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.44.0.tgz", @@ -4333,22 +4386,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@map-colonies/tracing/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@map-colonies/tracing/node_modules/@opentelemetry/sdk-logs": { "version": "0.208.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz", @@ -4687,40 +4724,6 @@ "pg-types": "^2.2.0" } }, - "node_modules/@map-colonies/tracing/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@map-colonies/tracing/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@map-colonies/tracing/node_modules/cjs-module-lexer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", @@ -4739,12 +4742,6 @@ "module-details-from-path": "^1.0.4" } }, - "node_modules/@map-colonies/tracing/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/@map-colonies/tracing/node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -4844,36 +4841,128 @@ "node": ">= 8" } }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "peer": true, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, "engines": { - "node": ">=8.0.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@opentelemetry/instrumentation-openai": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-openai/-/instrumentation-openai-0.7.1.tgz", - "integrity": "sha512-QDnnAYxByJoJ3jMly/EwRbXhnfZpGigfBcHyPcgWEMR4bfawJZhdOdFi1GVcC4ImdS7fGaYQOTX1WW24mftISg==", - "license": "Apache-2.0", + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "license": "ISC", "dependencies": { - "@opentelemetry/api-logs": "^0.208.0", - "@opentelemetry/instrumentation": "^0.208.0", - "@opentelemetry/semantic-conventions": "^1.36.0" + "semver": "^7.3.5" }, "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@opentelemetry/instrumentation-openai/node_modules/@opentelemetry/api-logs": { - "version": "0.208.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", + "node_modules/@oozcitak/dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", + "license": "MIT", + "dependencies": { + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", + "license": "MIT", + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", + "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation-openai": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-openai/-/instrumentation-openai-0.7.1.tgz", + "integrity": "sha512-QDnnAYxByJoJ3jMly/EwRbXhnfZpGigfBcHyPcgWEMR4bfawJZhdOdFi1GVcC4ImdS7fGaYQOTX1WW24mftISg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.208.0", + "@opentelemetry/instrumentation": "^0.208.0", + "@opentelemetry/semantic-conventions": "^1.36.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-openai/node_modules/@opentelemetry/api-logs": { + "version": "0.208.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz", "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==", "license": "Apache-2.0", "dependencies": { @@ -5092,6 +5181,38 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, + "node_modules/@opentelemetry/resource-detector-container": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.8.4.tgz", + "integrity": "sha512-kIvGHkMSacp+kb7btTuXbOAIWLyOCO+P/h/8xxaeLcp5ptmHRZ67uEdLAQo61ApdayFB/uqjJ9gY4x2/i/KsoA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/resources": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", + "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/semantic-conventions": { "version": "1.34.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.34.0.tgz", @@ -5111,6 +5232,12 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "license": "MIT" + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -5269,24 +5396,6 @@ "@types/json-schema": "^7.0.15" } }, - "node_modules/@readme/openapi-parser/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@readme/openapi-parser/node_modules/ajv-draft-04": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", @@ -5302,13 +5411,6 @@ } } }, - "node_modules/@readme/openapi-parser/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@readme/openapi-schemas": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@readme/openapi-schemas/-/openapi-schemas-3.1.0.tgz", @@ -5354,12 +5456,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@redocly/ajv/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/@redocly/cli": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/@redocly/cli/-/cli-2.16.0.tgz", @@ -5612,24 +5708,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@redocly/cli/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/@redocly/cli/node_modules/glob": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", @@ -5655,13 +5733,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@redocly/cli/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@redocly/cli/node_modules/minimatch": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", @@ -5711,9 +5782,9 @@ } }, "node_modules/@redocly/config": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.12.1.tgz", - "integrity": "sha512-RW3rSirfsPdr0uvATijRDU3f55SuZV3m7/ppdTDvGw4IB0cmeZRkFmqTrchxMqWP50Gfg1tpHnjdxUCNo0E2qg==", + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", "dev": true, "license": "MIT" }, @@ -5785,31 +5856,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@redocly/openapi-core/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@redocly/openapi-core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@redocly/openapi-core/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -5848,6 +5894,17 @@ "npm": ">=10" } }, + "node_modules/@redocly/respect-core/node_modules/@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, "node_modules/@redocly/respect-core/node_modules/ajv": { "name": "@redocly/ajv", "version": "8.17.1", @@ -5873,13 +5930,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@redocly/respect-core/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, "node_modules/@redocly/respect-core/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -6479,9 +6529,10 @@ "peer": true }, "node_modules/@types/lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" }, "node_modules/@types/memcached": { "version": "2.2.10", @@ -7590,6 +7641,15 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -7666,16 +7726,16 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -7683,10 +7743,10 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -7699,28 +7759,6 @@ } } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -7990,8 +8028,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/better-ajv-errors": { "version": "1.2.0", @@ -8079,7 +8116,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8178,37 +8214,112 @@ "node": ">= 0.8" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", + "node_modules/cacache": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", + "license": "ISC", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/cacache/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "18 || 20 || >=22" } }, - "node_modules/call-me-maybe": { - "version": "1.0.2", + "node_modules/cacache/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "dev": true, @@ -8336,6 +8447,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -8558,8 +8678,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/concat-stream": { "version": "2.0.0", @@ -9033,6 +9152,16 @@ "url": "https://dotenvx.com" } }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -9113,12 +9242,20 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/epsg-index": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/epsg-index/-/epsg-index-2.0.0.tgz", + "integrity": "sha512-JFRqtXMmxEO/BWbXZuLNJuWS44vbwDPQkfycnISnfm/I8/OLlAqwbYRImgAV9ulr63p/hyW2YIFT9jizHeDzvQ==", + "license": "ISC", + "engines": { + "node": ">=18" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -9488,6 +9625,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -9541,6 +9695,13 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9767,6 +9928,12 @@ "node": ">=12.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -9866,23 +10033,6 @@ "@types/express": "*" } }, - "node_modules/express-openapi-validator/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/express-openapi-validator/node_modules/ajv-draft-04": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", @@ -9896,28 +10046,6 @@ } } }, - "node_modules/express-openapi-validator/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/express-openapi-validator/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" - }, "node_modules/express-openapi-validator/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -10320,11 +10448,22 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.3", @@ -10350,34 +10489,268 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gdal-async": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/gdal-async/-/gdal-async-3.12.2.tgz", + "integrity": "sha512-R4XWgWEpiOx1AoxJzQsV1qF/TSR1KyAw8sdrs4SNNeulydwoFqK2+azbFWqg4u1t4q7pcPROFs4NsYyI6qesKw==", + "bundleDependencies": [ + "@mapbox/node-pre-gyp" + ], + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0", + "@petamoriken/float16": "^3.9.2", + "nan": "^2.23.0", + "node-gyp": "^12.1.0", + "xmlbuilder2": "^4.0.0", + "yatag": "^1.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/mmomtchev" + } + }, + "node_modules/gdal-async/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/gdal-async/node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.3", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gdal-async/node_modules/abbrev": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/gdal-async/node_modules/agent-base": { + "version": "7.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gdal-async/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/gdal-async/node_modules/consola": { + "version": "3.4.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/gdal-async/node_modules/debug": { + "version": "4.4.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/gdal-async/node_modules/detect-libc": { + "version": "2.0.4", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/gdal-async/node_modules/https-proxy-agent": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gdal-async/node_modules/minipass": { + "version": "7.1.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/gdal-async/node_modules/minizlib": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/gdal-async/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/gdal-async/node_modules/node-fetch": { + "version": "2.7.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gdal-async/node_modules/nopt": { + "version": "8.1.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/gdal-async/node_modules/semver": { + "version": "7.7.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gdal-async/node_modules/tar": { + "version": "7.5.2", + "inBundle": true, + "license": "BlueOak-1.0.0", "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, - "node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, + "node_modules/gdal-async/node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">=14" + "node": ">=18" + } + }, + "node_modules/gdal-async/node_modules/tr46": { + "version": "0.0.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/gdal-async/node_modules/webidl-conversions": { + "version": "3.0.1", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/gdal-async/node_modules/whatwg-url": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/gensync": { @@ -10389,6 +10762,12 @@ "node": ">=6.9.0" } }, + "node_modules/geographiclib-geodesic": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/geographiclib-geodesic/-/geographiclib-geodesic-2.2.0.tgz", + "integrity": "sha512-cIedo9VTYb0DFufodgibDmVfsWe9EASqb/kUByl09xc6PZYvLvlc89BHCThtGTPf2OII/zWJGxsR3Uz6O7QOVw==", + "license": "MIT" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -10516,7 +10895,6 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -10604,8 +10982,7 @@ "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/handlebars": { "version": "4.7.7", @@ -10690,6 +11067,12 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -10706,6 +11089,19 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-status-codes": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", @@ -10837,15 +11233,14 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } }, "node_modules/index-to-position": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz", - "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", "dev": true, "license": "MIT", "engines": { @@ -10859,7 +11254,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -10880,6 +11274,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -11424,6 +11827,102 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-extended": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-7.0.0.tgz", + "integrity": "sha512-96jBsVJDxZKFh+kWY7E18Is2usUsUYtBn97MxCtb4COnbgD4aE1h+P0fdFQNeJaI6KOeduas4Numc9yTuk0+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-diff": "^30.0.0" + }, + "engines": { + "node": "^20.9.0 || ^22.11.0 || ^24.11.0 || >=25.0.0" + }, + "peerDependencies": { + "jest": ">=27.2.5", + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + }, + "typescript": { + "optional": false + } + } + }, + "node_modules/jest-extended/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-extended/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-extended/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-extended/node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-extended/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-extended/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -11998,10 +12497,10 @@ } }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -12118,9 +12617,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -12137,12 +12637,6 @@ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "dev": true - }, "node_modules/lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -12230,7 +12724,6 @@ "version": "11.2.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -12286,6 +12779,37 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/make-fetch-happen": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.4.tgz", + "integrity": "sha512-vM2sG+wbVeVGYcCm16mM3d5fuem9oC28n436HjsGO3LcxoTI8LNVa4rwZDn3f76+cWyT4GGJDxjTYU1I2nr6zw==", + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -12448,7 +12972,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12466,15 +12989,144 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-fetch/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -12622,6 +13274,12 @@ "url": "https://github.com/sponsors/raouldeheer" } }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -12697,17 +13355,65 @@ } } }, - "node_modules/node-fetch-h2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", - "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", - "dev": true, - "license": "MIT", + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/node-gyp": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "license": "ISC", "dependencies": { - "http2-client": "^1.2.5" + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" }, "engines": { - "node": "4.x || >=6.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/node-int64": { @@ -12767,6 +13473,21 @@ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -12988,28 +13709,6 @@ "openapi-types": "^9.3.1" } }, - "node_modules/openapi-response-validator/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/openapi-response-validator/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/openapi-sampler": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.6.2.tgz", @@ -13034,28 +13733,24 @@ "openapi-types": "^9.3.1" } }, - "node_modules/openapi-schema-validator/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "node_modules/openapi-schema-validator/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "ajv": "^8.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/openapi-schema-validator/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/openapi-types": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-9.3.1.tgz", @@ -13064,17 +13759,18 @@ "peer": true }, "node_modules/openapi-typescript": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.4.1.tgz", - "integrity": "sha512-HrRoWveViADezHCNgQqZmPKmQ74q7nuH/yg9ursFucZaYQNUqsX38fE/V2sKBHVM+pws4tAHpuh/ext2UJ/AoQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@redocly/openapi-core": "^1.25.3", + "@redocly/openapi-core": "^1.34.6", "ansi-colors": "^4.1.3", "change-case": "^5.4.4", - "parse-json": "^8.1.0", - "supports-color": "^9.4.0", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", "yargs-parser": "^21.1.1" }, "bin": { @@ -13085,33 +13781,31 @@ } }, "node_modules/openapi-typescript/node_modules/@redocly/openapi-core": { - "version": "1.25.4", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.25.4.tgz", - "integrity": "sha512-qnpr4Z1rzfXdtxQxt/lfGD0wW3UVrm3qhrTpzLG5R/Ze+z+1u8sSRiQHp9N+RT3IuMjh00wq59nop9x9PPa1jQ==", + "version": "1.34.7", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.7.tgz", + "integrity": "sha512-gn2P0OER6qxF/+f4GqNv9XsnU5+6oszD/0SunulOvPYJDhrNkNVrVZV5waX25uqw5UDn2+roViWlRDHKFfHH0g==", "dev": true, "license": "MIT", "dependencies": { "@redocly/ajv": "^8.11.2", - "@redocly/config": "^0.12.1", + "@redocly/config": "^0.22.0", "colorette": "^1.2.0", - "https-proxy-agent": "^7.0.4", + "https-proxy-agent": "^7.0.5", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", - "lodash.isequal": "^4.5.0", "minimatch": "^5.0.1", - "node-fetch": "^2.6.1", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" }, "engines": { - "node": ">=14.19.0", - "npm": ">=7.0.0" + "node": ">=18.17.0", + "npm": ">=9.5.0" } }, "node_modules/openapi-typescript/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13132,15 +13826,15 @@ } }, "node_modules/openapi-typescript/node_modules/parse-json": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", - "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.13", - "index-to-position": "^0.1.2", - "type-fest": "^4.7.1" + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" }, "engines": { "node": ">=18" @@ -13150,13 +13844,13 @@ } }, "node_modules/openapi-typescript/node_modules/supports-color": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", - "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/supports-color?sponsor=1" @@ -13493,37 +14187,6 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/otlp-logger/node_modules/@opentelemetry/resources": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", - "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.5.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/otlp-logger/node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", - "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/otlp-logger/node_modules/@opentelemetry/sdk-logs": { "version": "0.206.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.206.0.tgz", @@ -13656,6 +14319,18 @@ "node": ">=8" } }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -13731,7 +14406,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -13762,17 +14436,16 @@ } }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "dev": true, + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -14230,6 +14903,15 @@ "node": ">=6" } }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -14338,6 +15020,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -14404,6 +15096,20 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -14558,13 +15264,6 @@ "styled-components": "^4.1.1 || ^5.1.1 || ^6.0.5" } }, - "node_modules/redoc/node_modules/@redocly/config": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", - "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", - "dev": true, - "license": "MIT" - }, "node_modules/redoc/node_modules/@redocly/openapi-core": { "version": "1.34.6", "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.6.tgz", @@ -14749,6 +15448,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -15272,6 +15990,44 @@ "node": ">=8.0.0" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sonic-boom": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", @@ -15323,6 +16079,18 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/stable-hash-x": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", @@ -15723,6 +16491,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tar": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -15775,7 +16559,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -15792,7 +16575,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -15810,7 +16592,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -16023,9 +16804,9 @@ } }, "node_modules/type-fest": { - "version": "4.37.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.37.0.tgz", - "integrity": "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==", + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" @@ -16136,6 +16917,30 @@ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, + "node_modules/unique-filename": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", + "license": "ISC", + "dependencies": { + "unique-slug": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/unique-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -16234,19 +17039,11 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/uri-js/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", @@ -16700,6 +17497,21 @@ } } }, + "node_modules/xmlbuilder2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -16716,6 +17528,15 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", @@ -16778,6 +17599,18 @@ "node": ">=12" } }, + "node_modules/yatag": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/yatag/-/yatag-1.3.0.tgz", + "integrity": "sha512-kkdqNFmWCWdz2FkGYnnkyhKlpJbd9zlZuMTtcKpDJZ3ZgfuX0beGhuZseI6npm120Ti5iMI+53JOK2EUgUVziw==", + "license": "ISC", + "dependencies": { + "glob": "^7.2.3" + }, + "bin": { + "yatag": "yatag.js" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -16802,6 +17635,30 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-schema-faker": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zod-schema-faker/-/zod-schema-faker-2.1.0.tgz", + "integrity": "sha512-k1WaldYU6nOXUa6Pup8HhuvqvAu2HWZUnxF9RSSi6GvQTTBkdXlvVN6cT4oRMUPxeoIHCBAr9DHKMK+H1r1zWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@faker-js/faker": "10.1.0", + "randexp": "0.5.3" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/package.json b/package.json index 42434bd..427fb97 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { - "name": "service-name", + "name": "dem-gateway", "version": "1.0.0", - "description": "This is template for map colonies typescript service", + "description": "Gateway for DEM resources manipulation", "main": "./src/index.ts", "scripts": { - "test:unit": "vitest run --coverage.enabled=false --project unit", - "test:integration": "vitest run --coverage.enabled=false --project integration", + "test:unit": "vitest run --coverage.enabled=true --project unit", + "test:integration": "vitest run --coverage.enabled=true --project integration", "test": "vitest run", "test:watch": "vitest watch", "test:ui": "vitest --ui", @@ -19,7 +19,7 @@ "prebuild": "npm run clean && npm run generate:openapi-types", "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json && npm run assets:copy", "start": "npm run build && cd dist && node --import ./instrumentation.mjs ./index.js", - "start:dev": "npm run build && cd dist && cross-env CONFIG_OFFLINE_MODE=true node --enable-source-maps --import ./instrumentation.mjs ./index.js", + "start:dev": "npm run build && cd dist && cross-env CONFIG_OFFLINE_MODE=true node --enable-source-maps --import ./instrumentation.mjs ./index.js", "assets:copy": "copyfiles -f ./config/* ./dist/config && copyfiles -f ./openapi3.yaml ./dist/ && copyfiles ./package.json dist", "clean": "rimraf dist", "generate:openapi-types": "openapi-helpers generate types ./openapi3.yaml ./src/openapi.d.ts --format --add-typed-request-handler", @@ -34,45 +34,58 @@ "@godaddy/terminus": "^4.12.1", "@map-colonies/config": "^4.0.1", "@map-colonies/error-express-handler": "^4.0.0", - "@map-colonies/express-access-log-middleware": "^4.0.0", - "@map-colonies/js-logger": "^4.0.0", + "@map-colonies/error-types": "^1.3.1", + "@map-colonies/express-access-log-middleware": "^4.1.0", + "@map-colonies/js-logger": "^5.0.0", "@map-colonies/openapi-express-viewer": "^5.0.0", "@map-colonies/prometheus": "^1.0.0", "@map-colonies/read-pkg": "^1.0.0", - "@map-colonies/schemas": "^1.17.0", + "@map-colonies/schemas": "https://ghatmpstorage.blob.core.windows.net/npm-packages/schemas-ba913c633a12f913c8c68774d46036fa5c2ab978.tgz", "@map-colonies/tracing": "^1.0.0", "@map-colonies/tracing-utils": "^1.0.0", "@opentelemetry/api": "^1.9.0", "compression": "^1.8.0", + "epsg-index": "^2.0.0", "express": "^4.21.2", "express-openapi-validator": "^5.6.2", + "gdal-async": "^3.12.2", + "geographiclib-geodesic": "^2.2.0", "http-status-codes": "^2.3.0", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", - "tsyringe": "^4.8.0" + "tsyringe": "^4.8.0", + "zod": "^4.3.6" }, "devDependencies": { "@commitlint/cli": "^20.4.1", + "@faker-js/faker": "^10.1.0", "@map-colonies/commitlint-config": "^2.0.0", "@map-colonies/eslint-config": "^7.2.0", "@map-colonies/openapi-helpers": "^5.1.0", "@map-colonies/prettier-config": "^1.0.0", "@map-colonies/tsconfig": "^2.0.0", + "@readme/openapi-parser": "^5.5.0", "@redocly/cli": "^2.16.0", "@types/compression": "^1.7.5", "@types/express": "^4.17.21", + "@types/lodash": "^4.17.23", "@types/multer": "^1.4.12", "@types/supertest": "^6.0.2", "@types/swagger-ui-express": "^4.1.8", "@vitest/coverage-v8": "^4.0.18", "@vitest/eslint-plugin": "^1.6.9", "@vitest/ui": "^4.0.18", + "ajv": "^8.18.0", + "ajv-formats": "^3.0.1", "copyfiles": "^2.4.1", "cross-env": "^10.1.0", "eslint": "^9.39.2", "eslint-plugin-jest": "^28.11.0", "husky": "^9.1.7", + "jest-extended": "^7.0.0", "jest-openapi": "^0.14.2", + "lodash": "^4.17.23", + "openapi-typescript": "^7.13.0", "prettier": "^3.8.1", "pretty-quick": "^4.2.2", "rimraf": "^6.1.2", @@ -80,6 +93,7 @@ "ts-jest": "^29.2.6", "tsc-alias": "^1.8.11", "typescript": "^5.9.3", - "vitest": "^4.0.18" + "vitest": "^4.0.18", + "zod-schema-faker": "^2.1.0" } } diff --git a/src/anotherResource/controllers/anotherResourceController.ts b/src/anotherResource/controllers/anotherResourceController.ts deleted file mode 100644 index 56f3879..0000000 --- a/src/anotherResource/controllers/anotherResourceController.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Logger } from '@map-colonies/js-logger'; -import { type Registry, Counter } from 'prom-client'; -import httpStatus from 'http-status-codes'; -import { injectable, inject } from 'tsyringe'; -import type { TypedRequestHandlers } from '@openapi'; -import { SERVICES } from '@common/constants'; -import { AnotherResourceManager } from '../models/anotherResourceManager'; - -@injectable() -export class AnotherResourceController { - private readonly getResourceCounter: Counter; - - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(AnotherResourceManager) private readonly manager: AnotherResourceManager, - @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry - ) { - this.getResourceCounter = new Counter({ - name: 'get_resource', - help: 'number of get resource requests', - registers: [this.metricsRegistry], - }); - } - - public getResource: TypedRequestHandlers['getAnotherResource'] = (req, res) => { - this.getResourceCounter.inc(1); - return res.status(httpStatus.OK).json(this.manager.getResource()); - }; -} diff --git a/src/anotherResource/models/anotherResourceManager.ts b/src/anotherResource/models/anotherResourceManager.ts deleted file mode 100644 index f54fcde..0000000 --- a/src/anotherResource/models/anotherResourceManager.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Logger } from '@map-colonies/js-logger'; -import { inject, injectable } from 'tsyringe'; -import { components } from '@src/openapi'; -import { SERVICES } from '@common/constants'; - -const resourceInstance: IAnotherResourceModel = { - kind: 'avi', - isAlive: false, -}; - -export type IAnotherResourceModel = components['schemas']['anotherResource']; - -@injectable() -export class AnotherResourceManager { - public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} - public getResource(): IAnotherResourceModel { - this.logger.info('logging'); - return resourceInstance; - } -} diff --git a/src/anotherResource/routes/anotherResourceRouter.ts b/src/anotherResource/routes/anotherResourceRouter.ts deleted file mode 100644 index 4ea0743..0000000 --- a/src/anotherResource/routes/anotherResourceRouter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Router } from 'express'; -import { FactoryFunction } from 'tsyringe'; -import { AnotherResourceController } from '../controllers/anotherResourceController'; - -const anotherResourceRouterFactory: FactoryFunction = (dependencyContainer) => { - const router = Router(); - const controller = dependencyContainer.resolve(AnotherResourceController); - - router.get('/', controller.getResource); - - return router; -}; - -export const ANOTHER_RESOURCE_ROUTER_SYMBOL = Symbol('anotherResourceRouterFactory'); - -export { anotherResourceRouterFactory }; diff --git a/src/common/config.ts b/src/common/config.ts index 7b1a941..5daa998 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,8 +1,8 @@ import { type ConfigInstance, config } from '@map-colonies/config'; -import { commonBoilerplateV2, type commonBoilerplateV2Type } from '@map-colonies/schemas'; +import { demDemGatewayV1, type demDemGatewayV1Type } from '@map-colonies/schemas'; // Choose here the type of the config instance and import this type from the entire application -type ConfigType = ConfigInstance; +type ConfigType = ConfigInstance; let configInstance: ConfigType | undefined; @@ -13,7 +13,7 @@ let configInstance: ConfigType | undefined; */ async function initConfig(offlineMode?: boolean): Promise { configInstance = await config({ - schema: commonBoilerplateV2, + schema: demDemGatewayV1, offlineMode, }); } diff --git a/src/common/constants.ts b/src/common/constants.ts index 6e7c100..1c9f54c 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -1,4 +1,13 @@ import { readPackageJsonSync } from '@map-colonies/read-pkg'; +import type { GeoTiffDataType, IsComplete, NoDuplicates } from './interfaces'; + +const defineConstTuple = + () => + ( + ...args: U & + (NoDuplicates extends false ? 'Error: Duplicate value found' : IsComplete extends false ? 'Error: Missing values from the union' : U) + ): U => + args; export const SERVICE_NAME = readPackageJsonSync().name ?? 'unknown_service'; export const DEFAULT_SERVER_PORT = 80; @@ -14,3 +23,8 @@ export const SERVICES = { METRICS: Symbol('METRICS'), } satisfies Record; /* eslint-enable @typescript-eslint/naming-convention */ + +export const GEOTIFF_DATA_TYPES = defineConstTuple()('Int8', 'Int16', 'Int32', 'Int64', 'Float16', 'Float32', 'Float64'); +export const RASTER_DATA_TYPES = { + geotiff: GEOTIFF_DATA_TYPES, +}; diff --git a/src/common/dependencyRegistration.ts b/src/common/dependencyRegistration.ts index a591ff9..97190d0 100644 --- a/src/common/dependencyRegistration.ts +++ b/src/common/dependencyRegistration.ts @@ -1,27 +1,45 @@ -import { ClassProvider, container as defaultContainer, FactoryProvider, InjectionToken, ValueProvider } from 'tsyringe'; -import { constructor, DependencyContainer } from 'tsyringe/dist/typings/types'; +import { + container as defaultContainer, + type ClassProvider, + type FactoryProvider, + type InjectionToken, + type ValueProvider, + type Provider, +} from 'tsyringe'; +import { type DependencyContainer, constructor } from 'tsyringe/dist/typings/types'; + +interface CreateAsyncProvider> { + useAsync: (dependencyContainer: DependencyContainer) => Promise; +} + +async function getProvider(injectionObj: InjectionObject, container: DependencyContainer): Promise> { + if ('useAsync' in injectionObj.provider) { + const provider = await injectionObj.provider.useAsync(container); + return provider; + } else { + return injectionObj.provider; + } +} export type Providers = ValueProvider | FactoryProvider | ClassProvider | constructor; export interface InjectionObject { token: InjectionToken; - provider: Providers; + provider: Providers | CreateAsyncProvider>; } -export const registerDependencies = ( +export const registerDependencies = async ( dependencies: InjectionObject[], override?: InjectionObject[], useChild = false -): DependencyContainer => { +): Promise => { const container = useChild ? defaultContainer.createChildContainer() : defaultContainer; - dependencies.forEach((injectionObj) => { - const inject = override?.find((overrideObj) => overrideObj.token === injectionObj.token) === undefined; - if (inject) { - container.register(injectionObj.token, injectionObj.provider as constructor); - } - }); - override?.forEach((injectionObj) => { - container.register(injectionObj.token, injectionObj.provider as constructor); - }); + + for (const injectionObj of dependencies) { + const inject = override?.find((overrideObj) => overrideObj.token === injectionObj.token) ?? injectionObj; + const provider = await getProvider(inject, container); + container.register(injectionObj.token, provider as constructor); + } + return container; }; diff --git a/src/common/epsg.ts b/src/common/epsg.ts new file mode 100644 index 0000000..fb8d397 --- /dev/null +++ b/src/common/epsg.ts @@ -0,0 +1,4 @@ +import epsg from 'epsg-index/all.json'; +import { epsgRecordsSchema } from './schemas'; + +export const EPSG_DATA_RECORDS = epsgRecordsSchema.parse(epsg); diff --git a/src/common/errors.ts b/src/common/errors.ts new file mode 100644 index 0000000..bbd61b4 --- /dev/null +++ b/src/common/errors.ts @@ -0,0 +1 @@ +export class UnsupportedSrsError extends Error {} diff --git a/src/common/gdal.ts b/src/common/gdal.ts new file mode 100644 index 0000000..46bcd11 --- /dev/null +++ b/src/common/gdal.ts @@ -0,0 +1,196 @@ +import * as gdalAsync from 'gdal-async'; +import { CoordinateTransformation, SpatialReference, type Dataset, type Envelope, type xyz } from 'gdal-async'; +import { Geodesic } from 'geographiclib-geodesic'; +import { z } from 'zod'; +import type { InfoResponse } from '@src/info/models/infoManager'; +import { EPSG_DATA_RECORDS } from './epsg'; +import { UnsupportedSrsError } from './errors'; +import { resolutionDegreeSchema, resolutionMeterSchema } from './schemas'; + +const EPSG_CODE_WGS84 = 4326; + +interface PixelInfo { + pixelWidth: number; + pixelHeight: number; +} + +const geoTransformSchema = z.tuple([z.number(), z.number(), z.number(), z.number(), z.number(), z.number()]); + +export type GdalAsync = typeof gdalAsync; +export const GDAL_ASYNC = Symbol('gdal'); + +export const getPixelInfo = (options: Pick): PixelInfo => { + const { geoTransform } = options; + const validGeoTransform = geoTransformSchema.parse(geoTransform, { error: () => 'Unsupported geo transform' }); + return { pixelHeight: Math.abs(validGeoTransform[5]), pixelWidth: Math.abs(validGeoTransform[1]) }; +}; + +/** + * Get resolutions in a bound region + * @param options - Object with the following properties: + * @param options.sourceSrs - EPSG code or {@link SpatialReference} instance + * @param options.minX - Minimum X of region, in `sourceSrs` units + * @param options.minY - Minimum Y of region, in `sourceSrs` units + * @param options.maxX - Maximum X of region, in `sourceSrs` units + * @param options.maxY - Maximum Y of region, in `sourceSrs` units + * @param options.pixelWidth - Pixel width, in `sourceSrs` units + * @param options.pixelHeight - Pixel height, in `sourceSrs` units + * @returns Object of resolutions in meters and degrees on WGS84 ellipsoid + */ +export const getResolutions = ( + options: { + sourceSrs: SpatialReference | number; + } & Pick & + PixelInfo +): Pick => { + const { maxX, maxY, minX, minY, pixelHeight, pixelWidth, sourceSrs } = options; + const resolvedSourceSrs = typeof sourceSrs === 'number' ? SpatialReference.fromEPSG(sourceSrs) : sourceSrs; + + const [dx, dy] = [maxX - minX, maxY - minY]; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + const [centerX, centerY] = [minX + dx / 2, minY + dy / 2]; + + /* eslint-disable @typescript-eslint/no-magic-numbers */ + const { x: targetMinX } = transformPoint({ + sourceSrs: resolvedSourceSrs, + targetSrs: SpatialReference.fromEPSG(EPSG_CODE_WGS84), + point: { x: centerX - pixelWidth / 2, y: centerY }, + }); + const { x: targetMaxX } = transformPoint({ + sourceSrs: resolvedSourceSrs, + targetSrs: SpatialReference.fromEPSG(EPSG_CODE_WGS84), + point: { x: centerX + pixelWidth / 2, y: centerY }, + }); + const { y: targetMinY } = transformPoint({ + sourceSrs: resolvedSourceSrs, + targetSrs: SpatialReference.fromEPSG(EPSG_CODE_WGS84), + point: { x: centerX, y: centerY - pixelHeight / 2 }, + }); + const { y: targetMaxY } = transformPoint({ + sourceSrs: resolvedSourceSrs, + targetSrs: SpatialReference.fromEPSG(EPSG_CODE_WGS84), + point: { x: centerX, y: centerY + pixelHeight / 2 }, + }); + /* eslint-enable @typescript-eslint/no-magic-numbers */ + + // approximation of the reprojected resolution + const getReprojectedDegreeResolution = (): number => { + const [reprojectedResolutionX, reprojectedResolutionY] = [targetMaxX - targetMinX, targetMaxY - targetMinY]; + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + return (reprojectedResolutionX + reprojectedResolutionY) / 2; + }; + + const getReprojectedMeterResolution = (): number => { + const geodesicDistanceX = Geodesic.WGS84.Inverse(centerY, targetMinX, centerY, targetMaxX).s12; + if (geodesicDistanceX === undefined) + throw new Error( + `Could not calculate geodesic distance between points (${[centerY, targetMinX].toString()})-(${[centerY, targetMaxX].toString()})]` + ); + + const geodesicDistanceY = Geodesic.WGS84.Inverse(targetMinY, centerX, targetMaxY, centerX).s12; + if (geodesicDistanceY === undefined) + throw new Error( + `Could not calculate geodesic distance between points (${[targetMinY, centerX].toString()})-(${[targetMaxY, centerY].toString()})]` + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + return (geodesicDistanceX + geodesicDistanceY) / 2; + }; + + const resolutions = ( + [ + /* eslint-disable @typescript-eslint/no-magic-numbers */ + [resolvedSourceSrs.isGeographic(), { resolutionMeter: getReprojectedMeterResolution(), resolutionDegree: (pixelWidth + pixelHeight) / 2 }], + [resolvedSourceSrs.isProjected(), { resolutionMeter: (pixelWidth + pixelHeight) / 2, resolutionDegree: getReprojectedDegreeResolution() }], + /* eslint-enable @typescript-eslint/no-magic-numbers */ + ] satisfies [boolean, { resolutionMeter: number; resolutionDegree: number }][] + ).find((value) => value[0])?.[1]; + + if (resolutions == undefined) { + throw new UnsupportedSrsError('Unsupported SRS type'); + } + + const response = z.strictObject({ resolutionMeter: resolutionMeterSchema, resolutionDegree: resolutionDegreeSchema }).parse(resolutions); + + return response; +}; + +export const getSrsName = (srsId: number): string => { + const srs = SpatialReference.fromEPSG(srsId); + const srsName = ( + [ + [srs.isGeographic(), srs.getAttrValue('GEOGCS')], + [srs.isProjected(), srs.getAttrValue('PROJCS')], + ] satisfies [boolean, string][] + ).find((value) => value[0])?.[1]; + + if (srsName == undefined) { + throw new UnsupportedSrsError('Unsupported SRS type'); + } + + return srsName; +}; + +export const getSrsGeographicBounds = (options: { srsId: number }): [number, number, number, number] => { + const { srsId } = options; + const epsgRecord = EPSG_DATA_RECORDS[srsId]; + if (!epsgRecord) throw new UnsupportedSrsError('Unsupported SRS'); + + const [sourceMaxY, sourceMinX, sourceMinY, sourceMaxX] = epsgRecord.bbox; + return [sourceMinX, sourceMinY, sourceMaxX, sourceMaxY]; +}; + +export const getSrsInfo = (srs: SpatialReference): Pick => { + const srsAuthorityCode = srs.getAuthorityCode(); + const srsId = parseInt(srsAuthorityCode); + if (Number.isNaN(srsId)) throw new UnsupportedSrsError('Unsupported SRS'); + const srsName = getSrsName(srsId); + + return { + srsId, + srsName, + }; +}; + +/** + * Swap coordinate order to have a { x: lon, y: lat } order, if needed + * @param options - Object with the following properties: + * @param options.point - Point to swap order if needed + * @param options.srs - SRS + * @returns Point with swapped coordinates + */ +export const swapCoordinateOrder = (options: { srs: SpatialReference; point: xyz }): xyz => { + const { point, srs } = options; + + let swappedPoint: xyz; + + if (srs.isGeographic()) { + swappedPoint = srs.EPSGTreatsAsLatLong() ? { x: point.y, y: point.x } : point; + } else if (srs.isProjected()) { + swappedPoint = srs.EPSGTreatsAsNorthingEasting() ? { x: point.y, y: point.x } : point; + } else { + throw new UnsupportedSrsError(`Unsupported SRS type of '${srs.getAuthorityName()}:${srs.getAuthorityCode()}'`); + } + + return swappedPoint; +}; + +/** + * Reproject point + * @param options - Object with the following properties: + * @param options.point - Point to be transfomed (long/lat or east/north order) + * @param options.sourceSrs - Source SRS + * @param options.targetSrs - Target SRS + * @returns Reprojected point + */ +export const transformPoint = (options: { point: xyz; sourceSrs: SpatialReference; targetSrs: SpatialReference }): xyz => { + const { point, sourceSrs, targetSrs } = options; + + const sourcePoint = swapCoordinateOrder({ point, srs: sourceSrs }); + const coordinateTransformation = new CoordinateTransformation(sourceSrs, targetSrs); + const transformedPoint = coordinateTransformation.transformPoint(sourcePoint); + const targetPoint = swapCoordinateOrder({ point: transformedPoint, srs: targetSrs }); + + return targetPoint; +}; diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts index 455054c..9398cbf 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -1,4 +1,18 @@ -export interface IConfig { - get: (setting: string) => T; - has: (setting: string) => boolean; -} +import type { z } from 'zod'; +import type { components } from '@src/openapi'; +import type { RASTER_DATA_TYPES } from './constants'; +import type { pixelDataTypesSchema } from './schemas'; + +export type NoDuplicates = T extends [infer First, ...infer Rest] + ? First extends Rest[number] + ? false + : NoDuplicates + : true; + +export type IsComplete = [Target] extends [U[number]] ? true : false; + +export type GeoTiffDataType = components['schemas']['InfoGeoTiff']['dataType']; +export type RasterDataType = components['schemas']['InfoResponse']['dataType']; + +export type PixelDataType = z.infer>; +export type RasterFormats = keyof typeof RASTER_DATA_TYPES; diff --git a/src/common/logger.ts b/src/common/logger.ts new file mode 100644 index 0000000..1ce0248 --- /dev/null +++ b/src/common/logger.ts @@ -0,0 +1,80 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import type { IncomingMessage, ServerResponse } from 'node:http'; +import { type Logger, jsLogger } from '@map-colonies/js-logger'; +import { getOtelMixin } from '@map-colonies/tracing-utils'; +import type { AttributeValue, Attributes } from '@opentelemetry/api'; +import * as api from '@opentelemetry/api'; +import type { NextFunction, Request, Response } from 'express'; +import { get } from 'lodash'; +import type { DependencyContainer } from 'tsyringe'; +import type { ConfigType } from './config'; +import { SERVICES } from './constants'; + +const logContext = new AsyncLocalStorage(); + +export function addOperationIdToLog(req: IncomingMessage, res: ServerResponse, loggableObject: Record): unknown { + const operationId = get(req, 'openapi.schema.operationId') as string | undefined; + if (operationId !== undefined) { + loggableObject['operationId'] = operationId; + } + + const store = logContext.getStore(); + const span = api.trace.getActiveSpan(); + + if (store) { + span?.setAttributes(store); + } + + return loggableObject; +} + +export function enrichLogContext(values: Attributes, addToCurrentTrace = false): void { + const store = logContext.getStore(); + if (store) { + Object.assign(store, values); + } + + if (addToCurrentTrace) { + const span = api.trace.getActiveSpan(); + span?.setAttributes(values); + } +} + +export function getLogContext(): Attributes | undefined { + return structuredClone(logContext.getStore()); +} + +export async function loggerFactory(container: DependencyContainer): Promise { + const config = container.resolve(SERVICES.CONFIG); + const loggerConfig = config.get('telemetry.logger'); + + const logger = await jsLogger({ + ...loggerConfig, + mixin: (mergeObj, level) => { + const otelMixin = getOtelMixin(); + const store = logContext.getStore(); + + return { ...otelMixin(mergeObj, level), ...store }; + }, + }); + + return logger; +} + +export function logContextInjectionMiddleware(req: Request, res: Response, next: NextFunction): void { + logContext.run({}, () => { + next(); + }); +} + +export function logEnrichmentParamMiddlewareFactory( + logEntry: string +): (req: Request, res: Response, next: NextFunction, paramValue: AttributeValue) => void { + return function (req: Request, res: Response, next: NextFunction, paramValue: AttributeValue): void { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (paramValue) { + enrichLogContext({ [logEntry]: paramValue }); + } + next(); + }; +} diff --git a/src/common/schemas.ts b/src/common/schemas.ts new file mode 100644 index 0000000..17f597c --- /dev/null +++ b/src/common/schemas.ts @@ -0,0 +1,50 @@ +import { z, type ZodLiteral, type ZodNumber } from 'zod'; +import { getConfig } from './config'; +import { RASTER_DATA_TYPES } from './constants'; + +const config = getConfig(); + +const blockSize = config.get('application.validation.blockSize'); +const compression = config.get('application.validation.compression'); +const resolutionDegree = config.get('application.validation.resolutionDegree'); +const resolutionMeter = config.get('application.validation.resolutionMeter'); +const supportedSrsIds = config.get('application.validation.supportedSrsIds'); + +export const areaOrPointSchema = z.literal(['Area', 'Point']); +export const blockSizeSchema = z.object({ x: z.literal(blockSize), y: z.literal(blockSize) }); +export const compressionSchema = z.literal(compression); +export const layoutSchema = z.literal('COG'); +export const noDataValueSchema = z.union([z.number(), z.nan()]).transform((value) => (Number.isNaN(value) ? 'NaN' : value)); +export const overviewsCountSchema = ({ + blockSize, + size: { x, y }, +}: { + blockSize: z.infer; + size: { x: number; y: number }; +}): ZodNumber => + z + .number() + .min(1) + .max(Math.min(...[Math.ceil(x / blockSize.x), Math.ceil(y / blockSize.y)])) + .int() + .positive(); +export const pixelDataTypesSchema = ( + format: keyof typeof RASTER_DATA_TYPES +): ZodLiteral<(typeof RASTER_DATA_TYPES)[keyof typeof RASTER_DATA_TYPES][number]> => z.literal(RASTER_DATA_TYPES[format]); +export const resolutionDegreeSchema = z.number().min(resolutionDegree.min).max(resolutionDegree.max); +export const resolutionMeterSchema = z.number().min(resolutionMeter.min).max(resolutionMeter.max); +export const srsIdSchema = z.literal(supportedSrsIds); +export const srsNameSchema = z.string().min(1); + +export const epsgRecordSchema = z.strictObject({ + code: z.string(), + kind: z.string(), + name: z.string(), + wkt: z.string().nullable(), + proj4: z.string().nullable(), + bbox: z.tuple([z.number(), z.number(), z.number(), z.number()]), + unit: z.string().nullable(), + area: z.string().nullable(), + accuracy: z.number().nullable(), +}); +export const epsgRecordsSchema = z.record(z.coerce.number().int().positive(), epsgRecordSchema); diff --git a/src/common/utils.ts b/src/common/utils.ts new file mode 100644 index 0000000..b01725e --- /dev/null +++ b/src/common/utils.ts @@ -0,0 +1,3 @@ +export const hasKey = >(x: PropertyKey, object: T): x is keyof T => { + return Object.keys(object).includes(String(x)); +}; diff --git a/src/containerConfig.ts b/src/containerConfig.ts index efaea90..94b2f8c 100644 --- a/src/containerConfig.ts +++ b/src/containerConfig.ts @@ -1,14 +1,17 @@ -import { getOtelMixin } from '@map-colonies/tracing-utils'; +import type { Logger } from '@map-colonies/js-logger'; import { trace } from '@opentelemetry/api'; +import * as gdalAsync from 'gdal-async'; import { Registry } from 'prom-client'; -import { DependencyContainer } from 'tsyringe/dist/typings/types'; -import { jsLogger } from '@map-colonies/js-logger'; -import { InjectionObject, registerDependencies } from '@common/dependencyRegistration'; +import type { DependencyContainer } from 'tsyringe/dist/typings/types'; +import { getConfig } from '@common/config'; import { SERVICES, SERVICE_NAME } from '@common/constants'; +import { InjectionObject, registerDependencies, type Providers } from '@common/dependencyRegistration'; +import { GDAL_ASYNC } from '@common/gdal'; import { getTracing } from '@common/tracing'; -import { resourceNameRouterFactory, RESOURCE_NAME_ROUTER_SYMBOL } from './resourceName/routes/resourceNameRouter'; -import { anotherResourceRouterFactory, ANOTHER_RESOURCE_ROUTER_SYMBOL } from './anotherResource/routes/anotherResourceRouter'; -import { getConfig } from './common/config'; +import { loggerFactory } from './common/logger'; +import { DEM_ROUTER_SYMBOL, demRouterFactory } from './dem/routes/demRouter'; +import { GDALHandler } from './info/fileHandlers/gdal'; +import { INFO_ROUTER_SYMBOL, infoRouterFactory } from './info/routes/infoRouter'; export interface RegisterOptions { override?: InjectionObject[]; @@ -18,21 +21,27 @@ export interface RegisterOptions { export const registerExternalValues = async (options?: RegisterOptions): Promise => { const configInstance = getConfig(); - const loggerConfig = configInstance.get('telemetry.logger'); - - const logger = jsLogger({ ...loggerConfig, prettyPrint: loggerConfig.prettyPrint, mixin: getOtelMixin() }); - const tracer = trace.getTracer(SERVICE_NAME); const metricsRegistry = new Registry(); configInstance.initializeMetrics(metricsRegistry); const dependencies: InjectionObject[] = [ { token: SERVICES.CONFIG, provider: { useValue: configInstance } }, - { token: SERVICES.LOGGER, provider: { useValue: logger } }, + { + token: SERVICES.LOGGER, + provider: { + useAsync: async (dependencyContainer: DependencyContainer): Promise> => { + const logger = await loggerFactory(dependencyContainer); + return { useValue: logger }; + }, + }, + }, { token: SERVICES.TRACER, provider: { useValue: tracer } }, { token: SERVICES.METRICS, provider: { useValue: metricsRegistry } }, - { token: RESOURCE_NAME_ROUTER_SYMBOL, provider: { useFactory: resourceNameRouterFactory } }, - { token: ANOTHER_RESOURCE_ROUTER_SYMBOL, provider: { useFactory: anotherResourceRouterFactory } }, + { token: DEM_ROUTER_SYMBOL, provider: { useFactory: demRouterFactory } }, + { token: GDAL_ASYNC, provider: { useValue: gdalAsync } }, + { token: 'FileHandler', provider: { useClass: GDALHandler } }, + { token: INFO_ROUTER_SYMBOL, provider: { useFactory: infoRouterFactory } }, { token: 'onSignal', provider: { diff --git a/src/dem/controllers/demController.ts b/src/dem/controllers/demController.ts new file mode 100644 index 0000000..418cade --- /dev/null +++ b/src/dem/controllers/demController.ts @@ -0,0 +1,54 @@ +import type { Logger } from '@map-colonies/js-logger'; +import httpStatus from 'http-status-codes'; +import { inject, injectable } from 'tsyringe'; +import type { TypedRequestHandlers } from '@openapi'; +import { SERVICES } from '@src/common/constants'; +import { DEMManager } from '../models/demManager'; + +@injectable() +export class DEMController { + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(DEMManager) private readonly demManager: DEMManager + ) {} + + public create: TypedRequestHandlers['create'] = (req, res, next) => { + try { + const response = this.demManager.create(req.body); + return res.status(httpStatus.OK).json(response); + } catch (error) { + this.logger.error(error); + next(error); + } + }; + + public delete: TypedRequestHandlers['delete'] = (req, res, next) => { + try { + const response = this.demManager.delete(req.params); + return res.status(httpStatus.OK).json(response); + } catch (error) { + this.logger.error(error); + next(error); + } + }; + + public edit: TypedRequestHandlers['edit'] = (req, res, next) => { + try { + const response = this.demManager.edit({ ...req.params, ...req.body }); + return res.status(httpStatus.OK).json(response); + } catch (error) { + this.logger.error(error); + next(error); + } + }; + + public editStatus: TypedRequestHandlers['editStatus'] = (req, res, next) => { + try { + this.demManager.editStatus({ ...req.params, ...req.body }); + return res.status(httpStatus.NO_CONTENT).send(); + } catch (error) { + this.logger.error(error); + next(error); + } + }; +} diff --git a/src/dem/models/demManager.ts b/src/dem/models/demManager.ts new file mode 100644 index 0000000..d26ff84 --- /dev/null +++ b/src/dem/models/demManager.ts @@ -0,0 +1,40 @@ +import { NotImplementedError } from '@map-colonies/error-types'; +import type { Logger } from '@map-colonies/js-logger'; +import { inject, injectable } from 'tsyringe'; +import type { components, operations } from '@openapi'; +import { SERVICES } from '@common/constants'; + +export type CreateOptions = components['schemas']['CreateRequestBody']; +export type DeleteOptions = operations['delete']['parameters']['path']; +export type EditOptions = components['schemas']['EditRequestBody'] & operations['edit']['parameters']['path']; +export type EditStatusOptions = components['schemas']['EditStatusRequestBody'] & operations['editStatus']['parameters']['path']; +export type DemResponse = components['schemas']['DemResponse']; + +@injectable() +export class DEMManager { + public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} + + public create(options: CreateOptions): DemResponse { + this.logger.info({ msg: 'Create DEM resource', resource: options }); + + throw new NotImplementedError('Not implemented'); + } + + public delete(options: DeleteOptions): DemResponse { + this.logger.info({ msg: 'Delete DEM resource', resource: options }); + + throw new NotImplementedError('Not implemented'); + } + + public edit(options: EditOptions): DemResponse { + this.logger.info({ msg: 'Edit DEM resource', resource: options }); + + throw new NotImplementedError('Not implemented'); + } + + public editStatus(options: EditStatusOptions): void { + this.logger.info({ msg: 'Edit DEM status', resource: options }); + + throw new NotImplementedError('Not implemented'); + } +} diff --git a/src/dem/routes/demRouter.ts b/src/dem/routes/demRouter.ts new file mode 100644 index 0000000..6114e6d --- /dev/null +++ b/src/dem/routes/demRouter.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import type { FactoryFunction } from 'tsyringe'; +import { logEnrichmentParamMiddlewareFactory } from '@src/common/logger'; +import { DEMController } from '../controllers/demController'; + +const demRouterFactory: FactoryFunction = (dependencyContainer) => { + const router = Router(); + const controller = dependencyContainer.resolve(DEMController); + + router.param('id', logEnrichmentParamMiddlewareFactory('id')); + + router.post('/:id', controller.edit); + router.delete('/:id', controller.edit); + router.patch('/:id', controller.edit); + router.patch('/:id/status', controller.edit); + + return router; +}; + +export const DEM_ROUTER_SYMBOL = Symbol('demRouterFactory'); + +export { demRouterFactory }; diff --git a/src/index.ts b/src/index.ts index b6ee4c5..a0dba25 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,9 @@ import 'reflect-metadata'; import { createServer } from 'http'; import { createTerminus } from '@godaddy/terminus'; -import { Logger } from '@map-colonies/js-logger'; +import type { Logger } from '@map-colonies/js-logger'; import { SERVICES } from '@common/constants'; -import { ConfigType } from '@common/config'; +import type { ConfigType } from '@common/config'; import { getApp } from './app'; void getApp() diff --git a/src/info/controllers/infoController.ts b/src/info/controllers/infoController.ts new file mode 100644 index 0000000..d882c21 --- /dev/null +++ b/src/info/controllers/infoController.ts @@ -0,0 +1,34 @@ +import { UnprocessableEntityError } from '@map-colonies/error-types'; +import httpStatus from 'http-status-codes'; +import { inject, injectable } from 'tsyringe'; +import { ZodError } from 'zod'; +import type { Logger } from '@map-colonies/js-logger'; +import type { TypedRequestHandlers } from '@openapi'; +import { SERVICES } from '@common/constants'; +import { UnsupportedSrsError } from '@src/common/errors'; +import { enrichLogContext } from '@src/common/logger'; +import { InfoManager } from '../models/infoManager'; + +@injectable() +export class InfoController { + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(InfoManager) private readonly infoManager: InfoManager + ) {} + + public info: TypedRequestHandlers['info'] = async (req, res, next) => { + try { + enrichLogContext({ demFilePath: req.body.demFilePath }); + const response = await this.infoManager.info(req.body); + return res.status(httpStatus.OK).json(response); + } catch (error) { + this.logger.error({ err: error }); + if (error instanceof ZodError) { + return next(new UnprocessableEntityError(error.issues[0]?.message ?? 'validation error')); + } else if (error instanceof UnsupportedSrsError) { + return next(new UnprocessableEntityError(error.message)); + } + next(error); + } + }; +} diff --git a/src/info/fileHandlers/gdal.ts b/src/info/fileHandlers/gdal.ts new file mode 100644 index 0000000..862400c --- /dev/null +++ b/src/info/fileHandlers/gdal.ts @@ -0,0 +1,153 @@ +import { access, constants } from 'node:fs/promises'; +import { extname, join } from 'node:path'; +import { NotFoundError } from '@map-colonies/error-types'; +import type { Logger } from '@map-colonies/js-logger'; +import { type Dataset, type Driver, type RasterBand } from 'gdal-async'; +import { inject, injectable } from 'tsyringe'; +import { z } from 'zod'; +import type { ConfigType } from '@src/common/config'; +import { RASTER_DATA_TYPES, SERVICES } from '@src/common/constants'; +import { UnsupportedSrsError } from '@src/common/errors'; +import { GDAL_ASYNC, getPixelInfo, getResolutions, getSrsInfo, type GdalAsync } from '@src/common/gdal'; +import type { RasterFormats } from '@src/common/interfaces'; +import { enrichLogContext } from '@src/common/logger'; +import { + areaOrPointSchema, + blockSizeSchema, + compressionSchema, + layoutSchema, + noDataValueSchema, + overviewsCountSchema, + pixelDataTypesSchema, + srsIdSchema, + srsNameSchema, +} from '@src/common/schemas'; +import { hasKey } from '@src/common/utils'; +import type { FileHandler, InfoResponse } from '@src/info/models/infoManager'; + +@injectable() +export class GDALHandler implements FileHandler { + public readonly name = GDALHandler.name; + private readonly supportedFormatsMap: Record; + private readonly sourceDir: string; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(GDAL_ASYNC) private readonly gdal: GdalAsync + ) { + this.supportedFormatsMap = this.config.get('application.supportedFormatsMap'); + this.sourceDir = this.config.get('storageExplorer.sourceDir'); + } + + public supports(filePath: string): boolean { + try { + this.logger.debug({ msg: 'Check if file is supported by handler' }); + this.getDriver(filePath); + this.logger.debug({ msg: `Handler '${this.name}' supports the requested file` }); + return true; + } catch (error) { + this.logger.debug({ msg: `Handler '${this.name}' cannot handle the requested file, caused by an error: ${JSON.stringify(error)}` }); + return false; + } + } + + public async getInfo(filePath: string): Promise { + enrichLogContext({ handler: this.name }); + this.logger.debug({ msg: 'Getting info' }); + let dataset: Dataset | undefined; + + const fullFilePath = join(this.sourceDir, filePath); + try { + await access(fullFilePath, constants.F_OK); + } catch (error) { + this.logger.error({ msg: `Cannot find file: ${fullFilePath}`, err: error }); + throw new NotFoundError(`Cannot find file: ${fullFilePath}. got error: ${JSON.stringify(error)}`); + } + + try { + const { driver, format } = this.getDriver(filePath); + dataset = await driver.openAsync(fullFilePath, 'r'); + const band = await dataset.bands.getAsync(1); // DEMs are mostly single banded + await this.validateMetadata({ band, dataset }); + const metadata = await this.getMetadata({ band, dataset, format }); + return metadata; + } finally { + dataset?.close(); + } + } + + private getDriver(filePath: string): { driver: Driver; format: RasterFormats } { + const fileExtension = extname(filePath).slice(1); + const supportedFormat = Object.entries(this.supportedFormatsMap).find(([, supportedDriver]) => { + const driver = this.gdal.drivers.get(supportedDriver); + // eslint-disable-next-line @typescript-eslint/naming-convention + const driverMetadata = driver.getMetadata() as { DMD_EXTENSION?: string; DMD_EXTENSIONS?: string }; + const { DMD_EXTENSION: extension = '', DMD_EXTENSIONS: extensions = '' } = driverMetadata; + return [extension, ...extensions.split(' ')].filter((extension) => extension.length > 0).includes(fileExtension); + }); + + if (supportedFormat === undefined) throw new Error(`Unsupported file format of file: ${filePath}`); + const [format, driverName] = supportedFormat; + if (!hasKey(format, RASTER_DATA_TYPES)) { + throw new Error(`Format '${format}' is not part of service's API`); + } + const driver = this.gdal.drivers.get(driverName); + this.logger.debug(`Found driver '${driverName}' supporting file`); + return { driver, format }; + } + + private async getMetadata({ band, dataset, format }: { band: RasterBand; dataset: Dataset; format: RasterFormats }): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const metadata = await dataset.getMetadataAsync(); + const areaOrPoint = z + // eslint-disable-next-line @typescript-eslint/naming-convention + .object({ AREA_OR_POINT: areaOrPointSchema }) + .parse(metadata, { error: () => 'Could not extract AREA_OR_POINT metadata' }).AREA_OR_POINT; + + const bandDataType = await band.dataTypeAsync; + const dataType = pixelDataTypesSchema(format).parse(bandDataType, { error: () => 'Unsupported band data type' }); + + const bandNoDataValueAsync = await band.noDataValueAsync; + const noDataValue = noDataValueSchema.parse(bandNoDataValueAsync, { error: () => 'Unsupported band nodata value' }); + + const srs = await dataset.srsAsync; + if (srs === null) throw new UnsupportedSrsError('Unsupported SRS'); + const srsInfo = getSrsInfo(srs); + const { srsId, srsName } = z.strictObject({ srsId: srsIdSchema, srsName: srsNameSchema }).parse(srsInfo, { error: () => 'Unsupported SRS' }); + + const geoTransform = await dataset.geoTransformAsync; + const pixelInfo = getPixelInfo({ geoTransform }); + + const { resolutionDegree, resolutionMeter } = getResolutions({ + ...dataset.bands.getEnvelope(), + ...pixelInfo, + sourceSrs: srs, + }); + + return { + areaOrPoint, + dataType, + noDataValue, + resolutionDegree, + resolutionMeter, + srsId, + srsName, + }; + } + + private async validateMetadata({ band, dataset }: { band: RasterBand; dataset: Dataset }): Promise { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const metadataImageStructure = await dataset.getMetadataAsync('IMAGE_STRUCTURE'); + void z + // eslint-disable-next-line @typescript-eslint/naming-convention + .object({ LAYOUT: layoutSchema, COMPRESSION: compressionSchema }) + .parse(metadataImageStructure, { error: () => 'Unsupported image structure metadata (LAYOUT and COMPRESSION)' }).LAYOUT; + + const bandBlockSize = await band.blockSizeAsync; + const blockSize = blockSizeSchema.parse(bandBlockSize, { error: () => 'Unsupported block size' }); + + const bandOverviewsCount = await band.overviews.countAsync(); + overviewsCountSchema({ blockSize, size: band.size }).parse(bandOverviewsCount, { error: () => 'Could not find overviews' }); + } +} diff --git a/src/info/models/infoManager.ts b/src/info/models/infoManager.ts new file mode 100644 index 0000000..e91e6a8 --- /dev/null +++ b/src/info/models/infoManager.ts @@ -0,0 +1,38 @@ +import { UnprocessableEntityError } from '@map-colonies/error-types'; +import type { Logger } from '@map-colonies/js-logger'; +import { inject, injectable, injectAll } from 'tsyringe'; +import { SERVICES } from '@src/common/constants'; +import { components } from '@src/openapi'; + +export type InfoOptions = components['schemas']['InfoRequestBody']; +export type InfoResponse = components['schemas']['InfoResponse']; +export interface FileHandler { + name: string; + supports: (filePath: string) => boolean; + getInfo: (filePath: string) => Promise; +} + +@injectable() +export class InfoManager { + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @injectAll('FileHandler') private readonly fileHandlers: FileHandler[] + ) {} + + public async info(options: InfoOptions): Promise { + const { demFilePath } = options; + + this.logger.debug({ msg: 'Handling info request', resource: options }); + const handler = this.fileHandlers.find((handler) => handler.supports(demFilePath)); + + if (!handler) { + throw new UnprocessableEntityError(`No handler found for file: ${demFilePath}`); + } + this.logger.debug({ msg: `Using handler '${handler.name}'` }); + + const response = await handler.getInfo(demFilePath); + this.logger.debug({ msg: 'Info response', response }); + + return response; + } +} diff --git a/src/info/routes/infoRouter.ts b/src/info/routes/infoRouter.ts new file mode 100644 index 0000000..4a7584c --- /dev/null +++ b/src/info/routes/infoRouter.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import type { FactoryFunction } from 'tsyringe'; +import { InfoController } from '../controllers/infoController'; + +const infoRouterFactory: FactoryFunction = (dependencyContainer) => { + const router = Router(); + const controller = dependencyContainer.resolve(InfoController); + + router.post('/', controller.info); + + return router; +}; + +export const INFO_ROUTER_SYMBOL = Symbol('infoRouterFactory'); + +export { infoRouterFactory }; diff --git a/src/instrumentation.mts b/src/instrumentation.mts index b1bd0c2..267beab 100644 --- a/src/instrumentation.mts +++ b/src/instrumentation.mts @@ -1,16 +1,19 @@ // This file handles the tracing initialization and starts the tracing process before the app starts. // You should be careful about editing this file, as it is a critical part of the application's functionality. // Because this file is a module it should imported using the `--import` flag in the `node` command, and should not be imported by any other file. +import { isMainThread } from 'node:worker_threads'; import { tracingFactory } from './common/tracing.js'; import { getConfig, initConfig } from './common/config.js'; -await initConfig(); +if (isMainThread) { + await initConfig(); -const config = getConfig(); + const config = getConfig(); -const tracingConfig = config.get('telemetry.tracing'); -const sharedConfig = config.get('telemetry.shared'); + const tracingConfig = config.get('telemetry.tracing'); + const sharedConfig = config.get('telemetry.shared'); -const tracing = tracingFactory({ ...tracingConfig, ...sharedConfig }); + const tracing = tracingFactory({ ...tracingConfig, ...sharedConfig }); -tracing.start(); + tracing.start(); +} diff --git a/src/openapi.d.ts b/src/openapi.d.ts index 3c3f57f..13b9534 100644 --- a/src/openapi.d.ts +++ b/src/openapi.d.ts @@ -4,35 +4,69 @@ import type { TypedRequestHandlers as ImportedTypedRequestHandlers } from '@map-colonies/openapi-helpers/typedRequestHandler'; export type paths = { - '/anotherResource': { + '/dem': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** gets the resource */ - get: operations['getAnotherResource']; + get?: never; put?: never; - post?: never; + /** Create a DEM resource */ + post: operations['create']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/resourceName': { + '/dem/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Delete an existing DEM resource */ + delete: operations['delete']; + options?: never; + head?: never; + /** Edit an existing DEM resource */ + patch: operations['edit']; + trace?: never; + }; + '/dem/{id}/status': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Edit status for existing DEM resource */ + patch: operations['editStatus']; + trace?: never; + }; + '/info': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** gets the resource */ - get: operations['getResourceName']; + get?: never; put?: never; - /** creates a new record of type resource */ - post: operations['createResource']; + /** Extracts GDAL info from the provided DEM file */ + post: operations['info']; delete?: never; options?: never; head?: never; @@ -43,19 +77,141 @@ export type paths = { export type webhooks = Record; export type components = { schemas: { - error: { + /** + * @description Indicates whether each raster pixel represents an area or point + * @enum {string} + */ + AreaOrPoint: 'Area' | 'Point'; + /** @description Permitted roles, value must be between 0 and 100 */ + Classification: string; + CreateRequestBody: { + metadata: components['schemas']['DemMetadata']; + inputFiles: components['schemas']['InputFiles']; + }; + /** + * @description GeoTiff supported data types + * @enum {string} + */ + GeoTiffDataType: 'Int8' | 'Int16' | 'Int32' | 'Int64' | 'Float16' | 'Float32' | 'Float64'; + /** + * @description Input file paths + * @example /path/to/example.tif + */ + DemFilePath: string; + DemMetadata: { + classification: components['schemas']['Classification']; + productId: components['schemas']['ProductId']; + productName: components['schemas']['ProductName']; + productType: components['schemas']['ProductType']; + region: components['schemas']['Region']; + description?: components['schemas']['Description']; + geoidModel?: components['schemas']['GeoidModel']; + keywords?: components['schemas']['Keywords']; + producerName?: components['schemas']['ProducerName']; + productSubType?: components['schemas']['ProductSubType']; + }; + DemResponse: { + /** Format: uuid */ + jobId: string; + }; + /** @description Layer's description */ + Description: string; + EditRequestBody: { + classification?: components['schemas']['Classification']; + description?: components['schemas']['Description']; + geoidModel?: components['schemas']['GeoidModel']; + keywords?: components['schemas']['Keywords']; + producerName?: components['schemas']['ProducerName']; + productName?: components['schemas']['ProductName']; + region?: components['schemas']['Region']; + }; + EditStatusRequestBody: { + status: components['schemas']['Status']; + }; + ErrorMessage: { message: string; + stacktrace?: string; + }; + /** @description Earth's geoid model */ + GeoidModel: string; + /** @description Common properties of regular grids */ + InfoCommonRegularGridProperties: { + areaOrPoint: components['schemas']['AreaOrPoint']; + resolutionDegree: components['schemas']['ResolutionDegree']; + resolutionMeter: components['schemas']['ResolutionMeter']; + srsId: components['schemas']['SrsId']; + srsName: components['schemas']['SrsName']; + }; + /** @description Info request body */ + InfoRequestBody: { + demFilePath: components['schemas']['DemFilePath']; }; - resource: { - /** Format: int64 */ - id: number; - name: string; - description: string; + /** @description Info response body */ + InfoResponse: components['schemas']['InfoGeoTiff']; + /** @description Info properties of GeoTiff */ + InfoGeoTiff: components['schemas']['InfoCommonRegularGridProperties'] & { + dataType: components['schemas']['GeoTiffDataType']; + noDataValue: components['schemas']['NoDataValue']; }; - anotherResource: { - kind: string; - isAlive: boolean; + /** @description Input files */ + InputFiles: { + demFilePath: components['schemas']['DemFilePath']; + metadataShapefilePath: components['schemas']['MetadataShapefilePath']; + productShapefilePath: components['schemas']['ProductShapefilePath']; }; + Keywords: string; + /** + * Format: uuid + * @description Layer's identifier + * @example c52d8189-7e07-456a-8c6b-53859523c3e9 + */ + LayerId: string; + /** + * @description Metadata shape file path + * @example /path/to/ShapeMetadata.shp + */ + MetadataShapefilePath: string; + NoDataValue: number | 'NaN'; + /** + * @description The status of the DEM + * @default UNPUBLISHED + * @enum {string} + */ + Status: 'PUBLISHED' | 'UNPUBLISHED'; + /** + * @description Layer's producer name default to 'IDFMU' + * @default IDFMU + */ + ProducerName: string; + /** + * @description Layer's external identifier, must start with a letter and contain only letters, numbers and underscores + * @example SRTM + */ + ProductId: string; + /** @description Layer's external name */ + ProductName: string; + /** @description Layer's sub type */ + ProductSubType: string; + /** + * @description Layer's type, list of DEM product types + * @enum {string} + */ + ProductType: 'DTM' | 'DSM' | 'DTMBest' | 'DSMBest'; + /** + * @description Product shape file path + * @example /path/to/Product.shp + */ + ProductShapefilePath: string; + /** @description List of layer's regions */ + Region: string[]; + /** @description DEM resolution in degrees */ + ResolutionDegree: number; + /** @description DEM resolution in meters */ + ResolutionMeter: number; + /** @description Projection code as registered by EPSG */ + SrsId: number; + /** @description Projection name as registered by EPSG */ + SrsName: string; }; responses: never; parameters: never; @@ -65,14 +221,18 @@ export type components = { }; export type $defs = Record; export interface operations { - getAnotherResource: { + create: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['CreateRequestBody']; + }; + }; responses: { /** @description OK */ 200: { @@ -80,7 +240,7 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['anotherResource']; + 'application/json': components['schemas']['DemResponse']; }; }; /** @description Bad Request */ @@ -89,16 +249,46 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['error']; + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Unprocessable Content */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; }; }; }; }; - getResourceName: { + delete: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description The id of the DEM to delete */ + id: components['schemas']['LayerId']; + }; cookie?: never; }; requestBody?: never; @@ -109,8 +299,150 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['DemResponse']; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Unprocessable Content */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + }; + }; + edit: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The id of the DEM to edit */ + id: components['schemas']['LayerId']; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['EditRequestBody']; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DemResponse']; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Unprocessable Content */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + }; + }; + editStatus: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The id of the DEM whose status to edit */ + id: components['schemas']['LayerId']; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['EditStatusRequestBody']; + }; + }; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; }; + content?: never; }; /** @description Bad Request */ 400: { @@ -118,31 +450,68 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['error']; + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Unprocessable Content */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; }; }; }; }; - createResource: { + info: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + /** @description An object containing object of DEM input file */ requestBody: { content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['InfoRequestBody']; }; }; responses: { - /** @description created */ - 201: { + /** @description OK */ + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['InfoResponse']; }; }; /** @description Bad Request */ @@ -151,7 +520,34 @@ export interface operations { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['error']; + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Unprocessable Content */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; + }; + }; + /** @description Internal Server Error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorMessage']; }; }; }; diff --git a/src/resourceName/controllers/resourceNameController.ts b/src/resourceName/controllers/resourceNameController.ts deleted file mode 100644 index c194ea6..0000000 --- a/src/resourceName/controllers/resourceNameController.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Logger } from '@map-colonies/js-logger'; -import httpStatus from 'http-status-codes'; -import { injectable, inject } from 'tsyringe'; -import { type Registry, Counter } from 'prom-client'; -import type { TypedRequestHandlers } from '@openapi'; -import { SERVICES } from '@common/constants'; - -import { ResourceNameManager } from '../models/resourceNameManager'; - -@injectable() -export class ResourceNameController { - private readonly createdResourceCounter: Counter; - - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(ResourceNameManager) private readonly manager: ResourceNameManager, - @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry - ) { - this.createdResourceCounter = new Counter({ - name: 'created_resource', - help: 'number of created resources', - registers: [this.metricsRegistry], - }); - } - - public getResource: TypedRequestHandlers['getResourceName'] = (req, res) => { - return res.status(httpStatus.OK).json(this.manager.getResource()); - }; - - public createResource: TypedRequestHandlers['POST /resourceName'] = (req, res) => { - const createdResource = this.manager.createResource(req.body); - this.createdResourceCounter.inc(1); - return res.status(httpStatus.CREATED).json(createdResource); - }; -} diff --git a/src/resourceName/models/resourceNameManager.ts b/src/resourceName/models/resourceNameManager.ts deleted file mode 100644 index be5aa5a..0000000 --- a/src/resourceName/models/resourceNameManager.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Logger } from '@map-colonies/js-logger'; -import { inject, injectable } from 'tsyringe'; -import type { components } from '@openapi'; -import { SERVICES } from '@common/constants'; - -const resourceInstance: IResourceNameModel = { - id: 1, - name: 'ronin', - description: 'can you do a logistics run?', -}; - -function generateRandomId(): number { - const rangeOfIds = 100; - return Math.floor(Math.random() * rangeOfIds); -} - -export type IResourceNameModel = components['schemas']['resource']; - -@injectable() -export class ResourceNameManager { - public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} - - public getResource(): IResourceNameModel { - this.logger.info({ msg: 'getting resource', resourceId: resourceInstance.id }); - - return resourceInstance; - } - - public createResource(resource: IResourceNameModel): IResourceNameModel { - const resourceId = generateRandomId(); - - this.logger.info({ msg: 'creating resource', resourceId }); - - return { ...resource, id: resourceId }; - } -} diff --git a/src/resourceName/routes/resourceNameRouter.ts b/src/resourceName/routes/resourceNameRouter.ts deleted file mode 100644 index d908906..0000000 --- a/src/resourceName/routes/resourceNameRouter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Router } from 'express'; -import { FactoryFunction } from 'tsyringe'; -import { ResourceNameController } from '../controllers/resourceNameController'; - -const resourceNameRouterFactory: FactoryFunction = (dependencyContainer) => { - const router = Router(); - const controller = dependencyContainer.resolve(ResourceNameController); - - router.get('/', controller.getResource); - router.post('/', controller.createResource); - - return router; -}; - -export const RESOURCE_NAME_ROUTER_SYMBOL = Symbol('resourceNameRouterFactory'); - -export { resourceNameRouterFactory }; diff --git a/src/serverBuilder.ts b/src/serverBuilder.ts index 0df4f55..e1552e8 100644 --- a/src/serverBuilder.ts +++ b/src/serverBuilder.ts @@ -1,18 +1,19 @@ -import express, { Router } from 'express'; -import bodyParser from 'body-parser'; -import compression from 'compression'; -import { OpenapiViewerRouter } from '@map-colonies/openapi-express-viewer'; import { getErrorHandlerMiddleware } from '@map-colonies/error-express-handler'; -import { middleware as OpenApiMiddleware } from 'express-openapi-validator'; -import { inject, injectable } from 'tsyringe'; -import type { Logger } from '@map-colonies/js-logger'; import { httpLogger } from '@map-colonies/express-access-log-middleware'; +import type { Logger } from '@map-colonies/js-logger'; +import { OpenapiViewerRouter } from '@map-colonies/openapi-express-viewer'; import { collectMetricsExpressMiddleware } from '@map-colonies/prometheus'; +import bodyParser from 'body-parser'; +import compression from 'compression'; +import express, { Router } from 'express'; +import { middleware as OpenApiMiddleware } from 'express-openapi-validator'; import { Registry } from 'prom-client'; +import { inject, injectable } from 'tsyringe'; import type { ConfigType } from '@common/config'; import { SERVICES } from '@common/constants'; -import { RESOURCE_NAME_ROUTER_SYMBOL } from './resourceName/routes/resourceNameRouter'; -import { ANOTHER_RESOURCE_ROUTER_SYMBOL } from './anotherResource/routes/anotherResourceRouter'; +import { addOperationIdToLog, logContextInjectionMiddleware } from '@common/logger'; +import { DEM_ROUTER_SYMBOL } from './dem/routes/demRouter'; +import { INFO_ROUTER_SYMBOL } from './info/routes/infoRouter'; @injectable() export class ServerBuilder { @@ -22,8 +23,8 @@ export class ServerBuilder { @inject(SERVICES.CONFIG) private readonly config: ConfigType, @inject(SERVICES.LOGGER) private readonly logger: Logger, @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry, - @inject(RESOURCE_NAME_ROUTER_SYMBOL) private readonly resourceNameRouter: Router, - @inject(ANOTHER_RESOURCE_ROUTER_SYMBOL) private readonly anotherResourceRouter: Router + @inject(DEM_ROUTER_SYMBOL) private readonly demRouter: Router, + @inject(INFO_ROUTER_SYMBOL) private readonly infoRouter: Router ) { this.serverInstance = express(); } @@ -46,14 +47,22 @@ export class ServerBuilder { } private buildRoutes(): void { - this.serverInstance.use('/resourceName', this.resourceNameRouter); - this.serverInstance.use('/anotherResource', this.anotherResourceRouter); + this.serverInstance.use('/dem', this.demRouter); + this.serverInstance.use('/info', this.infoRouter); this.buildDocsRoutes(); } private registerPreRoutesMiddleware(): void { + this.serverInstance.use(logContextInjectionMiddleware); this.serverInstance.use(collectMetricsExpressMiddleware({ registry: this.metricsRegistry })); - this.serverInstance.use(httpLogger({ logger: this.logger, ignorePaths: ['/metrics'] })); + this.serverInstance.use( + httpLogger({ + logger: this.logger, + ignorePaths: ['/metrics'], + customSuccessObject: addOperationIdToLog, + customErrorObject: (req, res, err, val) => addOperationIdToLog(req, res, val as Record), + }) + ); if (this.config.get('server.response.compression.enabled')) { this.serverInstance.use(compression(this.config.get('server.response.compression.options') as unknown as compression.CompressionFilter)); diff --git a/tests/configurations/initConfig.setup.ts b/tests/configurations/initConfig.setup.ts new file mode 100644 index 0000000..523c7b2 --- /dev/null +++ b/tests/configurations/initConfig.setup.ts @@ -0,0 +1,3 @@ +import { initConfig } from '@src/common/config'; + +void initConfig(true); diff --git a/tests/configurations/initCustomMatchers.setup.ts b/tests/configurations/initCustomMatchers.setup.ts new file mode 100644 index 0000000..1594057 --- /dev/null +++ b/tests/configurations/initCustomMatchers.setup.ts @@ -0,0 +1 @@ +import '../helpers/matchers/openApiSpec.matcher'; diff --git a/tests/configurations/initJestExtended.setup.ts b/tests/configurations/initJestExtended.setup.ts new file mode 100644 index 0000000..e6a82c6 --- /dev/null +++ b/tests/configurations/initJestExtended.setup.ts @@ -0,0 +1,4 @@ +import { expect } from 'vitest'; +import * as matchers from 'jest-extended'; + +expect.extend(matchers); diff --git a/tests/configurations/initJestOpenapi.setup.ts b/tests/configurations/initJestOpenapi.setup.ts index 72d9e9f..90465c7 100644 --- a/tests/configurations/initJestOpenapi.setup.ts +++ b/tests/configurations/initJestOpenapi.setup.ts @@ -1,12 +1,8 @@ /* eslint-disable */ -import path from 'node:path'; import { expect } from 'vitest'; -import jestOpenApi from 'jest-openapi'; //@ts-ignore globalThis.expect = expect; -jestOpenApi(path.join(process.cwd(), 'openapi3.yaml')); - //@ts-ignore globalThis.expect = undefined as any; // Reset global expect to avoid conflicts with other test frameworks diff --git a/tests/configurations/initZodSchemaFaker.setup.ts b/tests/configurations/initZodSchemaFaker.setup.ts new file mode 100644 index 0000000..ebf2a39 --- /dev/null +++ b/tests/configurations/initZodSchemaFaker.setup.ts @@ -0,0 +1,4 @@ +import { setFaker } from 'zod-schema-faker/v4'; +import { faker } from '@faker-js/faker'; + +setFaker(faker); diff --git a/tests/configurations/tmpFolder.setup.ts b/tests/configurations/tmpFolder.setup.ts new file mode 100644 index 0000000..e0e0014 --- /dev/null +++ b/tests/configurations/tmpFolder.setup.ts @@ -0,0 +1,10 @@ +import { existsSync, mkdirSync, rmSync } from 'node:fs'; +import { tmpDirPath } from '@tests/helpers/constants'; + +export default function setup() { + if (!existsSync(tmpDirPath)) mkdirSync(tmpDirPath); + + return function teardown(): void { + rmSync(tmpDirPath, { recursive: true, force: true }); + }; +} diff --git a/tests/helpers/constants.ts b/tests/helpers/constants.ts new file mode 100644 index 0000000..2631e93 --- /dev/null +++ b/tests/helpers/constants.ts @@ -0,0 +1,5 @@ +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { SERVICE_NAME } from '@src/common/constants'; + +export const tmpDirPath = join(tmpdir(), SERVICE_NAME); diff --git a/tests/helpers/faker/info.faker.ts b/tests/helpers/faker/info.faker.ts new file mode 100644 index 0000000..8af2590 --- /dev/null +++ b/tests/helpers/faker/info.faker.ts @@ -0,0 +1,49 @@ +import { z } from 'zod'; +import { fake } from 'zod-schema-faker/v4'; +import type { RASTER_DATA_TYPES } from '@src/common/constants'; +import { + areaOrPointSchema, + noDataValueSchema, + pixelDataTypesSchema, + resolutionDegreeSchema, + resolutionMeterSchema, + srsIdSchema, + srsNameSchema, +} from '@src/common/schemas'; +import { createGDALGeotiffCOGRaster, type CreateGDALRasterOptions } from '../generators/gdal'; +import type { DemType, InfoRequestBody, InfoResponse } from '../interfaces'; +import { createGDALRasterMetadata } from './rasterMetadata.faker'; + +export const createInfoMetadata = (options: DemType): CreateGDALRasterOptions => { + const { demType } = options; + + const rasterMetadata = createGDALRasterMetadata({ demType }); + return rasterMetadata; +}; + +export const createInfoResource = async (options: CreateGDALRasterOptions): Promise> => { + let demFilePath: string; + switch (options.driverName) { + case 'gtiff': { + demFilePath = await createGDALGeotiffCOGRaster(options); + break; + } + default: { + throw new Error(`Unsupported driver '${options.driverName}' for info resource creation`); + } + } + return { demFilePath }; +}; + +export const generateInfoResponse = (format: keyof typeof RASTER_DATA_TYPES): InfoResponse => { + const infoResponseSchema = z.strictObject({ + areaOrPoint: areaOrPointSchema, + resolutionDegree: resolutionDegreeSchema, + resolutionMeter: resolutionMeterSchema, + srsId: srsIdSchema, + srsName: srsNameSchema, + dataType: pixelDataTypesSchema(format), + noDataValue: noDataValueSchema, + }); + return fake(infoResponseSchema); +}; diff --git a/tests/helpers/faker/inputFiles.faker.ts b/tests/helpers/faker/inputFiles.faker.ts new file mode 100644 index 0000000..8baa279 --- /dev/null +++ b/tests/helpers/faker/inputFiles.faker.ts @@ -0,0 +1,17 @@ +import { merge } from 'lodash'; +import { fake } from 'zod-schema-faker/v4'; +import { z } from 'zod/v4'; +import type { paths } from '@openapi'; + +// TODO: get schema from dem-shared +const demFilePathSchema = z.strictObject({ + demFilePath: z.string().regex(new RegExp('^(\\/?[\\w-]+)(\\/[\\w-]+)*\\/[\\wא-ת\\.-]+\\.(tif)$')), // TODO: extract regex pattern to dem-shared +}); + +export type DemFilePath = Pick; + +export const createDemFilePath = (overrides: Partial = {}): DemFilePath => { + const demFilePath = fake(demFilePathSchema) satisfies DemFilePath; + + return merge(demFilePath, overrides); +}; diff --git a/tests/helpers/faker/rasterMetadata.faker.ts b/tests/helpers/faker/rasterMetadata.faker.ts new file mode 100644 index 0000000..9bc7817 --- /dev/null +++ b/tests/helpers/faker/rasterMetadata.faker.ts @@ -0,0 +1,240 @@ +/* eslint-disable @typescript-eslint/no-magic-numbers */ +import { faker } from '@faker-js/faker'; +import { drivers, SpatialReference } from 'gdal-async'; +import { merge } from 'lodash'; +import { z } from 'zod'; +import { fake } from 'zod-schema-faker/v4'; +import { getConfig } from '@src/common/config'; +import { RASTER_DATA_TYPES } from '@src/common/constants'; +import { getResolutions, getSrsGeographicBounds, getSrsName, transformPoint } from '@src/common/gdal'; +import { type PixelDataType } from '@src/common/interfaces'; +import { + areaOrPointSchema, + blockSizeSchema, + compressionSchema, + layoutSchema, + overviewsCountSchema, + resolutionDegreeSchema, + srsIdSchema, +} from '@src/common/schemas'; +import type { CreateGDALRasterOptions } from '@tests/helpers/generators/gdal'; +import type { DemType, InfoGeoTiff } from '@tests/helpers/interfaces'; + +const MAX_FLOAT32 = (2 - Math.pow(2, -23)) * Math.pow(2, 127); +const MAX_INT64 = 2n ** 63n; +const MAX_PIXELS_DIM = 10; // keep as low number of pixels for quicker performance in tests + +const config = getConfig(); +const supportedFormatsMap: Record = config.get('application.supportedFormatsMap'); + +const regularGridMetadata = z.strictObject({ + areaOrPoint: areaOrPointSchema, + srsId: srsIdSchema, +}); + +const geotiffMetadataSchema = z.strictObject({ + ...regularGridMetadata.shape, +}); + +const geotiffGDALMetadataSchema = z.strictObject({ + ...geotiffMetadataSchema.shape, + blockSize: blockSizeSchema.shape.x, + compression: compressionSchema, + layout: layoutSchema, +}); + +const createPixelDataTypeValue = (dataType: PixelDataType): number => { + switch (dataType) { + // case 'Byte': { + // const value = faker.number.int({ min: 0, max: 255 }); + // return Number(new Uint8Array([value])[0]); + // } + // case 'CInt16': { + // const [value1, value2] = [faker.number.int({ min: -32768, max: 32767 }), faker.number.int({ min: -32768, max: 32767 })]; + // return Number(new Int16Array([value1, value2])[0]); + // } + // case 'CInt32': { + // const [value1, value2] = [faker.number.int({ min: -2147483648, max: 2147483647 }), faker.number.int({ min: -2147483648, max: 2147483647 })]; + // return Number(new Int32Array([value1, value2])[0]); + // } + // case 'CFloat16': { + // const [value1, value2] = [faker.number.float({ min: -65504, max: 65504 }), faker.number.float({ min: -65504, max: 65504 })]; + // return Number(new Float16Array([value1, value2])[0]); + // } + // case 'CFloat32': { + // const [value1, value2] = [ + // faker.number.float({ min: -MAX_FLOAT32, max: MAX_FLOAT32 }), + // faker.number.float({ min: -MAX_FLOAT32, max: MAX_FLOAT32 }), + // ]; + // return Number(new Float32Array([value1, value2])[0]); + // } + // case 'CFloat64': { + // const [value1, value2] = [ + // faker.number.float({ min: -Number.MAX_VALUE, max: Number.MAX_VALUE }), + // faker.number.float({ min: -Number.MAX_VALUE, max: Number.MAX_VALUE }), + // ]; + // return Number(new Float64Array([value1, value2])[0]); + // } + case 'Int8': { + const value = faker.number.int({ min: -128, max: 127 }); + return Number(new Int8Array([value])[0]); + } + case 'Int16': { + const value = faker.number.int({ min: -32768, max: 32767 }); + return Number(new Int16Array([value])[0]); + } + case 'Int32': { + const value = faker.number.int({ min: -2147483648, max: 2147483647 }); + return Number(new Int32Array([value])[0]); + } + case 'Int64': { + const value = faker.number.bigInt({ min: -MAX_INT64, max: MAX_INT64 - 1n }); + return Number(new BigInt64Array([BigInt(value)])[0]); + } + // case 'UInt16': { + // const value = faker.number.int({ min: 0, max: 65535 }); + // return Number(new Uint16Array([value])[0]); + // } + // case 'UInt32': { + // const value = faker.number.int({ min: 0, max: 4294967295 }); + // return Number(new Uint32Array([value])[0]); + // } + // case 'UInt64': { + // const value = faker.number.bigInt({ min: 0n }); + // return Number(new BigUint64Array([BigInt(value)])[0]); + // } + case 'Float16': { + const value = faker.number.float({ min: -65504, max: 65504 }); + return Number(new Float16Array([value])[0]); + } + case 'Float32': { + const value = faker.number.float({ min: -MAX_FLOAT32, max: MAX_FLOAT32 }); + return Number(new Float32Array([value])[0]); + } + case 'Float64': { + // faker overflows to infinity in inner calculations thats we get half the range and then decide on the sign + const value = faker.helpers.arrayElement([1, -1]) * faker.number.float({ min: 0, max: Number.MAX_VALUE }); + return Number(new Float64Array([value])[0]); + } + default: + throw new Error('Unsupported pixel data type'); + } +}; + +export const createGDALRasterMetadata = (options: Partial & DemType): CreateGDALRasterOptions => { + const { demType, ...overrides } = options; + const driverName = supportedFormatsMap[demType]; + const supportedDataTypes = RASTER_DATA_TYPES[demType]; + if (driverName === undefined) throw new Error(`Unsupported dem type: ${demType}`); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const driverMetadata = drivers.get(driverName).getMetadata() as unknown as { DMD_CREATIONDATATYPES: string }; + + const driverPixelDataTypes = driverMetadata.DMD_CREATIONDATATYPES.split(' ').filter((driverPixelDataType): driverPixelDataType is PixelDataType => + (supportedDataTypes as string[]).includes(driverPixelDataType) + ); + if (driverPixelDataTypes.length === 0) throw new Error(`Unsupported data types for dem type: ${demType}`); + const dataType = faker.helpers.arrayElement(driverPixelDataTypes); + const fakePixelDataTypeValue = createPixelDataTypeValue(dataType); + const noDataValue = faker.helpers.arrayElement([fakePixelDataTypeValue, Number.NaN]); + + // TODO: adjust to support not only geotiff/COG + const rasterMetadata = fake(geotiffGDALMetadataSchema) satisfies Omit< + CreateGDALRasterOptions, + | 'dataType' + | 'driverName' + | 'maxX' + | 'maxY' + | 'minX' + | 'minY' + | 'noDataValue' + | 'overviewsCount' + | 'pixelHeight' + | 'pixelWidth' + | 'resolutionDegree' + | 'resolutionMeter' + | 'srsName' + | 'xSize' + | 'ySize' + >; + + const sourceSrs = SpatialReference.fromEPSG(4326); // source bbox is in WGS84 (EPSG:4326) + const targetSrs = SpatialReference.fromEPSG(rasterMetadata.srsId); + + const resolutionDegree = fake(resolutionDegreeSchema); + const [geoSrsMinX, geoSrsMinY, geoSrsMaxX, geoSrsMaxY] = getSrsGeographicBounds({ srsId: rasterMetadata.srsId }); + const [pixelsX, pixelsY] = [ + Math.max(1, Math.min(MAX_PIXELS_DIM, Math.floor((geoSrsMaxX - geoSrsMinX) / resolutionDegree))), + Math.max(1, Math.min(MAX_PIXELS_DIM, Math.floor((geoSrsMaxY - geoSrsMinY) / resolutionDegree))), + ]; + const [geoWidth, geoHeight] = [pixelsX * resolutionDegree, pixelsY * resolutionDegree]; + const [geoMinX, geoMinY] = [ + faker.number.float({ min: geoSrsMinX, max: geoSrsMaxX - geoWidth }), + faker.number.float({ min: geoSrsMinY, max: geoSrsMaxY - geoHeight }), + ]; + const [geoMaxX, geoMaxY] = [geoMinX + geoWidth, geoMinY + geoHeight]; + + const [{ x: minX }, { x: maxX }, { y: minY }, { y: maxY }] = [ + transformPoint({ + point: { x: geoMinX, y: (geoMinY + geoMaxY) / 2 }, + sourceSrs, + targetSrs, + }), + transformPoint({ + point: { x: geoMaxX, y: (geoMinY + geoMaxY) / 2 }, + sourceSrs, + targetSrs, + }), + transformPoint({ + point: { x: (geoMinX + geoMaxX) / 2, y: geoMinY }, + sourceSrs, + targetSrs, + }), + transformPoint({ + point: { x: (geoMinX + geoMaxX) / 2, y: geoMaxY }, + sourceSrs, + targetSrs, + }), + ]; + + const pixelHeight = Math.abs(maxY - minY) / pixelsY; + const pixelWidth = Math.abs(maxX - minX) / pixelsX; + + const overviewsCount = fake( + overviewsCountSchema({ + blockSize: { x: rasterMetadata.blockSize, y: rasterMetadata.blockSize }, + size: { x: pixelsX, y: pixelsY }, + }) + ); + + const resolutions = getResolutions({ + maxX, + maxY, + minX, + minY, + pixelHeight, + pixelWidth, + sourceSrs: rasterMetadata.srsId, + }); + + return merge( + rasterMetadata, + { + ...resolutions, + driverName, + dataType, + maxX, + maxY, + minX, + minY, + noDataValue: Number.isNaN(noDataValue) ? 'NaN' : noDataValue, + overviewsCount, + pixelHeight, + pixelWidth, + srsName: getSrsName(rasterMetadata.srsId), + xSize: pixelsX, + ySize: pixelsY, + }, + overrides + ); +}; diff --git a/tests/helpers/generators/gdal.ts b/tests/helpers/generators/gdal.ts new file mode 100644 index 0000000..d3aee24 --- /dev/null +++ b/tests/helpers/generators/gdal.ts @@ -0,0 +1,77 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { join } from 'node:path'; +import { faker } from '@faker-js/faker'; +import { drivers, SpatialReference, type StringOptions } from 'gdal-async'; +import type { z } from 'zod'; +import type { blockSizeSchema, compressionSchema, layoutSchema, overviewsCountSchema } from '@src/common/schemas'; +import { tmpDirPath } from '../constants'; +import type { InfoGeoTiff } from '../interfaces'; + +interface GDALCreationOptions { + driverName: string; + xSize: number; + ySize: number; + blockSize: z.infer['x']; + compression: z.infer; + layout: z.infer; + overviewsCount: z.infer>; + creationOptions?: StringOptions; +} + +const BAND_COUNT = 1; + +export type CreateGDALRasterOptions = GDALCreationOptions & + Omit & { + minX: number; + minY: number; + maxX: number; + maxY: number; + noDataValue: number | 'NaN'; // NaN is provided as string literal since JSON response cannot encode NaN numberically + pixelWidth: number; + pixelHeight: number; + }; + +/** + * @returns Path to the COG geotiff + */ +export const createGDALGeotiffCOGRaster = async (options: CreateGDALRasterOptions): Promise => { + const { + areaOrPoint, + blockSize, + compression, + dataType, + layout, + maxY, + minX, + noDataValue, + overviewsCount, + pixelHeight, + pixelWidth, + srsId, + creationOptions = {}, + xSize, + ySize, + } = options; + + const driverGeoTiff = drivers.get('MEM'); + const filePath = join(tmpDirPath, faker.system.commonFileName('tif')); + + const dataset = await driverGeoTiff.createAsync('', xSize, ySize, BAND_COUNT, dataType, creationOptions); + + const srs = SpatialReference.fromEPSG(srsId); + dataset.srs = srs; + dataset.geoTransform = [minX, pixelWidth, 0, maxY, 0, -pixelHeight]; + dataset.bands.forEach((band) => { + band.noDataValue = Number(noDataValue); + }); + dataset.setMetadata({ AREA_OR_POINT: areaOrPoint }); + await dataset.buildOverviewsAsync('NEAREST', [overviewsCount]); + + const cogDriver = drivers.get('COG'); + const cogOptions = { LAYOUT: layout, COMPRESS: compression, BLOCKSIZE: blockSize }; + + const finalDs = await cogDriver.createCopyAsync(filePath, dataset, cogOptions, false); + finalDs.close(); + dataset.close(); + return filePath; +}; diff --git a/tests/helpers/interfaces.ts b/tests/helpers/interfaces.ts new file mode 100644 index 0000000..75333a7 --- /dev/null +++ b/tests/helpers/interfaces.ts @@ -0,0 +1,10 @@ +import type { RasterFormats } from '@src/common/interfaces'; +import type { components, paths } from '@src/openapi'; + +export type InfoGeoTiff = components['schemas']['InfoGeoTiff']; +export type InfoResponse = paths['/info']['post']['responses']['200']['content']['application/json']; +export type InfoRequestBody = paths['/info']['post']['requestBody']['content']['application/json']; + +export interface DemType { + demType: RasterFormats; +} diff --git a/tests/helpers/matchers/index.d.ts b/tests/helpers/matchers/index.d.ts new file mode 100644 index 0000000..e30d796 --- /dev/null +++ b/tests/helpers/matchers/index.d.ts @@ -0,0 +1,7 @@ +import 'vitest'; + +declare module 'vitest' { + interface Matchers extends CustomMatchers { + toSatisfyApiSpec: () => T; + } +} diff --git a/tests/helpers/matchers/openApiSpec.matcher.ts b/tests/helpers/matchers/openApiSpec.matcher.ts new file mode 100644 index 0000000..12d93d5 --- /dev/null +++ b/tests/helpers/matchers/openApiSpec.matcher.ts @@ -0,0 +1,229 @@ +import { resolve } from 'node:path'; +import type { RequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import { dereference } from '@readme/openapi-parser'; +import addFormats from 'ajv-formats'; +import { Ajv2020, type ErrorObject } from 'ajv/dist/2020'; +import type { OpenAPIV3_1 } from 'openapi-types'; +import { beforeAll, expect } from 'vitest'; +import type { operations, paths } from '@src/openapi'; +import { matchRoute } from '../setupOpenApiSpec'; + +type ExpectationResult = ReturnType; +type MatcherState = ReturnType<(typeof expect)['getState']>; +type RawMatcherFn = Parameters<(typeof expect)['extend']>[0][number]; +type SupertestResponse = Awaited['sendRequest']>>; +type TestContext = { matcherName: string } & Pick & MatcherState['utils']; + +const strictAjv = new Ajv2020({ + allErrors: true, + discriminator: true, + strict: true, + strictRequired: false, + useDefaults: true, +}); +addFormats(strictAjv); + +const coerciveAjv = new Ajv2020({ + allErrors: true, + coerceTypes: true, + useDefaults: true, +}); +addFormats(coerciveAjv); + +const getHintedErrorOnProperty = ( + { received, property, type }: { received: unknown; property: string; type: string }, + { + isNot, + matcherHint, + matcherName, + // eslint-disable-next-line @typescript-eslint/naming-convention + RECEIVED_COLOR, + printReceived, + printWithType, + }: TestContext +): string => + matcherErrorMessage( + matcherHint(matcherName, 'response', 'expected', { isNot }), + `${RECEIVED_COLOR('received')} must have '${property}' property with '${type}' type`, + printWithType('Received', received, printReceived) + ); + +const isSupertestResponse = (received: unknown, context: TestContext): received is SupertestResponse => { + const { isNot, matcherHint, matcherName, RECEIVED_COLOR, printReceived, printWithType } = context; + if (typeof received !== 'object' || received === null) { + throw new TypeError( + matcherErrorMessage( + matcherHint(matcherName, 'object', 'expected', { isNot }), + `${RECEIVED_COLOR('Received:')} ${printReceived(received)}`, + printWithType('Received', received, printReceived) + ) + ); + } + + if (!('status' in received && typeof received.status === 'number')) { + throw new TypeError(getHintedErrorOnProperty({ received, property: 'status', type: 'number' }, context)); + } + + if (!('type' in received && typeof received.type === 'string')) { + throw new TypeError(getHintedErrorOnProperty({ received, property: 'type', type: 'string' }, context)); + } + + if (!('request' in received && typeof received.request === 'object' && received.request !== null)) { + throw new TypeError(getHintedErrorOnProperty({ received, property: 'request', type: 'object' }, context)); + } + + const request = received.request; + + if (!('method' in request && typeof request.method === 'string')) { + throw new TypeError(getHintedErrorOnProperty({ received, property: 'request.method', type: 'string' }, context)); + } + + if (!('url' in request && typeof request.method === 'string')) { + throw new TypeError(getHintedErrorOnProperty({ received, property: 'request.url', type: 'string' }, context)); + } + + return true; +}; + +const parseOpenApi = async (options?: { openApiDoc: string }): Promise => { + const { openApiDoc } = options ?? { openApiDoc: 'openapi3.yaml' }; + const specPath = resolve(process.cwd(), openApiDoc); + const openApiDocument = await dereference(specPath); + return openApiDocument; +}; + +const findSchemaInSpec = ( + openApiDocument: OpenAPIV3_1.Document, + path: string, + method: string, + status: number, + media?: string +): Pick & Pick => { + if (openApiDocument.paths?.[path] === undefined) { + throw new Error(`Path '${path}' not found in spec`); + } + + const pathItem = openApiDocument.paths[path]; + const operation = pathItem[method.toLowerCase() as OpenAPIV3_1.HttpMethods]; + + if (!operation) { + throw new Error(`Method '${method}' not defined for '${path}'`); + } + + const response = operation.responses[String(status)]; + + if (!response) throw new Error(`Could not find a matching response for '${method} ${path} ${status}'`); + if ('$ref' in response) throw new Error('Responses should have been dereferenced'); + + const schema = media !== undefined ? response.content?.[media]?.schema : undefined; + const headers = response.headers; + + return { schema, headers }; +}; + +const extractRequestInfo = (received: SupertestResponse): { mediaType: string; method: string; path: string } => { + const { + type: mediaType, + request: { method, url }, + } = received; + const path = new URL(url).pathname; + + return { mediaType, method, path }; +}; + +const matcherErrorMessage = (...args: string[]): string => args.join('\n'); + +const toSatisfyApiSpecFactory = (openApiDocument: OpenAPIV3_1.Document): RawMatcherFn => { + function toSatisfyApiSpec(this: MatcherState, received: unknown): ExpectationResult { + const matcherName = 'toSatisfyApiSpec'; + const { isNot, utils } = this; + const { matcherHint, printReceived, RECEIVED_COLOR } = utils; + + try { + if (!isSupertestResponse(received, { ...utils, isNot, matcherName })) throw new Error(); + const { mediaType: requestMediaType, method: requestMethod, path: requestPath } = extractRequestInfo(received); + const status = received.status; + + // Find the matching OpenAPI path (handles path parameters like {id}) + let matchedOpenApiPath: string | undefined; + if (openApiDocument.paths) { + matchedOpenApiPath = Object.keys(openApiDocument.paths).find((openApiPath) => matchRoute(openApiPath, requestPath)); + } + + if (matchedOpenApiPath === undefined) { + throw new Error(`No matching path found in OpenAPI spec for request path: '${requestPath}'`); + } + + // Find and validate the schema + const { schema, headers } = findSchemaInSpec(openApiDocument, matchedOpenApiPath, requestMethod, status, requestMediaType); + let passSchema = true; + let passHeaders = true; + let schemaErrors: ErrorObject[] | null | undefined; + let headersErrors: ErrorObject[] | null | undefined; + + if (schema && 'body' in received) { + const validateSchema = strictAjv.compile(schema); + passSchema = validateSchema(received.body); + schemaErrors = validateSchema.errors; + } + + if (headers && 'headers' in received) { + const headersSchema = { + type: 'object', + properties: Object.fromEntries( + Object.entries(headers as { [header: string]: OpenAPIV3_1.HeaderObject }).map(([key, value]) => { + if (!(value.schema || value.content?.[requestMediaType]?.schema)) { + throw new Error(`OpenAPI spec headers should contain either 'content' or 'schema'`); + } + return [key.toLowerCase(), value.schema ?? value.content?.[requestMediaType]?.schema]; + }) + ), + required: Object.entries(headers as { [header: string]: OpenAPIV3_1.HeaderObject }) + .filter(([, value]) => value.required === true) + .map(([key]) => key.toLowerCase()), + additionalProperties: true, // Accept extra headers that are typically undeclared + }; + const validateHeaders = coerciveAjv.compile(headersSchema); + passHeaders = validateHeaders(received.headers); + headersErrors = validateHeaders.errors; + } + + const pass = passSchema && passHeaders; + const errors = [ + !passSchema && strictAjv.errorsText(schemaErrors, { dataVar: 'response.body' }), + !passHeaders && coerciveAjv.errorsText(headersErrors, { dataVar: 'response.headers' }), + ] + .filter(Boolean) + .join(';'); + + return { + pass, + message: (): string => + pass + ? `Expected response not to match OpenAPI spec for ${requestMethod.toUpperCase()} ${matchedOpenApiPath} ${status}` + : `OAS 3.1 Validation Error for ${requestMethod.toUpperCase()} ${matchedOpenApiPath} ${status}: ${errors}`, + }; + } catch (error: unknown) { + return { + pass: false, + message: (): string => `${matcherHint(matcherName, 'received', '', { isNot })} + Error: ${error instanceof Error ? error.message : JSON.stringify(error)} + ${RECEIVED_COLOR('Received:')} ${printReceived(received)}`, + }; + } + } + return toSatisfyApiSpec; +}; + +beforeAll(async () => { + try { + const openApiDocument = await parseOpenApi(); + const toSatisfyApiSpec = toSatisfyApiSpecFactory(openApiDocument); + expect.extend({ + toSatisfyApiSpec, + }); + } catch (err) { + console.error(err); + throw err; + } +}); diff --git a/tests/helpers/setupOpenApiSpec.ts b/tests/helpers/setupOpenApiSpec.ts new file mode 100644 index 0000000..affe1c5 --- /dev/null +++ b/tests/helpers/setupOpenApiSpec.ts @@ -0,0 +1,66 @@ +import { resolve } from 'node:path'; +import { readFileSync } from 'node:fs'; +import type { OpenAPIV3_1 } from 'openapi-types'; +import { dereference } from '@readme/openapi-parser'; + +// eslint-disable-next-line no-useless-escape +const urlTemplateAllowedValues = `([A-Za-z0-9\-\._~]|%[0-9A-Fa-f]{2}|[!$&'()*+,;=]|[:@\/])*`; + +export type RemoveNever = { + [K in keyof T as T[K] extends undefined ? never : K]: T[K]; +}; +export type OasMethodsOfPath = keyof Omit, 'parameters'>; +export type OasOperationOfMethod> = P[T][M]; +export type OasStatusCodesOfMethod> = + OasOperationOfMethod extends Record ? keyof OasOperationOfMethod['responses'] : never; + +// @readme/openapi-parser +export type OpenApiDocument = Awaited>; +export type Paths = NonNullable; +export type Path = NonNullable; +export type PathKeys = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch'; +export type Method = NonNullable; +export type Responses = Method['responses']; +export type Response = NonNullable; +export type Content = NonNullable['content']>; +export type MediaType = Content[number | string]; +export type Schema = MediaType['schema']; + +/** + * Basic route matcher: converts "/users/{id}" in spec to regex + */ +export function matchRoute(openapiPath: string, actualPath: string): boolean { + const regexPath = openapiPath.replace(/{.+}/g, urlTemplateAllowedValues); + + return new RegExp(`^${regexPath}$`).test(actualPath); +} + +export const parseOpenApi = async (options?: { openApiDoc: string }): Promise => { + const { openApiDoc } = options ?? { openApiDoc: 'openapi3.yaml' }; + const specPath = resolve(process.cwd(), openApiDoc); + const spec = readFileSync(specPath, 'utf8'); + const openApiDocument = await dereference(spec); + return openApiDocument; +}; + +export function findSchemaInSpec(spec: OpenAPIV3_1.Document, path: string, method: string, status: number): OpenAPIV3_1.SchemaObject { + if (spec.paths?.[path] === undefined) { + throw new Error(`Path '${path}' not found in spec.`); + } + + const pathItem = spec.paths[path]; + const operation = pathItem[method.toLowerCase() as OpenAPIV3_1.HttpMethods]; + + if (!operation) { + throw new Error(`Method '${method}' not defined for '${path}'.`); + } + + const response = operation.responses[String(status)] as OpenAPIV3_1.ResponseObject; + const content = response.content?.['application/json']; + + if (!content?.schema) { + throw new Error(`No JSON schema found for '${method} ${path} ${status}'.`); + } + + return content.schema; +} diff --git a/tests/integration/anotherResource/anotherResourceName.spec.ts b/tests/integration/anotherResource/anotherResourceName.spec.ts deleted file mode 100644 index 4755f83..0000000 --- a/tests/integration/anotherResource/anotherResourceName.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { jsLogger } from '@map-colonies/js-logger'; -import { describe, beforeEach, it, expect, beforeAll } from 'vitest'; -import { trace } from '@opentelemetry/api'; -import httpStatusCodes from 'http-status-codes'; -import { createRequestSender, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { paths, operations } from '@openapi'; -import { getApp } from '@src/app'; -import { SERVICES } from '@src/common/constants'; -import { initConfig } from '@src/common/config'; - -describe('anotherResourceName', function () { - let requestSender: RequestSender; - - beforeAll(async function () { - await initConfig(true); - }); - - beforeEach(async function () { - const [app] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender('openapi3.yaml', app); - }); - - describe('Happy Path', function () { - it('should return 200 status code and the resource', async function () { - const response = await requestSender.getAnotherResource(); - - expect(response.status).toBe(httpStatusCodes.OK); - expect(response).toSatisfyApiSpec(); - - const resource = response.body as paths['/anotherResource']['get']['responses'][200]['content']['application/json']; - - expect(resource.kind).toBe('avi'); - expect(resource.isAlive).toBe(false); - }); - }); - - describe('Bad Path', function () { - // All requests with status code of 400 - it('should in theory test 400 status code', function () { - expect(true).toBe(true); - }); - }); - - describe('Sad Path', function () { - // All requests with status code 4XX-5XX - it('should in theory test 500 status code', function () { - expect(true).toBe(true); - }); - }); -}); diff --git a/tests/integration/docs/docs.spec.ts b/tests/integration/docs/docs.spec.ts index 95e11b0..dce1eeb 100644 --- a/tests/integration/docs/docs.spec.ts +++ b/tests/integration/docs/docs.spec.ts @@ -17,7 +17,7 @@ describe('docs', function () { beforeEach(async function () { const [app] = await getApp({ override: [ - { token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } }, + { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, ], useChild: true, diff --git a/tests/integration/info/info.spec.ts b/tests/integration/info/info.spec.ts new file mode 100644 index 0000000..f9e4796 --- /dev/null +++ b/tests/integration/info/info.spec.ts @@ -0,0 +1,709 @@ +import * as nodeFsPromise from 'node:fs/promises'; +import { faker } from '@faker-js/faker'; +import { jsLogger } from '@map-colonies/js-logger'; +import { createRequestSender, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; +import { trace } from '@opentelemetry/api'; +import * as gdalAsync from 'gdal-async'; +import { Dataset, DatasetBands, Driver } from 'gdal-async'; +import httpStatusCodes from 'http-status-codes'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { operations, paths } from '@openapi'; +import { getApp } from '@src/app'; +import { getConfig } from '@src/common/config'; +import { RASTER_DATA_TYPES, SERVICES } from '@src/common/constants'; +import { hasKey } from '@src/common/utils'; +import { createInfoMetadata, createInfoResource } from '@tests/helpers/faker/info.faker'; + +vi.mock('node:fs/promises', { spy: true }); + +const seed = process.env.TEST_SEED ?? Math.floor(Math.random() * 1000000); +faker.seed(Number(seed)); +console.info(`Test seed: ${seed}`); + +const config = getConfig(); + +const supportedFormatsMap: Record = config.get('application.supportedFormatsMap'); + +const happyTests = Object.keys(supportedFormatsMap).map((supportedFormat) => { + if (!hasKey(supportedFormat, RASTER_DATA_TYPES)) { + throw new Error(`Format '${supportedFormat}' is not part of service's API`); + } + return { demType: supportedFormat }; +}); + +describe('POST /info', () => { + type InfoRequestBody = paths['/info']['post']['requestBody']['content']['application/json']; + let requestSender: RequestSender; + + beforeEach(async () => { + const [app] = await getApp({ + override: [ + { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, + { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, + ], + useChild: true, + }); + requestSender = await createRequestSender('openapi3.yaml', app); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('Happy Path', () => { + it.for(happyTests)('should return 200 status code and respond with dem info for $demType', async ({ demType }) => { + const metadata = createInfoMetadata({ demType }); + const demFilePath = await createInfoResource(metadata); + const { areaOrPoint, resolutionDegree, resolutionMeter, srsId, srsName, dataType, noDataValue } = metadata; + const expected = { areaOrPoint, resolutionDegree, resolutionMeter, srsId, srsName, dataType, noDataValue }; + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + expect(response.body).toStrictEqual(expected); + expect.assertions(3); + }); + }); + + describe('Bad Path', () => { + type InfoResponseBody = paths['/info']['post']['responses'][400]['content']['application/json']; + + it('should return 400 status code and respond with error message when request body is incorrect', async () => { + const response = await requestSender.info({ requestBody: false as unknown as InfoRequestBody }); + + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBody).message).toBe('Unexpected token \'f\', "false" is not valid JSON'); + expect.assertions(3); + }); + + it('should return 400 status code and respond with error message when request body has redundant properties', async () => { + const response = await requestSender.info({ + requestBody: { demFilePath: '/path/to/tif.tif', redundantProperty: '' } as unknown as InfoRequestBody, + }); + + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBody).message).toBe('request/body must NOT have unevaluated properties'); + expect.assertions(3); + }); + + it('should return 400 status code and respond with error message when demFilePath property in request body has incorrect type', async () => { + const response = await requestSender.info({ + requestBody: { demFilePath: 0 } as unknown as InfoRequestBody, + }); + + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBody).message).toBe('request/body/demFilePath must be string'); + expect.assertions(3); + }); + + it('should return 400 status code and respond with error message when demFilePath property in request body does not follows validation pattern - file in root dir', async () => { + const response = await requestSender.info({ + requestBody: { demFilePath: '/file.tif' }, + }); + + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBody).message).toBe( + 'request/body/demFilePath must match pattern "^(\\/?[\\w-]+)(\\/[\\w-]+)*\\/[\\wא-ת\\.-]+\\.(tif)$"' + ); + expect.assertions(3); + }); + + it('should return 400 status code and respond with error message when demFilePath property in request body does not follows validation pattern - path contains invalid characters', async () => { + const response = await requestSender.info({ + requestBody: { demFilePath: '/fold r/file.tif' }, + }); + + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBody).message).toBe( + 'request/body/demFilePath must match pattern "^(\\/?[\\w-]+)(\\/[\\w-]+)*\\/[\\wא-ת\\.-]+\\.(tif)$"' + ); + expect.assertions(3); + }); + + it('should return 400 status code and respond with error message when demFilePath property in request body does not follows validation pattern - incorrect extension', async () => { + const response = await requestSender.info({ + requestBody: { demFilePath: '/folder/file.tiff' }, + }); + + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBody).message).toBe( + 'request/body/demFilePath must match pattern "^(\\/?[\\w-]+)(\\/[\\w-]+)*\\/[\\wא-ת\\.-]+\\.(tif)$"' + ); + expect.assertions(3); + }); + }); + + describe('Sad Path', () => { + type InfoResponseBodyNotFound = paths['/info']['post']['responses'][404]['content']['application/json']; + + it('should return 404 status code and respond with unsuccessful message when file does not exist', async () => { + const demFilePath = '/non/existent/file.tif'; + const response = await requestSender.info({ requestBody: { demFilePath } }); + + expect(response.status).toBe(httpStatusCodes.NOT_FOUND); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyNotFound).message).toStartWith(`Cannot find file: ${demFilePath}. got error:`); + expect.assertions(3); + }); + + it('should return 404 status code and respond with unsuccessful message when file cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + vi.mocked(nodeFsPromise.access).mockRejectedValueOnce(new Error()); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.NOT_FOUND); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyNotFound).message).toStartWith(`Cannot find file: ${demFilePath.demFilePath}. got error:`); + expect.assertions(3); + }); + + type InfoResponseBodyUnprocessableContent = paths['/info']['post']['responses'][422]['content']['application/json']; + + it('should return 422 status code and respond with unsuccessful message when supported extensions do not include provided file', async () => { + // eslint-disable-next-line @typescript-eslint/naming-convention + vi.spyOn(Driver.prototype, 'getMetadata').mockReturnValueOnce({ DMD_EXTENSION: 'tiff' }); + + const response = await requestSender.info({ requestBody: { demFilePath: '/non/existent/file.tif' } }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe('No handler found for file: /non/existent/file.tif'); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when layout in image structure metadata is incorrect', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported image structure metadata (LAYOUT and COMPRESSION)'; + vi.spyOn(gdalAsync.Dataset.prototype, 'getMetadataAsync') + // eslint-disable-next-line @typescript-eslint/naming-convention + .mockResolvedValueOnce({ LAYOUT: '', COMPRESSION: metadata.compression }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when compression image structure metadata is incorrect', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported image structure metadata (LAYOUT and COMPRESSION)'; + vi.spyOn(gdalAsync.Dataset.prototype, 'getMetadataAsync') + // eslint-disable-next-line @typescript-eslint/naming-convention + .mockResolvedValueOnce({ LAYOUT: metadata.layout, COMPRESSION: '' }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }, 100000000); + + it('should return 422 status code and respond with unsuccessful message when block size is incorrect', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported block size'; + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset.bands, 'getAsync').mockImplementationOnce(async (...args: Parameters) => { + const band = await dataset.bands.getAsync(...args); + vi.spyOn(band, 'blockSizeAsync', 'get').mockImplementationOnce(async () => { + return Promise.resolve({ x: -1, y: 5 }); + }); + return band; + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when overviews count is incorrect', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Could not find overviews'; + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset.bands, 'getAsync').mockImplementationOnce(async (...args: Parameters) => { + const band = await dataset.bands.getAsync(...args); + vi.spyOn(band.overviews, 'countAsync').mockImplementationOnce(async () => { + return Promise.resolve(0); + }); + return band; + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when area or point metadata is incorrect', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Could not extract AREA_OR_POINT metadata'; + vi.spyOn(gdalAsync.Dataset.prototype, 'getMetadataAsync') + // eslint-disable-next-line @typescript-eslint/naming-convention + .mockResolvedValueOnce({ LAYOUT: metadata.layout, COMPRESSION: metadata.compression }) + // eslint-disable-next-line @typescript-eslint/naming-convention + .mockResolvedValueOnce({ AREA_OR_POINT: '' }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when band data type is incorrect', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported band data type'; + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset.bands, 'getAsync').mockImplementationOnce(async (...args: Parameters) => { + const band = await dataset.bands.getAsync(...args); + vi.spyOn(band, 'dataTypeAsync', 'get').mockImplementationOnce(async () => { + return Promise.resolve('invalid data type'); + }); + return band; + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when nodata value is incorrect', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported band nodata value'; + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset.bands, 'getAsync').mockImplementationOnce(async (...args: Parameters) => { + const band = await dataset.bands.getAsync(...args); + vi.spyOn(band, 'noDataValueAsync', 'get').mockImplementationOnce(async () => { + return Promise.resolve(Number.POSITIVE_INFINITY); + }); + return band; + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when srs is null', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported SRS'; + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset, 'srsAsync', 'get').mockImplementationOnce(async () => { + return Promise.resolve(null); // this EPSG code should not be included in the supported srs ids config + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when srs authority code is unrecognized', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported SRS'; + vi.spyOn(gdalAsync.SpatialReference.prototype, 'getAuthorityCode').mockReturnValueOnce(undefined as unknown as string); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when srs type is unsupported', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported SRS type'; + vi.spyOn(gdalAsync.SpatialReference.prototype, 'isGeographic').mockReturnValueOnce(false); + vi.spyOn(gdalAsync.SpatialReference.prototype, 'isProjected').mockReturnValueOnce(false); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when srs is unsupported', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported SRS'; + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset, 'srsAsync', 'get').mockImplementationOnce(async () => { + return Promise.resolve(gdalAsync.SpatialReference.fromEPSG(3857)); // this EPSG code should not be included in the supported srs ids config + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 422 status code and respond with unsuccessful message when geo transform is incorrect', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported geo transform'; + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset, 'geoTransformAsync', 'get').mockImplementationOnce(async () => { + return Promise.resolve([]); + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.UNPROCESSABLE_ENTITY); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyUnprocessableContent).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + type InfoResponseBodyInternalServerError = paths['/info']['post']['responses'][500]['content']['application/json']; + + it('should return 500 status code and respond with unsuccessful message when file cannot be read', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot open dataset'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockRejectedValueOnce(error); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when band cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access band'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.DatasetBands.prototype, 'getAsync').mockRejectedValueOnce(error); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when metadata cannot be accessed for validation', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot read metadata'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Dataset.prototype, 'getMetadataAsync') + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + .mockImplementationOnce(async (...args: Parameters) => gdalAsync.Dataset.prototype.getMetadataAsync(...args)) + .mockRejectedValueOnce(error); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when block size cannot be accessed for validation', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported SRS'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset.bands, 'getAsync').mockImplementationOnce(async (...args: Parameters) => { + const band = await dataset.bands.getAsync(...args); + vi.spyOn(band, 'blockSizeAsync', 'get').mockRejectedValueOnce(error); + return band; + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when overviews cannot be accessed for validation', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'Unsupported SRS'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset.bands, 'getAsync').mockImplementationOnce(async (...args: Parameters) => { + const band = await dataset.bands.getAsync(...args); + vi.spyOn(band.overviews, 'countAsync').mockRejectedValueOnce(error); + return band; + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when metadata cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot read metadata'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Dataset.prototype, 'getMetadataAsync') + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + .mockImplementationOnce(async (...args: Parameters) => gdalAsync.Dataset.prototype.getMetadataAsync(...args)) + .mockRejectedValueOnce(error); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when band data type cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access band data type'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset.bands, 'getAsync').mockImplementationOnce(async (...args: Parameters) => { + const band = await dataset.bands.getAsync(...args); + vi.spyOn(band, 'dataTypeAsync', 'get').mockImplementationOnce(async () => { + return Promise.reject(error); + }); + return band; + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when band nodata value cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access nodata value'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset.bands, 'getAsync').mockImplementationOnce(async (...args: Parameters) => { + const band = await dataset.bands.getAsync(...args); + vi.spyOn(band, 'noDataValueAsync', 'get').mockImplementationOnce(async () => { + return Promise.reject(error); + }); + return band; + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when srs cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access srs'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset, 'srsAsync', 'get').mockImplementationOnce(async () => { + return Promise.reject(error); + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when srs authority code cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access srs authority code'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.SpatialReference.prototype, 'getAuthorityCode').mockImplementationOnce(() => { + throw error; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when is geographic srs check throws an error', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access is geographic srs'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.SpatialReference.prototype, 'isGeographic').mockImplementationOnce(() => { + throw error; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when is projected srs check throws an error', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access is projected srs'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.SpatialReference.prototype, 'isProjected').mockImplementationOnce(() => { + throw error; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when srs attribute cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access srs attribute'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.SpatialReference.prototype, 'getAttrValue').mockImplementationOnce(() => { + throw error; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when geo transform cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access geo transform'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.Driver.prototype, 'openAsync').mockImplementationOnce(async (...args: Parameters) => { + const dataset = await gdalAsync.drivers.get(metadata.driverName).openAsync(...args); + vi.spyOn(dataset, 'geoTransformAsync', 'get').mockImplementationOnce(async () => { + return Promise.reject(error); + }); + return dataset; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + + it('should return 500 status code and respond with unsuccessful message when bands envelope cannot be accessed', async () => { + const metadata = createInfoMetadata({ demType: 'geotiff' }); + const demFilePath = await createInfoResource(metadata); + const expectedErrorMessage = 'cannot access bands envelope'; + const error = new Error(expectedErrorMessage); + vi.spyOn(gdalAsync.DatasetBands.prototype, 'getEnvelope').mockImplementationOnce(() => { + throw error; + }); + + const response = await requestSender.info({ requestBody: demFilePath }); + + expect(response.status).toBe(httpStatusCodes.INTERNAL_SERVER_ERROR); + expect(response).toSatisfyApiSpec(); + expect((response.body as InfoResponseBodyInternalServerError).message).toBe(expectedErrorMessage); + expect.assertions(3); + }); + }); +}); diff --git a/tests/integration/resourceName/resourceName.spec.ts b/tests/integration/resourceName/resourceName.spec.ts deleted file mode 100644 index c1548dd..0000000 --- a/tests/integration/resourceName/resourceName.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { jsLogger } from '@map-colonies/js-logger'; -import { describe, beforeEach, it, expect, beforeAll } from 'vitest'; -import { trace } from '@opentelemetry/api'; -import httpStatusCodes from 'http-status-codes'; -import { createRequestSender, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; -import { paths, operations } from '@openapi'; -import { getApp } from '@src/app'; -import { SERVICES } from '@common/constants'; -import { initConfig } from '@src/common/config'; - -describe('resourceName', function () { - let requestSender: RequestSender; - - beforeAll(async function () { - await initConfig(true); - }); - - beforeEach(async function () { - const [app] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender('openapi3.yaml', app); - }); - - describe('Happy Path', function () { - it('should return 200 status code and the resource', async function () { - const response = await requestSender.getResourceName(); - - expect(response.status).toBe(httpStatusCodes.OK); - - const resource = response.body as paths['/resourceName']['get']['responses'][200]['content']['application/json']; - - expect(response).toSatisfyApiSpec(); - expect(resource.id).toBe(1); - expect(resource.name).toBe('ronin'); - expect(resource.description).toBe('can you do a logistics run?'); - }); - - it('should return 200 status code and create the resource', async function () { - const response = await requestSender.createResource({ - requestBody: { - description: 'aaa', - id: 1, - name: 'aaa', - }, - }); - - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.CREATED); - }); - }); - - describe('Bad Path', function () { - // All requests with status code of 400 - it('should in theory test 400 status code', function () { - expect(true).toBe(true); - }); - }); - - describe('Sad Path', function () { - // All requests with status code 4XX-5XX - it('should in theory test 500 status code', function () { - expect(true).toBe(true); - }); - }); -}); diff --git a/tests/unit/anotherResource/models/anotherResourceManager.spec.ts b/tests/unit/anotherResource/models/anotherResourceManager.spec.ts deleted file mode 100644 index 8b89300..0000000 --- a/tests/unit/anotherResource/models/anotherResourceManager.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { jsLogger } from '@map-colonies/js-logger'; -import { describe, beforeEach, it, expect } from 'vitest'; -import { AnotherResourceManager } from '@src/anotherResource/models/anotherResourceManager'; - -let anotherResourceManager: AnotherResourceManager; - -describe('ResourceNameManager', () => { - beforeEach(function () { - anotherResourceManager = new AnotherResourceManager(jsLogger({ enabled: false })); - }); - - describe('#getResource', () => { - it('should return resource of kind avi', function () { - // action - const resource = anotherResourceManager.getResource(); - - // expectation - expect(resource.kind).toBe('avi'); - expect(resource.isAlive).toBe(false); - }); - }); -}); diff --git a/tests/unit/info/fileHandlers/gdal.spec.ts b/tests/unit/info/fileHandlers/gdal.spec.ts new file mode 100644 index 0000000..9f9bf9d --- /dev/null +++ b/tests/unit/info/fileHandlers/gdal.spec.ts @@ -0,0 +1,537 @@ +import * as fsPromises from 'node:fs/promises'; +import { join } from 'node:path'; +import { faker } from '@faker-js/faker'; +import { NotFoundError } from '@map-colonies/error-types'; +import { jsLogger } from '@map-colonies/js-logger'; +import { + CoordinateTransformation, + SpatialReference, + type Dataset, + type DatasetBands, + type Driver, + type GDALDrivers, + type RasterBand, + type RasterBandOverviews, + type xyz, +} from 'gdal-async'; +import { afterEach, beforeEach, describe, expect, it, vi, type Mocked } from 'vitest'; +import { getConfig } from '@src/common/config'; +import { type GdalAsync, getPixelInfo, getResolutions, getSrsInfo } from '@src/common/gdal'; +import { GDALHandler } from '@src/info/fileHandlers/gdal'; +import { hasKey } from '@src/common/utils'; + +const config = getConfig(); +const { max: resolutionDegreeMax, min: resolutionDegreeMin } = config.get('application.validation.resolutionDegree'); +const { max: resolutionMeterMax, min: resolutionMeterMin } = config.get('application.validation.resolutionMeter'); +const blockSize = config.get('application.validation.blockSize'); + +const mockClose = vi.fn<() => void>(); +const mockGeoTransform = vi.fn<() => Promise>(); +const mockSrsAsync = vi.fn<() => Dataset['srsAsync']>(); +const mockBlockSize = vi.fn<() => Promise>(); +const mockDataType = vi.fn<() => Promise>(); +const mockNoDataValue = vi.fn<() => Promise>(); +const mockTransformPoint = vi.fn().mockReturnValue({ x: 100, y: 100, z: 0 }); + +vi.mock('node:fs/promises'); +vi.mock('@src/common/gdal'); +vi.mock('@src/common/utils'); + +const mockAccess = vi.mocked(fsPromises.access).mockResolvedValue(undefined); +const mockGetSrsInfo = vi.mocked(getSrsInfo).mockReturnValue({ srsId: 4326, srsName: 'WGS 84' }); +const mockGetPixelInfo = vi.mocked(getPixelInfo).mockReturnValue({ pixelHeight: 0.01, pixelWidth: 0.01 }); +const mockGetResolutions = vi.mocked(getResolutions).mockReturnValue({ resolutionDegree: 0.01, resolutionMeter: 9999 }); +const mockHasKey = vi.mocked(hasKey).mockReturnValue(true); + +const mockOverview = { + countAsync: vi.fn().mockReturnValue(1), +} satisfies Partial as unknown as Mocked; +const mockBand = { + get blockSizeAsync(): Promise { + return mockBlockSize.mockImplementation(async () => Promise.resolve({ x: blockSize, y: blockSize }))(); + }, + get dataTypeAsync(): Promise { + return mockDataType.mockImplementation(async () => Promise.resolve('Int16'))(); + }, + get noDataValueAsync(): Promise { + return mockNoDataValue.mockImplementation(async () => Promise.resolve(-9999))(); + }, + overviews: mockOverview, + size: { x: blockSize * 2, y: blockSize * 2 }, +} satisfies Partial as unknown as Mocked; +const mockBands = { + getAsync: vi.fn().mockResolvedValue(mockBand), + getEnvelope: vi.fn().mockReturnValue({ + minX: 0, + minY: 0, + maxX: 100, + maxY: 100, + }), +} satisfies Partial as unknown as Mocked; +const mockCoordinateTransformation = { + transformPoint: vi.fn().mockImplementation(() => {}), +} satisfies Partial as unknown as Mocked; +const mockSpatialReference = { + isGeographic: vi.fn().mockReturnValue(true), + isProjected: vi.fn().mockReturnValue(false), + // eslint-disable-next-line @typescript-eslint/naming-convention + EPSGTreatsAsLatLong: vi.fn().mockReturnValue(false), + // eslint-disable-next-line @typescript-eslint/naming-convention + EPSGTreatsAsNorthingEasting: vi.fn().mockReturnValue(false), + getAuthorityCode: vi.fn().mockReturnValue('4326'), + getAttrValue: vi.fn().mockReturnValue('WGS 84'), +} satisfies Partial as unknown as Mocked; +const mockDataset = { + getMetadataAsync: vi.fn().mockImplementation(async (...args: Parameters) => { + const domain = args[0]; + const resposnse = await Promise.resolve( + domain === 'IMAGE_STRUCTURE' + ? { + // eslint-disable-next-line @typescript-eslint/naming-convention + LAYOUT: 'COG', + // eslint-disable-next-line @typescript-eslint/naming-convention + COMPRESSION: 'LZW', + } + : { + // eslint-disable-next-line @typescript-eslint/naming-convention + AREA_OR_POINT: 'Area', + } + ); + return resposnse; + }), + bands: mockBands, + get srsAsync() { + return mockSrsAsync.mockImplementation(async () => Promise.resolve(mockSpatialReference))(); + }, + get geoTransformAsync() { + return mockGeoTransform.mockImplementation(async () => Promise.resolve([0, 0.01, 0, 0, 0, -0.01]))(); + }, + close: mockClose.mockImplementation(() => {}), +} satisfies Partial as unknown as Mocked; +const mockDriver = { + getMetadata: vi.fn().mockReturnValue({ + // eslint-disable-next-line @typescript-eslint/naming-convention + DMD_EXTENSION: 'tif', + // eslint-disable-next-line @typescript-eslint/naming-convention + DMD_EXTENSIONS: 'tiff tif', + }), + openAsync: vi.fn().mockResolvedValue(mockDataset), +} satisfies Partial as unknown as Mocked; +const mockDrivers = { + get: vi.fn().mockReturnValue(mockDriver), +} satisfies Partial as unknown as Mocked; +const mockGdal = { + // eslint-disable-next-line @typescript-eslint/naming-convention + CoordinateTransformation: vi.fn().mockImplementation(() => mockCoordinateTransformation), + // eslint-disable-next-line @typescript-eslint/naming-convention + SpatialReference: vi.fn().mockImplementation(() => mockSpatialReference), + drivers: mockDrivers, +} satisfies Partial as unknown as Mocked; + +describe('GDALHandler', () => { + let gdalHandler: GDALHandler; + const filePath = join('/path/to/', faker.system.commonFileName('tif')); + + beforeEach(async () => { + gdalHandler = new GDALHandler(config, await jsLogger({ enabled: false }), mockGdal); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('#supports', () => { + it('should return true when input file is supported by config and gdal driver', () => { + const response = gdalHandler.supports(filePath); + + expect(response).toBe(true); + }); + + it('should return false when a configured driver is not accessible', () => { + const error = new Error('Driver not accessible'); + mockDrivers.get.mockImplementationOnce(() => { + throw error; + }); + + const response = gdalHandler.supports(filePath); + + expect(response).toBe(false); + }); + + it('should return false when input file is not supported by config', () => { + const response = gdalHandler.supports(faker.system.commonFileName('bad_file_extension')); + + expect(response).toBe(false); + }); + + it('should return false when input file format is not supported by the API', () => { + mockHasKey.mockReturnValueOnce(false); + const response = gdalHandler.supports(filePath); + + expect(response).toBe(false); + }); + + it('should return false when configured driver is not accessible', () => { + const error = new Error('Driver not accessible'); + mockDrivers.get.mockImplementationOnce(() => { + throw error; + }); + + const response = gdalHandler.supports(filePath); + + expect(response).toBe(false); + }); + + it('should return false when configured driver metadata is not accessible', () => { + const error = new Error('Driver metadata not accessible'); + mockDriver.getMetadata.mockImplementationOnce(() => { + throw error; + }); + + const response = gdalHandler.supports(filePath); + + expect(response).toBe(false); + }); + + it('should return false when input file is not supported by any gdal driver', () => { + mockDriver.getMetadata.mockReturnValueOnce({ + // eslint-disable-next-line @typescript-eslint/naming-convention + DMD_EXTENSION: 'tiff', + }); + + const response = gdalHandler.supports(filePath); + + expect(response).toBe(false); + }); + }); + + describe('#getInfo', () => { + it('should successfully return info response for a supported file', async () => { + mockTransformPoint.mockReturnValueOnce({ x: 50, y: 50, z: 0 }); + mockTransformPoint.mockReturnValueOnce({ x: 100, y: 100, z: 0 }); + + const response = await gdalHandler.getInfo(filePath); + + expect(response).toBeObject(); + expect(response.areaOrPoint).toBe('Area'); + expect(response.dataType).toBe('Int16'); + expect(response.noDataValue).toBe(-9999); + expect(response.srsId).toBe(4326); + expect(response.srsName).toBe('WGS 84'); + expect(response.resolutionMeter).toBeWithin(resolutionMeterMin, resolutionMeterMax); + expect(response.resolutionDegree).toBeWithin(resolutionDegreeMin, resolutionDegreeMax); + expect(mockClose).toHaveBeenCalled(); + expect.assertions(9); + }); + + it('should raise an error when file does not exist', async () => { + const accessError = new Error('File not found'); + mockAccess.mockRejectedValueOnce(accessError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(NotFoundError); + expect.assertions(1); + }); + + it('should raise an error when configured driver is not accessible', async () => { + const expectedError = new Error('Driver not accessible'); + mockDrivers.get.mockImplementationOnce(() => { + throw expectedError; + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file is not supported by config', async () => { + const response = gdalHandler.getInfo(faker.system.commonFileName('bad_file_extension')); + + await expect(response).rejects.toThrow('Unsupported file format'); + expect.assertions(1); + }); + + it('should raise an error when configured driver metadata is not accessible', async () => { + const expectedError = new Error('Driver metadata not accessible'); + mockDriver.getMetadata.mockImplementationOnce(() => { + throw expectedError; + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file is not supported by any gdal driver', async () => { + mockDriver.getMetadata.mockReturnValueOnce({ + // eslint-disable-next-line @typescript-eslint/naming-convention + DMD_EXTENSION: 'tiff', + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(`Unsupported file format of file: ${filePath}`); + expect.assertions(1); + }); + + it('should raise an error when input file cannot be opened gdal driver', async () => { + const expectedError = new Error('File open failed'); + mockDriver.openAsync.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file band is not accessible', async () => { + const expectedError = new Error('Band not accessible'); + mockBands.getAsync.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file metadata of image structure is not accessible', async () => { + const expectedError = new Error('Metadata of image structure not accessible'); + mockDataset.getMetadataAsync.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file metadata of image structure is not valid', async () => { + mockDataset.getMetadataAsync.mockResolvedValueOnce({ + // eslint-disable-next-line @typescript-eslint/naming-convention + LAYOUT: '', + // eslint-disable-next-line @typescript-eslint/naming-convention + COMPRESSION: 'LZW', + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Unsupported image structure metadata (LAYOUT and COMPRESSION)'); + expect.assertions(1); + }); + + it('should raise an error when input file band block size is not accessible', async () => { + const expectedError = new Error('Block size not accessible'); + mockBlockSize.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file band block size is not valid', async () => { + mockBlockSize.mockResolvedValueOnce({ x: 0, y: 0 }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Unsupported block size'); + expect.assertions(1); + }); + + it('should raise an error when input file band overviews count is not accessible', async () => { + const expectedError = new Error('Overviews count not accessible'); + mockOverview.countAsync.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file file band overviews count is not valid', async () => { + mockOverview.countAsync.mockResolvedValueOnce(0); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Could not find overviews'); + expect.assertions(1); + }); + + it('should raise an error when input file metadata is not accessible', async () => { + const expectedError = new Error('Metadata not accessible'); + mockDataset.getMetadataAsync + .mockResolvedValueOnce({ + // eslint-disable-next-line @typescript-eslint/naming-convention + LAYOUT: 'COG', + // eslint-disable-next-line @typescript-eslint/naming-convention + COMPRESSION: 'LZW', + }) + .mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file metadata is not valid', async () => { + mockDataset.getMetadataAsync + .mockResolvedValueOnce({ + // eslint-disable-next-line @typescript-eslint/naming-convention + LAYOUT: 'COG', + // eslint-disable-next-line @typescript-eslint/naming-convention + COMPRESSION: 'LZW', + }) + .mockResolvedValueOnce({ + // eslint-disable-next-line @typescript-eslint/naming-convention + AREA_OR_POINT: '', + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Could not extract AREA_OR_POINT metadata'); + expect.assertions(1); + }); + + it('should raise an error when input file band data type is not accessible', async () => { + const expectedError = new Error('Data type not accessible'); + mockDataType.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file band data type is not valid', async () => { + mockDataType.mockResolvedValueOnce('InvalidType'); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Unsupported band data type'); + expect.assertions(1); + }); + + it('should raise an error when input file band nodata value is not accessible', async () => { + const expectedError = new Error('nodata value error'); + mockNoDataValue.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file band nodata value is not valid', async () => { + mockNoDataValue.mockResolvedValueOnce(Infinity); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Unsupported band nodata value'); + expect.assertions(1); + }); + + it('should raise an error when input file srs is not accessible', async () => { + const expectedError = new Error('Band not accessible'); + mockSrsAsync.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file srs is not defined', async () => { + mockSrsAsync.mockResolvedValueOnce(null); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Unsupported SRS'); + expect.assertions(1); + }); + + it('should raise an error when input file srs info extraction is not valid - invalid srs id', async () => { + mockGetSrsInfo.mockReturnValueOnce({ + srsId: 0, + srsName: 'name', + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Unsupported SRS'); + expect.assertions(1); + }); + + it('should raise an error when input file srs info extraction is not valid - invalid srs name', async () => { + mockGetSrsInfo.mockReturnValueOnce({ + srsId: 4326, + srsName: '', + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow('Unsupported SRS'); + expect.assertions(1); + }); + + it('should raise an error when input file srs info extraction throws an error', async () => { + const expectedError = 'srs info error'; + mockGetSrsInfo.mockImplementationOnce(() => { + throw new Error(expectedError); + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file geo transform is not accessible', async () => { + const expectedError = new Error('Geo transform not accessible'); + mockGeoTransform.mockRejectedValueOnce(expectedError); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file pixel info extraction throws an error', async () => { + const expectedError = 'pixel info error'; + mockGetPixelInfo.mockImplementationOnce(() => { + throw new Error(expectedError); + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file bands envelope is not accessible', async () => { + const expectedError = new Error('Envelope not accessible'); + mockBands.getEnvelope.mockImplementationOnce(() => { + throw expectedError; + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + + it('should raise an error when input file resolution extraction throws an error', async () => { + const expectedError = 'resolutions error'; + mockGetResolutions.mockImplementationOnce(() => { + throw new Error(expectedError); + }); + + const response = gdalHandler.getInfo(filePath); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + }); +}); diff --git a/tests/unit/info/models/infoManager.spec.ts b/tests/unit/info/models/infoManager.spec.ts new file mode 100644 index 0000000..686bdc2 --- /dev/null +++ b/tests/unit/info/models/infoManager.spec.ts @@ -0,0 +1,80 @@ +import { faker } from '@faker-js/faker'; +import { UnprocessableEntityError } from '@map-colonies/error-types'; +import { jsLogger } from '@map-colonies/js-logger'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { RASTER_DATA_TYPES } from '@src/common/constants'; +import { generateInfoResponse } from '@tests/helpers/faker/info.faker'; +import { FileHandler, InfoManager, InfoResponse } from '../../../../src/info/models/infoManager'; + +describe('InfoManager', () => { + let infoManager: InfoManager; + let mockFileHandler: FileHandler; + + beforeEach(async () => { + mockFileHandler = { + name: 'mockHandler', + supports: vi.fn(), + getInfo: vi.fn(), + }; + infoManager = new InfoManager(await jsLogger({ enabled: false }), [mockFileHandler, mockFileHandler]); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('#info', () => { + const format = faker.helpers.objectKey(RASTER_DATA_TYPES); + + it('should return info response when handler supports file', async () => { + const demFilePath = '/path/to/file.tif'; + const expectedResponse: InfoResponse = generateInfoResponse(format); + vi.mocked(mockFileHandler.supports).mockReturnValueOnce(true); + vi.mocked(mockFileHandler.getInfo).mockResolvedValueOnce(expectedResponse); + + const response = await infoManager.info({ demFilePath }); + + expect(response).toStrictEqual(expectedResponse); + expect(mockFileHandler.supports).toHaveBeenCalledOnce(); + expect(mockFileHandler.getInfo).toHaveBeenCalledOnce(); + expect.assertions(3); + }); + + it('should use first matching handler when multiple handlers exist', async () => { + const demFilePath = '/path/to/file.tif'; + const expectedResponse: InfoResponse = generateInfoResponse(format); + vi.mocked(mockFileHandler.supports).mockReturnValueOnce(true); + vi.mocked(mockFileHandler.getInfo).mockResolvedValueOnce(expectedResponse); + vi.mocked(mockFileHandler.supports).mockReturnValueOnce(true); + + const response = await infoManager.info({ demFilePath }); + + expect(response).toStrictEqual(expectedResponse); + expect(mockFileHandler.supports).toHaveBeenCalledOnce(); + expect(mockFileHandler.getInfo).toHaveBeenCalledOnce(); + expect.assertions(3); + }); + + it('should throw UnprocessableEntityError when no handler supports file', async () => { + const demFilePath = '/path/to/file.unknown'; + vi.mocked(mockFileHandler.supports).mockReturnValue(false); + + const response = infoManager.info({ demFilePath }); + + await expect(response).rejects.toThrow(new UnprocessableEntityError(`No handler found for file: ${demFilePath}`)); + expect.assertions(1); + }); + + it('should throw an error when getting info throws an error', async () => { + const demFilePath = '/path/to/file.tif'; + const expectedError = new Error('info error'); + vi.mocked(mockFileHandler.supports).mockReturnValueOnce(true); + vi.mocked(mockFileHandler.getInfo).mockRejectedValueOnce(expectedError); + + const response = infoManager.info({ demFilePath }); + + await expect(response).rejects.toThrow(expectedError); + expect.assertions(1); + }); + }); +}); diff --git a/tests/unit/resourceName/models/resourceNameModel.spec.ts b/tests/unit/resourceName/models/resourceNameModel.spec.ts deleted file mode 100644 index 17ed6f6..0000000 --- a/tests/unit/resourceName/models/resourceNameModel.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { jsLogger } from '@map-colonies/js-logger'; -import { describe, beforeEach, it, expect } from 'vitest'; -import { ResourceNameManager } from '@src/resourceName/models/resourceNameManager'; - -let resourceNameManager: ResourceNameManager; - -describe('ResourceNameManager', () => { - beforeEach(function () { - resourceNameManager = new ResourceNameManager(jsLogger({ enabled: false })); - }); - - describe('#getResource', () => { - it('should return the resource of id 1', function () { - // action - const resource = resourceNameManager.getResource(); - - // expectation - expect(resource.id).toBe(1); - expect(resource.name).toBe('ronin'); - expect(resource.description).toBe('can you do a logistics run?'); - }); - }); - - describe('#createResource', () => { - it('should return the resource of id 1', function () { - // action - const resource = resourceNameManager.createResource({ description: 'meow', id: 1, name: 'cat' }); - - // expectation - expect(resource.id).toBeLessThanOrEqual(100); - expect(resource.id).toBeGreaterThanOrEqual(0); - expect(resource).toHaveProperty('name', 'cat'); - expect(resource).toHaveProperty('description', 'meow'); - }); - }); -}); diff --git a/vitest.config.mts b/vitest.config.mts index e032130..47d46f5 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -25,7 +25,12 @@ export default defineConfig({ { test: { name: 'unit', - setupFiles: ['./tests/configurations/initJestOpenapi.setup.ts', './tests/configurations/vite.setup.ts'], + setupFiles: [ + './tests/configurations/initConfig.setup.ts', + './tests/configurations/initJestExtended.setup.ts', + './tests/configurations/initZodSchemaFaker.setup.ts', + './tests/configurations/vite.setup.ts', + ], include: ['tests/unit/**/*.spec.ts'], environment: 'node', }, @@ -36,7 +41,15 @@ export default defineConfig({ { test: { name: 'integration', - setupFiles: ['./tests/configurations/initJestOpenapi.setup.ts', './tests/configurations/vite.setup.ts'], + globalSetup: ['./tests/configurations/tmpFolder.setup.ts'], + setupFiles: [ + './tests/configurations/initConfig.setup.ts', + './tests/configurations/initCustomMatchers.setup.ts', + './tests/configurations/initJestExtended.setup.ts', + './tests/configurations/initZodSchemaFaker.setup.ts', + './tests/configurations/initJestOpenapi.setup.ts', + './tests/configurations/vite.setup.ts', + ], include: ['tests/integration/**/*.spec.ts'], environment: 'node', },