@critical-path/core 0.29.0 → 0.31.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 +41 -0
- package/README.md +11 -1
- package/dist/engine/authorization.test.js +8 -4
- package/dist/engine/authorization.test.js.map +1 -1
- package/dist/engine/index.d.ts +26 -4
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +68 -13
- package/dist/engine/index.js.map +1 -1
- package/dist/schemas/index.d.ts +84 -33
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +29 -3
- package/dist/schemas/index.js.map +1 -1
- package/dist/storage/file-storage.d.ts +10 -0
- package/dist/storage/file-storage.d.ts.map +1 -1
- package/dist/storage/file-storage.js +40 -4
- package/dist/storage/file-storage.js.map +1 -1
- package/dist/storage/firebase-storage.d.ts.map +1 -1
- package/dist/storage/firebase-storage.js +10 -13
- package/dist/storage/firebase-storage.js.map +1 -1
- package/dist/storage/s3.d.ts +46 -18
- package/dist/storage/s3.d.ts.map +1 -1
- package/dist/storage/s3.js +59 -89
- package/dist/storage/s3.js.map +1 -1
- package/dist/storage/storage.test.js +88 -27
- package/dist/storage/storage.test.js.map +1 -1
- package/dist/store/firebase.d.ts +4 -0
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +10 -0
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/index.d.ts +8 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +10 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/query.d.ts +50 -0
- package/dist/store/query.d.ts.map +1 -0
- package/dist/store/query.js +79 -0
- package/dist/store/query.js.map +1 -0
- package/dist/store/query.test.d.ts +2 -0
- package/dist/store/query.test.d.ts.map +1 -0
- package/dist/store/query.test.js +89 -0
- package/dist/store/query.test.js.map +1 -0
- package/dist/store/sqlite.d.ts +3 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +90 -0
- package/dist/store/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/authorization.test.ts +13 -5
- package/src/engine/index.ts +73 -16
- package/src/schemas/index.ts +34 -3
- package/src/storage/file-storage.ts +44 -4
- package/src/storage/firebase-storage.ts +10 -13
- package/src/storage/s3.ts +98 -101
- package/src/storage/storage.test.ts +86 -28
- package/src/store/firebase.ts +14 -0
- package/src/store/index.ts +24 -0
- package/src/store/query.test.ts +110 -0
- package/src/store/query.ts +113 -0
- package/src/store/sqlite.ts +93 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Activity, Task } from '../types/index.js';
|
|
2
|
+
import { ValidationError } from '../domain/errors.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PAGE_SIZE = 100;
|
|
5
|
+
export const MAX_PAGE_SIZE = 500;
|
|
6
|
+
|
|
7
|
+
/** Filters for `queryTasks`. All given filters must match. */
|
|
8
|
+
export interface TaskQuery {
|
|
9
|
+
projectId?: string;
|
|
10
|
+
/** Restrict to these projects (used for authorization when no `projectId` is given). */
|
|
11
|
+
projectIds?: string[];
|
|
12
|
+
/** Any of these statuses. */
|
|
13
|
+
status?: string[];
|
|
14
|
+
/** Matches `assigneeId` or any entry in `assignees`. */
|
|
15
|
+
assigneeId?: string;
|
|
16
|
+
/** Any of these priorities. */
|
|
17
|
+
priority?: string[];
|
|
18
|
+
iterationId?: string;
|
|
19
|
+
deliverableId?: string;
|
|
20
|
+
containerId?: string;
|
|
21
|
+
/** A task id for its subtasks, or `null` for top-level tasks only. */
|
|
22
|
+
parentId?: string | null;
|
|
23
|
+
limit?: number;
|
|
24
|
+
/** Opaque cursor from a previous page's `nextCursor`. */
|
|
25
|
+
cursor?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ActivityQuery {
|
|
29
|
+
projectId?: string;
|
|
30
|
+
projectIds?: string[];
|
|
31
|
+
taskId?: string;
|
|
32
|
+
limit?: number;
|
|
33
|
+
cursor?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One page of results. `nextCursor` is set when more results exist. */
|
|
37
|
+
export interface Page<T> {
|
|
38
|
+
items: T[];
|
|
39
|
+
nextCursor?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Clamps a requested page size to `[1, MAX_PAGE_SIZE]`, defaulting to `DEFAULT_PAGE_SIZE`. */
|
|
43
|
+
export function pageSize(limit: number | undefined): number {
|
|
44
|
+
if (limit === undefined || !Number.isFinite(limit)) return DEFAULT_PAGE_SIZE;
|
|
45
|
+
return Math.min(Math.max(1, Math.floor(limit)), MAX_PAGE_SIZE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Cursors encode the last item's sort key and id (keyset pagination). */
|
|
49
|
+
export function encodeCursor(sortKey: string, id: string): string {
|
|
50
|
+
return btoa(JSON.stringify([sortKey, id])).replace(/=+$/, '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function decodeCursor(cursor: string | undefined): [string, string] | undefined {
|
|
54
|
+
if (!cursor) return undefined;
|
|
55
|
+
try {
|
|
56
|
+
const value = JSON.parse(atob(cursor));
|
|
57
|
+
if (Array.isArray(value) && value.length === 2 && value.every((v) => typeof v === 'string')) {
|
|
58
|
+
return value as [string, string];
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// fall through
|
|
62
|
+
}
|
|
63
|
+
throw new ValidationError('Invalid pagination cursor.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function matchesTaskQuery(task: Task, query: TaskQuery): boolean {
|
|
67
|
+
if (query.projectId && task.projectId !== query.projectId) return false;
|
|
68
|
+
if (query.projectIds && !query.projectIds.includes(task.projectId)) return false;
|
|
69
|
+
if (query.status?.length && !query.status.includes(task.status)) return false;
|
|
70
|
+
if (query.priority?.length && !query.priority.includes(task.priority)) return false;
|
|
71
|
+
if (query.iterationId && task.iterationId !== query.iterationId) return false;
|
|
72
|
+
if (query.deliverableId && task.deliverableId !== query.deliverableId) return false;
|
|
73
|
+
if (query.containerId && task.containerId !== query.containerId) return false;
|
|
74
|
+
if (query.parentId === null && task.parentId) return false;
|
|
75
|
+
if (query.parentId && task.parentId !== query.parentId) return false;
|
|
76
|
+
if (query.assigneeId && task.assigneeId !== query.assigneeId && !task.assignees?.some((a) => a.id === query.assigneeId)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function matchesActivityQuery(activity: Activity, query: ActivityQuery): boolean {
|
|
83
|
+
if (query.projectId && activity.projectId !== query.projectId) return false;
|
|
84
|
+
if (query.projectIds && (!activity.projectId || !query.projectIds.includes(activity.projectId))) return false;
|
|
85
|
+
if (query.taskId && activity.taskId !== query.taskId) return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sorts by `(sortKey, id)` and returns the page after `cursor`. Tasks page oldest first
|
|
91
|
+
* (`asc`), activity feeds newest first (`desc`).
|
|
92
|
+
*/
|
|
93
|
+
export function paginate<T extends { id: string }>(
|
|
94
|
+
items: T[],
|
|
95
|
+
sortKey: (item: T) => string,
|
|
96
|
+
direction: 'asc' | 'desc',
|
|
97
|
+
limit: number | undefined,
|
|
98
|
+
cursor: string | undefined
|
|
99
|
+
): Page<T> {
|
|
100
|
+
const sign = direction === 'asc' ? 1 : -1;
|
|
101
|
+
const compare = (a: [string, string], b: [string, string]) =>
|
|
102
|
+
sign * (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0);
|
|
103
|
+
const after = decodeCursor(cursor);
|
|
104
|
+
const sorted = [...items].sort((a, b) => compare([sortKey(a), a.id], [sortKey(b), b.id]));
|
|
105
|
+
const remaining = after ? sorted.filter((item) => compare([sortKey(item), item.id], after) > 0) : sorted;
|
|
106
|
+
const size = pageSize(limit);
|
|
107
|
+
const page = remaining.slice(0, size);
|
|
108
|
+
const last = page[page.length - 1];
|
|
109
|
+
return {
|
|
110
|
+
items: page,
|
|
111
|
+
nextCursor: remaining.length > size && last ? encodeCursor(sortKey(last), last.id) : undefined
|
|
112
|
+
};
|
|
113
|
+
}
|
package/src/store/sqlite.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import type { StorageAdapter } from './index.js';
|
|
3
|
+
import { decodeCursor, encodeCursor, pageSize, type ActivityQuery, type Page, type TaskQuery } from './query.js';
|
|
3
4
|
import type {
|
|
4
5
|
Project,
|
|
5
6
|
Task,
|
|
@@ -288,6 +289,17 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
288
289
|
// Column may already exist
|
|
289
290
|
}
|
|
290
291
|
|
|
292
|
+
// Indexes for filtered queries and keyset pagination
|
|
293
|
+
this.db.exec(`
|
|
294
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project_created ON tasks (projectId, createdAt, id);
|
|
295
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks (parentId);
|
|
296
|
+
CREATE INDEX IF NOT EXISTS idx_activities_project_created ON activities (projectId, createdAt, id);
|
|
297
|
+
CREATE INDEX IF NOT EXISTS idx_activities_task_created ON activities (taskId, createdAt, id);
|
|
298
|
+
CREATE INDEX IF NOT EXISTS idx_comments_task ON comments (taskId);
|
|
299
|
+
CREATE INDEX IF NOT EXISTS idx_dependencies_task ON dependencies (taskId);
|
|
300
|
+
CREATE INDEX IF NOT EXISTS idx_dependencies_upstream ON dependencies (dependsOnTaskId);
|
|
301
|
+
`);
|
|
302
|
+
|
|
291
303
|
for (const column of ['name TEXT', 'tenantId TEXT']) {
|
|
292
304
|
try {
|
|
293
305
|
this.db.exec(`ALTER TABLE webhooks ADD COLUMN ${column}`);
|
|
@@ -688,6 +700,55 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
688
700
|
return rows.map((r) => this.mapTask(r));
|
|
689
701
|
}
|
|
690
702
|
|
|
703
|
+
async queryTasks(query: TaskQuery): Promise<Page<Task>> {
|
|
704
|
+
const where: string[] = [];
|
|
705
|
+
const params: Array<string | number> = [];
|
|
706
|
+
const inList = (column: string, values: string[]) => {
|
|
707
|
+
where.push(`${column} IN (${values.map(() => '?').join(', ')})`);
|
|
708
|
+
params.push(...values);
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
if (query.projectId) {
|
|
712
|
+
where.push('projectId = ?');
|
|
713
|
+
params.push(query.projectId);
|
|
714
|
+
}
|
|
715
|
+
if (query.projectIds) {
|
|
716
|
+
if (query.projectIds.length === 0) return { items: [] };
|
|
717
|
+
inList('projectId', query.projectIds);
|
|
718
|
+
}
|
|
719
|
+
if (query.status?.length) inList('status', query.status);
|
|
720
|
+
if (query.priority?.length) inList('priority', query.priority);
|
|
721
|
+
for (const column of ['iterationId', 'deliverableId', 'containerId'] as const) {
|
|
722
|
+
if (query[column]) {
|
|
723
|
+
where.push(`${column} = ?`);
|
|
724
|
+
params.push(query[column]!);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (query.parentId === null) where.push('parentId IS NULL');
|
|
728
|
+
else if (query.parentId) {
|
|
729
|
+
where.push('parentId = ?');
|
|
730
|
+
params.push(query.parentId);
|
|
731
|
+
}
|
|
732
|
+
if (query.assigneeId) {
|
|
733
|
+
where.push(
|
|
734
|
+
"(assigneeId = ? OR EXISTS (SELECT 1 FROM json_each(COALESCE(assignees, '[]')) WHERE json_extract(value, '$.id') = ?))"
|
|
735
|
+
);
|
|
736
|
+
params.push(query.assigneeId, query.assigneeId);
|
|
737
|
+
}
|
|
738
|
+
const after = decodeCursor(query.cursor);
|
|
739
|
+
if (after) {
|
|
740
|
+
where.push('(createdAt > ? OR (createdAt = ? AND id > ?))');
|
|
741
|
+
params.push(after[0], after[0], after[1]);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const size = pageSize(query.limit);
|
|
745
|
+
const sql = `SELECT * FROM tasks ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY createdAt ASC, id ASC LIMIT ?`;
|
|
746
|
+
const rows = this.db.prepare(sql).all(...params, size + 1) as any[];
|
|
747
|
+
const items = rows.slice(0, size).map((r) => this.mapTask(r));
|
|
748
|
+
const last = items[items.length - 1];
|
|
749
|
+
return { items, nextCursor: rows.length > size && last ? encodeCursor(last.createdAt, last.id) : undefined };
|
|
750
|
+
}
|
|
751
|
+
|
|
691
752
|
async getTask(id: string): Promise<Task | null> {
|
|
692
753
|
const stmt = this.db.prepare('SELECT * FROM tasks WHERE id = ?');
|
|
693
754
|
const row = stmt.get(id) as any;
|
|
@@ -1124,6 +1185,38 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
1124
1185
|
);
|
|
1125
1186
|
}
|
|
1126
1187
|
|
|
1188
|
+
async queryActivities(query: ActivityQuery): Promise<Page<Activity>> {
|
|
1189
|
+
const where: string[] = [];
|
|
1190
|
+
const params: Array<string | number> = [];
|
|
1191
|
+
if (query.projectId) {
|
|
1192
|
+
where.push('projectId = ?');
|
|
1193
|
+
params.push(query.projectId);
|
|
1194
|
+
}
|
|
1195
|
+
if (query.projectIds) {
|
|
1196
|
+
if (query.projectIds.length === 0) return { items: [] };
|
|
1197
|
+
where.push(`projectId IN (${query.projectIds.map(() => '?').join(', ')})`);
|
|
1198
|
+
params.push(...query.projectIds);
|
|
1199
|
+
}
|
|
1200
|
+
if (query.taskId) {
|
|
1201
|
+
where.push('taskId = ?');
|
|
1202
|
+
params.push(query.taskId);
|
|
1203
|
+
}
|
|
1204
|
+
const after = decodeCursor(query.cursor);
|
|
1205
|
+
if (after) {
|
|
1206
|
+
where.push('(createdAt < ? OR (createdAt = ? AND id < ?))');
|
|
1207
|
+
params.push(after[0], after[0], after[1]);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const size = pageSize(query.limit);
|
|
1211
|
+
const sql = `SELECT * FROM activities ${where.length ? `WHERE ${where.join(' AND ')}` : ''} ORDER BY createdAt DESC, id DESC LIMIT ?`;
|
|
1212
|
+
const rows = this.db.prepare(sql).all(...params, size + 1) as any[];
|
|
1213
|
+
const items: Activity[] = rows
|
|
1214
|
+
.slice(0, size)
|
|
1215
|
+
.map((r) => dropNulls({ ...r, details: r.details ? JSON.parse(r.details) : undefined }));
|
|
1216
|
+
const last = items[items.length - 1];
|
|
1217
|
+
return { items, nextCursor: rows.length > size && last ? encodeCursor(last.createdAt, last.id) : undefined };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1127
1220
|
async logActivity(activity: Omit<Activity, 'id' | 'createdAt'>): Promise<Activity> {
|
|
1128
1221
|
const id = `act_${Math.random().toString(36).substring(2, 9)}`;
|
|
1129
1222
|
const now = new Date().toISOString();
|
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/authorization.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/authorization.test.ts","./src/engine/cascade.test.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/plugins/plugins.test.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","./src/webhooks/dispatcher.ts","./src/webhooks/index.ts","./src/webhooks/signature.ts","./src/webhooks/webhooks.test.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/authorization.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/authorization.test.ts","./src/engine/cascade.test.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/plugins/plugins.test.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/query.test.ts","./src/store/query.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","./src/webhooks/dispatcher.ts","./src/webhooks/index.ts","./src/webhooks/signature.ts","./src/webhooks/webhooks.test.ts"],"version":"5.9.3"}
|