Skip to content

Commit ea88578

Browse files
authored
feat(projects): add project identity and lifecycle foundation (#8580)
* feat(projects): add project identity and lifecycle foundation * feat(projects): create projects with their initial environment * docs(projects): record project files follow-up * docs(projects): explain project and workspace creation flows * fix(projects): stage activation after compatible writers deploy * refactor(projects): prepare compatible writers for the SQL backfill * fix(projects): clean up automatically created fixture Projects * fix(workflows): guard restore against concurrent workspace archive * fix(projects): close lifecycle races and surface rollout conflicts * fix(workflows): return not found when import loses archive race
1 parent c723046 commit ea88578

66 files changed

Lines changed: 33464 additions & 399 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16016,6 +16016,16 @@
1601616016
"disableKnowledgeBaseExport": {
1601716017
"type": "boolean",
1601816018
"description": "Prevent downloading a whole knowledge base as an archive."
16019+
},
16020+
"deniedPartialAccessProjectIssues": {
16021+
"default": [],
16022+
"type": "array",
16023+
"items": {
16024+
"type": "string",
16025+
"minLength": 1,
16026+
"maxLength": 255
16027+
},
16028+
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
1601916029
}
1602016030
},
1602116031
"required": [
@@ -16060,7 +16070,8 @@
1606016070
"disableToolAutoApproval",
1606116071
"hideSandboxesTab",
1606216072
"disableOAuthAppAccess",
16063-
"disableKnowledgeBaseExport"
16073+
"disableKnowledgeBaseExport",
16074+
"deniedPartialAccessProjectIssues"
1606416075
],
1606516076
"additionalProperties": false,
1606616077
"description": "Resolved restrictions. True disables a boolean capability; null allowlists permit every value and empty allowlists permit none."
@@ -16190,7 +16201,8 @@
1619016201
"disableToolAutoApproval": false,
1619116202
"hideSandboxesTab": false,
1619216203
"disableOAuthAppAccess": false,
16193-
"disableKnowledgeBaseExport": false
16204+
"disableKnowledgeBaseExport": false,
16205+
"deniedPartialAccessProjectIssues": []
1619416206
},
1619516207
"isDefault": false,
1619616208
"membershipMode": "inherit",
@@ -16265,7 +16277,8 @@
1626516277
"disableToolAutoApproval": false,
1626616278
"hideSandboxesTab": false,
1626716279
"disableOAuthAppAccess": false,
16268-
"disableKnowledgeBaseExport": false
16280+
"disableKnowledgeBaseExport": false,
16281+
"deniedPartialAccessProjectIssues": []
1626916282
},
1627016283
"isDefault": false,
1627116284
"membershipMode": "inherit",
@@ -16519,6 +16532,15 @@
1651916532
"disableKnowledgeBaseExport": {
1652016533
"type": "boolean",
1652116534
"description": "Prevent downloading a whole knowledge base as an archive."
16535+
},
16536+
"deniedPartialAccessProjectIssues": {
16537+
"type": "array",
16538+
"items": {
16539+
"type": "string",
16540+
"minLength": 1,
16541+
"maxLength": 255
16542+
},
16543+
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
1652216544
}
1652316545
},
1652416546
"additionalProperties": false,
@@ -16610,7 +16632,8 @@
1661016632
"disableToolAutoApproval": false,
1661116633
"hideSandboxesTab": false,
1661216634
"disableOAuthAppAccess": false,
16613-
"disableKnowledgeBaseExport": false
16635+
"disableKnowledgeBaseExport": false,
16636+
"deniedPartialAccessProjectIssues": []
1661416637
},
1661516638
"isDefault": false,
1661616639
"membershipMode": "inherit",
@@ -16683,7 +16706,8 @@
1668316706
"disableToolAutoApproval": false,
1668416707
"hideSandboxesTab": false,
1668516708
"disableOAuthAppAccess": false,
16686-
"disableKnowledgeBaseExport": false
16709+
"disableKnowledgeBaseExport": false,
16710+
"deniedPartialAccessProjectIssues": []
1668716711
},
1668816712
"isDefault": false,
1668916713
"membershipMode": "inherit",
@@ -16944,6 +16968,15 @@
1694416968
"disableKnowledgeBaseExport": {
1694516969
"type": "boolean",
1694616970
"description": "Prevent downloading a whole knowledge base as an archive."
16971+
},
16972+
"deniedPartialAccessProjectIssues": {
16973+
"type": "array",
16974+
"items": {
16975+
"type": "string",
16976+
"minLength": 1,
16977+
"maxLength": 255
16978+
},
16979+
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
1694716980
}
1694816981
},
1694916982
"additionalProperties": false,
@@ -18220,6 +18253,16 @@
1822018253
"disableKnowledgeBaseExport": {
1822118254
"type": "boolean",
1822218255
"description": "Prevent downloading a whole knowledge base as an archive."
18256+
},
18257+
"deniedPartialAccessProjectIssues": {
18258+
"default": [],
18259+
"type": "array",
18260+
"items": {
18261+
"type": "string",
18262+
"minLength": 1,
18263+
"maxLength": 255
18264+
},
18265+
"description": "Issues in the listed Projects are unavailable to teammates without access to every active environment."
1822318266
}
1822418267
},
1822518268
"required": [
@@ -18264,7 +18307,8 @@
1826418307
"disableToolAutoApproval",
1826518308
"hideSandboxesTab",
1826618309
"disableOAuthAppAccess",
18267-
"disableKnowledgeBaseExport"
18310+
"disableKnowledgeBaseExport",
18311+
"deniedPartialAccessProjectIssues"
1826818312
],
1826918313
"additionalProperties": false
1827018314
},
@@ -20749,7 +20793,7 @@
2074920793
"description": "Access request being reviewed."
2075020794
},
2075120795
"changes": {
20752-
"maxItems": 42,
20796+
"maxItems": 43,
2075320797
"type": "array",
2075420798
"items": {
2075520799
"type": "object",
@@ -20798,7 +20842,8 @@
2079820842
"disableToolAutoApproval",
2079920843
"hideSandboxesTab",
2080020844
"disableOAuthAppAccess",
20801-
"disableKnowledgeBaseExport"
20845+
"disableKnowledgeBaseExport",
20846+
"deniedPartialAccessProjectIssues"
2080220847
],
2080320848
"description": "Permission restriction changed by approval."
2080420849
},
@@ -21310,7 +21355,7 @@
2131021355
"description": "Access request being reviewed."
2131121356
},
2131221357
"changes": {
21313-
"maxItems": 42,
21358+
"maxItems": 43,
2131421359
"type": "array",
2131521360
"items": {
2131621361
"type": "object",
@@ -21359,7 +21404,8 @@
2135921404
"disableToolAutoApproval",
2136021405
"hideSandboxesTab",
2136121406
"disableOAuthAppAccess",
21362-
"disableKnowledgeBaseExport"
21407+
"disableKnowledgeBaseExport",
21408+
"deniedPartialAccessProjectIssues"
2136321409
],
2136421410
"description": "Permission restriction changed by approval."
2136521411
},

