Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion packages/patchlogr-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"publishConfig": {
"access": "public"
},
"type": "module",
"main": "./dist/index.cjs",
Comment thread
toothlessdev marked this conversation as resolved.
Comment thread
toothlessdev marked this conversation as resolved.
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
Expand All @@ -31,6 +30,7 @@
"@patchlogr/types": "workspace:^"
},
"devDependencies": {
"@types/node": "^25.0.9",
"esbuild": "^0.27.2",
"openapi-types": "^12.1.3",
"typescript": "^5.9.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { CanonicalSpec } from "@patchlogr/types";
import { describe, expect, test } from "vitest";
import { partitionByMethod } from "../partitionByMethod";

describe("partitionByMethod", () => {
test("should group by first tag", () => {
Comment thread
toothlessdev marked this conversation as resolved.
Outdated
Comment thread
toothlessdev marked this conversation as resolved.
Outdated
const spec: CanonicalSpec = {
operations: {
"GET /user": {
key: "GET /user",
doc: { tags: ["user"] },
method: "GET",
path: "/user",
request: { params: [] },
responses: {},
},
"GET /user/{userId}": {
key: "GET /user/{userId}",
doc: { tags: ["user"] },
method: "GET",
path: "/user/{userId}",
request: { params: [] },
responses: {},
},
},
};

const partitions = partitionByMethod(spec).partitions;
Comment thread
toothlessdev marked this conversation as resolved.
expect(partitions).toHaveLength(1);
expect(partitions.get("GET")).toHaveLength(2);
expect(partitions.get("GET")?.[0]?.operationKey).toBe("GET /user");
expect(partitions.get("GET")?.[1]?.operationKey).toBe(
"GET /user/{userId}",
);
});
Comment thread
toothlessdev marked this conversation as resolved.
Outdated

test("should group by multiple tags", () => {
Comment thread
toothlessdev marked this conversation as resolved.
Outdated
Comment thread
toothlessdev marked this conversation as resolved.
Outdated
const spec: CanonicalSpec = {
operations: {
"GET /user": {
key: "GET /user",
doc: { tags: ["user"] },
method: "GET",
path: "/user",
request: { params: [] },
responses: {},
},
"POST /auth/login": {
key: "POST /auth/login",
doc: { tags: ["auth"] },
method: "POST",
path: "/auth/login",
request: { params: [] },
responses: {},
},
},
};

const partitions = partitionByMethod(spec).partitions;

expect(partitions).toHaveLength(2);
expect(partitions.get("GET")).toHaveLength(1);
expect(partitions.get("POST")).toHaveLength(1);
expect(partitions.get("GET")?.[0]?.operationKey).toBe("GET /user");
expect(partitions.get("POST")?.[0]?.operationKey).toBe(
"POST /auth/login",
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { CanonicalSpec } from "@patchlogr/types";
import { describe, expect, test } from "vitest";
import { partitionByTag } from "../partitionByTag";

describe("partitionByTag", () => {
test("should group by first tag", () => {
const spec: CanonicalSpec = {
operations: {
"GET /user": {
key: "GET /user",
doc: { tags: ["user"] },
method: "GET",
path: "/user",
request: { params: [] },
responses: {},
},
"GET /user/{userId}": {
key: "GET /user/{userId}",
doc: { tags: ["user"] },
method: "GET",
path: "/user/{userId}",
request: { params: [] },
responses: {},
},
},
};

const partitions = partitionByTag(spec).partitions;
Comment thread
toothlessdev marked this conversation as resolved.
expect(partitions).toHaveLength(1);
expect(partitions.get("user")).toHaveLength(2);
expect(partitions.get("user")?.[0]?.operationKey).toBe("GET /user");
expect(partitions.get("user")?.[1]?.operationKey).toBe(
"GET /user/{userId}",
);
});
Comment thread
toothlessdev marked this conversation as resolved.

test("should group by multiple tags", () => {
const spec: CanonicalSpec = {
operations: {
"GET /user": {
key: "GET /user",
doc: { tags: ["user"] },
method: "GET",
path: "/user",
request: { params: [] },
responses: {},
},
"POST /auth/login": {
key: "POST /auth/login",
doc: { tags: ["auth"] },
method: "POST",
path: "/auth/login",
request: { params: [] },
responses: {},
},
},
};

const partitions = partitionByTag(spec).partitions;

expect(partitions).toHaveLength(2);
expect(partitions.get("user")).toHaveLength(1);
expect(partitions.get("auth")).toHaveLength(1);
expect(partitions.get("user")?.[0]?.operationKey).toBe("GET /user");
expect(partitions.get("auth")?.[0]?.operationKey).toBe(
"POST /auth/login",
);
});
});
Comment thread
toothlessdev marked this conversation as resolved.
14 changes: 14 additions & 0 deletions packages/patchlogr-core/src/partition/partition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type PartitionManifest = {
key: string;
hash: string;
};
Comment thread
toothlessdev marked this conversation as resolved.

export type Partition = {
hash: string;
operationKey: string;
};

export type PartitionedSpec = {
metadata: Record<string, unknown>;
partitions: Map<string, Partition[]>;
};
Comment thread
toothlessdev marked this conversation as resolved.
26 changes: 26 additions & 0 deletions packages/patchlogr-core/src/partition/partitionByMethod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { CanonicalSpec, HTTPMethod } from "@patchlogr/types";
import type { Partition, PartitionedSpec } from "./partition";

import { createSHA256Hash } from "../utils/createHash";
import { stableStringify } from "../utils/stableStringify";

Comment thread
toothlessdev marked this conversation as resolved.
export function partitionByMethod(spec: CanonicalSpec): PartitionedSpec {
const partitions = new Map<HTTPMethod, Partition[]>();

Object.entries(spec.operations).forEach(([key, operation]) => {
const hash = createSHA256Hash(stableStringify(operation));

if (!partitions.has(operation.method))
partitions.set(operation.method, [{ hash, operationKey: key }]);
else
partitions.get(operation.method)?.push({ hash, operationKey: key });
Comment thread
toothlessdev marked this conversation as resolved.
});

return {
metadata: {
...spec.info,
...spec.security,
},
partitions,
};
}
28 changes: 28 additions & 0 deletions packages/patchlogr-core/src/partition/partitionByTag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { CanonicalSpec } from "@patchlogr/types";
import type { Partition, PartitionedSpec } from "./partition";

import { createSHA256Hash } from "../utils/createHash";
import { stableStringify } from "../utils/stableStringify";

Comment thread
toothlessdev marked this conversation as resolved.
const DEFAULT_TAG = "__DEFAULT__";

Comment thread
toothlessdev marked this conversation as resolved.
export function partitionByTag(spec: CanonicalSpec): PartitionedSpec {
const partitions = new Map<string, Partition[]>();

Object.entries(spec.operations).forEach(([key, operation]) => {
const tag = operation.doc?.tags?.[0] || DEFAULT_TAG;
Comment thread
toothlessdev marked this conversation as resolved.
const hash = createSHA256Hash(stableStringify(operation));

Comment thread
toothlessdev marked this conversation as resolved.
if (!partitions.has(tag))
partitions.set(tag, [{ hash, operationKey: key }]);
else partitions.get(tag)?.push({ hash, operationKey: key });
});

return {
metadata: {
...spec.info,
...spec.security,
},
partitions,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, test, expect } from "vitest";
import { stableStringify } from "../stableStringify";

describe("stableStringify", () => {
test("should stringify json", () => {
expect(stableStringify({ a: 1, b: 2, c: 3 })).toBe(
'{"a":1,"b":2,"c":3}',
);
});

test("should stringify json in a stable order", () => {
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 2, a: 1 };

expect(stableStringify(obj1)).toBe(stableStringify(obj2));
});
});
Comment thread
toothlessdev marked this conversation as resolved.
5 changes: 5 additions & 0 deletions packages/patchlogr-core/src/utils/createHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import crypto from "crypto";

Comment thread
toothlessdev marked this conversation as resolved.
export function createSHA256Hash(data: string) {
return crypto.createHash("sha256").update(data).digest("hex");
}
Comment thread
toothlessdev marked this conversation as resolved.
3 changes: 3 additions & 0 deletions packages/patchlogr-core/src/utils/stableStringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function stableStringify(obj: Record<string, unknown>) {
return JSON.stringify(obj, Object.keys(obj).sort());
Comment thread
toothlessdev marked this conversation as resolved.
Outdated
Comment thread
toothlessdev marked this conversation as resolved.
Outdated
}
Comment thread
toothlessdev marked this conversation as resolved.
Outdated
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ __metadata:
"@apidevtools/swagger-parser": "npm:^12.1.0"
"@patchlogr/oas": "workspace:^"
"@patchlogr/types": "workspace:^"
"@types/node": "npm:^25.0.9"
esbuild: "npm:^0.27.2"
openapi-types: "npm:^12.1.3"
typescript: "npm:^5.9.3"
Expand Down
Loading