@shipfox/api-projects 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 (202) 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 +117 -0
  5. package/LICENSE +21 -0
  6. package/dist/core/entities/index.d.ts +2 -0
  7. package/dist/core/entities/index.d.ts.map +1 -0
  8. package/dist/core/entities/index.js +3 -0
  9. package/dist/core/entities/index.js.map +1 -0
  10. package/dist/core/entities/project.d.ts +10 -0
  11. package/dist/core/entities/project.d.ts.map +1 -0
  12. package/dist/core/entities/project.js +3 -0
  13. package/dist/core/entities/project.js.map +1 -0
  14. package/dist/core/errors.d.ts +13 -0
  15. package/dist/core/errors.d.ts.map +1 -0
  16. package/dist/core/errors.js +20 -0
  17. package/dist/core/errors.js.map +1 -0
  18. package/dist/core/index.d.ts +5 -0
  19. package/dist/core/index.d.ts.map +1 -0
  20. package/dist/core/index.js +4 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/core/projects.d.ts +12 -0
  23. package/dist/core/projects.d.ts.map +1 -0
  24. package/dist/core/projects.js +62 -0
  25. package/dist/core/projects.js.map +1 -0
  26. package/dist/db/db.d.ts +763 -0
  27. package/dist/db/db.d.ts.map +1 -0
  28. package/dist/db/db.js +22 -0
  29. package/dist/db/db.js.map +1 -0
  30. package/dist/db/index.d.ts +8 -0
  31. package/dist/db/index.d.ts.map +1 -0
  32. package/dist/db/index.js +10 -0
  33. package/dist/db/index.js.map +1 -0
  34. package/dist/db/integration-event-dedup.d.ts +17 -0
  35. package/dist/db/integration-event-dedup.d.ts.map +1 -0
  36. package/dist/db/integration-event-dedup.js +27 -0
  37. package/dist/db/integration-event-dedup.js.map +1 -0
  38. package/dist/db/projects.d.ts +36 -0
  39. package/dist/db/projects.d.ts.map +1 -0
  40. package/dist/db/projects.js +84 -0
  41. package/dist/db/projects.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/db/schema/integration-event-dedup.d.ts +59 -0
  47. package/dist/db/schema/integration-event-dedup.d.ts.map +1 -0
  48. package/dist/db/schema/integration-event-dedup.js +19 -0
  49. package/dist/db/schema/integration-event-dedup.js.map +1 -0
  50. package/dist/db/schema/outbox.d.ts +195 -0
  51. package/dist/db/schema/outbox.d.ts.map +1 -0
  52. package/dist/db/schema/outbox.js +5 -0
  53. package/dist/db/schema/outbox.js.map +1 -0
  54. package/dist/db/schema/projects.d.ts +131 -0
  55. package/dist/db/schema/projects.d.ts.map +1 -0
  56. package/dist/db/schema/projects.js +32 -0
  57. package/dist/db/schema/projects.js.map +1 -0
  58. package/dist/index.d.ts +12 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +59 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/metrics/index.d.ts +3 -0
  63. package/dist/metrics/index.d.ts.map +1 -0
  64. package/dist/metrics/index.js +4 -0
  65. package/dist/metrics/index.js.map +1 -0
  66. package/dist/metrics/instance.d.ts +4 -0
  67. package/dist/metrics/instance.d.ts.map +1 -0
  68. package/dist/metrics/instance.js +25 -0
  69. package/dist/metrics/instance.js.map +1 -0
  70. package/dist/metrics/service.d.ts +2 -0
  71. package/dist/metrics/service.d.ts.map +1 -0
  72. package/dist/metrics/service.js +15 -0
  73. package/dist/metrics/service.js.map +1 -0
  74. package/dist/presentation/auth/require-project-access.d.ts +12 -0
  75. package/dist/presentation/auth/require-project-access.d.ts.map +1 -0
  76. package/dist/presentation/auth/require-project-access.js +25 -0
  77. package/dist/presentation/auth/require-project-access.js.map +1 -0
  78. package/dist/presentation/dto/index.d.ts +2 -0
  79. package/dist/presentation/dto/index.d.ts.map +1 -0
  80. package/dist/presentation/dto/index.js +3 -0
  81. package/dist/presentation/dto/index.js.map +1 -0
  82. package/dist/presentation/dto/project.d.ts +13 -0
  83. package/dist/presentation/dto/project.d.ts.map +1 -0
  84. package/dist/presentation/dto/project.js +15 -0
  85. package/dist/presentation/dto/project.js.map +1 -0
  86. package/dist/presentation/e2eRoutes/create-project.d.ts +2 -0
  87. package/dist/presentation/e2eRoutes/create-project.d.ts.map +1 -0
  88. package/dist/presentation/e2eRoutes/create-project.js +45 -0
  89. package/dist/presentation/e2eRoutes/create-project.js.map +1 -0
  90. package/dist/presentation/e2eRoutes/index.d.ts +3 -0
  91. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  92. package/dist/presentation/e2eRoutes/index.js +9 -0
  93. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  94. package/dist/presentation/index.d.ts +3 -0
  95. package/dist/presentation/index.d.ts.map +1 -0
  96. package/dist/presentation/index.js +4 -0
  97. package/dist/presentation/index.js.map +1 -0
  98. package/dist/presentation/routes/create-project.d.ts +3 -0
  99. package/dist/presentation/routes/create-project.d.ts.map +1 -0
  100. package/dist/presentation/routes/create-project.js +94 -0
  101. package/dist/presentation/routes/create-project.js.map +1 -0
  102. package/dist/presentation/routes/cursor.d.ts +2 -0
  103. package/dist/presentation/routes/cursor.d.ts.map +1 -0
  104. package/dist/presentation/routes/cursor.js +3 -0
  105. package/dist/presentation/routes/cursor.js.map +1 -0
  106. package/dist/presentation/routes/get-project.d.ts +2 -0
  107. package/dist/presentation/routes/get-project.d.ts.map +1 -0
  108. package/dist/presentation/routes/get-project.js +30 -0
  109. package/dist/presentation/routes/get-project.js.map +1 -0
  110. package/dist/presentation/routes/index.d.ts +4 -0
  111. package/dist/presentation/routes/index.d.ts.map +1 -0
  112. package/dist/presentation/routes/index.js +17 -0
  113. package/dist/presentation/routes/index.js.map +1 -0
  114. package/dist/presentation/routes/list-projects.d.ts +2 -0
  115. package/dist/presentation/routes/list-projects.d.ts.map +1 -0
  116. package/dist/presentation/routes/list-projects.js +43 -0
  117. package/dist/presentation/routes/list-projects.js.map +1 -0
  118. package/dist/presentation/subscribers/index.d.ts +2 -0
  119. package/dist/presentation/subscribers/index.d.ts.map +1 -0
  120. package/dist/presentation/subscribers/index.js +3 -0
  121. package/dist/presentation/subscribers/index.js.map +1 -0
  122. package/dist/presentation/subscribers/on-source-commit-pushed.d.ts +4 -0
  123. package/dist/presentation/subscribers/on-source-commit-pushed.d.ts.map +1 -0
  124. package/dist/presentation/subscribers/on-source-commit-pushed.js +54 -0
  125. package/dist/presentation/subscribers/on-source-commit-pushed.js.map +1 -0
  126. package/dist/temporal/activities/index.d.ts +5 -0
  127. package/dist/temporal/activities/index.d.ts.map +1 -0
  128. package/dist/temporal/activities/index.js +8 -0
  129. package/dist/temporal/activities/index.js.map +1 -0
  130. package/dist/temporal/activities/prune-integration-event-dedup.d.ts +4 -0
  131. package/dist/temporal/activities/prune-integration-event-dedup.d.ts.map +1 -0
  132. package/dist/temporal/activities/prune-integration-event-dedup.js +11 -0
  133. package/dist/temporal/activities/prune-integration-event-dedup.js.map +1 -0
  134. package/dist/temporal/constants.d.ts +3 -0
  135. package/dist/temporal/constants.d.ts.map +1 -0
  136. package/dist/temporal/constants.js +4 -0
  137. package/dist/temporal/constants.js.map +1 -0
  138. package/dist/temporal/workflows/index.d.ts +2 -0
  139. package/dist/temporal/workflows/index.d.ts.map +1 -0
  140. package/dist/temporal/workflows/index.js +3 -0
  141. package/dist/temporal/workflows/index.js.map +1 -0
  142. package/dist/temporal/workflows/prune-integration-event-dedup-cron.d.ts +2 -0
  143. package/dist/temporal/workflows/prune-integration-event-dedup-cron.d.ts.map +1 -0
  144. package/dist/temporal/workflows/prune-integration-event-dedup-cron.js +14 -0
  145. package/dist/temporal/workflows/prune-integration-event-dedup-cron.js.map +1 -0
  146. package/dist/tsconfig.test.tsbuildinfo +1 -0
  147. package/drizzle/0000_initial.sql +37 -0
  148. package/drizzle/meta/0000_snapshot.json +307 -0
  149. package/drizzle/meta/_journal.json +13 -0
  150. package/drizzle.config.ts +7 -0
  151. package/package.json +76 -0
  152. package/src/core/entities/index.ts +1 -0
  153. package/src/core/entities/project.ts +9 -0
  154. package/src/core/errors.ts +26 -0
  155. package/src/core/index.ts +8 -0
  156. package/src/core/projects.test.ts +142 -0
  157. package/src/core/projects.ts +97 -0
  158. package/src/db/db.ts +22 -0
  159. package/src/db/index.ts +26 -0
  160. package/src/db/integration-event-dedup.ts +45 -0
  161. package/src/db/projects.test.ts +22 -0
  162. package/src/db/projects.ts +158 -0
  163. package/src/db/schema/common.ts +3 -0
  164. package/src/db/schema/integration-event-dedup.ts +15 -0
  165. package/src/db/schema/outbox.ts +4 -0
  166. package/src/db/schema/projects.ts +39 -0
  167. package/src/index.ts +71 -0
  168. package/src/metrics/index.ts +2 -0
  169. package/src/metrics/instance.ts +34 -0
  170. package/src/metrics/service.ts +17 -0
  171. package/src/presentation/auth/require-project-access.ts +29 -0
  172. package/src/presentation/dto/index.ts +1 -0
  173. package/src/presentation/dto/project.ts +15 -0
  174. package/src/presentation/e2eRoutes/create-project.test.ts +119 -0
  175. package/src/presentation/e2eRoutes/create-project.ts +50 -0
  176. package/src/presentation/e2eRoutes/index.ts +7 -0
  177. package/src/presentation/index.ts +2 -0
  178. package/src/presentation/routes/create-project.ts +102 -0
  179. package/src/presentation/routes/cursor.ts +5 -0
  180. package/src/presentation/routes/get-project.ts +24 -0
  181. package/src/presentation/routes/index.ts +14 -0
  182. package/src/presentation/routes/list-projects.ts +34 -0
  183. package/src/presentation/routes/projects.test.ts +266 -0
  184. package/src/presentation/subscribers/index.ts +1 -0
  185. package/src/presentation/subscribers/on-source-commit-pushed.test.ts +149 -0
  186. package/src/presentation/subscribers/on-source-commit-pushed.ts +64 -0
  187. package/src/temporal/activities/index.ts +7 -0
  188. package/src/temporal/activities/prune-integration-event-dedup.test.ts +50 -0
  189. package/src/temporal/activities/prune-integration-event-dedup.ts +9 -0
  190. package/src/temporal/constants.ts +3 -0
  191. package/src/temporal/workflows/index.ts +1 -0
  192. package/src/temporal/workflows/prune-integration-event-dedup-cron.ts +15 -0
  193. package/test/env.ts +14 -0
  194. package/test/factories/project.ts +24 -0
  195. package/test/globalSetup.ts +17 -0
  196. package/test/index.ts +1 -0
  197. package/test/setup.ts +17 -0
  198. package/tsconfig.build.json +9 -0
  199. package/tsconfig.build.tsbuildinfo +1 -0
  200. package/tsconfig.json +3 -0
  201. package/tsconfig.test.json +8 -0
  202. package/vitest.config.ts +12 -0
