@objectql/create 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,474 +7,323 @@
7
7
  */
8
8
 
9
9
  import { ActionDefinition } from '@objectql/types';
10
- import { Projects } from '../../types';
11
10
 
12
11
  /**
13
- * Project Actions - Comprehensive Examples
12
+ * Project Actions - Custom Business Operations
14
13
  *
15
- * This file demonstrates action patterns according to the ObjectQL specification:
16
- * 1. Record actions (operate on specific records)
17
- * 2. Global actions (operate on collections)
18
- * 3. Input validation
19
- * 4. Multi-step business logic
20
- * 5. Error handling
14
+ * This file implements custom RPC actions for the Project object.
15
+ * Actions are explicitly invoked by users or systems (not triggered by CRUD).
21
16
  */
22
17
 
18
+ // ===== RECORD ACTIONS =====
19
+ // These actions operate on a specific project record (require ID)
20
+
23
21
  /**
24
- * Input type for the complete action
22
+ * Complete Action
23
+ *
24
+ * Marks a project as completed.
25
+ * Type: Record Action (operates on a single project)
25
26
  */
26
27
  interface CompleteInput {
27
28
  comment?: string;
28
- completion_date?: Date;
29
29
  }
30
30
 
31
- /**
32
- * complete - Record Action Example
33
- *
34
- * Demonstrates:
35
- * - Fetching and validating current state
36
- * - Performing atomic updates
37
- * - Returning structured results
38
- * - Input parameter usage
39
- */
40
- export const complete: ActionDefinition<Projects, CompleteInput> = {
41
- handler: async ({ id, input, api, user }) => {
42
- const { comment, completion_date } = input;
43
-
44
- console.log(`[Action] Completing project ${id} by ${user?.id}`);
45
-
46
- // 1. Validate - Fetch current state
47
- const project = await api.findOne('projects', id!);
48
-
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);
49
40
  if (!project) {
50
41
  throw new Error('Project not found');
51
42
  }
52
-
43
+
44
+ // Validate project is not already completed
53
45
  if (project.status === 'completed') {
54
46
  throw new Error('Project is already completed');
55
47
  }
56
-
57
- // 2. Validate - Check if user has permission
58
- /*
59
- if (!user?.isAdmin && project.owner !== user?.id) {
60
- throw new Error('Only the project owner or admin can complete the project');
61
- }
62
- */
63
-
64
- // 3. Perform update with atomic operation
65
- const updateData: any = {
48
+
49
+ // Update project status to completed
50
+ await api.update(objectName, id, {
66
51
  status: 'completed',
67
- end_date: completion_date || new Date()
68
- };
69
-
70
- // Add completion comment to description
71
- if (comment) {
72
- updateData.description = project.description
73
- ? `${project.description}\n\n[Completed on ${new Date().toISOString()}]: ${comment}`
74
- : `[Completed on ${new Date().toISOString()}]: ${comment}`;
75
- }
76
-
77
- await api.update('projects', id!, updateData);
78
-
79
- // 4. Optional: Create completion record or notification
80
- /*
81
- await api.create('project_completions', {
82
- project_id: id,
83
- completed_by: user?.id,
52
+ completed_by: user?.id || 'system',
84
53
  completed_at: new Date(),
85
- comment: comment
54
+ completion_comment: input.comment
86
55
  });
87
- */
88
-
89
- // 5. Return structured result
90
- return {
91
- success: true,
92
- message: "Project completed successfully",
93
- project_id: id,
94
- completed_at: updateData.end_date
56
+
57
+ return {
58
+ success: true,
59
+ message: `Project "${project.name}" completed successfully`
95
60
  };
96
61
  }
97
62
  };
98
63
 
99
64
  /**
100
- * Input type for the approve action
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)
101
69
  */
102
70
  interface ApproveInput {
103
71
  comment: string;
104
72
  }
105
73
 
106
- /**
107
- * approve - Record Action with State Transition
108
- *
109
- * Demonstrates:
110
- * - State machine validation
111
- * - Required input parameters
112
- * - Business logic enforcement
113
- */
114
- export const approve: ActionDefinition<Projects, ApproveInput> = {
115
- handler: async ({ id, input, api, user }) => {
116
- const { comment } = input;
117
-
118
- // 1. Validate input
119
- if (!comment || comment.trim().length === 0) {
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() === '') {
120
83
  throw new Error('Approval comment is required');
121
84
  }
122
-
123
- // 2. Fetch and validate current state
124
- const project = await api.findOne('projects', id!);
125
-
85
+
86
+ // Fetch current project state
87
+ const project = await api.findOne(objectName, id);
126
88
  if (!project) {
127
89
  throw new Error('Project not found');
128
90
  }
129
-
130
- if (project.status !== 'planned') {
131
- throw new Error('Only projects in "planned" status can be approved');
132
- }
133
-
134
- // 3. Check budget threshold
135
- if (project.budget > 100000 && !user?.isAdmin) {
136
- throw new Error('Projects with budget over $100,000 require admin approval');
137
- }
138
-
139
- // 4. Perform approval
140
- await api.update('projects', id!, {
91
+
92
+ // Update project to in_progress status
93
+ await api.update(objectName, id, {
141
94
  status: 'in_progress',
142
- approved_by: user?.id,
95
+ approved_by: user?.id || 'system',
143
96
  approved_at: new Date(),
144
- approval_comment: comment
97
+ approval_comment: input.comment
145
98
  });
146
-
147
- // 5. Log approval
148
- console.log(`[Action] Project ${project.name} approved by ${user?.id}`);
149
-
99
+
150
100
  return {
151
101
  success: true,
152
- message: 'Project approved and moved to in_progress',
102
+ message: `Project "${project.name}" approved`,
153
103
  new_status: 'in_progress'
154
104
  };
155
105
  }
156
106
  };
157
107
 
158
108
  /**
159
- * Input type for clone action
109
+ * Clone Action
110
+ *
111
+ * Creates a copy of an existing project.
112
+ * Type: Record Action (operates on a single project)
160
113
  */
161
114
  interface CloneInput {
162
115
  new_name: string;
163
116
  copy_tasks?: boolean;
164
117
  }
165
118
 
166
- /**
167
- * clone - Record Action with Related Data
168
- *
169
- * Demonstrates:
170
- * - Creating new records based on existing ones
171
- * - Copying related data
172
- * - Complex multi-step operations
173
- */
174
- export const clone: ActionDefinition<Projects, CloneInput> = {
175
- handler: async ({ id, input, api, user }) => {
176
- const { new_name, copy_tasks = false } = input;
177
-
178
- // 1. Validate input
179
- if (!new_name || new_name.trim().length === 0) {
180
- throw new Error('New project name is required');
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');
181
124
  }
182
-
183
- // 2. Fetch source project
184
- const sourceProject = await api.findOne('projects', id!);
185
-
125
+
126
+ // Fetch source project
127
+ const sourceProject = await api.findOne(objectName, id);
186
128
  if (!sourceProject) {
187
129
  throw new Error('Source project not found');
188
130
  }
189
-
190
- // 3. Create cloned project
191
- const clonedData = {
192
- name: new_name,
193
- description: `Cloned from: ${sourceProject.name}\n\n${sourceProject.description || ''}`,
194
- status: 'planned', // Always start as planned
131
+
132
+ // Create new project with cloned data
133
+ const newProject = await api.create(objectName, {
134
+ name: input.new_name,
135
+ description: sourceProject.description,
195
136
  priority: sourceProject.priority,
196
- owner: user?.id || sourceProject.owner, // Assign to current user
197
137
  budget: sourceProject.budget,
198
- start_date: sourceProject.start_date
199
- // Don't copy: end_date, completed status
200
- };
201
-
202
- const newProject = await api.create('projects', clonedData);
203
-
204
- // 4. Optional: Copy related tasks
205
- if (copy_tasks) {
206
- /*
207
- const tasks = await api.find('tasks', {
208
- filters: [['project_id', '=', id]]
209
- });
210
-
211
- for (const task of tasks) {
212
- await api.create('tasks', {
213
- name: task.name,
214
- description: task.description,
215
- project_id: newProject._id,
216
- status: 'pending', // Reset status
217
- priority: task.priority
218
- });
219
- }
220
- */
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
221
147
  }
222
-
223
- console.log(`[Action] Project cloned: ${sourceProject.name} -> ${new_name}`);
224
-
148
+
225
149
  return {
226
150
  success: true,
227
- message: 'Project cloned successfully',
228
- original_id: id,
229
- new_project_id: newProject._id,
230
- tasks_copied: copy_tasks
151
+ message: `Project cloned successfully`,
152
+ new_project_id: newProject._id
231
153
  };
232
154
  }
233
155
  };
234
156
 
157
+ // ===== GLOBAL ACTIONS =====
158
+ // These actions operate on the collection (no specific ID required)
159
+
235
160
  /**
236
- * Input type for bulk import
161
+ * Import Projects Action
162
+ *
163
+ * Bulk imports projects from external data sources.
164
+ * Type: Global Action (operates on the collection)
237
165
  */
238
166
  interface ImportProjectsInput {
239
- source: 'csv' | 'json' | 'api';
240
- data?: any[];
241
- file_url?: string;
167
+ source: string;
168
+ data: Array<{
169
+ name?: string;
170
+ description?: string;
171
+ status?: string;
172
+ priority?: string;
173
+ budget?: number;
174
+ }>;
242
175
  }
243
176
 
244
- /**
245
- * import_projects - Global Action Example
246
- *
247
- * Demonstrates:
248
- * - Batch operations
249
- * - Data transformation
250
- * - Error collection
251
- * - Progress reporting
252
- */
253
- export const import_projects: ActionDefinition<Projects, ImportProjectsInput> = {
254
- handler: async ({ input, api, user }) => {
255
- const { source, data, file_url } = input;
256
-
257
- console.log(`[Action] Importing projects from ${source} by ${user?.id}`);
258
-
259
- // 1. Validate input
260
- if (!data && !file_url) {
261
- throw new Error('Either data array or file_url must be provided');
262
- }
263
-
264
- // 2. Fetch data based on source
265
- let projectsData: any[] = data || [];
266
-
267
- if (file_url) {
268
- // Example: Fetch from URL
269
- /*
270
- const response = await fetch(file_url);
271
- projectsData = await response.json();
272
- */
273
- throw new Error('file_url import not yet implemented');
274
- }
275
-
276
- // 3. Validate and import each project
277
- const results = {
278
- successCount: 0,
279
- failed: 0,
280
- errors: [] as any[]
281
- };
282
-
283
- for (let i = 0; i < projectsData.length; i++) {
284
- const projectData = projectsData[i];
285
-
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
+
286
186
  try {
287
187
  // Validate required fields
288
- if (!projectData.name) {
188
+ if (!projectData.name || projectData.name.trim() === '') {
289
189
  throw new Error('Project name is required');
290
190
  }
291
-
292
- // Set defaults
293
- const importData = {
294
- name: projectData.name,
295
- description: projectData.description || '',
296
- status: projectData.status || 'planned',
297
- priority: projectData.priority || 'normal',
298
- budget: projectData.budget || 0,
299
- owner: projectData.owner || user?.id,
300
- start_date: projectData.start_date
301
- };
302
-
303
- // Create project
304
- await api.create('projects', importData);
305
- results.successCount++;
306
-
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++;
307
201
  } catch (error: any) {
308
- results.failed++;
309
- results.errors.push({
310
- row: i + 1,
311
- name: projectData.name || 'Unknown',
312
- error: error.message
202
+ errors.push({
203
+ index: i,
204
+ error: error.message || 'Unknown error'
313
205
  });
314
206
  }
315
207
  }
316
-
317
- console.log(`[Action] Import completed: ${results.successCount} succeeded, ${results.failed} failed`);
318
-
208
+
319
209
  return {
320
- success: results.failed === 0,
321
- message: `Imported ${results.successCount} projects, ${results.failed} failed`,
322
- ...results
210
+ success: true,
211
+ message: `Imported ${successCount} projects`,
212
+ successCount,
213
+ failed: errors.length,
214
+ errors
323
215
  };
324
216
  }
325
217
  };
326
218
 
327
219
  /**
328
- * Input type for bulk update
220
+ * Bulk Update Status Action
221
+ *
222
+ * Updates the status of multiple projects at once.
223
+ * Type: Global Action (operates on multiple records)
329
224
  */
330
225
  interface BulkUpdateStatusInput {
331
226
  project_ids: string[];
332
- new_status: 'planned' | 'in_progress' | 'completed';
227
+ new_status: string;
333
228
  }
334
229
 
335
- /**
336
- * bulk_update_status - Global Action for Batch Updates
337
- *
338
- * Demonstrates:
339
- * - Operating on multiple records
340
- * - Validation across multiple items
341
- * - Transactional operations (if supported)
342
- */
343
- export const bulk_update_status: ActionDefinition<Projects, BulkUpdateStatusInput> = {
344
- handler: async ({ input, api, user }) => {
345
- const { project_ids, new_status } = input;
346
-
347
- // 1. Validate input
348
- if (!project_ids || project_ids.length === 0) {
349
- throw new Error('At least one project ID must be provided');
350
- }
351
-
352
- if (project_ids.length > 100) {
353
- throw new Error('Cannot update more than 100 projects at once');
354
- }
355
-
356
- // 2. Update each project
357
- // Note: This uses an N+1 query pattern (fetching each project individually)
358
- // For production with large batches, consider fetching all projects at once:
359
- // const projects = await api.find('projects', { filters: [['_id', 'in', project_ids]] });
360
- // However, for this example with a 100-project limit, the current approach
361
- // provides clearer per-record validation and error handling.
362
- const results = {
363
- updated: 0,
364
- skipped: 0,
365
- errors: [] as any[]
366
- };
367
-
368
- for (const id of project_ids) {
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) {
369
237
  try {
370
- const project = await api.findOne('projects', id);
238
+ const project = await api.findOne(objectName, projectId);
371
239
 
372
240
  if (!project) {
373
- results.errors.push({
374
- project_id: id,
375
- error: 'Project not found'
376
- });
377
- results.skipped++;
241
+ skipped++;
378
242
  continue;
379
243
  }
380
-
381
- // Validate transition (simplified)
382
- if (project.status === 'completed' && new_status !== 'completed') {
383
- results.errors.push({
384
- project_id: id,
385
- name: project.name,
386
- error: 'Cannot change status of completed projects'
387
- });
388
- results.skipped++;
244
+
245
+ // Skip completed projects (they cannot be changed)
246
+ if (project.status === 'completed') {
247
+ skipped++;
389
248
  continue;
390
249
  }
391
-
392
- // Perform update
393
- await api.update('projects', id, { status: new_status });
394
- results.updated++;
395
-
396
- } catch (error: any) {
397
- results.errors.push({
398
- project_id: id,
399
- error: error.message
250
+
251
+ // Update the status
252
+ await api.update(objectName, projectId, {
253
+ status: input.new_status
400
254
  });
401
- results.skipped++;
255
+
256
+ updated++;
257
+ } catch (error) {
258
+ skipped++;
402
259
  }
403
260
  }
404
-
405
- console.log(`[Action] Bulk update: ${results.updated} updated, ${results.skipped} skipped`);
406
-
261
+
407
262
  return {
408
- success: results.skipped === 0,
409
- message: `Updated ${results.updated} projects, skipped ${results.skipped}`,
410
- ...results
263
+ success: true,
264
+ message: `Updated ${updated} projects`,
265
+ updated,
266
+ skipped
411
267
  };
412
268
  }
413
269
  };
414
270
 
415
271
  /**
416
- * generate_report - Global Action for Reporting
272
+ * Generate Report Action
417
273
  *
418
- * Demonstrates:
419
- * - Aggregation and analysis
420
- * - Data collection across records
421
- * - Computed results
274
+ * Generates statistical reports about projects.
275
+ * Type: Global Action (analytics on the collection)
422
276
  */
423
- export const generate_report: ActionDefinition<Projects, {}> = {
424
- handler: async ({ api, user }) => {
425
- console.log(`[Action] Generating project report for ${user?.id}`);
426
-
427
- // 1. Fetch all projects (or apply filters)
428
- const allProjects = await api.find('projects', {});
429
-
430
- // 2. Calculate statistics
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
431
287
  const report = {
432
- total_projects: allProjects.length,
288
+ total_projects: projects.length,
433
289
  by_status: {
434
290
  planned: 0,
435
291
  in_progress: 0,
436
292
  completed: 0
437
- },
438
- by_priority: {
439
- low: 0,
440
- normal: 0,
441
- high: 0
442
- },
293
+ } as Record<string, number>,
294
+ by_priority: {} as Record<string, number>,
443
295
  total_budget: 0,
444
- average_budget: 0,
445
- generated_at: new Date(),
446
- generated_by: user?.id
296
+ average_budget: 0
447
297
  };
448
-
449
- // 3. Aggregate data
450
- allProjects.forEach(project => {
298
+
299
+ // Aggregate data
300
+ projects.forEach((project: any) => {
451
301
  // Count by status
452
- const status = project.status || 'planned';
453
- if (status in report.by_status) {
454
- report.by_status[status as keyof typeof report.by_status]++;
302
+ if (project.status) {
303
+ report.by_status[project.status] = (report.by_status[project.status] || 0) + 1;
455
304
  }
456
-
305
+
457
306
  // Count by priority
458
- const priority = project.priority || 'normal';
459
- if (priority in report.by_priority) {
460
- report.by_priority[priority as keyof typeof report.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;
461
314
  }
462
-
463
- // Sum budget
464
- report.total_budget += project.budget || 0;
465
315
  });
466
-
467
- // 4. Calculate averages
468
- report.average_budget = allProjects.length > 0
469
- ? report.total_budget / allProjects.length
470
- : 0;
471
-
472
- console.log(`[Action] Report generated: ${report.total_projects} projects analyzed`);
473
-
316
+
317
+ // Calculate average budget
318
+ if (projects.length > 0) {
319
+ report.average_budget = report.total_budget / projects.length;
320
+ }
321
+
474
322
  return {
475
323
  success: true,
476
324
  message: 'Report generated successfully',
477
- report
325
+ report,
326
+ generated_at: new Date()
478
327
  };
479
328
  }
480
329
  };