@shipfox/api-projects 10.2.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +46 -0
- package/README.md +83 -0
- package/dist/core/entities/project.d.ts +4 -0
- package/dist/core/entities/project.d.ts.map +1 -1
- package/dist/core/entities/project.js.map +1 -1
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/projects.d.ts +8 -0
- package/dist/core/projects.d.ts.map +1 -1
- package/dist/core/projects.js +62 -19
- package/dist/core/projects.js.map +1 -1
- package/dist/db/db.d.ts +138 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js.map +1 -1
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/projects.d.ts +53 -0
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/projects.js +116 -18
- package/dist/db/projects.js.map +1 -1
- package/dist/db/schema/projects.d.ts +68 -0
- package/dist/db/schema/projects.d.ts.map +1 -1
- package/dist/db/schema/projects.js +12 -1
- package/dist/db/schema/projects.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/presentation/dto/project.d.ts +1 -0
- package/dist/presentation/dto/project.d.ts.map +1 -1
- package/dist/presentation/dto/project.js +1 -0
- package/dist/presentation/dto/project.js.map +1 -1
- package/dist/presentation/e2eRoutes/create-project.d.ts.map +1 -1
- package/dist/presentation/e2eRoutes/create-project.js +34 -8
- package/dist/presentation/e2eRoutes/create-project.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +4 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +44 -3
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/create-project.d.ts.map +1 -1
- package/dist/presentation/routes/create-project.js +8 -2
- package/dist/presentation/routes/create-project.js.map +1 -1
- package/dist/presentation/routes/index.d.ts.map +1 -1
- package/dist/presentation/routes/index.js +3 -1
- package/dist/presentation/routes/index.js.map +1 -1
- package/dist/presentation/routes/update-project.d.ts +2 -0
- package/dist/presentation/routes/update-project.d.ts.map +1 -0
- package/dist/presentation/routes/update-project.js +53 -0
- package/dist/presentation/routes/update-project.js.map +1 -0
- package/dist/presentation/subscribers/index.d.ts +1 -0
- package/dist/presentation/subscribers/index.d.ts.map +1 -1
- package/dist/presentation/subscribers/index.js +1 -0
- package/dist/presentation/subscribers/index.js.map +1 -1
- package/dist/presentation/subscribers/on-source-repository-updated.d.ts +4 -0
- package/dist/presentation/subscribers/on-source-repository-updated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-source-repository-updated.js +39 -0
- package/dist/presentation/subscribers/on-source-repository-updated.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0000_initial.sql +6 -0
- package/drizzle/meta/0000_snapshot.json +78 -0
- package/package.json +13 -12
- package/src/core/entities/project.ts +4 -0
- package/src/core/errors.ts +7 -0
- package/src/core/index.ts +3 -2
- package/src/core/projects.test.ts +10 -0
- package/src/core/projects.ts +90 -35
- package/src/db/db.ts +3 -0
- package/src/db/index.ts +6 -0
- package/src/db/projects.test.ts +44 -1
- package/src/db/projects.ts +242 -33
- package/src/db/schema/projects.ts +16 -0
- package/src/index.ts +9 -3
- package/src/presentation/dto/project.ts +1 -0
- package/src/presentation/e2eRoutes/create-project.test.ts +23 -0
- package/src/presentation/e2eRoutes/create-project.ts +39 -10
- package/src/presentation/inter-module.test.ts +309 -0
- package/src/presentation/inter-module.ts +76 -4
- package/src/presentation/routes/create-project.ts +10 -2
- package/src/presentation/routes/index.ts +7 -1
- package/src/presentation/routes/projects.test.ts +301 -5
- package/src/presentation/routes/update-project.ts +46 -0
- package/src/presentation/subscribers/index.ts +1 -0
- package/src/presentation/subscribers/on-source-commit-pushed.test.ts +2 -0
- package/src/presentation/subscribers/on-source-repository-updated.test.ts +108 -0
- package/src/presentation/subscribers/on-source-repository-updated.ts +50 -0
- package/test/factories/project.ts +6 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
|
|
2
|
+
import {projectsInterModuleContract} from '@shipfox/api-projects-dto/inter-module';
|
|
3
|
+
import {isInterModuleKnownError} from '@shipfox/inter-module';
|
|
4
|
+
import {createInMemoryInterModuleTransport} from '@shipfox/node-module/inter-module';
|
|
5
|
+
import {db} from '#db/index.js';
|
|
6
|
+
import {projects} from '#db/schema/projects.js';
|
|
7
|
+
import {createProjectsInterModulePresentation} from './inter-module.js';
|
|
8
|
+
|
|
9
|
+
function createClient(
|
|
10
|
+
integrations: Pick<IntegrationsModuleClient, 'resolveSourceRepository'> = {
|
|
11
|
+
resolveSourceRepository: vi.fn(async ({connectionId, externalRepositoryId}) => ({
|
|
12
|
+
connection: {id: connectionId, provider: 'github', slug: 'github'},
|
|
13
|
+
repository: {
|
|
14
|
+
externalRepositoryId,
|
|
15
|
+
owner: 'acme',
|
|
16
|
+
name: 'api',
|
|
17
|
+
fullName: 'acme/api',
|
|
18
|
+
defaultBranch: 'main',
|
|
19
|
+
visibility: 'private' as const,
|
|
20
|
+
cloneUrl: 'https://github.com/acme/api.git',
|
|
21
|
+
htmlUrl: 'https://github.com/acme/api',
|
|
22
|
+
},
|
|
23
|
+
})),
|
|
24
|
+
},
|
|
25
|
+
) {
|
|
26
|
+
const transport = createInMemoryInterModuleTransport();
|
|
27
|
+
const client = transport.createClient(projectsInterModuleContract);
|
|
28
|
+
transport.register(createProjectsInterModulePresentation({integrations}));
|
|
29
|
+
transport.seal();
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function insertProject(params: {
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
connectionId: string;
|
|
36
|
+
owner: string | null;
|
|
37
|
+
name: string | null;
|
|
38
|
+
externalRepositoryId?: string;
|
|
39
|
+
}) {
|
|
40
|
+
const [project] = await db()
|
|
41
|
+
.insert(projects)
|
|
42
|
+
.values({
|
|
43
|
+
workspaceId: params.workspaceId,
|
|
44
|
+
sourceConnectionId: params.connectionId,
|
|
45
|
+
sourceExternalRepositoryId:
|
|
46
|
+
params.externalRepositoryId ?? `repository-${crypto.randomUUID()}`,
|
|
47
|
+
sourceRepositoryOwner: params.owner,
|
|
48
|
+
sourceRepositoryName: params.name,
|
|
49
|
+
name: params.name ?? 'Project',
|
|
50
|
+
slug: `p-${crypto.randomUUID().slice(0, 8)}`,
|
|
51
|
+
})
|
|
52
|
+
.returning({
|
|
53
|
+
projectId: projects.id,
|
|
54
|
+
connectionId: projects.sourceConnectionId,
|
|
55
|
+
externalRepositoryId: projects.sourceExternalRepositoryId,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (project === undefined) throw new Error('Project insert returned no row');
|
|
59
|
+
return project;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function expectUnauthorized(
|
|
63
|
+
client: ReturnType<typeof createClient>,
|
|
64
|
+
input: Parameters<typeof client.resolveCheckoutTarget>[0],
|
|
65
|
+
) {
|
|
66
|
+
const error = await client.resolveCheckoutTarget(input).catch((caught) => caught);
|
|
67
|
+
|
|
68
|
+
expect(
|
|
69
|
+
isInterModuleKnownError(projectsInterModuleContract.methods.resolveCheckoutTarget, error),
|
|
70
|
+
).toBe(true);
|
|
71
|
+
if (isInterModuleKnownError(projectsInterModuleContract.methods.resolveCheckoutTarget, error)) {
|
|
72
|
+
expect(error.code).toBe('checkout-repository-not-authorized');
|
|
73
|
+
expect(error.details).toEqual({});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('Projects checkout target inter-module presentation', () => {
|
|
78
|
+
test('resolves a project by its workspace-scoped source repository', async () => {
|
|
79
|
+
const client = createClient();
|
|
80
|
+
const workspaceId = crypto.randomUUID();
|
|
81
|
+
const connectionId = crypto.randomUUID();
|
|
82
|
+
const project = await insertProject({
|
|
83
|
+
workspaceId,
|
|
84
|
+
connectionId,
|
|
85
|
+
owner: 'acme',
|
|
86
|
+
name: 'api',
|
|
87
|
+
externalRepositoryId: 'github:42',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await expect(
|
|
91
|
+
client.getProjectBySource({
|
|
92
|
+
workspaceId,
|
|
93
|
+
sourceConnectionId: connectionId,
|
|
94
|
+
sourceExternalRepositoryId: project.externalRepositoryId,
|
|
95
|
+
}),
|
|
96
|
+
).resolves.toMatchObject({project: {id: project.projectId}});
|
|
97
|
+
|
|
98
|
+
await expect(
|
|
99
|
+
client.getProjectBySource({
|
|
100
|
+
workspaceId: crypto.randomUUID(),
|
|
101
|
+
sourceConnectionId: connectionId,
|
|
102
|
+
sourceExternalRepositoryId: project.externalRepositoryId,
|
|
103
|
+
}),
|
|
104
|
+
).resolves.toEqual({project: null});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('resolves a project target in its workspace', async () => {
|
|
108
|
+
const client = createClient();
|
|
109
|
+
const workspaceId = crypto.randomUUID();
|
|
110
|
+
const project = await insertProject({
|
|
111
|
+
workspaceId,
|
|
112
|
+
connectionId: crypto.randomUUID(),
|
|
113
|
+
owner: 'acme',
|
|
114
|
+
name: 'api',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await expect(
|
|
118
|
+
client.resolveCheckoutTarget({
|
|
119
|
+
workspaceId,
|
|
120
|
+
defaults: {connectionId: crypto.randomUUID(), owner: 'other'},
|
|
121
|
+
target: {project: project.projectId},
|
|
122
|
+
}),
|
|
123
|
+
).resolves.toEqual(project);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('resolves a bare repository name against the default owner', async () => {
|
|
127
|
+
const client = createClient();
|
|
128
|
+
const workspaceId = crypto.randomUUID();
|
|
129
|
+
const connectionId = crypto.randomUUID();
|
|
130
|
+
const project = await insertProject({
|
|
131
|
+
workspaceId,
|
|
132
|
+
connectionId,
|
|
133
|
+
owner: 'AcMe',
|
|
134
|
+
name: 'Api',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
client.resolveCheckoutTarget({
|
|
139
|
+
workspaceId,
|
|
140
|
+
defaults: {connectionId, owner: 'acme'},
|
|
141
|
+
target: {repository: 'api'},
|
|
142
|
+
}),
|
|
143
|
+
).resolves.toEqual(project);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('resolves an owner/name repository case-insensitively', async () => {
|
|
147
|
+
const client = createClient();
|
|
148
|
+
const workspaceId = crypto.randomUUID();
|
|
149
|
+
const connectionId = crypto.randomUUID();
|
|
150
|
+
const project = await insertProject({
|
|
151
|
+
workspaceId,
|
|
152
|
+
connectionId,
|
|
153
|
+
owner: 'AcMe',
|
|
154
|
+
name: 'Api',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await expect(
|
|
158
|
+
client.resolveCheckoutTarget({
|
|
159
|
+
workspaceId,
|
|
160
|
+
defaults: {connectionId, owner: 'other'},
|
|
161
|
+
target: {repository: 'aCmE/aPI'},
|
|
162
|
+
}),
|
|
163
|
+
).resolves.toEqual(project);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('uses an explicit connection to select between identical repositories', async () => {
|
|
167
|
+
const client = createClient();
|
|
168
|
+
const workspaceId = crypto.randomUUID();
|
|
169
|
+
const defaultConnectionId = crypto.randomUUID();
|
|
170
|
+
const explicitConnectionId = crypto.randomUUID();
|
|
171
|
+
await insertProject({
|
|
172
|
+
workspaceId,
|
|
173
|
+
connectionId: defaultConnectionId,
|
|
174
|
+
owner: 'acme',
|
|
175
|
+
name: 'api',
|
|
176
|
+
});
|
|
177
|
+
const explicitProject = await insertProject({
|
|
178
|
+
workspaceId,
|
|
179
|
+
connectionId: explicitConnectionId,
|
|
180
|
+
owner: 'acme',
|
|
181
|
+
name: 'api',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await expect(
|
|
185
|
+
client.resolveCheckoutTarget({
|
|
186
|
+
workspaceId,
|
|
187
|
+
defaults: {connectionId: defaultConnectionId, owner: 'acme'},
|
|
188
|
+
target: {connection: explicitConnectionId, repository: 'acme/api'},
|
|
189
|
+
}),
|
|
190
|
+
).resolves.toEqual(explicitProject);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('rejects an ambiguous owner/name match instead of picking arbitrarily', async () => {
|
|
194
|
+
const client = createClient();
|
|
195
|
+
const workspaceId = crypto.randomUUID();
|
|
196
|
+
const connectionId = crypto.randomUUID();
|
|
197
|
+
await insertProject({workspaceId, connectionId, owner: 'acme', name: 'api'});
|
|
198
|
+
await insertProject({workspaceId, connectionId, owner: 'acme', name: 'api'});
|
|
199
|
+
|
|
200
|
+
await expectUnauthorized(client, {
|
|
201
|
+
workspaceId,
|
|
202
|
+
defaults: {connectionId, owner: 'acme'},
|
|
203
|
+
target: {repository: 'acme/api'},
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('refreshes provider metadata before resolving a repository name', async () => {
|
|
208
|
+
const workspaceId = crypto.randomUUID();
|
|
209
|
+
const connectionId = crypto.randomUUID();
|
|
210
|
+
const externalRepositoryId = `github:${crypto.randomUUID()}`;
|
|
211
|
+
const resolveSourceRepository = vi.fn(async () => ({
|
|
212
|
+
connection: {id: connectionId, provider: 'github', slug: 'github'},
|
|
213
|
+
repository: {
|
|
214
|
+
externalRepositoryId,
|
|
215
|
+
owner: 'acme',
|
|
216
|
+
name: 'new-name',
|
|
217
|
+
fullName: 'acme/new-name',
|
|
218
|
+
defaultBranch: 'main',
|
|
219
|
+
visibility: 'private' as const,
|
|
220
|
+
cloneUrl: 'https://github.com/acme/new-name.git',
|
|
221
|
+
htmlUrl: 'https://github.com/acme/new-name',
|
|
222
|
+
},
|
|
223
|
+
}));
|
|
224
|
+
const client = createClient({resolveSourceRepository});
|
|
225
|
+
const project = await insertProject({
|
|
226
|
+
workspaceId,
|
|
227
|
+
connectionId,
|
|
228
|
+
owner: 'acme',
|
|
229
|
+
name: 'old-name',
|
|
230
|
+
externalRepositoryId,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await expectUnauthorized(client, {
|
|
234
|
+
workspaceId,
|
|
235
|
+
defaults: {connectionId, owner: 'acme'},
|
|
236
|
+
target: {repository: 'acme/old-name'},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await expect(
|
|
240
|
+
client.resolveCheckoutTarget({
|
|
241
|
+
workspaceId,
|
|
242
|
+
defaults: {connectionId, owner: 'acme'},
|
|
243
|
+
target: {repository: 'acme/new-name'},
|
|
244
|
+
}),
|
|
245
|
+
).resolves.toEqual(project);
|
|
246
|
+
expect(resolveSourceRepository).toHaveBeenCalledWith({
|
|
247
|
+
workspaceId,
|
|
248
|
+
connectionId,
|
|
249
|
+
externalRepositoryId: project.externalRepositoryId,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('does not let a bare name escape the default owner', async () => {
|
|
254
|
+
const client = createClient();
|
|
255
|
+
const workspaceId = crypto.randomUUID();
|
|
256
|
+
const connectionId = crypto.randomUUID();
|
|
257
|
+
await insertProject({
|
|
258
|
+
workspaceId,
|
|
259
|
+
connectionId,
|
|
260
|
+
owner: 'other',
|
|
261
|
+
name: 'api',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await expectUnauthorized(client, {
|
|
265
|
+
workspaceId,
|
|
266
|
+
defaults: {connectionId, owner: 'acme'},
|
|
267
|
+
target: {repository: 'api'},
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('rejects a project from another workspace', async () => {
|
|
272
|
+
const client = createClient();
|
|
273
|
+
const project = await insertProject({
|
|
274
|
+
workspaceId: crypto.randomUUID(),
|
|
275
|
+
connectionId: crypto.randomUUID(),
|
|
276
|
+
owner: 'acme',
|
|
277
|
+
name: 'api',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await expectUnauthorized(client, {
|
|
281
|
+
workspaceId: crypto.randomUUID(),
|
|
282
|
+
defaults: {connectionId: project.connectionId, owner: 'acme'},
|
|
283
|
+
target: {project: project.projectId},
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test.each([
|
|
288
|
+
['an unknown target', {repository: 'acme/missing'}],
|
|
289
|
+
['a leading slash', {repository: '/api'}],
|
|
290
|
+
['a trailing slash', {repository: 'acme/'}],
|
|
291
|
+
['more than one slash', {repository: 'acme/api/extra'}],
|
|
292
|
+
])('rejects %s', async (_label, target) => {
|
|
293
|
+
const client = createClient();
|
|
294
|
+
const workspaceId = crypto.randomUUID();
|
|
295
|
+
const connectionId = crypto.randomUUID();
|
|
296
|
+
await insertProject({
|
|
297
|
+
workspaceId,
|
|
298
|
+
connectionId,
|
|
299
|
+
owner: 'acme',
|
|
300
|
+
name: 'new-name',
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await expectUnauthorized(client, {
|
|
304
|
+
workspaceId,
|
|
305
|
+
defaults: {connectionId, owner: 'acme'},
|
|
306
|
+
target,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/inter-module';
|
|
1
2
|
import {projectsInterModuleContract} from '@shipfox/api-projects-dto/inter-module';
|
|
2
3
|
import {
|
|
3
4
|
createInterModuleKnownError,
|
|
4
5
|
defineInterModulePresentation,
|
|
5
6
|
type InterModulePresentation,
|
|
6
7
|
} from '@shipfox/inter-module';
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getProjectById,
|
|
10
|
+
getProjectBySource,
|
|
11
|
+
getWorkspaceProjectCounts,
|
|
12
|
+
resolveCheckoutTarget,
|
|
13
|
+
updateProjectSourceMetadata,
|
|
14
|
+
} from '#db/projects.js';
|
|
8
15
|
|
|
9
|
-
export function createProjectsInterModulePresentation(
|
|
10
|
-
|
|
11
|
-
> {
|
|
16
|
+
export function createProjectsInterModulePresentation(params: {
|
|
17
|
+
integrations: Pick<IntegrationsModuleClient, 'resolveSourceRepository'>;
|
|
18
|
+
}): InterModulePresentation<typeof projectsInterModuleContract> {
|
|
12
19
|
return defineInterModulePresentation(projectsInterModuleContract, {
|
|
13
20
|
getProjectById: async ({projectId}) => ({project: (await getProjectById(projectId)) ?? null}),
|
|
21
|
+
getProjectBySource: async (input) => ({
|
|
22
|
+
project: (await getProjectBySource(input)) ?? null,
|
|
23
|
+
}),
|
|
14
24
|
requireProjectForWorkspace: async ({projectId, workspaceId}) => {
|
|
15
25
|
const project = await getProjectById(projectId);
|
|
16
26
|
if (project === undefined) {
|
|
@@ -32,5 +42,67 @@ export function createProjectsInterModulePresentation(): InterModulePresentation
|
|
|
32
42
|
getWorkspaceProjectCounts: async ({workspaceIds}) => ({
|
|
33
43
|
counts: await getWorkspaceProjectCounts({workspaceIds}),
|
|
34
44
|
}),
|
|
45
|
+
resolveCheckoutTarget: async (input) => {
|
|
46
|
+
const target = await resolveCheckoutTarget(input);
|
|
47
|
+
if (target === undefined) {
|
|
48
|
+
throw createInterModuleKnownError(
|
|
49
|
+
projectsInterModuleContract.methods.resolveCheckoutTarget,
|
|
50
|
+
'checkout-repository-not-authorized',
|
|
51
|
+
{},
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if ('repository' in input.target) {
|
|
56
|
+
const source = await params.integrations
|
|
57
|
+
.resolveSourceRepository({
|
|
58
|
+
workspaceId: input.workspaceId,
|
|
59
|
+
connectionId: target.connectionId,
|
|
60
|
+
externalRepositoryId: target.externalRepositoryId,
|
|
61
|
+
})
|
|
62
|
+
.catch(() => undefined);
|
|
63
|
+
if (source === undefined) {
|
|
64
|
+
throw createInterModuleKnownError(
|
|
65
|
+
projectsInterModuleContract.methods.resolveCheckoutTarget,
|
|
66
|
+
'checkout-repository-not-authorized',
|
|
67
|
+
{},
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const separator = input.target.repository.indexOf('/');
|
|
72
|
+
const requestedOwner =
|
|
73
|
+
separator === -1 ? input.defaults.owner : input.target.repository.slice(0, separator);
|
|
74
|
+
const requestedName =
|
|
75
|
+
separator === -1 ? input.target.repository : input.target.repository.slice(separator + 1);
|
|
76
|
+
const repositoryMatches =
|
|
77
|
+
source.repository.owner.toLowerCase() === requestedOwner.toLowerCase() &&
|
|
78
|
+
source.repository.name.toLowerCase() === requestedName.toLowerCase();
|
|
79
|
+
|
|
80
|
+
await updateProjectSourceMetadata({
|
|
81
|
+
workspaceId: input.workspaceId,
|
|
82
|
+
projectId: target.projectId,
|
|
83
|
+
sourceConnectionId: source.connection.id,
|
|
84
|
+
sourceExternalRepositoryId: source.repository.externalRepositoryId,
|
|
85
|
+
sourceRepositoryOwner: source.repository.owner,
|
|
86
|
+
sourceRepositoryName: source.repository.name,
|
|
87
|
+
sourceDefaultBranch: source.repository.defaultBranch,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!repositoryMatches) {
|
|
91
|
+
throw createInterModuleKnownError(
|
|
92
|
+
projectsInterModuleContract.methods.resolveCheckoutTarget,
|
|
93
|
+
'checkout-repository-not-authorized',
|
|
94
|
+
{},
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...target,
|
|
100
|
+
connectionId: source.connection.id,
|
|
101
|
+
externalRepositoryId: source.repository.externalRepositoryId,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return target;
|
|
106
|
+
},
|
|
35
107
|
});
|
|
36
108
|
}
|
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
import {createProjectBodySchema, projectResponseSchema} from '@shipfox/api-projects-dto';
|
|
7
7
|
import {isInterModuleKnownError} from '@shipfox/inter-module';
|
|
8
8
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createProjectFromSource,
|
|
11
|
+
ProjectAlreadyExistsError,
|
|
12
|
+
ProjectSlugConflictError,
|
|
13
|
+
} from '#core/index.js';
|
|
10
14
|
import {toProjectDto} from '#presentation/dto/index.js';
|
|
11
15
|
|
|
12
16
|
function providerStatus(reason: string): number {
|
|
@@ -73,6 +77,9 @@ export function createProjectRoute(integrations: IntegrationsModuleClient) {
|
|
|
73
77
|
status: 409,
|
|
74
78
|
});
|
|
75
79
|
}
|
|
80
|
+
if (error instanceof ProjectSlugConflictError) {
|
|
81
|
+
throw new ClientError('Project slug already exists', 'slug-conflict', {status: 409});
|
|
82
|
+
}
|
|
76
83
|
if (known?.code === 'provider-failure') {
|
|
77
84
|
throw new ClientError('Integration provider request failed', known.details.reason, {
|
|
78
85
|
details: {retry_after_seconds: known.details.retryAfterSeconds},
|
|
@@ -82,7 +89,7 @@ export function createProjectRoute(integrations: IntegrationsModuleClient) {
|
|
|
82
89
|
throw error;
|
|
83
90
|
},
|
|
84
91
|
handler: async (request, reply) => {
|
|
85
|
-
const {workspace_id: workspaceId, name, source} = request.body;
|
|
92
|
+
const {workspace_id: workspaceId, name, slug, source} = request.body;
|
|
86
93
|
const actor = requireUserContext(request);
|
|
87
94
|
|
|
88
95
|
requireWorkspaceAccess({request, workspaceId});
|
|
@@ -90,6 +97,7 @@ export function createProjectRoute(integrations: IntegrationsModuleClient) {
|
|
|
90
97
|
actorId: actor.userId,
|
|
91
98
|
workspaceId,
|
|
92
99
|
name,
|
|
100
|
+
slug,
|
|
93
101
|
sourceConnectionId: source.connection_id,
|
|
94
102
|
sourceExternalRepositoryId: source.external_repository_id,
|
|
95
103
|
integrations,
|
|
@@ -5,6 +5,7 @@ import {createAdminProjectsRoute} from './admin-projects.js';
|
|
|
5
5
|
import {createProjectRoute} from './create-project.js';
|
|
6
6
|
import {getProjectRoute} from './get-project.js';
|
|
7
7
|
import {listProjectsRoute} from './list-projects.js';
|
|
8
|
+
import {updateProjectRoute} from './update-project.js';
|
|
8
9
|
|
|
9
10
|
export function createProjectRoutes(
|
|
10
11
|
integrations: IntegrationsModuleClient,
|
|
@@ -13,7 +14,12 @@ export function createProjectRoutes(
|
|
|
13
14
|
return [
|
|
14
15
|
{
|
|
15
16
|
prefix: '/projects',
|
|
16
|
-
routes: [
|
|
17
|
+
routes: [
|
|
18
|
+
createProjectRoute(integrations),
|
|
19
|
+
listProjectsRoute,
|
|
20
|
+
getProjectRoute,
|
|
21
|
+
updateProjectRoute,
|
|
22
|
+
],
|
|
17
23
|
},
|
|
18
24
|
{
|
|
19
25
|
prefix: '/admin/projects',
|