Skip to content

Commit

Permalink
Refactorings & Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
R1c4rdCo5t4 committed Jun 25, 2024
1 parent 2780b34 commit d294384
Show file tree
Hide file tree
Showing 14 changed files with 47 additions and 87 deletions.
2 changes: 1 addition & 1 deletion code/client/src/domain/workspaces/useWorkspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function useWorkspaces() {

useEffect(() => {
async function fetchWorkspaces() {
const workspaces = await service.getWorkspaces();
const workspaces = await service.getUserWorkspaces();
setWorkspaces(workspaces);
}
fetchWorkspaces();
Expand Down
10 changes: 5 additions & 5 deletions code/client/src/services/workspace/workspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function workspaceService(http: HttpCommunication, errorHandler: ErrorHandler) {
return errorHandler(async () => await http.get(`/workspaces/${id}`));
}

async function getWorkspaces(): Promise<WorkspaceMeta[]> {
async function getUserWorkspaces(): Promise<WorkspaceMeta[]> {
return errorHandler(async () => await http.get('/workspaces'));
}

Expand All @@ -35,8 +35,8 @@ function workspaceService(http: HttpCommunication, errorHandler: ErrorHandler) {
return errorHandler(async () => await http.delete(`/workspaces/${id}/members`, { email }));
}

async function getWorkspacesFeed() {
return errorHandler(async () => await http.get('/workspaces/search'));
async function getWorkspaces() {
return errorHandler(async () => await http.get('/workspaces?publicOnly=true'));
}

async function searchWorkspaces(query: string, skip: number, limit: number): Promise<WorkspaceMeta[]> {
Expand All @@ -45,13 +45,13 @@ function workspaceService(http: HttpCommunication, errorHandler: ErrorHandler) {

return {
getWorkspace,
getWorkspaces,
getUserWorkspaces,
createWorkspace,
deleteWorkspace,
updateWorkspace,
addWorkspaceMember,
removeWorkspaceMember,
getWorkspacesFeed,
getWorkspaces,
searchWorkspaces,
};
}
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/ui/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function Header() {

function handleSearchSubmit(e: FormEvent) {
e.preventDefault();
if (!searchInput) return;
navigate(`/search?query=${searchInput}`);
}

Expand All @@ -34,7 +35,6 @@ function Header() {
/>
</form>
)}

<div className="account">
{currentUser ? (
<>
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/ui/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function Home() {
useEffect(() => {
async function getWorkspaces() {
startLoading();
const workspaces = await service.getWorkspacesFeed();
const workspaces = await service.getWorkspaces();
setWorkspaces(workspaces);
stopLoading();
}
Expand Down
2 changes: 1 addition & 1 deletion code/server/sql/create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ begin;
name text not null,
private boolean not null default false,
created_at timestamp not null default now(),
members char(16)[] not null default '{}'::char(16)[] -- references "user"(id)
members text[] not null default '{}'::text[] -- references "user"(email)
);

create table if not exists resource (
Expand Down
13 changes: 3 additions & 10 deletions code/server/src/controllers/http/handlers/workspacesHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ function workspacesHandlers(services: Services, io: Server) {
const { name, isPrivate } = req.body as WorkspaceInputModel;
if (!name) throw new InvalidParameterError('Workspace name is required');
if (isPrivate === undefined) throw new InvalidParameterError('Workspace visibility is required');

const id = await services.workspaces.createWorkspace(name, isPrivate);
await services.workspaces.addWorkspaceMember(id, req.user!.email);
const workspace: WorkspaceMeta = {
Expand All @@ -30,7 +29,8 @@ function workspacesHandlers(services: Services, io: Server) {
};

const getWorkspaces = async (req: Request, res: Response) => {
const workspaces = await services.workspaces.getWorkspaces(req.user!.id);
const { publicOnly } = req.query;
const workspaces = await services.workspaces.getWorkspaces(publicOnly ? undefined : req.user?.email);
httpResponse.ok(res).json(workspaces);
};

Expand All @@ -46,10 +46,8 @@ function workspacesHandlers(services: Services, io: Server) {
const updateWorkspace = async (req: Request, res: Response) => {
const { wid } = req.params;
if (!wid) throw new InvalidParameterError('Workspace id is required');

const { name } = req.body as WorkspaceMeta;
if (!name) throw new InvalidParameterError('Workspace name is required');

await services.workspaces.updateWorkspace(wid, name);
io.emit('updatedWorkspace', { id: wid, name } as WorkspaceMeta);
httpResponse.noContent(res).send();
Expand All @@ -58,7 +56,6 @@ function workspacesHandlers(services: Services, io: Server) {
const deleteWorkspace = async (req: Request, res: Response) => {
const { wid } = req.params;
if (!wid) throw new InvalidParameterError('Workspace id is required');

await services.workspaces.deleteWorkspace(wid);
io.emit('deletedWorkspace', wid);
httpResponse.noContent(res).send();
Expand All @@ -67,21 +64,17 @@ function workspacesHandlers(services: Services, io: Server) {
const addMemberToWorkspace = async (req: Request, res: Response) => {
const { wid } = req.params;
if (!wid) throw new InvalidParameterError('Workspace id is required');

const { email } = req.body;
if (!email) throw new InvalidParameterError('Email is required');

await services.workspaces.addWorkspaceMember(wid, email);
httpResponse.noContent(res).send();
};

const removeMemberFromWorkspace = async (req: Request, res: Response) => {
const { wid } = req.params;
if (!wid) throw new InvalidParameterError('Workspace id is required');

const { email } = req.body;
if (!email) throw new InvalidParameterError('Email is required');

await services.workspaces.removeWorkspaceMember(wid, email);
httpResponse.noContent(res).send();
};
Expand Down Expand Up @@ -121,8 +114,8 @@ function workspacesHandlers(services: Services, io: Server) {
}

const router = PromiseRouter();
router.get('/', getWorkspaces);
router.post('/', enforceAuth, createWorkspace);
router.get('/', enforceAuth, getWorkspaces);
router.get('/search', searchWorkspaces);
router.get('/:wid', workspaceReadPermission, getWorkspace);
router.put('/:wid', enforceAuth, workspaceWritePermission, updateWorkspace);
Expand Down
6 changes: 0 additions & 6 deletions code/server/src/databases/memory/MemoryUsersDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,4 @@ export class MemoryUsersDB implements UsersRepository {
async getUsers(): Promise<User[]> {
return Object.values(Memory.users);
}

async getUserByEmail(email: string): Promise<User> {
const user = Object.values(Memory.users).find(user => user.email === email);
if (!user) throw new NotFoundError(`User not found`);
return user;
}
}
13 changes: 3 additions & 10 deletions code/server/src/databases/memory/MemoryWorkspacesDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ export class MemoryWorkspacesDB implements WorkspacesRepository {
return id;
}

async getWorkspaces(userId?: string): Promise<WorkspaceMeta[]> {
async getWorkspaces(email?: string): Promise<WorkspaceMeta[]> {
return Object.values(Memory.workspaces)
.filter(workspace => !workspace.isPrivate || (userId && workspace.members.includes(userId)))
.filter(workspace => (email ? workspace.members.includes(email) : !workspace.isPrivate))
.map(props => {
const workspace = omit(props, ['resources']);
return { ...workspace, members: workspace.members?.length || 0 };
Expand All @@ -41,10 +41,8 @@ export class MemoryWorkspacesDB implements WorkspacesRepository {
async getWorkspace(id: string): Promise<Workspace> {
const workspace = Memory.workspaces[id];
if (!workspace) throw new NotFoundError(`Workspace not found`);

const resources = Object.values(workspace.resources);
const members = await this.getWorkspaceMembers(workspace.id);
return { ...workspace, resources, members };
return { ...workspace, resources };
}

async getResources(wid: string): Promise<Resource[]> {
Expand Down Expand Up @@ -78,11 +76,6 @@ export class MemoryWorkspacesDB implements WorkspacesRepository {
Memory.workspaces[wid].members = Memory.workspaces[wid].members.filter(member => member !== userId);
}

async getWorkspaceMembers(wid: string): Promise<string[]> {
const workspace = Memory.workspaces[wid];
return workspace.members?.map(userId => Memory.users[userId].email) || [];
}

async searchWorkspaces(searchParams: SearchParams): Promise<WorkspaceMeta[]> {
const { query, skip, limit } = searchParams;
return Object.values(Memory.workspaces)
Expand Down
6 changes: 0 additions & 6 deletions code/server/src/databases/postgres/PostgresUsersDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,4 @@ export class PostgresUsersDB implements UsersRepository {
async getUsers(): Promise<User[]> {
return await sql`select * from "user"`;
}

async getUserByEmail(email: string): Promise<User> {
const results: User[] = await sql`select * from "user" where email = ${email}`;
if (isEmpty(results)) throw new NotFoundError('User not found');
return results[0];
}
}
31 changes: 15 additions & 16 deletions code/server/src/databases/postgres/PostgresWorkspacesDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,23 @@ export class PostgresWorkspacesDB implements WorkspacesRepository {
return results[0].id;
}

async getWorkspaces(userId?: string): Promise<WorkspaceMeta[]> {
const user = userId || null;
async getWorkspaces(email?: string): Promise<WorkspaceMeta[]> {
const condition = email ? `${email} = any(members)` : 'private = false';
return (
await sql`
select row_to_json(t) as workspace
from (
select id, name, private, count(members) as members
from workspace
where private = false or (${user} is null or ${user} = any(members))
group by id
order by created_at desc
) as t
select row_to_json(t) as workspace
from (
select id, name, private, count(members) as members
from workspace
where ${condition}
group by id
order by created_at desc
) as t
`
).map(r => r.workspace);
}

async getWorkspace(id: string): Promise<Workspace> {
// TODO: convert member user ids to emails
const results: Workspace[] = await sql`select * from workspace where id = ${id}`;
if (isEmpty(results)) throw new NotFoundError(`Workspace not found`);
return results[0];
Expand Down Expand Up @@ -73,20 +72,20 @@ export class PostgresWorkspacesDB implements WorkspacesRepository {
if (isEmpty(results)) throw new NotFoundError(`Workspace not found`);
}

async addWorkspaceMember(wid: string, userId: string): Promise<void> {
async addWorkspaceMember(wid: string, email: string): Promise<void> {
const results = await sql`
update workspace
set members = array_append(members, ${userId}::char(16))
where id = ${wid} and not ${userId} = any(members)
set members = array_append(members, ${email}::text)
where id = ${wid} and not ${email} = any(members)
returning id
`;
if (isEmpty(results)) throw new NotFoundError(`Workspace not found or member already in workspace`);
}

async removeWorkspaceMember(wid: string, userId: string): Promise<void> {
async removeWorkspaceMember(wid: string, email: string): Promise<void> {
const results = await sql`
update workspace
set members = array_remove(members, ${userId}::char(16))
set members = array_remove(members, ${email}::text)
where id = ${wid}
returning id
`;
Expand Down
17 changes: 6 additions & 11 deletions code/server/src/databases/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ export interface WorkspacesRepository {
createWorkspace: (name: string, isPrivate: boolean) => Promise<string>;
/**
* Get all workspaces from the database
* If userId is provided, get user workspaces
* @param userId
* If email is provided, get user workspaces, otherwise get all public workspaces
* @param email
*/
getWorkspaces: (userId?: string) => Promise<WorkspaceMeta[]>;
getWorkspaces: (email?: string) => Promise<WorkspaceMeta[]>;
/**
* Get a workspace from the database
* @param id
Expand All @@ -110,13 +110,13 @@ export interface WorkspacesRepository {
* @param wid
* @param email
*/
addWorkspaceMember: (wid: string, userId: string) => Promise<void>;
addWorkspaceMember: (wid: string, email: string) => Promise<void>;
/**
* Remove a member from a workspace
* @param wid
* @param userId
* @param email
*/
removeWorkspaceMember: (wid: string, userId: string) => Promise<void>;
removeWorkspaceMember: (wid: string, email: string) => Promise<void>;
/**
* Search workspaces by name
* @param searchParams
Expand Down Expand Up @@ -151,11 +151,6 @@ export interface UsersRepository {
* Get all users from the database
*/
getUsers: () => Promise<User[]>;
/**
* Check if a user with an email exists
* @param email
*/
getUserByEmail: (email: string) => Promise<User>;
}

export interface CommitsRepository {
Expand Down
21 changes: 5 additions & 16 deletions code/server/src/services/WorkspacesService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Workspace, WorkspaceMeta } from '@notespace/shared/src/workspace/types/workspace';
import { Databases } from '@databases/types';
import { InvalidParameterError } from '@domain/errors/errors';
import { validateEmail, validateId, validateName, validatePositiveNumber } from '@services/utils';
import { SearchParams } from '@src/utils/searchParams';

Expand Down Expand Up @@ -30,8 +29,8 @@ export class WorkspacesService {
await this.databases.documents.removeWorkspace(id);
}

async getWorkspaces(userId?: string): Promise<WorkspaceMeta[]> {
return await this.databases.workspaces.getWorkspaces(userId);
async getWorkspaces(email?: string): Promise<WorkspaceMeta[]> {
return await this.databases.workspaces.getWorkspaces(email);
}

async getWorkspace(id: string): Promise<Workspace> {
Expand All @@ -45,23 +44,13 @@ export class WorkspacesService {
}

async addWorkspaceMember(wid: string, email: string) {
const { userId, userInWorkspace } = await this.userInWorkspace(wid, email);
if (userInWorkspace) throw new InvalidParameterError('User already in workspace');
await this.databases.workspaces.addWorkspaceMember(wid, userId);
validateEmail(email);
await this.databases.workspaces.addWorkspaceMember(wid, email);
}

async removeWorkspaceMember(wid: string, email: string) {
const { userId, userInWorkspace } = await this.userInWorkspace(wid, email);
if (!userInWorkspace) throw new InvalidParameterError('User not in workspace');
await this.databases.workspaces.removeWorkspaceMember(wid, userId);
}

private async userInWorkspace(wid: string, email: string) {
validateId(wid);
validateEmail(email);
const user = await this.databases.users.getUserByEmail(email); // check if user with email exists
const workspace = await this.databases.workspaces.getWorkspace(wid); // check if workspace exists
return { userId: user.id, userInWorkspace: workspace.members.includes(email) }; // check if user in workspace
await this.databases.workspaces.removeWorkspaceMember(wid, email);
}

async searchWorkspaces(searchParams: SearchParams) {
Expand Down
3 changes: 3 additions & 0 deletions code/server/src/utils/searchParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export type SearchParams = {

export function getSearchParams(params: Record<string, any> = {}): SearchParams {
const { query, skip, limit } = params;
if (!query) {
throw new InvalidParameterError('Search query is required');
}
if ([query, skip, limit].some(param => param !== undefined && typeof param !== 'string')) {
throw new InvalidParameterError('Invalid search params');
}
Expand Down
6 changes: 3 additions & 3 deletions code/server/test/workspaces/workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Workspace operations', () => {
test('should delete a workspace', async () => {
const id = await services.workspaces.createWorkspace('test', false);
await services.workspaces.deleteWorkspace(id);
const workspaces = await services.workspaces.getWorkspaces(id);
const workspaces = await services.workspaces.getWorkspaces();
expect(workspaces).toEqual([]);
});

Expand All @@ -30,9 +30,9 @@ describe('Workspace operations', () => {
});

test('should get all workspaces', async () => {
const id = await services.workspaces.createWorkspace('test', false);
await services.workspaces.createWorkspace('test', false);
await services.workspaces.createWorkspace('test2', false);
const workspaces = await services.workspaces.getWorkspaces(id);
const workspaces = await services.workspaces.getWorkspaces();
expect(workspaces.length).toEqual(2);
});
});

0 comments on commit d294384

Please sign in to comment.