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