fix(services): resolve project identifier in queries/work packages and use results link

This commit is contained in:
Lucas Winther
2026-09-05 23:12:46 +02:00
parent a8e4ce9bc4
commit ed38e5b537
5 changed files with 54 additions and 15 deletions
+1
View File
@@ -32,6 +32,7 @@ test_project.description = 'Dedicated test project for openproject-mcp companion
test_project.public = true test_project.public = true
test_project.workspace_type = 'project' if test_project.respond_to?(:workspace_type=) test_project.workspace_type = 'project' if test_project.respond_to?(:workspace_type=)
test_project.types = Type.all if Type.any? test_project.types = Type.all if Type.any?
test_project.enabled_module_names = OpenProject::AccessControl.available_project_modules if defined?(OpenProject::AccessControl)
test_project.save!(validate: false) test_project.save!(validate: false)
puts "[Seeder] Ensured project 'MCP Test Project' (ID: #{test_project.id}, identifier: #{test_project.identifier})." puts "[Seeder] Ensured project 'MCP Test Project' (ID: #{test_project.id}, identifier: #{test_project.identifier})."
+2 -4
View File
@@ -52,10 +52,8 @@ OPENPROJECT_API_KEY=${API_KEY}
OPENPROJECT_TEST_PROJECT=${PROJECT_IDENTIFIER} OPENPROJECT_TEST_PROJECT=${PROJECT_IDENTIFIER}
EOF EOF
# Also update .env.local if not present or populate key # Also update .env.local
if [ ! -f "${ROOT_DIR}/.env.local" ]; then cp "${ROOT_DIR}/.env.test" "${ROOT_DIR}/.env.local"
cp "${ROOT_DIR}/.env.test" "${ROOT_DIR}/.env.local"
fi
echo "" echo ""
echo "[seed.sh] Success! Test environment configured." echo "[seed.sh] Success! Test environment configured."
+21
View File
@@ -12,3 +12,24 @@ import type { OpenProjectClient } from "../client/api-client.ts";
export function resolveClient(client?: OpenProjectClient): OpenProjectClient { export function resolveClient(client?: OpenProjectClient): OpenProjectClient {
return client ?? getRequestContext().client; return client ?? getRequestContext().client;
} }
/**
* Resolves a numeric project ID from either a number or string identifier.
* If the value is a number or integer string, returns the parsed integer.
* Otherwise, queries the OpenProject API for the project by identifier to get its numeric ID.
*/
export async function resolveProjectId(
projectId: number | string,
client?: OpenProjectClient
): Promise<number> {
if (typeof projectId === "number") {
return projectId;
}
const trimmed = projectId.trim();
if (/^\d+$/.test(trimmed)) {
return parseInt(trimmed, 10);
}
const opClient = resolveClient(client);
const response = await opClient.get<{ id: number }>(`projects/${encodeURIComponent(trimmed)}`);
return response.id;
}
+18 -4
View File
@@ -2,7 +2,7 @@
* Queries Domain Service. * Queries Domain Service.
*/ */
import type { OpenProjectClient } from "../client/api-client.ts"; import { OpenProjectNotFoundError, type OpenProjectClient } from "../client/api-client.ts";
import { import {
normalizeQuery, normalizeQuery,
normalizeQuerySummary, normalizeQuerySummary,
@@ -17,7 +17,7 @@ import type {
QuerySummary, QuerySummary,
WorkPackageSummary, WorkPackageSummary,
} from "../client/types.ts"; } from "../client/types.ts";
import { resolveClient } from "./helper.ts"; import { resolveClient, resolveProjectId } from "./helper.ts";
export interface ListQueriesParams { export interface ListQueriesParams {
projectId?: number | string; projectId?: number | string;
@@ -43,9 +43,10 @@ export async function listQueries(
if (params?.pageSize !== undefined) query.pageSize = params.pageSize; if (params?.pageSize !== undefined) query.pageSize = params.pageSize;
if (params?.offset !== undefined) query.offset = params.offset; if (params?.offset !== undefined) query.offset = params.offset;
if (params?.projectId !== undefined) { if (params?.projectId !== undefined && params.projectId !== "") {
const numericProjectId = await resolveProjectId(params.projectId, opClient);
query.filters = JSON.stringify([ query.filters = JSON.stringify([
{ project: { operator: "=", values: [String(params.projectId)] } }, { project: { operator: "=", values: [String(numericProjectId)] } },
]); ]);
} }
@@ -79,9 +80,22 @@ export async function getQueryResults(
if (params?.pageSize !== undefined) query.pageSize = params.pageSize; if (params?.pageSize !== undefined) query.pageSize = params.pageSize;
if (params?.offset !== undefined) query.offset = params.offset; if (params?.offset !== undefined) query.offset = params.offset;
try {
const response = await opClient.get<HalCollection<HalResource>>( const response = await opClient.get<HalCollection<HalResource>>(
`queries/${id}/results`, `queries/${id}/results`,
query query
); );
return unpackCollection(response, normalizeWorkPackageSummary); return unpackCollection(response, normalizeWorkPackageSummary);
} catch (err) {
if (err instanceof OpenProjectNotFoundError) {
// In OpenProject API v3, query results endpoint is dynamically linked in query._links.results.href
const queryDetail = await opClient.get<HalResource>(`queries/${id}`);
const resultsHref = queryDetail._links?.results?.href;
if (typeof resultsHref === "string" && resultsHref.length > 0) {
const response = await opClient.get<HalCollection<HalResource>>(resultsHref, query);
return unpackCollection(response, normalizeWorkPackageSummary);
}
}
throw err;
}
} }
+7 -2
View File
@@ -17,7 +17,7 @@ import type {
WorkPackageFilterParams, WorkPackageFilterParams,
WorkPackageSummary, WorkPackageSummary,
} from "../client/types.ts"; } from "../client/types.ts";
import { resolveClient } from "./helper.ts"; import { resolveClient, resolveProjectId } from "./helper.ts";
export interface ListWorkPackagesParams extends WorkPackageFilterParams { export interface ListWorkPackagesParams extends WorkPackageFilterParams {
pageSize?: number; pageSize?: number;
@@ -46,7 +46,12 @@ export async function listWorkPackages(
if (params?.sortBy !== undefined) query.sortBy = params.sortBy; if (params?.sortBy !== undefined) query.sortBy = params.sortBy;
if (params) { if (params) {
const filters = buildWorkPackageFilters(params); let filterParams = params;
if (params.projectId !== undefined && params.projectId !== "") {
const numericProjectId = await resolveProjectId(params.projectId, opClient);
filterParams = { ...params, projectId: numericProjectId };
}
const filters = buildWorkPackageFilters(filterParams);
if (filters.length > 0) { if (filters.length > 0) {
query.filters = JSON.stringify(filters); query.filters = JSON.stringify(filters);
} }