@shipfox/api-projects 5.0.0 → 6.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 (33) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +30 -0
  3. package/dist/core/projects.d.ts +2 -2
  4. package/dist/core/projects.d.ts.map +1 -1
  5. package/dist/core/projects.js +2 -1
  6. package/dist/core/projects.js.map +1 -1
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +6 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/presentation/inter-module.d.ts +4 -0
  12. package/dist/presentation/inter-module.d.ts.map +1 -0
  13. package/dist/presentation/inter-module.js +29 -0
  14. package/dist/presentation/inter-module.js.map +1 -0
  15. package/dist/presentation/routes/create-project.d.ts +2 -2
  16. package/dist/presentation/routes/create-project.d.ts.map +1 -1
  17. package/dist/presentation/routes/create-project.js +19 -20
  18. package/dist/presentation/routes/create-project.js.map +1 -1
  19. package/dist/presentation/routes/index.d.ts +2 -2
  20. package/dist/presentation/routes/index.d.ts.map +1 -1
  21. package/dist/presentation/routes/index.js +2 -2
  22. package/dist/presentation/routes/index.js.map +1 -1
  23. package/dist/tsconfig.test.tsbuildinfo +1 -1
  24. package/package.json +12 -10
  25. package/src/core/projects.test.ts +21 -25
  26. package/src/core/projects.ts +7 -3
  27. package/src/index.ts +6 -4
  28. package/src/presentation/inter-module.ts +33 -0
  29. package/src/presentation/routes/create-project.ts +41 -41
  30. package/src/presentation/routes/index.ts +3 -3
  31. package/src/presentation/routes/projects.test.ts +17 -28
  32. package/src/presentation/subscribers/on-source-commit-pushed.test.ts +2 -1
  33. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/api-projects",
3
3
  "license": "MIT",
4
- "version": "5.0.0",
4
+ "version": "6.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -25,17 +25,17 @@
25
25
  "@temporalio/workflow": "1.18.1",
26
26
  "drizzle-orm": "^0.45.2",
27
27
  "zod": "^4.4.3",
28
- "@shipfox/api-auth-context": "5.0.0",
29
- "@shipfox/api-integration-core": "5.0.0",
30
- "@shipfox/api-integration-core-dto": "5.0.0",
31
- "@shipfox/api-projects-dto": "5.0.0",
32
- "@shipfox/node-drizzle": "0.3.1",
33
- "@shipfox/node-fastify": "0.2.3",
34
- "@shipfox/node-module": "0.3.2",
28
+ "@shipfox/api-integration-core-dto": "6.0.0",
29
+ "@shipfox/api-auth-context": "6.0.0",
30
+ "@shipfox/api-projects-dto": "6.0.0",
31
+ "@shipfox/inter-module": "0.2.0",
32
+ "@shipfox/node-module": "0.4.0",
33
+ "@shipfox/node-fastify": "0.2.4",
35
34
  "@shipfox/node-opentelemetry": "0.5.2",
36
- "@shipfox/node-outbox": "0.2.3",
35
+ "@shipfox/node-drizzle": "0.3.2",
36
+ "@shipfox/node-outbox": "0.2.4",
37
37
  "@shipfox/node-postgres": "0.4.2",
38
- "@shipfox/node-temporal": "0.3.1"
38
+ "@shipfox/node-temporal": "0.3.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@temporalio/activity": "1.18.1",
@@ -49,6 +49,7 @@
49
49
  "fastify-type-provider-zod": "^6.0.0",
50
50
  "fishery": "^2.4.0",
51
51
  "@shipfox/biome": "1.8.2",
52
+ "@shipfox/depcruise": "1.0.2",
52
53
  "@shipfox/swc": "1.2.6",
53
54
  "@shipfox/ts-config": "1.3.8",
54
55
  "@shipfox/typescript": "1.1.7",
@@ -58,6 +59,7 @@
58
59
  "build": "shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js",
59
60
  "check": "shipfox-biome-check",
60
61
  "check:fix": "shipfox-biome-check --write",
62
+ "depcruise": "shipfox-depcruise",
61
63
  "test": "shipfox-vitest-run",
62
64
  "test:watch": "shipfox-vitest-watch",
63
65
  "type": "shipfox-tsc-check",
@@ -1,5 +1,6 @@
1
- import type {IntegrationSourceControlService} from '@shipfox/api-integration-core';
2
- import {IntegrationProviderError} from '@shipfox/api-integration-core';
1
+ import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto';
2
+ import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto';
3
+ import {createInterModuleKnownError} from '@shipfox/inter-module';
3
4
  import {sql} from 'drizzle-orm';
