Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/count-aggregator-api-raw-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mapsight/count-aggregator-api": minor
---

Add `GET /{type}/raw-values` to the OpenAPI contract, typed client, and URL helpers.
5 changes: 5 additions & 0 deletions .changeset/count-aggregator-ui-raw-values.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@mapsight/count-aggregator-ui": minor
---

Add wizard Rohwerte mode (`features.rawValues`) that fetches unaggregated raw values and CSV.
38 changes: 38 additions & 0 deletions apps/showcase/count-aggregator-mock/mock-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,44 @@ export function resolveCountAggregatorMockRequest(
return jsonResponse(stationList);
}

const rawValuesMatch = subPath.match(/^\/([^/]+)\/raw-values$/);
if (rawValuesMatch !== null) {
const stationIds = parseStationIds(url.searchParams);
if (stationIds.length === 0) {
return jsonResponse({message: "stationIds is required"}, 400);
}

const fromYmd = (url.searchParams.get("from") ?? "2026-06-01").slice(
0,
10,
);
const toYmd = (url.searchParams.get("to") ?? fromYmd).slice(0, 10);
const map = buildValuesMap(stationIds, fromYmd, toYmd, "daily");
const rawMap: Record<string, unknown> = {};

for (const [stationId, series] of Object.entries(map)) {
const entry = series as {
id: number;
stationId: string;
values: {datetime: string; value: number}[];
};
rawMap[stationId] = {
id: entry.id,
stationId: entry.stationId,
values: entry.values,
};
}

if (format === "csv") {
return textResponse(
buildCsv(stationIds, fromYmd, toYmd, "daily"),
"text/csv; charset=utf-8",
);
}

return jsonResponse(rawMap);
}

