@shipfox/api-projects 5.0.0 → 7.1.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 +11 -14
- package/CHANGELOG.md +42 -0
- package/dist/core/projects.d.ts +2 -2
- package/dist/core/projects.d.ts.map +1 -1
- package/dist/core/projects.js +2 -1
- package/dist/core/projects.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +4 -0
- package/dist/presentation/inter-module.d.ts.map +1 -0
- package/dist/presentation/inter-module.js +29 -0
- package/dist/presentation/inter-module.js.map +1 -0
- package/dist/presentation/routes/create-project.d.ts +2 -2
- package/dist/presentation/routes/create-project.d.ts.map +1 -1
- package/dist/presentation/routes/create-project.js +19 -20
- package/dist/presentation/routes/create-project.js.map +1 -1
- package/dist/presentation/routes/index.d.ts +2 -2
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +2 -2
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +17164 -3306
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +12 -28
- package/src/core/projects.test.ts +21 -25
- package/src/core/projects.ts +7 -3
- package/src/index.ts +6 -4
- package/src/presentation/inter-module.ts +33 -0
- package/src/presentation/routes/create-project.ts +41 -41
- package/src/presentation/routes/index.ts +3 -3
- package/src/presentation/routes/projects.test.ts +17 -28
- package/src/presentation/subscribers/on-source-commit-pushed.test.ts +2 -1
- 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": "
|
|
4
|
+
"version": "7.1.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -25,39 +25,23 @@
|
|
|
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": "
|
|
29
|
-
"@shipfox/api-integration-core": "
|
|
30
|
-
"@shipfox/api-
|
|
31
|
-
"@shipfox/
|
|
32
|
-
"@shipfox/node-drizzle": "0.3.
|
|
33
|
-
"@shipfox/node-fastify": "0.
|
|
34
|
-
"@shipfox/node-module": "0.
|
|
35
|
-
"@shipfox/node-opentelemetry": "0.
|
|
36
|
-
"@shipfox/node-outbox": "0.2.
|
|
28
|
+
"@shipfox/api-auth-context": "7.1.0",
|
|
29
|
+
"@shipfox/api-integration-core-dto": "6.0.0",
|
|
30
|
+
"@shipfox/api-projects-dto": "6.0.0",
|
|
31
|
+
"@shipfox/inter-module": "0.2.0",
|
|
32
|
+
"@shipfox/node-drizzle": "0.3.2",
|
|
33
|
+
"@shipfox/node-fastify": "0.3.0",
|
|
34
|
+
"@shipfox/node-module": "0.5.0",
|
|
35
|
+
"@shipfox/node-opentelemetry": "0.6.0",
|
|
36
|
+
"@shipfox/node-outbox": "0.2.4",
|
|
37
37
|
"@shipfox/node-postgres": "0.4.2",
|
|
38
|
-
"@shipfox/node-temporal": "0.
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@temporalio/activity": "1.18.1",
|
|
42
|
-
"@temporalio/client": "1.18.1",
|
|
43
|
-
"@temporalio/common": "1.18.1",
|
|
44
|
-
"@temporalio/testing": "1.18.1",
|
|
45
|
-
"@temporalio/worker": "1.18.1",
|
|
46
|
-
"@types/pg": "^8.15.5",
|
|
47
|
-
"drizzle-kit": "^0.31.10",
|
|
48
|
-
"fastify": "^5.3.3",
|
|
49
|
-
"fastify-type-provider-zod": "^6.0.0",
|
|
50
|
-
"fishery": "^2.4.0",
|
|
51
|
-
"@shipfox/biome": "1.8.2",
|
|
52
|
-
"@shipfox/swc": "1.2.6",
|
|
53
|
-
"@shipfox/ts-config": "1.3.8",
|
|
54
|
-
"@shipfox/typescript": "1.1.7",
|
|
55
|
-
"@shipfox/vitest": "1.2.3"
|
|
38
|
+
"@shipfox/node-temporal": "0.4.0"
|
|
56
39
|
},
|
|
57
40
|
"scripts": {
|
|
58
41
|
"build": "shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js",
|
|
59
42
|
"check": "shipfox-biome-check",
|
|
60
43
|
"check:fix": "shipfox-biome-check --write",
|
|
44
|
+
"depcruise": "shipfox-depcruise",
|
|
61
45
|
"test": "shipfox-vitest-run",
|
|
62
46
|
"test:watch": "shipfox-vitest-watch",
|
|
63
47
|
"type": "shipfox-tsc-check",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
128
|
-
|
|
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
|
-
|
|
130
|
+
integrations: integrations as IntegrationsModuleClient,
|
|
138
131
|
});
|
|
139
132
|
|
|
140
|
-
await expect(result).rejects.toMatchObject({
|
|
133
|
+
await expect(result).rejects.toMatchObject({
|
|
134
|
+
code: 'provider-failure',
|
|
135
|
+
details: {reason: 'repository-not-found'},
|
|
136
|
+
});
|
|
141
137
|
});
|
|
142
138
|
});
|
package/src/core/projects.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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
|
-
|
|
22
|
+
integrations: IntegrationsModuleClient;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export async function createProjectFromSource(
|
|
26
26
|
params: CreateProjectFromSourceParams,
|
|
27
27
|
): Promise<Project> {
|
|
28
|
-
|
|
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 {
|
|
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
|
-
|
|
45
|
+
integrations: IntegrationsModuleClient;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
export function createProjectsModule({
|
|
48
|
+
export function createProjectsModule({integrations}: CreateProjectsModuleOptions): ShipfoxModule {
|
|
48
49
|
return {
|
|
49
50
|
name: 'projects',
|
|
50
51
|
database: {db, migrationsPath},
|
|
51
|
-
routes: createProjectRoutes(
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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 (
|
|
55
|
-
throw new ClientError(
|
|
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 (
|
|
58
|
-
throw new ClientError(
|
|
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 (
|
|
61
|
-
throw new ClientError(
|
|
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 (
|
|
64
|
-
throw new ClientError(
|
|
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 (
|
|
77
|
-
throw new ClientError(
|
|
78
|
-
details: {retry_after_seconds:
|
|
79
|
-
status: providerStatus(
|
|
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
|
-
|
|
95
|
+
integrations,
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
reply.status(201);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import type {
|
|
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(
|
|
7
|
+
export function createProjectRoutes(integrations: IntegrationsModuleClient): RouteGroup[] {
|
|
8
8
|
return [
|
|
9
9
|
{
|
|
10
10
|
prefix: '/projects',
|
|
11
|
-
routes: [createProjectRoute(
|
|
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
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
246
|
-
|
|
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({
|
|
144
|
+
const module = createProjectsModule({integrations: {} as IntegrationsModuleClient});
|
|
144
145
|
|
|
145
146
|
const events = module.subscribers?.map((subscriber) => subscriber.event);
|
|
146
147
|
|