Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

172 lines
6.1 KiB
JavaScript

const { randomUUID } = require('node:crypto');
function getApiKey() {
return process.env.WORKFLOW_HARNESS_API_KEY || process.env.ALGA_API_KEY || '';
}
async function pickOne(ctx, { label, sql, params }) {
const rows = await ctx.db.query(sql, params);
if (!rows.length) throw new Error(`Fixture requires ${label} in DB (tenant=${ctx.config.tenantId}).`);
return rows[0];
}
async function deleteTicketWithDbFallback(ctx, { tenantId, ticketId, apiKey }) {
try {
await ctx.http.request(`/api/v1/tickets/${ticketId}`, {
method: 'DELETE',
headers: { 'x-api-key': apiKey }
});
return;
} catch {
// Fall back to DB cleanup for common FK constraints (e.g. project_ticket_links, comments).
}
await ctx.dbWrite.query(`delete from project_ticket_links where tenant = $1 and ticket_id = $2`, [tenantId, ticketId]);
await ctx.dbWrite.query(`delete from comments where tenant = $1 and ticket_id = $2`, [tenantId, ticketId]);
await ctx.dbWrite.query(`delete from tickets where tenant = $1 and ticket_id = $2`, [tenantId, ticketId]);
}
async function deleteProjectWithDbFallback(ctx, { tenantId, projectId, apiKey }) {
try {
await ctx.http.request(`/api/v1/projects/${projectId}`, {
method: 'DELETE',
headers: { 'x-api-key': apiKey }
});
return;
} catch {
// Fall back to DB cleanup for common FK constraints (phases/tasks/links).
}
// Remove any ticket links first (FKs to tickets/projects/tasks/phases).
await ctx.dbWrite.query(`delete from project_ticket_links where tenant = $1 and project_id = $2`, [tenantId, projectId]);
// Remove task dependencies for tasks under this project.
await ctx.dbWrite.query(
`
with project_tasks_in_project as (
select t.task_id
from project_tasks t
join project_phases p on p.tenant = t.tenant and p.phase_id = t.phase_id
where p.tenant = $1 and p.project_id = $2
)
delete from project_task_dependencies d
using project_tasks_in_project pt
where d.tenant = $1
and (d.predecessor_task_id = pt.task_id or d.successor_task_id = pt.task_id)
`,
[tenantId, projectId]
);
// Remove project tasks (cascades task_checklist_items/task_resources/task_comments).
await ctx.dbWrite.query(
`
delete from project_tasks t
using project_phases p
where t.tenant = $1
and p.tenant = t.tenant
and p.phase_id = t.phase_id
and p.project_id = $2
`,
[tenantId, projectId]
);
await ctx.dbWrite.query(`delete from project_materials where tenant = $1 and project_id = $2`, [tenantId, projectId]);
await ctx.dbWrite.query(`delete from project_phases where tenant = $1 and project_id = $2`, [tenantId, projectId]);
await ctx.dbWrite.query(`delete from projects where tenant = $1 and project_id = $2`, [tenantId, projectId]);
}
module.exports = async function run(ctx) {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error('Missing WORKFLOW_HARNESS_API_KEY (or ALGA_API_KEY) for /api/v1 calls.');
}
const tenantId = ctx.config.tenantId;
const marker = '[fixture ticket-created-create-project-task]';
const client = await pickOne(ctx, {
label: 'a client',
sql: `select client_id from clients where tenant = $1 order by created_at asc limit 1`,
params: [tenantId]
});
const board = await pickOne(ctx, {
label: 'a ticket board',
sql: `select board_id from boards where tenant = $1 order by is_default desc, display_order asc limit 1`,
params: [tenantId]
});
const status = await pickOne(ctx, {
label: 'a ticket status',
sql: `select status_id from statuses where tenant = $1 and board_id = $2 and status_type = 'ticket' order by is_default desc, order_number asc limit 1`,
params: [tenantId, board.board_id]
});
const priority = await pickOne(ctx, {
label: 'a ticket priority',
sql: `select priority_id from priorities where tenant = $1 order by order_number asc limit 1`,
params: [tenantId]
});
const projectName = `Fixture onboarding project ${randomUUID()}`;
const createProjectRes = await ctx.http.request('/api/v1/projects', {
method: 'POST',
headers: { 'x-api-key': apiKey },
json: {
client_id: client.client_id,
project_name: projectName,
create_default_phase: true
}
});
const projectId = createProjectRes.json?.data?.project_id;
if (!projectId) throw new Error('Project create response missing data.project_id');
ctx.onCleanup(async () => {
await deleteProjectWithDbFallback(ctx, { tenantId, projectId, apiKey });
});
const title = `Fixture onboarding ticket ${randomUUID()}`;
const createTicketRes = await ctx.http.request('/api/v1/tickets', {
method: 'POST',
headers: { 'x-api-key': apiKey },
json: {
title,
client_id: client.client_id,
board_id: board.board_id,
status_id: status.status_id,
priority_id: priority.priority_id,
attributes: {
fixture_project_id: projectId
}
}
});
const ticketId = createTicketRes.json?.data?.ticket_id;
if (!ticketId) throw new Error('Ticket create response missing data.ticket_id');
ctx.onCleanup(async () => {
await deleteTicketWithDbFallback(ctx, { tenantId, ticketId, apiKey });
});
const runRow = await ctx.waitForRun({ startedAfter: ctx.triggerStartedAt });
if (runRow.status !== 'SUCCEEDED') {
const steps = await ctx.getRunSteps(runRow.run_id);
throw new Error(`Expected run SUCCEEDED, got ${runRow.status}. Steps: ${JSON.stringify(ctx.summarizeSteps(steps))}`);
}
const tasks = await ctx.db.query(
`
select t.task_id, t.task_name
from project_tasks t
join project_phases p on p.phase_id = t.phase_id and p.tenant = t.tenant
where p.tenant = $1 and p.project_id = $2
order by t.created_at desc
limit 25
`,
[tenantId, projectId]
);
const found = tasks.find((t) => typeof t.task_name === 'string' && t.task_name.includes(marker) && t.task_name.includes(ticketId));
if (!found) {
throw new Error(`Expected a project task containing "${marker}" and ticketId on project ${projectId}. Found ${tasks.length} task(s).`);
}
};