From 885c3039bc4bd75604282a6b6033b1903f8b4e61 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Thu, 9 Oct 2025 18:21:52 +0700 Subject: [PATCH] Fix packages publish --- packages/client/package.json | 6 +- packages/collaboration/package.json | 6 +- .../src/__tests__/storage.test.ts | 533 ++++++++++++++++++ .../collaboration/src/__tests__/ydoc.test.ts | 334 +++++++++++ packages/core/package.json | 6 +- packages/datalake/package.json | 6 +- packages/elastic/package.json | 2 +- packages/kafka/package.json | 6 +- packages/middleware/package.json | 6 +- packages/minio/package.json | 6 +- packages/mongo/package.json | 6 +- packages/postgres/package.json | 6 +- packages/s3/package.json | 6 +- packages/server-storage/package.json | 6 +- packages/server/package.json | 6 +- 15 files changed, 904 insertions(+), 37 deletions(-) create mode 100644 packages/collaboration/src/__tests__/storage.test.ts create mode 100644 packages/collaboration/src/__tests__/ydoc.test.ts diff --git a/packages/client/package.json b/packages/client/package.json index 90e609c3cd..1acfead645 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -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/**/*", diff --git a/packages/collaboration/package.json b/packages/collaboration/package.json index 32f877698a..f82fb8b164 100644 --- a/packages/collaboration/package.json +++ b/packages/collaboration/package.json @@ -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/**/*", diff --git a/packages/collaboration/src/__tests__/storage.test.ts b/packages/collaboration/src/__tests__/storage.test.ts new file mode 100644 index 0000000000..7a898fa142 --- /dev/null +++ b/packages/collaboration/src/__tests__/storage.test.ts @@ -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() + + async stat (ctx: MeasureContext, wsIds: WorkspaceIds, name: string): Promise { + const data = this.storage.get(name) + if (data === undefined) { + return undefined + } + return { + _id: name as Ref, + _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 { + 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> { + const buffer = Buffer.isBuffer(data) ? data : await this.streamToBuffer(data) + this.storage.set(name, { buffer, contentType, size: size ?? buffer.length }) + return name as Ref + } + + async remove (ctx: MeasureContext, wsIds: WorkspaceIds, names: string[]): Promise { + for (const name of names) { + this.storage.delete(name) + } + } + + async partial ( + ctx: MeasureContext, + wsIds: WorkspaceIds, + name: string, + offset: number, + length?: number + ): Promise { + 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 } + ): 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 { + return [] + } + + async listChunksBuckets (ctx: MeasureContext): Promise { + return [] + } + + async make (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + // No-op for mock + } + + async delete (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + // No-op for mock + } + + async exists (ctx: MeasureContext, wsIds: WorkspaceIds): Promise { + return true + } + + private async streamToBuffer (stream: Readable): Promise { + 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 + 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 + 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 + 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 + 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 + 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 + 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) + }) + }) +}) diff --git a/packages/collaboration/src/__tests__/ydoc.test.ts b/packages/collaboration/src/__tests__/ydoc.test.ts new file mode 100644 index 0000000000..9f4dc34320 --- /dev/null +++ b/packages/collaboration/src/__tests__/ydoc.test.ts @@ -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') + }) + }) +}) diff --git a/packages/core/package.json b/packages/core/package.json index 6563e6ff7a..4fe905c277 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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/**/*", diff --git a/packages/datalake/package.json b/packages/datalake/package.json index 96674c9cde..42f1f75cb1 100644 --- a/packages/datalake/package.json +++ b/packages/datalake/package.json @@ -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/**/*", diff --git a/packages/elastic/package.json b/packages/elastic/package.json index 9f1189f08a..b2589c089d 100644 --- a/packages/elastic/package.json +++ b/packages/elastic/package.json @@ -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" } diff --git a/packages/kafka/package.json b/packages/kafka/package.json index 9d0f1534d1..892ff8a8fe 100644 --- a/packages/kafka/package.json +++ b/packages/kafka/package.json @@ -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/**/*", diff --git a/packages/middleware/package.json b/packages/middleware/package.json index 32fcbc2408..0fc65c0839 100644 --- a/packages/middleware/package.json +++ b/packages/middleware/package.json @@ -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/**/*", diff --git a/packages/minio/package.json b/packages/minio/package.json index 60a0c304a7..1b7cef80b8 100644 --- a/packages/minio/package.json +++ b/packages/minio/package.json @@ -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/**/*", diff --git a/packages/mongo/package.json b/packages/mongo/package.json index 0eb249a678..717e641cc7 100644 --- a/packages/mongo/package.json +++ b/packages/mongo/package.json @@ -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/**/*", diff --git a/packages/postgres/package.json b/packages/postgres/package.json index 4344e5219d..d6a34b1317 100644 --- a/packages/postgres/package.json +++ b/packages/postgres/package.json @@ -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/**/*", diff --git a/packages/s3/package.json b/packages/s3/package.json index 1426e7f8ce..d248502a2d 100644 --- a/packages/s3/package.json +++ b/packages/s3/package.json @@ -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/**/*", diff --git a/packages/server-storage/package.json b/packages/server-storage/package.json index 9a64eb1f4b..e59f192a61 100644 --- a/packages/server-storage/package.json +++ b/packages/server-storage/package.json @@ -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" } } diff --git a/packages/server/package.json b/packages/server/package.json index 72667bfce5..6c1e140577 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -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/**/*",