@critical-path/core 0.23.0 → 0.24.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 +21 -0
- package/README.md +11 -0
- package/dist/domain/errors.d.ts +7 -1
- package/dist/domain/errors.d.ts.map +1 -1
- package/dist/domain/errors.js +3 -4
- package/dist/domain/errors.js.map +1 -1
- package/dist/schemas/index.d.ts +1941 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +289 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/schemas.test.d.ts +2 -0
- package/dist/schemas/schemas.test.d.ts.map +1 -0
- package/dist/schemas/schemas.test.js +58 -0
- package/dist/schemas/schemas.test.js.map +1 -0
- package/package.json +10 -3
- package/src/domain/errors.ts +10 -1
- package/src/schemas/index.ts +337 -0
- package/src/schemas/schemas.test.ts +111 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request payload schemas for Critical Path resources.
|
|
3
|
+
*
|
|
4
|
+
* Exposed from `@critical-path/core/schemas` (not the package root) so browser bundles of core do
|
|
5
|
+
* not pull in zod. Create schemas omit server-assigned fields (`id`, `createdAt`, `updatedAt`, task
|
|
6
|
+
* `key`); update schemas additionally make every field optional and omit the owning `projectId`.
|
|
7
|
+
* Unknown keys are stripped rather than rejected, so older clients that send whole objects keep
|
|
8
|
+
* working while protected fields are never written.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { ValidationError, type ValidationIssue } from '../domain/errors.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parses `data` with `schema`, returning the cleaned value (unknown keys stripped, defaults
|
|
15
|
+
* applied) or throwing a `ValidationError` whose `issues` list every problem.
|
|
16
|
+
*/
|
|
17
|
+
export function parsePayload<S extends z.ZodTypeAny>(schema: S, data: unknown): z.output<S> {
|
|
18
|
+
const result = schema.safeParse(data ?? {});
|
|
19
|
+
if (result.success) return result.data;
|
|
20
|
+
const issues: ValidationIssue[] = result.error.issues.map((issue) => ({
|
|
21
|
+
path: issue.path.join('.'),
|
|
22
|
+
message: issue.message
|
|
23
|
+
}));
|
|
24
|
+
const summary = issues.map((i) => (i.path ? `${i.path}: ${i.message}` : i.message)).join('; ');
|
|
25
|
+
throw new ValidationError(`Invalid request body: ${summary}`, issues);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const isoString = z.string();
|
|
29
|
+
const nonEmpty = z.string().min(1);
|
|
30
|
+
|
|
31
|
+
export const SemanticStatusSchema = z.enum(['not_started', 'in_progress', 'completed', 'canceled']);
|
|
32
|
+
export const AuthorTypeSchema = z.enum(['user', 'agent', 'system']);
|
|
33
|
+
|
|
34
|
+
export const StatusDefinitionSchema = z.object({
|
|
35
|
+
key: nonEmpty,
|
|
36
|
+
label: z.string(),
|
|
37
|
+
category: SemanticStatusSchema
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const WorkScheduleSchema = z.object({
|
|
41
|
+
id: z.string().optional(),
|
|
42
|
+
name: z.string().optional(),
|
|
43
|
+
timezone: z.string().optional(),
|
|
44
|
+
defaultHoursPerDay: z.number().nonnegative().optional(),
|
|
45
|
+
days: z.array(
|
|
46
|
+
z.object({
|
|
47
|
+
dayOfWeek: z.number().int().min(0).max(6),
|
|
48
|
+
isWorkingDay: z.boolean(),
|
|
49
|
+
hours: z.array(z.object({ start: z.string(), end: z.string() })).optional()
|
|
50
|
+
})
|
|
51
|
+
),
|
|
52
|
+
holidays: z
|
|
53
|
+
.array(z.object({ date: z.string(), name: z.string().optional(), halfDay: z.boolean().optional() }))
|
|
54
|
+
.optional()
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const CustomFieldDefinitionSchema = z.object({
|
|
58
|
+
id: z.string(),
|
|
59
|
+
key: nonEmpty,
|
|
60
|
+
label: z.string(),
|
|
61
|
+
type: z.enum(['text', 'number', 'date', 'boolean', 'single_select', 'multi_select', 'user']),
|
|
62
|
+
options: z.array(z.string()).optional(),
|
|
63
|
+
required: z.boolean().optional(),
|
|
64
|
+
defaultValue: z.unknown().optional()
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const TaskTypeDefinitionSchema = z.object({
|
|
68
|
+
key: nonEmpty,
|
|
69
|
+
label: z.string(),
|
|
70
|
+
description: z.string().optional(),
|
|
71
|
+
icon: z.string().optional(),
|
|
72
|
+
defaultStatusKey: z.string().optional(),
|
|
73
|
+
workflowId: z.string().optional()
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const customFields = z.record(z.unknown());
|
|
77
|
+
const metadata = z.record(z.unknown());
|
|
78
|
+
|
|
79
|
+
// --- Workflows ---
|
|
80
|
+
|
|
81
|
+
export const CreateWorkflowSchema = z.object({
|
|
82
|
+
name: nonEmpty,
|
|
83
|
+
description: z.string().optional(),
|
|
84
|
+
statuses: z.array(StatusDefinitionSchema),
|
|
85
|
+
transitions: z
|
|
86
|
+
.array(
|
|
87
|
+
z.object({
|
|
88
|
+
id: z.string().optional(),
|
|
89
|
+
name: z.string().optional(),
|
|
90
|
+
fromStatusKey: z.string(),
|
|
91
|
+
toStatusKey: z.string()
|
|
92
|
+
})
|
|
93
|
+
)
|
|
94
|
+
.default([]),
|
|
95
|
+
taskTypes: z.array(TaskTypeDefinitionSchema).optional(),
|
|
96
|
+
defaultStatusKey: z.string().optional(),
|
|
97
|
+
isDefault: z.boolean().optional()
|
|
98
|
+
});
|
|
99
|
+
export const UpdateWorkflowSchema = CreateWorkflowSchema.partial();
|
|
100
|
+
|
|
101
|
+
// --- Projects ---
|
|
102
|
+
|
|
103
|
+
export const CreateProjectSchema = z.object({
|
|
104
|
+
key: z.string().optional(),
|
|
105
|
+
name: nonEmpty,
|
|
106
|
+
description: z.string().optional(),
|
|
107
|
+
ownerId: z.string().optional(),
|
|
108
|
+
members: z.array(z.string()).optional(),
|
|
109
|
+
teamIds: z.array(z.string()).optional(),
|
|
110
|
+
workflowId: z.string().optional(),
|
|
111
|
+
taskTypes: z.array(TaskTypeDefinitionSchema).optional(),
|
|
112
|
+
statusDefinitions: z.array(StatusDefinitionSchema).optional(),
|
|
113
|
+
priorityDefinitions: z
|
|
114
|
+
.array(z.object({ key: z.string(), label: z.string(), level: z.number().optional() }))
|
|
115
|
+
.optional(),
|
|
116
|
+
customFieldDefinitions: z.array(CustomFieldDefinitionSchema).optional(),
|
|
117
|
+
schedule: WorkScheduleSchema.optional(),
|
|
118
|
+
startDate: isoString.optional(),
|
|
119
|
+
targetEndDate: isoString.optional()
|
|
120
|
+
});
|
|
121
|
+
export const UpdateProjectSchema = CreateProjectSchema.partial();
|
|
122
|
+
|
|
123
|
+
// --- Tasks ---
|
|
124
|
+
|
|
125
|
+
const taskFields = {
|
|
126
|
+
title: nonEmpty,
|
|
127
|
+
description: z.string().optional(),
|
|
128
|
+
status: z.string().optional(),
|
|
129
|
+
semanticStatus: SemanticStatusSchema.optional(),
|
|
130
|
+
priority: z.string().optional(),
|
|
131
|
+
taskType: z.string().optional(),
|
|
132
|
+
assigneeId: z.string().optional(),
|
|
133
|
+
assignees: z
|
|
134
|
+
.array(
|
|
135
|
+
z.object({
|
|
136
|
+
id: z.string(),
|
|
137
|
+
name: z.string().optional(),
|
|
138
|
+
role: z.string().optional(),
|
|
139
|
+
type: z.enum(['user', 'agent', 'team']).optional(),
|
|
140
|
+
avatarUrl: z.string().optional()
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
.optional(),
|
|
144
|
+
reporterId: z.string().optional(),
|
|
145
|
+
reviewerId: z.string().optional(),
|
|
146
|
+
iterationId: z.string().optional(),
|
|
147
|
+
teamId: z.string().optional(),
|
|
148
|
+
containerId: z.string().optional(),
|
|
149
|
+
deliverableId: z.string().optional(),
|
|
150
|
+
plannedStartDate: isoString.optional(),
|
|
151
|
+
actualStartDate: isoString.optional(),
|
|
152
|
+
actualEndDate: isoString.optional(),
|
|
153
|
+
completedAt: isoString.optional(),
|
|
154
|
+
dueDate: isoString.optional(),
|
|
155
|
+
estimatedHours: z.number().nonnegative().optional(),
|
|
156
|
+
loggedHours: z.number().nonnegative().optional(),
|
|
157
|
+
actualHours: z.number().nonnegative().optional(),
|
|
158
|
+
billableHours: z.number().nonnegative().optional(),
|
|
159
|
+
estimatedDurationMinutes: z.number().nonnegative().optional(),
|
|
160
|
+
actualDurationMinutes: z.number().nonnegative().optional(),
|
|
161
|
+
billableDurationMinutes: z.number().nonnegative().optional(),
|
|
162
|
+
actualDurationSeconds: z.number().nonnegative().optional(),
|
|
163
|
+
inProgressSince: isoString.nullable().optional(),
|
|
164
|
+
blockedDurationSeconds: z.number().nonnegative().optional(),
|
|
165
|
+
blockedSince: isoString.nullable().optional(),
|
|
166
|
+
progress: z.number().min(0).max(100).optional(),
|
|
167
|
+
isBlocked: z.boolean().optional(),
|
|
168
|
+
blockedReason: z.string().nullable().optional(),
|
|
169
|
+
tags: z.array(z.string()).optional(),
|
|
170
|
+
todos: z
|
|
171
|
+
.array(
|
|
172
|
+
z.object({
|
|
173
|
+
id: z.string(),
|
|
174
|
+
title: z.string(),
|
|
175
|
+
completed: z.boolean(),
|
|
176
|
+
createdAt: isoString.optional(),
|
|
177
|
+
completedAt: isoString.optional()
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
.optional(),
|
|
181
|
+
customFields: customFields.optional(),
|
|
182
|
+
parentId: z.string().optional()
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export const CreateTaskSchema = z.object({ projectId: nonEmpty, ...taskFields });
|
|
186
|
+
|
|
187
|
+
/** Actor fields accepted on task updates; ignored when the server resolves the caller. */
|
|
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);
|
|
198
|
+
|
|
199
|
+
// --- Dependencies ---
|
|
200
|
+
|
|
201
|
+
export const CreateDependencySchema = z.object({
|
|
202
|
+
dependsOnTaskId: nonEmpty,
|
|
203
|
+
type: z.enum(['blocking', 'blocked_by', 'relates_to']).default('blocking')
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// --- Deliverables ---
|
|
207
|
+
|
|
208
|
+
const deliverableFields = {
|
|
209
|
+
title: nonEmpty,
|
|
210
|
+
description: z.string().optional(),
|
|
211
|
+
status: z.string().optional(),
|
|
212
|
+
format: z.string().optional(),
|
|
213
|
+
specs: z.record(z.unknown()).optional(),
|
|
214
|
+
leadId: z.string().optional(),
|
|
215
|
+
reviewerId: z.string().optional(),
|
|
216
|
+
dueDate: isoString.optional(),
|
|
217
|
+
deliveredAt: isoString.optional(),
|
|
218
|
+
outputUrls: z.array(z.string()).optional(),
|
|
219
|
+
customFields: customFields.optional()
|
|
220
|
+
};
|
|
221
|
+
export const CreateDeliverableSchema = z.object({ projectId: nonEmpty, ...deliverableFields });
|
|
222
|
+
export const UpdateDeliverableSchema = z.object(deliverableFields).partial();
|
|
223
|
+
|
|
224
|
+
// --- Teams ---
|
|
225
|
+
|
|
226
|
+
export const CreateTeamSchema = z.object({
|
|
227
|
+
name: nonEmpty,
|
|
228
|
+
description: z.string().optional(),
|
|
229
|
+
leaderId: z.string().optional(),
|
|
230
|
+
memberIds: z.array(z.string()).default([]),
|
|
231
|
+
weeklyCapacityHours: z.number().nonnegative().optional(),
|
|
232
|
+
schedule: WorkScheduleSchema.optional()
|
|
233
|
+
});
|
|
234
|
+
export const UpdateTeamSchema = CreateTeamSchema.partial();
|
|
235
|
+
|
|
236
|
+
// --- Containers ---
|
|
237
|
+
|
|
238
|
+
const containerFields = {
|
|
239
|
+
name: nonEmpty,
|
|
240
|
+
description: z.string().optional(),
|
|
241
|
+
parentId: z.string().optional(),
|
|
242
|
+
type: z.string().optional(),
|
|
243
|
+
color: z.string().optional()
|
|
244
|
+
};
|
|
245
|
+
export const CreateContainerSchema = z.object({ projectId: nonEmpty, ...containerFields });
|
|
246
|
+
export const UpdateContainerSchema = z.object(containerFields).partial();
|
|
247
|
+
|
|
248
|
+
// --- Iterations ---
|
|
249
|
+
|
|
250
|
+
const iterationFields = {
|
|
251
|
+
name: nonEmpty,
|
|
252
|
+
goal: z.string().optional(),
|
|
253
|
+
type: z.string().optional(),
|
|
254
|
+
startDate: isoString.optional(),
|
|
255
|
+
endDate: isoString.optional(),
|
|
256
|
+
status: z.enum(['planning', 'active', 'completed']).default('planning')
|
|
257
|
+
};
|
|
258
|
+
export const CreateIterationSchema = z.object({ projectId: nonEmpty, ...iterationFields });
|
|
259
|
+
export const UpdateIterationSchema = z
|
|
260
|
+
.object({ ...iterationFields, status: z.enum(['planning', 'active', 'completed']) })
|
|
261
|
+
.partial();
|
|
262
|
+
|
|
263
|
+
// --- Comments & Reactions ---
|
|
264
|
+
|
|
265
|
+
export const CreateCommentSchema = z.object({
|
|
266
|
+
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
|
+
parentId: z.string().optional(),
|
|
271
|
+
content: nonEmpty,
|
|
272
|
+
mentions: z.array(z.string()).optional(),
|
|
273
|
+
metadata: metadata.optional()
|
|
274
|
+
});
|
|
275
|
+
export const UpdateCommentSchema = z
|
|
276
|
+
.object({ content: nonEmpty, mentions: z.array(z.string()), metadata })
|
|
277
|
+
.partial();
|
|
278
|
+
|
|
279
|
+
export const CommentReactionSchema = z.object({
|
|
280
|
+
emoji: nonEmpty,
|
|
281
|
+
/** Required unless the server resolves the caller, in which case it is replaced. */
|
|
282
|
+
userId: z.string().optional()
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// --- Attachments ---
|
|
286
|
+
|
|
287
|
+
const attachmentLinks = {
|
|
288
|
+
taskId: z.string().optional(),
|
|
289
|
+
projectId: z.string().optional(),
|
|
290
|
+
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
|
+
artifactType: z.enum(['plan', 'spec', 'deliverable', 'review', 'general']).optional(),
|
|
295
|
+
metadata: metadata.optional()
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const CreateAttachmentSchema = z.object({
|
|
299
|
+
...attachmentLinks,
|
|
300
|
+
filename: nonEmpty,
|
|
301
|
+
// Linked (rather than uploaded) files often have no known type or size.
|
|
302
|
+
mimeType: nonEmpty.default('application/octet-stream'),
|
|
303
|
+
sizeBytes: z.number().int().nonnegative().default(0),
|
|
304
|
+
url: nonEmpty,
|
|
305
|
+
storageKey: z.string().optional()
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
export const UploadAttachmentSchema = z.object({
|
|
309
|
+
...attachmentLinks,
|
|
310
|
+
filename: nonEmpty,
|
|
311
|
+
data: z.string(),
|
|
312
|
+
mimeType: z.string().optional(),
|
|
313
|
+
pathPrefix: z.string().optional(),
|
|
314
|
+
encoding: z.enum(['base64', 'utf-8', 'binary']).optional()
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
export const PresignAttachmentSchema = z.object({
|
|
318
|
+
storageKey: nonEmpty,
|
|
319
|
+
expiresInSeconds: z.number().int().positive().optional(),
|
|
320
|
+
contentType: z.string().optional()
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// --- Time Tracking ---
|
|
324
|
+
|
|
325
|
+
export const LogTimeSchema = z.object({
|
|
326
|
+
taskId: nonEmpty,
|
|
327
|
+
hours: z.number().positive(),
|
|
328
|
+
isBillable: z.boolean().optional(),
|
|
329
|
+
description: z.string().optional(),
|
|
330
|
+
userId: z.string().optional(),
|
|
331
|
+
loggedAt: isoString.optional()
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
export type CreateTaskPayload = z.infer<typeof CreateTaskSchema>;
|
|
335
|
+
export type UpdateTaskPayload = z.infer<typeof UpdateTaskSchema>;
|
|
336
|
+
export type CreateProjectPayload = z.infer<typeof CreateProjectSchema>;
|
|
337
|
+
export type UpdateProjectPayload = z.infer<typeof UpdateProjectSchema>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type {
|
|
4
|
+
CreateTaskInput,
|
|
5
|
+
Task,
|
|
6
|
+
CreateProjectInput,
|
|
7
|
+
Project,
|
|
8
|
+
Workflow,
|
|
9
|
+
CreateDeliverableInput,
|
|
10
|
+
Deliverable,
|
|
11
|
+
Team,
|
|
12
|
+
TaskContainer,
|
|
13
|
+
Iteration,
|
|
14
|
+
Comment,
|
|
15
|
+
CreateAttachmentInput,
|
|
16
|
+
TimeEntry
|
|
17
|
+
} from '../types/index.js';
|
|
18
|
+
import {
|
|
19
|
+
CreateTaskSchema,
|
|
20
|
+
UpdateTaskSchema,
|
|
21
|
+
CreateProjectSchema,
|
|
22
|
+
UpdateProjectSchema,
|
|
23
|
+
CreateWorkflowSchema,
|
|
24
|
+
CreateDeliverableSchema,
|
|
25
|
+
UpdateDeliverableSchema,
|
|
26
|
+
CreateTeamSchema,
|
|
27
|
+
CreateContainerSchema,
|
|
28
|
+
UpdateContainerSchema,
|
|
29
|
+
CreateIterationSchema,
|
|
30
|
+
CreateCommentSchema,
|
|
31
|
+
UpdateCommentSchema,
|
|
32
|
+
CreateAttachmentSchema,
|
|
33
|
+
LogTimeSchema
|
|
34
|
+
} from './index.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compile-time check that a schema covers exactly the writable keys of a domain type. If a field
|
|
38
|
+
* is added to a type in `types/index.ts` but not to its schema, the build fails here instead of
|
|
39
|
+
* the field being silently stripped from API requests.
|
|
40
|
+
*/
|
|
41
|
+
type SameKeys<Schema, Expected> = [Exclude<keyof Schema, keyof Expected>, Exclude<keyof Expected, keyof Schema>] extends [
|
|
42
|
+
never,
|
|
43
|
+
never
|
|
44
|
+
]
|
|
45
|
+
? true
|
|
46
|
+
: { extraInSchema: Exclude<keyof Schema, keyof Expected>; missingFromSchema: Exclude<keyof Expected, keyof Schema> };
|
|
47
|
+
|
|
48
|
+
type Infer<S extends z.ZodTypeAny> = z.infer<S>;
|
|
49
|
+
type ServerAssigned = 'id' | 'createdAt' | 'updatedAt';
|
|
50
|
+
type ActorClaims = 'actorId' | 'actorName' | 'actorType' | 'actor';
|
|
51
|
+
|
|
52
|
+
const keyChecks: true[] = [
|
|
53
|
+
true as SameKeys<Infer<typeof CreateTaskSchema>, Omit<CreateTaskInput, 'key'>>,
|
|
54
|
+
true as SameKeys<Omit<Infer<typeof UpdateTaskSchema>, ActorClaims>, Omit<Task, ServerAssigned | 'key' | 'projectId'>>,
|
|
55
|
+
// `workflow` is a denormalised copy resolved from `workflowId`, so it is not writable.
|
|
56
|
+
true as SameKeys<Infer<typeof CreateProjectSchema>, Omit<CreateProjectInput, 'workflow'>>,
|
|
57
|
+
true as SameKeys<Infer<typeof UpdateProjectSchema>, Omit<Project, ServerAssigned | 'workflow'>>,
|
|
58
|
+
true as SameKeys<Infer<typeof CreateWorkflowSchema>, Omit<Workflow, ServerAssigned>>,
|
|
59
|
+
true as SameKeys<Infer<typeof CreateDeliverableSchema>, CreateDeliverableInput>,
|
|
60
|
+
true as SameKeys<Infer<typeof UpdateDeliverableSchema>, Omit<Deliverable, ServerAssigned | 'projectId'>>,
|
|
61
|
+
true as SameKeys<Infer<typeof CreateTeamSchema>, Omit<Team, ServerAssigned>>,
|
|
62
|
+
true as SameKeys<Infer<typeof CreateContainerSchema>, Omit<TaskContainer, ServerAssigned>>,
|
|
63
|
+
true as SameKeys<Infer<typeof UpdateContainerSchema>, Omit<TaskContainer, ServerAssigned | 'projectId'>>,
|
|
64
|
+
true as SameKeys<Infer<typeof CreateIterationSchema>, Omit<Iteration, 'id' | 'createdAt'>>,
|
|
65
|
+
// Reactions are managed through the reactions endpoints.
|
|
66
|
+
true as SameKeys<Infer<typeof CreateCommentSchema>, Omit<Comment, ServerAssigned | 'reactions'>>,
|
|
67
|
+
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
|
+
];
|
|
71
|
+
|
|
72
|
+
describe('request schemas', () => {
|
|
73
|
+
it('cover the writable fields of each domain type', () => {
|
|
74
|
+
expect(keyChecks.every(Boolean)).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('strip server-assigned and unknown fields', () => {
|
|
78
|
+
const parsed = UpdateTaskSchema.parse({
|
|
79
|
+
title: 'ok',
|
|
80
|
+
id: 'hijack',
|
|
81
|
+
projectId: 'other',
|
|
82
|
+
createdAt: '1999-01-01',
|
|
83
|
+
key: 'X-1',
|
|
84
|
+
junk: true
|
|
85
|
+
});
|
|
86
|
+
expect(parsed).toEqual({ title: 'ok' });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('reject wrong types and out-of-range values', () => {
|
|
90
|
+
expect(UpdateTaskSchema.safeParse({ status: 123 }).success).toBe(false);
|
|
91
|
+
expect(UpdateTaskSchema.safeParse({ progress: 140 }).success).toBe(false);
|
|
92
|
+
expect(CreateTaskSchema.safeParse({ projectId: 'p1' }).success).toBe(false);
|
|
93
|
+
expect(LogTimeSchema.safeParse({ taskId: 't1', hours: 0 }).success).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('apply defaults for optional collections and enums', () => {
|
|
97
|
+
expect(CreateTeamSchema.parse({ name: 'Core' }).memberIds).toEqual([]);
|
|
98
|
+
expect(CreateIterationSchema.parse({ projectId: 'p1', name: 'S1' }).status).toBe('planning');
|
|
99
|
+
expect(CreateWorkflowSchema.parse({ name: 'W', statuses: [] }).transitions).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('keep lifecycle fields writable for imports and backfills', () => {
|
|
103
|
+
const parsed = CreateTaskSchema.parse({
|
|
104
|
+
projectId: 'p1',
|
|
105
|
+
title: 'Imported',
|
|
106
|
+
completedAt: '2026-01-02T00:00:00.000Z',
|
|
107
|
+
actualStartDate: '2026-01-01T00:00:00.000Z'
|
|
108
|
+
});
|
|
109
|
+
expect(parsed.completedAt).toBe('2026-01-02T00:00:00.000Z');
|
|
110
|
+
});
|
|
111
|
+
});
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/task-state.test.ts","./src/use-case-validation.test.ts","./src/domain/calendar.test.ts","./src/domain/calendar.ts","./src/domain/cpm.test.ts","./src/domain/cpm.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/errors.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/domain/ladder.test.ts","./src/domain/ladder.ts","./src/domain/metrics.test.ts","./src/domain/metrics.ts","./src/domain/task-duration.test.ts","./src/domain/workload.test.ts","./src/domain/workload.ts","./src/engine/actor.test.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/fractional-index.test.ts","./src/utils/fractional-index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/task-state.test.ts","./src/use-case-validation.test.ts","./src/domain/calendar.test.ts","./src/domain/calendar.ts","./src/domain/cpm.test.ts","./src/domain/cpm.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/errors.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/domain/ladder.test.ts","./src/domain/ladder.ts","./src/domain/metrics.test.ts","./src/domain/metrics.ts","./src/domain/task-duration.test.ts","./src/domain/workload.test.ts","./src/domain/workload.ts","./src/engine/actor.test.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/schemas/index.ts","./src/schemas/schemas.test.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/fractional-index.test.ts","./src/utils/fractional-index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|