-
Notifications
You must be signed in to change notification settings - Fork 171
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Remix projects: collaborators (#4923)
* add project_collaborators table * add liveblocks api client * update collaborators when adding myself to collabs * udpate schema * split base url var * port over initials function * get and update collaborators * add route to update collaborators * show collaborators avatars * fix position * use brackets * mustenv * keys * no defaults * no need to transact * add comment * test get collaborators * test updateCollaborators * give postgres some time to breathe * try forcing sort * add comment * maybe update
- Loading branch information
Showing
21 changed files
with
539 additions
and
76 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
CREATE TABLE project_collaborators( | ||
id serial, | ||
project_id character varying NOT NULL REFERENCES project_i_d(proj_id) ON DELETE CASCADE, | ||
user_id character varying NOT NULL REFERENCES user_details(user_id) ON DELETE CASCADE, | ||
created_at timestamp with time zone NOT NULL DEFAULT NOW() | ||
); | ||
|
||
CREATE INDEX "idx_project_collaborators_project_id" ON "public"."project_collaborators"(project_id); | ||
|
||
ALTER TABLE ONLY "public"."project_collaborators" | ||
ADD CONSTRAINT "unique_project_collaborator_project_id_user_id" UNIQUE ("project_id", "user_id"); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import urlJoin from 'url-join' | ||
import { ServerEnvironment } from '../env.server' | ||
import { Method } from '../util/methods.server' | ||
import { Collaborator } from '../types' | ||
|
||
const BASE_URL = 'https://api.liveblocks.io/v2' | ||
|
||
async function makeRequest<T>(method: Method, path: string): Promise<T> { | ||
const url = urlJoin(BASE_URL, path) | ||
const resp = await fetch(url, { | ||
method: method, | ||
headers: { | ||
Authorization: `Bearer ${ServerEnvironment.LiveblocksSecretKey}`, | ||
}, | ||
}) | ||
return resp.json() | ||
} | ||
|
||
function roomIdFromProjectId(projectId: string): string { | ||
return `project-room-${projectId}` | ||
} | ||
|
||
export interface RoomStorage { | ||
data: { | ||
collaborators: RoomCollaborators | ||
} | ||
} | ||
|
||
export interface RoomCollaborators { | ||
data: { [userId: string]: { data: Collaborator } } | ||
} | ||
|
||
async function getRoomStorage(projectId: string): Promise<RoomStorage> { | ||
const roomId = roomIdFromProjectId(projectId) | ||
return makeRequest('GET', `/rooms/${roomId}/storage`) | ||
} | ||
|
||
// REST API docs: https://liveblocks.io/docs/api-reference/rest-api-endpoints | ||
export const LiveblocksAPI = { | ||
getRoomStorage, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
utopia-remix/app/models/projectCollaborators.server.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import { LiveblocksAPI, RoomCollaborators, RoomStorage } from '../clients/liveblocks.server' | ||
import { prisma } from '../db.server' | ||
import { | ||
createTestProject, | ||
createTestProjectCollaborator, | ||
createTestUser, | ||
truncateTables, | ||
} from '../test-util' | ||
import { getCollaborators, updateCollaborators } from './projectCollaborators.server' | ||
|
||
describe('projectCollaborators model', () => { | ||
afterEach(async () => { | ||
// cleanup | ||
await truncateTables([ | ||
prisma.projectCollaborator, | ||
prisma.projectID, | ||
prisma.project, | ||
prisma.userDetails, | ||
]) | ||
}) | ||
|
||
describe('getCollaborators', () => { | ||
beforeEach(async () => { | ||
await createTestUser(prisma, { id: 'bob' }) | ||
await createTestUser(prisma, { id: 'alice' }) | ||
await createTestUser(prisma, { id: 'wendy' }) | ||
await createTestProject(prisma, { id: 'one', ownerId: 'bob' }) | ||
await createTestProject(prisma, { id: 'two', ownerId: 'bob' }) | ||
await createTestProject(prisma, { id: 'three', ownerId: 'alice' }) | ||
await createTestProject(prisma, { id: 'four', ownerId: 'bob' }) | ||
await createTestProject(prisma, { id: 'five', ownerId: 'bob' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'one', userId: 'bob' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'two', userId: 'bob' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'two', userId: 'wendy' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'three', userId: 'alice' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'three', userId: 'bob' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'five', userId: 'alice' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'five', userId: 'wendy' }) | ||
}) | ||
it('returns an empty object if no ids are passed', async () => { | ||
const got = await getCollaborators({ ids: [], userId: 'bob' }) | ||
expect(got).toEqual({}) | ||
}) | ||
it("returns an empty object if ids don't match the given user id", async () => { | ||
let got = await getCollaborators({ ids: ['one', 'two'], userId: 'alice' }) | ||
expect(got).toEqual({}) | ||
got = await getCollaborators({ ids: ['one', 'two'], userId: 'NOBODY' }) | ||
expect(got).toEqual({}) | ||
}) | ||
it('returns the collaborator details by project id', async () => { | ||
const ids = ['one', 'two', 'four', 'five'] | ||
const got = await getCollaborators({ ids: ids, userId: 'bob' }) | ||
expect(Object.keys(got)).toEqual(ids) | ||
expect(got['one'].map((c) => c.id)).toEqual(['bob']) | ||
expect(got['two'].map((c) => c.id)).toEqual(['bob', 'wendy']) | ||
expect(got['four'].map((c) => c.id)).toEqual([]) | ||
expect(got['five'].map((c) => c.id)).toEqual(['alice', 'wendy']) | ||
}) | ||
it('ignores mismatching projects', async () => { | ||
const ids = ['one', 'two', 'three'] | ||
const got = await getCollaborators({ ids: ids, userId: 'bob' }) | ||
expect(Object.keys(got)).toEqual(['one', 'two']) | ||
expect(got['one'].map((c) => c.id)).toEqual(['bob']) | ||
expect(got['two'].map((c) => c.id)).toEqual(['bob', 'wendy']) | ||
}) | ||
}) | ||
|
||
describe('updateCollaborators', () => { | ||
beforeEach(async () => { | ||
await createTestUser(prisma, { id: 'bob' }) | ||
await createTestUser(prisma, { id: 'alice' }) | ||
await createTestUser(prisma, { id: 'wendy' }) | ||
await createTestProject(prisma, { id: 'one', ownerId: 'bob' }) | ||
await createTestProject(prisma, { id: 'two', ownerId: 'bob' }) | ||
await createTestProject(prisma, { id: 'three', ownerId: 'alice' }) | ||
await createTestProject(prisma, { id: 'four', ownerId: 'bob' }) | ||
await createTestProject(prisma, { id: 'five', ownerId: 'bob' }) | ||
await createTestProject(prisma, { id: 'six', ownerId: 'bob' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'one', userId: 'bob' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'two', userId: 'bob' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'two', userId: 'wendy' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'three', userId: 'alice' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'three', userId: 'bob' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'five', userId: 'alice' }) | ||
await createTestProjectCollaborator(prisma, { projectId: 'five', userId: 'wendy' }) | ||
}) | ||
|
||
const mockGetRoomStorage = (collabs: RoomCollaborators) => async (): Promise<RoomStorage> => { | ||
return { | ||
data: { | ||
collaborators: collabs, | ||
}, | ||
} | ||
} | ||
|
||
describe('when the project has collaborators', () => { | ||
describe('when the room storage has existing users', () => { | ||
it('updates the collaborators with the data from the room storage', async () => { | ||
LiveblocksAPI.getRoomStorage = mockGetRoomStorage({ | ||
data: { | ||
alice: { data: { id: 'alice', name: 'Alice Alisson', avatar: 'alice.png' } }, | ||
}, | ||
}) | ||
await updateCollaborators({ id: 'one' }) | ||
|
||
const got = await prisma.projectCollaborator.findMany({ where: { project_id: 'one' } }) | ||
expect(got.map((c) => c.user_id)).toEqual(['bob', 'alice']) | ||
}) | ||
}) | ||
describe('when the room storage has duplicate users', () => { | ||
it('only adds the missing ones', async () => { | ||
LiveblocksAPI.getRoomStorage = mockGetRoomStorage({ | ||
data: { | ||
alice: { data: { id: 'alice', name: 'Alice Alisson', avatar: 'alice.png' } }, | ||
bob: { data: { id: 'bob', name: 'Bob Bobson', avatar: 'bob.png' } }, | ||
}, | ||
}) | ||
await updateCollaborators({ id: 'one' }) | ||
|
||
const got = await prisma.projectCollaborator.findMany({ | ||
where: { project_id: 'one' }, | ||
}) | ||
expect(got.map((c) => c.user_id)).toEqual(['bob', 'alice']) | ||
}) | ||
}) | ||
describe('when the room storage has non-existing users', () => { | ||
it('updates the collaborators with only the existing users', async () => { | ||
LiveblocksAPI.getRoomStorage = mockGetRoomStorage({ | ||
data: { | ||
alice: { | ||
data: { | ||
id: 'alice', | ||
name: 'Alice Alisson', | ||
avatar: 'alice.png', | ||
}, | ||
}, | ||
johndoe: { | ||
data: { | ||
id: 'johndoe', | ||
name: 'John Doe', | ||
avatar: 'johndoe.png', | ||
}, | ||
}, | ||
}, | ||
}) | ||
await updateCollaborators({ id: 'one' }) | ||
|
||
const got = await prisma.projectCollaborator.findMany({ where: { project_id: 'one' } }) | ||
expect(got.map((c) => c.user_id)).toEqual(['bob', 'alice']) | ||
}) | ||
}) | ||
}) | ||
|
||
describe('when the project has no collaborators', () => { | ||
it('adds the collaborators with the data from the room storage', async () => { | ||
LiveblocksAPI.getRoomStorage = mockGetRoomStorage({ | ||
data: { | ||
alice: { data: { id: 'alice', name: 'Alice Alisson', avatar: 'alice.png' } }, | ||
johndoe: { data: { id: 'johndoe', name: 'John Doe', avatar: 'johndoe.png' } }, | ||
bob: { data: { id: 'bob', name: 'Bob Bobson', avatar: 'bob.png' } }, | ||
}, | ||
}) | ||
await updateCollaborators({ id: 'six' }) | ||
|
||
const got = await prisma.projectCollaborator.findMany({ where: { project_id: 'six' } }) | ||
expect(got.map((c) => c.user_id)).toEqual(['bob', 'alice']) | ||
}) | ||
}) | ||
|
||
describe('when the project does not exist', () => { | ||
it('errors', async () => { | ||
LiveblocksAPI.getRoomStorage = mockGetRoomStorage({ | ||
data: { | ||
alice: { data: { id: 'alice', name: 'Alice Alisson', avatar: 'alice.png' } }, | ||
}, | ||
}) | ||
const fn = async () => updateCollaborators({ id: 'unknown' }) | ||
await expect(fn).rejects.toThrow() | ||
}) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.