Fix packages publish

This commit is contained in:
Andrey Sobolev
2025-10-09 18:21:52 +07:00
parent 16a61a496c
commit 885c3039bc
15 changed files with 904 additions and 37 deletions
+3 -3
View File
@@ -45,9 +45,9 @@
"@hcengineering/server-token": "^0.7.0",
"ws": "^8.18.2"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+3 -3
View File
@@ -41,9 +41,9 @@
"base64-js": "^1.5.1",
"yjs": "^13.6.27"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
@@ -0,0 +1,533 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import {
type Blob,
type BlobMetadata,
type CollaborativeDoc,
type MeasureContext,
type Ref,
type WorkspaceId,
type WorkspaceIds,
makeCollabJsonId,
makeCollabYdocId
} from '@hcengineering/core'
import { type StorageAdapter } from '@hcengineering/server-core'
import { Doc as YDoc } from 'yjs'
import {
loadCollabYdoc,
saveCollabYdoc,
removeCollabYdoc,
loadCollabJson,
saveCollabJson
} from '../storage'
import { yDocToBuffer } from '../ydoc'
// Mock StorageAdapter
class MockStorageAdapter implements StorageAdapter {
private readonly storage = new Map<string, { buffer: Buffer, contentType: string, size: number }>()
async stat (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise<BlobMetadata | undefined> {
const data = this.storage.get(name)
if (data === undefined) {
return undefined
}
return {
_id: name as Ref<Blob>,
_class: '' as any,
space: '' as any,
modifiedBy: '' as any,
modifiedOn: Date.now(),
provider: '',
size: data.size,
storageId: name,
contentType: data.contentType,
etag: 'mock-etag',
version: null
}
}
async read (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise<Buffer[]> {
const data = this.storage.get(name)
if (data === undefined) {
throw new Error(`Blob not found: ${name}`)
}
return [data.buffer]
}
async put (
ctx: MeasureContext,
wsIds: WorkspaceIds,
name: string,
data: Buffer | Readable,
contentType: string,
size?: number
): Promise<Ref<Blob>> {
const buffer = Buffer.isBuffer(data) ? data : await this.streamToBuffer(data)
this.storage.set(name, { buffer, contentType, size: size ?? buffer.length })
return name as Ref<Blob>
}
async remove (ctx: MeasureContext, wsIds: WorkspaceIds, names: string[]): Promise<void> {
for (const name of names) {
this.storage.delete(name)
}
}
async partial (
ctx: MeasureContext,
wsIds: WorkspaceIds,
name: string,
offset: number,
length?: number
): Promise<Buffer | undefined> {
const data = this.storage.get(name)
if (data === undefined) {
return undefined
}
const end = length !== undefined ? offset + length : undefined
return data.buffer.slice(offset, end)
}
async deleteMany (
ctx: MeasureContext,
iterator: { next: () => Promise<string | undefined> }
): Promise<{ fileCount: number, size: number }> {
let fileCount = 0
let size = 0
while (true) {
const name = await iterator.next()
if (name === undefined) break
const data = this.storage.get(name)
if (data !== undefined) {
size += data.size
this.storage.delete(name)
fileCount++
}
}
return { fileCount, size }
}
async listAllBuckets (ctx: MeasureContext): Promise<WorkspaceId[]> {
return []
}
async listChunksBuckets (ctx: MeasureContext): Promise<WorkspaceId[]> {
return []
}
async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<void> {
// No-op for mock
}
async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<void> {
// No-op for mock
}
async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise<boolean> {
return true
}
private async streamToBuffer (stream: Readable): Promise<Buffer> {
const chunks: Buffer[] = []
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk))
}
return Buffer.concat(chunks)
}
}
// Mock MeasureContext
const mockContext: MeasureContext = {
id: 'test-context',
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
with: jest.fn().mockImplementation(async (name, params, fn) => await fn()),
measure: jest.fn(),
newChild: jest.fn().mockReturnThis(),
end: jest.fn(),
logger: {} as any,
metrics: {} as any,
parent: undefined,
ctx: {}
}
const mockWorkspaceIds: WorkspaceIds = {
workspaceId: 'test-workspace' as WorkspaceId,
workspaceName: 'Test Workspace',
workspaceUrl: 'test-workspace'
}
const createMockCollaborativeDoc = (id: string): CollaborativeDoc => ({
_id: id as any,
_class: 'collab:class:CollaborativeDoc' as any,
space: 'space:test' as any,
modifiedBy: 'user:test' as any,
modifiedOn: Date.now(),
objectId: `object:${id}` as any,
objectClass: 'class:test' as any,
objectAttr: 'attr:test'
})
describe('storage', () => {
let storageAdapter: MockStorageAdapter
let ctx: MeasureContext
beforeEach(() => {
storageAdapter = new MockStorageAdapter()
ctx = mockContext
})
describe('loadCollabYdoc', () => {
it('should load existing YDoc from storage', async () => {
const doc = createMockCollaborativeDoc('test-doc-1')
const ydoc = new YDoc()
ydoc.getMap('test').set('key', 'value')
const buffer = yDocToBuffer(ydoc)
const blobId = makeCollabYdocId(doc)
await storageAdapter.put(ctx, mockWorkspaceIds, blobId, buffer, 'application/ydoc', buffer.length)
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedDoc).toBeDefined()
expect(loadedDoc?.getMap('test').get('key')).toBe('value')
})
it('should return undefined for non-existent document', async () => {
const doc = createMockCollaborativeDoc('non-existent')
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedDoc).toBeUndefined()
})
it('should warn on invalid content type', async () => {
const doc = createMockCollaborativeDoc('test-doc-2')
const ydoc = new YDoc()
const buffer = yDocToBuffer(ydoc)
const blobId = makeCollabYdocId(doc)
await storageAdapter.put(ctx, mockWorkspaceIds, blobId, buffer, 'text/plain', buffer.length)
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedDoc).toBeDefined()
expect(ctx.warn).toHaveBeenCalledWith('invalid content type', { contentType: 'text/plain' })
})
it('should load YDoc from blob reference', async () => {
const blobId = 'blob:test-123' as Ref<Blob>
const ydoc = new YDoc()
ydoc.getMap('data').set('test', 42)
const buffer = yDocToBuffer(ydoc)
await storageAdapter.put(ctx, mockWorkspaceIds, blobId, buffer, 'application/ydoc', buffer.length)
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedDoc).toBeDefined()
expect(loadedDoc?.getMap('data').get('test')).toBe(42)
})
it('should preserve complex YDoc structures', async () => {
const doc = createMockCollaborativeDoc('complex-doc')
const ydoc = new YDoc()
const ymap = ydoc.getMap('map')
ymap.set('string', 'text')
ymap.set('number', 123)
ymap.set('boolean', true)
const yarray = ydoc.getArray('array')
yarray.push(['item1', 'item2', 'item3'])
const ytext = ydoc.getText('text')
ytext.insert(0, 'Hello World')
const buffer = yDocToBuffer(ydoc)
const blobId = makeCollabYdocId(doc)
await storageAdapter.put(ctx, mockWorkspaceIds, blobId, buffer, 'application/ydoc', buffer.length)
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedDoc).toBeDefined()
expect(loadedDoc?.getMap('map').get('string')).toBe('text')
expect(loadedDoc?.getMap('map').get('number')).toBe(123)
expect(loadedDoc?.getMap('map').get('boolean')).toBe(true)
expect(loadedDoc?.getArray('array').toArray()).toEqual(['item1', 'item2', 'item3'])
expect(String(loadedDoc?.getText('text'))).toBe('Hello World')
})
})
describe('saveCollabYdoc', () => {
it('should save YDoc to storage', async () => {
const doc = createMockCollaborativeDoc('save-test')
const ydoc = new YDoc()
ydoc.getMap('data').set('saved', true)
const blobId = await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc, ydoc)
expect(blobId).toBe(makeCollabYdocId(doc))
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedDoc?.getMap('data').get('saved')).toBe(true)
})
it('should save YDoc with blob reference', async () => {
const blobId = 'blob:custom-id' as Ref<Blob>
const ydoc = new YDoc()
ydoc.getMap('test').set('value', 'custom')
const returnedId = await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, blobId, ydoc)
expect(returnedId).toBe(blobId)
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedDoc?.getMap('test').get('value')).toBe('custom')
})
it('should overwrite existing document', async () => {
const doc = createMockCollaborativeDoc('overwrite-test')
const ydoc1 = new YDoc()
ydoc1.getMap('data').set('version', 1)
await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc, ydoc1)
const ydoc2 = new YDoc()
ydoc2.getMap('data').set('version', 2)
await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc, ydoc2)
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedDoc?.getMap('data').get('version')).toBe(2)
})
it('should save empty YDoc', async () => {
const doc = createMockCollaborativeDoc('empty-doc')
const ydoc = new YDoc()
const blobId = await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc, ydoc)
expect(blobId).toBe(makeCollabYdocId(doc))
const loadedDoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedDoc).toBeDefined()
})
})
describe('removeCollabYdoc', () => {
it('should remove single document', async () => {
const doc = createMockCollaborativeDoc('remove-test')
const ydoc = new YDoc()
await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc, ydoc)
const loadedBefore = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedBefore).toBeDefined()
await removeCollabYdoc(storageAdapter, mockWorkspaceIds, [doc], ctx)
const loadedAfter = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedAfter).toBeUndefined()
})
it('should remove multiple documents', async () => {
const doc1 = createMockCollaborativeDoc('remove-test-1')
const doc2 = createMockCollaborativeDoc('remove-test-2')
const doc3 = createMockCollaborativeDoc('remove-test-3')
const ydoc = new YDoc()
await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc1, ydoc)
await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc2, ydoc)
await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc3, ydoc)
await removeCollabYdoc(storageAdapter, mockWorkspaceIds, [doc1, doc2, doc3], ctx)
expect(await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc1)).toBeUndefined()
expect(await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc2)).toBeUndefined()
expect(await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc3)).toBeUndefined()
})
it('should handle empty array', async () => {
await expect(
removeCollabYdoc(storageAdapter, mockWorkspaceIds, [], ctx)
).resolves.not.toThrow()
})
it('should not error on non-existent documents', async () => {
const doc = createMockCollaborativeDoc('non-existent-doc')
await expect(
removeCollabYdoc(storageAdapter, mockWorkspaceIds, [doc], ctx)
).resolves.not.toThrow()
})
})
describe('loadCollabJson', () => {
it('should load JSON markup from storage', async () => {
const blobId = 'blob:json-test' as Ref<Blob>
const markup = '{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Hello"}]}]}'
const buffer = Buffer.from(markup)
await storageAdapter.put(ctx, mockWorkspaceIds, blobId, buffer, 'application/json', buffer.length)
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedMarkup).toBe(markup)
})
it('should return undefined for non-existent blob', async () => {
const blobId = 'blob:non-existent' as Ref<Blob>
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedMarkup).toBeUndefined()
})
it('should warn on invalid content type', async () => {
const blobId = 'blob:invalid-type' as Ref<Blob>
const markup = '{"type":"doc"}'
const buffer = Buffer.from(markup)
await storageAdapter.put(ctx, mockWorkspaceIds, blobId, buffer, 'text/plain', buffer.length)
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedMarkup).toBe(markup)
expect(ctx.warn).toHaveBeenCalledWith('invalid content type', { contentType: 'text/plain' })
})
it('should handle empty JSON', async () => {
const blobId = 'blob:empty-json' as Ref<Blob>
const markup = ''
const buffer = Buffer.from(markup)
await storageAdapter.put(ctx, mockWorkspaceIds, blobId, buffer, 'application/json', buffer.length)
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedMarkup).toBe('')
})
})
describe('saveCollabJson', () => {
it('should save markup string to storage', async () => {
const doc = createMockCollaborativeDoc('json-save-test')
const markup = '{"type":"doc","content":[]}'
const blobId = await saveCollabJson(ctx, storageAdapter, mockWorkspaceIds, doc, markup)
expect(blobId).toBe(makeCollabJsonId(doc))
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedMarkup).toBe(markup)
})
it('should save YDoc as markup to storage', async () => {
const doc = createMockCollaborativeDoc('ydoc-to-json-test')
const ydoc = new YDoc()
// Note: yDocToMarkup is mocked, so we're testing the flow not the actual conversion
const blobId = await saveCollabJson(ctx, storageAdapter, mockWorkspaceIds, doc, ydoc)
expect(blobId).toBe(makeCollabJsonId(doc))
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedMarkup).toBeDefined()
})
it('should overwrite existing JSON', async () => {
const doc = createMockCollaborativeDoc('json-overwrite-test')
const markup1 = '{"version":1}'
await saveCollabJson(ctx, storageAdapter, mockWorkspaceIds, doc, markup1)
const markup2 = '{"version":2}'
await saveCollabJson(ctx, storageAdapter, mockWorkspaceIds, doc, markup2)
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, makeCollabJsonId(doc))
expect(loadedMarkup).toBe(markup2)
})
it('should handle empty markup', async () => {
const doc = createMockCollaborativeDoc('empty-markup-test')
const markup = ''
const blobId = await saveCollabJson(ctx, storageAdapter, mockWorkspaceIds, doc, markup)
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedMarkup).toBe('')
})
it('should handle complex JSON structures', async () => {
const doc = createMockCollaborativeDoc('complex-json-test')
const markup = JSON.stringify({
type: 'doc',
content: [
{
type: 'heading',
attrs: { level: 1 },
content: [{ type: 'text', text: 'Title' }]
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'This is ' },
{ type: 'text', marks: [{ type: 'bold' }], text: 'bold' },
{ type: 'text', text: ' text.' }
]
}
]
})
const blobId = await saveCollabJson(ctx, storageAdapter, mockWorkspaceIds, doc, markup)
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, blobId)
expect(loadedMarkup).toBe(markup)
expect(JSON.parse(loadedMarkup ?? '')).toEqual(JSON.parse(markup))
})
})
describe('integration tests', () => {
it('should handle round-trip save and load of YDoc', async () => {
const doc = createMockCollaborativeDoc('round-trip-test')
const originalYdoc = new YDoc()
originalYdoc.getMap('meta').set('title', 'Test Document')
originalYdoc.getArray('items').push(['a', 'b', 'c'])
await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc, originalYdoc)
const loadedYdoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
expect(loadedYdoc?.getMap('meta').get('title')).toBe('Test Document')
expect(loadedYdoc?.getArray('items').toArray()).toEqual(['a', 'b', 'c'])
})
it('should handle independent YDoc and JSON storage', async () => {
const doc = createMockCollaborativeDoc('independent-test')
const ydoc = new YDoc()
ydoc.getMap('data').set('type', 'ydoc')
await saveCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc, ydoc)
const markup = '{"type":"json"}'
await saveCollabJson(ctx, storageAdapter, mockWorkspaceIds, doc, markup)
const loadedYdoc = await loadCollabYdoc(ctx, storageAdapter, mockWorkspaceIds, doc)
const loadedMarkup = await loadCollabJson(ctx, storageAdapter, mockWorkspaceIds, makeCollabJsonId(doc))
expect(loadedYdoc?.getMap('data').get('type')).toBe('ydoc')
expect(loadedMarkup).toBe(markup)
})
})
})
@@ -0,0 +1,334 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Doc as YDoc, XmlElement as YXmlElement, XmlText as YXmlText } from 'yjs'
import { yDocFromBuffer, yDocToBuffer, yDocCopyXmlField, yXmlElementClone } from '../ydoc'
describe('ydoc', () => {
describe('yDocFromBuffer and yDocToBuffer', () => {
it('should convert YDoc to buffer and back', () => {
const ydoc = new YDoc()
const ymap = ydoc.getMap('test')
ymap.set('key', 'value')
ymap.set('number', 42)
ymap.set('boolean', true)
const buffer = yDocToBuffer(ydoc)
expect(buffer).toBeInstanceOf(Buffer)
expect(buffer.length).toBeGreaterThan(0)
const restoredDoc = yDocFromBuffer(buffer)
const restoredMap = restoredDoc.getMap('test')
expect(restoredMap.get('key')).toBe('value')
expect(restoredMap.get('number')).toBe(42)
expect(restoredMap.get('boolean')).toBe(true)
})
it('should preserve XML fragments', () => {
const ydoc = new YDoc()
const fragment = ydoc.getXmlFragment('content')
const element = new YXmlElement('p')
element.setAttribute('id', 'test-paragraph')
fragment.insert(0, [element])
const buffer = yDocToBuffer(ydoc)
const restoredDoc = yDocFromBuffer(buffer)
const restoredFragment = restoredDoc.getXmlFragment('content')
expect(restoredFragment.length).toBe(1)
const restoredElement = restoredFragment.get(0) as YXmlElement
expect(restoredElement.nodeName).toBe('p')
expect(restoredElement.getAttribute('id')).toBe('test-paragraph')
})
it('should work with provided YDoc instance', () => {
const sourceDoc = new YDoc()
sourceDoc.getMap('data').set('test', 'value')
const buffer = yDocToBuffer(sourceDoc)
const targetDoc = new YDoc()
const restoredDoc = yDocFromBuffer(buffer, targetDoc)
expect(restoredDoc).toBe(targetDoc)
expect(restoredDoc.getMap('data').get('test')).toBe('value')
})
it('should handle empty YDoc', () => {
const ydoc = new YDoc()
const buffer = yDocToBuffer(ydoc)
expect(buffer).toBeInstanceOf(Buffer)
const restoredDoc = yDocFromBuffer(buffer)
expect(restoredDoc).toBeInstanceOf(YDoc)
})
it('should throw error on invalid buffer', () => {
const invalidBuffer = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF])
expect(() => {
yDocFromBuffer(invalidBuffer)
}).toThrow('Failed to apply ydoc update')
})
it('should preserve array data', () => {
const ydoc = new YDoc()
const yarray = ydoc.getArray('items')
yarray.push(['item1', 'item2', 'item3'])
const buffer = yDocToBuffer(ydoc)
const restoredDoc = yDocFromBuffer(buffer)
const restoredArray = restoredDoc.getArray('items')
expect(restoredArray.length).toBe(3)
expect(restoredArray.toArray()).toEqual(['item1', 'item2', 'item3'])
})
it('should preserve text data', () => {
const ydoc = new YDoc()
const ytext = ydoc.getText('note')
ytext.insert(0, 'Hello World')
const buffer = yDocToBuffer(ydoc)
const restoredDoc = yDocFromBuffer(buffer)
const restoredText = restoredDoc.getText('note')
expect(String(restoredText)).toBe('Hello World')
})
})
describe('yDocCopyXmlField', () => {
it('should copy XML fragment from source to target', () => {
const ydoc = new YDoc()
const source = ydoc.getXmlFragment('source')
const target = ydoc.getXmlFragment('target')
const elem1 = new YXmlElement('p')
const text1 = new YXmlText('Hello')
elem1.insert(0, [text1])
elem1.setAttribute('class', 'paragraph')
const elem2 = new YXmlElement('div')
elem2.setAttribute('id', 'container')
source.insert(0, [elem1, elem2])
yDocCopyXmlField(ydoc, 'source', 'target')
expect(target.length).toBe(2)
const copiedElem1 = target.get(0) as YXmlElement
const copiedElem2 = target.get(1) as YXmlElement
expect(copiedElem1.nodeName).toBe('p')
expect(copiedElem1.getAttribute('class')).toBe('paragraph')
expect(copiedElem2.nodeName).toBe('div')
expect(copiedElem2.getAttribute('id')).toBe('container')
})
it('should overwrite existing target content', () => {
const ydoc = new YDoc()
const sourceFragment = ydoc.getXmlFragment('source')
const target = ydoc.getXmlFragment('target')
// Add content to target
const oldElem = new YXmlElement('old')
target.insert(0, [oldElem])
expect(target.length).toBe(1)
// Add content to source
const newElem = new YXmlElement('new')
sourceFragment.insert(0, [newElem])
yDocCopyXmlField(ydoc, 'source', 'target')
expect(target.length).toBe(1)
const copiedElem = target.get(0) as YXmlElement
expect(copiedElem.nodeName).toBe('new')
})
it('should handle empty source', () => {
const ydoc = new YDoc()
const source = ydoc.getXmlFragment('source')
const target = ydoc.getXmlFragment('target')
const elem = new YXmlElement('test')
target.insert(0, [elem])
yDocCopyXmlField(ydoc, 'source', 'target')
expect(target.length).toBe(0)
})
it('should copy nested XML structures', () => {
const ydoc = new YDoc()
const source = ydoc.getXmlFragment('source')
const parent = new YXmlElement('div')
const child = new YXmlElement('span')
child.setAttribute('class', 'child')
parent.insert(0, [child])
parent.setAttribute('class', 'parent')
source.insert(0, [parent])
yDocCopyXmlField(ydoc, 'source', 'target')
const target = ydoc.getXmlFragment('target')
const copiedParent = target.get(0) as YXmlElement
expect(copiedParent.nodeName).toBe('div')
expect(copiedParent.getAttribute('class')).toBe('parent')
expect(copiedParent.length).toBe(1)
const copiedChild = copiedParent.get(0) as YXmlElement
expect(copiedChild.nodeName).toBe('span')
expect(copiedChild.getAttribute('class')).toBe('child')
})
})
describe('yXmlElementClone', () => {
it('should clone simple XML element', () => {
const elem = new YXmlElement('p')
elem.setAttribute('class', 'text')
elem.setAttribute('id', 'paragraph-1')
const clone = yXmlElementClone(elem)
expect(clone.nodeName).toBe('p')
expect(clone.getAttribute('class')).toBe('text')
expect(clone.getAttribute('id')).toBe('paragraph-1')
})
it('should clone element with text content', () => {
const elem = new YXmlElement('p')
const text = new YXmlText('Hello World')
elem.insert(0, [text])
const clone = yXmlElementClone(elem)
expect(clone.nodeName).toBe('p')
expect(clone.length).toBe(1)
const clonedText = clone.get(0) as YXmlText
expect(clonedText.toString()).toBe('Hello World')
})
it('should clone nested XML elements', () => {
const parent = new YXmlElement('div')
parent.setAttribute('class', 'container')
const child1 = new YXmlElement('p')
child1.setAttribute('id', 'para-1')
const child2 = new YXmlElement('span')
child2.setAttribute('id', 'span-1')
parent.insert(0, [child1, child2])
const clone = yXmlElementClone(parent)
expect(clone.nodeName).toBe('div')
expect(clone.getAttribute('class')).toBe('container')
expect(clone.length).toBe(2)
const clonedChild1 = clone.get(0) as YXmlElement
expect(clonedChild1.nodeName).toBe('p')
expect(clonedChild1.getAttribute('id')).toBe('para-1')
const clonedChild2 = clone.get(1) as YXmlElement
expect(clonedChild2.nodeName).toBe('span')
expect(clonedChild2.getAttribute('id')).toBe('span-1')
})
it('should clone deeply nested structure', () => {
const root = new YXmlElement('div')
const level1 = new YXmlElement('section')
const level2 = new YXmlElement('article')
const level3 = new YXmlElement('p')
const text = new YXmlText('Deep text')
level3.insert(0, [text])
level2.insert(0, [level3])
level1.insert(0, [level2])
root.insert(0, [level1])
root.setAttribute('data-root', 'true')
level1.setAttribute('data-level', '1')
level2.setAttribute('data-level', '2')
level3.setAttribute('data-level', '3')
const clone = yXmlElementClone(root)
expect(clone.getAttribute('data-root')).toBe('true')
const clonedLevel1 = clone.get(0) as YXmlElement
expect(clonedLevel1.getAttribute('data-level')).toBe('1')
const clonedLevel2 = clonedLevel1.get(0) as YXmlElement
expect(clonedLevel2.getAttribute('data-level')).toBe('2')
const clonedLevel3 = clonedLevel2.get(0) as YXmlElement
expect(clonedLevel3.getAttribute('data-level')).toBe('3')
const clonedText = clonedLevel3.get(0) as YXmlText
expect(clonedText.toString()).toBe('Deep text')
})
it('should handle empty element', () => {
const elem = new YXmlElement('div')
const clone = yXmlElementClone(elem)
expect(clone.nodeName).toBe('div')
expect(clone.length).toBe(0)
})
it('should clone all attribute types', () => {
const elem = new YXmlElement('div')
elem.setAttribute('string', 'text')
elem.setAttribute('number', 42 as any)
elem.setAttribute('boolean', true as any)
elem.setAttribute('null', null as any)
const clone = yXmlElementClone(elem)
expect(clone.getAttribute('string')).toBe('text')
expect(clone.getAttribute('number')).toBe(42)
expect(clone.getAttribute('boolean')).toBe(true)
expect(clone.getAttribute('null')).toBe(null)
})
it('should clone mixed content (elements and text)', () => {
const elem = new YXmlElement('p')
const text1 = new YXmlText('Start ')
const span = new YXmlElement('span')
span.setAttribute('class', 'bold')
const spanText = new YXmlText('bold')
span.insert(0, [spanText])
const text2 = new YXmlText(' end')
elem.insert(0, [text1, span, text2])
const clone = yXmlElementClone(elem)
expect(clone.length).toBe(3)
const clonedText1 = clone.get(0) as YXmlText
expect(clonedText1.toString()).toBe('Start ')
const clonedSpan = clone.get(1) as YXmlElement
expect(clonedSpan.nodeName).toBe('span')
expect(clonedSpan.getAttribute('class')).toBe('bold')
const clonedText2 = clone.get(2) as YXmlText
expect(clonedText2.toString()).toBe(' end')
})
})
})
+3 -3
View File
@@ -47,9 +47,9 @@
"fast-equals": "^5.2.2",
"uuid": "^8.3.2"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+3 -3
View File
@@ -40,9 +40,9 @@
"@hcengineering/server-core": "workspace:0.7.0",
"@hcengineering/server-token": "^0.7.0"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+1 -1
View File
@@ -60,7 +60,7 @@
"@elastic/elasticsearch": "^7.17.14",
"@hcengineering/analytics": "^0.7.3"
},
"repository": "https://github.com/hcengineering/platform",
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
}
+3 -3
View File
@@ -40,9 +40,9 @@
"@hcengineering/storage": "^0.7.3",
"kafkajs": "^2.2.4"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+3 -3
View File
@@ -41,9 +41,9 @@
"@hcengineering/analytics": "^0.7.3",
"fast-equals": "^5.2.2"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+3 -3
View File
@@ -39,9 +39,9 @@
"@hcengineering/server-core": "workspace:0.7.0",
"minio": "^8.0.5"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+3 -3
View File
@@ -40,9 +40,9 @@
"mongodb": "^6.16.0",
"bson": "^6.10.3"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+3 -3
View File
@@ -40,9 +40,9 @@
"@hcengineering/server-core": "workspace:0.7.0",
"@hcengineering/postgres-base": "^0.7.6"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+3 -3
View File
@@ -43,9 +43,9 @@
"@aws-sdk/lib-storage": "^3.738.0",
"@smithy/node-http-handler": "^4.0.2"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",
+3 -3
View File
@@ -65,8 +65,8 @@
"@hcengineering/analytics": "^0.7.3",
"@hcengineering/server-token": "^0.7.0"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
}
}
+3 -3
View File
@@ -44,9 +44,9 @@
"@hcengineering/server-token": "^0.7.0",
"utf-8-validate": "^6.0.4"
},
"repository": {
"type": "git",
"url": "https://github.com/hcengineering/huly.server"
"repository": "https://github.com/hcengineering/huly.server",
"publishConfig": {
"access": "public"
},
"files": [
"lib/**/*",