@shipfox/api-projects 11.0.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 +39 -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 +300 -4
- 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
package/drizzle/0000_initial.sql
CHANGED
|
@@ -3,7 +3,11 @@ CREATE TABLE "projects_projects" (
|
|
|
3
3
|
"workspace_id" uuid NOT NULL,
|
|
4
4
|
"source_connection_id" uuid NOT NULL,
|
|
5
5
|
"source_external_repository_id" text NOT NULL,
|
|
6
|
+
"source_repository_owner" text,
|
|
7
|
+
"source_repository_name" text,
|
|
8
|
+
"source_default_branch" text,
|
|
6
9
|
"name" text NOT NULL,
|
|
10
|
+
"slug" text NOT NULL,
|
|
7
11
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
8
12
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
9
13
|
);
|
|
@@ -30,7 +34,9 @@ CREATE TABLE "projects_integration_event_dedup" (
|
|
|
30
34
|
);
|
|
31
35
|
--> statement-breakpoint
|
|
32
36
|
CREATE UNIQUE INDEX "projects_source_unique" ON "projects_projects" USING btree ("source_connection_id","source_external_repository_id");--> statement-breakpoint
|
|
37
|
+
CREATE UNIQUE INDEX "projects_workspace_slug_unique" ON "projects_projects" USING btree ("workspace_id","slug");--> statement-breakpoint
|
|
33
38
|
CREATE INDEX "projects_workspace_created_id_idx" ON "projects_projects" USING btree ("workspace_id","created_at","id");--> statement-breakpoint
|
|
39
|
+
CREATE INDEX "projects_source_repository_lookup_idx" ON "projects_projects" USING btree ("workspace_id","source_connection_id",lower("source_repository_owner"),lower("source_repository_name"));--> statement-breakpoint
|
|
34
40
|
CREATE INDEX "projects_outbox_pending_idx" ON "projects_outbox" USING btree ("next_dispatch_at","created_at") WHERE "dispatched_at" IS NULL AND "dead_lettered_at" IS NULL;
|
|
35
41
|
--> statement-breakpoint
|
|
36
42
|
CREATE INDEX "projects_outbox_dispatched_retention_idx" ON "projects_outbox" USING btree ("dispatched_at","id") WHERE "dispatched_at" IS NOT NULL;--> statement-breakpoint
|
|
@@ -214,12 +214,36 @@
|
|
|
214
214
|
"primaryKey": false,
|
|
215
215
|
"notNull": true
|
|
216
216
|
},
|
|
217
|
+
"source_repository_owner": {
|
|
218
|
+
"name": "source_repository_owner",
|
|
219
|
+
"type": "text",
|
|
220
|
+
"primaryKey": false,
|
|
221
|
+
"notNull": false
|
|
222
|
+
},
|
|
223
|
+
"source_repository_name": {
|
|
224
|
+
"name": "source_repository_name",
|
|
225
|
+
"type": "text",
|
|
226
|
+
"primaryKey": false,
|
|
227
|
+
"notNull": false
|
|
228
|
+
},
|
|
229
|
+
"source_default_branch": {
|
|
230
|
+
"name": "source_default_branch",
|
|
231
|
+
"type": "text",
|
|
232
|
+
"primaryKey": false,
|
|
233
|
+
"notNull": false
|
|
234
|
+
},
|
|
217
235
|
"name": {
|
|
218
236
|
"name": "name",
|
|
219
237
|
"type": "text",
|
|
220
238
|
"primaryKey": false,
|
|
221
239
|
"notNull": true
|
|
222
240
|
},
|
|
241
|
+
"slug": {
|
|
242
|
+
"name": "slug",
|
|
243
|
+
"type": "text",
|
|
244
|
+
"primaryKey": false,
|
|
245
|
+
"notNull": true
|
|
246
|
+
},
|
|
223
247
|
"created_at": {
|
|
224
248
|
"name": "created_at",
|
|
225
249
|
"type": "timestamp with time zone",
|
|
@@ -257,6 +281,27 @@
|
|
|
257
281
|
"method": "btree",
|
|
258
282
|
"with": {}
|
|
259
283
|
},
|
|
284
|
+
"projects_workspace_slug_unique": {
|
|
285
|
+
"name": "projects_workspace_slug_unique",
|
|
286
|
+
"columns": [
|
|
287
|
+
{
|
|
288
|
+
"expression": "workspace_id",
|
|
289
|
+
"isExpression": false,
|
|
290
|
+
"asc": true,
|
|
291
|
+
"nulls": "last"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"expression": "slug",
|
|
295
|
+
"isExpression": false,
|
|
296
|
+
"asc": true,
|
|
297
|
+
"nulls": "last"
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
"isUnique": true,
|
|
301
|
+
"concurrently": false,
|
|
302
|
+
"method": "btree",
|
|
303
|
+
"with": {}
|
|
304
|
+
},
|
|
260
305
|
"projects_workspace_created_id_idx": {
|
|
261
306
|
"name": "projects_workspace_created_id_idx",
|
|
262
307
|
"columns": [
|
|
@@ -283,6 +328,39 @@
|
|
|
283
328
|
"concurrently": false,
|
|
284
329
|
"method": "btree",
|
|
285
330
|
"with": {}
|
|
331
|
+
},
|
|
332
|
+
"projects_source_repository_lookup_idx": {
|
|
333
|
+
"name": "projects_source_repository_lookup_idx",
|
|
334
|
+
"columns": [
|
|
335
|
+
{
|
|
336
|
+
"expression": "workspace_id",
|
|
337
|
+
"isExpression": false,
|
|
338
|
+
"asc": true,
|
|
339
|
+
"nulls": "last"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"expression": "source_connection_id",
|
|
343
|
+
"isExpression": false,
|
|
344
|
+
"asc": true,
|
|
345
|
+
"nulls": "last"
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"expression": "lower(\"source_repository_owner\")",
|
|
349
|
+
"asc": true,
|
|
350
|
+
"isExpression": true,
|
|
351
|
+
"nulls": "last"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"expression": "lower(\"source_repository_name\")",
|
|
355
|
+
"asc": true,
|
|
356
|
+
"isExpression": true,
|
|
357
|
+
"nulls": "last"
|
|
358
|
+
}
|
|
359
|
+
],
|
|
360
|
+
"isUnique": false,
|
|
361
|
+
"concurrently": false,
|
|
362
|
+
"method": "btree",
|
|
363
|
+
"with": {}
|
|
286
364
|
}
|
|
287
365
|
},
|
|
288
366
|
"foreignKeys": {},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-projects",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "12.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -21,18 +21,19 @@
|
|
|
21
21
|
"@temporalio/workflow": "1.18.1",
|
|
22
22
|
"drizzle-orm": "^0.45.2",
|
|
23
23
|
"zod": "^4.4.3",
|
|
24
|
-
"@shipfox/api-auth-
|
|
25
|
-
"@shipfox/api-
|
|
26
|
-
"@shipfox/api-
|
|
27
|
-
"@shipfox/api-projects-dto": "
|
|
28
|
-
"@shipfox/inter-module": "0.2.
|
|
29
|
-
"@shipfox/node-drizzle": "0.3.
|
|
30
|
-
"@shipfox/
|
|
31
|
-
"@shipfox/node-
|
|
24
|
+
"@shipfox/api-auth-context": "12.0.0",
|
|
25
|
+
"@shipfox/api-common-dto": "12.0.0",
|
|
26
|
+
"@shipfox/api-auth-dto": "12.0.0",
|
|
27
|
+
"@shipfox/api-projects-dto": "12.0.0",
|
|
28
|
+
"@shipfox/inter-module": "0.2.3",
|
|
29
|
+
"@shipfox/node-drizzle": "0.3.5",
|
|
30
|
+
"@shipfox/api-integration-core-dto": "12.0.0",
|
|
31
|
+
"@shipfox/node-fastify": "0.4.1",
|
|
32
|
+
"@shipfox/node-module": "1.0.5",
|
|
32
33
|
"@shipfox/node-opentelemetry": "0.6.3",
|
|
33
|
-
"@shipfox/node-
|
|
34
|
-
"@shipfox/node-
|
|
35
|
-
"@shipfox/node-
|
|
34
|
+
"@shipfox/node-postgres": "0.5.0",
|
|
35
|
+
"@shipfox/node-temporal": "0.4.4",
|
|
36
|
+
"@shipfox/node-outbox": "0.2.6"
|
|
36
37
|
},
|
|
37
38
|
"imports": {
|
|
38
39
|
"#*": "./dist/*"
|
|
@@ -3,7 +3,11 @@ export interface Project {
|
|
|
3
3
|
workspaceId: string;
|
|
4
4
|
sourceConnectionId: string;
|
|
5
5
|
sourceExternalRepositoryId: string;
|
|
6
|
+
sourceRepositoryOwner: string | null;
|
|
7
|
+
sourceRepositoryName: string | null;
|
|
8
|
+
sourceDefaultBranch: string | null;
|
|
6
9
|
name: string;
|
|
10
|
+
slug: string;
|
|
7
11
|
createdAt: Date;
|
|
8
12
|
updatedAt: Date;
|
|
9
13
|
}
|
package/src/core/errors.ts
CHANGED
|
@@ -24,3 +24,10 @@ export class ProjectAlreadyExistsError extends Error {
|
|
|
24
24
|
this.name = 'ProjectAlreadyExistsError';
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
export class ProjectSlugConflictError extends Error {
|
|
29
|
+
constructor(public readonly slug: string) {
|
|
30
|
+
super(`Project slug already exists: ${slug}`);
|
|
31
|
+
this.name = 'ProjectSlugConflictError';
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export {
|
|
|
3
3
|
ProjectAccessDeniedError,
|
|
4
4
|
ProjectAlreadyExistsError,
|
|
5
5
|
ProjectNotFoundError,
|
|
6
|
+
ProjectSlugConflictError,
|
|
6
7
|
} from './errors.js';
|
|
7
|
-
export type {CreateProjectFromSourceParams} from './projects.js';
|
|
8
|
-
export {createProjectFromSource} from './projects.js';
|
|
8
|
+
export type {CreateProjectFromSourceParams, UpdateProjectDetailsParams} from './projects.js';
|
|
9
|
+
export {createProjectFromSource, updateProjectDetails} from './projects.js';
|
|
@@ -45,6 +45,7 @@ describe('createProjectFromSource', () => {
|
|
|
45
45
|
actorId,
|
|
46
46
|
workspaceId,
|
|
47
47
|
name: 'Platform',
|
|
48
|
+
slug: 'platform',
|
|
48
49
|
sourceConnectionId,
|
|
49
50
|
sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
|
|
50
51
|
integrations: integrations as IntegrationsModuleClient,
|
|
@@ -52,8 +53,12 @@ describe('createProjectFromSource', () => {
|
|
|
52
53
|
|
|
53
54
|
expect(project.workspaceId).toBe(workspaceId);
|
|
54
55
|
expect(project.name).toBe('Platform');
|
|
56
|
+
expect(project.slug).toBe('platform');
|
|
55
57
|
expect(project.sourceConnectionId).toBe(sourceConnectionId);
|
|
56
58
|
expect(project.sourceExternalRepositoryId).toBe('gitea:gitea-owner/platform');
|
|
59
|
+
expect(project.sourceRepositoryOwner).toBe('gitea-owner');
|
|
60
|
+
expect(project.sourceRepositoryName).toBe('platform');
|
|
61
|
+
expect(project.sourceDefaultBranch).toBe('main');
|
|
57
62
|
});
|
|
58
63
|
|
|
59
64
|
test('emits project lifecycle events in the same transaction', async () => {
|
|
@@ -61,6 +66,7 @@ describe('createProjectFromSource', () => {
|
|
|
61
66
|
actorId,
|
|
62
67
|
workspaceId,
|
|
63
68
|
name: 'Platform',
|
|
69
|
+
slug: 'platform',
|
|
64
70
|
sourceConnectionId,
|
|
65
71
|
sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
|
|
66
72
|
integrations: integrations as IntegrationsModuleClient,
|
|
@@ -78,6 +84,7 @@ describe('createProjectFromSource', () => {
|
|
|
78
84
|
expect(events.map((event) => event.payload)).toEqual(
|
|
79
85
|
expect.arrayContaining([
|
|
80
86
|
expect.objectContaining({
|
|
87
|
+
slug: 'platform',
|
|
81
88
|
sourceConnectionId,
|
|
82
89
|
sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
|
|
83
90
|
}),
|
|
@@ -95,6 +102,7 @@ describe('createProjectFromSource', () => {
|
|
|
95
102
|
actorId,
|
|
96
103
|
workspaceId,
|
|
97
104
|
name: 'Platform',
|
|
105
|
+
slug: 'platform',
|
|
98
106
|
sourceConnectionId,
|
|
99
107
|
sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
|
|
100
108
|
integrations: integrations as IntegrationsModuleClient,
|
|
@@ -104,6 +112,7 @@ describe('createProjectFromSource', () => {
|
|
|
104
112
|
actorId,
|
|
105
113
|
workspaceId,
|
|
106
114
|
name: 'Platform Again',
|
|
115
|
+
slug: 'platform-again',
|
|
107
116
|
sourceConnectionId,
|
|
108
117
|
sourceExternalRepositoryId: 'gitea:gitea-owner/platform',
|
|
109
118
|
integrations: integrations as IntegrationsModuleClient,
|
|
@@ -125,6 +134,7 @@ describe('createProjectFromSource', () => {
|
|
|
125
134
|
actorId,
|
|
126
135
|
workspaceId,
|
|
127
136
|
name: 'Missing',
|
|
137
|
+
slug: 'missing',
|
|
128
138
|
sourceConnectionId,
|
|
129
139
|
sourceExternalRepositoryId: 'not-found',
|
|
130
140
|
integrations: integrations as IntegrationsModuleClient,
|
package/src/core/projects.ts
CHANGED
|
@@ -2,16 +2,25 @@ import type {IntegrationsModuleClient} from '@shipfox/api-integration-core-dto/i
|
|
|
2
2
|
import {
|
|
3
3
|
PROJECT_CREATED,
|
|
4
4
|
PROJECT_SOURCE_BOUND,
|
|
5
|
+
PROJECT_UPDATED,
|
|
5
6
|
type ProjectsEventMap,
|
|
6
7
|
} from '@shipfox/api-projects-dto';
|
|
8
|
+
import {isUniqueViolation} from '@shipfox/node-drizzle';
|
|
7
9
|
import {writeOutboxEvent} from '@shipfox/node-outbox';
|
|
8
10
|
import {and, eq} from 'drizzle-orm';
|
|
9
11
|
import {db} from '#db/db.js';
|
|
12
|
+
import {updateProject} from '#db/projects.js';
|
|
10
13
|
import {projectsOutbox} from '#db/schema/outbox.js';
|
|
11
14
|
import {projects, toProject} from '#db/schema/projects.js';
|
|
12
15
|
import {recordProjectCreated} from '#metrics/instance.js';
|
|
13
16
|
import type {Project} from './entities/project.js';
|
|
14
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
ProjectAlreadyExistsError,
|
|
19
|
+
ProjectNotFoundError,
|
|
20
|
+
ProjectSlugConflictError,
|
|
21
|
+
} from './errors.js';
|
|
22
|
+
|
|
23
|
+
const PROJECTS_WORKSPACE_SLUG_UNIQUE_CONSTRAINT = 'projects_workspace_slug_unique';
|
|
15
24
|
|
|
16
25
|
export interface CreateProjectFromSourceParams {
|
|
17
26
|
actorId: string;
|
|
@@ -19,54 +28,68 @@ export interface CreateProjectFromSourceParams {
|
|
|
19
28
|
name: string;
|
|
20
29
|
sourceConnectionId: string;
|
|
21
30
|
sourceExternalRepositoryId: string;
|
|
31
|
+
slug: string;
|
|
22
32
|
integrations: IntegrationsModuleClient;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
export async function createProjectFromSource(
|
|
26
36
|
params: CreateProjectFromSourceParams,
|
|
27
37
|
): Promise<Project> {
|
|
28
|
-
|
|
29
|
-
connection: {id: string; provider: string};
|
|
30
|
-
repository: {externalRepositoryId: string};
|
|
31
|
-
};
|
|
32
|
-
source = await params.integrations.resolveSourceRepository({
|
|
38
|
+
const source = await params.integrations.resolveSourceRepository({
|
|
33
39
|
workspaceId: params.workspaceId,
|
|
34
40
|
connectionId: params.sourceConnectionId,
|
|
35
41
|
externalRepositoryId: params.sourceExternalRepositoryId,
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
const project = await db().transaction(async (tx) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
let projectRow: typeof projects.$inferSelect | undefined;
|
|
46
|
+
for (let attempt = 0; attempt < 2 && !projectRow; attempt += 1) {
|
|
47
|
+
try {
|
|
48
|
+
[projectRow] = await tx
|
|
49
|
+
.insert(projects)
|
|
50
|
+
.values({
|
|
51
|
+
workspaceId: params.workspaceId,
|
|
52
|
+
sourceConnectionId: source.connection.id,
|
|
53
|
+
sourceExternalRepositoryId: source.repository.externalRepositoryId,
|
|
54
|
+
sourceRepositoryOwner: source.repository.owner,
|
|
55
|
+
sourceRepositoryName: source.repository.name,
|
|
56
|
+
sourceDefaultBranch: source.repository.defaultBranch,
|
|
57
|
+
name: params.name,
|
|
58
|
+
slug: params.slug,
|
|
59
|
+
})
|
|
60
|
+
.onConflictDoNothing({
|
|
61
|
+
target: [projects.sourceConnectionId, projects.sourceExternalRepositoryId],
|
|
62
|
+
})
|
|
63
|
+
.returning();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isUniqueViolation(error, PROJECTS_WORKSPACE_SLUG_UNIQUE_CONSTRAINT)) {
|
|
66
|
+
throw new ProjectSlugConflictError(params.slug);
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
51
70
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
if (!projectRow) {
|
|
72
|
+
const [existing] = await tx
|
|
73
|
+
.select()
|
|
74
|
+
.from(projects)
|
|
75
|
+
.where(
|
|
76
|
+
and(
|
|
77
|
+
eq(projects.sourceConnectionId, source.connection.id),
|
|
78
|
+
eq(projects.sourceExternalRepositoryId, source.repository.externalRepositoryId),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
.limit(1);
|
|
82
|
+
if (existing) {
|
|
83
|
+
throw new ProjectAlreadyExistsError(
|
|
84
|
+
existing.id,
|
|
85
|
+
source.connection.id,
|
|
86
|
+
source.repository.externalRepositoryId,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
69
89
|
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!projectRow) {
|
|
70
93
|
throw new Error('Project insert returned no rows');
|
|
71
94
|
}
|
|
72
95
|
|
|
@@ -78,6 +101,7 @@ export async function createProjectFromSource(
|
|
|
78
101
|
actorId: params.actorId,
|
|
79
102
|
workspaceId: project.workspaceId,
|
|
80
103
|
projectId: project.id,
|
|
104
|
+
slug: project.slug,
|
|
81
105
|
sourceConnectionId: project.sourceConnectionId,
|
|
82
106
|
sourceExternalRepositoryId: project.sourceExternalRepositoryId,
|
|
83
107
|
},
|
|
@@ -99,3 +123,34 @@ export async function createProjectFromSource(
|
|
|
99
123
|
recordProjectCreated();
|
|
100
124
|
return project;
|
|
101
125
|
}
|
|
126
|
+
|
|
127
|
+
export interface UpdateProjectDetailsParams {
|
|
128
|
+
actorId: string;
|
|
129
|
+
projectId: string;
|
|
130
|
+
name?: string | undefined;
|
|
131
|
+
slug?: string | undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function updateProjectDetails(params: UpdateProjectDetailsParams): Promise<Project> {
|
|
135
|
+
return db().transaction(async (tx) => {
|
|
136
|
+
const update = await updateProject(
|
|
137
|
+
{projectId: params.projectId, name: params.name, slug: params.slug},
|
|
138
|
+
{tx},
|
|
139
|
+
);
|
|
140
|
+
if (!update) throw new ProjectNotFoundError(params.projectId);
|
|
141
|
+
if (!update.changed) return update.project;
|
|
142
|
+
|
|
143
|
+
await writeOutboxEvent<ProjectsEventMap>(tx, projectsOutbox, {
|
|
144
|
+
type: PROJECT_UPDATED,
|
|
145
|
+
payload: {
|
|
146
|
+
actorId: params.actorId,
|
|
147
|
+
workspaceId: update.project.workspaceId,
|
|
148
|
+
projectId: update.project.id,
|
|
149
|
+
name: update.project.name,
|
|
150
|
+
slug: update.project.slug,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return update.project;
|
|
155
|
+
});
|
|
156
|
+
}
|
package/src/db/db.ts
CHANGED
package/src/db/index.ts
CHANGED
|
@@ -14,6 +14,10 @@ export type {
|
|
|
14
14
|
ListAdminProjectsResult,
|
|
15
15
|
ListProjectsParams,
|
|
16
16
|
ListProjectsResult,
|
|
17
|
+
ResolveCheckoutTargetParams,
|
|
18
|
+
ResolvedCheckoutTarget,
|
|
19
|
+
UpdateProjectParams,
|
|
20
|
+
UpdateProjectResult,
|
|
17
21
|
} from './projects.js';
|
|
18
22
|
export {
|
|
19
23
|
createProject,
|
|
@@ -24,6 +28,8 @@ export {
|
|
|
24
28
|
listAdminProjects,
|
|
25
29
|
listProjects,
|
|
26
30
|
requireProjectForWorkspace,
|
|
31
|
+
resolveCheckoutTarget,
|
|
32
|
+
updateProject,
|
|
27
33
|
} from './projects.js';
|
|
28
34
|
export {projectsIntegrationEventDedup} from './schema/integration-event-dedup.js';
|
|
29
35
|
export {projectsOutbox} from './schema/outbox.js';
|
package/src/db/projects.test.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import {eq} from 'drizzle-orm';
|
|
1
2
|
import {projectFactory} from '#test/index.js';
|
|
2
|
-
import {
|
|
3
|
+
import {db} from './db.js';
|
|
4
|
+
import {
|
|
5
|
+
getProjectById,
|
|
6
|
+
getProjectCount,
|
|
7
|
+
getWorkspaceProjectCounts,
|
|
8
|
+
updateProject,
|
|
9
|
+
} from './projects.js';
|
|
10
|
+
import {projects} from './schema/projects.js';
|
|
3
11
|
|
|
4
12
|
describe('getProjectCount', () => {
|
|
5
13
|
it('reports the current project count', async () => {
|
|
@@ -28,3 +36,38 @@ describe('getProjectCount', () => {
|
|
|
28
36
|
expect(counts).toEqual([{workspaceId, count: 0}]);
|
|
29
37
|
});
|
|
30
38
|
});
|
|
39
|
+
|
|
40
|
+
describe('updateProject', () => {
|
|
41
|
+
it('preserves a concurrent update to fields omitted from a partial update', async () => {
|
|
42
|
+
const project = await projectFactory.create();
|
|
43
|
+
let allowStaleTransactionUpdate!: () => void;
|
|
44
|
+
let staleTransactionRead!: () => void;
|
|
45
|
+
const staleTransactionCanUpdate = new Promise<void>((resolve) => {
|
|
46
|
+
allowStaleTransactionUpdate = resolve;
|
|
47
|
+
});
|
|
48
|
+
const staleTransactionHasRead = new Promise<void>((resolve) => {
|
|
49
|
+
staleTransactionRead = resolve;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const staleTransaction = db().transaction(async (tx) => {
|
|
53
|
+
await tx.select({id: projects.id}).from(projects).where(eq(projects.id, project.id)).limit(1);
|
|
54
|
+
staleTransactionRead();
|
|
55
|
+
await staleTransactionCanUpdate;
|
|
56
|
+
return updateProject({projectId: project.id, slug: 'stale-slug-update'}, {tx});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await staleTransactionHasRead;
|
|
60
|
+
try {
|
|
61
|
+
await updateProject({projectId: project.id, name: 'Concurrent name'});
|
|
62
|
+
} finally {
|
|
63
|
+
allowStaleTransactionUpdate();
|
|
64
|
+
}
|
|
65
|
+
await staleTransaction;
|
|
66
|
+
|
|
67
|
+
const finalProject = await getProjectById(project.id);
|
|
68
|
+
expect(finalProject).toMatchObject({
|
|
69
|
+
name: 'Concurrent name',
|
|
70
|
+
slug: 'stale-slug-update',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|