UBERF-5044 Pass workspace name as part of collaborative document name ()

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-01-24 15:10:47 +07:00 committed by GitHub
parent b0c64803bb
commit 6882c75775
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 127 additions and 41 deletions

View File

@ -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'
/**
@ -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(
@ -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(

View File

@ -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}`
}

View File

@ -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'
@ -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)
}
/**

View File

@ -150,7 +150,7 @@
return commandHandler
}
export function takeSnapshot (snapshotId: string): void {
export function takeSnapshot (snapshotId: DocumentId): void {
copyDocumentContent(documentId, snapshotId, { provider: remoteProvider }, initialContentId)
}

View File

@ -56,7 +56,7 @@
return collaborativeEditor?.commands()
}
export function takeSnapshot (snapshotId: string): void {
export function takeSnapshot (snapshotId: DocumentId): void {
collaborativeEditor?.takeSnapshot(snapshotId)
}

View File

@ -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

View File

@ -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(() => {

View File

@ -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'>> &

View File

@ -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
}

View File

@ -88,7 +88,7 @@ export function copyDocumentField (
export function copyDocumentContent (
documentId: DocumentId,
snapshotId: string,
snapshotId: DocumentId,
providerData: ProviderData,
initialContentId?: DocumentId
): void {

View File

@ -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> {

View File

@ -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('%')
}
@ -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
@ -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)
@ -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
}
@ -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) {

View File

@ -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 {
@ -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 } })
})

View File

@ -22,22 +22,29 @@ import { Context } from '../context'
import { StorageAdapter } from './adapter'
interface PlatformDocumentId {
workspace: string
objectClass: Ref<Class<Doc>>
objectId: Ref<Doc>
objectAttr: string
}
function parseDocumentId (documentId: string): PlatformDocumentId {
const [objectClass, objectId, objectAttr] = documentId.split('/')
const [workspace, objectClass, objectId, objectAttr] = documentId.split('/')
return {
workspace: workspace ?? '',
objectClass: (objectClass ?? '') as Ref<Class<Doc>>,
objectId: (objectId ?? '') as Ref<Doc>,
objectAttr: objectAttr ?? ''
}
}
function isValidDocumentId (documentId: PlatformDocumentId): boolean {
return documentId.objectClass !== '' && documentId.objectId !== '' && documentId.objectAttr !== ''
function isValidDocumentId (documentId: PlatformDocumentId, context: Context): boolean {
return (
documentId.objectClass !== '' &&
documentId.objectId !== '' &&
documentId.objectAttr !== '' &&
documentId.workspace === context.workspaceId.name
)
}
export class PlatformStorageAdapter implements StorageAdapter {
@ -48,9 +55,9 @@ export class PlatformStorageAdapter implements StorageAdapter {
async loadDocument (documentId: string, context: Context): Promise<YDoc | undefined> {
const { clientFactory } = context
const { objectId, objectClass, objectAttr } = parseDocumentId(documentId)
const { workspace, objectId, objectClass, objectAttr } = parseDocumentId(documentId)
if (!isValidDocumentId({ objectId, objectClass, objectAttr })) {
if (!isValidDocumentId({ workspace, objectId, objectClass, objectAttr }, context)) {
console.warn('malformed document id', documentId)
return undefined
}
@ -77,9 +84,9 @@ export class PlatformStorageAdapter implements StorageAdapter {
async saveDocument (documentId: string, document: YDoc, context: Context): Promise<void> {
const { clientFactory } = context
const { objectId, objectClass, objectAttr } = parseDocumentId(documentId)
const { workspace, objectId, objectClass, objectAttr } = parseDocumentId(documentId)
if (!isValidDocumentId({ objectId, objectClass, objectAttr })) {
if (!isValidDocumentId({ workspace, objectId, objectClass, objectAttr }, context)) {
console.warn('malformed document id', documentId)
return undefined
}