@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,490 +0,0 @@
1
- /**
2
- * Comprehensive Test Suite for Project Hooks and Actions
3
- *
4
- * This test file demonstrates and validates:
5
- * 1. All hook types (beforeCreate, afterCreate, beforeUpdate, etc.)
6
- * 2. All action types (record actions and global actions)
7
- * 3. Business logic patterns from the specification
8
- */
9
-
10
- import { ObjectQL } from '@objectql/core';
11
- import hooks from '../src/modules/projects/projects.hook';
12
- import * as actions from '../src/modules/projects/projects.action';
13
-
14
- describe('Project Hooks - Comprehensive Examples', () => {
15
- let app: ObjectQL;
16
-
17
- beforeEach(async () => {
18
- // Use in-memory driver for testing
19
- app = new ObjectQL({
20
- datasources: {
21
- default: {
22
- find: jest.fn().mockResolvedValue([]),
23
- findOne: jest.fn().mockResolvedValue(null),
24
- create: jest.fn((obj, data) => ({ ...data, _id: 'test-id' })),
25
- update: jest.fn((obj, id, data) => data),
26
- delete: jest.fn().mockResolvedValue(true),
27
- count: jest.fn().mockResolvedValue(0)
28
- } as any
29
- },
30
- objects: {
31
- 'projects': {
32
- name: 'projects',
33
- fields: {
34
- name: { type: 'text' },
35
- status: { type: 'text' },
36
- budget: { type: 'number' },
37
- owner: { type: 'text' }
38
- }
39
- }
40
- }
41
- });
42
- await app.init();
43
-
44
- // Register hooks
45
- if (hooks.beforeCreate) app.on('beforeCreate', 'projects', hooks.beforeCreate);
46
- if (hooks.afterCreate) app.on('afterCreate', 'projects', hooks.afterCreate);
47
- if (hooks.beforeFind) app.on('beforeFind', 'projects', hooks.beforeFind);
48
- if (hooks.afterFind) app.on('afterFind', 'projects', hooks.afterFind);
49
- if (hooks.beforeUpdate) app.on('beforeUpdate', 'projects', hooks.beforeUpdate);
50
- if (hooks.afterUpdate) app.on('afterUpdate', 'projects', hooks.afterUpdate);
51
- if (hooks.beforeDelete) app.on('beforeDelete', 'projects', hooks.beforeDelete);
52
- if (hooks.afterDelete) app.on('afterDelete', 'projects', hooks.afterDelete);
53
- });
54
-
55
- describe('beforeCreate Hook', () => {
56
- it('should auto-assign owner from user context', async () => {
57
- const repo = app.createContext({ userId: 'user123' }).object('projects');
58
-
59
- await repo.create({ name: 'Test Project' });
60
-
61
- const driver = app.datasource('default');
62
- expect(driver.create).toHaveBeenCalledWith(
63
- 'projects',
64
- expect.objectContaining({
65
- name: 'Test Project',
66
- owner: 'user123'
67
- }),
68
- expect.any(Object)
69
- );
70
- });
71
-
72
- it('should set default status to planned', async () => {
73
- const repo = app.createContext({}).object('projects');
74
-
75
- await repo.create({ name: 'Test Project' });
76
-
77
- const driver = app.datasource('default');
78
- expect(driver.create).toHaveBeenCalledWith(
79
- 'projects',
80
- expect.objectContaining({
81
- status: 'planned'
82
- }),
83
- expect.any(Object)
84
- );
85
- });
86
-
87
- it('should validate project name is required', async () => {
88
- const repo = app.createContext({}).object('projects');
89
-
90
- await expect(repo.create({ name: '' }))
91
- .rejects
92
- .toThrow('Project name is required');
93
- });
94
-
95
- it('should validate project name length', async () => {
96
- const repo = app.createContext({}).object('projects');
97
- const longName = 'a'.repeat(101);
98
-
99
- await expect(repo.create({ name: longName }))
100
- .rejects
101
- .toThrow('Project name must be 100 characters or less');
102
- });
103
-
104
- it('should set default budget to 0', async () => {
105
- const repo = app.createContext({}).object('projects');
106
-
107
- await repo.create({ name: 'Test Project' });
108
-
109
- const driver = app.datasource('default');
110
- expect(driver.create).toHaveBeenCalledWith(
111
- 'projects',
112
- expect.objectContaining({
113
- budget: 0
114
- }),
115
- expect.any(Object)
116
- );
117
- });
118
- });
119
-
120
- describe('beforeUpdate Hook', () => {
121
- it('should validate budget is not negative', async () => {
122
- const repo = app.createContext({}).object('projects');
123
- const driver = app.datasource('default');
124
-
125
- // Mock existing project
126
- (driver.findOne as jest.Mock).mockResolvedValueOnce({
127
- _id: '1',
128
- name: 'Test',
129
- status: 'planned',
130
- budget: 1000
131
- });
132
-
133
- await expect(repo.update('1', { budget: -100 }))
134
- .rejects
135
- .toThrow('Budget cannot be negative');
136
- });
137
-
138
- it('should prevent invalid status transitions', async () => {
139
- const repo = app.createContext({}).object('projects');
140
- const driver = app.datasource('default');
141
-
142
- // Mock completed project
143
- (driver.findOne as jest.Mock).mockResolvedValueOnce({
144
- _id: '1',
145
- name: 'Test',
146
- status: 'completed',
147
- budget: 1000
148
- });
149
-
150
- await expect(repo.update('1', { status: 'planned' }))
151
- .rejects
152
- .toThrow('Invalid status transition');
153
- });
154
-
155
- it('should allow valid status transition from planned to in_progress', async () => {
156
- const repo = app.createContext({}).object('projects');
157
- const driver = app.datasource('default');
158
-
159
- (driver.findOne as jest.Mock).mockResolvedValueOnce({
160
- _id: '1',
161
- name: 'Test',
162
- status: 'planned',
163
- budget: 1000
164
- });
165
-
166
- await repo.update('1', { status: 'in_progress' });
167
-
168
- expect(driver.update).toHaveBeenCalled();
169
- });
170
-
171
- it('should require end_date when marking as completed', async () => {
172
- const repo = app.createContext({}).object('projects');
173
- const driver = app.datasource('default');
174
-
175
- (driver.findOne as jest.Mock).mockResolvedValueOnce({
176
- _id: '1',
177
- name: 'Test',
178
- status: 'in_progress',
179
- budget: 1000
180
- });
181
-
182
- await expect(repo.update('1', { status: 'completed' }))
183
- .rejects
184
- .toThrow('End date is required when completing a project');
185
- });
186
- });
187
-
188
- describe('beforeDelete Hook', () => {
189
- it('should prevent deletion of completed projects', async () => {
190
- const repo = app.createContext({}).object('projects');
191
- const driver = app.datasource('default');
192
-
193
- (driver.findOne as jest.Mock).mockResolvedValueOnce({
194
- _id: '1',
195
- name: 'Test',
196
- status: 'completed'
197
- });
198
-
199
- await expect(repo.delete('1'))
200
- .rejects
201
- .toThrow('Cannot delete completed projects');
202
- });
203
- });
204
-
205
- describe('afterFind Hook', () => {
206
- it('should add computed progress field', async () => {
207
- const repo = app.createContext({}).object('projects');
208
- const driver = app.datasource('default');
209
-
210
- (driver.find as jest.Mock).mockResolvedValueOnce([
211
- { name: 'Project 1', status: 'planned' },
212
- { name: 'Project 2', status: 'in_progress' },
213
- { name: 'Project 3', status: 'completed' }
214
- ]);
215
-
216
- const results = await repo.find({});
217
-
218
- expect(results[0].progress).toBe(0);
219
- expect(results[1].progress).toBe(50);
220
- expect(results[2].progress).toBe(100);
221
- });
222
- });
223
- });
224
-
225
- describe('Project Actions - Comprehensive Examples', () => {
226
- let mockApi: any;
227
- let mockUser: any;
228
-
229
- beforeEach(() => {
230
- mockUser = { id: 'user123', isAdmin: false };
231
- mockApi = {
232
- findOne: jest.fn(),
233
- find: jest.fn(),
234
- create: jest.fn(),
235
- update: jest.fn(),
236
- delete: jest.fn(),
237
- count: jest.fn()
238
- };
239
- });
240
-
241
- describe('complete - Record Action', () => {
242
- it('should complete a project successfully', async () => {
243
- const mockProject = {
244
- _id: 'proj1',
245
- name: 'Test Project',
246
- status: 'in_progress',
247
- description: 'Original description'
248
- };
249
-
250
- mockApi.findOne.mockResolvedValue(mockProject);
251
- mockApi.update.mockResolvedValue({ ...mockProject, status: 'completed' });
252
-
253
- const result = await actions.complete.handler({
254
- objectName: 'projects',
255
- actionName: 'complete',
256
- id: 'proj1',
257
- input: { comment: 'All tasks done' },
258
- api: mockApi,
259
- user: mockUser
260
- });
261
-
262
- expect(result.success).toBe(true);
263
- expect(result.message).toContain('completed successfully');
264
- expect(mockApi.update).toHaveBeenCalledWith(
265
- 'projects',
266
- 'proj1',
267
- expect.objectContaining({
268
- status: 'completed'
269
- })
270
- );
271
- });
272
-
273
- it('should reject if project is already completed', async () => {
274
- mockApi.findOne.mockResolvedValue({
275
- _id: 'proj1',
276
- status: 'completed'
277
- });
278
-
279
- await expect(actions.complete.handler({
280
- objectName: 'projects',
281
- actionName: 'complete',
282
- id: 'proj1',
283
- input: {},
284
- api: mockApi,
285
- user: mockUser
286
- })).rejects.toThrow('already completed');
287
- });
288
- });
289
-
290
- describe('approve - Record Action', () => {
291
- it('should approve a planned project', async () => {
292
- mockApi.findOne.mockResolvedValue({
293
- _id: 'proj1',
294
- name: 'Test Project',
295
- status: 'planned',
296
- budget: 50000
297
- });
298
-
299
- const result = await actions.approve.handler({
300
- objectName: 'projects',
301
- actionName: 'approve',
302
- id: 'proj1',
303
- input: { comment: 'Looks good' },
304
- api: mockApi,
305
- user: mockUser
306
- });
307
-
308
- expect(result.success).toBe(true);
309
- expect(result.new_status).toBe('in_progress');
310
- expect(mockApi.update).toHaveBeenCalledWith(
311
- 'projects',
312
- 'proj1',
313
- expect.objectContaining({
314
- status: 'in_progress',
315
- approved_by: 'user123'
316
- })
317
- );
318
- });
319
-
320
- it('should require approval comment', async () => {
321
- await expect(actions.approve.handler({
322
- objectName: 'projects',
323
- actionName: 'approve',
324
- id: 'proj1',
325
- input: { comment: '' },
326
- api: mockApi,
327
- user: mockUser
328
- })).rejects.toThrow('Approval comment is required');
329
- });
330
- });
331
-
332
- describe('clone - Record Action', () => {
333
- it('should clone a project with new name', async () => {
334
- const sourceProject = {
335
- _id: 'proj1',
336
- name: 'Original Project',
337
- description: 'Original description',
338
- status: 'in_progress',
339
- priority: 'high',
340
- budget: 10000,
341
- owner: 'owner123'
342
- };
343
-
344
- mockApi.findOne.mockResolvedValue(sourceProject);
345
- mockApi.create.mockResolvedValue({ _id: 'proj2', name: 'Cloned Project' });
346
-
347
- const result = await actions.clone.handler({
348
- objectName: 'projects',
349
- actionName: 'clone',
350
- id: 'proj1',
351
- input: { new_name: 'Cloned Project', copy_tasks: false },
352
- api: mockApi,
353
- user: mockUser
354
- });
355
-
356
- expect(result.success).toBe(true);
357
- expect(result.new_project_id).toBe('proj2');
358
- expect(mockApi.create).toHaveBeenCalledWith(
359
- 'projects',
360
- expect.objectContaining({
361
- name: 'Cloned Project',
362
- status: 'planned', // Reset to planned
363
- priority: 'high',
364
- budget: 10000,
365
- owner: 'user123' // Assigned to current user
366
- })
367
- );
368
- });
369
- });
370
-
371
- describe('import_projects - Global Action', () => {
372
- it('should import multiple projects successfully', async () => {
373
- const projectsData = [
374
- { name: 'Project 1', description: 'Desc 1' },
375
- { name: 'Project 2', description: 'Desc 2', status: 'in_progress' }
376
- ];
377
-
378
- mockApi.create.mockResolvedValue({ _id: 'new-id' });
379
-
380
- const result = await actions.import_projects.handler({
381
- objectName: 'projects',
382
- actionName: 'import_projects',
383
- input: { source: 'json', data: projectsData },
384
- api: mockApi,
385
- user: mockUser
386
- });
387
-
388
- expect(result.success).toBe(true);
389
- expect(result.successCount).toBe(2);
390
- expect(mockApi.create).toHaveBeenCalledTimes(2);
391
- });
392
-
393
- it('should collect errors for invalid projects', async () => {
394
- const projectsData = [
395
- { name: 'Valid Project' },
396
- { description: 'Missing name' }, // Invalid - no name
397
- { name: 'Another Valid' }
398
- ];
399
-
400
- mockApi.create.mockResolvedValue({ _id: 'new-id' });
401
-
402
- const result = await actions.import_projects.handler({
403
- objectName: 'projects',
404
- actionName: 'import_projects',
405
- input: { source: 'json', data: projectsData },
406
- api: mockApi,
407
- user: mockUser
408
- });
409
-
410
- expect(result.failed).toBe(1);
411
- expect(result.errors).toHaveLength(1);
412
- });
413
- });
414
-
415
- describe('bulk_update_status - Global Action', () => {
416
- it('should update multiple projects status', async () => {
417
- mockApi.findOne.mockImplementation((obj: string, id: string) => ({
418
- _id: id,
419
- name: `Project ${id}`,
420
- status: 'planned'
421
- }));
422
-
423
- const result = await actions.bulk_update_status.handler({
424
- objectName: 'projects',
425
- actionName: 'bulk_update_status',
426
- input: {
427
- project_ids: ['proj1', 'proj2', 'proj3'],
428
- new_status: 'in_progress'
429
- },
430
- api: mockApi,
431
- user: mockUser
432
- });
433
-
434
- expect(result.updated).toBe(3);
435
- expect(mockApi.update).toHaveBeenCalledTimes(3);
436
- });
437
-
438
- it('should skip completed projects', async () => {
439
- mockApi.findOne.mockImplementation((obj: string, id: string) => {
440
- if (id === 'proj2') {
441
- return { _id: id, name: 'Completed Project', status: 'completed' };
442
- }
443
- return { _id: id, name: 'Project', status: 'planned' };
444
- });
445
-
446
- const result = await actions.bulk_update_status.handler({
447
- objectName: 'projects',
448
- actionName: 'bulk_update_status',
449
- input: {
450
- project_ids: ['proj1', 'proj2', 'proj3'],
451
- new_status: 'planned'
452
- },
453
- api: mockApi,
454
- user: mockUser
455
- });
456
-
457
- expect(result.updated).toBe(2);
458
- expect(result.skipped).toBe(1);
459
- });
460
- });
461
-
462
- describe('generate_report - Global Action', () => {
463
- it('should generate statistical report', async () => {
464
- const mockProjects = [
465
- { name: 'P1', status: 'planned', priority: 'high', budget: 10000 },
466
- { name: 'P2', status: 'in_progress', priority: 'normal', budget: 20000 },
467
- { name: 'P3', status: 'completed', priority: 'low', budget: 15000 },
468
- { name: 'P4', status: 'planned', priority: 'normal', budget: 5000 }
469
- ];
470
-
471
- mockApi.find.mockResolvedValue(mockProjects);
472
-
473
- const result = await actions.generate_report.handler({
474
- objectName: 'projects',
475
- actionName: 'generate_report',
476
- input: {},
477
- api: mockApi,
478
- user: mockUser
479
- });
480
-
481
- expect(result.success).toBe(true);
482
- expect(result.report.total_projects).toBe(4);
483
- expect(result.report.by_status.planned).toBe(2);
484
- expect(result.report.by_status.in_progress).toBe(1);
485
- expect(result.report.by_status.completed).toBe(1);
486
- expect(result.report.total_budget).toBe(50000);
487
- expect(result.report.average_budget).toBe(12500);
488
- });
489
- });
490
- });