@@ -0,0 +1,149 @@
1
+ import {
2
+ INTEGRATION_SOURCE_COMMIT_PUSHED,
3
+ type IntegrationSourceCommitPushedEvent,
4
+ type SourcePushPayload,
5
+ } from '@shipfox/api-integration-core-dto';
6
+ import {PROJECT_SOURCE_COMMIT_OBSERVED} from '@shipfox/api-projects-dto';
7
+ import type {DomainEvent} from '@shipfox/node-outbox';
8
+ import {and, eq, sql} from 'drizzle-orm';
9
+ import {db} from '#db/db.js';
10
+ import {projectsOutbox} from '#db/schema/outbox.js';
11
+ import {projectFactory} from '#test/index.js';
12
+ import {createProjectsModule} from '../../index.js';
13
+ import {onSourceCommitPushed} from './on-source-commit-pushed.js';
14
+
15
+ function buildPush(overrides: Partial<SourcePushPayload> = {}): SourcePushPayload {
16
+ return {
17
+ externalRepositoryId: 'github:42',
18
+ ref: 'main',
19
+ headCommitSha: 'abc123',
20
+ defaultBranch: 'main',
21
+ isDefaultBranch: true,
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ function buildEvent(
27
+ overrides: Partial<IntegrationSourceCommitPushedEvent> = {},
28
+ pushOverrides: Partial<SourcePushPayload> = {},
29
+ id = crypto.randomUUID(),
30
+ ): DomainEvent<IntegrationSourceCommitPushedEvent> {
31
+ return {
32
+ id,
33
+ type: INTEGRATION_SOURCE_COMMIT_PUSHED,
34
+ createdAt: new Date(),
35
+ payload: {
36
+ provider: 'github',
37
+ workspaceId: crypto.randomUUID(),
38
+ connectionId: crypto.randomUUID(),
39
+ deliveryId: crypto.randomUUID(),
40
+ receivedAt: new Date().toISOString(),
41
+ push: buildPush(pushOverrides),
42
+ ...overrides,
43
+ },
44
+ };
45
+ }
46
+
47
+ async function listCommitObservedEvents(externalRepositoryId: string) {
48
+ return await db()
49
+ .select()
50
+ .from(projectsOutbox)
51
+ .where(
52
+ and(
53
+ eq(projectsOutbox.eventType, PROJECT_SOURCE_COMMIT_OBSERVED),
54
+ sql`${projectsOutbox.payload}->>'externalRepositoryId' = ${externalRepositoryId}`,
55
+ ),
56
+ );
57
+ }
58
+
59
+ describe('onSourceCommitPushed', () => {
60
+ it('publishes a project source commit event for a default-branch push', async () => {
61
+ const sourceConnectionId = crypto.randomUUID();
62
+ const workspaceId = crypto.randomUUID();
63
+ const externalRepositoryId = `github:${crypto.randomUUID()}`;
64
+ const project = await projectFactory.create({
65
+ workspaceId,
66
+ sourceConnectionId,
67
+ sourceExternalRepositoryId: externalRepositoryId,
68
+ });
69
+ const event = buildEvent(
70
+ {workspaceId, connectionId: sourceConnectionId},
71
+ {externalRepositoryId},
72
+ );
73
+
74
+ await onSourceCommitPushed(event.payload, event);
75
+
76
+ const rows = await listCommitObservedEvents(externalRepositoryId);
77
+ expect(rows).toHaveLength(1);
78
+ expect(rows[0]?.payload).toMatchObject({
79
+ workspaceId,
80
+ projectId: project.id,
81
+ sourceConnectionId,
82
+ provider: 'github',
83
+ externalRepositoryId,
84
+ ref: 'main',
85
+ headCommitSha: 'abc123',
86
+ });
87
+ });
88
+
89
+ it('does not publish for a non-default branch push', async () => {
90
+ const sourceConnectionId = crypto.randomUUID();
91
+ const workspaceId = crypto.randomUUID();
92
+ const externalRepositoryId = `github:${crypto.randomUUID()}`;
93
+ await projectFactory.create({
94
+ workspaceId,
95
+ sourceConnectionId,
96
+ sourceExternalRepositoryId: externalRepositoryId,
97
+ });
98
+ const event = buildEvent(
99
+ {workspaceId, connectionId: sourceConnectionId},
100
+ {externalRepositoryId, ref: 'feature/x', isDefaultBranch: false},
101
+ );
102
+
103
+ await onSourceCommitPushed(event.payload, event);
104
+
105
+ const rows = await listCommitObservedEvents(externalRepositoryId);
106
+ expect(rows).toHaveLength(0);
107
+ });
108
+
109
+ it('does not publish when no project is bound to the pushed repository', async () => {
110
+ const externalRepositoryId = `github:${crypto.randomUUID()}`;
111
+ const event = buildEvent({}, {externalRepositoryId});
112
+
113
+ await onSourceCommitPushed(event.payload, event);
114
+
115
+ const rows = await listCommitObservedEvents(externalRepositoryId);
116
+ expect(rows).toHaveLength(0);
117
+ });
118
+
119
+ it('deduplicates the same source event for the same project', async () => {
120
+ const sourceConnectionId = crypto.randomUUID();
121
+ const workspaceId = crypto.randomUUID();
122
+ const externalRepositoryId = `github:${crypto.randomUUID()}`;
123
+ await projectFactory.create({
124
+ workspaceId,
125
+ sourceConnectionId,
126
+ sourceExternalRepositoryId: externalRepositoryId,
127
+ });
128
+ const event = buildEvent(
129
+ {workspaceId, connectionId: sourceConnectionId},
130
+ {externalRepositoryId},
131
+ );
132
+
133
+ await onSourceCommitPushed(event.payload, event);
134
+ await onSourceCommitPushed(event.payload, event);
135
+
136
+ const rows = await listCommitObservedEvents(externalRepositoryId);
137
+ expect(rows).toHaveLength(1);
138
+ });
139
+
140
+ // The source/event filter is the subscription itself, so this module should
141
+ // only receive the typed source-control event.
142
+ it('registers the projects module on INTEGRATION_SOURCE_COMMIT_PUSHED', () => {
143
+ const module = createProjectsModule({sourceControl: {} as never});
144
+
145
+ const events = module.subscribers?.map((subscriber) => subscriber.event);
146
+
147
+ expect(events).toContain(INTEGRATION_SOURCE_COMMIT_PUSHED);
148
+ });
149
+ });
@@ -0,0 +1,64 @@
1
+ import type {IntegrationSourceCommitPushedEvent} from '@shipfox/api-integration-core-dto';
2
+ import {PROJECT_SOURCE_COMMIT_OBSERVED} from '@shipfox/api-projects-dto';
3
+ import {logger} from '@shipfox/node-opentelemetry';
4
+ import {type DomainEvent, writeOutboxEvent} from '@shipfox/node-outbox';
5
+ import {db} from '#db/db.js';
6
+ import {recordIntegrationEventForProject} from '#db/integration-event-dedup.js';
7
+ import {getProjectBySource} from '#db/projects.js';
8
+ import {projectsOutbox} from '#db/schema/outbox.js';
9
+ import {recordSourceCommitPushed, type SourceCommitPushedOutcome} from '#metrics/instance.js';
10
+
11
+ export async function onSourceCommitPushed(
12
+ payload: IntegrationSourceCommitPushedEvent,
13
+ event: DomainEvent<IntegrationSourceCommitPushedEvent>,
14
+ ): Promise<void> {
15
+ const {provider, workspaceId, connectionId, push} = payload;
16
+
17
+ // Projects only track the default branch; other branches are someone else's policy.
18
+ if (!push.isDefaultBranch) {
19
+ recordSourceCommitPushed('non_default_branch');
20
+ return;
21
+ }
22
+
23
+ const project = await getProjectBySource({
24
+ workspaceId,
25
+ sourceConnectionId: connectionId,
26
+ sourceExternalRepositoryId: push.externalRepositoryId,
27
+ });
28
+ if (!project) {
29
+ logger().info(
30
+ {
31
+ eventId: event.id,
32
+ connectionId,
33
+ externalRepositoryId: push.externalRepositoryId,
34
+ },
35
+ 'source commit pushed: no project bound to source, dropping',
36
+ );
37
+ recordSourceCommitPushed('unbound_source');
38
+ return;
39
+ }
40
+
41
+ const outcome = await db().transaction<SourceCommitPushedOutcome>(async (tx) => {
42
+ const {firstSeen} = await recordIntegrationEventForProject({
43
+ tx,
44
+ integrationEventId: event.id,
45
+ projectId: project.id,
46
+ });
47
+ if (!firstSeen) return 'duplicate';
48
+
49
+ await writeOutboxEvent(tx, projectsOutbox, {
50
+ type: PROJECT_SOURCE_COMMIT_OBSERVED,
51
+ payload: {
52
+ workspaceId: project.workspaceId,
53
+ projectId: project.id,
54
+ sourceConnectionId: project.sourceConnectionId,
55
+ provider,
56
+ externalRepositoryId: push.externalRepositoryId,
57
+ ref: push.ref,
58
+ headCommitSha: push.headCommitSha,
59
+ },
60
+ });
61
+ return 'observed';
62
+ });
63
+ recordSourceCommitPushed(outcome);
64
+ }
@@ -0,0 +1,7 @@
1
+ import {pruneIntegrationEventDedupActivity} from './prune-integration-event-dedup.js';
2
+
3
+ export function createProjectsMaintenanceActivities() {
4
+ return {
5
+ pruneIntegrationEventDedupActivity,
6
+ };
7
+ }
@@ -0,0 +1,50 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import {inArray} from 'drizzle-orm';
3
+ import {db} from '#db/db.js';
4
+ import {projectsIntegrationEventDedup} from '#db/schema/integration-event-dedup.js';
5
+ import {pruneIntegrationEventDedupActivity} from './prune-integration-event-dedup.js';
6
+
7
+ async function insertDedupRow(receivedAt: Date): Promise<{
8
+ integrationEventId: string;
9
+ projectId: string;
10
+ }> {
11
+ const integrationEventId = randomUUID();
12
+ const projectId = randomUUID();
13
+ await db()
14
+ .insert(projectsIntegrationEventDedup)
15
+ .values({integrationEventId, projectId, receivedAt});
16
+ return {integrationEventId, projectId};
17
+ }
18
+
19
+ describe('pruneIntegrationEventDedupActivity', () => {
20
+ it('deletes rows older than the retention window and keeps recent ones', async () => {
21
+ const longAgo = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000);
22
+ const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
23
+ const old = await insertDedupRow(longAgo);
24
+ const recent = await insertDedupRow(yesterday);
25
+
26
+ const result = await pruneIntegrationEventDedupActivity();
27
+
28
+ expect(result.deleted).toBe(1);
29
+ const remaining = await db()
30
+ .select()
31
+ .from(projectsIntegrationEventDedup)
32
+ .where(
33
+ inArray(projectsIntegrationEventDedup.integrationEventId, [
34
+ old.integrationEventId,
35
+ recent.integrationEventId,
36
+ ]),
37
+ );
38
+ expect(remaining.map((row) => row.integrationEventId)).toEqual([recent.integrationEventId]);
39
+ expect(remaining.map((row) => row.projectId)).toEqual([recent.projectId]);
40
+ expect(old.integrationEventId).not.toEqual(recent.integrationEventId);
41
+ });
42
+
43
+ it('returns zero deleted when nothing is past the retention window', async () => {
44
+ await insertDedupRow(new Date());
45
+
46
+ const result = await pruneIntegrationEventDedupActivity();
47
+
48
+ expect(result.deleted).toBe(0);
49
+ });
50
+ });
@@ -0,0 +1,9 @@
1
+ import {pruneIntegrationEventDedup} from '#db/integration-event-dedup.js';
2
+ import {INTEGRATION_EVENT_DEDUP_RETENTION_DAYS} from '#temporal/constants.js';
3
+
4
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
5
+
6
+ export async function pruneIntegrationEventDedupActivity(): Promise<{deleted: number}> {
7
+ const olderThan = new Date(Date.now() - INTEGRATION_EVENT_DEDUP_RETENTION_DAYS * MS_PER_DAY);
8
+ return await pruneIntegrationEventDedup({olderThan});
9
+ }
@@ -0,0 +1,3 @@
1
+ export const PROJECTS_MAINTENANCE_TASK_QUEUE = 'projects-maintenance';
2
+
3
+ export const INTEGRATION_EVENT_DEDUP_RETENTION_DAYS = 30;
@@ -0,0 +1 @@
1
+ export {pruneIntegrationEventDedupCron} from './prune-integration-event-dedup-cron.js';
@@ -0,0 +1,15 @@
1
+ import {log, proxyActivities} from '@temporalio/workflow';
2
+ import type {createProjectsMaintenanceActivities} from '../activities/index.js';
3
+
4
+ const {pruneIntegrationEventDedupActivity} = proxyActivities<
5
+ ReturnType<typeof createProjectsMaintenanceActivities>
6
+ >({
7
+ startToCloseTimeout: '5 minutes',
8
+ });
9
+
10
+ export async function pruneIntegrationEventDedupCron(): Promise<void> {
11
+ const {deleted} = await pruneIntegrationEventDedupActivity();
12
+ if (deleted > 0) {
13
+ log.info('Pruned projects integration event dedup rows', {deleted});
14
+ }
15
+ }
package/test/env.ts ADDED
@@ -0,0 +1,14 @@
1
+ process.env.POSTGRES_HOST ??= 'localhost';
2
+ process.env.POSTGRES_PORT ??= '5432';
3
+ process.env.POSTGRES_USERNAME ??= 'shipfox';
4
+ process.env.POSTGRES_PASSWORD ??= 'password';
5
+ process.env.POSTGRES_DATABASE = 'api_test';
6
+ process.env.POSTGRES_MAX_CONNECTIONS ??= '5';
7
+ process.env.TZ = 'UTC';
8
+ process.env.GITHUB_APP_ID = '1';
9
+ process.env.GITHUB_APP_PRIVATE_KEY = 'test-private-key';
10
+ process.env.GITHUB_APP_CLIENT_ID = 'test-client-id';
11
+ process.env.GITHUB_APP_CLIENT_SECRET = 'test-client-secret';
12
+ process.env.GITHUB_APP_WEBHOOK_SECRET = 'test-webhook-secret';
13
+ process.env.GITHUB_APP_SLUG = 'shipfox-test';
14
+ process.env.GITHUB_INSTALL_STATE_SECRET = 'test-install-state-secret';
@@ -0,0 +1,24 @@
1
+ import {Factory} from 'fishery';
2
+ import type {Project} from '#core/entities/index.js';
3
+ import {createProject} from '#db/index.js';
4
+
5
+ export const projectFactory = Factory.define<Project>(({sequence, onCreate}) => {
6
+ onCreate(async (project) =>
7
+ createProject({
8
+ workspaceId: project.workspaceId,
9
+ sourceConnectionId: project.sourceConnectionId,
10
+ sourceExternalRepositoryId: project.sourceExternalRepositoryId,
11
+ name: project.name,
12
+ }),
13
+ );
14
+
15
+ return {
16
+ id: crypto.randomUUID(),
17
+ workspaceId: crypto.randomUUID(),
18
+ sourceConnectionId: crypto.randomUUID(),
19
+ sourceExternalRepositoryId: `repository-${sequence}`,
20
+ name: `Project ${sequence}`,
21
+ createdAt: new Date(),
22
+ updatedAt: new Date(),
23
+ };
24
+ });
@@ -0,0 +1,17 @@
1
+ import './env.js';
2
+ import {runMigrations} from '@shipfox/node-drizzle';
3
+ import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
4
+ import {sql} from 'drizzle-orm';
5
+ import {closeDb, db, migrationsPath} from '#db/index.js';
6
+
7
+ export async function setup() {
8
+ createPostgresClient();
9
+
10
+ await runMigrations(db(), migrationsPath, '__drizzle_migrations_projects');
11
+ await db().execute(sql`TRUNCATE projects_projects CASCADE`);
12
+ await db().execute(sql`TRUNCATE projects_outbox CASCADE`);
13
+ await db().execute(sql`TRUNCATE projects_integration_event_dedup CASCADE`);
14
+
15
+ closeDb();
16
+ await closePostgresClient();
17
+ }
package/test/index.ts ADDED
@@ -0,0 +1 @@
1
+ export {projectFactory} from './factories/project.js';
package/test/setup.ts ADDED
@@ -0,0 +1,17 @@
1
+ import './env.js';
2
+ import {closePostgresClient, createPostgresClient} from '@shipfox/node-postgres';
3
+ import {afterAll, afterEach, beforeAll, vi} from '@shipfox/vitest/vi';
4
+ import {closeDb} from '#db/index.js';
5
+
6
+ beforeAll(() => {
7
+ createPostgresClient();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ afterAll(async () => {
15
+ closeDb();
16
+ await closePostgresClient();
17
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@shipfox/ts-config/node",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/*.test.tsx", "**/*.test.ts"]
9
+ }