feat(services): implement projects domain service
This commit is contained in:
1 parent
c50ab8989c
commit
6e0d3faa61
2 files changed
+163
No files matched your search
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Projects Domain Service.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { OpenProjectClient } from "../client/api-client.ts";
|
||||||
|
import {
|
||||||
|
normalizeProject,
|
||||||
|
normalizeProjectSummary,
|
||||||
|
unpackCollection,
|
||||||
|
} from "../client/hal-parser.ts";
|
||||||
|
import type {
|
||||||
|
FilterElement,
|
||||||
|
HalCollection,
|
||||||
|
HalResource,
|
||||||
|
PaginatedResult,
|
||||||
|
ProjectDetail,
|
||||||
|
ProjectSummary,
|
||||||
|
} from "../client/types.ts";
|
||||||
|
import { resolveClient } from "./helper.ts";
|
||||||
|
|
||||||
|
export interface ListProjectsParams {
|
||||||
|
pageSize?: number;
|
||||||
|
offset?: number;
|
||||||
|
sortBy?: string;
|
||||||
|
filters?: FilterElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists projects accessible to the authenticated user.
|
||||||
|
*/
|
||||||
|
export async function listProjects(
|
||||||
|
params?: ListProjectsParams,
|
||||||
|
client?: OpenProjectClient
|
||||||
|
): Promise<PaginatedResult<ProjectSummary>> {
|
||||||
|
const opClient = resolveClient(client);
|
||||||
|
const query: Record<string, string | number | boolean | undefined> = {};
|
||||||
|
|
||||||
|
if (params?.pageSize !== undefined) query.pageSize = params.pageSize;
|
||||||
|
if (params?.offset !== undefined) query.offset = params.offset;
|
||||||
|
if (params?.sortBy !== undefined) query.sortBy = params.sortBy;
|
||||||
|
if (params?.filters && params.filters.length > 0) {
|
||||||
|
query.filters = JSON.stringify(params.filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await opClient.get<HalCollection<HalResource>>("projects", query);
|
||||||
|
return unpackCollection(response, normalizeProjectSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single project by its numeric ID or string identifier.
|
||||||
|
*/
|
||||||
|
export async function getProject(
|
||||||
|
idOrIdentifier: number | string,
|
||||||
|
client?: OpenProjectClient
|
||||||
|
): Promise<ProjectDetail> {
|
||||||
|
const opClient = resolveClient(client);
|
||||||
|
const response = await opClient.get<HalResource>(`projects/${idOrIdentifier}`);
|
||||||
|
return normalizeProject(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the schema definition for projects.
|
||||||
|
*/
|
||||||
|
export async function getProjectSchema(
|
||||||
|
client?: OpenProjectClient
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const opClient = resolveClient(client);
|
||||||
|
return opClient.get<Record<string, unknown>>("projects/schema");
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { resolveClient } from "../src/services/helper.ts";
|
import { resolveClient } from "../src/services/helper.ts";
|
||||||
|
import { listProjects, getProject, getProjectSchema } from "../src/services/projects.ts";
|
||||||
import { OpenProjectClient } from "../src/client/api-client.ts";
|
import { OpenProjectClient } from "../src/client/api-client.ts";
|
||||||
import { runWithContext, type RequestContext } from "../src/context.ts";
|
import { runWithContext, type RequestContext } from "../src/context.ts";
|
||||||
|
|
||||||
@@ -33,3 +34,96 @@ describe("Domain Services Helper", () => {
|
|||||||
expect(() => resolveClient()).toThrow("No active RequestContext found");
|
expect(() => resolveClient()).toThrow("No active RequestContext found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Projects Service", () => {
|
||||||
|
const sampleProjectHal = {
|
||||||
|
_type: "Project",
|
||||||
|
id: 4,
|
||||||
|
identifier: "mcp-test-project",
|
||||||
|
name: "MCP Test Project",
|
||||||
|
active: true,
|
||||||
|
public: true,
|
||||||
|
description: { format: "markdown", raw: "Dedicated test project" },
|
||||||
|
_links: {
|
||||||
|
self: { href: "/api/v3/projects/4", title: "MCP Test Project" },
|
||||||
|
parent: { href: "/api/v3/projects/1", title: "Parent Project" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test("listProjects sends correct parameters and returns paginated ProjectSummary items", async () => {
|
||||||
|
let requestedUrl = "";
|
||||||
|
const mockFetch = async (input: string | URL | Request) => {
|
||||||
|
requestedUrl = String(input);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
_type: "Collection",
|
||||||
|
total: 1,
|
||||||
|
count: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
offset: 1,
|
||||||
|
_embedded: {
|
||||||
|
elements: [sampleProjectHal],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ headers: { "Content-Type": "application/hal+json" } }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = new OpenProjectClient({
|
||||||
|
baseUrl: "http://example.com",
|
||||||
|
apiKey: "test-key",
|
||||||
|
fetchFn: mockFetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await listProjects({ pageSize: 10, offset: 1, sortBy: '[["name","asc"]]' }, client);
|
||||||
|
|
||||||
|
expect(requestedUrl).toContain("/api/v3/projects");
|
||||||
|
expect(requestedUrl).toContain("pageSize=10");
|
||||||
|
expect(requestedUrl).toContain("offset=1");
|
||||||
|
expect(requestedUrl).toContain("sortBy=");
|
||||||
|
expect(result.total).toBe(1);
|
||||||
|
expect(result.items[0].id).toBe(4);
|
||||||
|
expect(result.items[0].identifier).toBe("mcp-test-project");
|
||||||
|
expect(result.items[0].name).toBe("MCP Test Project");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getProject retrieves project detail by id or identifier", async () => {
|
||||||
|
let requestedUrl = "";
|
||||||
|
const mockFetch = async (input: string | URL | Request) => {
|
||||||
|
requestedUrl = String(input);
|
||||||
|
return new Response(JSON.stringify(sampleProjectHal), {
|
||||||
|
headers: { "Content-Type": "application/hal+json" },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = new OpenProjectClient({
|
||||||
|
baseUrl: "http://example.com",
|
||||||
|
apiKey: "test-key",
|
||||||
|
fetchFn: mockFetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getProject("mcp-test-project", client);
|
||||||
|
expect(requestedUrl).toContain("/api/v3/projects/mcp-test-project");
|
||||||
|
expect(result.id).toBe(4);
|
||||||
|
expect(result.parentId).toBe(1);
|
||||||
|
expect(result.parentName).toBe("Parent Project");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getProjectSchema retrieves project schema definition", async () => {
|
||||||
|
const mockFetch = async () => {
|
||||||
|
return new Response(JSON.stringify({ _type: "Schema", name: { type: "String" } }), {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = new OpenProjectClient({
|
||||||
|
baseUrl: "http://example.com",
|
||||||
|
apiKey: "test-key",
|
||||||
|
fetchFn: mockFetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getProjectSchema(client);
|
||||||
|
expect(result._type).toBe("Schema");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in new issue
Block a user