@objectql/create 4.0.0 → 4.0.2

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