From 74cbcdb03dd468da1844523183f2dedb57458085 Mon Sep 17 00:00:00 2001 From: TULCHINSKI LIRAN Date: Mon, 28 Sep 2026 17:58:03 +0300 Subject: [PATCH 1/4] feat: scaffold 3d-ops-trigger skeleton (MAPCO-11834) Rename boilerplate identity to 3d-ops-trigger across package.json, openapi, helm chart/values, catalog-info and README. Pin Node>=24 (engines) and wire core deps: @map-colonies/jobnik-sdk, @map-colonies/3d-shared (local file link), zod. --- README.md | 123 +++++++----------------------- catalog-info.yaml | 10 +-- helm/Chart.yaml | 4 +- helm/values.yaml | 6 +- openapi3.yaml | 4 +- package-lock.json | 190 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 12 ++- 7 files changed, 225 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index aa757fb..4e601b6 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,53 @@ -# Map Colonies typescript service template +# 3D Operations Trigger ---------------------------------- -This is a basic repo template for building new MapColonies web services in Typescript. +Entry point for 3D operations — **ingest, delete, update, publish/unpublish**. It validates incoming requests (light & fast) and triggers the corresponding [Jobnik](https://mapcolonies.github.io/infra-portal/docs/knowledge-base/jobnik) jobs. Successor to the legacy `3d-gateway` (and, in the near future, `store-trigger`). -> [!IMPORTANT] -> To regenerate the types on openapi change run the command `npm run generate:openapi-types`. +> Epic: MAPCO-11833 · Design: [3D Ingestion High level architecture](https://mapcolonies.atlassian.net/wiki/spaces/MAPConflicResolution/pages/3342172161) · See [docs/PRD.md](./docs/PRD.md) -> [!WARNING] -> After creating a new repo based on this template, you should delete the CODEOWNERS file. - - -## 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 - -- feature request - -- pull request - -- github actions - -- on pull_request - -- LGTM +## API -- test +| Endpoint | Method | Summary | +| --- | --- | --- | +| `/record` | POST | Start an ingestion flow (validate, create Jobnik ingestion job) | +| `/record/{id}` | DELETE | Validate deletability, create Jobnik delete job | +| `/record/{id}` | PATCH | Update metadata for a record | +| `/record/status/{id}` | PATCH | Publish / unpublish a record | -- lint +Full OpenAPI spec: [openapi3.yaml](/openapi3.yaml). Regenerate types on spec change with `npm run generate:openapi-types`. -- snyk +## Requirements -## API -Checkout the OpenAPI spec [here](/openapi3.yaml) +- **Node.js ≥ 24** (required by `@map-colonies/jobnik-sdk`). Use the pinned version via `nvm use`. ## Installation -Install deps with npm - ```bash npm install ``` -## Run Locally - -Clone the project - -```bash - -git clone https://link-to-project - -``` - -Go to the project directory - -```bash - -cd my-project - -``` - -Install dependencies +> During development this service consumes `@map-colonies/3d-shared` (and, later, mc-models v2) via local `file:` links until those packages are published to npm. -```bash - -npm install - -``` - -Start the server +## Run Locally ```bash - -npm run start - +npm run start # build + run +npm run start:dev # offline config + source maps ``` ## Running Tests -To run tests, run the following command - ```bash - -npm run test - +npm run test # all +npm run test:unit # unit only +npm run test:integration # integration only ``` -To only run unit tests: -```bash -npm run test:unit -``` +## Development notes -To only run integration tests: -```bash -npm run test:integration -``` +- eslint / prettier via `@map-colonies/eslint-config` and `@map-colonies/prettier-config` +- vitest for tests +- OpenAPI request validation at the middleware layer +- config via [node-config](https://www.npmjs.com/package/node-config) +- tracing & metrics via `@map-colonies/telemetry` diff --git a/catalog-info.yaml b/catalog-info.yaml index 55a51de..b4ab881 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: 3d-ops-trigger + description: Entry point for 3D operations (ingest, delete, update, publish/unpublish) — validates requests and triggers Jobnik jobs annotations: - github.com/project-slug: MapColonies/ts-server-boilerplate + github.com/project-slug: MapColonies/3d-ops-trigger tags: - nodejs - typescript - expressjs - - boilerplate + - 3d spec: type: service lifecycle: production owner: DevInfra - system: boilerplate + system: 3d diff --git a/helm/Chart.yaml b/helm/Chart.yaml index b1f2c0f..853bc57 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: 3d-ops-trigger +description: A Helm chart for the 3d-ops-trigger service type: application version: 1.0.0 appVersion: 1.0.0 diff --git a/helm/values.yaml b/helm/values.yaml index f982729..46736ea 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -7,7 +7,7 @@ global: mclabels: component: backend - partOf: boilerplates + partOf: 3d owner: common prometheus: enabled: true @@ -23,7 +23,7 @@ fullnameOverride: "" configManagement: offlineMode: false - name: 'service-name' + name: '3d-ops-trigger' version: 'latest' serverUrl: 'http://localhost:8080/api' @@ -66,7 +66,7 @@ caPath: '/usr/local/share/ca-certificates' caKey: 'ca.crt' image: - repository: ts-server-boilerplate + repository: 3d-ops-trigger # If commented, appVersion will be taken. See: _helpers.tpl # tag: 'latest' pullPolicy: IfNotPresent diff --git a/openapi3.yaml b/openapi3.yaml index 55ee8ee..710048e 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -1,7 +1,7 @@ openapi: 3.0.1 info: - title: service-name - description: basic template for map colonies service + title: 3d-ops-trigger + description: Entry point for 3D operations — validates requests and triggers Jobnik jobs version: 1.0.0 license: name: MIT diff --git a/package-lock.json b/package-lock.json index 6ee91ea..0a0f98a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,20 @@ { - "name": "service-name", + "name": "3d-ops-trigger", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "service-name", + "name": "3d-ops-trigger", "version": "1.0.0", "license": "ISC", "dependencies": { "@godaddy/terminus": "^4.12.1", + "@map-colonies/3d-shared": "file:../../3d-general/3d-shared", "@map-colonies/config": "^4.0.1", "@map-colonies/error-express-handler": "^4.0.0", "@map-colonies/express-access-log-middleware": "^5.0.0", + "@map-colonies/jobnik-sdk": "^1.0.1", "@map-colonies/js-logger": "^5.0.0", "@map-colonies/openapi-express-viewer": "^5.0.0", "@map-colonies/prometheus": "^1.0.0", @@ -27,7 +29,8 @@ "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": "^3.24.1" }, "devDependencies": { "@commitlint/cli": "^21.2.2", @@ -60,6 +63,48 @@ "tsc-alias": "^1.9.1", "typescript": "^5.9.3", "vitest": "^4.1.10" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "../../3d-general/3d-shared": { + "name": "@map-colonies/3d-shared", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@map-colonies/types": "^1.4.0", + "geojson": "^0.5.0", + "mime-types": "^2.1.35", + "zod": "^3.24.1" + }, + "devDependencies": { + "@commitlint/cli": "^19.6.1", + "@commitlint/config-conventional": "^19.6.0", + "@map-colonies/eslint-config": "^4.0.0", + "@map-colonies/prettier-config": "^0.0.1", + "@swc/core": "^1.7.26", + "@swc/jest": "^0.2.36", + "@types/geojson": "^7946.0.14", + "@types/jest": "^29.4.0", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "commitlint": "^19.6.1", + "eslint": "^8.36.0", + "husky": "^9.1.7", + "jest": "^29.0.0", + "jest-html-reporters": "^3.1.4", + "prettier": "^3.4.2", + "pretty-quick": "^4.0.0", + "rimraf": "^5.0.0", + "standard-version": "^9.5.0", + "ts-jest": "^29.0.0", + "typedoc": "^0.27.6", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=24.0.0" } }, "node_modules/@apideck/better-ajv-errors": { @@ -99,7 +144,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -114,7 +158,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/@babel/helper-string-parser": { @@ -131,7 +174,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -157,7 +199,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -888,7 +929,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.10.0" @@ -969,6 +1009,10 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "license": "MIT" }, + "node_modules/@map-colonies/3d-shared": { + "resolved": "../../3d-general/3d-shared", + "link": true + }, "node_modules/@map-colonies/commitlint-config": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@map-colonies/commitlint-config/-/commitlint-config-2.0.1.tgz", @@ -1134,6 +1178,81 @@ "node": ">=24" } }, + "node_modules/@map-colonies/jobnik-sdk": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@map-colonies/jobnik-sdk/-/jobnik-sdk-1.0.1.tgz", + "integrity": "sha512-ieV91q/+F+5QWaKjW3rmvNTfENw/1mLOM/LlMqb6khYs7ODQMCQcXapEYM3VIifYMc51E46XezHNXOxQKBUtfA==", + "license": "ISC", + "dependencies": { + "@map-colonies/read-pkg": "^2.0.0", + "@map-colonies/semantic-conventions": "^1.0.0", + "@readme/openapi-parser": "^4.0.1", + "date-fns": "^4.1.0", + "http-status-codes": "^2.3.0", + "openapi-fetch": "^0.14.0", + "opossum": "^9.0.0", + "undici": "^7.19.1", + "yaml": "^2.8.1" + }, + "engines": { + "node": ">=24" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "prom-client": "^15.0.0" + } + }, + "node_modules/@map-colonies/jobnik-sdk/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-13.0.5.tgz", + "integrity": "sha512-xfh4xVJD62gG6spIc7lwxoWT+l16nZu1ELyU8FkjaP/oD2yP09EvLAU6KhtudN9aML2Khhs9pY6Slr7KGTES3w==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@map-colonies/jobnik-sdk/node_modules/@readme/openapi-parser": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@readme/openapi-parser/-/openapi-parser-4.1.2.tgz", + "integrity": "sha512-lAFH88r/CHs5VZDUocEda0OSMSQsr6801sziIjOKyVA+0hSFN+BPuelPF5XvkMROHecnPd+XEJN1iNQqCgER/g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^13.0.5", + "@readme/better-ajv-errors": "^2.3.2", + "@readme/openapi-schemas": "^3.1.0", + "@types/json-schema": "^7.0.15", + "ajv": "^8.12.0", + "ajv-draft-04": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@map-colonies/jobnik-sdk/node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/@map-colonies/js-logger": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@map-colonies/js-logger/-/js-logger-5.0.0.tgz", @@ -1281,6 +1400,14 @@ "integrity": "sha512-XUQ2/gvC6d6/nGMKIGVyZo/kemGH5uGOy4xhkYCVLNV/CJsHvJXmhiov7QpqVpb58x+/2WVVUThAStj9ogipug==", "license": "MIT" }, + "node_modules/@map-colonies/semantic-conventions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@map-colonies/semantic-conventions/-/semantic-conventions-1.0.0.tgz", + "integrity": "sha512-QcR+hHVcH3GyHtOXjmsWZXWWR6GG4Kx8+xWdkjuLlBAEG6sLX6hQObLJ1H5YhLZiw3HKCA39hjLwXyl+6ExUNg==", + "engines": { + "node": ">=24" + } + }, "node_modules/@map-colonies/tracing": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@map-colonies/tracing/-/tracing-1.0.0.tgz", @@ -3543,7 +3670,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/@readme/better-ajv-errors/-/better-ajv-errors-2.4.0.tgz", "integrity": "sha512-9WODaOAKSl/mU+MYNZ2aHCrkoRSvmQ+1YkLj589OEqqjOAhbn8j7Z+ilYoiTu/he6X63/clsxxAB4qny9/dDzg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@babel/code-frame": "^7.22.5", @@ -3611,7 +3737,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@readme/openapi-schemas/-/openapi-schemas-3.1.0.tgz", "integrity": "sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6415,6 +6540,16 @@ "node": ">= 8" } }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -9481,6 +9616,15 @@ "@jsdevtools/ono": "7.1.3" } }, + "node_modules/openapi-fetch": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.14.1.tgz", + "integrity": "sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, "node_modules/openapi-response-validator": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/openapi-response-validator/-/openapi-response-validator-9.3.1.tgz", @@ -9541,7 +9685,6 @@ "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "dev": true, "license": "MIT" }, "node_modules/openapi-typescript": { @@ -9566,6 +9709,12 @@ "typescript": "^5.x" } }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "license": "MIT" + }, "node_modules/openapi-typescript/node_modules/parse-json": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", @@ -9643,6 +9792,15 @@ "@types/node": "*" } }, + "node_modules/opossum": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-9.0.0.tgz", + "integrity": "sha512-K76U0QkxOfUZamneQuzz+AP0fyfTJcCplZ2oZL93nxeupuJbN4s6uFNbmVCt4eWqqGqRnnowdFuBicJ1fLMVxw==", + "license": "Apache-2.0", + "engines": { + "node": "^24 || ^22 || ^20" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10115,7 +10273,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -12450,6 +12607,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index f080ea9..5791b80 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "service-name", + "name": "3d-ops-trigger", "version": "1.0.0", - "description": "This is template for map colonies typescript service", + "description": "Entry point for 3D operations (ingest, delete, update, publish/unpublish) — validates requests and triggers Jobnik jobs", "main": "./src/index.ts", "scripts": { "test:unit": "vitest run --coverage.enabled=false --project unit", @@ -30,9 +30,14 @@ }, "author": "MapColonies", "license": "ISC", + "engines": { + "node": ">=24.0.0" + }, "dependencies": { "@godaddy/terminus": "^4.12.1", + "@map-colonies/3d-shared": "file:../../3d-general/3d-shared", "@map-colonies/config": "^4.0.1", + "@map-colonies/jobnik-sdk": "^1.0.1", "@map-colonies/error-express-handler": "^4.0.0", "@map-colonies/express-access-log-middleware": "^5.0.0", "@map-colonies/js-logger": "^5.0.0", @@ -49,7 +54,8 @@ "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": "^3.24.1" }, "devDependencies": { "@commitlint/cli": "^21.2.2", From 1658058268436831fcfe4cae4e00e5eed8303528 Mon Sep 17 00:00:00 2001 From: TULCHINSKI LIRAN Date: Mon, 28 Sep 2026 18:19:07 +0300 Subject: [PATCH 2/4] feat: add OpenAPI spec and /record endpoints (MAPCO-11588) Define the four 3D operations in openapi3.yaml (POST /record, DELETE /record/{id}, PATCH /record/{id}, PATCH /record/status/{id}) and wire the record resource (router, controller, manager) with tsyringe. Managers are stubbed pending Jobnik/catalog integration. Replace boilerplate example resources. Integration tests at the HTTP seam + a RecordManager unit test. --- .prettierignore | 1 + openapi3.yaml | 163 ++++++++++++---- .../controllers/anotherResourceController.ts | 29 --- .../models/anotherResourceManager.ts | 20 -- .../routes/anotherResourceRouter.ts | 16 -- src/common/interfaces.ts | 6 + src/containerConfig.ts | 6 +- src/openapi.d.ts | 180 ++++++++++++++---- src/record/controllers/recordController.ts | 78 ++++++++ src/record/models/recordManager.ts | 47 +++++ src/record/routes/recordRouter.ts | 19 ++ .../controllers/resourceNameController.ts | 35 ---- .../models/resourceNameManager.ts | 36 ---- src/resourceName/routes/resourceNameRouter.ts | 17 -- src/serverBuilder.ts | 9 +- .../anotherResourceName.spec.ts | 56 ------ tests/integration/record/record.spec.ts | 101 ++++++++++ .../resourceName/resourceName.spec.ts | 70 ------- .../models/anotherResourceManager.spec.ts | 22 --- .../unit/record/models/recordManager.spec.ts | 51 +++++ .../models/resourceNameModel.spec.ts | 36 ---- 21 files changed, 581 insertions(+), 417 deletions(-) delete mode 100644 src/anotherResource/controllers/anotherResourceController.ts delete mode 100644 src/anotherResource/models/anotherResourceManager.ts delete mode 100644 src/anotherResource/routes/anotherResourceRouter.ts create mode 100644 src/record/controllers/recordController.ts create mode 100644 src/record/models/recordManager.ts create mode 100644 src/record/routes/recordRouter.ts delete mode 100644 src/resourceName/controllers/resourceNameController.ts delete mode 100644 src/resourceName/models/resourceNameManager.ts delete mode 100644 src/resourceName/routes/resourceNameRouter.ts delete mode 100644 tests/integration/anotherResource/anotherResourceName.spec.ts create mode 100644 tests/integration/record/record.spec.ts delete mode 100644 tests/integration/resourceName/resourceName.spec.ts delete mode 100644 tests/unit/anotherResource/models/anotherResourceManager.spec.ts create mode 100644 tests/unit/record/models/recordManager.spec.ts delete mode 100644 tests/unit/resourceName/models/resourceNameModel.spec.ts diff --git a/.prettierignore b/.prettierignore index 805dd09..d30b4be 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ coverage helm reports .release-please-manifest.json +HANDOFF.md diff --git a/openapi3.yaml b/openapi3.yaml index 710048e..74a26a0 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -7,71 +7,136 @@ info: name: MIT url: https://opensource.org/licenses/MIT paths: - /anotherResource: - get: - operationId: getAnotherResource + /record: + post: + operationId: createRecord tags: - - anotherResource - summary: gets the resource + - record + summary: Start an ingestion process flow + description: Validates the request (light & fast) and creates a Jobnik ingestion job. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ingestionPayload' + responses: + 201: + description: Ingestion job created + content: + application/json: + schema: + $ref: '#/components/schemas/jobResponse' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /record/{id}: + delete: + operationId: deleteRecord + tags: + - record + summary: Start a delete process flow + description: Validates the record can be deleted and creates a Jobnik delete job. + parameters: + - $ref: '#/components/parameters/recordId' responses: 200: - description: OK + description: Delete job created content: application/json: schema: - $ref: '#/components/schemas/anotherResource' + $ref: '#/components/schemas/jobResponse' 400: description: Bad Request content: application/json: schema: $ref: '#/components/schemas/error' - /resourceName: - get: - operationId: getResourceName + 404: + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' + patch: + operationId: updateRecord tags: - - resourceName - summary: gets the resource + - record + summary: Update metadata for a record + parameters: + - $ref: '#/components/parameters/recordId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/updatePayload' responses: 200: - description: OK + description: Metadata updated content: application/json: schema: - $ref: '#/components/schemas/resource' + $ref: '#/components/schemas/ackResponse' 400: description: Bad Request content: application/json: schema: $ref: '#/components/schemas/error' - post: - operationId: createResource + 404: + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /record/status/{id}: + patch: + operationId: updateRecordStatus tags: - - resourceName - summary: creates a new record of type resource + - record + summary: Update the publish/unpublish status of a record + parameters: + - $ref: '#/components/parameters/recordId' requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/resource' + $ref: '#/components/schemas/statusPayload' responses: - 201: - description: created + 200: + description: Status updated content: application/json: schema: - $ref: '#/components/schemas/resource' + $ref: '#/components/schemas/ackResponse' 400: description: Bad Request content: application/json: schema: $ref: '#/components/schemas/error' + 404: + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' security: - {} components: + parameters: + recordId: + name: id + in: path + required: true + description: The record identifier + schema: + type: string schemas: error: type: object @@ -80,27 +145,51 @@ components: properties: message: type: string - resource: + ingestionPayload: type: object required: - - id - - name - - description + - modelPath + - tilesetFilename + - metadata properties: - id: - type: number - format: int64 - name: + modelPath: type: string - description: + description: Path on the shared storage to the 3DTiles folder or 3TZ archive + tilesetFilename: type: string - anotherResource: + description: The tileset entry name (e.g. tileset.json) + metadata: + type: object + description: 3D record metadata (business validation applied downstream) + additionalProperties: true + updatePayload: + type: object + description: Partial metadata fields to update + additionalProperties: true + statusPayload: + type: object + required: + - status + properties: + status: + type: string + enum: + - PUBLISHED + - UNPUBLISHED + jobResponse: + type: object + required: + - jobId + - status + properties: + jobId: + type: string + status: + type: string + ackResponse: type: object required: - - kind - - isAlive + - message properties: - kind: + message: type: string - isAlive: - type: boolean 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 937ba6d..0000000 --- a/src/anotherResource/routes/anotherResourceRouter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Router } from 'express'; -import type { 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/interfaces.ts b/src/common/interfaces.ts index 455054c..1f49da0 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -2,3 +2,9 @@ export interface IConfig { get: (setting: string) => T; has: (setting: string) => boolean; } + +export interface LogContext { + fileName: string; + class: string; + function?: string; +} diff --git a/src/containerConfig.ts b/src/containerConfig.ts index ecd03fa..afe665b 100644 --- a/src/containerConfig.ts +++ b/src/containerConfig.ts @@ -6,8 +6,7 @@ import { jsLogger } from '@map-colonies/js-logger'; import { type InjectionObject, registerDependencies } from '@common/dependencyRegistration'; import { SERVICES, SERVICE_NAME } from '@common/constants'; 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 { recordRouterFactory, RECORD_ROUTER_SYMBOL } from './record/routes/recordRouter'; import { getConfig } from './common/config'; export interface RegisterOptions { @@ -31,8 +30,7 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise { token: SERVICES.LOGGER, provider: { 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: RECORD_ROUTER_SYMBOL, provider: { useFactory: recordRouterFactory } }, { token: 'onSignal', provider: { diff --git a/src/openapi.d.ts b/src/openapi.d.ts index eb9ba76..ca8c610 100644 --- a/src/openapi.d.ts +++ b/src/openapi.d.ts @@ -4,39 +4,62 @@ import type { TypedRequestHandlers as ImportedTypedRequestHandlers } from '@map-colonies/openapi-express-types'; export type paths = { - '/anotherResource': { + '/record': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** gets the resource */ - get: operations['getAnotherResource']; + get?: never; put?: never; - post?: never; + /** + * Start an ingestion process flow + * @description Validates the request (light & fast) and creates a Jobnik ingestion job. + */ + post: operations['createRecord']; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - '/resourceName': { + '/record/{id}': { 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']; + post?: never; + /** + * Start a delete process flow + * @description Validates the record can be deleted and creates a Jobnik delete job. + */ + delete: operations['deleteRecord']; + options?: never; + head?: never; + /** Update metadata for a record */ + patch: operations['updateRecord']; + trace?: never; + }; + '/record/status/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; delete?: never; options?: never; head?: never; - patch?: never; + /** Update the publish/unpublish status of a record */ + patch: operations['updateRecordStatus']; trace?: never; }; }; @@ -46,41 +69,95 @@ export type components = { error: { message: string; }; - resource: { - /** Format: int64 */ - id: number; - name: string; - description: string; + ingestionPayload: { + /** @description Path on the shared storage to the 3DTiles folder or 3TZ archive */ + modelPath: string; + /** @description The tileset entry name (e.g. tileset.json) */ + tilesetFilename: string; + /** @description 3D record metadata (business validation applied downstream) */ + metadata: { + [key: string]: unknown; + }; + }; + /** @description Partial metadata fields to update */ + updatePayload: { + [key: string]: unknown; + }; + statusPayload: { + /** @enum {string} */ + status: 'PUBLISHED' | 'UNPUBLISHED'; }; - anotherResource: { - kind: string; - isAlive: boolean; + jobResponse: { + jobId: string; + status: string; + }; + ackResponse: { + message: string; }; }; responses: never; - parameters: never; + parameters: { + /** @description The record identifier */ + recordId: string; + }; requestBodies: never; headers: never; pathItems: never; }; export type $defs = Record; export interface operations { - getAnotherResource: { + createRecord: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + requestBody: { + content: { + 'application/json': components['schemas']['ingestionPayload']; + }; + }; + responses: { + /** @description Ingestion job created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['jobResponse']; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; + }; + }; + deleteRecord: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The record identifier */ + id: components['parameters']['recordId']; + }; + cookie?: never; + }; requestBody?: never; responses: { - /** @description OK */ + /** @description Delete job created */ 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['anotherResource']; + 'application/json': components['schemas']['jobResponse']; }; }; /** @description Bad Request */ @@ -92,24 +169,40 @@ export interface operations { 'application/json': components['schemas']['error']; }; }; + /** @description Record not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; }; }; - getResourceName: { + updateRecord: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description The record identifier */ + id: components['parameters']['recordId']; + }; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['updatePayload']; + }; + }; responses: { - /** @description OK */ + /** @description Metadata updated */ 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['ackResponse']; }; }; /** @description Bad Request */ @@ -121,28 +214,40 @@ export interface operations { 'application/json': components['schemas']['error']; }; }; + /** @description Record not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; }; }; - createResource: { + updateRecordStatus: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description The record identifier */ + id: components['parameters']['recordId']; + }; cookie?: never; }; requestBody: { content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['statusPayload']; }; }; responses: { - /** @description created */ - 201: { + /** @description Status updated */ + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['ackResponse']; }; }; /** @description Bad Request */ @@ -154,6 +259,15 @@ export interface operations { 'application/json': components['schemas']['error']; }; }; + /** @description Record not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; }; }; } diff --git a/src/record/controllers/recordController.ts b/src/record/controllers/recordController.ts new file mode 100644 index 0000000..65a4bce --- /dev/null +++ b/src/record/controllers/recordController.ts @@ -0,0 +1,78 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } 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 type { LogContext } from '@common/interfaces'; +import { RecordManager } from '../models/recordManager'; + +@injectable() +export class RecordController { + private readonly logContext: LogContext; + private readonly ingestionJobCounter: Counter; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(RecordManager) private readonly manager: RecordManager, + @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry + ) { + this.logContext = { + fileName: __filename, + class: RecordController.name, + }; + this.ingestionJobCounter = new Counter({ + name: 'ingestion_jobs_created', + help: 'number of ingestion jobs created', + registers: [this.metricsRegistry], + }); + } + + public createRecord: TypedRequestHandlers['createRecord'] = (req, res, next) => { + const logContext = { ...this.logContext, function: this.createRecord.name }; + try { + const job = this.manager.createIngestion(req.body); + this.ingestionJobCounter.inc(1); + return res.status(StatusCodes.CREATED).json(job); + } catch (err) { + this.logger.error({ msg: 'failed to create ingestion job', logContext, err }); + return next(err); + } + }; + + public deleteRecord: TypedRequestHandlers['deleteRecord'] = (req, res, next) => { + const logContext = { ...this.logContext, function: this.deleteRecord.name }; + const { id } = req.params; + try { + const job = this.manager.deleteRecord(id); + return res.status(StatusCodes.OK).json(job); + } catch (err) { + this.logger.error({ msg: 'failed to create delete job', logContext, err, recordId: id }); + return next(err); + } + }; + + public updateRecord: TypedRequestHandlers['updateRecord'] = (req, res, next) => { + const logContext = { ...this.logContext, function: this.updateRecord.name }; + const { id } = req.params; + try { + const ack = this.manager.updateMetadata(id, req.body); + return res.status(StatusCodes.OK).json(ack); + } catch (err) { + this.logger.error({ msg: 'failed to update record metadata', logContext, err, recordId: id }); + return next(err); + } + }; + + public updateRecordStatus: TypedRequestHandlers['updateRecordStatus'] = (req, res, next) => { + const logContext = { ...this.logContext, function: this.updateRecordStatus.name }; + const { id } = req.params; + try { + const ack = this.manager.updateStatus(id, req.body); + return res.status(StatusCodes.OK).json(ack); + } catch (err) { + this.logger.error({ msg: 'failed to update record status', logContext, err, recordId: id }); + return next(err); + } + }; +} diff --git a/src/record/models/recordManager.ts b/src/record/models/recordManager.ts new file mode 100644 index 0000000..a152175 --- /dev/null +++ b/src/record/models/recordManager.ts @@ -0,0 +1,47 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { inject, injectable } from 'tsyringe'; +import type { components } from '@openapi'; +import { SERVICES } from '@common/constants'; +import type { LogContext } from '@common/interfaces'; + +export type IngestionPayload = components['schemas']['ingestionPayload']; +export type UpdatePayload = components['schemas']['updatePayload']; +export type StatusPayload = components['schemas']['statusPayload']; +export type JobResponse = components['schemas']['jobResponse']; +export type AckResponse = components['schemas']['ackResponse']; + +@injectable() +export class RecordManager { + private readonly logContext: LogContext; + + public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) { + this.logContext = { + fileName: __filename, + class: RecordManager.name, + }; + } + + public createIngestion(payload: IngestionPayload): JobResponse { + const logContext = { ...this.logContext, function: this.createIngestion.name }; + this.logger.info({ msg: 'creating ingestion job', logContext, modelPath: payload.modelPath, tilesetFilename: payload.tilesetFilename }); + return { jobId: 'stub-ingestion-job-id', status: 'PENDING' }; + } + + public deleteRecord(id: string): JobResponse { + const logContext = { ...this.logContext, function: this.deleteRecord.name }; + this.logger.info({ msg: 'creating delete job', logContext, recordId: id }); + return { jobId: 'stub-delete-job-id', status: 'PENDING' }; + } + + public updateMetadata(id: string, update: UpdatePayload): AckResponse { + const logContext = { ...this.logContext, function: this.updateMetadata.name }; + this.logger.info({ msg: 'updating record metadata', logContext, recordId: id, fields: Object.keys(update) }); + return { message: `metadata update accepted for record ${id}` }; + } + + public updateStatus(id: string, payload: StatusPayload): AckResponse { + const logContext = { ...this.logContext, function: this.updateStatus.name }; + this.logger.info({ msg: 'updating record status', logContext, recordId: id, status: payload.status }); + return { message: `status ${payload.status} accepted for record ${id}` }; + } +} diff --git a/src/record/routes/recordRouter.ts b/src/record/routes/recordRouter.ts new file mode 100644 index 0000000..4a5b495 --- /dev/null +++ b/src/record/routes/recordRouter.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import type { FactoryFunction } from 'tsyringe'; +import { RecordController } from '../controllers/recordController'; + +const recordRouterFactory: FactoryFunction = (dependencyContainer) => { + const router = Router(); + const controller = dependencyContainer.resolve(RecordController); + + router.post('/', controller.createRecord); + router.patch('/status/:id', controller.updateRecordStatus); + router.delete('/:id', controller.deleteRecord); + router.patch('/:id', controller.updateRecord); + + return router; +}; + +export const RECORD_ROUTER_SYMBOL = Symbol('recordRouterFactory'); + +export { recordRouterFactory }; 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 a0e3f4a..0000000 --- a/src/resourceName/routes/resourceNameRouter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Router } from 'express'; -import type { 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 5554627..3e2fc98 100644 --- a/src/serverBuilder.ts +++ b/src/serverBuilder.ts @@ -10,8 +10,7 @@ import { collectMetricsExpressMiddleware } from '@map-colonies/prometheus'; import { Registry } from 'prom-client'; 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 { RECORD_ROUTER_SYMBOL } from './record/routes/recordRouter'; @injectable() export class ServerBuilder { @@ -21,8 +20,7 @@ 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(RECORD_ROUTER_SYMBOL) private readonly recordRouter: Router ) { this.serverInstance = express(); } @@ -45,8 +43,7 @@ export class ServerBuilder { } private buildRoutes(): void { - this.serverInstance.use('/resourceName', this.resourceNameRouter); - this.serverInstance.use('/anotherResource', this.anotherResourceRouter); + this.serverInstance.use('/record', this.recordRouter); this.buildDocsRoutes(); } diff --git a/tests/integration/anotherResource/anotherResourceName.spec.ts b/tests/integration/anotherResource/anotherResourceName.spec.ts deleted file mode 100644 index fce250d..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, type RequestSender } from '@map-colonies/openapi-supertest'; -import type { 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: await 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/record/record.spec.ts b/tests/integration/record/record.spec.ts new file mode 100644 index 0000000..9ac5a72 --- /dev/null +++ b/tests/integration/record/record.spec.ts @@ -0,0 +1,101 @@ +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, type RequestSender } from '@map-colonies/openapi-supertest'; +import type { paths, operations } from '@openapi'; +import { getApp } from '@src/app'; +import { SERVICES } from '@common/constants'; +import { initConfig } from '@src/common/config'; + +const validIngestionPayload = { + modelPath: '/shared/models/afula', + tilesetFilename: 'tileset.json', + metadata: { productName: 'afula', productType: 'PHOTO_REALISTIC' }, +}; + +describe('record', function () { + let requestSender: RequestSender; + + beforeAll(async function () { + await initConfig(true); + }); + + beforeEach(async function () { + 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); + }); + + describe('POST /record', function () { + it('should return 201 and a job response for a valid ingestion request', async function () { + const response = await requestSender.createRecord({ requestBody: validIngestionPayload }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.CREATED); + + const body = response.body as paths['/record']['post']['responses']['201']['content']['application/json']; + + expect(body.jobId).toBeTypeOf('string'); + expect(body.status).toBeTypeOf('string'); + }); + + it('should return 400 when a required field is missing', async function () { + const response = await requestSender.createRecord({ + // @ts-expect-error intentionally invalid: missing tilesetFilename and metadata + requestBody: { modelPath: '/shared/models/afula' }, + }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + }); + }); + + describe('DELETE /record/{id}', function () { + it('should return 200 and a job response', async function () { + const response = await requestSender.deleteRecord({ pathParams: { id: 'rec-1' } }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + + const body = response.body as paths['/record/{id}']['delete']['responses']['200']['content']['application/json']; + + expect(body.jobId).toBeTypeOf('string'); + }); + }); + + describe('PATCH /record/{id}', function () { + it('should return 200 and an ack for a metadata update', async function () { + const response = await requestSender.updateRecord({ pathParams: { id: 'rec-1' }, requestBody: { description: 'updated' } }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + expect(response.body.message).toBeTypeOf('string'); + }); + }); + + describe('PATCH /record/status/{id}', function () { + it('should return 200 and an ack for a valid status change', async function () { + const response = await requestSender.updateRecordStatus({ pathParams: { id: 'rec-1' }, requestBody: { status: 'PUBLISHED' } }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + }); + + it('should return 400 for an invalid status value', async function () { + const response = await requestSender.updateRecordStatus({ + pathParams: { id: 'rec-1' }, + // @ts-expect-error intentionally invalid status enum value + requestBody: { status: 'NOT_A_STATUS' }, + }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + }); + }); +}); diff --git a/tests/integration/resourceName/resourceName.spec.ts b/tests/integration/resourceName/resourceName.spec.ts deleted file mode 100644 index be26f69..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, type RequestSender } from '@map-colonies/openapi-supertest'; -import type { 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: await 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 c086bb4..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(async function () { - anotherResourceManager = new AnotherResourceManager(await 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/record/models/recordManager.spec.ts b/tests/unit/record/models/recordManager.spec.ts new file mode 100644 index 0000000..a6d87ab --- /dev/null +++ b/tests/unit/record/models/recordManager.spec.ts @@ -0,0 +1,51 @@ +import { jsLogger } from '@map-colonies/js-logger'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { RecordManager, type IngestionPayload } from '@src/record/models/recordManager'; + +describe('RecordManager', function () { + let manager: RecordManager; + + beforeEach(async function () { + manager = new RecordManager(await jsLogger({ enabled: false })); + }); + + describe('createIngestion', function () { + it('should return a job response with a jobId and status', function () { + const payload: IngestionPayload = { + modelPath: '/shared/models/afula', + tilesetFilename: 'tileset.json', + metadata: { productName: 'afula' }, + }; + + const result = manager.createIngestion(payload); + + expect(result.jobId).toBeTypeOf('string'); + expect(result.status).toBeTypeOf('string'); + }); + }); + + describe('deleteRecord', function () { + it('should return a job response', function () { + const result = manager.deleteRecord('rec-1'); + + expect(result.jobId).toBeTypeOf('string'); + expect(result.status).toBeTypeOf('string'); + }); + }); + + describe('updateMetadata', function () { + it('should return an ack referencing the record id', function () { + const result = manager.updateMetadata('rec-1', { description: 'x' }); + + expect(result.message).toContain('rec-1'); + }); + }); + + describe('updateStatus', function () { + it('should return an ack referencing the requested status', function () { + const result = manager.updateStatus('rec-1', { status: 'PUBLISHED' }); + + expect(result.message).toContain('PUBLISHED'); + }); + }); +}); diff --git a/tests/unit/resourceName/models/resourceNameModel.spec.ts b/tests/unit/resourceName/models/resourceNameModel.spec.ts deleted file mode 100644 index 61d9bbe..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(async function () { - resourceNameManager = new ResourceNameManager(await 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'); - }); - }); -}); From 4ad771646f695353340b8bddd3a05b40814d7b9c Mon Sep 17 00:00:00 2001 From: TULCHINSKI LIRAN Date: Mon, 5 Oct 2026 15:46:59 +0300 Subject: [PATCH 3/4] chore: remove the reduntant metrics --- src/record/controllers/recordController.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/record/controllers/recordController.ts b/src/record/controllers/recordController.ts index 65a4bce..df3e5ad 100644 --- a/src/record/controllers/recordController.ts +++ b/src/record/controllers/recordController.ts @@ -1,7 +1,6 @@ import type { Logger } from '@map-colonies/js-logger'; import { StatusCodes } 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 type { LogContext } from '@common/interfaces'; @@ -10,29 +9,21 @@ import { RecordManager } from '../models/recordManager'; @injectable() export class RecordController { private readonly logContext: LogContext; - private readonly ingestionJobCounter: Counter; public constructor( @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(RecordManager) private readonly manager: RecordManager, - @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry + @inject(RecordManager) private readonly manager: RecordManager ) { this.logContext = { fileName: __filename, class: RecordController.name, }; - this.ingestionJobCounter = new Counter({ - name: 'ingestion_jobs_created', - help: 'number of ingestion jobs created', - registers: [this.metricsRegistry], - }); } public createRecord: TypedRequestHandlers['createRecord'] = (req, res, next) => { const logContext = { ...this.logContext, function: this.createRecord.name }; try { const job = this.manager.createIngestion(req.body); - this.ingestionJobCounter.inc(1); return res.status(StatusCodes.CREATED).json(job); } catch (err) { this.logger.error({ msg: 'failed to create ingestion job', logContext, err }); From f4a9bf7ffc4f2e3cd3c650879ae624e1b6c48e39 Mon Sep 17 00:00:00 2001 From: TULCHINSKI LIRAN Date: Mon, 5 Oct 2026 17:26:16 +0300 Subject: [PATCH 4/4] chore: update the openapi ingestion payload --- openapi3.yaml | 57 ++++++++++++++++--- src/openapi.d.ts | 39 ++++++++++--- src/record/models/recordManager.ts | 2 +- tests/integration/record/record.spec.ts | 20 ++++--- .../unit/record/models/recordManager.spec.ts | 11 +++- 5 files changed, 102 insertions(+), 27 deletions(-) diff --git a/openapi3.yaml b/openapi3.yaml index 74a26a0..807eae0 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -149,19 +149,58 @@ components: type: object required: - modelPath - - tilesetFilename - - metadata + - productShapefilePath + - metadataShapefilePath + - productName + - productId + - productType + - classification + - region properties: modelPath: type: string - description: Path on the shared storage to the 3DTiles folder or 3TZ archive - tilesetFilename: + description: Path to the model inside the data/ folder - a tileset.json (3D Tiles) or a .3tz file + example: /shared/models/afula/data/tileset.json + productShapefilePath: type: string - description: The tileset entry name (e.g. tileset.json) - metadata: - type: object - description: 3D record metadata (business validation applied downstream) - additionalProperties: true + description: Path to the footprint shapefile (shape/Product.shp) + example: /shared/models/afula/shape/Product.shp + metadataShapefilePath: + type: string + description: Path to the parts metadata shapefile (shape/ShapeMetadata.shp) + example: /shared/models/afula/shape/ShapeMetadata.shp + productName: + type: string + maxLength: 120 + description: Pre-filled from the prodName shapefile attribute, user editable + productId: + type: string + description: Deterministic id derived from the prodID attribute and the storage id + productType: + type: string + enum: + - 3DPhotoRealistic + - 3DPhotoRealisticBest + - 3DSemantic + - 3DSemanticMesh + - QuantizedMeshDTMBest + - QuantizedMeshDSMBest + - 3DPointCloud + productSubType: + type: string + description: + type: string + region: + type: array + minItems: 1 + items: + type: string + classification: + type: string + pattern: '^[a-zA-Z0-9]+$' + keywords: + type: string + additionalProperties: false updatePayload: type: object description: Partial metadata fields to update diff --git a/src/openapi.d.ts b/src/openapi.d.ts index ca8c610..f6c9b81 100644 --- a/src/openapi.d.ts +++ b/src/openapi.d.ts @@ -70,14 +70,39 @@ export type components = { message: string; }; ingestionPayload: { - /** @description Path on the shared storage to the 3DTiles folder or 3TZ archive */ + /** + * @description Path to the model inside the data/ folder - a tileset.json (3D Tiles) or a .3tz file + * @example /shared/models/afula/data/tileset.json + */ modelPath: string; - /** @description The tileset entry name (e.g. tileset.json) */ - tilesetFilename: string; - /** @description 3D record metadata (business validation applied downstream) */ - metadata: { - [key: string]: unknown; - }; + /** + * @description Path to the footprint shapefile (shape/Product.shp) + * @example /shared/models/afula/shape/Product.shp + */ + productShapefilePath: string; + /** + * @description Path to the parts metadata shapefile (shape/ShapeMetadata.shp) + * @example /shared/models/afula/shape/ShapeMetadata.shp + */ + metadataShapefilePath: string; + /** @description Pre-filled from the prodName shapefile attribute, user editable */ + productName: string; + /** @description Deterministic id derived from the prodID attribute and the storage id */ + productId: string; + /** @enum {string} */ + productType: + | '3DPhotoRealistic' + | '3DPhotoRealisticBest' + | '3DSemantic' + | '3DSemanticMesh' + | 'QuantizedMeshDTMBest' + | 'QuantizedMeshDSMBest' + | '3DPointCloud'; + productSubType?: string; + description?: string; + region: string[]; + classification: string; + keywords?: string; }; /** @description Partial metadata fields to update */ updatePayload: { diff --git a/src/record/models/recordManager.ts b/src/record/models/recordManager.ts index a152175..9c01be9 100644 --- a/src/record/models/recordManager.ts +++ b/src/record/models/recordManager.ts @@ -23,7 +23,7 @@ export class RecordManager { public createIngestion(payload: IngestionPayload): JobResponse { const logContext = { ...this.logContext, function: this.createIngestion.name }; - this.logger.info({ msg: 'creating ingestion job', logContext, modelPath: payload.modelPath, tilesetFilename: payload.tilesetFilename }); + this.logger.info({ msg: 'creating ingestion job', logContext, productId: payload.productId, modelPath: payload.modelPath }); return { jobId: 'stub-ingestion-job-id', status: 'PENDING' }; } diff --git a/tests/integration/record/record.spec.ts b/tests/integration/record/record.spec.ts index 9ac5a72..95caa17 100644 --- a/tests/integration/record/record.spec.ts +++ b/tests/integration/record/record.spec.ts @@ -7,11 +7,17 @@ import type { paths, operations } from '@openapi'; import { getApp } from '@src/app'; import { SERVICES } from '@common/constants'; import { initConfig } from '@src/common/config'; - -const validIngestionPayload = { - modelPath: '/shared/models/afula', - tilesetFilename: 'tileset.json', - metadata: { productName: 'afula', productType: 'PHOTO_REALISTIC' }, +import type { IngestionPayload } from '@src/record/models/recordManager'; + +const validIngestionPayload: IngestionPayload = { + modelPath: '/shared/models/afula/data/tileset.json', + productShapefilePath: '/shared/models/afula/shape/Product.shp', + metadataShapefilePath: '/shared/models/afula/shape/ShapeMetadata.shp', + productName: 'afula', + productId: 'afula-1', + productType: '3DPhotoRealistic', + classification: '4', + region: ['israel'], }; describe('record', function () { @@ -47,8 +53,8 @@ describe('record', function () { it('should return 400 when a required field is missing', async function () { const response = await requestSender.createRecord({ - // @ts-expect-error intentionally invalid: missing tilesetFilename and metadata - requestBody: { modelPath: '/shared/models/afula' }, + // @ts-expect-error intentionally invalid: missing shapefile paths and metadata fields + requestBody: { modelPath: '/shared/models/afula/data/tileset.json' }, }); expect(response).toSatisfyApiSpec(); diff --git a/tests/unit/record/models/recordManager.spec.ts b/tests/unit/record/models/recordManager.spec.ts index a6d87ab..1187469 100644 --- a/tests/unit/record/models/recordManager.spec.ts +++ b/tests/unit/record/models/recordManager.spec.ts @@ -12,9 +12,14 @@ describe('RecordManager', function () { describe('createIngestion', function () { it('should return a job response with a jobId and status', function () { const payload: IngestionPayload = { - modelPath: '/shared/models/afula', - tilesetFilename: 'tileset.json', - metadata: { productName: 'afula' }, + modelPath: '/shared/models/afula/data/tileset.json', + productShapefilePath: '/shared/models/afula/shape/Product.shp', + metadataShapefilePath: '/shared/models/afula/shape/ShapeMetadata.shp', + productName: 'afula', + productId: 'afula-1', + productType: '3DPhotoRealistic', + classification: '4', + region: ['israel'], }; const result = manager.createIngestion(payload);