‎apps/sim/app/api/folders/[id]/duplicate/route.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { getSession } from '@/lib/auth'
1313
import { asOrchestrationError } from '@/lib/core/orchestration/types'
1414
import { generateRequestId } from '@/lib/core/utils/request'
1515
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
16-
import type { DbOrTx } from '@/lib/db/types'
16+
import type { DbOrTx, DbTransaction } from '@/lib/db/types'
1717
import { deduplicateFolderName } from '@/lib/folders/naming'
1818
import { nextFolderSortOrder } from '@/lib/folders/orchestration'
1919
import { assertFolderCollectionHasRoom, toFolderApi } from '@/lib/folders/queries'
@@ -409,7 +409,7 @@ async function duplicateFolderStructure(
409409
}
410410

411411
async function duplicateWorkflowsInFolderTree(
412-
tx: DbOrTx,
412+
tx: DbTransaction,
413413
sourceWorkspaceId: string,
414414
targetWorkspaceId: string,
415415
folderMapping: Map<string, string>,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
archiveProjectContract,
3+
getProjectContract,
4+
renameProjectContract,
5+
} from '@/lib/api/contracts/projects'
6+
import {
7+
defineInternalJsonRoute,
8+
internalOrchestrationErrorPolicy,
9+
internalRateLimits,
10+
internalSessionAuth,
11+
} from '@/lib/api/server/routes'
12+
import {
13+
archiveProject,
14+
getProject,
15+
projectOperations,
16+
renameProject,
17+
} from '@/lib/projects/application'
18+
19+
export const GET = defineInternalJsonRoute({
20+
contract: getProjectContract,
21+
auth: internalSessionAuth,
22+
operation: projectOperations.get,
23+
rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }),
24+
errorPolicy: internalOrchestrationErrorPolicy,
25+
mapInput: ({ params, query }) => ({ projectId: params.id, ...query }),
26+
useCase: getProject,
27+
})
28+
export const PATCH = defineInternalJsonRoute({
29+
contract: renameProjectContract,
30+
auth: internalSessionAuth,
31+
operation: projectOperations.rename,
32+
rateLimit: internalRateLimits.user({ bucketName: 'projects.write' }),
33+
errorPolicy: internalOrchestrationErrorPolicy,
34+
mapInput: ({ params, body }) => ({ projectId: params.id, name: body.name }),
35+
useCase: renameProject,
36+
})
37+
export const DELETE = defineInternalJsonRoute({
38+
contract: archiveProjectContract,
39+
auth: internalSessionAuth,
40+
operation: projectOperations.archive,
41+
rateLimit: internalRateLimits.user({ bucketName: 'projects.write' }),
42+
errorPolicy: internalOrchestrationErrorPolicy,
43+
mapInput: ({ params }) => ({ projectId: params.id }),
44+
useCase: archiveProject,
45+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { getWorkspaceProjectContract } from '@/lib/api/contracts/projects'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { getWorkspaceProject, projectOperations } from '@/lib/projects/application'
9+
10+
export const GET = defineInternalJsonRoute({
11+
contract: getWorkspaceProjectContract,
12+
auth: internalSessionAuth,
13+
operation: projectOperations.get,
14+
rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }),
15+
errorPolicy: internalOrchestrationErrorPolicy,
16+
mapInput: ({ params }) => params,
17+
useCase: getWorkspaceProject,
18+
})

