|
1 | | -import type { Project, Query } from '~/insomnia-data'; |
| 1 | +import { createTeamProject, fetchTeamProjects } from 'insomnia-api'; |
| 2 | + |
| 3 | +import type { Project, Query, RemoteProject } from '~/insomnia-data'; |
2 | 4 | import { database as db, models } from '~/insomnia-data'; |
3 | 5 |
|
| 6 | +import { get as getUser } from './user-session'; |
| 7 | +import type { SyncVCSLike } from './vcs'; |
| 8 | +import { commitAllAndPush, getWorkspacesOfProject, hasGitRepositoryId } from './workspace'; |
| 9 | + |
4 | 10 | const { type } = models.project; |
5 | 11 |
|
6 | 12 | export function create(patch: Partial<Project> = {}) { |
@@ -46,3 +52,228 @@ export async function remove(idOrProject: string | Project) { |
46 | 52 | const project = await getProjectByIdOrProject(idOrProject); |
47 | 53 | return db.remove(project); |
48 | 54 | } |
| 55 | + |
| 56 | +export async function getFirstProjectUnderOrg(organizationId: string) { |
| 57 | + return db.findOne<Project>(type, { parentId: organizationId }); |
| 58 | +} |
| 59 | + |
| 60 | +interface TeamProject { |
| 61 | + id: string; |
| 62 | + name: string; |
| 63 | +} |
| 64 | + |
| 65 | +export async function syncProjectsWithBackend(teamProjects: TeamProject[], organizationId: string) { |
| 66 | + // assumption: api teamProjects is the source of truth for migrated projects |
| 67 | + // once migrated orgs become the source of truth for projects |
| 68 | + // its important that migration be completed before this code is run |
| 69 | + const existingRemoteProjects = await db.find<Project>(type, { |
| 70 | + remoteId: { $in: teamProjects.map(p => p.id) }, |
| 71 | + }); |
| 72 | + |
| 73 | + const existingRemoteProjectsRemoteIds = existingRemoteProjects.map(p => p.remoteId); |
| 74 | + const remoteProjectsThatNeedToBeCreated = teamProjects.filter(p => !existingRemoteProjectsRemoteIds.includes(p.id)); |
| 75 | + |
| 76 | + // this will create a new project for any remote projects that don't exist in the current organization |
| 77 | + await Promise.all( |
| 78 | + remoteProjectsThatNeedToBeCreated.map(async prj => { |
| 79 | + await create({ |
| 80 | + remoteId: prj.id, |
| 81 | + name: prj.name, |
| 82 | + parentId: organizationId, |
| 83 | + }); |
| 84 | + }), |
| 85 | + ); |
| 86 | + |
| 87 | + const remoteProjectsThatNeedToBeUpdated = await db.find<Project>(type, { |
| 88 | + // Remote ID is in the list of remote projects |
| 89 | + remoteId: { $in: teamProjects.map(p => p.id) }, |
| 90 | + }); |
| 91 | + |
| 92 | + await Promise.all( |
| 93 | + remoteProjectsThatNeedToBeUpdated.map(async prj => { |
| 94 | + const remoteProject = teamProjects.find(p => p.id === prj.remoteId); |
| 95 | + if (remoteProject && remoteProject.name !== prj.name) { |
| 96 | + await update(prj, { |
| 97 | + name: remoteProject.name, |
| 98 | + }); |
| 99 | + } |
| 100 | + }), |
| 101 | + ); |
| 102 | + |
| 103 | + // Turn remote projects from the current organization that are not in the list of remote projects into local projects. |
| 104 | + const removedRemoteProjects = await db.find<Project>(type, { |
| 105 | + // filter by this organization so no legacy data can be accidentally removed, because legacy had null parentId |
| 106 | + parentId: organizationId, |
| 107 | + // Remote ID is not in the list of remote projects. |
| 108 | + // add `$ne: null` condition because if remoteId is already null, we dont need to remove it again. |
| 109 | + // nedb use append-only format, all updates and deletes actually result in lines added |
| 110 | + remoteId: { |
| 111 | + $nin: teamProjects.map(p => p.id), |
| 112 | + $ne: null, |
| 113 | + }, |
| 114 | + }); |
| 115 | + |
| 116 | + await Promise.all( |
| 117 | + removedRemoteProjects.map(async prj => { |
| 118 | + await update(prj, { |
| 119 | + remoteId: null, |
| 120 | + }); |
| 121 | + }), |
| 122 | + ); |
| 123 | +} |
| 124 | + |
| 125 | +export async function migrateProjectsIntoOrganization(personalOrganizationId: string) { |
| 126 | + // Legacy remote projects without organizations |
| 127 | + // Local projects without organizations except scratchpad |
| 128 | + const [legacyRemoteProjects, localProjects] = await Promise.all([ |
| 129 | + db.find<RemoteProject>(models.project.type, { |
| 130 | + remoteId: { $ne: null }, |
| 131 | + parentId: null, |
| 132 | + }), |
| 133 | + db.find<Project>(models.project.type, { |
| 134 | + remoteId: null, |
| 135 | + parentId: null, |
| 136 | + _id: { $ne: models.project.SCRATCHPAD_PROJECT_ID }, |
| 137 | + }), |
| 138 | + ]); |
| 139 | + |
| 140 | + const updatePromises = []; |
| 141 | + // Legacy remoteId should be orgId and legacy _id should be remoteId |
| 142 | + for (const remoteProject of legacyRemoteProjects) { |
| 143 | + updatePromises.push( |
| 144 | + update(remoteProject, { |
| 145 | + parentId: remoteProject.remoteId, |
| 146 | + remoteId: remoteProject._id, |
| 147 | + }), |
| 148 | + ); |
| 149 | + } |
| 150 | + |
| 151 | + // Assign all local projects to personal organization |
| 152 | + for (const localProject of localProjects) { |
| 153 | + updatePromises.push( |
| 154 | + update(localProject, { |
| 155 | + parentId: personalOrganizationId, |
| 156 | + }), |
| 157 | + ); |
| 158 | + } |
| 159 | + |
| 160 | + await Promise.all(updatePromises); |
| 161 | +} |
| 162 | + |
| 163 | +// Migration: |
| 164 | +// Team ~= Project > Workspaces |
| 165 | +// In the previous API: { _id: 'proj_team_123', remoteId: 'team_123', parentId: null } |
| 166 | + |
| 167 | +// Organization > TeamProject > Workspaces |
| 168 | +// In the new API: { _id: 'proj_team_123', remoteId: 'proj_team_123', parentId: 'team_123' } |
| 169 | + |
| 170 | +// the remote id field previously tracked "team_id" |
| 171 | +// (remote concept for matching 1:1 with this project) which is now org_id |
| 172 | +// the _id field previously tracked the "proj_team_id" |
| 173 | +// which was a wrapper for the team_id prefixing proj_to the above id, |
| 174 | +// which is now the remoteId for tracking the projects within an org |
| 175 | +export async function hasProjectsToMigrate() { |
| 176 | + const [localProjectCount, legacyRemoteProjectCount] = await Promise.all([ |
| 177 | + db.count<Project>(models.project.type, { |
| 178 | + remoteId: null, |
| 179 | + parentId: null, |
| 180 | + }), |
| 181 | + db.count<Project>(models.project.type, { |
| 182 | + remoteId: { $ne: null }, |
| 183 | + parentId: null, |
| 184 | + }), |
| 185 | + ]); |
| 186 | + |
| 187 | + return localProjectCount > 0 || legacyRemoteProjectCount > 0; |
| 188 | +} |
| 189 | + |
| 190 | +export async function getLocalProjectsOfOrg(orgId: string) { |
| 191 | + return await db.find<Project>(models.project.type, { |
| 192 | + parentId: orgId, |
| 193 | + remoteId: null, |
| 194 | + }); |
| 195 | +} |
| 196 | + |
| 197 | +export async function pushLocalProject({ |
| 198 | + project, |
| 199 | + vcs, |
| 200 | + organizationId, |
| 201 | +}: { |
| 202 | + project: Project; |
| 203 | + vcs: SyncVCSLike; |
| 204 | + organizationId: string; |
| 205 | +}) { |
| 206 | + const { id: sessionId } = await getUser(); |
| 207 | + const newCloudProject = await createTeamProject({ |
| 208 | + sessionId, |
| 209 | + organizationId, |
| 210 | + name: project.name, |
| 211 | + }); |
| 212 | + const updatedProject = await update(project, { |
| 213 | + name: newCloudProject.name, |
| 214 | + remoteId: newCloudProject.id, |
| 215 | + }); |
| 216 | + |
| 217 | + // For each workspace in the local project |
| 218 | + const projectWorkspaces = await getWorkspacesOfProject(updatedProject._id); |
| 219 | + |
| 220 | + for (const workspace of projectWorkspaces) { |
| 221 | + const hasGitRepoId = await hasGitRepositoryId(workspace); |
| 222 | + |
| 223 | + // Initialize Sync on the workspace if it's not using Git sync |
| 224 | + try { |
| 225 | + if (!hasGitRepoId) { |
| 226 | + if (!vcs) { |
| 227 | + throw new Error('VCS must be initialized'); |
| 228 | + } |
| 229 | + |
| 230 | + await vcs.switchAndCreateBackendProjectIfNotExist(workspace._id, workspace.name); |
| 231 | + await commitAllAndPush({ workspace, vcs, message: 'Initial Snapshot', project: updatedProject }); |
| 232 | + } |
| 233 | + } catch (e) { |
| 234 | + console.warn( |
| 235 | + 'Failed to initialize sync on workspace. This will be retried when the workspace is opened on the app.', |
| 236 | + e, |
| 237 | + ); |
| 238 | + // TODO: here we should show the try again dialog |
| 239 | + } |
| 240 | + } |
| 241 | +} |
| 242 | + |
| 243 | +export async function getAllTeamProjects(organizationId: string) { |
| 244 | + const { id: sessionId } = await getUser(); |
| 245 | + if (!sessionId) { |
| 246 | + return []; |
| 247 | + } |
| 248 | + |
| 249 | + console.log('[project] Fetching', organizationId); |
| 250 | + const response = await fetchTeamProjects({ sessionId, organizationId }); |
| 251 | + return response.data; |
| 252 | +} |
| 253 | + |
| 254 | +export async function migrateProjectsUnderOrganization( |
| 255 | + orgId: string, |
| 256 | + preferredProjectType: string | null, |
| 257 | + vcs: SyncVCSLike, |
| 258 | +) { |
| 259 | + if (await hasProjectsToMigrate()) { |
| 260 | + await migrateProjectsIntoOrganization(orgId); |
| 261 | + |
| 262 | + if (preferredProjectType === 'remote') { |
| 263 | + const localProjects = await getLocalProjectsOfOrg(orgId); |
| 264 | + |
| 265 | + // If any of those fail projects will still be under the organization as local projects |
| 266 | + for (const project of localProjects) { |
| 267 | + try { |
| 268 | + await pushLocalProject({ |
| 269 | + project, |
| 270 | + organizationId: orgId, |
| 271 | + vcs, |
| 272 | + }); |
| 273 | + } catch { |
| 274 | + console.log(`Failed to push project ${project._id} to the cloud`); |
| 275 | + } |
| 276 | + } |
| 277 | + } |
| 278 | + } |
| 279 | +} |
0 commit comments