4
5
  import {db} from '#db/index.js';
5
6
  import {projectsOutbox} from '#db/schema/outbox.js';
@@ -9,29 +10,20 @@ describe('createProjectFromSource', () => {
9
10
  let actorId: string;
10
11
  let workspaceId: string;
11
12
  let sourceConnectionId: string;
12
- let sourceControl: IntegrationSourceControlService;
13
+ let integrations: Pick<IntegrationsModuleClient, 'resolveSourceRepository'>;
13
14
 
14
15
  beforeEach(() => {
15
16
  actorId = crypto.randomUUID();
16
17
  workspaceId = crypto.randomUUID();
17
18
  sourceConnectionId = crypto.randomUUID();
18
- sourceControl = {
19
- getConnection: vi.fn(),
20
- listRepositories: vi.fn(),
21
- resolveRepository: vi.fn(async () => {
19
+ integrations = {
20
+ resolveSourceRepository: vi.fn(async () => {
22
21
  await Promise.resolve();
23
22
  return {
24
23
  connection: {
25
24
  id: sourceConnectionId,
26
- workspaceId,
27
25
  provider: 'gitea' as const,
28
- externalAccountId: 'gitea-owner',
29
26
  slug: 'gitea_owner',
30
- displayName: 'Gitea',
31
- lifecycleStatus: 'active' as const,
32
- capabilities: ['source_control' as const],
33
- createdAt: new Date(),
34
- updatedAt: new Date(),
35
27
  },
36
28
  repository: {
37
29
  externalRepositoryId: 'gitea:gitea-owner/platform',
@@ -45,9 +37,6 @@ describe('createProjectFromSource', () => {
45
37
  },
46
38
  };
47
39
  }),
48
- listFiles: vi.fn(),
49
- fetchFile: vi.fn(),
50
- createCheckoutSpec: vi.fn(),
51
40
  };
52
41
  });
53
42
 
@@ -58,7 +47,7 @@ describe('createProjectFromSource', () => {
58
47
  name: 'Platform',
59
48
  sourceConnectionId,
60
49
  sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
61
- sourceControl,
50
+ integrations: integrations as IntegrationsModuleClient,
62
51
  });
63
52
 
64
53
  expect(project.workspaceId).toBe(workspaceId);
@@ -74,7 +63,7 @@ describe('createProjectFromSource', () => {
74
63
  name: 'Platform',
75
64
  sourceConnectionId,
76
65
  sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
77
- sourceControl,
66
+ integrations: integrations as IntegrationsModuleClient,
78
67
  });
79
68
 
80
69
  const events = await db()
@@ -108,7 +97,7 @@ describe('createProjectFromSource', () => {
108
97
  name: 'Platform',
109
98
  sourceConnectionId,
110
99
  sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
111
- sourceControl,
100
+ integrations: integrations as IntegrationsModuleClient,
112
101
  });
113
102
 
114
103
  const result = createProjectFromSource({
@@ -117,15 +106,19 @@ describe('createProjectFromSource', () => {
117
106
  name: 'Platform Again',
118
107
  sourceConnectionId,
119
108
  sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
120
- sourceControl,
109
+ integrations: integrations as IntegrationsModuleClient,
121
110
  });
122
111
 
123
112
  await expect(result).rejects.toThrow('Project already exists');
124
113
  });
125
114
 
126
115
  test('surfaces provider repository access failures', async () => {
127
- vi.mocked(sourceControl.resolveRepository).mockRejectedValueOnce(
128
- new IntegrationProviderError('repository-not-found', 'Repository not found'),
116
+ vi.mocked(integrations.resolveSourceRepository).mockRejectedValueOnce(
117
+ createInterModuleKnownError(
118
+ integrationsInterModuleContract.methods.resolveSourceRepository,
119
+ 'provider-failure',
120
+ {reason: 'repository-not-found'},
121
+ ),
129
122
  );
130
123
 
131
124
  const result = createProjectFromSource({
@@ -134,9 +127,12 @@ describe('createProjectFromSource', () => {
134
127
  name: 'Missing',
135
128
  sourceConnectionId,
136
129
  sourceExternalRepositoryId: 'not-found',
137
- sourceControl,
130
+ integrations: integrations as IntegrationsModuleClient,
138
131
  });
139
132
 
140
- await expect(result).rejects.toMatchObject({reason: 'repository-not-found'});
133
+ await expect(result).rejects.toMatchObject({
134
+ code: 'provider-failure',
135
+ details: {reason: 'repository-not-found'},
136
+ });
141
137
  });
142
138
  });
@@ -1,4 +1,4 @@
1
- import type {IntegrationSourceControlService} from '@shipfox/api-integration-core';
1
+ import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto';
2
2
  import {
3
3
  PROJECT_CREATED,
4
4
  PROJECT_SOURCE_BOUND,
@@ -19,13 +19,17 @@ export interface CreateProjectFromSourceParams {
19
19
  name: string;
20
20
  sourceConnectionId: string;
21
21
  sourceExternalRepositoryId: string;
22
- sourceControl: IntegrationSourceControlService;
22
+ integrations: IntegrationsModuleClient;
23
23
  }
24
24
 
25
25
  export async function createProjectFromSource(
26
26
  params: CreateProjectFromSourceParams,
27
27
  ): Promise<Project> {
28
- const source = await params.sourceControl.resolveRepository({
28
+ let source: {
29
+ connection: {id: string; provider: string};
30
+ repository: {externalRepositoryId: string};
31
+ };
32
+ source = await params.integrations.resolveSourceRepository({
29
33
  workspaceId: params.workspaceId,
30
34
  connectionId: params.sourceConnectionId,
31
35
  externalRepositoryId: params.sourceExternalRepositoryId,
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {dirname, resolve} from 'node:path';
2
2
  import {fileURLToPath} from 'node:url';
3
- import type {IntegrationSourceControlService} from '@shipfox/api-integration-core';
3
+ import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto';
4
4
  import {
5
5
  INTEGRATION_SOURCE_COMMIT_PUSHED,
6
6
  type IntegrationsEventMap,
@@ -11,6 +11,7 @@ import {db, migrationsPath, projectsOutbox} from '#db/index.js';
11
11
  import {registerProjectsServiceMetrics} from '#metrics/index.js';
12
12
  import {projectsE2eRoutes} from '#presentation/e2eRoutes/index.js';
13
13
  import {createProjectRoutes} from '#presentation/index.js';
14
+ import {createProjectsInterModulePresentation} from '#presentation/inter-module.js';
14
15
  import {onSourceCommitPushed} from '#presentation/subscribers/index.js';
15
16
  import {createProjectsMaintenanceActivities} from '#temporal/activities/index.js';
16
17
  import {PROJECTS_MAINTENANCE_TASK_QUEUE} from '#temporal/constants.js';
@@ -41,17 +42,18 @@ export {
41
42
  export {createProjectRoutes, requireProjectAccess} from '#presentation/index.js';
42
43
 
43
44
  export interface CreateProjectsModuleOptions {
44
- sourceControl: IntegrationSourceControlService;
45
+ integrations: IntegrationsModuleClient;
45
46
  }
46
47
 
47
- export function createProjectsModule({sourceControl}: CreateProjectsModuleOptions): ShipfoxModule {
48
+ export function createProjectsModule({integrations}: CreateProjectsModuleOptions): ShipfoxModule {
48
49
  return {
49
50
  name: 'projects',
50
51
  database: {db, migrationsPath},
51
- routes: createProjectRoutes(sourceControl),
52
+ routes: createProjectRoutes(integrations),
52
53
  e2eRoutes: [projectsE2eRoutes],
53
54
  metrics: registerProjectsServiceMetrics,
54
55
  publishers: [{name: 'projects', table: projectsOutbox, db, eventSchemas: projectsEventSchemas}],
56
+ interModulePresentations: [createProjectsInterModulePresentation()],
55
57
  subscribers: [subscriber(INTEGRATION_SOURCE_COMMIT_PUSHED, onSourceCommitPushed)],
56
58
  workers: [
57
59
  {
@@ -0,0 +1,33 @@
1
+ import {projectsInterModuleContract} from '@shipfox/api-projects-dto';
2
+ import {
3
+ createInterModuleKnownError,
4
+ defineInterModulePresentation,
5
+ type InterModulePresentation,
6
+ } from '@shipfox/inter-module';
7
+ import {getProjectById} from '#db/projects.js';
8
+
9
+ export function createProjectsInterModulePresentation(): InterModulePresentation<
10
+ typeof projectsInterModuleContract
11
+ > {
12
+ return defineInterModulePresentation(projectsInterModuleContract, {
13
+ getProjectById: async ({projectId}) => ({project: (await getProjectById(projectId)) ?? null}),
14
+ requireProjectForWorkspace: async ({projectId, workspaceId}) => {
15
+ const project = await getProjectById(projectId);
16
+ if (project === undefined) {
17
+ throw createInterModuleKnownError(
18
+ projectsInterModuleContract.methods.requireProjectForWorkspace,
19
+ 'project-not-found',
20
+ {projectId},
21
+ );
22
+ }
23
+ if (project.workspaceId !== workspaceId) {
24
+ throw createInterModuleKnownError(
25
+ projectsInterModuleContract.methods.requireProjectForWorkspace,
26
+ 'project-workspace-mismatch',
27
+ {projectId, workspaceId},
28
+ );
29
+ }
30
+ return {project};
31
+ },
32
+ });
33
+ }
@@ -1,41 +1,21 @@
1
1
  import {AUTH_USER, requireUserContext, requireWorkspaceAccess} from '@shipfox/api-auth-context';
2
2
  import {
3
- IntegrationCapabilityUnavailableError,
4
- IntegrationConnectionInactiveError,
5
- IntegrationConnectionNotFoundError,
6
- IntegrationConnectionWorkspaceMismatchError,
7
- IntegrationProviderError,
8
- type IntegrationProviderErrorReason,
9
- IntegrationProviderUnavailableError,
10
- type IntegrationSourceControlService,
11
- } from '@shipfox/api-integration-core';
3
+ type IntegrationsModuleClient,
4
+ integrationsInterModuleContract,
5
+ } from '@shipfox/api-integration-core-dto';
12
6
  import {createProjectBodySchema, projectResponseSchema} from '@shipfox/api-projects-dto';
7
+ import {isInterModuleKnownError} from '@shipfox/inter-module';
13
8
  import {ClientError, defineRoute} from '@shipfox/node-fastify';
14
9
  import {createProjectFromSource, ProjectAlreadyExistsError} from '#core/index.js';
15
10
  import {toProjectDto} from '#presentation/dto/index.js';
16
11
 
17
- function providerStatus(reason: IntegrationProviderErrorReason): number {
12
+ function providerStatus(reason: string): number {
18
13
  if (reason === 'rate-limited') return 429;
19
14
  if (reason === 'timeout' || reason === 'provider-unavailable') return 503;
20
15
  return 422;
21
16
  }
22
17
 
23
- function isProviderError(error: unknown): error is IntegrationProviderError {
24
- return (
25
- error instanceof IntegrationProviderError ||
26
- (error instanceof Error &&
27
- 'reason' in error &&
28
- typeof error.reason === 'string' &&
29
- (error.reason === 'repository-not-found' ||
30
- error.reason === 'access-denied' ||
31
- error.reason === 'rate-limited' ||
32
- error.reason === 'timeout' ||
33
- error.reason === 'provider-unavailable' ||
34
- error.reason === 'malformed-provider-response'))
35
- );
36
- }
37
-
38
- export function createProjectRoute(sourceControl: IntegrationSourceControlService) {
18
+ export function createProjectRoute(integrations: IntegrationsModuleClient) {
39
19
  return defineRoute({
40
20
  method: 'POST',
41
21
  path: '/',
@@ -48,20 +28,40 @@ export function createProjectRoute(sourceControl: IntegrationSourceControlServic
48
28
  },
49
29
  },
50
30
  errorHandler: (error) => {
51
- if (error instanceof IntegrationConnectionNotFoundError) {
52
- throw new ClientError(error.message, 'source-connection-not-found', {status: 404});
31
+ const known = isInterModuleKnownError(
32
+ integrationsInterModuleContract.methods.resolveSourceRepository,
33
+ error,
34
+ )
35
+ ? error
36
+ : undefined;
37
+ if (known?.code === 'connection-not-found') {
38
+ throw new ClientError('Source connection not found', 'source-connection-not-found', {
39
+ status: 404,
40
+ });
53
41
  }
54
- if (error instanceof IntegrationConnectionWorkspaceMismatchError) {
55
- throw new ClientError(error.message, 'forbidden', {status: 403});
42
+ if (known?.code === 'connection-workspace-mismatch') {
43
+ throw new ClientError('Source connection does not belong to this workspace', 'forbidden', {
44
+ status: 403,
45
+ });
56
46
  }
57
- if (error instanceof IntegrationConnectionInactiveError) {
58
- throw new ClientError(error.message, 'source-connection-inactive', {status: 422});
47
+ if (known?.code === 'connection-inactive') {
48
+ throw new ClientError('Source connection is not active', 'source-connection-inactive', {
49
+ status: 422,
50
+ });
59
51
  }
60
- if (error instanceof IntegrationProviderUnavailableError) {
61
- throw new ClientError(error.message, 'integration-provider-unavailable', {status: 422});
52
+ if (known?.code === 'provider-unavailable') {
53
+ throw new ClientError(
54
+ 'Integration provider is unavailable',
55
+ 'integration-provider-unavailable',
56
+ {status: 422},
57
+ );
62
58
  }
63
- if (error instanceof IntegrationCapabilityUnavailableError) {
64
- throw new ClientError(error.message, 'integration-capability-unavailable', {status: 422});
59
+ if (known?.code === 'capability-unavailable') {
60
+ throw new ClientError(
61
+ 'Integration capability is unavailable',
62
+ 'integration-capability-unavailable',
63
+ {status: 422},
64
+ );
65
65
  }
66
66
  if (error instanceof ProjectAlreadyExistsError) {
67
67
  throw new ClientError(error.message, 'project-already-exists', {
@@ -73,10 +73,10 @@ export function createProjectRoute(sourceControl: IntegrationSourceControlServic
73
73
  status: 409,
74
74
  });
75
75
  }
76
- if (isProviderError(error)) {
77
- throw new ClientError(error.message, error.reason, {
78
- details: {retry_after_seconds: error.retryAfterSeconds},
79
- status: providerStatus(error.reason),
76
+ if (known?.code === 'provider-failure') {
77
+ throw new ClientError('Integration provider request failed', known.details.reason, {
78
+ details: {retry_after_seconds: known.details.retryAfterSeconds},
79
+ status: providerStatus(known.details.reason),
80
80
  });
81
81
  }
82
82
  throw error;
@@ -92,7 +92,7 @@ export function createProjectRoute(sourceControl: IntegrationSourceControlServic
92
92
  name,
93
93
  sourceConnectionId: source.connection_id,
94
94
  sourceExternalRepositoryId: source.external_repository_id,
95
- sourceControl,
95
+ integrations,
96
96
  });
97
97
 
98
98
  reply.status(201);
@@ -1,14 +1,14 @@
1
- import type {IntegrationSourceControlService} from '@shipfox/api-integration-core';
1
+ import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto';
2
2
  import type {RouteGroup} from '@shipfox/node-fastify';
3
3
  import {createProjectRoute} from './create-project.js';
4
4
  import {getProjectRoute} from './get-project.js';
5
5
  import {listProjectsRoute} from './list-projects.js';
6
6
 
7
- export function createProjectRoutes(sourceControl: IntegrationSourceControlService): RouteGroup[] {
7
+ export function createProjectRoutes(integrations: IntegrationsModuleClient): RouteGroup[] {
8
8
  return [
9
9
  {
10
10
  prefix: '/projects',
11
- routes: [createProjectRoute(sourceControl), listProjectsRoute, getProjectRoute],
11
+ routes: [createProjectRoute(integrations), listProjectsRoute, getProjectRoute],
12
12
  },
13
13
  ];
14
14
  }
@@ -4,8 +4,11 @@ import {
4
4
  setUserContext,
5
5
  type UserContextMembership,
6
6
  } from '@shipfox/api-auth-context';
7
- import type {IntegrationSourceControlService} from '@shipfox/api-integration-core';
8
- import {IntegrationConnectionNotFoundError} from '@shipfox/api-integration-core';
7
+ import {
8
+ type IntegrationsModuleClient,
9
+ integrationsInterModuleContract,
10
+ } from '@shipfox/api-integration-core-dto';
11
+ import {createInterModuleKnownError} from '@shipfox/inter-module';
9
12
  import type {AuthMethod} from '@shipfox/node-fastify';
10
13
  import {closeApp, createApp} from '@shipfox/node-fastify';
11
14
  import type {FastifyInstance, FastifyRequest} from 'fastify';
@@ -33,30 +36,21 @@ describe('project routes', () => {
33
36
  let app: FastifyInstance;
34
37
  let workspaceId: string;
35
38
  let sourceConnectionId: string;
36
- let sourceControl: IntegrationSourceControlService;
39
+ let integrations: Pick<IntegrationsModuleClient, 'resolveSourceRepository'>;
37
40
 
38
41
  beforeEach(async () => {
39
42
  await closeApp();
40
43
  workspaceId = crypto.randomUUID();
41
44
  sourceConnectionId = crypto.randomUUID();
42
45
  authenticatedMemberships = [{workspaceId, role: 'admin'}];
43
- sourceControl = {
44
- getConnection: vi.fn(),
45
- listRepositories: vi.fn(),
46
- resolveRepository: vi.fn(async () => {
46
+ integrations = {
47
+ resolveSourceRepository: vi.fn(async () => {
47
48
  await Promise.resolve();
48
49
  return {
49
50
  connection: {
50
51
  id: sourceConnectionId,
51
- workspaceId,
52
52
  provider: 'gitea' as const,
53
- externalAccountId: 'gitea-owner',
54
53
  slug: 'gitea_owner',
55
- displayName: 'Gitea',
56
- lifecycleStatus: 'active' as const,
57
- capabilities: ['source_control' as const],
58
- createdAt: new Date(),
59
- updatedAt: new Date(),
60
54
  },
61
55
  repository: {
62
56
  externalRepositoryId: 'gitea:gitea-owner/platform',
@@ -70,13 +64,10 @@ describe('project routes', () => {
70
64
  },
71
65
  };
72
66
  }),
73
- listFiles: vi.fn(),
74
- fetchFile: vi.fn(),
75
- createCheckoutSpec: vi.fn(),
76
67
  };
77
68
  app = await createApp({
78
69
  auth: [fakeUserAuth],
79
- routes: createProjectRoutes(sourceControl),
70
+ routes: createProjectRoutes(integrations as IntegrationsModuleClient),
80
71
  swagger: false,
81
72
  });
82
73
  await app.ready();
@@ -129,7 +120,7 @@ describe('project routes', () => {
129
120
  });
130
121
 
131
122
  expect(res.statusCode).toBe(400);
132
- expect(sourceControl.resolveRepository).not.toHaveBeenCalled();
123
+ expect(integrations.resolveSourceRepository).not.toHaveBeenCalled();
133
124
  });
134
125
 
135
126
  test('lists projects for a workspace with source references', async () => {
@@ -164,17 +155,11 @@ describe('project routes', () => {
164
155
  test('filters projects by `search` (case-insensitive substring on name)', async () => {
165
156
  const names = ['Platform', 'Runner', 'Notifier'];
166
157
  for (const [index, name] of names.entries()) {
167
- vi.mocked(sourceControl.resolveRepository).mockResolvedValueOnce({
158
+ vi.mocked(integrations.resolveSourceRepository).mockResolvedValueOnce({
168
159
  connection: {
169
160
  id: sourceConnectionId,
170
- workspaceId,
171
161
  provider: 'gitea',
172
- externalAccountId: 'gitea-owner',
173
162
  slug: 'gitea_owner',
174
- displayName: 'Gitea',
175
- lifecycleStatus: 'active',
176
- createdAt: new Date(),
177
- updatedAt: new Date(),
178
163
  },
179
164
  repository: {
180
165
  externalRepositoryId: `gitea:gitea-owner/${name.toLowerCase()}-${index}`,
@@ -242,8 +227,12 @@ describe('project routes', () => {
242
227
  });
243
228
 
244
229
  test('maps missing source connections to a stable error', async () => {
245
- vi.mocked(sourceControl.resolveRepository).mockRejectedValueOnce(
246
- new IntegrationConnectionNotFoundError(sourceConnectionId),
230
+ vi.mocked(integrations.resolveSourceRepository).mockRejectedValueOnce(
231
+ createInterModuleKnownError(
232
+ integrationsInterModuleContract.methods.resolveSourceRepository,
233
+ 'connection-not-found',
234
+ {connectionId: sourceConnectionId},
235
+ ),
247
236
  );
248
237
 
249
238
  const res = await app.inject({
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  INTEGRATION_SOURCE_COMMIT_PUSHED,
3
3
  type IntegrationSourceCommitPushedEvent,
4
+ type IntegrationsModuleClient,
4
5
  type SourcePushPayload,
5
6
  } from '@shipfox/api-integration-core-dto';
6
7
  import {PROJECT_SOURCE_COMMIT_OBSERVED} from '@shipfox/api-projects-dto';
@@ -140,7 +141,7 @@ describe('onSourceCommitPushed', () => {
140
141
  // The source/event filter is the subscription itself, so this module should
141
142
  // only receive the typed source-control event.
142
143
  it('registers the projects module on INTEGRATION_SOURCE_COMMIT_PUSHED', () => {
143
- const module = createProjectsModule({sourceControl: {} as never});
144
+ const module = createProjectsModule({integrations: {} as IntegrationsModuleClient});
144
145
 
145
146
  const events = module.subscribers?.map((subscriber) => subscriber.event);
146
147