Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions packages/insomnia-data/node-src/services/project.ts
Original file line number Diff line number Diff line change
@@ -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<Project> = {}) {
Expand Down Expand Up @@ -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<Project>(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 });
}
}
27 changes: 27 additions & 0 deletions packages/insomnia/src/common/organization.ts
Original file line number Diff line number Diff line change
@@ -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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should place this file into ui instead of common, as it uses localStorage?

localStorage.setItem(`${accountId}:user`, JSON.stringify(user));
localStorage.setItem(`${accountId}:currentPlan`, JSON.stringify(currentPlan));
} catch (error) {
console.log('[organization] Failed to load Organizations', error);
}
}
4 changes: 4 additions & 0 deletions packages/insomnia/src/common/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,7 @@ export async function getProjectsWithGitRepositories({
};
});
}

export const syncProjects = projectLock.wrapWithLock(async (organizationId: string) => {
await services.project.syncProjects(organizationId);
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
3 changes: 2 additions & 1 deletion packages/insomnia/src/routes/organization._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/insomnia/src/routes/organization.sync.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
12 changes: 10 additions & 2 deletions packages/insomnia/src/routes/trial.start.tsx
Original file line number Diff line number Diff line change
@@ -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();

Expand Down
140 changes: 3 additions & 137 deletions packages/insomnia/src/ui/organization-utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
});
}
});
Loading