‎apps/sim/app/api/projects/route.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createProjectContract, listProjectsContract } from '@/lib/api/contracts/projects'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import { createProject, listProjects, projectOperations } from '@/lib/projects/application'
9+
10+
export const GET = defineInternalJsonRoute({
11+
contract: listProjectsContract,
12+
auth: internalSessionAuth,
13+
operation: projectOperations.list,
14+
rateLimit: internalRateLimits.user({ bucketName: 'projects.read' }),
15+
errorPolicy: internalOrchestrationErrorPolicy,
16+
mapInput: ({ query }) => query,
17+
useCase: listProjects,
18+
})
19+
20+
export const POST = defineInternalJsonRoute({
21+
contract: createProjectContract,
22+
auth: internalSessionAuth,
23+
operation: projectOperations.create,
24+
rateLimit: internalRateLimits.user({ bucketName: 'projects.write' }),
25+
errorPolicy: internalOrchestrationErrorPolicy,
26+
mapInput: ({ body }) => body,
27+
useCase: createProject,
28+
})

‎apps/sim/app/api/superuser/import-workflow/route.ts‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server'
77
import { importWorkflowAsSuperuserContract } from '@/lib/api/contracts/workflows'
88
import { parseRequest } from '@/lib/api/server'
99
import { getSession } from '@/lib/auth'
10+
import { OrchestrationError } from '@/lib/core/orchestration/types'
1011
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1112
import { loadCopilotChatMessages } from '@/lib/mothership/chat/lifecycle'
1213
import { appendCopilotChatMessages } from '@/lib/mothership/chat/messages-store'
@@ -136,17 +137,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
136137
null
137138
)
138139

139-
await db.insert(workflow).values(
140-
await buildNewWorkflowRow(db, {
141-
id: newWorkflowId,
142-
userId: session.user.id,
143-
workspaceId: targetWorkspaceId,
144-
folderId: null,
145-
name: dedupedName,
146-
description: sourceWorkflow.description,
147-
variables: sourceWorkflow.variables || {},
148-
})
149-
)
140+
await db.transaction(async (tx) => {
141+
await tx.insert(workflow).values(
142+
await buildNewWorkflowRow(tx, {
143+
id: newWorkflowId,
144+
userId: session.user.id,
145+
workspaceId: targetWorkspaceId,
146+
folderId: null,
147+
name: dedupedName,
148+
description: sourceWorkflow.description,
149+
variables: sourceWorkflow.variables || {},
150+
})
151+
)
152+
})
150153

151154
// Save using existing persistence logic
152155
const saveResult = await saveWorkflowToNormalizedTables(newWorkflowId, importedData, {
@@ -225,6 +228,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
225228
copilotChatsImported,
226229
})
227230
} catch (error) {
231+
if (error instanceof OrchestrationError && error.code === 'not_found') {
232+
return NextResponse.json({ error: 'Target workspace not found' }, { status: 404 })
233+
}
228234
logger.error('Error importing workflow', error)
229235
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
230236
}

‎apps/sim/app/api/v1/admin/workflows/import/route.ts‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { and, eq, isNull } from 'drizzle-orm'
2828
import { NextResponse } from 'next/server'
2929
import { adminV1ImportWorkflowContract } from '@/lib/api/contracts/v1/admin'
3030
import { parseRequest } from '@/lib/api/server'
31+
import { OrchestrationError } from '@/lib/core/orchestration/types'
3132
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
3233
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
3334
import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
@@ -115,16 +116,18 @@ export const POST = withRouteHandler(
115116
const workflowId = generateId()
116117
const dedupedName = await deduplicateWorkflowName(workflowName, workspaceId, folderId || null)
117118

118-
await db.insert(workflow).values(
119-
await buildNewWorkflowRow(db, {
120-
id: workflowId,
121-
userId: workspaceData.ownerId,
122-
workspaceId,
123-
folderId: folderId || null,
124-
name: dedupedName,
125-
description: workflowDescription,
126-
})
127-
)
119+
await db.transaction(async (tx) => {
120+
await tx.insert(workflow).values(
121+
await buildNewWorkflowRow(tx, {
122+
id: workflowId,
123+
userId: workspaceData.ownerId,
124+
workspaceId,
125+
folderId: folderId || null,
126+
name: dedupedName,
127+
description: workflowDescription,
128+
})
129+
)
130+
})
128131

129132
/**
130133
* Same normalization the editor and the v1 import API run, via the one
@@ -180,6 +183,9 @@ export const POST = withRouteHandler(
180183

181184
return NextResponse.json(response)
182185
} catch (error) {
186+
if (error instanceof OrchestrationError && error.code === 'not_found') {
187+
return notFoundResponse('Workspace')
188+
}
183189
if (error instanceof FolderNotFoundError) {
184190
return badRequestResponse(error.message)
185191
}

0 commit comments

Comments
 (0)