@shipfox/annotations 12.0.0 → 12.3.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/db/annotations.d.ts +20 -0
- package/dist/db/annotations.d.ts.map +1 -1
- package/dist/db/annotations.js +51 -1
- package/dist/db/annotations.js.map +1 -1
- package/dist/db/db.d.ts +2 -2
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema/annotations.d.ts +2 -2
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +2 -0
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/read-annotation-summary.d.ts +2 -0
- package/dist/presentation/routes/read-annotation-summary.d.ts.map +1 -0
- package/dist/presentation/routes/read-annotation-summary.js +43 -0
- package/dist/presentation/routes/read-annotation-summary.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/db/annotations.ts +84 -1
- package/src/db/index.ts +10 -2
- package/src/presentation/routes/index.ts +2 -1
- package/src/presentation/routes/read-annotation-summary.ts +50 -0
- package/src/presentation/routes/read-annotations.test.ts +56 -1
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/annotations",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.3.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"drizzle-orm": "^0.45.2",
|
|
22
22
|
"zod": "^4.4.3",
|
|
23
|
-
"@shipfox/annotations-dto": "12.
|
|
24
|
-
"@shipfox/api-auth-context": "12.
|
|
23
|
+
"@shipfox/annotations-dto": "12.3.0",
|
|
24
|
+
"@shipfox/api-auth-context": "12.2.0",
|
|
25
25
|
"@shipfox/config": "1.2.4",
|
|
26
26
|
"@shipfox/inter-module": "0.2.3",
|
|
27
27
|
"@shipfox/node-drizzle": "0.3.5",
|
|
28
|
-
"@shipfox/node-fastify": "0.4.
|
|
29
|
-
"@shipfox/node-module": "1.0.
|
|
28
|
+
"@shipfox/node-fastify": "0.4.2",
|
|
29
|
+
"@shipfox/node-module": "1.0.6",
|
|
30
30
|
"@shipfox/node-postgres": "0.5.0"
|
|
31
31
|
},
|
|
32
32
|
"imports": {
|
package/src/db/annotations.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {type AnnotationStyleDto, READ_ANNOTATIONS_MAX_LIMIT} from '@shipfox/annotations-dto';
|
|
2
|
-
import {and, asc, eq, gt, inArray, or, type SQL, sql} from 'drizzle-orm';
|
|
2
|
+
import {and, asc, count, eq, gt, inArray, or, type SQL, sql} from 'drizzle-orm';
|
|
3
3
|
import type {Annotation} from '#core/entities/annotation.js';
|
|
4
4
|
import {db} from './db.js';
|
|
5
5
|
import {annotations, toAnnotation} from './schema/annotations.js';
|
|
@@ -61,6 +61,89 @@ export async function listAnnotationsForRunAttempt(
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export interface AnnotationSummary {
|
|
65
|
+
total: number;
|
|
66
|
+
error: number;
|
|
67
|
+
warning: number;
|
|
68
|
+
info: number;
|
|
69
|
+
success: number;
|
|
70
|
+
stepCounts: Array<{
|
|
71
|
+
originStepId: string;
|
|
72
|
+
originStepAttempt: number;
|
|
73
|
+
total: number;
|
|
74
|
+
}>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface SummarizeAnnotationsForRunAttemptParams {
|
|
78
|
+
workflowRunId: string;
|
|
79
|
+
workflowRunAttempt: number;
|
|
80
|
+
workspaceIds: readonly string[];
|
|
81
|
+
jobExecutionId?: string | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Count annotation styles without reading any annotation bodies. */
|
|
85
|
+
export async function summarizeAnnotationsForRunAttempt(
|
|
86
|
+
params: SummarizeAnnotationsForRunAttemptParams,
|
|
87
|
+
): Promise<AnnotationSummary> {
|
|
88
|
+
const summary: AnnotationSummary = {
|
|
89
|
+
total: 0,
|
|
90
|
+
error: 0,
|
|
91
|
+
warning: 0,
|
|
92
|
+
info: 0,
|
|
93
|
+
success: 0,
|
|
94
|
+
stepCounts: [],
|
|
95
|
+
};
|
|
96
|
+
if (params.workspaceIds.length === 0) return summary;
|
|
97
|
+
|
|
98
|
+
const conditions: SQL[] = [
|
|
99
|
+
eq(annotations.workflowRunId, params.workflowRunId),
|
|
100
|
+
eq(annotations.workflowRunAttempt, params.workflowRunAttempt),
|
|
101
|
+
inArray(annotations.workspaceId, [...params.workspaceIds]),
|
|
102
|
+
];
|
|
103
|
+
if (params.jobExecutionId) {
|
|
104
|
+
conditions.push(eq(annotations.jobExecutionId, params.jobExecutionId));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const {rows, stepRows} = await db().transaction(async (tx) => {
|
|
108
|
+
// Both aggregates must observe the same committed state. READ COMMITTED would allow a
|
|
109
|
+
// concurrent annotation write between these selects, producing contradictory totals.
|
|
110
|
+
await tx.execute(sql`set transaction isolation level repeatable read, read only`);
|
|
111
|
+
|
|
112
|
+
const rows = await tx
|
|
113
|
+
.select({style: annotations.style, count: count()})
|
|
114
|
+
.from(annotations)
|
|
115
|
+
.where(and(...conditions))
|
|
116
|
+
.groupBy(annotations.style);
|
|
117
|
+
|
|
118
|
+
const stepRows = await tx
|
|
119
|
+
.select({
|
|
120
|
+
originStepId: annotations.originStepId,
|
|
121
|
+
originStepAttempt: annotations.originStepAttempt,
|
|
122
|
+
total: count(),
|
|
123
|
+
})
|
|
124
|
+
.from(annotations)
|
|
125
|
+
.where(and(...conditions))
|
|
126
|
+
.groupBy(annotations.originStepId, annotations.originStepAttempt)
|
|
127
|
+
.orderBy(asc(annotations.originStepId), asc(annotations.originStepAttempt));
|
|
128
|
+
|
|
129
|
+
return {rows, stepRows};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
for (const row of rows) {
|
|
133
|
+
const value = Number(row.count);
|
|
134
|
+
summary.total += value;
|
|
135
|
+
if (row.style !== 'default') summary[row.style] += value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
summary.stepCounts = stepRows.map((row) => ({
|
|
139
|
+
originStepId: row.originStepId,
|
|
140
|
+
originStepAttempt: row.originStepAttempt,
|
|
141
|
+
total: Number(row.total),
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
return summary;
|
|
145
|
+
}
|
|
146
|
+
|
|
64
147
|
export interface StoredAnnotation {
|
|
65
148
|
id: string;
|
|
66
149
|
context: string;
|
package/src/db/index.ts
CHANGED
|
@@ -3,5 +3,13 @@ import {fileURLToPath} from 'node:url';
|
|
|
3
3
|
|
|
4
4
|
export const migrationsPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../drizzle');
|
|
5
5
|
|
|
6
|
-
export type {
|
|
7
|
-
|
|
6
|
+
export type {
|
|
7
|
+
AnnotationSummary,
|
|
8
|
+
ListAnnotationsForRunAttemptParams,
|
|
9
|
+
SummarizeAnnotationsForRunAttemptParams,
|
|
10
|
+
} from './annotations.js';
|
|
11
|
+
export {
|
|
12
|
+
DEFAULT_ANNOTATIONS_READ_LIMIT,
|
|
13
|
+
listAnnotationsForRunAttempt,
|
|
14
|
+
summarizeAnnotationsForRunAttempt,
|
|
15
|
+
} from './annotations.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {AUTH_LEASED_JOB, AUTH_USER} from '@shipfox/api-auth-context';
|
|
2
2
|
import type {RouteGroup} from '@shipfox/node-fastify';
|
|
3
|
+
import {readAnnotationSummaryRoute} from './read-annotation-summary.js';
|
|
3
4
|
import {readAnnotationsRoute} from './read-annotations.js';
|
|
4
5
|
import {writeAnnotationsRoute} from './write-annotations.js';
|
|
5
6
|
|
|
@@ -7,7 +8,7 @@ export const annotationsRoutes: RouteGroup[] = [
|
|
|
7
8
|
{
|
|
8
9
|
prefix: '/annotations',
|
|
9
10
|
auth: AUTH_USER,
|
|
10
|
-
routes: [readAnnotationsRoute],
|
|
11
|
+
routes: [readAnnotationSummaryRoute, readAnnotationsRoute],
|
|
11
12
|
},
|
|
12
13
|
{
|
|
13
14
|
prefix: '/runs/jobs/current',
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
annotationSummaryResponseSchema,
|
|
3
|
+
readAnnotationsQuerySchema,
|
|
4
|
+
} from '@shipfox/annotations-dto';
|
|
5
|
+
import {requireUserContext} from '@shipfox/api-auth-context';
|
|
6
|
+
import {defineRoute} from '@shipfox/node-fastify';
|
|
7
|
+
import {summarizeAnnotationsForRunAttempt} from '#db/index.js';
|
|
8
|
+
|
|
9
|
+
export const readAnnotationSummaryRoute = defineRoute({
|
|
10
|
+
method: 'GET',
|
|
11
|
+
path: '/summary',
|
|
12
|
+
description: 'Read annotation counts for a workflow run attempt.',
|
|
13
|
+
schema: {
|
|
14
|
+
querystring: readAnnotationsQuerySchema.omit({cursor: true, limit: true}),
|
|
15
|
+
response: {
|
|
16
|
+
200: annotationSummaryResponseSchema,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
handler: async (request) => {
|
|
20
|
+
const user = requireUserContext(request);
|
|
21
|
+
const {
|
|
22
|
+
workflow_run_id: workflowRunId,
|
|
23
|
+
attempt,
|
|
24
|
+
job_execution_id: jobExecutionId,
|
|
25
|
+
} = request.query;
|
|
26
|
+
const workspaceIds = user.memberships
|
|
27
|
+
.filter((membership) => membership.workspaceStatus === 'active')
|
|
28
|
+
.map((membership) => membership.workspaceId);
|
|
29
|
+
|
|
30
|
+
const summary = await summarizeAnnotationsForRunAttempt({
|
|
31
|
+
workflowRunId,
|
|
32
|
+
workflowRunAttempt: attempt,
|
|
33
|
+
workspaceIds,
|
|
34
|
+
jobExecutionId,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
total: summary.total,
|
|
39
|
+
error: summary.error,
|
|
40
|
+
warning: summary.warning,
|
|
41
|
+
info: summary.info,
|
|
42
|
+
success: summary.success,
|
|
43
|
+
step_counts: summary.stepCounts.map((step) => ({
|
|
44
|
+
origin_step_id: step.originStepId,
|
|
45
|
+
origin_step_attempt: step.originStepAttempt,
|
|
46
|
+
total: step.total,
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -2,6 +2,7 @@ import {AUTH_USER, buildUserContext, setUserContext} from '@shipfox/api-auth-con
|
|
|
2
2
|
import {type AuthMethod, ClientError, closeApp, createApp} from '@shipfox/node-fastify';
|
|
3
3
|
import type {FastifyRequest} from 'fastify';
|
|
4
4
|
import {annotationFactory} from '#test/index.js';
|
|
5
|
+
import {readAnnotationSummaryRoute} from './read-annotation-summary.js';
|
|
5
6
|
import {readAnnotationsRoute} from './read-annotations.js';
|
|
6
7
|
|
|
7
8
|
const fakeUserAuth: AuthMethod = {
|
|
@@ -42,7 +43,13 @@ describe('GET /annotations', () => {
|
|
|
42
43
|
beforeAll(async () => {
|
|
43
44
|
app = await createApp({
|
|
44
45
|
auth: [fakeUserAuth],
|
|
45
|
-
routes: [
|
|
46
|
+
routes: [
|
|
47
|
+
{
|
|
48
|
+
prefix: '/annotations',
|
|
49
|
+
auth: AUTH_USER,
|
|
50
|
+
routes: [readAnnotationSummaryRoute, readAnnotationsRoute],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
46
53
|
swagger: false,
|
|
47
54
|
});
|
|
48
55
|
await app.ready();
|
|
@@ -136,6 +143,54 @@ describe('GET /annotations', () => {
|
|
|
136
143
|
expect(res.json()).toEqual({annotations: [], has_more: false, next_cursor: null});
|
|
137
144
|
});
|
|
138
145
|
|
|
146
|
+
it('returns complete style counts without loading annotation bodies', async () => {
|
|
147
|
+
const workspaceId = crypto.randomUUID();
|
|
148
|
+
const workflowRunId = crypto.randomUUID();
|
|
149
|
+
const firstStepId = '11111111-1111-4111-8111-111111111111';
|
|
150
|
+
const secondStepId = '22222222-2222-4222-8222-222222222222';
|
|
151
|
+
await annotationFactory.create({
|
|
152
|
+
workspaceId,
|
|
153
|
+
workflowRunId,
|
|
154
|
+
originStepId: firstStepId,
|
|
155
|
+
context: 'default',
|
|
156
|
+
style: 'default',
|
|
157
|
+
});
|
|
158
|
+
await annotationFactory.create({
|
|
159
|
+
workspaceId,
|
|
160
|
+
workflowRunId,
|
|
161
|
+
originStepId: firstStepId,
|
|
162
|
+
context: 'error',
|
|
163
|
+
style: 'error',
|
|
164
|
+
});
|
|
165
|
+
await annotationFactory.create({
|
|
166
|
+
workspaceId,
|
|
167
|
+
workflowRunId,
|
|
168
|
+
originStepId: secondStepId,
|
|
169
|
+
originStepAttempt: 2,
|
|
170
|
+
context: 'warning',
|
|
171
|
+
style: 'warning',
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const res = await app.inject({
|
|
175
|
+
method: 'GET',
|
|
176
|
+
url: `/annotations/summary?workflow_run_id=${workflowRunId}&attempt=1`,
|
|
177
|
+
headers: {authorization: 'Bearer user', 'x-test-workspaces': workspaceId},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(res.statusCode).toBe(200);
|
|
181
|
+
expect(res.json()).toEqual({
|
|
182
|
+
total: 3,
|
|
183
|
+
error: 1,
|
|
184
|
+
warning: 1,
|
|
185
|
+
info: 0,
|
|
186
|
+
success: 0,
|
|
187
|
+
step_counts: [
|
|
188
|
+
{origin_step_id: firstStepId, origin_step_attempt: 1, total: 2},
|
|
189
|
+
{origin_step_id: secondStepId, origin_step_attempt: 2, total: 1},
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
139
194
|
it('returns an empty list for annotations outside the user workspaces', async () => {
|
|
140
195
|
const workflowRunId = crypto.randomUUID();
|
|
141
196
|
await annotationFactory.create({workflowRunId, workspaceId: crypto.randomUUID()});
|