@shipfox/annotations 17.0.0 → 19.0.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/.turbo/turbo-type.log +0 -1
- package/CHANGELOG.md +26 -0
- package/dist/core/write-annotations.d.ts.map +1 -1
- package/dist/core/write-annotations.js +69 -70
- package/dist/core/write-annotations.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +25 -0
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/core/write-annotations.ts +82 -69
- package/src/presentation/inter-module.test.ts +70 -0
- package/src/presentation/inter-module.ts +20 -0
- package/src/presentation/routes/read-annotations.test.ts +7 -3
- 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": "
|
|
4
|
+
"version": "19.0.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": "
|
|
24
|
-
"@shipfox/api-auth-context": "
|
|
23
|
+
"@shipfox/annotations-dto": "19.0.0",
|
|
24
|
+
"@shipfox/api-auth-context": "19.0.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
28
|
"@shipfox/node-fastify": "0.4.3",
|
|
29
|
-
"@shipfox/node-module": "1.0.
|
|
29
|
+
"@shipfox/node-module": "1.0.8",
|
|
30
30
|
"@shipfox/node-postgres": "0.5.1"
|
|
31
31
|
},
|
|
32
32
|
"imports": {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type {LeasedWriteAnnotationOperationDto} from '@shipfox/annotations-dto';
|
|
2
2
|
import {config} from '#config.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnnotationWriteRepository,
|
|
5
|
+
type StoredAnnotation,
|
|
6
|
+
withAnnotationLock,
|
|
7
|
+
} from '#db/annotations.js';
|
|
4
8
|
import {
|
|
5
9
|
AnnotationBodyTooLargeError,
|
|
6
10
|
AnnotationCountLimitExceededError,
|
|
@@ -28,88 +32,97 @@ export interface WriteAnnotationsResult {
|
|
|
28
32
|
};
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const results: WriteAnnotationsResult['annotations'] = [];
|
|
35
|
+
interface AnnotationWriteState {
|
|
36
|
+
current: Map<string, StoredAnnotation>;
|
|
37
|
+
nextSequence: number;
|
|
38
|
+
results: WriteAnnotationsResult['annotations'];
|
|
39
|
+
}
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
async function applyAnnotationOperation(
|
|
42
|
+
repo: AnnotationWriteRepository,
|
|
43
|
+
params: WriteAnnotationsParams,
|
|
44
|
+
operation: LeasedWriteAnnotationOperationDto,
|
|
45
|
+
state: AnnotationWriteState,
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
if (operation.op === 'remove') {
|
|
48
|
+
await repo.removeAnnotation(params.jobExecutionId, operation.context);
|
|
49
|
+
state.current.delete(operation.context);
|
|
50
|
+
state.results.push({context: operation.context, id: null});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
const existing = state.current.get(operation.context);
|
|
55
|
+
const body =
|
|
56
|
+
operation.op === 'append' ? `${existing?.body ?? ''}${operation.body}` : operation.body;
|
|
57
|
+
const bodyBytes = Buffer.byteLength(body);
|
|
58
|
+
ensureBodyBudget(bodyBytes);
|
|
59
|
+
const unchanged =
|
|
60
|
+
operation.op === 'replace' &&
|
|
61
|
+
existing !== undefined &&
|
|
62
|
+
existing.body === body &&
|
|
63
|
+
existing.style === operation.style;
|
|
64
|
+
if (unchanged) {
|
|
65
|
+
state.results.push({context: operation.context, id: existing.id});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
const draft = new Map(state.current);
|
|
70
|
+
draft.set(operation.context, {
|
|
71
|
+
id: existing?.id ?? '',
|
|
72
|
+
context: operation.context,
|
|
73
|
+
style: operation.style,
|
|
74
|
+
body,
|
|
75
|
+
bodyBytes,
|
|
76
|
+
sequence: existing?.sequence ?? state.nextSequence,
|
|
77
|
+
});
|
|
78
|
+
ensureExecutionBudgets(draft);
|
|
61
79
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
id: existing
|
|
80
|
+
const row = existing
|
|
81
|
+
? await repo.updateAnnotation({
|
|
82
|
+
id: existing.id,
|
|
83
|
+
originStepId: params.originStepId,
|
|
84
|
+
originStepAttempt: params.originStepAttempt,
|
|
85
|
+
style: operation.style,
|
|
86
|
+
body,
|
|
87
|
+
bodyBytes,
|
|
88
|
+
})
|
|
89
|
+
: await repo.createAnnotation({
|
|
90
|
+
workspaceId: params.workspaceId,
|
|
91
|
+
projectId: params.projectId,
|
|
92
|
+
workflowRunId: params.workflowRunId,
|
|
93
|
+
workflowRunAttempt: params.workflowRunAttempt,
|
|
94
|
+
workflowRunAttemptId: params.workflowRunAttemptId,
|
|
95
|
+
jobId: params.jobId,
|
|
96
|
+
jobExecutionId: params.jobExecutionId,
|
|
97
|
+
originStepId: params.originStepId,
|
|
98
|
+
originStepAttempt: params.originStepAttempt,
|
|
65
99
|
context: operation.context,
|
|
66
100
|
style: operation.style,
|
|
67
101
|
body,
|
|
68
102
|
bodyBytes,
|
|
69
|
-
sequence:
|
|
103
|
+
sequence: state.nextSequence,
|
|
70
104
|
});
|
|
71
|
-
|
|
105
|
+
state.current.set(operation.context, row);
|
|
106
|
+
if (!existing) state.nextSequence += 1;
|
|
107
|
+
state.results.push({context: operation.context, id: row.id});
|
|
108
|
+
}
|
|
72
109
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
: await repo.createAnnotation({
|
|
83
|
-
workspaceId: params.workspaceId,
|
|
84
|
-
projectId: params.projectId,
|
|
85
|
-
workflowRunId: params.workflowRunId,
|
|
86
|
-
workflowRunAttempt: params.workflowRunAttempt,
|
|
87
|
-
workflowRunAttemptId: params.workflowRunAttemptId,
|
|
88
|
-
jobId: params.jobId,
|
|
89
|
-
jobExecutionId: params.jobExecutionId,
|
|
90
|
-
originStepId: params.originStepId,
|
|
91
|
-
originStepAttempt: params.originStepAttempt,
|
|
92
|
-
context: operation.context,
|
|
93
|
-
style: operation.style,
|
|
94
|
-
body,
|
|
95
|
-
bodyBytes,
|
|
96
|
-
sequence: nextSequence,
|
|
97
|
-
});
|
|
110
|
+
export function writeAnnotations(params: WriteAnnotationsParams): Promise<WriteAnnotationsResult> {
|
|
111
|
+
return withAnnotationLock(params.jobExecutionId, async (repo) => {
|
|
112
|
+
const current = await repo.loadCurrentAnnotations(params.jobExecutionId);
|
|
113
|
+
const state: AnnotationWriteState = {
|
|
114
|
+
current,
|
|
115
|
+
nextSequence:
|
|
116
|
+
Math.max(0, ...Array.from(current.values()).map((annotation) => annotation.sequence)) + 1,
|
|
117
|
+
results: [],
|
|
118
|
+
};
|
|
98
119
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
context: row.context,
|
|
102
|
-
style: row.style,
|
|
103
|
-
body: row.body,
|
|
104
|
-
bodyBytes: row.bodyBytes,
|
|
105
|
-
sequence: row.sequence,
|
|
106
|
-
});
|
|
107
|
-
if (!existing) nextSequence += 1;
|
|
108
|
-
results.push({context: operation.context, id: row.id});
|
|
120
|
+
for (const operation of params.operations) {
|
|
121
|
+
await applyAnnotationOperation(repo, params, operation, state);
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
return {
|
|
112
|
-
annotations: results,
|
|
125
|
+
annotations: state.results,
|
|
113
126
|
accounting: currentAccounting(current),
|
|
114
127
|
};
|
|
115
128
|
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '#core/errors.js';
|
|
9
9
|
import {db} from '#db/db.js';
|
|
10
10
|
import {annotations} from '#db/schema/annotations.js';
|
|
11
|
+
import {annotationFactory} from '#test/index.js';
|
|
11
12
|
import {
|
|
12
13
|
createAnnotationsInterModulePresentation,
|
|
13
14
|
toReplaceOrRemoveAnnotationKnownError,
|
|
@@ -29,6 +30,75 @@ function input() {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
describe('Annotations inter-module presentation', () => {
|
|
33
|
+
test('lists only annotations owned by the requested workspace with pagination', async () => {
|
|
34
|
+
const workspaceId = crypto.randomUUID();
|
|
35
|
+
const workflowRunId = crypto.randomUUID();
|
|
36
|
+
const visible = await annotationFactory.create({
|
|
37
|
+
workspaceId,
|
|
38
|
+
workflowRunId,
|
|
39
|
+
context: 'visible',
|
|
40
|
+
sequence: 1,
|
|
41
|
+
});
|
|
42
|
+
const next = await annotationFactory.create({
|
|
43
|
+
workspaceId,
|
|
44
|
+
workflowRunId,
|
|
45
|
+
context: 'next',
|
|
46
|
+
sequence: 2,
|
|
47
|
+
});
|
|
48
|
+
await annotationFactory.create({
|
|
49
|
+
workspaceId,
|
|
50
|
+
workflowRunId,
|
|
51
|
+
context: 'after',
|
|
52
|
+
sequence: 3,
|
|
53
|
+
});
|
|
54
|
+
await annotationFactory.create({
|
|
55
|
+
workspaceId: crypto.randomUUID(),
|
|
56
|
+
workflowRunId,
|
|
57
|
+
context: 'hidden',
|
|
58
|
+
sequence: 4,
|
|
59
|
+
});
|
|
60
|
+
const presentation = createAnnotationsInterModulePresentation();
|
|
61
|
+
|
|
62
|
+
const result = await presentation.handlers.listAnnotationsForRunAttempt(
|
|
63
|
+
{
|
|
64
|
+
workspaceId,
|
|
65
|
+
workflowRunId,
|
|
66
|
+
workflowRunAttempt: 1,
|
|
67
|
+
limit: 2,
|
|
68
|
+
},
|
|
69
|
+
{signal: new AbortController().signal},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(result).toEqual({
|
|
73
|
+
annotations: [
|
|
74
|
+
{
|
|
75
|
+
id: visible.id,
|
|
76
|
+
job_id: visible.jobId,
|
|
77
|
+
job_execution_id: visible.jobExecutionId,
|
|
78
|
+
origin_step_id: visible.originStepId,
|
|
79
|
+
origin_step_attempt: visible.originStepAttempt,
|
|
80
|
+
context: 'visible',
|
|
81
|
+
style: visible.style,
|
|
82
|
+
sequence: 1,
|
|
83
|
+
body: visible.body,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: next.id,
|
|
87
|
+
job_id: next.jobId,
|
|
88
|
+
job_execution_id: next.jobExecutionId,
|
|
89
|
+
origin_step_id: next.originStepId,
|
|
90
|
+
origin_step_attempt: next.originStepAttempt,
|
|
91
|
+
context: 'next',
|
|
92
|
+
style: next.style,
|
|
93
|
+
sequence: 2,
|
|
94
|
+
body: next.body,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
hasMore: true,
|
|
98
|
+
nextCursor: {value: next.sequence, id: next.id},
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
32
102
|
test('replaces and removes a warning annotation through PostgreSQL', async () => {
|
|
33
103
|
const presentation = createAnnotationsInterModulePresentation();
|
|
34
104
|
const target = input();
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
AnnotationTotalBytesLimitExceededError,
|
|
11
11
|
} from '#core/errors.js';
|
|
12
12
|
import {writeAnnotations} from '#core/write-annotations.js';
|
|
13
|
+
import {listAnnotationsForRunAttempt} from '#db/index.js';
|
|
14
|
+
import {toAnnotationDto} from './dto/annotation.js';
|
|
13
15
|
|
|
14
16
|
export function createAnnotationsInterModulePresentation(): InterModulePresentation<
|
|
15
17
|
typeof annotationsInterModuleContract
|
|
@@ -31,6 +33,24 @@ export function createAnnotationsInterModulePresentation(): InterModulePresentat
|
|
|
31
33
|
throw toReplaceOrRemoveAnnotationKnownError(error);
|
|
32
34
|
}
|
|
33
35
|
},
|
|
36
|
+
listAnnotationsForRunAttempt: async (input) => {
|
|
37
|
+
const result = await listAnnotationsForRunAttempt({
|
|
38
|
+
workflowRunId: input.workflowRunId,
|
|
39
|
+
workflowRunAttempt: input.workflowRunAttempt,
|
|
40
|
+
workspaceIds: [input.workspaceId],
|
|
41
|
+
jobExecutionId: input.jobExecutionId,
|
|
42
|
+
after: input.cursor ? {sequence: input.cursor.value, id: input.cursor.id} : undefined,
|
|
43
|
+
limit: input.limit,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
annotations: result.annotations.map(toAnnotationDto),
|
|
48
|
+
hasMore: result.hasMore,
|
|
49
|
+
nextCursor: result.nextCursor
|
|
50
|
+
? {value: result.nextCursor.sequence, id: result.nextCursor.id}
|
|
51
|
+
: null,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
34
54
|
});
|
|
35
55
|
}
|
|
36
56
|
|
|
@@ -5,6 +5,12 @@ import {annotationFactory} from '#test/index.js';
|
|
|
5
5
|
import {readAnnotationSummaryRoute} from './read-annotation-summary.js';
|
|
6
6
|
import {readAnnotationsRoute} from './read-annotations.js';
|
|
7
7
|
|
|
8
|
+
function workspaceStatus(value: string | undefined): 'active' | 'suspended' | 'deleted' {
|
|
9
|
+
if (value === 'suspended') return 'suspended';
|
|
10
|
+
if (value === 'deleted') return 'deleted';
|
|
11
|
+
return 'active';
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
const fakeUserAuth: AuthMethod = {
|
|
9
15
|
name: AUTH_USER,
|
|
10
16
|
authenticate: (request: FastifyRequest) => {
|
|
@@ -20,9 +26,7 @@ const fakeUserAuth: AuthMethod = {
|
|
|
20
26
|
.map((value) => {
|
|
21
27
|
const [workspaceId, status] = value.split('|');
|
|
22
28
|
if (!workspaceId) throw new Error('missing test workspace id');
|
|
23
|
-
|
|
24
|
-
status === 'suspended' ? 'suspended' : status === 'deleted' ? 'deleted' : 'active';
|
|
25
|
-
return {workspaceId, role: 'admin' as const, workspaceStatus};
|
|
29
|
+
return {workspaceId, role: 'admin' as const, workspaceStatus: workspaceStatus(status)};
|
|
26
30
|
});
|
|
27
31
|
|
|
28
32
|
setUserContext(
|