@critical-path/core 0.35.0 → 0.36.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/README.md +11 -0
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +25 -2
- package/dist/engine/index.js.map +1 -1
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -1
- package/CHANGELOG.md +0 -584
- package/dist/deliverables.test.d.ts +0 -2
- package/dist/deliverables.test.d.ts.map +0 -1
- package/dist/deliverables.test.js +0 -232
- package/dist/deliverables.test.js.map +0 -1
- package/dist/domain/calendar.test.d.ts +0 -2
- package/dist/domain/calendar.test.d.ts.map +0 -1
- package/dist/domain/calendar.test.js +0 -181
- package/dist/domain/calendar.test.js.map +0 -1
- package/dist/domain/cpm.test.d.ts +0 -2
- package/dist/domain/cpm.test.d.ts.map +0 -1
- package/dist/domain/cpm.test.js +0 -166
- package/dist/domain/cpm.test.js.map +0 -1
- package/dist/domain/ddd.test.d.ts +0 -2
- package/dist/domain/ddd.test.d.ts.map +0 -1
- package/dist/domain/ddd.test.js +0 -349
- package/dist/domain/ddd.test.js.map +0 -1
- package/dist/domain/ladder.test.d.ts +0 -2
- package/dist/domain/ladder.test.d.ts.map +0 -1
- package/dist/domain/ladder.test.js +0 -213
- package/dist/domain/ladder.test.js.map +0 -1
- package/dist/domain/metrics.test.d.ts +0 -2
- package/dist/domain/metrics.test.d.ts.map +0 -1
- package/dist/domain/metrics.test.js +0 -252
- package/dist/domain/metrics.test.js.map +0 -1
- package/dist/domain/task-duration.test.d.ts +0 -2
- package/dist/domain/task-duration.test.d.ts.map +0 -1
- package/dist/domain/task-duration.test.js +0 -307
- package/dist/domain/task-duration.test.js.map +0 -1
- package/dist/domain/workload.test.d.ts +0 -2
- package/dist/domain/workload.test.d.ts.map +0 -1
- package/dist/domain/workload.test.js +0 -261
- package/dist/domain/workload.test.js.map +0 -1
- package/dist/engine/actor.test.d.ts +0 -2
- package/dist/engine/actor.test.d.ts.map +0 -1
- package/dist/engine/actor.test.js +0 -54
- package/dist/engine/actor.test.js.map +0 -1
- package/dist/engine/authorization.test.d.ts +0 -2
- package/dist/engine/authorization.test.d.ts.map +0 -1
- package/dist/engine/authorization.test.js +0 -154
- package/dist/engine/authorization.test.js.map +0 -1
- package/dist/engine/cascade.test.d.ts +0 -2
- package/dist/engine/cascade.test.d.ts.map +0 -1
- package/dist/engine/cascade.test.js +0 -99
- package/dist/engine/cascade.test.js.map +0 -1
- package/dist/engine/config.test.d.ts +0 -2
- package/dist/engine/config.test.d.ts.map +0 -1
- package/dist/engine/config.test.js +0 -46
- package/dist/engine/config.test.js.map +0 -1
- package/dist/engine/entity-events.test.d.ts +0 -2
- package/dist/engine/entity-events.test.d.ts.map +0 -1
- package/dist/engine/entity-events.test.js +0 -61
- package/dist/engine/entity-events.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -429
- package/dist/index.test.js.map +0 -1
- package/dist/plugins/plugins.test.d.ts +0 -2
- package/dist/plugins/plugins.test.d.ts.map +0 -1
- package/dist/plugins/plugins.test.js +0 -137
- package/dist/plugins/plugins.test.js.map +0 -1
- package/dist/schemas/schemas.test.d.ts +0 -2
- package/dist/schemas/schemas.test.d.ts.map +0 -1
- package/dist/schemas/schemas.test.js +0 -58
- package/dist/schemas/schemas.test.js.map +0 -1
- package/dist/status.test.d.ts +0 -2
- package/dist/status.test.d.ts.map +0 -1
- package/dist/status.test.js +0 -295
- package/dist/status.test.js.map +0 -1
- package/dist/storage/storage.test.d.ts +0 -2
- package/dist/storage/storage.test.d.ts.map +0 -1
- package/dist/storage/storage.test.js +0 -236
- package/dist/storage/storage.test.js.map +0 -1
- package/dist/store/conformance.test.d.ts +0 -2
- package/dist/store/conformance.test.d.ts.map +0 -1
- package/dist/store/conformance.test.js +0 -15
- package/dist/store/conformance.test.js.map +0 -1
- package/dist/store/firebase.test.d.ts +0 -2
- package/dist/store/firebase.test.d.ts.map +0 -1
- package/dist/store/firebase.test.js +0 -103
- package/dist/store/firebase.test.js.map +0 -1
- package/dist/store/query.test.d.ts +0 -2
- package/dist/store/query.test.d.ts.map +0 -1
- package/dist/store/query.test.js +0 -89
- package/dist/store/query.test.js.map +0 -1
- package/dist/store/sqlite.test.d.ts +0 -2
- package/dist/store/sqlite.test.d.ts.map +0 -1
- package/dist/store/sqlite.test.js +0 -177
- package/dist/store/sqlite.test.js.map +0 -1
- package/dist/task-state.test.d.ts +0 -2
- package/dist/task-state.test.d.ts.map +0 -1
- package/dist/task-state.test.js +0 -423
- package/dist/task-state.test.js.map +0 -1
- package/dist/use-case-validation.test.d.ts +0 -2
- package/dist/use-case-validation.test.d.ts.map +0 -1
- package/dist/use-case-validation.test.js +0 -281
- package/dist/use-case-validation.test.js.map +0 -1
- package/dist/utils/fractional-index.test.d.ts +0 -2
- package/dist/utils/fractional-index.test.d.ts.map +0 -1
- package/dist/utils/fractional-index.test.js +0 -182
- package/dist/utils/fractional-index.test.js.map +0 -1
- package/dist/utils/key.test.d.ts +0 -2
- package/dist/utils/key.test.d.ts.map +0 -1
- package/dist/utils/key.test.js +0 -65
- package/dist/utils/key.test.js.map +0 -1
- package/dist/utils/mentions.test.d.ts +0 -2
- package/dist/utils/mentions.test.d.ts.map +0 -1
- package/dist/utils/mentions.test.js +0 -114
- package/dist/utils/mentions.test.js.map +0 -1
- package/dist/webhooks/webhooks.test.d.ts +0 -2
- package/dist/webhooks/webhooks.test.d.ts.map +0 -1
- package/dist/webhooks/webhooks.test.js +0 -154
- package/dist/webhooks/webhooks.test.js.map +0 -1
- package/src/deliverables.test.ts +0 -284
- package/src/domain/authorization.ts +0 -82
- package/src/domain/calendar.test.ts +0 -216
- package/src/domain/calendar.ts +0 -502
- package/src/domain/cpm.test.ts +0 -189
- package/src/domain/cpm.ts +0 -258
- package/src/domain/custom-fields.ts +0 -172
- package/src/domain/ddd.test.ts +0 -442
- package/src/domain/entities.ts +0 -798
- package/src/domain/errors.ts +0 -38
- package/src/domain/events.ts +0 -369
- package/src/domain/graph.ts +0 -70
- package/src/domain/index.ts +0 -12
- package/src/domain/ladder.test.ts +0 -251
- package/src/domain/ladder.ts +0 -474
- package/src/domain/metrics.test.ts +0 -288
- package/src/domain/metrics.ts +0 -497
- package/src/domain/task-duration.test.ts +0 -354
- package/src/domain/workload.test.ts +0 -311
- package/src/domain/workload.ts +0 -457
- package/src/engine/actor.test.ts +0 -63
- package/src/engine/authorization.test.ts +0 -199
- package/src/engine/cascade.test.ts +0 -121
- package/src/engine/config.test.ts +0 -51
- package/src/engine/entity-events.test.ts +0 -67
- package/src/engine/index.ts +0 -2472
- package/src/index.test.ts +0 -514
- package/src/index.ts +0 -9
- package/src/plugins/index.ts +0 -103
- package/src/plugins/plugins.test.ts +0 -161
- package/src/schemas/index.ts +0 -402
- package/src/schemas/schemas.test.ts +0 -116
- package/src/status.test.ts +0 -354
- package/src/storage/file-storage.ts +0 -200
- package/src/storage/firebase-storage.ts +0 -209
- package/src/storage/index.ts +0 -3
- package/src/storage/s3.ts +0 -151
- package/src/storage/storage.test.ts +0 -274
- package/src/store/conformance.test.ts +0 -15
- package/src/store/firebase.test.ts +0 -131
- package/src/store/firebase.ts +0 -658
- package/src/store/index.ts +0 -623
- package/src/store/query.test.ts +0 -110
- package/src/store/query.ts +0 -113
- package/src/store/sqlite.test.ts +0 -218
- package/src/store/sqlite.ts +0 -1480
- package/src/task-state.test.ts +0 -506
- package/src/testing/conformance.ts +0 -319
- package/src/testing/index.ts +0 -1
- package/src/types/index.ts +0 -859
- package/src/use-case-validation.test.ts +0 -324
- package/src/utils/fractional-index.test.ts +0 -228
- package/src/utils/fractional-index.ts +0 -254
- package/src/utils/index.ts +0 -7
- package/src/utils/key.test.ts +0 -76
- package/src/utils/key.ts +0 -62
- package/src/utils/mentions.test.ts +0 -132
- package/src/utils/mentions.ts +0 -131
- package/src/utils/status.ts +0 -419
- package/src/utils/workflow.ts +0 -306
- package/src/webhooks/dispatcher.ts +0 -190
- package/src/webhooks/index.ts +0 -2
- package/src/webhooks/signature.ts +0 -57
- package/src/webhooks/webhooks.test.ts +0 -201
- package/tsconfig.json +0 -8
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
CriticalPathEngine,
|
|
4
|
-
CustomFieldValidationError,
|
|
5
|
-
ValidationError,
|
|
6
|
-
WorkflowValidationError,
|
|
7
|
-
type CriticalPathPlugin
|
|
8
|
-
} from '../index.js';
|
|
9
|
-
|
|
10
|
-
const strictWorkflow = {
|
|
11
|
-
name: 'Strict',
|
|
12
|
-
defaultStatusKey: 'todo',
|
|
13
|
-
statuses: [
|
|
14
|
-
{ key: 'todo', label: 'To Do', category: 'not_started' as const },
|
|
15
|
-
{ key: 'doing', label: 'Doing', category: 'in_progress' as const },
|
|
16
|
-
{ key: 'done', label: 'Done', category: 'completed' as const }
|
|
17
|
-
],
|
|
18
|
-
transitions: [
|
|
19
|
-
{ fromStatusKey: 'todo', toStatusKey: 'doing' },
|
|
20
|
-
{ fromStatusKey: 'doing', toStatusKey: 'done' }
|
|
21
|
-
]
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
describe('plugin system', () => {
|
|
25
|
-
it('runs init once with the engine before ready resolves', async () => {
|
|
26
|
-
const init = vi.fn(async (engine: CriticalPathEngine) => {
|
|
27
|
-
await engine.createProject({ name: 'Seeded by plugin' });
|
|
28
|
-
});
|
|
29
|
-
const engine = new CriticalPathEngine({ plugins: [{ id: 'seed', name: 'Seed', version: '1', init }] });
|
|
30
|
-
await engine.ready;
|
|
31
|
-
|
|
32
|
-
expect(init).toHaveBeenCalledTimes(1);
|
|
33
|
-
expect(init).toHaveBeenCalledWith(engine);
|
|
34
|
-
expect(await engine.getProjects()).toHaveLength(1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('surfaces init failures through ready without unhandled rejections', async () => {
|
|
38
|
-
const engine = new CriticalPathEngine({
|
|
39
|
-
plugins: [{ id: 'bad', name: 'Bad', version: '1', init: () => Promise.reject(new Error('boom')) }]
|
|
40
|
-
});
|
|
41
|
-
await expect(engine.ready).rejects.toThrow('boom');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('validates values of plugin-registered custom field types and rejects unknown types', async () => {
|
|
45
|
-
const urlField: CriticalPathPlugin = {
|
|
46
|
-
id: 'url-field',
|
|
47
|
-
name: 'URL field',
|
|
48
|
-
version: '1',
|
|
49
|
-
customFieldTypes: [
|
|
50
|
-
{ type: 'url', validate: (value) => (typeof value === 'string' && /^https?:\/\//.test(value) ? null : 'must be an http(s) URL') }
|
|
51
|
-
]
|
|
52
|
-
};
|
|
53
|
-
const engine = new CriticalPathEngine({ plugins: [urlField] });
|
|
54
|
-
const project = await engine.createProject({
|
|
55
|
-
name: 'Links',
|
|
56
|
-
customFieldDefinitions: [{ id: 'f1', key: 'spec', label: 'Spec', type: 'url' }]
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
await engine.createTask({ projectId: project.id, title: 'ok', customFields: { spec: 'https://example.com' } });
|
|
60
|
-
await expect(engine.createTask({ projectId: project.id, title: 'bad', customFields: { spec: 'ftp://x' } })).rejects.toThrow(
|
|
61
|
-
/must be an http\(s\) URL/
|
|
62
|
-
);
|
|
63
|
-
await expect(
|
|
64
|
-
engine.createProject({ name: 'Unknown', customFieldDefinitions: [{ id: 'f', key: 'x', label: 'X', type: 'nope' }] })
|
|
65
|
-
).rejects.toThrow(CustomFieldValidationError);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('refuses custom field types that clash with built-in or already registered types', () => {
|
|
69
|
-
const clash = (type: string): CriticalPathPlugin => ({
|
|
70
|
-
id: `p-${type}-${Math.random()}`,
|
|
71
|
-
name: 'Clash',
|
|
72
|
-
version: '1',
|
|
73
|
-
customFieldTypes: [{ type, validate: () => null }]
|
|
74
|
-
});
|
|
75
|
-
expect(() => new CriticalPathEngine({ plugins: [clash('number')] })).toThrow(/already exists/);
|
|
76
|
-
expect(() => new CriticalPathEngine({ plugins: [clash('money'), clash('money')] })).toThrow(/already exists/);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('validates the output of before-hooks, so plugins cannot bypass workflow rules', async () => {
|
|
80
|
-
const skipAhead: CriticalPathPlugin = {
|
|
81
|
-
id: 'skip',
|
|
82
|
-
name: 'Skip ahead',
|
|
83
|
-
version: '1',
|
|
84
|
-
hooks: { beforeTaskUpdate: (_id, updates) => ({ ...updates, status: 'done' }) }
|
|
85
|
-
};
|
|
86
|
-
const engine = new CriticalPathEngine({ plugins: [skipAhead] });
|
|
87
|
-
const workflow = await engine.createWorkflow(strictWorkflow);
|
|
88
|
-
const project = await engine.createProject({ name: 'Strict', workflowId: workflow.id });
|
|
89
|
-
const task = await engine.createTask({ projectId: project.id, title: 'T' });
|
|
90
|
-
|
|
91
|
-
await expect(engine.updateTask(task.id, { title: 'renamed' })).rejects.toThrow(WorkflowValidationError);
|
|
92
|
-
expect((await engine.getTask(task.id))?.status).toBe('todo');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('prevents before-hooks from moving tasks between projects', async () => {
|
|
96
|
-
let otherProjectId = '';
|
|
97
|
-
const mover: CriticalPathPlugin = {
|
|
98
|
-
id: 'mover',
|
|
99
|
-
name: 'Mover',
|
|
100
|
-
version: '1',
|
|
101
|
-
hooks: {
|
|
102
|
-
beforeTaskCreate: (task) => (task.title === 'move me' ? { ...task, projectId: otherProjectId } : task),
|
|
103
|
-
beforeTaskUpdate: (_id, updates) => ({ ...updates, projectId: otherProjectId })
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
const engine = new CriticalPathEngine({ plugins: [mover] });
|
|
107
|
-
const home = await engine.createProject({ name: 'Home' });
|
|
108
|
-
otherProjectId = (await engine.createProject({ name: 'Other' })).id;
|
|
109
|
-
|
|
110
|
-
await expect(engine.createTask({ projectId: home.id, title: 'move me' })).rejects.toThrow(ValidationError);
|
|
111
|
-
const task = await engine.createTask({ projectId: home.id, title: 'stay' });
|
|
112
|
-
await engine.updateTask(task.id, { title: 'still here' });
|
|
113
|
-
expect((await engine.getTask(task.id))?.projectId).toBe(home.id);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('enforces required custom fields even when none are supplied', async () => {
|
|
117
|
-
const engine = new CriticalPathEngine();
|
|
118
|
-
const project = await engine.createProject({
|
|
119
|
-
name: 'Required',
|
|
120
|
-
customFieldDefinitions: [{ id: 'f', key: 'client', label: 'Client', type: 'text', required: true }]
|
|
121
|
-
});
|
|
122
|
-
await expect(engine.createTask({ projectId: project.id, title: 'missing' })).rejects.toThrow(/required/);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('logs after-hook failures without failing the write or skipping events', async () => {
|
|
126
|
-
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
127
|
-
const engine = new CriticalPathEngine({
|
|
128
|
-
plugins: [{ id: 'flaky', name: 'Flaky', version: '1', hooks: { afterTaskCreate: () => { throw new Error('downstream'); } } }]
|
|
129
|
-
});
|
|
130
|
-
const published: string[] = [];
|
|
131
|
-
engine.events.subscribe('task.created', (e) => void published.push(e.name));
|
|
132
|
-
|
|
133
|
-
const project = await engine.createProject({ name: 'After' });
|
|
134
|
-
const task = await engine.createTask({ projectId: project.id, title: 'stored' });
|
|
135
|
-
expect(await engine.getTask(task.id)).not.toBeNull();
|
|
136
|
-
expect(published).toEqual(['task.created']);
|
|
137
|
-
expect(consoleError).toHaveBeenCalledWith(expect.stringContaining('"flaky" afterTaskCreate'), expect.any(Error));
|
|
138
|
-
consoleError.mockRestore();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('passes the deleted task to delete hooks', async () => {
|
|
142
|
-
const seen: string[] = [];
|
|
143
|
-
const engine = new CriticalPathEngine({
|
|
144
|
-
plugins: [
|
|
145
|
-
{
|
|
146
|
-
id: 'audit',
|
|
147
|
-
name: 'Audit',
|
|
148
|
-
version: '1',
|
|
149
|
-
hooks: {
|
|
150
|
-
beforeTaskDelete: (_id, task) => void seen.push(`before:${task.title}`),
|
|
151
|
-
afterTaskDelete: (_id, task) => void seen.push(`after:${task.title}`)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
]
|
|
155
|
-
});
|
|
156
|
-
const project = await engine.createProject({ name: 'Del' });
|
|
157
|
-
const task = await engine.createTask({ projectId: project.id, title: 'gone' });
|
|
158
|
-
await engine.deleteTask(task.id);
|
|
159
|
-
expect(seen).toEqual(['before:gone', 'after:gone']);
|
|
160
|
-
});
|
|
161
|
-
});
|
package/src/schemas/index.ts
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
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
|
-
* 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`).
|
|
10
|
-
*/
|
|
11
|
-
import { z } from 'zod';
|
|
12
|
-
import { ValidationError, type ValidationIssue } from '../domain/errors.js';
|
|
13
|
-
import { DOMAIN_EVENT_NAMES } from '../domain/events.js';
|
|
14
|
-
import { MAX_PAGE_SIZE } from '../store/query.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Parses `data` with `schema`, returning the cleaned value (unknown keys stripped, defaults
|
|
18
|
-
* applied) or throwing a `ValidationError` whose `issues` list every problem.
|
|
19
|
-
*/
|
|
20
|
-
export function parsePayload<S extends z.ZodTypeAny>(schema: S, data: unknown): z.output<S> {
|
|
21
|
-
const result = schema.safeParse(data ?? {});
|
|
22
|
-
if (result.success) return result.data;
|
|
23
|
-
const issues: ValidationIssue[] = result.error.issues.map((issue) => ({
|
|
24
|
-
path: issue.path.join('.'),
|
|
25
|
-
message: issue.message
|
|
26
|
-
}));
|
|
27
|
-
const summary = issues.map((i) => (i.path ? `${i.path}: ${i.message}` : i.message)).join('; ');
|
|
28
|
-
throw new ValidationError(`Invalid request body: ${summary}`, issues);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Objects reject unknown keys so typos and protected fields surface as 400s. */
|
|
32
|
-
function strictObject<T extends z.ZodRawShape>(shape: T) {
|
|
33
|
-
return z.object(shape).strict();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const isoString = z.string();
|
|
37
|
-
const nonEmpty = z.string().min(1);
|
|
38
|
-
|
|
39
|
-
export const SemanticStatusSchema = z.enum(['not_started', 'in_progress', 'completed', 'canceled']);
|
|
40
|
-
export const AuthorTypeSchema = z.enum(['user', 'agent', 'system']);
|
|
41
|
-
|
|
42
|
-
export const StatusDefinitionSchema = strictObject({
|
|
43
|
-
key: nonEmpty,
|
|
44
|
-
label: z.string(),
|
|
45
|
-
category: SemanticStatusSchema
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
export const WorkScheduleSchema = strictObject({
|
|
49
|
-
id: z.string().optional(),
|
|
50
|
-
name: z.string().optional(),
|
|
51
|
-
timezone: z.string().optional(),
|
|
52
|
-
defaultHoursPerDay: z.number().nonnegative().optional(),
|
|
53
|
-
days: z.array(
|
|
54
|
-
strictObject({
|
|
55
|
-
dayOfWeek: z.number().int().min(0).max(6),
|
|
56
|
-
isWorkingDay: z.boolean(),
|
|
57
|
-
hours: z.array(strictObject({ start: z.string(), end: z.string() })).optional()
|
|
58
|
-
})
|
|
59
|
-
),
|
|
60
|
-
holidays: z
|
|
61
|
-
.array(strictObject({ date: z.string(), name: z.string().optional(), halfDay: z.boolean().optional() }))
|
|
62
|
-
.optional()
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
export const CustomFieldDefinitionSchema = strictObject({
|
|
66
|
-
id: z.string(),
|
|
67
|
-
key: nonEmpty,
|
|
68
|
-
label: z.string(),
|
|
69
|
-
/** Built-in type or one registered by a plugin (checked by the engine). */
|
|
70
|
-
type: nonEmpty,
|
|
71
|
-
options: z.array(z.string()).optional(),
|
|
72
|
-
required: z.boolean().optional(),
|
|
73
|
-
defaultValue: z.unknown().optional()
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
export const TaskTypeDefinitionSchema = strictObject({
|
|
77
|
-
key: nonEmpty,
|
|
78
|
-
label: z.string(),
|
|
79
|
-
description: z.string().optional(),
|
|
80
|
-
icon: z.string().optional(),
|
|
81
|
-
defaultStatusKey: z.string().optional(),
|
|
82
|
-
workflowId: z.string().optional()
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const customFields = z.record(z.unknown());
|
|
86
|
-
const metadata = z.record(z.unknown());
|
|
87
|
-
|
|
88
|
-
// --- Workflows ---
|
|
89
|
-
|
|
90
|
-
export const CreateWorkflowSchema = strictObject({
|
|
91
|
-
name: nonEmpty,
|
|
92
|
-
description: z.string().optional(),
|
|
93
|
-
statuses: z.array(StatusDefinitionSchema),
|
|
94
|
-
transitions: z
|
|
95
|
-
.array(
|
|
96
|
-
strictObject({
|
|
97
|
-
id: z.string().optional(),
|
|
98
|
-
name: z.string().optional(),
|
|
99
|
-
fromStatusKey: z.string(),
|
|
100
|
-
toStatusKey: z.string()
|
|
101
|
-
})
|
|
102
|
-
)
|
|
103
|
-
.default([]),
|
|
104
|
-
taskTypes: z.array(TaskTypeDefinitionSchema).optional(),
|
|
105
|
-
defaultStatusKey: z.string().optional(),
|
|
106
|
-
isDefault: z.boolean().optional()
|
|
107
|
-
});
|
|
108
|
-
export const UpdateWorkflowSchema = CreateWorkflowSchema.partial();
|
|
109
|
-
|
|
110
|
-
// --- Projects ---
|
|
111
|
-
|
|
112
|
-
export const CreateProjectSchema = strictObject({
|
|
113
|
-
key: z.string().optional(),
|
|
114
|
-
name: nonEmpty,
|
|
115
|
-
description: z.string().optional(),
|
|
116
|
-
ownerId: z.string().optional(),
|
|
117
|
-
members: z
|
|
118
|
-
.array(strictObject({ userId: nonEmpty, role: z.enum(['admin', 'project_manager', 'contributor', 'viewer']) }))
|
|
119
|
-
.optional(),
|
|
120
|
-
teamIds: z.array(z.string()).optional(),
|
|
121
|
-
workflowId: z.string().optional(),
|
|
122
|
-
taskTypes: z.array(TaskTypeDefinitionSchema).optional(),
|
|
123
|
-
statusDefinitions: z.array(StatusDefinitionSchema).optional(),
|
|
124
|
-
priorityDefinitions: z
|
|
125
|
-
.array(strictObject({ key: z.string(), label: z.string(), level: z.number().optional() }))
|
|
126
|
-
.optional(),
|
|
127
|
-
customFieldDefinitions: z.array(CustomFieldDefinitionSchema).optional(),
|
|
128
|
-
schedule: WorkScheduleSchema.optional(),
|
|
129
|
-
startDate: isoString.optional(),
|
|
130
|
-
targetEndDate: isoString.optional()
|
|
131
|
-
});
|
|
132
|
-
export const UpdateProjectSchema = CreateProjectSchema.partial();
|
|
133
|
-
|
|
134
|
-
// --- Tasks ---
|
|
135
|
-
|
|
136
|
-
const taskFields = {
|
|
137
|
-
title: nonEmpty,
|
|
138
|
-
description: z.string().optional(),
|
|
139
|
-
status: z.string().optional(),
|
|
140
|
-
semanticStatus: SemanticStatusSchema.optional(),
|
|
141
|
-
priority: z.string().optional(),
|
|
142
|
-
taskType: z.string().optional(),
|
|
143
|
-
assigneeId: z.string().optional(),
|
|
144
|
-
assignees: z
|
|
145
|
-
.array(
|
|
146
|
-
strictObject({
|
|
147
|
-
id: z.string(),
|
|
148
|
-
name: z.string().optional(),
|
|
149
|
-
role: z.string().optional(),
|
|
150
|
-
type: z.enum(['user', 'agent', 'team']).optional(),
|
|
151
|
-
avatarUrl: z.string().optional()
|
|
152
|
-
})
|
|
153
|
-
)
|
|
154
|
-
.optional(),
|
|
155
|
-
reporterId: z.string().optional(),
|
|
156
|
-
reviewerId: z.string().optional(),
|
|
157
|
-
iterationId: z.string().optional(),
|
|
158
|
-
teamId: z.string().optional(),
|
|
159
|
-
containerId: z.string().optional(),
|
|
160
|
-
deliverableId: z.string().optional(),
|
|
161
|
-
plannedStartDate: isoString.optional(),
|
|
162
|
-
actualStartDate: isoString.optional(),
|
|
163
|
-
actualEndDate: isoString.optional(),
|
|
164
|
-
completedAt: isoString.optional(),
|
|
165
|
-
dueDate: isoString.optional(),
|
|
166
|
-
estimatedHours: z.number().nonnegative().optional(),
|
|
167
|
-
loggedHours: z.number().nonnegative().optional(),
|
|
168
|
-
actualHours: z.number().nonnegative().optional(),
|
|
169
|
-
billableHours: z.number().nonnegative().optional(),
|
|
170
|
-
estimatedDurationMinutes: z.number().nonnegative().optional(),
|
|
171
|
-
actualDurationMinutes: z.number().nonnegative().optional(),
|
|
172
|
-
billableDurationMinutes: z.number().nonnegative().optional(),
|
|
173
|
-
actualDurationSeconds: z.number().nonnegative().optional(),
|
|
174
|
-
inProgressSince: isoString.nullable().optional(),
|
|
175
|
-
blockedDurationSeconds: z.number().nonnegative().optional(),
|
|
176
|
-
blockedSince: isoString.nullable().optional(),
|
|
177
|
-
progress: z.number().min(0).max(100).optional(),
|
|
178
|
-
isBlocked: z.boolean().optional(),
|
|
179
|
-
blockedReason: z.string().nullable().optional(),
|
|
180
|
-
tags: z.array(z.string()).optional(),
|
|
181
|
-
todos: z
|
|
182
|
-
.array(
|
|
183
|
-
strictObject({
|
|
184
|
-
id: z.string(),
|
|
185
|
-
title: z.string(),
|
|
186
|
-
completed: z.boolean(),
|
|
187
|
-
createdAt: isoString.optional(),
|
|
188
|
-
completedAt: isoString.optional()
|
|
189
|
-
})
|
|
190
|
-
)
|
|
191
|
-
.optional(),
|
|
192
|
-
customFields: customFields.optional(),
|
|
193
|
-
parentId: z.string().optional()
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
export const CreateTaskSchema = strictObject({ projectId: nonEmpty, ...taskFields });
|
|
197
|
-
|
|
198
|
-
export const UpdateTaskSchema = strictObject(taskFields).partial();
|
|
199
|
-
|
|
200
|
-
// --- Dependencies ---
|
|
201
|
-
|
|
202
|
-
export const CreateDependencySchema = strictObject({
|
|
203
|
-
dependsOnTaskId: nonEmpty,
|
|
204
|
-
type: z.enum(['blocking', 'blocked_by', 'relates_to']).default('blocking')
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// --- Deliverables ---
|
|
208
|
-
|
|
209
|
-
const deliverableFields = {
|
|
210
|
-
title: nonEmpty,
|
|
211
|
-
description: z.string().optional(),
|
|
212
|
-
status: z.string().optional(),
|
|
213
|
-
format: z.string().optional(),
|
|
214
|
-
specs: z.record(z.unknown()).optional(),
|
|
215
|
-
leadId: z.string().optional(),
|
|
216
|
-
reviewerId: z.string().optional(),
|
|
217
|
-
dueDate: isoString.optional(),
|
|
218
|
-
deliveredAt: isoString.optional(),
|
|
219
|
-
outputUrls: z.array(z.string()).optional(),
|
|
220
|
-
customFields: customFields.optional()
|
|
221
|
-
};
|
|
222
|
-
export const CreateDeliverableSchema = strictObject({ projectId: nonEmpty, ...deliverableFields });
|
|
223
|
-
export const UpdateDeliverableSchema = strictObject(deliverableFields).partial();
|
|
224
|
-
|
|
225
|
-
// --- Teams ---
|
|
226
|
-
|
|
227
|
-
export const CreateTeamSchema = strictObject({
|
|
228
|
-
name: nonEmpty,
|
|
229
|
-
description: z.string().optional(),
|
|
230
|
-
leaderId: z.string().optional(),
|
|
231
|
-
memberIds: z.array(z.string()).default([]),
|
|
232
|
-
weeklyCapacityHours: z.number().nonnegative().optional(),
|
|
233
|
-
schedule: WorkScheduleSchema.optional()
|
|
234
|
-
});
|
|
235
|
-
export const UpdateTeamSchema = CreateTeamSchema.partial();
|
|
236
|
-
|
|
237
|
-
// --- Containers ---
|
|
238
|
-
|
|
239
|
-
const containerFields = {
|
|
240
|
-
name: nonEmpty,
|
|
241
|
-
description: z.string().optional(),
|
|
242
|
-
parentId: z.string().optional(),
|
|
243
|
-
type: z.string().optional(),
|
|
244
|
-
color: z.string().optional()
|
|
245
|
-
};
|
|
246
|
-
export const CreateContainerSchema = strictObject({ projectId: nonEmpty, ...containerFields });
|
|
247
|
-
export const UpdateContainerSchema = strictObject(containerFields).partial();
|
|
248
|
-
|
|
249
|
-
// --- Iterations ---
|
|
250
|
-
|
|
251
|
-
const iterationFields = {
|
|
252
|
-
name: nonEmpty,
|
|
253
|
-
goal: z.string().optional(),
|
|
254
|
-
type: z.string().optional(),
|
|
255
|
-
startDate: isoString.optional(),
|
|
256
|
-
endDate: isoString.optional(),
|
|
257
|
-
status: z.enum(['planning', 'active', 'completed']).default('planning')
|
|
258
|
-
};
|
|
259
|
-
export const CreateIterationSchema = strictObject({ projectId: nonEmpty, ...iterationFields });
|
|
260
|
-
export const UpdateIterationSchema = z
|
|
261
|
-
.object({ ...iterationFields, status: z.enum(['planning', 'active', 'completed']) })
|
|
262
|
-
.partial();
|
|
263
|
-
|
|
264
|
-
// --- Comments & Reactions ---
|
|
265
|
-
|
|
266
|
-
export const CreateCommentSchema = strictObject({
|
|
267
|
-
taskId: nonEmpty,
|
|
268
|
-
parentId: z.string().optional(),
|
|
269
|
-
content: nonEmpty,
|
|
270
|
-
mentions: z.array(z.string()).optional(),
|
|
271
|
-
metadata: metadata.optional()
|
|
272
|
-
});
|
|
273
|
-
export const UpdateCommentSchema = z
|
|
274
|
-
.object({ content: nonEmpty, mentions: z.array(z.string()), metadata })
|
|
275
|
-
.partial();
|
|
276
|
-
|
|
277
|
-
export const CommentReactionSchema = strictObject({
|
|
278
|
-
emoji: nonEmpty
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// --- Attachments ---
|
|
282
|
-
|
|
283
|
-
const attachmentLinks = {
|
|
284
|
-
taskId: z.string().optional(),
|
|
285
|
-
projectId: z.string().optional(),
|
|
286
|
-
commentId: z.string().optional(),
|
|
287
|
-
artifactType: z.enum(['plan', 'spec', 'deliverable', 'review', 'general']).optional(),
|
|
288
|
-
metadata: metadata.optional()
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
export const CreateAttachmentSchema = strictObject({
|
|
292
|
-
...attachmentLinks,
|
|
293
|
-
filename: nonEmpty,
|
|
294
|
-
// Linked (rather than uploaded) files often have no known type or size.
|
|
295
|
-
mimeType: nonEmpty.default('application/octet-stream'),
|
|
296
|
-
sizeBytes: z.number().int().nonnegative().default(0),
|
|
297
|
-
url: nonEmpty,
|
|
298
|
-
storageKey: z.string().optional()
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
export const UploadAttachmentSchema = strictObject({
|
|
302
|
-
...attachmentLinks,
|
|
303
|
-
filename: nonEmpty,
|
|
304
|
-
data: z.string(),
|
|
305
|
-
mimeType: z.string().optional(),
|
|
306
|
-
encoding: z.enum(['base64', 'utf-8', 'binary']).optional()
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
export const PresignAttachmentSchema = strictObject({
|
|
310
|
-
/** Project the upload belongs to; the file is stored under `projects/<projectId>/`. */
|
|
311
|
-
projectId: nonEmpty,
|
|
312
|
-
/** Original file name; the engine generates the storage key from it. */
|
|
313
|
-
filename: nonEmpty,
|
|
314
|
-
expiresInSeconds: z.number().int().positive().optional(),
|
|
315
|
-
contentType: z.string().optional()
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// --- Time Tracking ---
|
|
319
|
-
|
|
320
|
-
export const LogTimeSchema = strictObject({
|
|
321
|
-
taskId: nonEmpty,
|
|
322
|
-
hours: z.number().positive(),
|
|
323
|
-
isBillable: z.boolean().optional(),
|
|
324
|
-
description: z.string().optional(),
|
|
325
|
-
loggedAt: isoString.optional()
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// --- List queries (URL search parameters) ---
|
|
329
|
-
|
|
330
|
-
const csv = z
|
|
331
|
-
.string()
|
|
332
|
-
.transform((value) => value.split(',').map((part) => part.trim()).filter(Boolean));
|
|
333
|
-
const pageLimit = z.coerce.number().int().min(1).max(MAX_PAGE_SIZE);
|
|
334
|
-
|
|
335
|
-
/** `GET /tasks` query parameters. `status` and `priority` are comma-separated; `parentId=none` means top-level. */
|
|
336
|
-
export const TaskListQuerySchema = strictObject({
|
|
337
|
-
projectId: nonEmpty.optional(),
|
|
338
|
-
status: csv.optional(),
|
|
339
|
-
priority: csv.optional(),
|
|
340
|
-
assigneeId: nonEmpty.optional(),
|
|
341
|
-
iterationId: nonEmpty.optional(),
|
|
342
|
-
deliverableId: nonEmpty.optional(),
|
|
343
|
-
containerId: nonEmpty.optional(),
|
|
344
|
-
parentId: nonEmpty.transform((value) => (value === 'none' ? null : value)).optional(),
|
|
345
|
-
limit: pageLimit.optional(),
|
|
346
|
-
cursor: nonEmpty.optional()
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
/** `GET /activities` query parameters. */
|
|
350
|
-
export const ActivityListQuerySchema = strictObject({
|
|
351
|
-
projectId: nonEmpty.optional(),
|
|
352
|
-
taskId: nonEmpty.optional(),
|
|
353
|
-
limit: pageLimit.optional(),
|
|
354
|
-
cursor: nonEmpty.optional()
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// --- Webhooks ---
|
|
358
|
-
|
|
359
|
-
export const CreateWebhookSchema = strictObject({
|
|
360
|
-
name: nonEmpty,
|
|
361
|
-
/** http(s) URL; private and local addresses are rejected unless the engine allows them. */
|
|
362
|
-
url: nonEmpty,
|
|
363
|
-
/** Domain event names (e.g. `task.created`) or `'*'` for all events. */
|
|
364
|
-
events: z.array(z.enum(['*', ...DOMAIN_EVENT_NAMES])).min(1),
|
|
365
|
-
/** Signing secret (at least 16 characters). Generated when omitted and returned once. */
|
|
366
|
-
secret: z.string().min(16).optional(),
|
|
367
|
-
active: z.boolean().optional()
|
|
368
|
-
});
|
|
369
|
-
export const UpdateWebhookSchema = CreateWebhookSchema.partial();
|
|
370
|
-
|
|
371
|
-
export type CreateTaskPayload = z.infer<typeof CreateTaskSchema>;
|
|
372
|
-
export type UpdateTaskPayload = z.infer<typeof UpdateTaskSchema>;
|
|
373
|
-
export type CreateProjectPayload = z.infer<typeof CreateProjectSchema>;
|
|
374
|
-
export type UpdateProjectPayload = z.infer<typeof UpdateProjectSchema>;
|
|
375
|
-
|
|
376
|
-
/** Request body types (before defaults are applied), for typed API clients. */
|
|
377
|
-
export type CreateWorkflowBody = z.input<typeof CreateWorkflowSchema>;
|
|
378
|
-
export type UpdateWorkflowBody = z.input<typeof UpdateWorkflowSchema>;
|
|
379
|
-
export type CreateProjectBody = z.input<typeof CreateProjectSchema>;
|
|
380
|
-
export type UpdateProjectBody = z.input<typeof UpdateProjectSchema>;
|
|
381
|
-
export type CreateTaskBody = z.input<typeof CreateTaskSchema>;
|
|
382
|
-
export type UpdateTaskBody = z.input<typeof UpdateTaskSchema>;
|
|
383
|
-
export type CreateDependencyBody = z.input<typeof CreateDependencySchema>;
|
|
384
|
-
export type CreateDeliverableBody = z.input<typeof CreateDeliverableSchema>;
|
|
385
|
-
export type UpdateDeliverableBody = z.input<typeof UpdateDeliverableSchema>;
|
|
386
|
-
export type CreateTeamBody = z.input<typeof CreateTeamSchema>;
|
|
387
|
-
export type UpdateTeamBody = z.input<typeof UpdateTeamSchema>;
|
|
388
|
-
export type CreateContainerBody = z.input<typeof CreateContainerSchema>;
|
|
389
|
-
export type UpdateContainerBody = z.input<typeof UpdateContainerSchema>;
|
|
390
|
-
export type CreateIterationBody = z.input<typeof CreateIterationSchema>;
|
|
391
|
-
export type UpdateIterationBody = z.input<typeof UpdateIterationSchema>;
|
|
392
|
-
export type CreateCommentBody = z.input<typeof CreateCommentSchema>;
|
|
393
|
-
export type UpdateCommentBody = z.input<typeof UpdateCommentSchema>;
|
|
394
|
-
export type CommentReactionBody = z.input<typeof CommentReactionSchema>;
|
|
395
|
-
export type CreateAttachmentBody = z.input<typeof CreateAttachmentSchema>;
|
|
396
|
-
export type UploadAttachmentBody = z.input<typeof UploadAttachmentSchema>;
|
|
397
|
-
export type PresignAttachmentBody = z.input<typeof PresignAttachmentSchema>;
|
|
398
|
-
export type LogTimeBody = z.input<typeof LogTimeSchema>;
|
|
399
|
-
export type CreateWebhookBody = z.input<typeof CreateWebhookSchema>;
|
|
400
|
-
export type TaskListQueryParams = z.input<typeof TaskListQuerySchema>;
|
|
401
|
-
export type UpdateWebhookBody = z.input<typeof UpdateWebhookSchema>;
|
|
402
|
-
|
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
Webhook
|
|
18
|
-
} from '../types/index.js';
|
|
19
|
-
import {
|
|
20
|
-
CreateTaskSchema,
|
|
21
|
-
UpdateTaskSchema,
|
|
22
|
-
CreateProjectSchema,
|
|
23
|
-
UpdateProjectSchema,
|
|
24
|
-
CreateWorkflowSchema,
|
|
25
|
-
CreateDeliverableSchema,
|
|
26
|
-
UpdateDeliverableSchema,
|
|
27
|
-
CreateTeamSchema,
|
|
28
|
-
CreateContainerSchema,
|
|
29
|
-
UpdateContainerSchema,
|
|
30
|
-
CreateIterationSchema,
|
|
31
|
-
CreateCommentSchema,
|
|
32
|
-
UpdateCommentSchema,
|
|
33
|
-
CreateAttachmentSchema,
|
|
34
|
-
LogTimeSchema,
|
|
35
|
-
CreateWebhookSchema
|
|
36
|
-
} from './index.js';
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Compile-time check that a schema covers exactly the writable keys of a domain type. If a field
|
|
40
|
-
* is added to a type in `types/index.ts` but not to its schema, the build fails here instead of
|
|
41
|
-
* the field being silently stripped from API requests.
|
|
42
|
-
*/
|
|
43
|
-
type SameKeys<Schema, Expected> = [Exclude<keyof Schema, keyof Expected>, Exclude<keyof Expected, keyof Schema>] extends [
|
|
44
|
-
never,
|
|
45
|
-
never
|
|
46
|
-
]
|
|
47
|
-
? true
|
|
48
|
-
: { extraInSchema: Exclude<keyof Schema, keyof Expected>; missingFromSchema: Exclude<keyof Expected, keyof Schema> };
|
|
49
|
-
|
|
50
|
-
type Infer<S extends z.ZodTypeAny> = z.infer<S>;
|
|
51
|
-
// tenantId is stamped from the actor by the engine, never accepted from payloads.
|
|
52
|
-
type ServerAssigned = 'id' | 'createdAt' | 'updatedAt' | 'tenantId';
|
|
53
|
-
// Identity is resolved by the server (withActor), never accepted in payloads.
|
|
54
|
-
type CommentIdentity = 'authorId' | 'authorType';
|
|
55
|
-
type UploaderIdentity = 'uploaderId' | 'uploaderType';
|
|
56
|
-
|
|
57
|
-
const keyChecks: true[] = [
|
|
58
|
-
true as SameKeys<Infer<typeof CreateTaskSchema>, Omit<CreateTaskInput, 'key'>>,
|
|
59
|
-
true as SameKeys<Infer<typeof UpdateTaskSchema>, Omit<Task, ServerAssigned | 'key' | 'projectId'>>,
|
|
60
|
-
// `workflow` is a denormalised copy resolved from `workflowId`, so it is not writable.
|
|
61
|
-
true as SameKeys<Infer<typeof CreateProjectSchema>, Omit<CreateProjectInput, 'workflow' | 'tenantId'>>,
|
|
62
|
-
true as SameKeys<Infer<typeof UpdateProjectSchema>, Omit<Project, ServerAssigned | 'workflow'>>,
|
|
63
|
-
true as SameKeys<Infer<typeof CreateWorkflowSchema>, Omit<Workflow, ServerAssigned>>,
|
|
64
|
-
true as SameKeys<Infer<typeof CreateDeliverableSchema>, CreateDeliverableInput>,
|
|
65
|
-
true as SameKeys<Infer<typeof UpdateDeliverableSchema>, Omit<Deliverable, ServerAssigned | 'projectId'>>,
|
|
66
|
-
true as SameKeys<Infer<typeof CreateTeamSchema>, Omit<Team, ServerAssigned>>,
|
|
67
|
-
true as SameKeys<Infer<typeof CreateContainerSchema>, Omit<TaskContainer, ServerAssigned>>,
|
|
68
|
-
true as SameKeys<Infer<typeof UpdateContainerSchema>, Omit<TaskContainer, ServerAssigned | 'projectId'>>,
|
|
69
|
-
true as SameKeys<Infer<typeof CreateIterationSchema>, Omit<Iteration, 'id' | 'createdAt'>>,
|
|
70
|
-
// Reactions are managed through the reactions endpoints.
|
|
71
|
-
true as SameKeys<Infer<typeof CreateCommentSchema>, Omit<Comment, ServerAssigned | 'reactions' | CommentIdentity>>,
|
|
72
|
-
true as SameKeys<Infer<typeof UpdateCommentSchema>, Pick<Comment, 'content' | 'mentions' | 'metadata'>>,
|
|
73
|
-
true as SameKeys<Infer<typeof CreateAttachmentSchema>, Omit<CreateAttachmentInput, UploaderIdentity>>,
|
|
74
|
-
true as SameKeys<Infer<typeof LogTimeSchema>, Omit<TimeEntry, 'id' | 'userId'>>,
|
|
75
|
-
true as SameKeys<Infer<typeof CreateWebhookSchema>, Pick<Webhook, 'name' | 'url' | 'events' | 'secret' | 'active'>>
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
describe('request schemas', () => {
|
|
79
|
-
it('cover the writable fields of each domain type', () => {
|
|
80
|
-
expect(keyChecks.every(Boolean)).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('reject server-assigned, identity and unknown fields', () => {
|
|
84
|
-
for (const extra of [{ id: 'x' }, { projectId: 'other' }, { createdAt: '1999-01-01' }, { key: 'X-1' }, { junk: true }, { actorId: 'ceo' }]) {
|
|
85
|
-
const result = UpdateTaskSchema.safeParse({ title: 'ok', ...extra });
|
|
86
|
-
expect(result.success, JSON.stringify(extra)).toBe(false);
|
|
87
|
-
}
|
|
88
|
-
expect(CreateCommentSchema.safeParse({ taskId: 't1', content: 'hi', authorId: 'ceo' }).success).toBe(false);
|
|
89
|
-
// Nested objects are strict too
|
|
90
|
-
expect(UpdateTaskSchema.safeParse({ todos: [{ id: '1', title: 'a', completed: false, extra: 1 }] }).success).toBe(false);
|
|
91
|
-
expect(UpdateTaskSchema.parse({ title: 'ok' })).toEqual({ title: 'ok' });
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('reject wrong types and out-of-range values', () => {
|
|
95
|
-
expect(UpdateTaskSchema.safeParse({ status: 123 }).success).toBe(false);
|
|
96
|
-
expect(UpdateTaskSchema.safeParse({ progress: 140 }).success).toBe(false);
|
|
97
|
-
expect(CreateTaskSchema.safeParse({ projectId: 'p1' }).success).toBe(false);
|
|
98
|
-
expect(LogTimeSchema.safeParse({ taskId: 't1', hours: 0 }).success).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('apply defaults for optional collections and enums', () => {
|
|
102
|
-
expect(CreateTeamSchema.parse({ name: 'Core' }).memberIds).toEqual([]);
|
|
103
|
-
expect(CreateIterationSchema.parse({ projectId: 'p1', name: 'S1' }).status).toBe('planning');
|
|
104
|
-
expect(CreateWorkflowSchema.parse({ name: 'W', statuses: [] }).transitions).toEqual([]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('keep lifecycle fields writable for imports and backfills', () => {
|
|
108
|
-
const parsed = CreateTaskSchema.parse({
|
|
109
|
-
projectId: 'p1',
|
|
110
|
-
title: 'Imported',
|
|
111
|
-
completedAt: '2026-01-02T00:00:00.000Z',
|
|
112
|
-
actualStartDate: '2026-01-01T00:00:00.000Z'
|
|
113
|
-
});
|
|
114
|
-
expect(parsed.completedAt).toBe('2026-01-02T00:00:00.000Z');
|
|
115
|
-
});
|
|
116
|
-
});
|