diff --git a/packages/insomnia-data/node-src/services/project.ts b/packages/insomnia-data/node-src/services/project.ts index 9cad52c5f29f..db9cae9cd7d9 100644 --- a/packages/insomnia-data/node-src/services/project.ts +++ b/packages/insomnia-data/node-src/services/project.ts @@ -1,6 +1,9 @@ +import { fetchTeamProjects } from 'insomnia-api'; import type { Project, Query, RemoteProject } from 'insomnia-data'; import { database as db, models } from 'insomnia-data'; +import * as userSessionService from './user-session'; + const { type } = models.project; export function create(patch: Partial = {}) { @@ -59,3 +62,98 @@ export function listByGitRepositoryIds(gitRepositoryIds: string | string[]) { const queryIds = ids.flatMap(id => models.project.getQueryableGitRepositoryIds(id)); return list({ gitRepositoryId: { $in: queryIds } }); } + +export async function getFirstProjectOfOrganization(organizationId: string) { + return db.findOne(type, { parentId: organizationId }); +} + +export async function getAllTeamProjects(organizationId: string) { + const { id: sessionId } = await userSessionService.get(); + if (!sessionId) { + return []; + } + + console.log('[project] Fetching', organizationId); + const response = await fetchTeamProjects({ sessionId, organizationId }); + return response.data; +} + +interface TeamProject { + id: string; + name: string; +} + +export async function syncTeamProjects({ + organizationId, + teamProjects, +}: { + teamProjects: TeamProject[]; + organizationId: string; +}) { + // assumption: api teamProjects is the source of truth for migrated projects + // once migrated orgs become the source of truth for projects + // its important that migration be completed before this code is run + const existingRemoteProjects = await list({ + remoteId: { $in: teamProjects.map(p => p.id) }, + }); + + const existingRemoteProjectsRemoteIds = existingRemoteProjects.map(p => p.remoteId); + const remoteProjectsThatNeedToBeCreated = teamProjects.filter(p => !existingRemoteProjectsRemoteIds.includes(p.id)); + + // this will create a new project for any remote projects that don't exist in the current organization + await Promise.all( + remoteProjectsThatNeedToBeCreated.map(async prj => { + await create({ + remoteId: prj.id, + name: prj.name, + parentId: organizationId, + }); + }), + ); + + const remoteProjectsThatNeedToBeUpdated = await list({ + // Remote ID is in the list of remote projects + remoteId: { $in: teamProjects.map(p => p.id) }, + }); + + await Promise.all( + remoteProjectsThatNeedToBeUpdated.map(async prj => { + const remoteProject = teamProjects.find(p => p.id === prj.remoteId); + if (remoteProject && remoteProject.name !== prj.name) { + await update(prj, { + name: remoteProject.name, + }); + } + }), + ); + + // Turn remote projects from the current organization that are not in the list of remote projects into local projects. + const removedRemoteProjects = await list({ + // filter by this organization so no legacy data can be accidentally removed, because legacy had null parentId + parentId: organizationId, + // Remote ID is not in the list of remote projects. + // add `$ne: null` condition because if remoteId is already null, we dont need to remove it again. + // nedb use append-only format, all updates and deletes actually result in lines added + remoteId: { + $nin: teamProjects.map(p => p.id), + $ne: null, + }, + }); + + await Promise.all( + removedRemoteProjects.map(async prj => { + await update(prj, { + remoteId: null, + }); + }), + ); +} + +export async function syncProjects(organizationId: string) { + const user = await userSessionService.get(); + const teamProjects = await getAllTeamProjects(organizationId); + // ensure we don't sync projects in the wrong place + if (Array.isArray(teamProjects) && user.id && !models.organization.isScratchpadOrganizationId(organizationId)) { + await syncTeamProjects({ teamProjects, organizationId }); + } +} diff --git a/packages/insomnia/src/common/organization.ts b/packages/insomnia/src/common/organization.ts new file mode 100644 index 000000000000..405a6be7d165 --- /dev/null +++ b/packages/insomnia/src/common/organization.ts @@ -0,0 +1,27 @@ +import { getCurrentPlan, getUserProfile } from 'insomnia-api'; +import { services } from 'insomnia-data'; + +import { invariant } from '~/common/utils/invariant'; + +// This is reusable action/loader implementations. +export async function syncOrganizations(sessionId: string, accountId: string) { + try { + const [organizations, user, currentPlan] = await Promise.all([ + services.organization.list(), + getUserProfile({ sessionId }), + getCurrentPlan({ sessionId }), + ]); + + invariant(organizations, 'Failed to load organizations'); + invariant(user && user.id, 'Failed to load user'); + invariant(currentPlan && currentPlan.planId, 'Failed to load current plan'); + + invariant(accountId, 'Account ID is not defined'); + + localStorage.setItem(`${accountId}:spaces`, JSON.stringify(organizations)); + localStorage.setItem(`${accountId}:user`, JSON.stringify(user)); + localStorage.setItem(`${accountId}:currentPlan`, JSON.stringify(currentPlan)); + } catch (error) { + console.log('[organization] Failed to load Organizations', error); + } +} diff --git a/packages/insomnia/src/common/project.ts b/packages/insomnia/src/common/project.ts index 1885dbb5d8d4..99c986e0ec87 100644 --- a/packages/insomnia/src/common/project.ts +++ b/packages/insomnia/src/common/project.ts @@ -255,3 +255,7 @@ export async function getProjectsWithGitRepositories({ }; }); } + +export const syncProjects = projectLock.wrapWithLock(async (organizationId: string) => { + await services.project.syncProjects(organizationId); +}); diff --git a/packages/insomnia/src/routes/organization.$organizationId._index.tsx b/packages/insomnia/src/routes/organization.$organizationId._index.tsx index 5c40404bef74..acbb9f88e1d9 100644 --- a/packages/insomnia/src/routes/organization.$organizationId._index.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId._index.tsx @@ -1,6 +1,6 @@ import { redirect } from 'react-router'; -import { syncProjects } from '~/ui/organization-utils'; +import { syncProjects } from '~/common/project'; import { getInitialRouteForOrganization } from '~/ui/utils/router'; import type { Route } from './+types/organization.$organizationId._index'; diff --git a/packages/insomnia/src/routes/organization.$organizationId.project._index.tsx b/packages/insomnia/src/routes/organization.$organizationId.project._index.tsx index 579d683b03bb..3116b18320b4 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.project._index.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project._index.tsx @@ -20,11 +20,7 @@ export interface ProjectIndexLoaderData { projects: (Project & { gitRepository?: GitRepository })[]; } -const shouldAutoCreateInitialProject = async ({ - accountId, -}: { - accountId: string | null | undefined; -}) => { +const shouldAutoCreateInitialProject = async ({ accountId }: { accountId: string | null | undefined }) => { if (!accountId) { return false; } diff --git a/packages/insomnia/src/routes/organization.$organizationId.sync-projects.tsx b/packages/insomnia/src/routes/organization.$organizationId.sync-projects.tsx index 7a724ebb12da..832c10dac5fc 100644 --- a/packages/insomnia/src/routes/organization.$organizationId.sync-projects.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.sync-projects.tsx @@ -1,6 +1,6 @@ import { href } from 'react-router'; -import { syncProjects } from '~/ui/organization-utils'; +import { syncProjects } from '~/common/project'; import { createFetcherSubmitHook } from '~/ui/utils/router'; import type { Route } from './+types/organization.$organizationId.sync-projects'; diff --git a/packages/insomnia/src/routes/organization._index.tsx b/packages/insomnia/src/routes/organization._index.tsx index 2973582d435c..5ea17f5ce1d0 100644 --- a/packages/insomnia/src/routes/organization._index.tsx +++ b/packages/insomnia/src/routes/organization._index.tsx @@ -2,9 +2,10 @@ import type { Organization } from 'insomnia-api'; import { services } from 'insomnia-data'; import { href, redirect } from 'react-router'; +import { syncOrganizations } from '~/common/organization'; import { invariant } from '~/common/utils/invariant'; import * as session from '~/ui/account/session'; -import { findMigrationTargetSpaceId, migrateProjectsUnderOrganization, syncOrganizations } from '~/ui/organization-utils'; +import { findMigrationTargetSpaceId, migrateProjectsUnderOrganization } from '~/ui/organization-utils'; import type { Route } from './+types/organization._index'; diff --git a/packages/insomnia/src/routes/organization.sync-organizations-and-projects.tsx b/packages/insomnia/src/routes/organization.sync-organizations-and-projects.tsx index 55ce75754214..9aed14d40cac 100644 --- a/packages/insomnia/src/routes/organization.sync-organizations-and-projects.tsx +++ b/packages/insomnia/src/routes/organization.sync-organizations-and-projects.tsx @@ -2,8 +2,10 @@ import type { Organization } from 'insomnia-api'; import { services } from 'insomnia-data'; import { href, redirect } from 'react-router'; +import { syncOrganizations } from '~/common/organization'; +import { syncProjects } from '~/common/project'; import { invariant } from '~/common/utils/invariant'; -import { findMigrationTargetSpaceId, migrateProjectsUnderOrganization, syncOrganizations, syncProjects } from '~/ui/organization-utils'; +import { findMigrationTargetSpaceId, migrateProjectsUnderOrganization } from '~/ui/organization-utils'; import { AsyncTask, createFetcherSubmitHook } from '~/ui/utils/router'; import type { Route } from './+types/organization.sync-organizations-and-projects'; diff --git a/packages/insomnia/src/routes/organization.sync.tsx b/packages/insomnia/src/routes/organization.sync.tsx index 8c694e5b72e5..4700a51a819f 100644 --- a/packages/insomnia/src/routes/organization.sync.tsx +++ b/packages/insomnia/src/routes/organization.sync.tsx @@ -1,6 +1,6 @@ import { services } from 'insomnia-data'; -import { syncOrganizations } from '~/ui/organization-utils'; +import { syncOrganizations } from '~/common/organization'; import { createFetcherSubmitHook } from '~/ui/utils/router'; import type { Route } from './+types/organization.sync'; diff --git a/packages/insomnia/src/routes/trial.start.tsx b/packages/insomnia/src/routes/trial.start.tsx index c45dbdddd491..4a2ea6d7eee4 100644 --- a/packages/insomnia/src/routes/trial.start.tsx +++ b/packages/insomnia/src/routes/trial.start.tsx @@ -1,11 +1,19 @@ -import { startTrial } from 'insomnia-api'; +import { getCurrentPlan, startTrial } from 'insomnia-api'; import { services } from 'insomnia-data'; -import { syncCurrentPlan } from '~/ui/organization-utils'; import { createFetcherSubmitHook } from '~/ui/utils/router'; import type { Route } from './+types/settings.update'; +async function syncCurrentPlan(sessionId: string, accountId: string) { + const [currentPlanResult] = await Promise.allSettled([getCurrentPlan({ sessionId })]); + if (currentPlanResult.status === 'fulfilled' && currentPlanResult.value) { + localStorage.setItem(`${accountId}:currentPlan`, JSON.stringify(currentPlanResult.value)); + } else { + console.log('[current-plan] Failed to load current-plan', currentPlanResult.status); + } +} + export async function clientAction(_args: Route.ClientActionArgs) { const { id: sessionId, accountId } = await services.userSession.get(); diff --git a/packages/insomnia/src/ui/organization-utils.ts b/packages/insomnia/src/ui/organization-utils.ts index f860dee45f57..3c0b5f837069 100644 --- a/packages/insomnia/src/ui/organization-utils.ts +++ b/packages/insomnia/src/ui/organization-utils.ts @@ -1,17 +1,10 @@ -import { - createTeamProject, - fetchTeamProjects, - getCurrentPlan, - getUserProfile, - isApiError, - type Organization, -} from 'insomnia-api'; +import { createTeamProject, isApiError, type Organization } from 'insomnia-api'; import type { Project } from 'insomnia-data'; -import { models, services } from 'insomnia-data'; +import { services } from 'insomnia-data'; -import { projectLock } from '~/common/project'; import { invariant } from '~/common/utils/invariant'; +// TODO: move vcs into services so we can remove this file. import { initializeLocalBackendProjectAndMarkForSync, pushSnapshotOnInitialize, @@ -21,38 +14,6 @@ import { migrateProjectsIntoOrganization, shouldMigrateProjectUnderOrganization, } from '../sync/vcs/migrate-projects-into-organization'; -export { DEFAULT_STORAGE_RULES, fetchAndCacheOrganizationStorageRule } from '~/common/organization-storage-rules'; - -export async function syncCurrentPlan(sessionId: string, accountId: string) { - const [currentPlanResult] = await Promise.allSettled([getCurrentPlan({ sessionId })]); - if (currentPlanResult.status === 'fulfilled' && currentPlanResult.value) { - localStorage.setItem(`${accountId}:currentPlan`, JSON.stringify(currentPlanResult.value)); - } else { - console.log('[current-plan] Failed to load current-plan', currentPlanResult.status); - } -} - -export async function syncOrganizations(sessionId: string, accountId: string) { - try { - const [organizations, user, currentPlan] = await Promise.all([ - services.organization.list(), - getUserProfile({ sessionId }), - getCurrentPlan({ sessionId }), - ]); - - invariant(organizations, 'Failed to load organizations'); - invariant(user && user.id, 'Failed to load user'); - invariant(currentPlan && currentPlan.planId, 'Failed to load current plan'); - - invariant(accountId, 'Account ID is not defined'); - - localStorage.setItem(`${accountId}:spaces`, JSON.stringify(organizations)); - localStorage.setItem(`${accountId}:user`, JSON.stringify(user)); - localStorage.setItem(`${accountId}:currentPlan`, JSON.stringify(currentPlan)); - } catch (error) { - console.log('[organization] Failed to load Organizations', error); - } -} export async function updateLocalProjectToRemote({ project, @@ -154,98 +115,3 @@ export async function migrateProjectsUnderOrganization(personalOrganizationId: s } } } - -interface TeamProject { - id: string; - name: string; -} - -async function getAllTeamProjects(organizationId: string) { - const { id: sessionId } = await services.userSession.get(); - if (!sessionId) { - return []; - } - - console.log('[project] Fetching', organizationId); - const response = await fetchTeamProjects({ sessionId, organizationId }); - - return response.data; -} - -async function syncTeamProjects({ - organizationId, - teamProjects, -}: { - teamProjects: TeamProject[]; - organizationId: string; -}) { - // assumption: api teamProjects is the source of truth for migrated projects - // once migrated orgs become the source of truth for projects - // its important that migration be completed before this code is run - const existingRemoteProjects = await services.project.list({ - remoteId: { $in: teamProjects.map(p => p.id) }, - }); - - const existingRemoteProjectsRemoteIds = existingRemoteProjects.map(p => p.remoteId); - const remoteProjectsThatNeedToBeCreated = teamProjects.filter(p => !existingRemoteProjectsRemoteIds.includes(p.id)); - - // this will create a new project for any remote projects that don't exist in the current organization - await Promise.all( - remoteProjectsThatNeedToBeCreated.map(async prj => { - await services.project.create({ - remoteId: prj.id, - name: prj.name, - parentId: organizationId, - }); - }), - ); - - const remoteProjectsThatNeedToBeUpdated = await services.project.list({ - // Remote ID is in the list of remote projects - remoteId: { $in: teamProjects.map(p => p.id) }, - }); - - await Promise.all( - remoteProjectsThatNeedToBeUpdated.map(async prj => { - const remoteProject = teamProjects.find(p => p.id === prj.remoteId); - if (remoteProject && remoteProject.name !== prj.name) { - await services.project.update(prj, { - name: remoteProject.name, - }); - } - }), - ); - - // Turn remote projects from the current organization that are not in the list of remote projects into local projects. - const removedRemoteProjects = await services.project.list({ - // filter by this organization so no legacy data can be accidentally removed, because legacy had null parentId - parentId: organizationId, - // Remote ID is not in the list of remote projects. - // add `$ne: null` condition because if remoteId is already null, we dont need to remove it again. - // nedb use append-only format, all updates and deletes actually result in lines added - remoteId: { - $nin: teamProjects.map(p => p.id), - $ne: null, - }, - }); - - await Promise.all( - removedRemoteProjects.map(async prj => { - await services.project.update(prj, { - remoteId: null, - }); - }), - ); -} - -export const syncProjects = projectLock.wrapWithLock(async (organizationId: string) => { - const user = await services.userSession.get(); - const teamProjects = await getAllTeamProjects(organizationId); - // ensure we don't sync projects in the wrong place - if (Array.isArray(teamProjects) && user.id && !models.organization.isScratchpadOrganizationId(organizationId)) { - await syncTeamProjects({ - organizationId, - teamProjects, - }); - } -});