@critical-path/core 0.24.0 → 0.25.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/CHANGELOG.md +29 -0
- package/README.md +3 -3
- package/dist/engine/actor.test.js +1 -0
- package/dist/engine/actor.test.js.map +1 -1
- package/dist/engine/index.d.ts +12 -17
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +18 -38
- package/dist/engine/index.js.map +1 -1
- package/dist/index.test.js +1 -5
- package/dist/index.test.js.map +1 -1
- package/dist/schemas/index.d.ts +87 -124
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +36 -49
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/schemas.test.js +10 -11
- package/dist/schemas/schemas.test.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/actor.test.ts +2 -1
- package/src/engine/index.ts +24 -52
- package/src/index.test.ts +1 -5
- package/src/schemas/index.ts +62 -50
- package/src/schemas/schemas.test.ts +16 -15
package/src/schemas/index.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Exposed from `@critical-path/core/schemas` (not the package root) so browser bundles of core do
|
|
5
5
|
* not pull in zod. Create schemas omit server-assigned fields (`id`, `createdAt`, `updatedAt`, task
|
|
6
6
|
* `key`); update schemas additionally make every field optional and omit the owning `projectId`.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Schemas are strict: unknown or server-assigned keys are rejected with a 400 rather than ignored.
|
|
8
|
+
* Identity (authors, reactors, uploaders, time-entry users) is never taken from payloads; it comes
|
|
9
|
+
* from the caller resolved by the server (see `CriticalPathEngine.withActor`).
|
|
9
10
|
*/
|
|
10
11
|
import { z } from 'zod';
|
|
11
12
|
import { ValidationError, type ValidationIssue } from '../domain/errors.js';
|
|
@@ -25,36 +26,41 @@ export function parsePayload<S extends z.ZodTypeAny>(schema: S, data: unknown):
|
|
|
25
26
|
throw new ValidationError(`Invalid request body: ${summary}`, issues);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
/** Objects reject unknown keys so typos and protected fields surface as 400s. */
|
|
30
|
+
function strictObject<T extends z.ZodRawShape>(shape: T) {
|
|
31
|
+
return z.object(shape).strict();
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
const isoString = z.string();
|
|
29
35
|
const nonEmpty = z.string().min(1);
|
|
30
36
|
|
|
31
37
|
export const SemanticStatusSchema = z.enum(['not_started', 'in_progress', 'completed', 'canceled']);
|
|
32
38
|
export const AuthorTypeSchema = z.enum(['user', 'agent', 'system']);
|
|
33
39
|
|
|
34
|
-
export const StatusDefinitionSchema =
|
|
40
|
+
export const StatusDefinitionSchema = strictObject({
|
|
35
41
|
key: nonEmpty,
|
|
36
42
|
label: z.string(),
|
|
37
43
|
category: SemanticStatusSchema
|
|
38
44
|
});
|
|
39
45
|
|
|
40
|
-
export const WorkScheduleSchema =
|
|
46
|
+
export const WorkScheduleSchema = strictObject({
|
|
41
47
|
id: z.string().optional(),
|
|
42
48
|
name: z.string().optional(),
|
|
43
49
|
timezone: z.string().optional(),
|
|
44
50
|
defaultHoursPerDay: z.number().nonnegative().optional(),
|
|
45
51
|
days: z.array(
|
|
46
|
-
|
|
52
|
+
strictObject({
|
|
47
53
|
dayOfWeek: z.number().int().min(0).max(6),
|
|
48
54
|
isWorkingDay: z.boolean(),
|
|
49
|
-
hours: z.array(
|
|
55
|
+
hours: z.array(strictObject({ start: z.string(), end: z.string() })).optional()
|
|
50
56
|
})
|
|
51
57
|
),
|
|
52
58
|
holidays: z
|
|
53
|
-
.array(
|
|
59
|
+
.array(strictObject({ date: z.string(), name: z.string().optional(), halfDay: z.boolean().optional() }))
|
|
54
60
|
.optional()
|
|
55
61
|
});
|
|
56
62
|
|
|
57
|
-
export const CustomFieldDefinitionSchema =
|
|
63
|
+
export const CustomFieldDefinitionSchema = strictObject({
|
|
58
64
|
id: z.string(),
|
|
59
65
|
key: nonEmpty,
|
|
60
66
|
label: z.string(),
|
|
@@ -64,7 +70,7 @@ export const CustomFieldDefinitionSchema = z.object({
|
|
|
64
70
|
defaultValue: z.unknown().optional()
|
|
65
71
|
});
|
|
66
72
|
|
|
67
|
-
export const TaskTypeDefinitionSchema =
|
|
73
|
+
export const TaskTypeDefinitionSchema = strictObject({
|
|
68
74
|
key: nonEmpty,
|
|
69
75
|
label: z.string(),
|
|
70
76
|
description: z.string().optional(),
|
|
@@ -78,13 +84,13 @@ const metadata = z.record(z.unknown());
|
|
|
78
84
|
|
|
79
85
|
// --- Workflows ---
|
|
80
86
|
|
|
81
|
-
export const CreateWorkflowSchema =
|
|
87
|
+
export const CreateWorkflowSchema = strictObject({
|
|
82
88
|
name: nonEmpty,
|
|
83
89
|
description: z.string().optional(),
|
|
84
90
|
statuses: z.array(StatusDefinitionSchema),
|
|
85
91
|
transitions: z
|
|
86
92
|
.array(
|
|
87
|
-
|
|
93
|
+
strictObject({
|
|
88
94
|
id: z.string().optional(),
|
|
89
95
|
name: z.string().optional(),
|
|
90
96
|
fromStatusKey: z.string(),
|
|
@@ -100,7 +106,7 @@ export const UpdateWorkflowSchema = CreateWorkflowSchema.partial();
|
|
|
100
106
|
|
|
101
107
|
// --- Projects ---
|
|
102
108
|
|
|
103
|
-
export const CreateProjectSchema =
|
|
109
|
+
export const CreateProjectSchema = strictObject({
|
|
104
110
|
key: z.string().optional(),
|
|
105
111
|
name: nonEmpty,
|
|
106
112
|
description: z.string().optional(),
|
|
@@ -111,7 +117,7 @@ export const CreateProjectSchema = z.object({
|
|
|
111
117
|
taskTypes: z.array(TaskTypeDefinitionSchema).optional(),
|
|
112
118
|
statusDefinitions: z.array(StatusDefinitionSchema).optional(),
|
|
113
119
|
priorityDefinitions: z
|
|
114
|
-
.array(
|
|
120
|
+
.array(strictObject({ key: z.string(), label: z.string(), level: z.number().optional() }))
|
|
115
121
|
.optional(),
|
|
116
122
|
customFieldDefinitions: z.array(CustomFieldDefinitionSchema).optional(),
|
|
117
123
|
schedule: WorkScheduleSchema.optional(),
|
|
@@ -132,7 +138,7 @@ const taskFields = {
|
|
|
132
138
|
assigneeId: z.string().optional(),
|
|
133
139
|
assignees: z
|
|
134
140
|
.array(
|
|
135
|
-
|
|
141
|
+
strictObject({
|
|
136
142
|
id: z.string(),
|
|
137
143
|
name: z.string().optional(),
|
|
138
144
|
role: z.string().optional(),
|
|
@@ -169,7 +175,7 @@ const taskFields = {
|
|
|
169
175
|
tags: z.array(z.string()).optional(),
|
|
170
176
|
todos: z
|
|
171
177
|
.array(
|
|
172
|
-
|
|
178
|
+
strictObject({
|
|
173
179
|
id: z.string(),
|
|
174
180
|
title: z.string(),
|
|
175
181
|
completed: z.boolean(),
|
|
@@ -182,23 +188,13 @@ const taskFields = {
|
|
|
182
188
|
parentId: z.string().optional()
|
|
183
189
|
};
|
|
184
190
|
|
|
185
|
-
export const CreateTaskSchema =
|
|
191
|
+
export const CreateTaskSchema = strictObject({ projectId: nonEmpty, ...taskFields });
|
|
186
192
|
|
|
187
|
-
|
|
188
|
-
const actorClaimFields = {
|
|
189
|
-
actorId: z.string().optional(),
|
|
190
|
-
actorName: z.string().optional(),
|
|
191
|
-
actorType: z.string().optional(),
|
|
192
|
-
actor: z
|
|
193
|
-
.object({ userId: z.string(), username: z.string().optional(), actorType: z.string().optional() })
|
|
194
|
-
.optional()
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
export const UpdateTaskSchema = z.object(taskFields).partial().extend(actorClaimFields);
|
|
193
|
+
export const UpdateTaskSchema = strictObject(taskFields).partial();
|
|
198
194
|
|
|
199
195
|
// --- Dependencies ---
|
|
200
196
|
|
|
201
|
-
export const CreateDependencySchema =
|
|
197
|
+
export const CreateDependencySchema = strictObject({
|
|
202
198
|
dependsOnTaskId: nonEmpty,
|
|
203
199
|
type: z.enum(['blocking', 'blocked_by', 'relates_to']).default('blocking')
|
|
204
200
|
});
|
|
@@ -218,12 +214,12 @@ const deliverableFields = {
|
|
|
218
214
|
outputUrls: z.array(z.string()).optional(),
|
|
219
215
|
customFields: customFields.optional()
|
|
220
216
|
};
|
|
221
|
-
export const CreateDeliverableSchema =
|
|
222
|
-
export const UpdateDeliverableSchema =
|
|
217
|
+
export const CreateDeliverableSchema = strictObject({ projectId: nonEmpty, ...deliverableFields });
|
|
218
|
+
export const UpdateDeliverableSchema = strictObject(deliverableFields).partial();
|
|
223
219
|
|
|
224
220
|
// --- Teams ---
|
|
225
221
|
|
|
226
|
-
export const CreateTeamSchema =
|
|
222
|
+
export const CreateTeamSchema = strictObject({
|
|
227
223
|
name: nonEmpty,
|
|
228
224
|
description: z.string().optional(),
|
|
229
225
|
leaderId: z.string().optional(),
|
|
@@ -242,8 +238,8 @@ const containerFields = {
|
|
|
242
238
|
type: z.string().optional(),
|
|
243
239
|
color: z.string().optional()
|
|
244
240
|
};
|
|
245
|
-
export const CreateContainerSchema =
|
|
246
|
-
export const UpdateContainerSchema =
|
|
241
|
+
export const CreateContainerSchema = strictObject({ projectId: nonEmpty, ...containerFields });
|
|
242
|
+
export const UpdateContainerSchema = strictObject(containerFields).partial();
|
|
247
243
|
|
|
248
244
|
// --- Iterations ---
|
|
249
245
|
|
|
@@ -255,18 +251,15 @@ const iterationFields = {
|
|
|
255
251
|
endDate: isoString.optional(),
|
|
256
252
|
status: z.enum(['planning', 'active', 'completed']).default('planning')
|
|
257
253
|
};
|
|
258
|
-
export const CreateIterationSchema =
|
|
254
|
+
export const CreateIterationSchema = strictObject({ projectId: nonEmpty, ...iterationFields });
|
|
259
255
|
export const UpdateIterationSchema = z
|
|
260
256
|
.object({ ...iterationFields, status: z.enum(['planning', 'active', 'completed']) })
|
|
261
257
|
.partial();
|
|
262
258
|
|
|
263
259
|
// --- Comments & Reactions ---
|
|
264
260
|
|
|
265
|
-
export const CreateCommentSchema =
|
|
261
|
+
export const CreateCommentSchema = strictObject({
|
|
266
262
|
taskId: nonEmpty,
|
|
267
|
-
/** Required unless the server resolves the caller, in which case it is replaced. */
|
|
268
|
-
authorId: z.string().optional(),
|
|
269
|
-
authorType: AuthorTypeSchema.optional(),
|
|
270
263
|
parentId: z.string().optional(),
|
|
271
264
|
content: nonEmpty,
|
|
272
265
|
mentions: z.array(z.string()).optional(),
|
|
@@ -276,10 +269,8 @@ export const UpdateCommentSchema = z
|
|
|
276
269
|
.object({ content: nonEmpty, mentions: z.array(z.string()), metadata })
|
|
277
270
|
.partial();
|
|
278
271
|
|
|
279
|
-
export const CommentReactionSchema =
|
|
280
|
-
emoji: nonEmpty
|
|
281
|
-
/** Required unless the server resolves the caller, in which case it is replaced. */
|
|
282
|
-
userId: z.string().optional()
|
|
272
|
+
export const CommentReactionSchema = strictObject({
|
|
273
|
+
emoji: nonEmpty
|
|
283
274
|
});
|
|
284
275
|
|
|
285
276
|
// --- Attachments ---
|
|
@@ -288,14 +279,11 @@ const attachmentLinks = {
|
|
|
288
279
|
taskId: z.string().optional(),
|
|
289
280
|
projectId: z.string().optional(),
|
|
290
281
|
commentId: z.string().optional(),
|
|
291
|
-
/** Required unless the server resolves the caller, in which case it is replaced. */
|
|
292
|
-
uploaderId: z.string().optional(),
|
|
293
|
-
uploaderType: AuthorTypeSchema.optional(),
|
|
294
282
|
artifactType: z.enum(['plan', 'spec', 'deliverable', 'review', 'general']).optional(),
|
|
295
283
|
metadata: metadata.optional()
|
|
296
284
|
};
|
|
297
285
|
|
|
298
|
-
export const CreateAttachmentSchema =
|
|
286
|
+
export const CreateAttachmentSchema = strictObject({
|
|
299
287
|
...attachmentLinks,
|
|
300
288
|
filename: nonEmpty,
|
|
301
289
|
// Linked (rather than uploaded) files often have no known type or size.
|
|
@@ -305,7 +293,7 @@ export const CreateAttachmentSchema = z.object({
|
|
|
305
293
|
storageKey: z.string().optional()
|
|
306
294
|
});
|
|
307
295
|
|
|
308
|
-
export const UploadAttachmentSchema =
|
|
296
|
+
export const UploadAttachmentSchema = strictObject({
|
|
309
297
|
...attachmentLinks,
|
|
310
298
|
filename: nonEmpty,
|
|
311
299
|
data: z.string(),
|
|
@@ -314,7 +302,7 @@ export const UploadAttachmentSchema = z.object({
|
|
|
314
302
|
encoding: z.enum(['base64', 'utf-8', 'binary']).optional()
|
|
315
303
|
});
|
|
316
304
|
|
|
317
|
-
export const PresignAttachmentSchema =
|
|
305
|
+
export const PresignAttachmentSchema = strictObject({
|
|
318
306
|
storageKey: nonEmpty,
|
|
319
307
|
expiresInSeconds: z.number().int().positive().optional(),
|
|
320
308
|
contentType: z.string().optional()
|
|
@@ -322,12 +310,11 @@ export const PresignAttachmentSchema = z.object({
|
|
|
322
310
|
|
|
323
311
|
// --- Time Tracking ---
|
|
324
312
|
|
|
325
|
-
export const LogTimeSchema =
|
|
313
|
+
export const LogTimeSchema = strictObject({
|
|
326
314
|
taskId: nonEmpty,
|
|
327
315
|
hours: z.number().positive(),
|
|
328
316
|
isBillable: z.boolean().optional(),
|
|
329
317
|
description: z.string().optional(),
|
|
330
|
-
userId: z.string().optional(),
|
|
331
318
|
loggedAt: isoString.optional()
|
|
332
319
|
});
|
|
333
320
|
|
|
@@ -335,3 +322,28 @@ export type CreateTaskPayload = z.infer<typeof CreateTaskSchema>;
|
|
|
335
322
|
export type UpdateTaskPayload = z.infer<typeof UpdateTaskSchema>;
|
|
336
323
|
export type CreateProjectPayload = z.infer<typeof CreateProjectSchema>;
|
|
337
324
|
export type UpdateProjectPayload = z.infer<typeof UpdateProjectSchema>;
|
|
325
|
+
|
|
326
|
+
/** Request body types (before defaults are applied), for typed API clients. */
|
|
327
|
+
export type CreateWorkflowBody = z.input<typeof CreateWorkflowSchema>;
|
|
328
|
+
export type UpdateWorkflowBody = z.input<typeof UpdateWorkflowSchema>;
|
|
329
|
+
export type CreateProjectBody = z.input<typeof CreateProjectSchema>;
|
|
330
|
+
export type UpdateProjectBody = z.input<typeof UpdateProjectSchema>;
|
|
331
|
+
export type CreateTaskBody = z.input<typeof CreateTaskSchema>;
|
|
332
|
+
export type UpdateTaskBody = z.input<typeof UpdateTaskSchema>;
|
|
333
|
+
export type CreateDependencyBody = z.input<typeof CreateDependencySchema>;
|
|
334
|
+
export type CreateDeliverableBody = z.input<typeof CreateDeliverableSchema>;
|
|
335
|
+
export type UpdateDeliverableBody = z.input<typeof UpdateDeliverableSchema>;
|
|
336
|
+
export type CreateTeamBody = z.input<typeof CreateTeamSchema>;
|
|
337
|
+
export type UpdateTeamBody = z.input<typeof UpdateTeamSchema>;
|
|
338
|
+
export type CreateContainerBody = z.input<typeof CreateContainerSchema>;
|
|
339
|
+
export type UpdateContainerBody = z.input<typeof UpdateContainerSchema>;
|
|
340
|
+
export type CreateIterationBody = z.input<typeof CreateIterationSchema>;
|
|
341
|
+
export type UpdateIterationBody = z.input<typeof UpdateIterationSchema>;
|
|
342
|
+
export type CreateCommentBody = z.input<typeof CreateCommentSchema>;
|
|
343
|
+
export type UpdateCommentBody = z.input<typeof UpdateCommentSchema>;
|
|
344
|
+
export type CommentReactionBody = z.input<typeof CommentReactionSchema>;
|
|
345
|
+
export type CreateAttachmentBody = z.input<typeof CreateAttachmentSchema>;
|
|
346
|
+
export type UploadAttachmentBody = z.input<typeof UploadAttachmentSchema>;
|
|
347
|
+
export type PresignAttachmentBody = z.input<typeof PresignAttachmentSchema>;
|
|
348
|
+
export type LogTimeBody = z.input<typeof LogTimeSchema>;
|
|
349
|
+
|
|
@@ -47,11 +47,13 @@ type SameKeys<Schema, Expected> = [Exclude<keyof Schema, keyof Expected>, Exclud
|
|
|
47
47
|
|
|
48
48
|
type Infer<S extends z.ZodTypeAny> = z.infer<S>;
|
|
49
49
|
type ServerAssigned = 'id' | 'createdAt' | 'updatedAt';
|
|
50
|
-
|
|
50
|
+
// Identity is resolved by the server (withActor), never accepted in payloads.
|
|
51
|
+
type CommentIdentity = 'authorId' | 'authorType';
|
|
52
|
+
type UploaderIdentity = 'uploaderId' | 'uploaderType';
|
|
51
53
|
|
|
52
54
|
const keyChecks: true[] = [
|
|
53
55
|
true as SameKeys<Infer<typeof CreateTaskSchema>, Omit<CreateTaskInput, 'key'>>,
|
|
54
|
-
true as SameKeys<
|
|
56
|
+
true as SameKeys<Infer<typeof UpdateTaskSchema>, Omit<Task, ServerAssigned | 'key' | 'projectId'>>,
|
|
55
57
|
// `workflow` is a denormalised copy resolved from `workflowId`, so it is not writable.
|
|
56
58
|
true as SameKeys<Infer<typeof CreateProjectSchema>, Omit<CreateProjectInput, 'workflow'>>,
|
|
57
59
|
true as SameKeys<Infer<typeof UpdateProjectSchema>, Omit<Project, ServerAssigned | 'workflow'>>,
|
|
@@ -63,10 +65,10 @@ const keyChecks: true[] = [
|
|
|
63
65
|
true as SameKeys<Infer<typeof UpdateContainerSchema>, Omit<TaskContainer, ServerAssigned | 'projectId'>>,
|
|
64
66
|
true as SameKeys<Infer<typeof CreateIterationSchema>, Omit<Iteration, 'id' | 'createdAt'>>,
|
|
65
67
|
// Reactions are managed through the reactions endpoints.
|
|
66
|
-
true as SameKeys<Infer<typeof CreateCommentSchema>, Omit<Comment, ServerAssigned | 'reactions'>>,
|
|
68
|
+
true as SameKeys<Infer<typeof CreateCommentSchema>, Omit<Comment, ServerAssigned | 'reactions' | CommentIdentity>>,
|
|
67
69
|
true as SameKeys<Infer<typeof UpdateCommentSchema>, Pick<Comment, 'content' | 'mentions' | 'metadata'>>,
|
|
68
|
-
true as SameKeys<Infer<typeof CreateAttachmentSchema>, CreateAttachmentInput
|
|
69
|
-
true as SameKeys<Infer<typeof LogTimeSchema>, Omit<TimeEntry, 'id'>>
|
|
70
|
+
true as SameKeys<Infer<typeof CreateAttachmentSchema>, Omit<CreateAttachmentInput, UploaderIdentity>>,
|
|
71
|
+
true as SameKeys<Infer<typeof LogTimeSchema>, Omit<TimeEntry, 'id' | 'userId'>>
|
|
70
72
|
];
|
|
71
73
|
|
|
72
74
|
describe('request schemas', () => {
|
|
@@ -74,16 +76,15 @@ describe('request schemas', () => {
|
|
|
74
76
|
expect(keyChecks.every(Boolean)).toBe(true);
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
it('
|
|
78
|
-
const
|
|
79
|
-
title: 'ok',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
expect(parsed).toEqual({ title: 'ok' });
|
|
79
|
+
it('reject server-assigned, identity and unknown fields', () => {
|
|
80
|
+
for (const extra of [{ id: 'x' }, { projectId: 'other' }, { createdAt: '1999-01-01' }, { key: 'X-1' }, { junk: true }, { actorId: 'ceo' }]) {
|
|
81
|
+
const result = UpdateTaskSchema.safeParse({ title: 'ok', ...extra });
|
|
82
|
+
expect(result.success, JSON.stringify(extra)).toBe(false);
|
|
83
|
+
}
|
|
84
|
+
expect(CreateCommentSchema.safeParse({ taskId: 't1', content: 'hi', authorId: 'ceo' }).success).toBe(false);
|
|
85
|
+
// Nested objects are strict too
|
|
86
|
+
expect(UpdateTaskSchema.safeParse({ todos: [{ id: '1', title: 'a', completed: false, extra: 1 }] }).success).toBe(false);
|
|
87
|
+
expect(UpdateTaskSchema.parse({ title: 'ok' })).toEqual({ title: 'ok' });
|
|
87
88
|
});
|
|
88
89
|
|
|
89
90
|
it('reject wrong types and out-of-range values', () => {
|