Skip to content

Commit ba04cbe

Browse files
authored
Fix JSON Schema Endpoints (#7)
* fix absolute import paths * fix single schema path
1 parent 97a0025 commit ba04cbe

18 files changed

Lines changed: 283 additions & 237 deletions

File tree

.cursor/rules/jsdoc-comments.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description:
2+
description:
33
globs: *.ts,*.tsx,*js,*.mjs,*.jsx
44
alwaysApply: false
55
---

apps/docs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"dependencies": {
1616
"@astrojs/starlight": "^0.34.3",
1717
"@forge/schema": "workspace:*",
18+
"@forge/helpers": "workspace:*",
1819
"astro": "^5.6.1",
1920
"sharp": "^0.34.2",
2021
"zod": "^3.25.32"

apps/docs/src/components/version-list.astro

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
---
2-
import { getGroupedSchemaVersions } from "@forge/schema/versions/group-versions.ts";
2+
import {
3+
getGroupedSchemaVersions,
4+
listSchemaFiles,
5+
} from "@forge/helpers/files";
6+
7+
const monorepoRoot = new URL("../../../../", import.meta.url);
8+
const schemaFiles = listSchemaFiles(monorepoRoot);
39
---
410

511
<div>
612
{
7-
getGroupedSchemaVersions().map((major) => (
13+
getGroupedSchemaVersions(schemaFiles).map((major) => (
814
<section>
915
<h3>Version {major.version}</h3>
1016
<ul>

apps/docs/src/pages/schema/[...file].ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import {
2-
getFilename,
3-
getSchemaJson,
4-
} from "@forge/schema/versions/get-version.ts";
5-
import { listSchemaFiles } from "@forge/schema/versions/list-versions.ts";
1+
import { listSchemaFiles, useGetSchemaJson } from "@forge/helpers/files";
62
import type { APIRoute } from "astro";
3+
import path from "node:path";
4+
5+
const monorepoRoot = new URL("../../../../../", import.meta.url);
6+
const getSchemaJson = useGetSchemaJson(monorepoRoot);
77

88
export const GET: APIRoute = async ({ params, request }) => {
99
if (!params.file) {
1010
return new Response("No file provided", { status: 400 });
1111
}
1212

1313
const schema = getSchemaJson(params.file);
14-
const filename = getFilename(params.file);
14+
const filename = path.basename(params.file);
1515

1616
const isDev = import.meta.env.DEV;
1717
const serverHost = isDev ? "docs.localhost" : "dh-forge.com";
@@ -31,7 +31,7 @@ export const GET: APIRoute = async ({ params, request }) => {
3131
};
3232

3333
export function getStaticPaths() {
34-
const paths = listSchemaFiles();
34+
const paths = listSchemaFiles(monorepoRoot);
3535

3636
return paths.map((path) => ({
3737
params: { file: path },

packages/forge-helpers/package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,34 @@
22
"name": "@forge/helpers",
33
"description": "DHFS Helpers for resolving and formatting data",
44
"version": "0.0.0",
5+
"type": "module",
56
"main": "./src/index.ts",
67
"module": "./src/index.ts",
78
"types": "./src/index.ts",
89
"sideEffects": false,
910
"license": "MIT",
10-
"files": ["src/**"],
11+
"files": [
12+
"src/**"
13+
],
1114
"exports": {
1215
".": "./src/index.ts",
1316
"./version": "./src/version/index.ts",
14-
"./version/*": "./src/version/*"
17+
"./version/*": "./src/version/*",
18+
"./files": "./src/files/index.ts",
19+
"./files/*": "./src/files/*"
1520
},
1621
"scripts": {
1722
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
1823
"check": "tsc --noEmit"
1924
},
2025
"devDependencies": {
2126
"@forge/tsconfig": "workspace:*",
27+
"@types/node": "^20.17.52",
2228
"typescript": "5.8.3"
2329
},
2430
"dependencies": {
31+
"just-group-by": "^2.2.0",
32+
"just-sort-by": "^3.2.0",
2533
"zod": "^3.25.32"
2634
},
2735
"publishConfig": {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
5+
/**
6+
* Retrieves the JSON content of a schema file.
7+
* @param relativeFilePath - The relative path to the JSON schema file (e.g., "0.0.0/core.json").
8+
* @returns The parsed JSON content of the file.
9+
* @throws Will throw an error if the file cannot be read or parsed.
10+
*/
11+
export function useGetSchemaJson(monorepoRoot: URL) {
12+
// Determine the absolute path to the 'schema' directory, assuming this file is within it.
13+
const schemaDirectory = fileURLToPath(
14+
new URL("./packages/forge-schema/src/versions", monorepoRoot),
15+
);
16+
17+
return (relativeFilePath: string): string => {
18+
// Construct the absolute path to the target JSON file.
19+
const absoluteJsonPath = path.join(schemaDirectory, relativeFilePath);
20+
21+
// Read the file content.
22+
const fileContent = fs.readFileSync(absoluteJsonPath, "utf-8");
23+
24+
// Return the raw JSON content.
25+
return fileContent;
26+
};
27+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import groupBy from "just-group-by";
2+
3+
type SchemaData = {
4+
version: string;
5+
majorVersion: string;
6+
minorVersion: string;
7+
patchVersion: string;
8+
model: string;
9+
extension: string;
10+
};
11+
12+
/**
13+
* Parses a schema file path into its constituent parts: version (full, major, minor, patch), model, and extension.
14+
* Assumes the path is in the format "version/model.extension".
15+
* @param versionPath - The schema file path string (e.g., "1.2.3/MyModel.json").
16+
* @returns A {@link SchemaData} object containing the parsed components.
17+
*/
18+
function formatVersionPath(versionPath: string): SchemaData {
19+
const [version, filename] = versionPath.split("/");
20+
const [model, extension] = filename.split(".");
21+
22+
const [majorVersion, minorVersion, patchVersion] = version.split(".");
23+
24+
return {
25+
version,
26+
majorVersion,
27+
minorVersion,
28+
patchVersion,
29+
model,
30+
extension,
31+
};
32+
}
33+
34+
type SchemaGroup = {
35+
version: string;
36+
schemas: SchemaData[];
37+
};
38+
39+
/**
40+
* Groups an array of {@link SchemaData} objects by a specified key.
41+
*
42+
* @template K - The key of {@link SchemaData} to group by.
43+
* @param schemas - The array of {@link SchemaData} objects to group.
44+
* @param key - The key from {@link SchemaData} to use for grouping (e.g., "majorVersion", "model").
45+
* @returns An array of {@link SchemaGroup} objects. Each object contains a `version` property (the value of the group key)
46+
* and a `schemas` property (an array of {@link SchemaData} objects belonging to that group).
47+
*/
48+
function groupByKey<K extends keyof SchemaData>(
49+
schemas: SchemaData[],
50+
key: K,
51+
): SchemaGroup[] {
52+
const grouped = groupBy(schemas, (schema) => schema[key]);
53+
const groups = Object.entries<SchemaData[]>(grouped);
54+
55+
return groups.map(([version, groupedSchemas]) => ({
56+
version,
57+
schemas: groupedSchemas,
58+
}));
59+
}
60+
61+
/**
62+
* Comparator function to sort an array of {@link SchemaGroup} objects by their `version` property in descending order.
63+
* @param a - The first {@link SchemaGroup} object for comparison.
64+
* @param b - The second {@link SchemaGroup} object for comparison.
65+
* @returns A number indicating the sort order. Negative if `b.version` comes before `a.version`, positive if `a.version` comes before `b.version`, zero if equal.
66+
*/
67+
function sortGroup(a: SchemaGroup, b: SchemaGroup): number {
68+
const aVersion = a.version;
69+
const bVersion = b.version;
70+
return bVersion.localeCompare(aVersion);
71+
}
72+
73+
/**
74+
* Comparator function to sort an array of {@link SchemaData} objects by their `model` property in ascending alphabetical order.
75+
* @param a - The first {@link SchemaData} object for comparison.
76+
* @param b - The second {@link SchemaData} object for comparison.
77+
* @returns A number indicating the sort order. Negative if `a.model` comes before `b.model`, positive if `b.model` comes before `a.model`, zero if equal.
78+
*/
79+
function sortData(a: SchemaData, b: SchemaData): number {
80+
const aVersion = a.model;
81+
const bVersion = b.model;
82+
return aVersion.localeCompare(bVersion);
83+
}
84+
85+
/**
86+
* Takes a flat list of schema file paths and groups them hierarchically by major, minor, and patch versions.
87+
* Schemas within each patch version are sorted by model name.
88+
* Minor and patch version groups are sorted in descending order.
89+
* @param schemaFiles - An array of schema file path strings (e.g., ["1.0.0/User.json", "1.0.1/Order.json"]).
90+
* @returns An array of objects, where each object represents a major version group. These groups contain nested minor version groups,
91+
* which in turn contain nested patch version groups. Each patch version group lists its {@link SchemaData} objects.
92+
*/
93+
export function getGroupedSchemaVersions(schemaFiles: string[]) {
94+
const schemas = schemaFiles.map(formatVersionPath);
95+
96+
const majorVersions = groupByKey(schemas, "majorVersion").map((major) => {
97+
const majorVersion = major.version;
98+
const majorSchemas = groupByKey(major.schemas, "minorVersion").sort(
99+
sortGroup,
100+
);
101+
102+
return {
103+
version: majorVersion,
104+
schemas: majorSchemas.map((minor) => {
105+
const minorVersion = `${major.version}.${minor.version}.x`;
106+
const minorSchemas = groupByKey(minor.schemas, "patchVersion").sort(
107+
sortGroup,
108+
);
109+
110+
return {
111+
version: minorVersion,
112+
schemas: minorSchemas.map((patch) => {
113+
const patchVersion = `${major.version}.${minor.version}.${patch.version}`;
114+
115+
return {
116+
version: patchVersion,
117+
schemas: patch.schemas.sort(sortData),
118+
};
119+
}),
120+
};
121+
}),
122+
};
123+
});
124+
125+
return majorVersions;
126+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { useGetSchemaJson } from "./get-version.js";
2+
export { getGroupedSchemaVersions } from "./group-versions.js";
3+
export { listSchemaFiles } from "./list-versions.js";
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import { fileURLToPath } from "node:url";
4+
5+
/**
6+
* Retrieves all directory paths within a specified directory.
7+
* @param directoryPath - The absolute path of the directory to scan.
8+
* @returns An array of absolute string paths to directories.
9+
*/
10+
function getDirectoryPaths(directoryPath: string): string[] {
11+
return fs
12+
.readdirSync(directoryPath, { withFileTypes: true })
13+
.filter((dirent) => dirent.isDirectory())
14+
.map((dirent) => path.join(directoryPath, dirent.name));
15+
}
16+
17+
/**
18+
* Retrieves all file paths with a specific extension within a specified directory.
19+
* @param directoryPath - The absolute path of the directory to scan.
20+
* @param extension - The file extension to filter by (e.g., ".json").
21+
* @returns An array of absolute string paths to files.
22+
*/
23+
function getFilePaths(
24+
directoryPath: string,
25+
extension: `.${string}`,
26+
): string[] {
27+
return fs
28+
.readdirSync(directoryPath, { withFileTypes: true })
29+
.filter((dirent) => dirent.isFile() && dirent.name.endsWith(extension))
30+
.map((dirent) => path.join(directoryPath, dirent.name));
31+
}
32+
33+
/**
34+
* Converts an absolute file path to a path relative to a specified directory.
35+
* The returned relative path will not start with './' or '/'.
36+
* @param directoryPath - The absolute path of the directory to make the filePath relative to.
37+
* @param filePath - The absolute path of the file to convert.
38+
* @returns A string representing the relative file path.
39+
*/
40+
function getRelativeFilePath(directoryPath: string, filePath: string): string {
41+
const relativePath = path.relative(directoryPath, filePath);
42+
// Remove leading './' or '/'
43+
return relativePath.startsWith("./")
44+
? relativePath.slice(2)
45+
: relativePath.startsWith("/")
46+
? relativePath.slice(1)
47+
: relativePath;
48+
}
49+
50+
/**
51+
* Lists all JSON schema file paths within the 'packages/forge-schema/src/versions' directory
52+
* of the current monorepo, sorted in reverse alphabetical order.
53+
* It scans subdirectories of this specific path for JSON files.
54+
* @param monorepoRoot - A URL object representing the root directory of the current monorepo.
55+
* @returns An array of string file paths, relative to the 'packages/forge-schema/src/versions' directory, for all found JSON files.
56+
*/
57+
export function listSchemaFiles(monorepoRoot: URL): string[] {
58+
const versionsRoot = new URL(
59+
"./packages/forge-schema/src/versions",
60+
monorepoRoot,
61+
);
62+
const schemaDirectory = fileURLToPath(versionsRoot);
63+
64+
const directories = getDirectoryPaths(schemaDirectory);
65+
66+
const jsonFiles = directories
67+
.flatMap((directory) => getFilePaths(directory, ".json"))
68+
.map((filePath) => getRelativeFilePath(schemaDirectory, filePath));
69+
70+
return jsonFiles.sort((a, b) => b.localeCompare(a));
71+
}

packages/forge-helpers/src/version/create.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { VersionRecord, VersionString } from "./validate";
1+
import type { VersionRecord, VersionString } from "./validate.js";
22

33
/**
44
* Converts a VersionRecord to a VersionString

0 commit comments

Comments
 (0)