diff --git a/test/unit/bug-report.test.ts b/test/unit/bug-report.test.ts index 540a2cdc14e..a525af4fb25 100644 --- a/test/unit/bug-report.test.ts +++ b/test/unit/bug-report.test.ts @@ -9,18 +9,35 @@ * - 'npm run test:browser' so it runs in the browser */ import assert from 'assert'; -import AsyncTestUtil from 'async-test-util'; import config from './config.ts'; -import { - createRxDatabase, - randomToken -} from '../../plugins/core/index.mjs'; -import { - isNode -} from '../../plugins/test-utils/index.mjs'; +import {createRxDatabase, randomToken, addRxPlugin} from '../../plugins/core/index.mjs'; +import {isNode} from '../../plugins/test-utils/index.mjs'; +import {RxDBPipelinePlugin} from '../../plugins/pipeline/index.mjs'; + +interface Creatable { + created_at: number; +} + +interface Updatable extends Creatable { + updated_at: number; +} + +function preInsert(doc: T) { + const now = Math.floor(Date.now() / 1000); + if (!doc.created_at) doc.created_at = now; + if (!doc.updated_at) doc.updated_at = now; + return doc; +} + +function preSave(doc: T) { + doc.updated_at = Math.floor(Date.now() / 1000); + return doc; +} + describe('bug-report.test.js', () => { it('should fail because it reproduces the bug', async function () { + addRxPlugin(RxDBPipelinePlugin); /** * If your test should only run in nodejs or only run in the browser, @@ -38,28 +55,82 @@ describe('bug-report.test.js', () => { } // create a schema - const mySchema = { + const fileSchema = { version: 0, - primaryKey: 'passportId', + primaryKey: 'id', type: 'object', properties: { - passportId: { + id: { type: 'string', - maxLength: 100 + maxLength: 24, }, - firstName: { - type: 'string' + type: { + type: 'string', }, - lastName: { - type: 'string' + name: { + type: 'string', }, - age: { + folderId: { + type: 'string', + ref: 'folders', + maxLength: 24, + }, + folderPath: { + type: 'array', + ref: 'folders', + items: { + type: 'string', + maxLength: 24, + }, + }, + created_at: { type: 'integer', - minimum: 0, - maximum: 150 - } - } - }; + }, + updated_at: { + type: 'integer', + }, + }, + required: ['id', 'type', 'name', 'folderPath', 'created_at', 'updated_at'], + attachments: {}, + } as const; + const folderSchema = { + version: 0, + primaryKey: 'id', + type: 'object', + properties: { + id: { + type: 'string', + maxLength: 24, + }, + name: { + type: 'string', + }, + parentFolder: { + type: 'string', + maxLength: 24, + ref: 'folders', + }, + path: { + type: 'array', + ref: 'folders', + items: { + type: 'string', + maxLength: 24, + }, + }, + fileCount: { + type: 'number', + }, + created_at: { + type: 'integer', + }, + updated_at: { + type: 'integer', + }, + }, + required: ['id', 'name', 'path', 'fileCount', 'created_at', 'updated_at'], + attachments: {}, + } as const; /** * Always generate a random database-name @@ -78,64 +149,134 @@ describe('bug-report.test.js', () => { eventReduce: true, ignoreDuplicate: true }); + // create a collection const collections = await db.addCollections({ - mycollection: { - schema: mySchema - } + files: { + schema: fileSchema, + }, + folders: { + schema: folderSchema, + }, }); - // insert a document - await collections.mycollection.insert({ - passportId: 'foobar', - firstName: 'Bob', - lastName: 'Kelso', - age: 56 - }); + collections.files.preInsert(preInsert, false); + collections.files.preSave(preSave, false); + collections.folders.preInsert(preInsert, false); + collections.folders.preSave(preSave, false); - /** - * to simulate the event-propagation over multiple browser-tabs, - * we create the same database again - */ - const dbInOtherTab = await createRxDatabase({ - name, - storage: config.storage.getStorage(), - eventReduce: true, - ignoreDuplicate: true + const filePathSyncPipeline = await collections.files.addPipeline({ + identifier: 'file-path-sync', + destination: collections.files, + async handler(docs) { + try { + console.log('sycning file paths'); + const parentFolders = Array.from( + new Set( + docs + .map((fileDoc) => fileDoc.folderId) + .filter((folderId): folderId is string => Boolean(folderId)), + ), + ); + const folderPathMap = new Map( + await collections.folders + .find({selector: {id: {$in: parentFolders}}}) + .exec() + .then((folderDocs) => + folderDocs.map((doc) => [doc.id, doc.path.concat([doc.id])] as const), + ), + ); + console.log('got folder paths for files', folderPathMap); + + await Promise.all( + docs + .filter((doc) => { + if (!doc.folderId) return doc.folderPath.length !== 0; + + const docFolderPaths = folderPathMap.get(doc.folderId) ?? []; + return doc.folderPath.some( + (folderId, index) => folderId !== docFolderPaths[index], + ); + }) + .map((doc) => + doc.incrementalPatch({ + folderPath: doc.folderId + ? (folderPathMap.get(doc.folderId) ?? []) + : [], + }), + ), + ); + console.log('updated file paths'); + } catch (e) { + console.error('Error while trying to sync file paths', e); + } + }, }); - // create a collection - const collectionInOtherTab = await dbInOtherTab.addCollections({ - mycollection: { - schema: mySchema - } + + // This code cuases both pipelines to hang + await collections.files.addPipeline({ + identifier: 'file-changed-file-count', + destination: collections.folders, + async handler(docs) { + console.log('waiting on file paths sync'); + // Make sure our file paths are in sync before we do anything + await filePathSyncPipeline.awaitIdle(); + + const foldersToUpdate = new Set(docs.flatMap((doc) => doc.getLatest().folderPath)); + console.log('updating folders with new file count', foldersToUpdate); + await Promise.all( + Array.from(foldersToUpdate).map(async (folderId) => { + const fileCount = await collections.files + .count({ + selector: { + folderPath: {$elemMatch: {$eq: folderId}}, + }, + }) + .exec(); + + await collections.folders + .findOne({selector: {id: folderId}}) + .incrementalPatch({fileCount}); + }), + ); + console.log('file paths updated'); + }, }); - // find the document in the other tab - const myDocument = await collectionInOtherTab.mycollection - .findOne() - .where('firstName') - .eq('Bob') - .exec(); + // insert a document + const rootFolder = await collections.folders.insert({ + id: '696e92a203bdc7b552a6e0af', + name, + parentFolder: undefined, + path: [], + fileCount: 0, + created_at: 0, + updated_at: 0, + }); - /* - * assert things, - * here your tests should fail to show that there is a bug - */ - assert.strictEqual(myDocument.age, 56); + const subFolder = await collections.folders.insert({ + id: '696e92a803bdc7b552a6e0b0', + name, + parentFolder: rootFolder.id, + path: [rootFolder.id], + fileCount: 0, + created_at: 0, + updated_at: 0, + }); + await collections.files.insert({ + id: '696e92ae03bdc7b552a6e0b1', + name: 'myfile.pdf', + type: 'application/pdf', + folderId: subFolder.id, + folderPath: [], + created_at: 0, + updated_at: 0, + }); - // you can also wait for events - const emitted: any[] = []; - const sub = collectionInOtherTab.mycollection - .findOne().$ - .subscribe(doc => { - emitted.push(doc); - }); - await AsyncTestUtil.waitUntil(() => emitted.length === 1); + assert.strictEqual(rootFolder.getLatest().fileCount, 1); + assert.strictEqual(subFolder.getLatest().fileCount, 1); - // clean up afterwards - sub.unsubscribe(); db.close(); - dbInOtherTab.close(); }); });