@objectql/create 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/bin.js +125 -0
- package/package.json +27 -0
- package/templates/enterprise/CHANGELOG.md +140 -0
- package/templates/enterprise/README.md +352 -0
- package/templates/enterprise/__tests__/data-api.test.ts +546 -0
- package/templates/enterprise/__tests__/data-api.test.ts.backup +526 -0
- package/templates/enterprise/__tests__/metadata-api.test.ts +307 -0
- package/templates/enterprise/__tests__/metadata-loading.test.ts +250 -0
- package/templates/enterprise/jest.config.js +22 -0
- package/templates/enterprise/package.json +51 -0
- package/templates/enterprise/src/apps/erp.app.yml +4 -0
- package/templates/enterprise/src/core/attachment.object.yml +57 -0
- package/templates/enterprise/src/core/i18n/en/core.json +60 -0
- package/templates/enterprise/src/core/i18n/zh-CN/core.json +60 -0
- package/templates/enterprise/src/core/index.ts +24 -0
- package/templates/enterprise/src/core/organization.object.yml +78 -0
- package/templates/enterprise/src/core/user.object.yml +80 -0
- package/templates/enterprise/src/extensions/README.md +56 -0
- package/templates/enterprise/src/extensions/user.extension.object.yml +42 -0
- package/templates/enterprise/src/extensions/user.ts +26 -0
- package/templates/enterprise/src/index.ts +47 -0
- package/templates/enterprise/src/modules/crm/README.md +99 -0
- package/templates/enterprise/src/modules/crm/crm_account.object.yml +105 -0
- package/templates/enterprise/src/modules/crm/crm_contact.object.yml +103 -0
- package/templates/enterprise/src/modules/crm/crm_lead.object.yml +148 -0
- package/templates/enterprise/src/modules/crm/crm_opportunity.object.yml +128 -0
- package/templates/enterprise/src/modules/crm/i18n/en/crm.json +61 -0
- package/templates/enterprise/src/modules/crm/i18n/zh-CN/crm.json +61 -0
- package/templates/enterprise/src/modules/crm/index.ts +29 -0
- package/templates/enterprise/src/modules/finance/README.md +112 -0
- package/templates/enterprise/src/modules/finance/finance_budget.object.yml +108 -0
- package/templates/enterprise/src/modules/finance/finance_expense.object.yml +151 -0
- package/templates/enterprise/src/modules/finance/finance_invoice.object.yml +143 -0
- package/templates/enterprise/src/modules/finance/finance_payment.object.yml +96 -0
- package/templates/enterprise/src/modules/finance/index.ts +26 -0
- package/templates/enterprise/src/modules/hr/README.md +95 -0
- package/templates/enterprise/src/modules/hr/hr_department.object.yml +59 -0
- package/templates/enterprise/src/modules/hr/hr_employee.object.yml +137 -0
- package/templates/enterprise/src/modules/hr/hr_position.object.yml +79 -0
- package/templates/enterprise/src/modules/hr/hr_timesheet.object.yml +114 -0
- package/templates/enterprise/src/modules/hr/index.ts +26 -0
- package/templates/enterprise/src/modules/project/README.md +132 -0
- package/templates/enterprise/src/modules/project/index.ts +26 -0
- package/templates/enterprise/src/modules/project/project_milestone.object.yml +70 -0
- package/templates/enterprise/src/modules/project/project_project.object.yml +135 -0
- package/templates/enterprise/src/modules/project/project_task.object.yml +121 -0
- package/templates/enterprise/src/modules/project/project_timesheet_entry.object.yml +95 -0
- package/templates/enterprise/src/plugins/audit/audit.plugin.ts +23 -0
- package/templates/enterprise/src/plugins/audit/index.ts +6 -0
- package/templates/enterprise/src/plugins/audit/note.object.yml +3 -0
- package/templates/enterprise/src/shared/constants.ts +30 -0
- package/templates/enterprise/src/shared/utils.ts +54 -0
- package/templates/enterprise/src/shared/validators.ts +47 -0
- package/templates/enterprise/src/types/attachment.ts +41 -0
- package/templates/enterprise/src/types/crm_account.ts +61 -0
- package/templates/enterprise/src/types/crm_contact.ts +61 -0
- package/templates/enterprise/src/types/crm_lead.ts +77 -0
- package/templates/enterprise/src/types/crm_opportunity.ts +53 -0
- package/templates/enterprise/src/types/finance_budget.ts +61 -0
- package/templates/enterprise/src/types/finance_expense.ts +65 -0
- package/templates/enterprise/src/types/finance_invoice.ts +69 -0
- package/templates/enterprise/src/types/finance_payment.ts +49 -0
- package/templates/enterprise/src/types/hr_department.ts +37 -0
- package/templates/enterprise/src/types/hr_employee.ts +85 -0
- package/templates/enterprise/src/types/hr_position.ts +49 -0
- package/templates/enterprise/src/types/hr_timesheet.ts +57 -0
- package/templates/enterprise/src/types/index.ts +20 -0
- package/templates/enterprise/src/types/note.ts +9 -0
- package/templates/enterprise/src/types/organization.ts +53 -0
- package/templates/enterprise/src/types/project_milestone.ts +41 -0
- package/templates/enterprise/src/types/project_project.ts +69 -0
- package/templates/enterprise/src/types/project_task.ts +57 -0
- package/templates/enterprise/src/types/project_timesheet_entry.ts +45 -0
- package/templates/enterprise/src/types/user.ts +65 -0
- package/templates/enterprise/tsconfig.json +10 -0
- package/templates/enterprise/tsconfig.tsbuildinfo +1 -0
- package/templates/hello-world/CHANGELOG.md +33 -0
- package/templates/hello-world/README.md +29 -0
- package/templates/hello-world/package.json +24 -0
- package/templates/hello-world/src/index.ts +58 -0
- package/templates/hello-world/tsconfig.json +10 -0
- package/templates/starter/CHANGELOG.md +191 -0
- package/templates/starter/README.md +17 -0
- package/templates/starter/__tests__/projects-hooks-actions.test.ts +490 -0
- package/templates/starter/jest.config.js +22 -0
- package/templates/starter/package.json +51 -0
- package/templates/starter/src/README.pages.md +110 -0
- package/templates/starter/src/demo.app.yml +4 -0
- package/templates/starter/src/i18n/zh-CN/projects.json +22 -0
- package/templates/starter/src/index.ts +55 -0
- package/templates/starter/src/modules/kitchen-sink/kitchen_sink.data.yml +18 -0
- package/templates/starter/src/modules/kitchen-sink/kitchen_sink.object.yml +156 -0
- package/templates/starter/src/modules/projects/project_approval.workflow.yml +51 -0
- package/templates/starter/src/modules/projects/projects.action.ts +472 -0
- package/templates/starter/src/modules/projects/projects.data.yml +13 -0
- package/templates/starter/src/modules/projects/projects.hook.ts +339 -0
- package/templates/starter/src/modules/projects/projects.object.yml +148 -0
- package/templates/starter/src/modules/projects/projects.permission.yml +141 -0
- package/templates/starter/src/modules/projects/projects.validation.yml +37 -0
- package/templates/starter/src/modules/tasks/tasks.data.yml +23 -0
- package/templates/starter/src/modules/tasks/tasks.object.yml +34 -0
- package/templates/starter/src/modules/tasks/tasks.permission.yml +167 -0
- package/templates/starter/src/types/index.ts +3 -0
- package/templates/starter/src/types/kitchen_sink.ts +101 -0
- package/templates/starter/src/types/projects.ts +49 -0
- package/templates/starter/src/types/tasks.ts +33 -0
- package/templates/starter/tsconfig.json +11 -0
- package/templates/starter/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,490 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
5
|
+
moduleNameMapper: {
|
|
6
|
+
'^@objectql/core$': '<rootDir>/../../foundation/core/src',
|
|
7
|
+
'^@objectql/types$': '<rootDir>/../../foundation/types/src',
|
|
8
|
+
'^@objectql/platform-node$': '<rootDir>/../../foundation/platform-node/src',
|
|
9
|
+
'^@objectql/driver-sql$': '<rootDir>/../../drivers/sql/src',
|
|
10
|
+
'^@objectql/driver-mongo$': '<rootDir>/../../drivers/mongo/src',
|
|
11
|
+
'^@objectql/server$': '<rootDir>/../../runtime/server/src',
|
|
12
|
+
},
|
|
13
|
+
transform: {
|
|
14
|
+
'^.+\\.ts$': ['ts-jest', {
|
|
15
|
+
isolatedModules: true,
|
|
16
|
+
}],
|
|
17
|
+
},
|
|
18
|
+
collectCoverageFrom: [
|
|
19
|
+
'src/**/*.ts',
|
|
20
|
+
'!src/**/*.d.ts',
|
|
21
|
+
],
|
|
22
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@objectql/example-project-tracker",
|
|
3
|
+
"version": "1.8.4",
|
|
4
|
+
"description": "ObjectQL Basic Example Project",
|
|
5
|
+
"private": true,
|
|
6
|
+
"keywords": [
|
|
7
|
+
"objectql",
|
|
8
|
+
"module",
|
|
9
|
+
"projects",
|
|
10
|
+
"tasks",
|
|
11
|
+
"basic",
|
|
12
|
+
"standard-library"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "ObjectQL Contributors",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/objectql/objectql.git",
|
|
19
|
+
"directory": "examples/showcase/project-tracker"
|
|
20
|
+
},
|
|
21
|
+
"main": "dist/index.js",
|
|
22
|
+
"types": "dist/index.d.ts",
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"start": "ts-node src/index.ts",
|
|
28
|
+
"codegen": "objectql generate -s src -o src/types",
|
|
29
|
+
"build": "npm run codegen && tsc && rsync -a --include '*/' --include '*.yml' --exclude '*' src/ dist/",
|
|
30
|
+
"repl": "objectql repl",
|
|
31
|
+
"test": "jest"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@objectql/core": "workspace:*",
|
|
35
|
+
"@objectql/driver-sql": "workspace:*",
|
|
36
|
+
"@objectql/platform-node": "workspace:*",
|
|
37
|
+
"@objectql/types": "workspace:*",
|
|
38
|
+
"sqlite3": "^5.1.7"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@objectql/cli": "workspace:*",
|
|
42
|
+
"@objectql/core": "workspace:*",
|
|
43
|
+
"@objectql/driver-sql": "workspace:*",
|
|
44
|
+
"@objectql/platform-node": "workspace:*",
|
|
45
|
+
"@objectql/types": "workspace:*",
|
|
46
|
+
"@types/jest": "^30.0.0",
|
|
47
|
+
"jest": "^30.2.0",
|
|
48
|
+
"ts-jest": "^29.4.6",
|
|
49
|
+
"typescript": "^5.3.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Page Examples
|
|
2
|
+
|
|
3
|
+
This directory contains example page definitions demonstrating ObjectQL's page metadata capabilities.
|
|
4
|
+
|
|
5
|
+
## Available Pages
|
|
6
|
+
|
|
7
|
+
### 1. Dashboard (`dashboard.page.yml`)
|
|
8
|
+
A comprehensive dashboard page showing:
|
|
9
|
+
- KPI metrics (total projects, active tasks, etc.)
|
|
10
|
+
- Charts (pie chart, line chart)
|
|
11
|
+
- Data grid with recent tasks
|
|
12
|
+
- Grid-based layout for flexible positioning
|
|
13
|
+
- Real-time updates enabled
|
|
14
|
+
|
|
15
|
+
**Layout:** Dashboard (grid-based)
|
|
16
|
+
**Use Case:** Overview and monitoring
|
|
17
|
+
|
|
18
|
+
### 2. Project Detail (`project_detail.page.yml`)
|
|
19
|
+
A two-column detail page featuring:
|
|
20
|
+
- Main content area with editable form
|
|
21
|
+
- Sidebar with metrics and activity timeline
|
|
22
|
+
- Task list for the project
|
|
23
|
+
- Quick action buttons
|
|
24
|
+
- Responsive design that stacks on mobile
|
|
25
|
+
|
|
26
|
+
**Layout:** Two Column
|
|
27
|
+
**Use Case:** Viewing and editing individual records
|
|
28
|
+
|
|
29
|
+
### 3. Create Project Wizard (`create_project_wizard.page.yml`)
|
|
30
|
+
A multi-step wizard for creating projects:
|
|
31
|
+
- Step 1: Basic information
|
|
32
|
+
- Step 2: Team and resources
|
|
33
|
+
- Step 3: Timeline and milestones
|
|
34
|
+
- Step 4: Review and confirmation
|
|
35
|
+
- State management with draft saving
|
|
36
|
+
|
|
37
|
+
**Layout:** Wizard (multi-step)
|
|
38
|
+
**Use Case:** Guided data entry processes
|
|
39
|
+
|
|
40
|
+
### 4. Landing Page (`landing.page.yml`)
|
|
41
|
+
A custom landing page with:
|
|
42
|
+
- Hero section with CTA
|
|
43
|
+
- Features grid
|
|
44
|
+
- Statistics display
|
|
45
|
+
- Canvas layout for absolute positioning
|
|
46
|
+
- Public access (no authentication)
|
|
47
|
+
|
|
48
|
+
**Layout:** Canvas (free-form)
|
|
49
|
+
**Use Case:** Marketing and public pages
|
|
50
|
+
|
|
51
|
+
## Loading Pages
|
|
52
|
+
|
|
53
|
+
Pages are automatically loaded by ObjectQL when scanning the directory:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { ObjectQL } from '@objectql/core';
|
|
57
|
+
|
|
58
|
+
const app = new ObjectQL({
|
|
59
|
+
source: './src'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await app.init();
|
|
63
|
+
|
|
64
|
+
// Access loaded pages
|
|
65
|
+
const pages = app.registry.list('page');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Using Pages in Navigation
|
|
69
|
+
|
|
70
|
+
Reference pages in your application navigation:
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
# demo.app.yml
|
|
74
|
+
navigation:
|
|
75
|
+
- type: page
|
|
76
|
+
name: dashboard
|
|
77
|
+
label: Dashboard
|
|
78
|
+
path: /dashboard
|
|
79
|
+
|
|
80
|
+
- type: page
|
|
81
|
+
name: create_project_wizard
|
|
82
|
+
label: New Project
|
|
83
|
+
path: /projects/new
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Best Practices Demonstrated
|
|
87
|
+
|
|
88
|
+
1. **Descriptive IDs**: Components use clear, descriptive identifiers
|
|
89
|
+
2. **Data Binding**: Examples use `{{}}` syntax for dynamic values
|
|
90
|
+
3. **Responsive Design**: Pages include responsive configurations
|
|
91
|
+
4. **Access Control**: Different permission levels shown
|
|
92
|
+
5. **AI Context**: All pages include AI context for understanding
|
|
93
|
+
6. **State Management**: Wizard demonstrates state handling
|
|
94
|
+
7. **Component Composition**: Nested components for complex UIs
|
|
95
|
+
|
|
96
|
+
## Documentation
|
|
97
|
+
|
|
98
|
+
- [Page Metadata Guide](../../../docs/guide/page-metadata.md)
|
|
99
|
+
- [Page Specification](../../../docs/spec/page.md)
|
|
100
|
+
- [Data Modeling](../../../docs/guide/data-modeling.md)
|
|
101
|
+
|
|
102
|
+
## Contributing
|
|
103
|
+
|
|
104
|
+
When adding new page examples:
|
|
105
|
+
1. Follow the naming convention: `[name].page.yml`
|
|
106
|
+
2. Include all standard fields (name, label, layout)
|
|
107
|
+
3. Add helpful comments in YAML
|
|
108
|
+
4. Provide AI context
|
|
109
|
+
5. Configure appropriate permissions
|
|
110
|
+
6. Test responsive behavior
|