Skip to content

Commit b77a933

Browse files
authored
fix: paths and cache (#22)
1 parent caa6be7 commit b77a933

36 files changed

+1651
-1143
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44

55
## [unreleased]
66

7+
## [0.8.0] - 2025-04-08
8+
9+
### Added
10+
11+
- Standardized path handling
12+
13+
### Changed
14+
15+
- Improved Caching
16+
- Encapsulated business logic
17+
718
## [0.7.4] - 2025-03-25
819

920
- Workflow file changes

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ This project helps to expose static metadata using [Open Resource Discovery](htt
66

77
## Usage
88

9+
Replace "/path-to-your-metadata" with the directory, that has your ORD Documents.
10+
911
### Via **Docker**
1012

1113
1. **Local** (default: open)
1214

1315
```bash
1416
docker run -p 8080:8080 \
15-
-v "$(pwd)/example:/app/data" \
17+
-v "$(pwd)/path-to-your-metadata:/app/data" \
1618
ghcr.io/open-resource-discovery/provider-server:latest \
1719
-d /app/data \
1820
--base-url 'http://127.0.0.1:8080'
@@ -21,7 +23,7 @@ docker run -p 8080:8080 \
2123
2. **Local** (basic auth)
2224

2325
```bash
24-
docker run -p 8080:8080 -v "$(pwd)/example:/app/data" \
26+
docker run -p 8080:8080 -v "$(pwd)/path-to-your-metadata:/app/data" \
2527
-e BASIC_AUTH='{"admin":"$2y$05$TjeC./ljKi7VLTBbzjTVyOi6lQBYpzfXiZSfJiGECHVi0eEN6/QG."}' \
2628
ghcr.io/open-resource-discovery/provider-server:latest \
2729
-d /app/data --auth basic --base-url 'http://127.0.0.1:8080'
@@ -50,13 +52,13 @@ docker run -p 8080:8080 \
5052
1. **Local** (default: open)
5153

5254
```bash
53-
npx @open-resource-discovery/provider-server -d ./example --base-url 'http://127.0.0.1:8080'
55+
npx @open-resource-discovery/provider-server -d /path-to-your-metadata --base-url 'http://127.0.0.1:8080'
5456
```
5557

5658
2. **Local** (basic auth)
5759

5860
```bash
59-
BASIC_AUTH='{"admin":"$2y$05$TjeC./ljKi7VLTBbzjTVyOi6lQBYpzfXiZSfJiGECHVi0eEN6/QG."}' npx @open-resource-discovery/provider-server -d ./example --auth basic --base-url 'http://127.0.0.1:8080'
61+
BASIC_AUTH='{"admin":"$2y$05$TjeC./ljKi7VLTBbzjTVyOi6lQBYpzfXiZSfJiGECHVi0eEN6/QG."}' npx @open-resource-discovery/provider-server -d /path-to-your-metadata --auth basic --base-url 'http://127.0.0.1:8080'
6062
```
6163

6264
3. **GitHub** (open)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@open-resource-discovery/provider-server",
3-
"version": "0.7.4",
3+
"version": "0.8.0",
44
"description": "A CLI application or server that takes multiple ORD documents and other metadata files and exposes them as a ORD Provider implementation (ORD Document API)",
55
"engines": {
66
"node": ">=22.8.0",

src/__tests__/e2e.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ORDConfiguration, ORDDocument } from "@open-resource-discovery/specific
33
import path from "path";
44
import { OptAuthMethod, OptSourceType } from "src/model/cli.js";
55
import { startProviderServer } from "src/server.js";
6-
import { ORD_DOCUMENTS_SUB_DIRECTORY } from "../constant.js";
6+
import { PATH_CONSTANTS } from "../constant.js";
77

88
// Mock bcrypt to avoid native module issues in tests
99
jest.mock("bcryptjs", () => ({
@@ -22,7 +22,7 @@ describe("End-to-End Testing", () => {
2222
beforeAll(async () => {
2323
shutdownServer = await startProviderServer({
2424
ordDirectory: path.join(process.cwd(), "src/__tests__/test-files"),
25-
ordDocumentsSubDirectory: ORD_DOCUMENTS_SUB_DIRECTORY,
25+
ordDocumentsSubDirectory: PATH_CONSTANTS.DOCUMENTS_SUBDIRECTORY,
2626
sourceType: OptSourceType.Local,
2727
baseUrl: SERVER_URL,
2828
host: "0.0.0.0",
@@ -40,7 +40,7 @@ describe("End-to-End Testing", () => {
4040

4141
it("should complete full user journey", async () => {
4242
// 1. Discover API
43-
const configResponse = await fetch(`${SERVER_URL}/.well-known/open-resource-discovery`);
43+
const configResponse = await fetch(`${SERVER_URL}${PATH_CONSTANTS.WELL_KNOWN_ENDPOINT}`);
4444
expect(configResponse.status).toBe(200);
4545
const config = (await configResponse.json()) as ORDConfiguration;
4646

src/__tests__/server.integration.test.ts

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
22
import { ORDConfiguration, ORDDocument, ORDV1DocumentDescription } from "@open-resource-discovery/specification";
33
import path from "path";
4-
import {
5-
ORD_DOCUMENTS_SUB_DIRECTORY,
6-
ORD_DOCUMENTS_URL_PATH,
7-
ORD_SERVER_PREFIX_PATH,
8-
WELL_KNOWN_ENDPOINT,
9-
} from "src/constant.js";
4+
import { PATH_CONSTANTS } from "src/constant.js";
105
import { OptAuthMethod, OptSourceType } from "src/model/cli.js";
116
import { ProviderServerOptions, startProviderServer } from "src/server.js";
127

@@ -29,7 +24,7 @@ describe("Server Integration", () => {
2924
beforeAll(async () => {
3025
const options: ProviderServerOptions = {
3126
ordDirectory: LOCAL_DIRECTORY,
32-
ordDocumentsSubDirectory: ORD_DOCUMENTS_SUB_DIRECTORY,
27+
ordDocumentsSubDirectory: PATH_CONSTANTS.DOCUMENTS_SUBDIRECTORY,
3328
sourceType: OptSourceType.Local,
3429
host: TEST_HOST,
3530
port: TEST_PORT,
@@ -49,15 +44,15 @@ describe("Server Integration", () => {
4944

5045
describe("Well-Known Endpoint", () => {
5146
it("should return ORD configuration without authentication", async () => {
52-
const response = await fetch(`${SERVER_URL}${WELL_KNOWN_ENDPOINT}`);
47+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.WELL_KNOWN_ENDPOINT}`);
5348

5449
expect(response.status).toBe(200);
5550
const data = await response.json();
5651
expect(data).toHaveProperty("openResourceDiscoveryV1");
5752
});
5853

5954
it("should list all available documents in configuration", async () => {
60-
const response = await fetch(`${SERVER_URL}${WELL_KNOWN_ENDPOINT}`);
55+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.WELL_KNOWN_ENDPOINT}`);
6156
const data = (await response.json()) as ORDConfiguration;
6257

6358
expect(data.openResourceDiscoveryV1.documents).toBeDefined();
@@ -73,13 +68,13 @@ describe("Server Integration", () => {
7368

7469
describe("ORD Documents Endpoint", () => {
7570
it("should require authentication for accessing documents", async () => {
76-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1`);
71+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1`);
7772
expect(response.status).toBe(401);
7873
});
7974

8075
it("should return document with valid authentication", async () => {
8176
const credentials = Buffer.from("admin:secret").toString("base64");
82-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1`, {
77+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1`, {
8378
headers: {
8479
Authorization: `Basic ${credentials}`,
8580
},
@@ -95,7 +90,7 @@ describe("Server Integration", () => {
9590
const credentials = Buffer.from("admin:secret").toString("base64");
9691
const headers = { Authorization: `Basic ${credentials}` };
9792

98-
const configResponse = await fetch(`${SERVER_URL}${WELL_KNOWN_ENDPOINT}`);
93+
const configResponse = await fetch(`${SERVER_URL}${PATH_CONSTANTS.WELL_KNOWN_ENDPOINT}`);
9994
const config = (await configResponse.json()) as ORDConfiguration;
10095
const documents = config.openResourceDiscoveryV1.documents!;
10196

@@ -110,7 +105,7 @@ describe("Server Integration", () => {
110105

111106
it("should handle document names with special characters", async () => {
112107
const credentials = Buffer.from("admin:secret").toString("base64");
113-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1`, {
108+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1`, {
114109
headers: {
115110
Authorization: `Basic ${credentials}`,
116111
},
@@ -121,7 +116,7 @@ describe("Server Integration", () => {
121116

122117
it("should correctly extract document name when URL contains .json extension", async () => {
123118
const credentials = Buffer.from("admin:secret").toString("base64");
124-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1.json`, {
119+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1.json`, {
125120
headers: {
126121
Authorization: `Basic ${credentials}`,
127122
},
@@ -135,7 +130,7 @@ describe("Server Integration", () => {
135130

136131
it("should correctly extract document name when URL contains dots and a .json extension", async () => {
137132
const credentials = Buffer.from("admin:secret").toString("base64");
138-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/sap.ref-app-example.json`, {
133+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/sap.ref-app-example.json`, {
139134
headers: {
140135
Authorization: `Basic ${credentials}`,
141136
},
@@ -151,7 +146,7 @@ describe("Server Integration", () => {
151146
describe("Static Resources", () => {
152147
it("should serve static files with authentication", async () => {
153148
const credentials = Buffer.from("admin:secret").toString("base64");
154-
const response = await fetch(`${SERVER_URL}${ORD_SERVER_PREFIX_PATH}/astronomy/v1/openapi/oas3.json`, {
149+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.SERVER_PREFIX}/astronomy/v1/openapi/oas3.json`, {
155150
headers: {
156151
Authorization: `Basic ${credentials}`,
157152
},
@@ -171,7 +166,7 @@ describe("Server Integration", () => {
171166
describe("Error Handling", () => {
172167
it("should return 404 for non-existent documents", async () => {
173168
const credentials = Buffer.from("admin:secret").toString("base64");
174-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/non-existent-document`, {
169+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/non-existent-document`, {
175170
headers: {
176171
Authorization: `Basic ${credentials}`,
177172
},
@@ -184,7 +179,7 @@ describe("Server Integration", () => {
184179

185180
it("should return 401 with invalid credentials", async () => {
186181
const credentials = Buffer.from("admin:wrong").toString("base64");
187-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1`, {
182+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1`, {
188183
headers: {
189184
Authorization: `Basic ${credentials}`,
190185
},
@@ -195,7 +190,7 @@ describe("Server Integration", () => {
195190

196191
it("should handle malformed document names gracefully", async () => {
197192
const credentials = Buffer.from("admin:secret").toString("base64");
198-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/../../etc/passwd`, {
193+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/../../etc/passwd`, {
199194
headers: {
200195
Authorization: `Basic ${credentials}`,
201196
},
@@ -211,7 +206,7 @@ describe("Server Integration", () => {
211206
beforeAll(async () => {
212207
const options: ProviderServerOptions = {
213208
ordDirectory: LOCAL_DIRECTORY,
214-
ordDocumentsSubDirectory: ORD_DOCUMENTS_SUB_DIRECTORY,
209+
ordDocumentsSubDirectory: PATH_CONSTANTS.DOCUMENTS_SUBDIRECTORY,
215210
sourceType: OptSourceType.Local,
216211
host: TEST_HOST,
217212
port: MULTI_AUTH_PORT,
@@ -231,7 +226,7 @@ describe("Server Integration", () => {
231226

232227
it("should accept basic auth when multiple auth methods are configured", async () => {
233228
const credentials = Buffer.from("admin:secret").toString("base64");
234-
const response = await fetch(`${SERVER_URL_2}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1`, {
229+
const response = await fetch(`${SERVER_URL_2}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1`, {
235230
headers: {
236231
Authorization: `Basic ${credentials}`,
237232
},
@@ -244,7 +239,7 @@ describe("Server Integration", () => {
244239
describe("Server Configuration", () => {
245240
it("should set correct content type headers", async () => {
246241
const credentials = Buffer.from("admin:secret").toString("base64");
247-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1`, {
242+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1`, {
248243
headers: {
249244
Authorization: `Basic ${credentials}`,
250245
},
@@ -263,7 +258,7 @@ describe("Server Integration", () => {
263258

264259
const requests = Array(10)
265260
.fill(null)
266-
.map(() => fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1`, { headers }));
261+
.map(() => fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1`, { headers }));
267262

268263
const responses = await Promise.all(requests);
269264
responses.forEach((response) => {
@@ -273,7 +268,7 @@ describe("Server Integration", () => {
273268

274269
it("should return ETag headers for caching", async () => {
275270
const credentials = Buffer.from("admin:secret").toString("base64");
276-
const response = await fetch(`${SERVER_URL}${ORD_DOCUMENTS_URL_PATH}/ref-app-example-1`, {
271+
const response = await fetch(`${SERVER_URL}${PATH_CONSTANTS.DOCUMENTS_URL_PATH}/ref-app-example-1`, {
277272
headers: {
278273
Authorization: `Basic ${credentials}`,
279274
},

src/cli.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { log } from "src/util/logger.js";
99
import { validateAndParseOptions } from "src/util/optsValidation.js";
1010
import { ValidationError } from "./model/error/ValidationError.js";
1111
import { showCleanHelp } from "./util/cliHelp.js";
12+
import { PATH_CONSTANTS } from "./constant.js";
1213

1314
config();
1415

@@ -30,12 +31,12 @@ program
3031
.option(
3132
"-d, --directory <directory>",
3233
'Directory containing ORD documents. Required when source-type is "local".',
33-
process.env.ORD_DIRECTORY,
34+
process.env.ORD_DIRECTORY || PATH_CONSTANTS.GITHUB_DEFAULT_ROOT,
3435
)
3536
.option(
3637
"--documents-subdirectory <subdirectory>",
3738
"Subdirectory name for ORD documents within the main directory (default: 'documents')",
38-
process.env.ORD_DOCUMENTS_SUBDIRECTORY || "documents",
39+
PATH_CONSTANTS.DOCUMENTS_SUBDIRECTORY,
3940
)
4041
.option(
4142
"-a, --auth <authTypes>",

src/constant.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
export const ORD_URL_PATH = "ord";
2-
export const ORD_VERSION = "v1";
3-
export const WELL_KNOWN_ENDPOINT = `/.well-known/open-resource-discovery`;
4-
export const ORD_SERVER_PREFIX_PATH = `/${ORD_URL_PATH}/${ORD_VERSION}`;
5-
export const ORD_DOCUMENTS_URL_PATH = `${ORD_SERVER_PREFIX_PATH}/documents`;
6-
export const ORD_DOCUMENTS_SUB_DIRECTORY = process.env.ORD_DOCUMENTS_SUBDIRECTORY || "documents";
7-
export const ORD_DOCUMENTS_GITHUB_DIRECTORY = "data/documents";
8-
export const ORD_GITHUB_DEFAULT_ROOT_DIRECTORY = "data";
1+
// URL path constants
2+
export const PATH_CONSTANTS = {
3+
// Base paths
4+
ORD_URL_PATH: "ord",
5+
ORD_VERSION: "v1",
6+
WELL_KNOWN_ENDPOINT: "/.well-known/open-resource-discovery",
7+
8+
// Derived paths
9+
SERVER_PREFIX: `/ord/v1`,
10+
DOCUMENTS_URL_PATH: `/ord/v1/documents`,
11+
12+
// Directory constants
13+
DOCUMENTS_SUBDIRECTORY: process.env.ORD_DOCUMENTS_SUBDIRECTORY || "documents",
14+
GITHUB_DEFAULT_ROOT: "data",
15+
GITHUB_DOCUMENTS_PATH: "data/documents",
16+
};

src/factories/routerFactory.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { GithubOpts } from "../model/github.js";
2+
import { OptAuthMethod, OptSourceType } from "../model/cli.js";
3+
import { DocumentRepository } from "../repositories/interfaces/documentRepository.js";
4+
import { GithubDocumentRepository } from "../repositories/githubDocumentRepository.js";
5+
import { LocalDocumentRepository } from "../repositories/localDocumentRepository.js";
6+
import { CacheService } from "../services/cacheService.js";
7+
import { DocumentService } from "../services/documentService.js";
8+
import { DocumentRouter } from "../routes/documentRouter.js";
9+
import { FqnDocumentMap } from "../util/fqnHelpers.js";
10+
import { ProcessingContext } from "../services/interfaces/processingContext.js";
11+
import { PATH_CONSTANTS } from "../constant.js";
12+
13+
interface FactoryOptions {
14+
sourceType: OptSourceType;
15+
baseUrl: string;
16+
authMethods: OptAuthMethod[];
17+
fqnDocumentMap: FqnDocumentMap;
18+
documentsSubDirectory?: string;
19+
githubOpts?: GithubOpts;
20+
ordDirectory?: string;
21+
}
22+
23+
export class RouterFactory {
24+
public static async createRouter(options: FactoryOptions): Promise<DocumentRouter> {
25+
const cacheService = new CacheService();
26+
27+
let repository: DocumentRepository;
28+
let processingContext: ProcessingContext;
29+
const documentsSubDirectory = options.documentsSubDirectory || PATH_CONSTANTS.DOCUMENTS_SUBDIRECTORY;
30+
31+
if (options.sourceType === OptSourceType.Github && options.githubOpts) {
32+
repository = new GithubDocumentRepository(options.githubOpts);
33+
processingContext = {
34+
baseUrl: options.baseUrl,
35+
authMethods: options.authMethods,
36+
githubBranch: options.githubOpts.githubBranch,
37+
githubApiUrl: options.githubOpts.githubApiUrl,
38+
githubRepo: options.githubOpts.githubRepository,
39+
githubToken: options.githubOpts.githubToken,
40+
};
41+
} else if (options.sourceType === OptSourceType.Local && options.ordDirectory) {
42+
repository = new LocalDocumentRepository(options.ordDirectory);
43+
processingContext = {
44+
baseUrl: options.baseUrl,
45+
authMethods: options.authMethods,
46+
};
47+
} else {
48+
throw new Error("Invalid configuration: Missing required options for the specified source type.");
49+
}
50+
51+
const documentService = new DocumentService(repository, cacheService, processingContext, documentsSubDirectory);
52+
53+
// Ensure FQN map is generated and retrieve it
54+
// Calling getFqnMap also ensures ensureDataLoaded has run
55+
const fqnDocumentMap = await documentService.getFqnMap();
56+
57+
const routerOptions = {
58+
baseUrl: options.baseUrl,
59+
authMethods: options.authMethods,
60+
fqnDocumentMap: fqnDocumentMap,
61+
documentsSubDirectory: documentsSubDirectory,
62+
};
63+
64+
return new DocumentRouter(documentService, routerOptions);
65+
}
66+
}

0 commit comments

Comments
 (0)