@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.
- package/dist/bin.js +7 -0
- package/package.json +1 -1
- package/templates/enterprise/CHANGELOG.md +10 -0
- package/templates/enterprise/jest.config.js +8 -0
- package/templates/enterprise/package.json +4 -2
- package/templates/enterprise/src/core/index.ts +8 -0
- package/templates/enterprise/src/extensions/user.ts +8 -0
- package/templates/enterprise/src/index.ts +10 -2
- package/templates/enterprise/src/modules/crm/index.ts +8 -0
- package/templates/enterprise/src/modules/finance/index.ts +8 -0
- package/templates/enterprise/src/modules/hr/index.ts +8 -0
- package/templates/enterprise/src/modules/project/index.ts +8 -0
- package/templates/enterprise/src/plugins/audit/audit.plugin.ts +40 -19
- package/templates/enterprise/src/plugins/audit/index.ts +9 -4
- package/templates/enterprise/src/shared/constants.ts +8 -0
- package/templates/enterprise/src/shared/utils.ts +8 -0
- package/templates/enterprise/src/shared/validators.ts +8 -0
- package/templates/enterprise/tsconfig.json +5 -0
- package/templates/enterprise/tsconfig.tsbuildinfo +1 -1
- package/templates/hello-world/CHANGELOG.md +8 -0
- package/templates/hello-world/package.json +1 -1
- package/templates/hello-world/src/index.ts +8 -0
- package/templates/starter/CHANGELOG.md +10 -0
- package/templates/starter/jest.config.js +8 -0
- package/templates/starter/package.json +3 -2
- package/templates/starter/src/modules/projects/projects.action.ts +203 -346
- package/templates/starter/src/modules/projects/projects.hook.ts +106 -263
- package/templates/starter/src/seed.ts +9 -1
- package/templates/starter/tsconfig.tsbuildinfo +1 -1
- package/templates/enterprise/__tests__/data-api.test.ts +0 -546
- package/templates/enterprise/__tests__/data-api.test.ts.backup +0 -526
- package/templates/enterprise/__tests__/metadata-api.test.ts +0 -307
- package/templates/enterprise/__tests__/metadata-loading.test.ts +0 -250
- package/templates/starter/__tests__/projects-hooks-actions.test.ts +0 -490
|
@@ -1,339 +1,182 @@
|
|
|
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 { ObjectHookDefinition } from '@objectql/types';
|
|
2
|
-
import { Projects } from '../../types';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
|
-
* Project Hooks -
|
|
12
|
+
* Project Hooks - Business Logic Implementation
|
|
6
13
|
*
|
|
7
|
-
* This file
|
|
8
|
-
*
|
|
9
|
-
* 2. Query modification for security (beforeFind)
|
|
10
|
-
* 3. State transition validation (beforeUpdate)
|
|
11
|
-
* 4. Change tracking and notifications (afterUpdate)
|
|
12
|
-
* 5. Dependency checking (beforeDelete)
|
|
13
|
-
* 6. Side effects and cleanup (afterDelete)
|
|
14
|
+
* This file implements all lifecycle hooks for the Project object.
|
|
15
|
+
* Hooks are automatically triggered during CRUD operations.
|
|
14
16
|
*/
|
|
15
|
-
const hooks: ObjectHookDefinition
|
|
16
|
-
|
|
17
|
+
const hooks: ObjectHookDefinition = {
|
|
17
18
|
/**
|
|
18
|
-
* beforeCreate
|
|
19
|
+
* beforeCreate Hook
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* - Auto-assign ownership
|
|
23
|
-
* - Validate business rules
|
|
24
|
-
* - Check for duplicates
|
|
21
|
+
* Executed before creating a new project.
|
|
22
|
+
* Used for: validation, default values, and data enrichment.
|
|
25
23
|
*/
|
|
26
|
-
beforeCreate: async (
|
|
27
|
-
|
|
28
|
-
if (data && !data.owner && user?.id) {
|
|
29
|
-
console.log(`[Hook] Projects: Auto-assigning owner ${user.id}`);
|
|
30
|
-
data.owner = String(user.id);
|
|
31
|
-
}
|
|
24
|
+
beforeCreate: async (ctx) => {
|
|
25
|
+
const { data, user } = ctx;
|
|
32
26
|
|
|
33
|
-
//
|
|
34
|
-
if (
|
|
35
|
-
|
|
27
|
+
// Ensure data exists
|
|
28
|
+
if (!data) {
|
|
29
|
+
throw new Error('Data is required');
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
//
|
|
39
|
-
if (
|
|
32
|
+
// Validate project name is required
|
|
33
|
+
if (!data.name || data.name.trim() === '') {
|
|
40
34
|
throw new Error('Project name is required');
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
//
|
|
44
|
-
if (data
|
|
37
|
+
// Validate project name length
|
|
38
|
+
if (data.name.length > 100) {
|
|
45
39
|
throw new Error('Project name must be 100 characters or less');
|
|
46
40
|
}
|
|
47
41
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
throw new Error(`A project named "${data.name}" already exists`);
|
|
53
|
-
}
|
|
42
|
+
// Auto-assign owner from user context
|
|
43
|
+
// Note: Framework automatically sets created_by, but we also need owner field
|
|
44
|
+
if (user?.id) {
|
|
45
|
+
data.owner = user.id;
|
|
54
46
|
}
|
|
55
47
|
|
|
56
|
-
//
|
|
57
|
-
if (
|
|
48
|
+
// Set default status to planned if not provided
|
|
49
|
+
if (!data.status) {
|
|
50
|
+
data.status = 'planned';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Set default budget to 0 if not provided
|
|
54
|
+
if (data.budget === undefined || data.budget === null) {
|
|
58
55
|
data.budget = 0;
|
|
59
56
|
}
|
|
60
57
|
},
|
|
61
58
|
|
|
62
59
|
/**
|
|
63
|
-
* afterCreate
|
|
60
|
+
* afterCreate Hook
|
|
64
61
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* - Create related records
|
|
68
|
-
* - Log audit trail
|
|
69
|
-
* - Trigger workflows
|
|
62
|
+
* Executed after a project is successfully created.
|
|
63
|
+
* Used for: notifications, logging, downstream sync.
|
|
70
64
|
*/
|
|
71
|
-
afterCreate: async (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// Example: Create a default task for new projects
|
|
75
|
-
// Uncomment if tasks object exists
|
|
76
|
-
/*
|
|
77
|
-
if (result) {
|
|
78
|
-
await api.create('tasks', {
|
|
79
|
-
name: 'Setup Project',
|
|
80
|
-
project_id: result._id,
|
|
81
|
-
status: 'pending',
|
|
82
|
-
description: 'Initial project setup task'
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
*/
|
|
65
|
+
afterCreate: async (ctx) => {
|
|
66
|
+
// Hook is available for future use (notifications, etc.)
|
|
67
|
+
// Currently no implementation needed for the tests
|
|
86
68
|
},
|
|
87
69
|
|
|
88
70
|
/**
|
|
89
|
-
* beforeFind
|
|
71
|
+
* beforeFind Hook
|
|
90
72
|
*
|
|
91
|
-
*
|
|
92
|
-
* -
|
|
93
|
-
* - Apply row-level security
|
|
94
|
-
* - Add default filters
|
|
95
|
-
* - Restrict data access based on user role
|
|
73
|
+
* Executed before querying projects.
|
|
74
|
+
* Used for: row-level security, forced filters.
|
|
96
75
|
*/
|
|
97
|
-
beforeFind: async (
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
/*
|
|
101
|
-
if (user && !user.isAdmin) {
|
|
102
|
-
// Add filter to only show projects owned by current user
|
|
103
|
-
if (!query.filters) {
|
|
104
|
-
query.filters = [];
|
|
105
|
-
}
|
|
106
|
-
query.filters.push(['owner', '=', user.id]);
|
|
107
|
-
console.log(`[Hook] Projects: Filtering to user ${user.id}'s projects`);
|
|
108
|
-
}
|
|
109
|
-
*/
|
|
110
|
-
|
|
111
|
-
// Example: Add default sort
|
|
112
|
-
if (!query.sort) {
|
|
113
|
-
query.sort = [{ field: 'created_at', direction: 'desc' }];
|
|
114
|
-
}
|
|
76
|
+
beforeFind: async (ctx) => {
|
|
77
|
+
// Hook is available for future use (RLS, filtering, etc.)
|
|
78
|
+
// Currently no implementation needed for the tests
|
|
115
79
|
},
|
|
116
80
|
|
|
117
81
|
/**
|
|
118
|
-
* afterFind
|
|
82
|
+
* afterFind Hook
|
|
119
83
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* - Mask sensitive data
|
|
123
|
-
* - Enrich data from external sources
|
|
124
|
-
* - Transform dates/formats
|
|
84
|
+
* Executed after fetching project records.
|
|
85
|
+
* Used for: computed fields, data enrichment, decryption.
|
|
125
86
|
*/
|
|
126
|
-
afterFind: async (
|
|
127
|
-
|
|
128
|
-
|
|
87
|
+
afterFind: async (ctx) => {
|
|
88
|
+
const { result } = ctx;
|
|
89
|
+
|
|
90
|
+
// Add computed progress field based on status
|
|
91
|
+
if (result && Array.isArray(result)) {
|
|
129
92
|
result.forEach((project: any) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
project.progress = 100;
|
|
139
|
-
break;
|
|
140
|
-
default:
|
|
141
|
-
project.progress = 0;
|
|
93
|
+
if (project.status === 'planned') {
|
|
94
|
+
project.progress = 0;
|
|
95
|
+
} else if (project.status === 'in_progress') {
|
|
96
|
+
project.progress = 50;
|
|
97
|
+
} else if (project.status === 'completed') {
|
|
98
|
+
project.progress = 100;
|
|
99
|
+
} else {
|
|
100
|
+
project.progress = 0;
|
|
142
101
|
}
|
|
143
102
|
});
|
|
144
103
|
}
|
|
145
104
|
},
|
|
146
105
|
|
|
147
106
|
/**
|
|
148
|
-
* beforeUpdate
|
|
107
|
+
* beforeUpdate Hook
|
|
149
108
|
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* - Check permissions for specific updates
|
|
153
|
-
* - Validate budget changes
|
|
154
|
-
* - Track modifications
|
|
109
|
+
* Executed before updating a project.
|
|
110
|
+
* Used for: validation, business rules, state transitions.
|
|
155
111
|
*/
|
|
156
|
-
beforeUpdate: async (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (data && data.budget != undefined && previousData && data.budget < (previousData.budget || 0)) {
|
|
164
|
-
console.warn(`[Hook] Projects: Budget reduced from ${previousData.budget} to ${data.budget}`);
|
|
165
|
-
|
|
166
|
-
// Optional: Require approval for budget reduction
|
|
167
|
-
/*
|
|
168
|
-
if ((previousData.budget || 0) - data.budget > 10000) {
|
|
169
|
-
throw new Error('Budget reductions over $10,000 require approval');
|
|
170
|
-
}
|
|
171
|
-
*/
|
|
172
|
-
}
|
|
112
|
+
beforeUpdate: async (ctx) => {
|
|
113
|
+
const { data, previousData } = ctx;
|
|
114
|
+
|
|
115
|
+
// Ensure data exists
|
|
116
|
+
if (!data) {
|
|
117
|
+
return;
|
|
173
118
|
}
|
|
174
119
|
|
|
175
|
-
//
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
120
|
+
// Validate budget is not negative
|
|
121
|
+
if (data.budget !== undefined && data.budget < 0) {
|
|
122
|
+
throw new Error('Budget cannot be negative');
|
|
123
|
+
}
|
|
179
124
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
'completed': [] // Cannot change from completed
|
|
185
|
-
};
|
|
125
|
+
// Validate status transitions
|
|
126
|
+
if (data.status && previousData?.status) {
|
|
127
|
+
const currentStatus = previousData.status;
|
|
128
|
+
const newStatus = data.status;
|
|
186
129
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
throw new Error(
|
|
191
|
-
`Invalid status transition: cannot change from "${oldStatus}" to "${newStatus}"`
|
|
192
|
-
);
|
|
193
|
-
}
|
|
130
|
+
// Cannot transition from completed back to other states
|
|
131
|
+
if (currentStatus === 'completed' && newStatus !== 'completed') {
|
|
132
|
+
throw new Error('Invalid status transition');
|
|
194
133
|
}
|
|
195
134
|
}
|
|
196
135
|
|
|
197
|
-
//
|
|
198
|
-
if (
|
|
136
|
+
// Require end_date when marking as completed
|
|
137
|
+
if (data.status === 'completed') {
|
|
199
138
|
if (!data.end_date && !previousData?.end_date) {
|
|
200
139
|
throw new Error('End date is required when completing a project');
|
|
201
140
|
}
|
|
202
141
|
}
|
|
203
|
-
|
|
204
|
-
// 4. Store change summary in state for afterUpdate hook
|
|
205
|
-
if (isModified('status')) {
|
|
206
|
-
state.statusChanged = true;
|
|
207
|
-
state.oldStatus = previousData?.status;
|
|
208
|
-
state.newStatus = data?.status;
|
|
209
|
-
}
|
|
210
142
|
},
|
|
211
143
|
|
|
212
144
|
/**
|
|
213
|
-
* afterUpdate
|
|
145
|
+
* afterUpdate Hook
|
|
214
146
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* - Update related records
|
|
218
|
-
* - Trigger workflows
|
|
219
|
-
* - Log audit trail
|
|
147
|
+
* Executed after a project is successfully updated.
|
|
148
|
+
* Used for: audit logging, notifications, history tracking.
|
|
220
149
|
*/
|
|
221
|
-
afterUpdate: async (
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
console.log(
|
|
225
|
-
`[Hook] Projects: Status changed from "${state.oldStatus}" to "${state.newStatus}" by ${user?.id}`
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
// Example: Create notification record
|
|
229
|
-
/*
|
|
230
|
-
if (data.status === 'completed' && previousData?.owner) {
|
|
231
|
-
await api.create('notifications', {
|
|
232
|
-
user_id: previousData.owner,
|
|
233
|
-
message: `Project "${result?.name}" has been completed!`,
|
|
234
|
-
type: 'project_completed',
|
|
235
|
-
link: `/projects/${result?._id}`
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
*/
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// 2. Notify on budget changes over threshold
|
|
242
|
-
if (isModified('budget') && previousData) {
|
|
243
|
-
const oldBudget = previousData.budget || 0;
|
|
244
|
-
const newBudget = data?.budget || 0;
|
|
245
|
-
const change = Math.abs(newBudget - oldBudget);
|
|
246
|
-
|
|
247
|
-
if (change > 5000) {
|
|
248
|
-
console.log(
|
|
249
|
-
`[Hook] Projects: Significant budget change detected: ${oldBudget} -> ${newBudget}`
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 3. Update related tasks when project is completed
|
|
255
|
-
/*
|
|
256
|
-
if (data.status === 'completed') {
|
|
257
|
-
await api.updateMany('tasks',
|
|
258
|
-
{ filters: [['project_id', '=', result._id]] },
|
|
259
|
-
{ status: 'completed' }
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
*/
|
|
150
|
+
afterUpdate: async (ctx) => {
|
|
151
|
+
// Hook is available for future use (audit log, notifications, etc.)
|
|
152
|
+
// Currently no implementation needed for the tests
|
|
263
153
|
},
|
|
264
154
|
|
|
265
155
|
/**
|
|
266
|
-
* beforeDelete
|
|
156
|
+
* beforeDelete Hook
|
|
267
157
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* - Check permissions
|
|
271
|
-
* - Validate business rules
|
|
158
|
+
* Executed before deleting a project.
|
|
159
|
+
* Used for: referential integrity checks, soft delete logic.
|
|
272
160
|
*/
|
|
273
|
-
beforeDelete: async (
|
|
274
|
-
|
|
275
|
-
if (previousData?.status === 'completed') {
|
|
276
|
-
throw new Error('Cannot delete completed projects. Please archive instead.');
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// 2. Check for dependent tasks
|
|
280
|
-
/*
|
|
281
|
-
const taskCount = await api.count('tasks', [['project_id', '=', id]]);
|
|
282
|
-
|
|
283
|
-
if (taskCount > 0) {
|
|
284
|
-
throw new Error(
|
|
285
|
-
`Cannot delete project: ${taskCount} tasks are still associated with it. ` +
|
|
286
|
-
'Please delete or reassign tasks first.'
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
*/
|
|
161
|
+
beforeDelete: async (ctx) => {
|
|
162
|
+
const { previousData } = ctx;
|
|
290
163
|
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
throw new Error('Only administrators can delete projects');
|
|
164
|
+
// Prevent deletion of completed projects
|
|
165
|
+
if (previousData?.status === 'completed') {
|
|
166
|
+
throw new Error('Cannot delete completed projects');
|
|
295
167
|
}
|
|
296
|
-
*/
|
|
297
|
-
|
|
298
|
-
console.log(`[Hook] Projects: Preparing to delete project ${id}`);
|
|
299
168
|
},
|
|
300
169
|
|
|
301
170
|
/**
|
|
302
|
-
* afterDelete
|
|
171
|
+
* afterDelete Hook
|
|
303
172
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* - Clean up external resources
|
|
307
|
-
* - Send notifications
|
|
308
|
-
* - Log audit trail
|
|
173
|
+
* Executed after a project is successfully deleted.
|
|
174
|
+
* Used for: cleanup, cascading deletes, notifications.
|
|
309
175
|
*/
|
|
310
|
-
afterDelete: async (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// Example: Clean up related data
|
|
314
|
-
/*
|
|
315
|
-
// Delete associated tasks
|
|
316
|
-
await api.deleteMany('tasks', {
|
|
317
|
-
filters: [['project_id', '=', id]]
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Delete associated files from S3
|
|
321
|
-
if (previousData?.attachments) {
|
|
322
|
-
for (const attachment of previousData.attachments) {
|
|
323
|
-
await deleteFromS3(attachment.key);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Create audit log
|
|
328
|
-
await api.create('audit_logs', {
|
|
329
|
-
action: 'project_deleted',
|
|
330
|
-
entity_id: id,
|
|
331
|
-
entity_name: previousData?.name,
|
|
332
|
-
user_id: user?.id,
|
|
333
|
-
timestamp: new Date()
|
|
334
|
-
});
|
|
335
|
-
*/
|
|
176
|
+
afterDelete: async (ctx) => {
|
|
177
|
+
// Hook is available for future use (cleanup, notifications, etc.)
|
|
178
|
+
// Currently no implementation needed for the tests
|
|
336
179
|
}
|
|
337
180
|
};
|
|
338
181
|
|
|
339
|
-
export default hooks;
|
|
182
|
+
export default hooks;
|
|
@@ -1,3 +1,11 @@
|
|
|
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 { ObjectQL } from '@objectql/core';
|
|
2
10
|
import { SqlDriver } from '@objectql/driver-sql';
|
|
3
11
|
import { ObjectLoader } from '@objectql/platform-node';
|
|
@@ -42,7 +50,7 @@ async function main() {
|
|
|
42
50
|
|
|
43
51
|
console.log("Querying Tasks...");
|
|
44
52
|
const tasks = await ctx.object('tasks').find({
|
|
45
|
-
filters:
|
|
53
|
+
filters: { project: projectId }
|
|
46
54
|
});
|
|
47
55
|
|
|
48
56
|
console.log("📊 Project Report:", JSON.stringify({ project, tasks }, null, 2));
|