Skip to content

Commit 1d9ab59

Browse files
authored
Merge pull request #187 from objectstack-ai/copilot/update-ci-pipeline-configuration
2 parents a01f3d5 + 0a11fb6 commit 1d9ab59

4 files changed

Lines changed: 516 additions & 5 deletions

File tree

examples/showcase/project-tracker/__tests__/projects-hooks-actions.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ describe('Project Hooks - Comprehensive Examples', () => {
2929
default: {
3030
find: jest.fn().mockResolvedValue([]),
3131
findOne: jest.fn().mockResolvedValue(null),
32-
create: jest.fn((obj, data) => ({ ...data, _id: 'test-id' })),
33-
update: jest.fn((obj, id, data) => data),
32+
create: jest.fn((obj, data, ctx) => ({ ...data, _id: 'test-id' })),
33+
update: jest.fn((obj, id, data, ctx) => data),
3434
delete: jest.fn().mockResolvedValue(true),
3535
count: jest.fn().mockResolvedValue(0)
3636
} as any
Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
/**
2+
* ObjectQL
3+
* Copyright (c) 2026-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { ActionDefinition } from '@objectql/types';
10+
11+
/**
12+
* Project Actions - Custom Business Operations
13+
*
14+
* This file implements custom RPC actions for the Project object.
15+
* Actions are explicitly invoked by users or systems (not triggered by CRUD).
16+
*/
17+
18+
// ===== RECORD ACTIONS =====
19+
// These actions operate on a specific project record (require ID)
20+
21+
/**
22+
* Complete Action
23+
*
24+
* Marks a project as completed.
25+
* Type: Record Action (operates on a single project)
26+
*/
27+
interface CompleteInput {
28+
comment?: string;
29+
}
30+
31+
export const complete: ActionDefinition<any, CompleteInput> = {
32+
handler: async ({ id, input, api, user, objectName }) => {
33+
// Validate id is provided
34+
if (!id) {
35+
throw new Error('Project ID is required');
36+
}
37+
38+
// Fetch current project state
39+
const project = await api.findOne(objectName, id);
40+
if (!project) {
41+
throw new Error('Project not found');
42+
}
43+
44+
// Validate project is not already completed
45+
if (project.status === 'completed') {
46+
throw new Error('Project is already completed');
47+
}
48+
49+
// Update project status to completed
50+
await api.update(objectName, id, {
51+
status: 'completed',
52+
completed_by: user?.id || 'system',
53+
completed_at: new Date(),
54+
completion_comment: input.comment
55+
});
56+
57+
return {
58+
success: true,
59+
message: `Project "${project.name}" completed successfully`
60+
};
61+
}
62+
};
63+
64+
/**
65+
* Approve Action
66+
*
67+
* Approves a planned project and moves it to in_progress status.
68+
* Type: Record Action (operates on a single project)
69+
*/
70+
interface ApproveInput {
71+
comment: string;
72+
}
73+
74+
export const approve: ActionDefinition<any, ApproveInput> = {
75+
handler: async ({ id, input, api, user, objectName }) => {
76+
// Validate id is provided
77+
if (!id) {
78+
throw new Error('Project ID is required');
79+
}
80+
81+
// Validate approval comment is required
82+
if (!input.comment || input.comment.trim() === '') {
83+
throw new Error('Approval comment is required');
84+
}
85+
86+
// Fetch current project state
87+
const project = await api.findOne(objectName, id);
88+
if (!project) {
89+
throw new Error('Project not found');
90+
}
91+
92+
// Update project to in_progress status
93+
await api.update(objectName, id, {
94+
status: 'in_progress',
95+
approved_by: user?.id || 'system',
96+
approved_at: new Date(),
97+
approval_comment: input.comment
98+
});
99+
100+
return {
101+
success: true,
102+
message: `Project "${project.name}" approved`,
103+
new_status: 'in_progress'
104+
};
105+
}
106+
};
107+
108+
/**
109+
* Clone Action
110+
*
111+
* Creates a copy of an existing project.
112+
* Type: Record Action (operates on a single project)
113+
*/
114+
interface CloneInput {
115+
new_name: string;
116+
copy_tasks?: boolean;
117+
}
118+
119+
export const clone: ActionDefinition<any, CloneInput> = {
120+
handler: async ({ id, input, api, user, objectName }) => {
121+
// Validate id is provided
122+
if (!id) {
123+
throw new Error('Project ID is required');
124+
}
125+
126+
// Fetch source project
127+
const sourceProject = await api.findOne(objectName, id);
128+
if (!sourceProject) {
129+
throw new Error('Source project not found');
130+
}
131+
132+
// Create new project with cloned data
133+
const newProject = await api.create(objectName, {
134+
name: input.new_name,
135+
description: sourceProject.description,
136+
priority: sourceProject.priority,
137+
budget: sourceProject.budget,
138+
status: 'planned', // Always start cloned projects as planned
139+
owner: user?.id || 'system', // Assign to current user
140+
cloned_from: id,
141+
cloned_at: new Date()
142+
});
143+
144+
// TODO: Copy tasks if requested (when tasks functionality is implemented)
145+
if (input.copy_tasks) {
146+
// This would copy related tasks
147+
}
148+
149+
return {
150+
success: true,
151+
message: `Project cloned successfully`,
152+
new_project_id: newProject._id
153+
};
154+
}
155+
};
156+
157+
// ===== GLOBAL ACTIONS =====
158+
// These actions operate on the collection (no specific ID required)
159+
160+
/**
161+
* Import Projects Action
162+
*
163+
* Bulk imports projects from external data sources.
164+
* Type: Global Action (operates on the collection)
165+
*/
166+
interface ImportProjectsInput {
167+
source: string;
168+
data: Array<{
169+
name?: string;
170+
description?: string;
171+
status?: string;
172+
priority?: string;
173+
budget?: number;
174+
}>;
175+
}
176+
177+
export const import_projects: ActionDefinition<any, ImportProjectsInput> = {
178+
handler: async ({ input, api, user, objectName }) => {
179+
const errors: Array<{ index: number; error: string }> = [];
180+
let successCount = 0;
181+
182+
// Process each project in the data array
183+
for (let i = 0; i < input.data.length; i++) {
184+
const projectData = input.data[i];
185+
186+
try {
187+
// Validate required fields
188+
if (!projectData.name || projectData.name.trim() === '') {
189+
throw new Error('Project name is required');
190+
}
191+
192+
// Create the project
193+
await api.create(objectName, {
194+
...projectData,
195+
imported_from: input.source,
196+
imported_by: user?.id || 'system',
197+
imported_at: new Date()
198+
});
199+
200+
successCount++;
201+
} catch (error: any) {
202+
errors.push({
203+
index: i,
204+
error: error.message || 'Unknown error'
205+
});
206+
}
207+
}
208+
209+
return {
210+
success: true,
211+
message: `Imported ${successCount} projects`,
212+
successCount,
213+
failed: errors.length,
214+
errors
215+
};
216+
}
217+
};
218+
219+
/**
220+
* Bulk Update Status Action
221+
*
222+
* Updates the status of multiple projects at once.
223+
* Type: Global Action (operates on multiple records)
224+
*/
225+
interface BulkUpdateStatusInput {
226+
project_ids: string[];
227+
new_status: string;
228+
}
229+
230+
export const bulk_update_status: ActionDefinition<any, BulkUpdateStatusInput> = {
231+
handler: async ({ input, api, objectName }) => {
232+
let updated = 0;
233+
let skipped = 0;
234+
235+
// Process each project
236+
for (const projectId of input.project_ids) {
237+
try {
238+
const project = await api.findOne(objectName, projectId);
239+
240+
if (!project) {
241+
skipped++;
242+
continue;
243+
}
244+
245+
// Skip completed projects (they cannot be changed)
246+
if (project.status === 'completed') {
247+
skipped++;
248+
continue;
249+
}
250+
251+
// Update the status
252+
await api.update(objectName, projectId, {
253+
status: input.new_status
254+
});
255+
256+
updated++;
257+
} catch (error) {
258+
skipped++;
259+
}
260+
}
261+
262+
return {
263+
success: true,
264+
message: `Updated ${updated} projects`,
265+
updated,
266+
skipped
267+
};
268+
}
269+
};
270+
271+
/**
272+
* Generate Report Action
273+
*
274+
* Generates statistical reports about projects.
275+
* Type: Global Action (analytics on the collection)
276+
*/
277+
interface GenerateReportInput {
278+
// Optional filters could be added here
279+
}
280+
281+
export const generate_report: ActionDefinition<any, GenerateReportInput> = {
282+
handler: async ({ api, objectName }) => {
283+
// Fetch all projects
284+
const projects = await api.find(objectName, {});
285+
286+
// Calculate statistics
287+
const report = {
288+
total_projects: projects.length,
289+
by_status: {
290+
planned: 0,
291+
in_progress: 0,
292+
completed: 0
293+
} as Record<string, number>,
294+
by_priority: {} as Record<string, number>,
295+
total_budget: 0,
296+
average_budget: 0
297+
};
298+
299+
// Aggregate data
300+
projects.forEach((project: any) => {
301+
// Count by status
302+
if (project.status) {
303+
report.by_status[project.status] = (report.by_status[project.status] || 0) + 1;
304+
}
305+
306+
// Count by priority
307+
if (project.priority) {
308+
report.by_priority[project.priority] = (report.by_priority[project.priority] || 0) + 1;
309+
}
310+
311+
// Sum budgets
312+
if (project.budget) {
313+
report.total_budget += project.budget;
314+
}
315+
});
316+
317+
// Calculate average budget
318+
if (projects.length > 0) {
319+
report.average_budget = report.total_budget / projects.length;
320+
}
321+
322+
return {
323+
success: true,
324+
message: 'Report generated successfully',
325+
report,
326+
generated_at: new Date()
327+
};
328+
}
329+
};

0 commit comments

Comments
 (0)