mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-26 10:20:01 +00:00
UBERF-5044 Pass workspace name as part of collaborative document name (#4424)
Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
parent
b0c64803bb
commit
6882c75775
packages
collaborator-client/src
presentation/src
text-editor/src
server/collaborator/src
@ -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(
|
||||
|
@ -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}`
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -150,7 +150,7 @@
|
||||
return commandHandler
|
||||
}
|
||||
|
||||
export function takeSnapshot (snapshotId: string): void {
|
||||
export function takeSnapshot (snapshotId: DocumentId): void {
|
||||
copyDocumentContent(documentId, snapshotId, { provider: remoteProvider }, initialContentId)
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@
|
||||
return collaborativeEditor?.commands()
|
||||
}
|
||||
|
||||
export function takeSnapshot (snapshotId: string): void {
|
||||
export function takeSnapshot (snapshotId: DocumentId): void {
|
||||
collaborativeEditor?.takeSnapshot(snapshotId)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(() => {
|
||||
|
@ -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'>> &
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ export function copyDocumentField (
|
||||
|
||||
export function copyDocumentContent (
|
||||
documentId: DocumentId,
|
||||
snapshotId: string,
|
||||
snapshotId: DocumentId,
|
||||
providerData: ProviderData,
|
||||
initialContentId?: DocumentId
|
||||
): void {
|
||||
|
@ -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> {
|
||||
|
@ -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) {
|
||||
|
@ -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 } })
|
||||
})
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user