const valuesMatch = subPath.match(
/^\/([^/]+)\/values\/(\d{4}-\d{2}-\d{2})\/(\d{4}-\d{2}-\d{2})\/([^/]+)$/,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function CountAggregatorDemoBootstrap(): ReactElement {
uiVariant: "stepped",
features: {
resolutionSelect: true,
rawValues: true,
chartTypeSelect: true,
export: true,
presets: false,
Expand Down
1 change: 1 addition & 0 deletions packages/count-aggregator-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Use these helpers for JSON API calls:
| `listStationTypes` | `GET /station-types` |
| `listStations` | `GET /:type/stations` |
| `getValues` | `GET /:type/values/:from/:to/:resolution` |
| `getRawValues` | `GET /:type/raw-values` |
| `getLastValues` | `GET /:type/last-values/:resolution` |
| `getStationLastValues` | `GET /:type/:stationId/last-values/:resolution` |
| `getStationSums` | `GET /:type/:stationId/sums` |
Expand Down
184 changes: 184 additions & 0 deletions packages/count-aggregator-api/openapi/count-aggregator.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,141 @@
}
}
},
"/{type}/raw-values": {
"get": {
"operationId": "count-aggregator.public.type.raw-values",
"tags": ["Typed stations"],
"parameters": [
{
"name": "type",
"in": "path",
"required": true,
"description": "Station type identifier. See `GET /station-types` for currently available types.",
"schema": {
"type": "string",
"enum": [
"airQualityCO",
"airQualityIndex",
"airQualityNO2",
"airQualityNO2Index",
"airQualityO3",
"airQualityO3Index",
"airQualityPM10",
"airQualityPM10Index",
"airQualityPM25",
"airQualitySO2",
"airQualityStation",
"bicycleSensorTotal",
"peopleCount",
"waterLevelStation",
"waterLevelSurface",
"waterLevelUnderground",
"waterTemp",
"weatherAirPressure",
"weatherHumidity",
"weatherLightingDistance",
"weatherLightnings",
"weatherRain",
"weatherStation",
"weatherSun",
"weatherTemp",
"weatherVaporPressure",
"weatherWindDirection",
"weatherWindSpeed",
"weatherWindSpeedMax"
]
},
"example": "peopleCount"
},
{
"name": "stationIds",
"in": "query",
"required": true,
"description": "Comma-separated MSP station IDs.",
"schema": {},
"example": "75,15"
},
{
"name": "from",
"in": "query",
"description": "Start of an optional query window in local time (`Y-m-d` or `Y-m-d H:i:s`, `T` separator accepted). Must be used together with `to`. When omitted, returns the latest records per station.",
"schema": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}([ T]\\d{2}:\\d{2}:\\d{2})?$"
},
"example": "2026-06-01T00:00:00"
},
{
"name": "to",
"in": "query",
"description": "End of an optional query window in local time (`Y-m-d` or `Y-m-d H:i:s`, `T` separator accepted). Must be used together with `from`.",
"schema": {
"type": "string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}([ T]\\d{2}:\\d{2}:\\d{2})?$"
},
"example": "2026-06-01T12:00:00"
},
{
"name": "limit",
"in": "query",
"description": "Maximum number of exact telemetry records per station. Defaults to 50. Maximum 500.",
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 500
},
"example": 50
},
{
"name": "order",
"in": "query",
"description": "Sort order of records by datetime. Defaults to `desc` (newest first).",
"schema": {
"type": "string",
"enum": ["asc", "desc"]
},
"example": "desc"
},
{
"name": "format",
"in": "query",
"description": "Response serialization format. Omit this parameter or use `json` for JSON (default). Use `csv` to download multi-station raw telemetry as semicolon-separated CSV.",
"schema": {
"type": "string",
"enum": ["json", "csv"]
},
"example": "json"
}
],
"responses": {
"200": {
"description": "Exact stored telemetry records keyed by MSP station id. No aggregation, no quality flag until a column exists.",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"150": {
"id": 150,
"stationId": "138969",
"values": [
{
"datetime": "2026-06-01 12:34:56",
"value": 67.25
}
]
}
},
"additionalProperties": {
"$ref": "#/components/schemas/RawValuesResponse"
}
}
}
}
}
}
}
},
"/{type}/values": {
"get": {
"operationId": "count-aggregator.public.type.values.query",
Expand Down Expand Up @@ -1958,6 +2093,55 @@
"title": "DataValuePoint",
"required": ["datetime"]
},
"RawValuePoint": {
"type": "object",
"example": {
"datetime": "2026-06-01 12:34:56",
"value": 67.25
},
"properties": {
"datetime": {
"type": "string",
"description": "Local station datetime in `Y-m-d H:i:s` format (no timezone offset).",
"pattern": "^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$"
},
"value": {
"type": "number",
"description": "Exact stored numeric measurement. No aggregation."
}
},
"title": "RawValuePoint",
"required": ["datetime", "value"]
},
"RawValuesResponse": {
"type": "object",
"example": {
"id": 150,
"stationId": "138969",
"values": [
{
"datetime": "2026-06-01 12:34:56",
"value": 67.25
}
]
},
"properties": {
"id": {
"type": "integer"
},
"stationId": {
"type": "string"
},
"values": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RawValuePoint"
}
}
},
"required": ["id", "stationId", "values"],
"title": "RawValuesResponse"
},
"DetectedDataProblem": {
"type": "object",
"example": {
Expand Down
38 changes: 24 additions & 14 deletions packages/count-aggregator-api/src/contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
import {readFileSync} from "node:fs";
import path from "node:path";
import {fileURLToPath} from "node:url";

import {describe, expect, it} from "vitest";

import rawOpenApiDocument from "../openapi/count-aggregator.openapi.json";
import {schemas} from "./generated/client.js";

const packageRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
const openApiDocument = JSON.parse(
readFileSync(
path.join(packageRoot, "openapi/count-aggregator.openapi.json"),
"utf8",
),
) as {
const openApiDocument = rawOpenApiDocument as {
paths: Record<string, unknown>;
servers?: unknown;
components: {
Expand Down Expand Up @@ -85,6 +73,28 @@ describe("OpenAPI contract shape", () => {
expect(parameterNames).not.toContain("originIds");
});

it("documents raw-values route", () => {
const operation = (
openApiDocument.paths["/{type}/raw-values"] as
{get?: {parameters?: Array<{name: string}>}} | undefined
)?.get;

const parameterNames =
operation?.parameters?.map((parameter) => parameter.name) ?? [];
expect(parameterNames).toEqual(
expect.arrayContaining([
"stationIds",
"from",
"to",
"limit",
"order",
"format",
]),
);
expect(parameterNames).not.toContain("resolution");
expect(parameterNames).not.toContain("metrics");
});

it("documents datetime query values route", () => {
const operation = (
openApiDocument.paths["/{type}/values"] as
Expand Down
16 changes: 16 additions & 0 deletions packages/count-aggregator-api/src/fixtures/raw-values-map.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"150": {
"id": 150,
"stationId": "138969",
"values": [
{
"datetime": "2026-06-01 12:34:56",
"value": 67.25
},
{
"datetime": "2026-06-01 12:29:56",
"value": 64.5
}
]
}
}
Loading
Loading