@shipfox/annotations 2.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.
Files changed (109) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +67 -0
  5. package/LICENSE +21 -0
  6. package/dist/config.d.ts +6 -0
  7. package/dist/config.d.ts.map +1 -0
  8. package/dist/config.js +26 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/core/entities/annotation.d.ts +21 -0
  11. package/dist/core/entities/annotation.d.ts.map +1 -0
  12. package/dist/core/entities/annotation.js +3 -0
  13. package/dist/core/entities/annotation.js.map +1 -0
  14. package/dist/core/entities/index.d.ts +2 -0
  15. package/dist/core/entities/index.d.ts.map +1 -0
  16. package/dist/core/entities/index.js +3 -0
  17. package/dist/core/entities/index.js.map +1 -0
  18. package/dist/core/errors.d.ts +13 -0
  19. package/dist/core/errors.d.ts.map +1 -0
  20. package/dist/core/errors.js +20 -0
  21. package/dist/core/errors.js.map +1 -0
  22. package/dist/core/write-annotations.d.ts +25 -0
  23. package/dist/core/write-annotations.d.ts.map +1 -0
  24. package/dist/core/write-annotations.js +105 -0
  25. package/dist/core/write-annotations.js.map +1 -0
  26. package/dist/db/annotations.d.ts +63 -0
  27. package/dist/db/annotations.d.ts.map +1 -0
  28. package/dist/db/annotations.js +96 -0
  29. package/dist/db/annotations.js.map +1 -0
  30. package/dist/db/db.d.ts +304 -0
  31. package/dist/db/db.d.ts.map +1 -0
  32. package/dist/db/db.js +18 -0
  33. package/dist/db/db.js.map +1 -0
  34. package/dist/db/index.d.ts +4 -0
  35. package/dist/db/index.d.ts.map +1 -0
  36. package/dist/db/index.js +6 -0
  37. package/dist/db/index.js.map +1 -0
  38. package/dist/db/schema/annotations.d.ts +302 -0
  39. package/dist/db/schema/annotations.d.ts.map +1 -0
  40. package/dist/db/schema/annotations.js +67 -0
  41. package/dist/db/schema/annotations.js.map +1 -0
  42. package/dist/db/schema/common.d.ts +2 -0
  43. package/dist/db/schema/common.d.ts.map +1 -0
  44. package/dist/db/schema/common.js +4 -0
  45. package/dist/db/schema/common.js.map +1 -0
  46. package/dist/index.d.ts +5 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +15 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/presentation/dto/annotation.d.ts +4 -0
  51. package/dist/presentation/dto/annotation.d.ts.map +1 -0
  52. package/dist/presentation/dto/annotation.js +15 -0
  53. package/dist/presentation/dto/annotation.js.map +1 -0
  54. package/dist/presentation/dto/index.d.ts +2 -0
  55. package/dist/presentation/dto/index.d.ts.map +1 -0
  56. package/dist/presentation/dto/index.js +3 -0
  57. package/dist/presentation/dto/index.js.map +1 -0
  58. package/dist/presentation/routes/index.d.ts +3 -0
  59. package/dist/presentation/routes/index.d.ts.map +1 -0
  60. package/dist/presentation/routes/index.js +21 -0
  61. package/dist/presentation/routes/index.js.map +1 -0
  62. package/dist/presentation/routes/read-annotations.d.ts +2 -0
  63. package/dist/presentation/routes/read-annotations.d.ts.map +1 -0
  64. package/dist/presentation/routes/read-annotations.js +49 -0
  65. package/dist/presentation/routes/read-annotations.js.map +1 -0
  66. package/dist/presentation/routes/write-annotations.d.ts +2 -0
  67. package/dist/presentation/routes/write-annotations.d.ts.map +1 -0
  68. package/dist/presentation/routes/write-annotations.js +73 -0
  69. package/dist/presentation/routes/write-annotations.js.map +1 -0
  70. package/dist/tsconfig.test.tsbuildinfo +1 -0
  71. package/drizzle/0000_initial.sql +32 -0
  72. package/drizzle/meta/0000_snapshot.json +247 -0
  73. package/drizzle/meta/_journal.json +13 -0
  74. package/drizzle.config.ts +7 -0
  75. package/package.json +69 -0
  76. package/src/config.test.ts +45 -0
  77. package/src/config.ts +26 -0
  78. package/src/core/entities/annotation.ts +21 -0
  79. package/src/core/entities/index.ts +1 -0
  80. package/src/core/errors.ts +20 -0
  81. package/src/core/write-annotations.test.ts +261 -0
  82. package/src/core/write-annotations.ts +142 -0
  83. package/src/db/annotations.test.ts +261 -0
  84. package/src/db/annotations.ts +180 -0
  85. package/src/db/db.ts +19 -0
  86. package/src/db/index.ts +7 -0
  87. package/src/db/schema/annotations.test.ts +50 -0
  88. package/src/db/schema/annotations.ts +96 -0
  89. package/src/db/schema/common.ts +3 -0
  90. package/src/index.ts +17 -0
  91. package/src/presentation/dto/annotation.test.ts +42 -0
  92. package/src/presentation/dto/annotation.ts +16 -0
  93. package/src/presentation/dto/index.ts +1 -0
  94. package/src/presentation/routes/index.ts +17 -0
  95. package/src/presentation/routes/read-annotations.test.ts +279 -0
  96. package/src/presentation/routes/read-annotations.ts +50 -0
  97. package/src/presentation/routes/write-annotations.test.ts +188 -0
  98. package/src/presentation/routes/write-annotations.ts +76 -0
  99. package/test/env.ts +5 -0
  100. package/test/factories/annotation.ts +57 -0
  101. package/test/fixtures/lease-token.ts +43 -0
  102. package/test/globalSetup.ts +20 -0
  103. package/test/index.ts +2 -0
  104. package/test/setup.ts +17 -0
  105. package/tsconfig.build.json +9 -0
  106. package/tsconfig.build.tsbuildinfo +1 -0
  107. package/tsconfig.json +3 -0
  108. package/tsconfig.test.json +8 -0
  109. package/vitest.config.ts +13 -0
