diff --git a/scripts/seed-test-data.rb b/scripts/seed-test-data.rb index 353a0e4..bada6b9 100644 --- a/scripts/seed-test-data.rb +++ b/scripts/seed-test-data.rb @@ -32,6 +32,7 @@ test_project.description = 'Dedicated test project for openproject-mcp companion test_project.public = true test_project.workspace_type = 'project' if test_project.respond_to?(:workspace_type=) 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) puts "[Seeder] Ensured project 'MCP Test Project' (ID: #{test_project.id}, identifier: #{test_project.identifier})." diff --git a/scripts/seed.sh b/scripts/seed.sh index acc1d4b..11eea27 100755 --- a/scripts/seed.sh +++ b/scripts/seed.sh @@ -52,10 +52,8 @@ OPENPROJECT_API_KEY=${API_KEY} OPENPROJECT_TEST_PROJECT=${PROJECT_IDENTIFIER} EOF -# Also update .env.local if not present or populate key -if [ ! -f "${ROOT_DIR}/.env.local" ]; then - cp "${ROOT_DIR}/.env.test" "${ROOT_DIR}/.env.local" -fi +# Also update .env.local +cp "${ROOT_DIR}/.env.test" "${ROOT_DIR}/.env.local" echo "" echo "[seed.sh] Success! Test environment configured." diff --git a/src/services/helper.ts b/src/services/helper.ts index 8963b87..cf23b3a 100644 --- a/src/services/helper.ts +++ b/src/services/helper.ts @@ -12,3 +12,24 @@ import type { OpenProjectClient } from "../client/api-client.ts"; export function resolveClient(client?: OpenProjectClient): OpenProjectClient { 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 { + 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; +} diff --git a/src/services/queries.ts b/src/services/queries.ts index 1885530..fa2a7f3 100644 --- a/src/services/queries.ts +++ b/src/services/queries.ts @@ -2,7 +2,7 @@ * Queries Domain Service. */ -import type { OpenProjectClient } from "../client/api-client.ts"; +import { OpenProjectNotFoundError, type OpenProjectClient } from "../client/api-client.ts"; import { normalizeQuery, normalizeQuerySummary, @@ -17,7 +17,7 @@ import type { QuerySummary, WorkPackageSummary, } from "../client/types.ts"; -import { resolveClient } from "./helper.ts"; +import { resolveClient, resolveProjectId } from "./helper.ts"; export interface ListQueriesParams { projectId?: number | string; @@ -43,9 +43,10 @@ export async function listQueries( if (params?.pageSize !== undefined) query.pageSize = params.pageSize; 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([ - { 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?.offset !== undefined) query.offset = params.offset; - const response = await opClient.get>( - `queries/${id}/results`, - query - ); - return unpackCollection(response, normalizeWorkPackageSummary); + try { + const response = await opClient.get>( + `queries/${id}/results`, + query + ); + 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(`queries/${id}`); + const resultsHref = queryDetail._links?.results?.href; + if (typeof resultsHref === "string" && resultsHref.length > 0) { + const response = await opClient.get>(resultsHref, query); + return unpackCollection(response, normalizeWorkPackageSummary); + } + } + throw err; + } } diff --git a/src/services/work-packages.ts b/src/services/work-packages.ts index 67f6203..6885b06 100644 --- a/src/services/work-packages.ts +++ b/src/services/work-packages.ts @@ -17,7 +17,7 @@ import type { WorkPackageFilterParams, WorkPackageSummary, } from "../client/types.ts"; -import { resolveClient } from "./helper.ts"; +import { resolveClient, resolveProjectId } from "./helper.ts"; export interface ListWorkPackagesParams extends WorkPackageFilterParams { pageSize?: number; @@ -46,7 +46,12 @@ export async function listWorkPackages( if (params?.sortBy !== undefined) query.sortBy = params.sortBy; 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) { query.filters = JSON.stringify(filters); }