Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UBERF-5044 Pass workspace name as part of collaborative document name #4424

Merged
merged 1 commit into from
Jan 24, 2024
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
26 changes: 17 additions & 9 deletions packages/collaborator-client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.
//

import { Class, Doc, Hierarchy, Markup, Ref, concatLink } from '@hcengineering/core'
import { Class, Doc, Hierarchy, Markup, Ref, WorkspaceId, concatLink } from '@hcengineering/core'
import { minioDocumentId, mongodbDocumentId } from './utils'

/**
Expand All @@ -27,25 +27,32 @@ export interface CollaboratorClient {
/**
* @public
*/
export function getClient (hierarchy: Hierarchy, token: string, collaboratorUrl: string): CollaboratorClient {
return new CollaboratorClientImpl(hierarchy, token, collaboratorUrl)
export function getClient (
hierarchy: Hierarchy,
workspaceId: WorkspaceId,
token: string,
collaboratorUrl: string
): CollaboratorClient {
return new CollaboratorClientImpl(hierarchy, workspaceId, token, collaboratorUrl)
}

class CollaboratorClientImpl implements CollaboratorClient {
constructor (
private readonly hierarchy: Hierarchy,
private readonly workspace: WorkspaceId,
private readonly token: string,
private readonly collaboratorUrl: string
) {}

initialContentId (classId: Ref<Class<Doc>>, docId: Ref<Doc>, attribute: string): string {
initialContentId (workspace: string, classId: Ref<Class<Doc>>, docId: Ref<Doc>, attribute: string): string {
const domain = this.hierarchy.getDomain(classId)
return mongodbDocumentId(domain, docId, attribute)
return mongodbDocumentId(workspace, domain, docId, attribute)
}

async get (classId: Ref<Class<Doc>>, docId: Ref<Doc>, attribute: string): Promise<Markup> {
const documentId = encodeURIComponent(minioDocumentId(docId, attribute))
const initialContentId = encodeURIComponent(this.initialContentId(classId, docId, attribute))
const workspace = this.workspace.name
const documentId = encodeURIComponent(minioDocumentId(workspace, docId, attribute))
const initialContentId = encodeURIComponent(this.initialContentId(workspace, classId, docId, attribute))
attribute = encodeURIComponent(attribute)

const url = concatLink(
Expand All @@ -65,8 +72,9 @@ class CollaboratorClientImpl implements CollaboratorClient {
}

async update (classId: Ref<Class<Doc>>, docId: Ref<Doc>, attribute: string, value: Markup): Promise<void> {
const documentId = encodeURIComponent(minioDocumentId(docId, attribute))
const initialContentId = encodeURIComponent(this.initialContentId(classId, docId, attribute))
const workspace = this.workspace.name
const documentId = encodeURIComponent(minioDocumentId(workspace, docId, attribute))
const initialContentId = encodeURIComponent(this.initialContentId(workspace, classId, docId, attribute))
attribute = encodeURIComponent(attribute)

const url = concatLink(
Expand Down
8 changes: 4 additions & 4 deletions packages/collaborator-client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

import { Doc, Domain, Ref } from '@hcengineering/core'

export function minioDocumentId (docId: Ref<Doc>, attribute?: string): string {
return attribute !== undefined ? `minio://${docId}%${attribute}` : `minio://${docId}`
export function minioDocumentId (workspace: string, docId: Ref<Doc>, attribute?: string): string {
return attribute !== undefined ? `minio://${workspace}/${docId}%${attribute}` : `minio://${workspace}/${docId}`
}

export function mongodbDocumentId (domain: Domain, docId: Ref<Doc>, attribute: string): string {
return `mongodb://${domain}/${docId}/${attribute}`
export function mongodbDocumentId (workspace: string, domain: Domain, docId: Ref<Doc>, attribute: string): string {
return `mongodb://${workspace}/${domain}/${docId}/${attribute}`
}
6 changes: 4 additions & 2 deletions packages/presentation/src/collaborator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
//

import { type CollaboratorClient, getClient as getCollaborator } from '@hcengineering/collaborator-client'
import type { Class, Doc, Markup, Ref } from '@hcengineering/core'
import { getWorkspaceId, type Class, type Doc, type Markup, type Ref } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import { getCurrentLocation } from '@hcengineering/ui'

import { getClient } from '.'
import presentation from './plugin'
Expand All @@ -24,11 +25,12 @@ import presentation from './plugin'
* @public
*/
export function getCollaboratorClient (): CollaboratorClient {
const workspaceId = getWorkspaceId(getCurrentLocation().path[1] ?? '')
const hierarchy = getClient().getHierarchy()
const token = getMetadata(presentation.metadata.Token) ?? ''
const collaboratorURL = getMetadata(presentation.metadata.CollaboratorUrl) ?? ''

return getCollaborator(hierarchy, token, collaboratorURL)
return getCollaborator(hierarchy, workspaceId, token, collaboratorURL)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
return commandHandler
}

export function takeSnapshot (snapshotId: string): void {
export function takeSnapshot (snapshotId: DocumentId): void {
copyDocumentContent(documentId, snapshotId, { provider: remoteProvider }, initialContentId)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
return collaborativeEditor?.commands()
}

export function takeSnapshot (snapshotId: string): void {
export function takeSnapshot (snapshotId: DocumentId): void {
collaborativeEditor?.takeSnapshot(snapshotId)
}

Expand Down
1 change: 1 addition & 0 deletions packages/text-editor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export { ImageExtension, type ImageOptions } from './components/extension/imageE
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'

export {
type DocumentId,
TiptapCollabProvider,
type TiptapCollabProviderConfiguration,
createTiptapCollaborationData
Expand Down
4 changes: 4 additions & 0 deletions packages/text-editor/src/provider/minio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export class MinioProvider extends Observable<EVENTS> {

if (name.startsWith('minio://')) {
name = name.split('://', 2)[1]
if (name.includes('/')) {
// drop workspace part
name = name.split('/', 2)[1]
}
}

void fetchContent(doc, name).then(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/text-editor/src/provider/tiptap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { Doc as Ydoc } from 'yjs'
import { HocuspocusProvider, type HocuspocusProviderConfiguration } from '@hocuspocus/provider'

export type DocumentId = string
export type DocumentId = string & { __documentId: true }

export type TiptapCollabProviderConfiguration = HocuspocusProviderConfiguration &
Required<Pick<HocuspocusProviderConfiguration, 'token'>> &
Expand Down
16 changes: 13 additions & 3 deletions packages/text-editor/src/provider/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,28 @@

import type { Doc, Ref } from '@hcengineering/core'
import { type KeyedAttribute, getClient } from '@hcengineering/presentation'
import { getCurrentLocation } from '@hcengineering/ui'

import { type DocumentId } from './tiptap'

function getWorkspace (): string {
return getCurrentLocation().path[1] ?? ''
}

export function minioDocumentId (docId: Ref<Doc>, attr?: KeyedAttribute): DocumentId {
return attr !== undefined ? `minio://${docId}%${attr.key}` : `minio://${docId}`
const workspace = getWorkspace()
return attr !== undefined
? (`minio://${workspace}/${docId}%${attr.key}` as DocumentId)
: (`minio://${workspace}/${docId}` as DocumentId)
}

export function platformDocumentId (docId: Ref<Doc>, attr: KeyedAttribute): DocumentId {
return `platform://${attr.attr.attributeOf}/${docId}/${attr.key}`
const workspace = getWorkspace()
return `platform://${workspace}/${attr.attr.attributeOf}/${docId}/${attr.key}` as DocumentId
}

export function mongodbDocumentId (docId: Ref<Doc>, attr: KeyedAttribute): DocumentId {
const workspace = getWorkspace()
const domain = getClient().getHierarchy().getDomain(attr.attr.attributeOf)
return `mongodb://${domain}/${docId}/${attr.key}`
return `mongodb://${workspace}/${domain}/${docId}/${attr.key}` as DocumentId
}
2 changes: 1 addition & 1 deletion packages/text-editor/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function copyDocumentField (

export function copyDocumentContent (
documentId: DocumentId,
snapshotId: string,
snapshotId: DocumentId,
providerData: ProviderData,
initialContentId?: DocumentId
): void {
Expand Down
19 changes: 18 additions & 1 deletion server/collaborator/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,24 @@ export async function start (

async onAuthenticate (data: onAuthenticatePayload): Promise<Context> {
ctx.measure('authenticate', 1)
return buildContext(data, controller)
const context = buildContext(data, controller)

// verify document name
let documentName = data.documentName
if (documentName.includes('://')) {
documentName = documentName.split('://', 2)[1]
}

if (documentName.includes('/')) {
const [workspace] = documentName.split('/', 2)
if (workspace !== context.workspaceId.name) {
throw new Error('documentName must include workspace')
}
} else {
throw new Error('documentName must include workspace')
}

return context
},

async onDestroy (data: onDestroyPayload): Promise<void> {
Expand Down
39 changes: 35 additions & 4 deletions server/collaborator/src/storage/minio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ import { Context } from '../context'

import { StorageAdapter } from './adapter'

interface MinioDocumentId {
workspace: string
minioDocumentId: string
}

function parseDocumentId (documentId: string): MinioDocumentId {
const [workspace, minioDocumentId] = documentId.split('/')
return {
workspace: workspace ?? '',
minioDocumentId: minioDocumentId ?? ''
}
}

function isValidDocumentId (documentId: MinioDocumentId, context: Context): boolean {
return documentId.minioDocumentId !== '' && documentId.workspace === context.workspaceId.name
}

function maybePlatformDocumentId (documentId: string): boolean {
return !documentId.includes('%')
}
Expand All @@ -35,10 +52,17 @@ export class MinioStorageAdapter implements StorageAdapter {
async loadDocument (documentId: string, context: Context): Promise<YDoc | undefined> {
const { workspaceId } = context

const { workspace, minioDocumentId } = parseDocumentId(documentId)

if (!isValidDocumentId({ workspace, minioDocumentId }, context)) {
console.warn('malformed document id', documentId)
return undefined
}

return await this.ctx.with('load-document', {}, async (ctx) => {
const minioDocument = await ctx.with('query', {}, async () => {
try {
const buffer = await this.minio.read(workspaceId, documentId)
const buffer = await this.minio.read(workspaceId, minioDocumentId)
return Buffer.concat(buffer)
} catch {
return undefined
Expand Down Expand Up @@ -67,6 +91,13 @@ export class MinioStorageAdapter implements StorageAdapter {
async saveDocument (documentId: string, document: YDoc, context: Context): Promise<void> {
const { clientFactory, workspaceId } = context

const { workspace, minioDocumentId } = parseDocumentId(documentId)

if (!isValidDocumentId({ workspace, minioDocumentId }, context)) {
console.warn('malformed document id', documentId)
return undefined
}

await this.ctx.with('save-document', {}, async (ctx) => {
const buffer = await ctx.with('transform', {}, () => {
const updates = encodeStateAsUpdate(document)
Expand All @@ -75,13 +106,13 @@ export class MinioStorageAdapter implements StorageAdapter {

await ctx.with('update', {}, async () => {
const metadata = { 'content-type': 'application/ydoc' }
await this.minio.put(workspaceId, documentId, buffer, buffer.length, metadata)
await this.minio.put(workspaceId, minioDocumentId, buffer, buffer.length, metadata)
})

// minio file is usually an attachment document
// we need to touch an attachment from here to notify platform about changes

if (!maybePlatformDocumentId(documentId)) {
if (!maybePlatformDocumentId(minioDocumentId)) {
// documentId is not a platform document id, we can skip platform notification
return
}
Expand All @@ -92,7 +123,7 @@ export class MinioStorageAdapter implements StorageAdapter {
})

const current = await ctx.with('query', {}, async () => {
return await client.findOne(attachment.class.Attachment, { _id: documentId as Ref<Attachment> })
return await client.findOne(attachment.class.Attachment, { _id: minioDocumentId as Ref<Attachment> })
})

if (current !== undefined) {
Expand Down
20 changes: 13 additions & 7 deletions server/collaborator/src/storage/mongodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,29 @@ import { Context } from '../context'
import { StorageAdapter } from './adapter'

interface MongodbDocumentId {
workspace: string
objectDomain: string
objectId: string
objectAttr: string
}

function parseDocumentId (documentId: string): MongodbDocumentId {
const [objectDomain, objectId, objectAttr] = documentId.split('/')
const [workspace, objectDomain, objectId, objectAttr] = documentId.split('/')
return {
workspace: workspace ?? '',
objectId: objectId ?? '',
objectDomain: objectDomain ?? '',
objectAttr: objectAttr ?? ''
}
}

function isValidDocumentId (documentId: MongodbDocumentId): boolean {
return documentId.objectDomain !== '' && documentId.objectId !== '' && documentId.objectAttr !== ''
function isValidDocumentId (documentId: MongodbDocumentId, context: Context): boolean {
return (
documentId.objectDomain !== '' &&
documentId.objectId !== '' &&
documentId.objectAttr !== '' &&
documentId.workspace === context.workspaceId.name
)
}

export class MongodbStorageAdapter implements StorageAdapter {
Expand All @@ -49,17 +56,16 @@ export class MongodbStorageAdapter implements StorageAdapter {
) {}

async loadDocument (documentId: string, context: Context): Promise<YDoc | undefined> {
const { workspaceId } = context
const { objectId, objectDomain, objectAttr } = parseDocumentId(documentId)
const { workspace, objectId, objectDomain, objectAttr } = parseDocumentId(documentId)

if (!isValidDocumentId({ objectId, objectDomain, objectAttr })) {
if (!isValidDocumentId({ workspace, objectId, objectDomain, objectAttr }, context)) {
console.warn('malformed document id', documentId)
return undefined
}

return await this.ctx.with('load-document', {}, async (ctx) => {
const doc = await ctx.with('query', {}, async () => {
const db = this.mongodb.db(toWorkspaceString(workspaceId))
const db = this.mongodb.db(toWorkspaceString(context.workspaceId))
return await db.collection(objectDomain).findOne({ _id: objectId }, { projection: { [objectAttr]: 1 } })
})

Expand Down
Loading