@@ -0,0 +1,180 @@
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';
3
+ import type {Annotation} from '#core/entities/annotation.js';
4
+ import {db} from './db.js';
5
+ import {annotations, toAnnotation} from './schema/annotations.js';
6
+
7
+ export const DEFAULT_ANNOTATIONS_READ_LIMIT = READ_ANNOTATIONS_MAX_LIMIT;
8
+
9
+ export interface ListAnnotationsForRunAttemptParams {
10
+ workflowRunId: string;
11
+ workflowRunAttempt: number;
12
+ workspaceIds: readonly string[];
13
+ jobExecutionId?: string | undefined;
14
+ after?: {sequence: number; id: string} | undefined;
15
+ limit?: number | undefined;
16
+ }
17
+
18
+ export interface ListAnnotationsForRunAttemptResult {
19
+ annotations: Annotation[];
20
+ hasMore: boolean;
21
+ nextCursor: {sequence: number; id: string} | null;
22
+ }
23
+
24
+ export async function listAnnotationsForRunAttempt(
25
+ params: ListAnnotationsForRunAttemptParams,
26
+ ): Promise<ListAnnotationsForRunAttemptResult> {
27
+ if (params.workspaceIds.length === 0) return {annotations: [], hasMore: false, nextCursor: null};
28
+
29
+ const limit = params.limit ?? DEFAULT_ANNOTATIONS_READ_LIMIT;
30
+
31
+ const conditions: SQL[] = [
32
+ eq(annotations.workflowRunId, params.workflowRunId),
33
+ eq(annotations.workflowRunAttempt, params.workflowRunAttempt),
34
+ inArray(annotations.workspaceId, [...params.workspaceIds]),
35
+ ];
36
+ if (params.jobExecutionId) {
37
+ conditions.push(eq(annotations.jobExecutionId, params.jobExecutionId));
38
+ }
39
+ if (params.after) {
40
+ const cursorCondition = or(
41
+ gt(annotations.sequence, params.after.sequence),
42
+ and(eq(annotations.sequence, params.after.sequence), gt(annotations.id, params.after.id)),
43
+ );
44
+ if (cursorCondition) conditions.push(cursorCondition);
45
+ }
46
+
47
+ const rows = await db()
48
+ .select()
49
+ .from(annotations)
50
+ .where(and(...conditions))
51
+ .orderBy(asc(annotations.sequence), asc(annotations.id))
52
+ .limit(limit + 1);
53
+
54
+ const pageRows = rows.slice(0, limit);
55
+ const last = pageRows.at(-1);
56
+
57
+ return {
58
+ annotations: pageRows.map(toAnnotation),
59
+ hasMore: rows.length > limit,
60
+ nextCursor: rows.length > limit && last ? {sequence: last.sequence, id: last.id} : null,
61
+ };
62
+ }
63
+
64
+ export interface StoredAnnotation {
65
+ id: string;
66
+ context: string;
67
+ style: AnnotationStyleDto;
68
+ body: string;
69
+ bodyBytes: number;
70
+ sequence: number;
71
+ }
72
+
73
+ export interface CreateAnnotationParams {
74
+ workspaceId: string;
75
+ projectId: string;
76
+ workflowRunId: string;
77
+ workflowRunAttempt: number;
78
+ workflowRunAttemptId: string;
79
+ jobId: string;
80
+ jobExecutionId: string;
81
+ originStepId: string;
82
+ originStepAttempt: number;
83
+ context: string;
84
+ style: AnnotationStyleDto;
85
+ body: string;
86
+ bodyBytes: number;
87
+ sequence: number;
88
+ }
89
+
90
+ export interface UpdateAnnotationParams {
91
+ id: string;
92
+ originStepId: string;
93
+ originStepAttempt: number;
94
+ style: AnnotationStyleDto;
95
+ body: string;
96
+ bodyBytes: number;
97
+ }
98
+
99
+ export interface AnnotationWriteRepository {
100
+ loadCurrentAnnotations(jobExecutionId: string): Promise<Map<string, StoredAnnotation>>;
101
+ removeAnnotation(jobExecutionId: string, context: string): Promise<void>;
102
+ createAnnotation(params: CreateAnnotationParams): Promise<StoredAnnotation>;
103
+ updateAnnotation(params: UpdateAnnotationParams): Promise<StoredAnnotation>;
104
+ }
105
+
106
+ export function withAnnotationLock<T>(
107
+ jobExecutionId: string,
108
+ work: (repo: AnnotationWriteRepository) => Promise<T>,
109
+ ): Promise<T> {
110
+ return db().transaction(async (tx) => {
111
+ await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${jobExecutionId}))`);
112
+
113
+ const repo: AnnotationWriteRepository = {
114
+ loadCurrentAnnotations: async (lockedJobExecutionId) => {
115
+ const rows = await tx
116
+ .select({
117
+ id: annotations.id,
118
+ context: annotations.context,
119
+ style: annotations.style,
120
+ body: annotations.body,
121
+ bodyBytes: annotations.bodyBytes,
122
+ sequence: annotations.sequence,
123
+ })
124
+ .from(annotations)
125
+ .where(eq(annotations.jobExecutionId, lockedJobExecutionId));
126
+
127
+ return new Map(rows.map((row) => [row.context, row]));
128
+ },
129
+ removeAnnotation: async (lockedJobExecutionId, context) => {
130
+ await tx
131
+ .delete(annotations)
132
+ .where(
133
+ and(
134
+ eq(annotations.jobExecutionId, lockedJobExecutionId),
135
+ eq(annotations.context, context),
136
+ ),
137
+ );
138
+ },
139
+ createAnnotation: async (params) => {
140
+ const [row] = await tx.insert(annotations).values(params).returning({
141
+ id: annotations.id,
142
+ context: annotations.context,
143
+ style: annotations.style,
144
+ body: annotations.body,
145
+ bodyBytes: annotations.bodyBytes,
146
+ sequence: annotations.sequence,
147
+ });
148
+
149
+ if (!row) throw new Error('createAnnotation: insert returned no row');
150
+ return row;
151
+ },
152
+ updateAnnotation: async (params) => {
153
+ const [row] = await tx
154
+ .update(annotations)
155
+ .set({
156
+ originStepId: params.originStepId,
157
+ originStepAttempt: params.originStepAttempt,
158
+ style: params.style,
159
+ body: params.body,
160
+ bodyBytes: params.bodyBytes,
161
+ updatedAt: new Date(),
162
+ })
163
+ .where(eq(annotations.id, params.id))
164
+ .returning({
165
+ id: annotations.id,
166
+ context: annotations.context,
167
+ style: annotations.style,
168
+ body: annotations.body,
169
+ bodyBytes: annotations.bodyBytes,
170
+ sequence: annotations.sequence,
171
+ });
172
+
173
+ if (!row) throw new Error('updateAnnotation: update returned no row');
174
+ return row;
175
+ },
176
+ };
177
+
178
+ return await work(repo);
179
+ });
180
+ }
package/src/db/db.ts ADDED
@@ -0,0 +1,19 @@
1
+ import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
2
+ import {pgClient} from '@shipfox/node-postgres';
3
+ import {annotations} from './schema/annotations.js';
4
+
5
+ export const schema = {annotations};
6
+
7
+ export type Database = NodePgDatabase<typeof schema>;
8
+ export type Transaction = Parameters<Parameters<Database['transaction']>[0]>[0];
9
+
10
+ let _db: Database | undefined;
11
+
12
+ export function db() {
13
+ if (!_db) _db = drizzle(pgClient(), {schema});
14
+ return _db;
15
+ }
16
+
17
+ export function closeDb(): void {
18
+ _db = undefined;
19
+ }
@@ -0,0 +1,7 @@
1
+ import {dirname, resolve} from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+
4
+ export const migrationsPath = resolve(dirname(fileURLToPath(import.meta.url)), '../../drizzle');
5
+
6
+ export type {ListAnnotationsForRunAttemptParams} from './annotations.js';
7
+ export {DEFAULT_ANNOTATIONS_READ_LIMIT, listAnnotationsForRunAttempt} from './annotations.js';
@@ -0,0 +1,50 @@
1
+ import {eq} from 'drizzle-orm';
2
+ import {db} from '#db/db.js';
3
+ import {annotationFactory} from '#test/index.js';
4
+ import {annotations, toAnnotation} from './annotations.js';
5
+
6
+ describe('annotations schema', () => {
7
+ it('maps a persisted annotation row to the domain entity', async () => {
8
+ const createdAt = new Date('2026-07-07T10:00:00.000Z');
9
+ const updatedAt = new Date('2026-07-07T10:01:00.000Z');
10
+ const annotation = await annotationFactory.create({
11
+ id: '11111111-1111-4111-8111-111111111111',
12
+ workspaceId: '22222222-2222-4222-8222-222222222222',
13
+ projectId: '33333333-3333-4333-8333-333333333333',
14
+ workflowRunId: '44444444-4444-4444-8444-444444444444',
15
+ workflowRunAttempt: 4,
16
+ workflowRunAttemptId: '55555555-5555-4555-8555-555555555555',
17
+ jobId: '66666666-6666-4666-8666-666666666666',
18
+ jobExecutionId: '77777777-7777-4777-8777-777777777777',
19
+ originStepId: '88888888-8888-4888-8888-888888888888',
20
+ originStepAttempt: 3,
21
+ context: 'deploy',
22
+ style: 'success',
23
+ body: 'Deployed **v42** to staging',
24
+ bodyBytes: Buffer.byteLength('Deployed **v42** to staging'),
25
+ sequence: 2,
26
+ createdAt,
27
+ updatedAt,
28
+ });
29
+
30
+ const [row] = await db().select().from(annotations).where(eq(annotations.id, annotation.id));
31
+
32
+ expect(row).toBeDefined();
33
+ expect(row ? toAnnotation(row) : undefined).toEqual(annotation);
34
+ });
35
+
36
+ it.each([
37
+ ' deploy ',
38
+ '\tdeploy\n',
39
+ ])('rejects untrimmed context values at the database boundary: %s', async (context) => {
40
+ const createAnnotation = annotationFactory.create({context});
41
+
42
+ await expect(createAnnotation).rejects.toThrow();
43
+ });
44
+
45
+ it('rejects non-positive workflow run attempts at the database boundary', async () => {
46
+ const createAnnotation = annotationFactory.create({workflowRunAttempt: 0});
47
+
48
+ await expect(createAnnotation).rejects.toThrow();
49
+ });
50
+ });
@@ -0,0 +1,96 @@
1
+ import {
2
+ ANNOTATION_CONTEXT_MAX_LENGTH,
3
+ ANNOTATION_CONTEXT_TRIM_CODE_POINTS,
4
+ ANNOTATION_STYLES,
5
+ } from '@shipfox/annotations-dto';
6
+ import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
7
+ import {sql} from 'drizzle-orm';
8
+ import {
9
+ check,
10
+ index,
11
+ integer,
12
+ pgEnum,
13
+ text,
14
+ timestamp,
15
+ uniqueIndex,
16
+ uuid,
17
+ } from 'drizzle-orm/pg-core';
18
+ import type {Annotation} from '#core/entities/annotation.js';
19
+ import {pgTable} from './common.js';
20
+
21
+ export const annotationStyleEnum = pgEnum('annotations_style', [...ANNOTATION_STYLES]);
22
+
23
+ const annotationContextTrimCharactersSql = sql`concat(${sql.join(
24
+ ANNOTATION_CONTEXT_TRIM_CODE_POINTS.map((codePoint) => sql.raw(`chr(${codePoint})`)),
25
+ sql`, `,
26
+ )})`;
27
+
28
+ export const annotations = pgTable(
29
+ 'annotations',
30
+ {
31
+ id: uuidv7PrimaryKey(),
32
+ workspaceId: uuid('workspace_id').notNull(),
33
+ projectId: uuid('project_id').notNull(),
34
+ workflowRunId: uuid('workflow_run_id').notNull(),
35
+ workflowRunAttempt: integer('workflow_run_attempt').notNull(),
36
+ workflowRunAttemptId: uuid('workflow_run_attempt_id').notNull(),
37
+ jobId: uuid('job_id').notNull(),
38
+ jobExecutionId: uuid('job_execution_id').notNull(),
39
+ originStepId: uuid('origin_step_id').notNull(),
40
+ originStepAttempt: integer('origin_step_attempt').notNull(),
41
+ context: text('context').notNull(),
42
+ style: annotationStyleEnum('style').notNull().default('default'),
43
+ body: text('body').notNull(),
44
+ bodyBytes: integer('body_bytes').notNull(),
45
+ sequence: integer('sequence').notNull(),
46
+ createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
47
+ updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
48
+ },
49
+ (table) => [
50
+ uniqueIndex('annotations_job_execution_context_unique').on(table.jobExecutionId, table.context),
51
+ index('annotations_workflow_run_attempt_idx').on(table.workflowRunId, table.workflowRunAttempt),
52
+ index('annotations_workflow_run_attempt_id_idx').on(table.workflowRunAttemptId),
53
+ index('annotations_job_execution_id_idx').on(table.jobExecutionId),
54
+ check('annotations_workflow_run_attempt_positive_ck', sql`${table.workflowRunAttempt} > 0`),
55
+ check('annotations_origin_step_attempt_positive_ck', sql`${table.originStepAttempt} > 0`),
56
+ check(
57
+ 'annotations_body_bytes_matches_body_ck',
58
+ sql`${table.bodyBytes} = octet_length(${table.body})`,
59
+ ),
60
+ check('annotations_sequence_positive_ck', sql`${table.sequence} > 0`),
61
+ check('annotations_context_not_empty_ck', sql`length(${table.context}) > 0`),
62
+ check(
63
+ 'annotations_context_trimmed_ck',
64
+ sql`${table.context} = btrim(${table.context}, ${annotationContextTrimCharactersSql})`,
65
+ ),
66
+ check(
67
+ 'annotations_context_max_length_ck',
68
+ sql`length(${table.context}) <= ${ANNOTATION_CONTEXT_MAX_LENGTH}`,
69
+ ),
70
+ ],
71
+ );
72
+
73
+ export type AnnotationDb = typeof annotations.$inferSelect;
74
+ export type AnnotationCreateDb = typeof annotations.$inferInsert;
75
+
76
+ export function toAnnotation(row: AnnotationDb): Annotation {
77
+ return {
78
+ id: row.id,
79
+ workspaceId: row.workspaceId,
80
+ projectId: row.projectId,
81
+ workflowRunId: row.workflowRunId,
82
+ workflowRunAttempt: row.workflowRunAttempt,
83
+ workflowRunAttemptId: row.workflowRunAttemptId,
84
+ jobId: row.jobId,
85
+ jobExecutionId: row.jobExecutionId,
86
+ originStepId: row.originStepId,
87
+ originStepAttempt: row.originStepAttempt,
88
+ context: row.context,
89
+ style: row.style,
90
+ body: row.body,
91
+ bodyBytes: row.bodyBytes,
92
+ sequence: row.sequence,
93
+ createdAt: row.createdAt,
94
+ updatedAt: row.updatedAt,
95
+ };
96
+ }
@@ -0,0 +1,3 @@
1
+ import {pgTableCreator} from 'drizzle-orm/pg-core';
2
+
3
+ export const pgTable = pgTableCreator((name) => `annotations_${name}`);
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type {ShipfoxModule} from '@shipfox/node-module';
2
+ import {db} from '#db/db.js';
3
+ import {migrationsPath} from '#db/index.js';
4
+ import {annotationsRoutes} from '#presentation/routes/index.js';
5
+
6
+ export {
7
+ AnnotationBodyTooLargeError,
8
+ AnnotationCountLimitExceededError,
9
+ AnnotationTotalBytesLimitExceededError,
10
+ } from '#core/errors.js';
11
+ export {type WriteAnnotationsParams, writeAnnotations} from '#core/write-annotations.js';
12
+
13
+ export const annotationsModule: ShipfoxModule = {
14
+ name: 'annotations',
15
+ database: {db, migrationsPath},
16
+ routes: annotationsRoutes,
17
+ };
@@ -0,0 +1,42 @@
1
+ import type {Annotation} from '#core/entities/annotation.js';
2
+ import {toAnnotationDto} from './annotation.js';
3
+
4
+ describe('toAnnotationDto', () => {
5
+ it('maps an annotation domain entity to the public DTO shape', () => {
6
+ const annotation: Annotation = {
7
+ id: '11111111-1111-4111-8111-111111111111',
8
+ workspaceId: '22222222-2222-4222-8222-222222222222',
9
+ projectId: '33333333-3333-4333-8333-333333333333',
10
+ workflowRunId: '44444444-4444-4444-8444-444444444444',
11
+ workflowRunAttempt: 2,
12
+ workflowRunAttemptId: '55555555-5555-4555-8555-555555555555',
13
+ jobId: '66666666-6666-4666-8666-666666666666',
14
+ jobExecutionId: '77777777-7777-4777-8777-777777777777',
15
+ originStepId: '88888888-8888-4888-8888-888888888888',
16
+ originStepAttempt: 3,
17
+ context: 'deploy',
18
+ style: 'success',
19
+ sequence: 4,
20
+ body: 'Deployed **v42**',
21
+ bodyBytes: 16,
22
+ createdAt: new Date('2026-07-07T10:00:00.000Z'),
23
+ updatedAt: new Date('2026-07-07T10:01:00.000Z'),
24
+ };
25
+
26
+ const result = toAnnotationDto(annotation);
27
+
28
+ expect(result).toEqual({
29
+ id: '11111111-1111-4111-8111-111111111111',
30
+ job_id: '66666666-6666-4666-8666-666666666666',
31
+ job_execution_id: '77777777-7777-4777-8777-777777777777',
32
+ origin_step_id: '88888888-8888-4888-8888-888888888888',
33
+ origin_step_attempt: 3,
34
+ context: 'deploy',
35
+ style: 'success',
36
+ sequence: 4,
37
+ body: 'Deployed **v42**',
38
+ });
39
+ expect(result).not.toHaveProperty('workspace_id');
40
+ expect(result).not.toHaveProperty('workflow_run_attempt_id');
41
+ });
42
+ });
@@ -0,0 +1,16 @@
1
+ import type {AnnotationDto} from '@shipfox/annotations-dto';
2
+ import type {Annotation} from '#core/entities/annotation.js';
3
+
4
+ export function toAnnotationDto(annotation: Annotation): AnnotationDto {
5
+ return {
6
+ id: annotation.id,
7
+ job_id: annotation.jobId,
8
+ job_execution_id: annotation.jobExecutionId,
9
+ origin_step_id: annotation.originStepId,
10
+ origin_step_attempt: annotation.originStepAttempt,
11
+ context: annotation.context,
12
+ style: annotation.style,
13
+ sequence: annotation.sequence,
14
+ body: annotation.body,
15
+ };
16
+ }
@@ -0,0 +1 @@
1
+ export {toAnnotationDto} from './annotation.js';
@@ -0,0 +1,17 @@
1
+ import {AUTH_LEASED_JOB, AUTH_USER} from '@shipfox/api-auth-context';
2
+ import type {RouteGroup} from '@shipfox/node-fastify';
3
+ import {readAnnotationsRoute} from './read-annotations.js';
4
+ import {writeAnnotationsRoute} from './write-annotations.js';
5
+
6
+ export const annotationsRoutes: RouteGroup[] = [
7
+ {
8
+ prefix: '/annotations',
9
+ auth: AUTH_USER,
10
+ routes: [readAnnotationsRoute],
11
+ },
12
+ {
13
+ prefix: '/runs/jobs/current',
14
+ auth: AUTH_LEASED_JOB,
15
+ routes: [writeAnnotationsRoute],
16
+ },
17
+ ];