Skip to content

Commit ebca4ee

Browse files
committed
fix: restore Transitous API-key scaffolding
1 parent 67d0397 commit ebca4ee

14 files changed

Lines changed: 492 additions & 25 deletions

File tree

apps/api/src/routes/admin-compose.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { dirname, resolve } from "node:path";
2-
import { services } from "@openmapx/core/server";
1+
import { repoPaths, services } from "@openmapx/core/server";
32
import type { FastifyInstance } from "fastify";
43
import { applyHardlinksFromPlan, renderAndPersistCompose } from "../services/admin-ops";
54
import { resolveAllServiceConfigs } from "../services/service-config-resolver";
@@ -14,7 +13,7 @@ const { buildAppApiServiceEnv, renderCompose } = services;
1413
// render` writes to disk. The file isn't actually written here; this only keeps
1514
// the previewed YAML byte-identical to what an operator would see after running
1615
// the CLI render.
17-
const COMPOSE_OUT_DIR = resolve(dirname(process.cwd()), "..", "infra", "docker");
16+
const COMPOSE_OUT_DIR = repoPaths().infraDir;
1817

1918
export async function registerAdminComposeRoutes(
2019
// biome-ignore lint/suspicious/noExplicitAny: accept any Fastify logger variant

apps/api/src/services/service-registry.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { existsSync, readFileSync } from "node:fs";
2-
import { join, resolve } from "node:path";
3-
import { services } from "@openmapx/core/server";
2+
import { join } from "node:path";
3+
import { findRepoRoot, services } from "@openmapx/core/server";
44

55
const {
66
DEFAULT_SELECTED_SERVICE_IDS,
@@ -32,7 +32,7 @@ function readSelectionFile(rootDir: string): string[] | null {
3232
export async function initServiceRegistry(): Promise<void> {
3333
// Reset warnings on each init so a hot-reload path doesn't accumulate stale entries.
3434
warnings.length = 0;
35-
const rootDir = resolve(process.cwd(), "..", "..");
35+
const rootDir = findRepoRoot();
3636
registry = new ServiceRegistry({ rootDir, warnings });
3737
await registry.load();
3838
const envSelection = parseServiceIdList(process.env[SERVICE_SELECTION_ENV]);

apps/api/src/utils/docker-compose.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
import { execFile as execFileCb, spawn } from "node:child_process";
22
import type { ServerResponse } from "node:http";
3-
import { join, resolve } from "node:path";
43
import { promisify } from "node:util";
4+
import { repoPaths } from "@openmapx/core/server";
55

66
const execFile = promisify(execFileCb);
77

88
function composePath(): string {
9-
// apps/api is two levels down from repo root; generated compose lives in infra/docker/
10-
return join(
11-
resolve(process.cwd(), "..", ".."),
12-
"infra",
13-
"docker",
14-
"docker-compose.generated.yml",
15-
);
9+
return repoPaths().composeOutPath;
1610
}
1711

1812
export interface PsEntry {

docs/INTEGRATIONS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@
546546

547547
## geocoding-pelias
548548

549-
### Pelias Search — `http://localhost:4000/v1/search`
549+
### Pelias Search — `http://localhost:4300/v1/search`
550550
- Data sent: Search query text, result limit, language
551551
- Data received: GeoJSON features with gid, label, name, layer (venue/address/street/locality), confidence, coordinates
552552
- Purpose: Forward geocoding via self-hosted Pelias instance
@@ -562,10 +562,10 @@
562562
- End-user data exposure: Server-only (self-hosted)
563563
- DPA: Not applicable (self-hosted software)
564564
- Coverage: Configurable (depends on imported data)
565-
- Env vars: `PELIAS_URL` — optional (default: `http://localhost:4000`)
565+
- Env vars: `PELIAS_URL` — optional (default: `http://localhost:4300`)
566566
- Self-hostable: Yes — already self-hosted; Docker setup at <https://github.com/pelias/docker>
567567

568-
### Pelias Reverse — `http://localhost:4000/v1/reverse`
568+
### Pelias Reverse — `http://localhost:4300/v1/reverse`
569569
- Data sent: Coordinates (latitude, longitude), language
570570
- Data received: GeoJSON feature with label (address), locality, region
571571
- Purpose: Reverse geocoding via self-hosted Pelias
@@ -582,7 +582,7 @@
582582
- Env vars: `PELIAS_URL` — optional
583583
- Self-hostable: Yes
584584

585-
### Pelias Autocomplete — `http://localhost:4000/v1/autocomplete`
585+
### Pelias Autocomplete — `http://localhost:4300/v1/autocomplete`
586586
- Data sent: Partial search query text, result limit, language
587587
- Data received: GeoJSON features with name, locality/region/country (sublabel), coordinates, layer type
588588
- Purpose: Autocomplete/typeahead optimized for partial input via self-hosted Pelias

integrations/geocoding-pelias/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { peliasService, setPeliasUrl } from "./provider.js";
44
export function setup(ctx: IntegrationContext): void {
55
const resolved = ctx.getRequiredService("pelias");
66
const url =
7-
resolved?.url ?? (ctx.config.endpoint as string | undefined) ?? "http://localhost:4000";
7+
resolved?.url ?? (ctx.config.endpoint as string | undefined) ?? "http://localhost:4300";
88

99
setPeliasUrl(url);
1010

integrations/geocoding-pelias/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
},
2929
"healthCheck": {
3030
"type": "http",
31-
"urlTemplate": "http://localhost:4000/v1/search?text=test&size=1"
31+
"urlTemplate": "http://localhost:4300/v1/search?text=test&size=1"
3232
},
3333
"quality": "built-in",
3434
"category": "Geocoding",

integrations/geocoding-pelias/provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { GeocodingProviderImpl } from "./types.js";
22
/**
33
* Pelias geocoding client (self-hosted).
4-
* Set PELIAS_URL to your Pelias instance (e.g. http://localhost:4000).
4+
* Set PELIAS_URL to your Pelias instance (e.g. http://localhost:4300).
55
* https://github.com/pelias/pelias
66
*/
77

88
import type { AutocompleteResult, ReverseGeocodingResult, SearchResult } from "@openmapx/core";
99

1010
// Populated by setup(ctx); see setPeliasUrl.
11-
let PELIAS_URL = "http://localhost:4000";
11+
let PELIAS_URL = "http://localhost:4300";
1212

1313
/** Update the Pelias base URL (called from setup() when service registry resolves it). */
1414
export function setPeliasUrl(url: string): void {

packages/cli/__tests__/cli-osrm-graph.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ describe("resolveOsmPbfForOsrm", () => {
4444
});
4545

4646
describe("buildOsrmGraph", () => {
47+
it("rejects planet-scale inputs", async () => {
48+
const pbf = join(tmp, "infra", "docker", "data", "osm", "planet.osm.pbf");
49+
writeFileSync(pbf, "PBF");
50+
51+
await expect(
52+
buildOsrmGraph({
53+
rootDir: tmp,
54+
region: "planet",
55+
image: "ghcr.io/project-osrm/osrm-backend:latest",
56+
}),
57+
).rejects.toThrow("OSRM cannot build planet-scale graphs");
58+
});
59+
4760
it("runs OSRM extract, partition, and customize in the OSRM Docker image", async () => {
4861
const pbf = join(tmp, "infra", "docker", "data", "osm", "europe-germany.osm.pbf");
4962
writeFileSync(pbf, "PBF");
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
5+
import {
6+
type CommandRunner,
7+
DEFAULT_TRANSITOUS_REPO_URL,
8+
generateTransitousApiKeys,
9+
} from "../src/lib/transitous-api-keys";
10+
11+
let tmp: string;
12+
13+
function writeTransitousCatalog(catalogDir: string): void {
14+
mkdirSync(join(catalogDir, "feeds"), { recursive: true });
15+
mkdirSync(join(catalogDir, "transitland-atlas", "feeds", "nested"), { recursive: true });
16+
17+
writeFileSync(
18+
join(catalogDir, "feeds", "us-ca.json"),
19+
JSON.stringify({
20+
sources: [
21+
{ name: "NeedsKey", "transitland-atlas-id": "atlas-auth-1" },
22+
{ name: "NoAuthNeeded", "transitland-atlas-id": "atlas-open" },
23+
{ name: "AlreadyInCatalog", "transitland-atlas-id": "atlas-auth-1", "api-key": "inline" },
24+
{ name: "CustomUrl", "transitland-atlas-id": "atlas-auth-1", "url-override": "https://x" },
25+
],
26+
}),
27+
"utf-8",
28+
);
29+
writeFileSync(
30+
join(catalogDir, "feeds", "ca-qc.json"),
31+
JSON.stringify({
32+
sources: [{ name: "QuebecAgency", "transitland-atlas-id": "atlas-auth-2", skip: true }],
33+
}),
34+
"utf-8",
35+
);
36+
writeFileSync(
37+
join(catalogDir, "transitland-atlas", "feeds", "nested", "agencies.dmfr.json"),
38+
JSON.stringify({
39+
feeds: [
40+
{ id: "atlas-auth-1", authorization: { type: "api-key" } },
41+
{ id: "atlas-auth-2", authorization: { type: "token" } },
42+
{ id: "atlas-open" },
43+
],
44+
}),
45+
"utf-8",
46+
);
47+
}
48+
49+
beforeEach(() => {
50+
tmp = mkdtempSync(join(tmpdir(), "openmapx-transitous-api-keys-"));
51+
writeFileSync(join(tmp, "pnpm-workspace.yaml"), "packages: []\n");
52+
mkdirSync(join(tmp, "services"), { recursive: true });
53+
mkdirSync(join(tmp, "infra", "docker", "services", "transitous"), { recursive: true });
54+
});
55+
56+
afterEach(() => {
57+
rmSync(tmp, { recursive: true, force: true });
58+
});
59+
60+
describe("generateTransitousApiKeys", () => {
61+
it("clones the Transitous catalog and preserves existing filled keys", async () => {
62+
const outputPath = join(tmp, "infra", "docker", "services", "transitous", "api-keys.json");
63+
writeFileSync(
64+
outputPath,
65+
JSON.stringify(
66+
{
67+
"us-ca/NeedsKey": "preserved-secret",
68+
"legacy/no-longer-needed": "stale-value",
69+
},
70+
null,
71+
2,
72+
),
73+
"utf-8",
74+
);
75+
76+
const calls: Array<{ command: string; args: string[]; cwd?: string }> = [];
77+
const runner: CommandRunner = async (command, args, opts) => {
78+
calls.push({ command, args, cwd: opts.cwd });
79+
if (command === "git" && args[0] === "clone") {
80+
const targetDir = args.at(-1);
81+
if (typeof targetDir === "string") {
82+
writeTransitousCatalog(targetDir);
83+
}
84+
}
85+
};
86+
87+
const result = await generateTransitousApiKeys({ rootDir: tmp, runner });
88+
89+
expect(result.requiredCount).toBe(2);
90+
expect(result.preservedCount).toBe(1);
91+
expect(result.droppedCount).toBe(1);
92+
expect(result.outputPath).toBe(outputPath);
93+
expect(JSON.parse(readFileSync(outputPath, "utf-8")) as Record<string, string>).toStrictEqual({
94+
"ca-qc/QuebecAgency": "",
95+
"us-ca/NeedsKey": "preserved-secret",
96+
});
97+
expect(calls).toEqual([
98+
{
99+
command: "git",
100+
args: [
101+
"clone",
102+
"--depth",
103+
"1",
104+
"--recurse-submodules",
105+
"--shallow-submodules",
106+
DEFAULT_TRANSITOUS_REPO_URL,
107+
join(tmp, "infra", "docker", "data", ".transitous-catalog"),
108+
],
109+
cwd: join(tmp, "infra", "docker", "data"),
110+
},
111+
]);
112+
});
113+
114+
it("uses an existing cached catalog and continues when git pull fails", async () => {
115+
const catalogDir = join(tmp, "infra", "docker", "data", ".transitous-catalog");
116+
mkdirSync(join(catalogDir, ".git"), { recursive: true });
117+
writeTransitousCatalog(catalogDir);
118+
119+
const calls: Array<{ command: string; args: string[]; cwd?: string }> = [];
120+
const runner: CommandRunner = async (command, args, opts) => {
121+
calls.push({ command, args, cwd: opts.cwd });
122+
if (command === "git" && args[2] === "pull") {
123+
throw new Error("network unavailable");
124+
}
125+
};
126+
127+
const result = await generateTransitousApiKeys({ rootDir: tmp, runner });
128+
129+
expect(result.requiredCount).toBe(2);
130+
expect(result.preservedCount).toBe(0);
131+
expect(result.droppedCount).toBe(0);
132+
const outputPath = join(tmp, "infra", "docker", "services", "transitous", "api-keys.json");
133+
expect(JSON.parse(readFileSync(outputPath, "utf-8")) as Record<string, string>).toStrictEqual({
134+
"ca-qc/QuebecAgency": "",
135+
"us-ca/NeedsKey": "",
136+
});
137+
expect(calls).toEqual([
138+
{
139+
command: "git",
140+
args: ["-C", catalogDir, "reset", "--hard", "HEAD"],
141+
cwd: catalogDir,
142+
},
143+
{
144+
command: "git",
145+
args: ["-C", catalogDir, "pull", "--ff-only"],
146+
cwd: join(tmp, "infra", "docker", "data"),
147+
},
148+
{
149+
command: "git",
150+
args: ["-C", catalogDir, "submodule", "update", "--init", "--checkout", "--depth", "1"],
151+
cwd: join(tmp, "infra", "docker", "data"),
152+
},
153+
]);
154+
});
155+
});

packages/cli/src/commands/data.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { applyGeneratedHardlinks } from "../lib/hardlinks";
1313
import { log, table } from "../lib/output";
1414
import { repoPaths } from "../lib/paths";
1515
import { buildServices, resolveDataBuildServiceId } from "../lib/service-builds";
16+
import { generateTransitousApiKeys } from "../lib/transitous-api-keys";
1617
import { renderComposeForRepo } from "./compose";
1718

1819
const { DataManagerClient } = services;
@@ -274,6 +275,42 @@ export function registerDataCommands(program: Command): void {
274275
}
275276
});
276277

278+
data
279+
.command("generate-api-keys")
280+
.description(
281+
"Generate Transitous API-key template at infra/docker/services/transitous/api-keys.json",
282+
)
283+
.option("--repo-url <url>", "Override Transitous catalog git URL")
284+
.option(
285+
"--output <path>",
286+
"Override output path (default: infra/docker/services/transitous/api-keys.json)",
287+
)
288+
.action(async (options: { repoUrl?: string; output?: string }) => {
289+
try {
290+
log.dim("Syncing Transitous catalog and scanning feeds that require API keys...");
291+
const result = await generateTransitousApiKeys({
292+
transitousRepoUrl: options.repoUrl,
293+
outputPath: options.output,
294+
});
295+
const preserved =
296+
result.preservedCount > 0
297+
? ` (${result.preservedCount} existing value${result.preservedCount === 1 ? "" : "s"} preserved)`
298+
: "";
299+
log.ok(
300+
`Generated ${result.outputPath} with ${result.requiredCount} API-key slot${result.requiredCount === 1 ? "" : "s"}${preserved}`,
301+
);
302+
if (result.droppedCount > 0) {
303+
log.warn(
304+
`Dropped ${result.droppedCount} stale key${result.droppedCount === 1 ? "" : "s"} no longer required by the current Transitous catalog`,
305+
);
306+
}
307+
log.dim("Fill in missing values, then run: openmapx data download gtfs");
308+
} catch (err) {
309+
log.err(`generate-api-keys failed: ${(err as Error).message}`);
310+
process.exit(1);
311+
}
312+
});
313+
277314
data
278315
.command("status")
279316
.description("Show downloaded datasets and sizes")

0 commit comments

Comments
 (0)