UNPKG

@shaxpir/sharedb-storage-sqlite

Version:

Shared SQLite storage components for ShareDB adapters

538 lines (461 loc) 15.5 kB
/** * Compound ID Handling Tests * Testing that compound IDs are properly handled - stored with full ID in documents * but with simple ID in inventory */ const expect = require('chai').expect; const TestDbHelper = require('./helpers/test-db-helper'); const CollectionPerTableStrategy = require('../lib/schema/collection-per-table-strategy'); const AttachedCollectionPerTableStrategy = require('../lib/schema/attached-collection-per-table-strategy'); describe('Compound ID Handling', function() { let helper; let db; let strategy; beforeEach(async function() { helper = new TestDbHelper('compound-id-test'); db = await helper.createAdapter(); }); afterEach(async function() { await helper.cleanup(); }); after(function() { TestDbHelper.cleanupAll(); }); describe('CollectionPerTableStrategy', function() { it('should store simple ID in inventory when document has compound ID', async function() { const config = { collectionConfig: { manifest: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Write a document with compound ID (as ShareDB does) const doc = { id: 'manifest/m3ttEidoeclNAhlT', payload: { collection: 'manifest', v: 1, type: 'json0', data: { type: 'manifest', content: 'test manifest data' } } }; await new Promise((resolve, reject) => { strategy.writeRecords(db, { docs: [doc] }, (err) => { if (err) return reject(err); resolve(); }); }); // Check inventory - should have simple ID const inventoryRows = await db.getAllAsync( 'SELECT * FROM sharedb_inventory WHERE collection = ?', ['manifest'] ); expect(inventoryRows).to.have.lengthOf(1); expect(inventoryRows[0].doc_id).to.equal('m3ttEidoeclNAhlT'); // Simple ID, not compound // Document table should have simple ID const docRows = await db.getAllAsync( 'SELECT * FROM manifest WHERE id = ?', ['m3ttEidoeclNAhlT'] ); expect(docRows).to.have.lengthOf(1); const storedDoc = JSON.parse(docRows[0].data); // The data still contains the compound ID expect(storedDoc.id).to.equal('manifest/m3ttEidoeclNAhlT'); }); it('should reject documents with slashes in document ID', async function() { const config = { collectionConfig: { items: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Try to write a document with slashes in the document ID part const doc = { id: 'items/category/subcategory/item123', payload: { collection: 'items', v: 1, type: 'json0', data: { name: 'Invalid Item' } } }; // Should throw an error because document ID contains slashes let errorThrown = false; try { await new Promise((resolve, reject) => { strategy.writeRecords(db, { docs: [doc] }, (err) => { if (err) reject(err); else resolve(); }); }); } catch (error) { errorThrown = true; expect(error.message).to.include('Document ID part of compound key cannot contain slashes'); expect(error.message).to.include('category/subcategory/item123'); } expect(errorThrown).to.be.true; }); it('should update inventory correctly through updateInventory method', async function() { const config = { collectionConfig: { users: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Call updateInventoryItem directly (as ShareDB would) await new Promise((resolve, reject) => { strategy.updateInventoryItem(db, 'users', 'user123', 5, 'add', (err) => { if (err) return reject(err); resolve(); }); }); // Check inventory - should have simple ID const inventoryRows = await db.getAllAsync( 'SELECT * FROM sharedb_inventory WHERE collection = ? AND doc_id = ?', ['users', 'user123'] ); expect(inventoryRows).to.have.lengthOf(1); expect(inventoryRows[0].doc_id).to.equal('user123'); expect(inventoryRows[0].version_num).to.equal(5); }); it('should not create duplicate inventory entries', async function() { const config = { collectionConfig: { docs: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Write document with compound ID const doc = { id: 'docs/doc1', payload: { collection: 'docs', v: 1, type: 'json0', data: { content: 'test' } } }; await new Promise((resolve, reject) => { strategy.writeRecords(db, { docs: [doc] }, (err) => { if (err) return reject(err); resolve(); }); }); // Update inventory separately (simulating ShareDB behavior) await new Promise((resolve, reject) => { strategy.updateInventoryItem(db, 'docs', 'doc1', 2, 'add', (err) => { if (err) return reject(err); resolve(); }); }); // Check there's only one inventory entry const inventoryRows = await db.getAllAsync( 'SELECT * FROM sharedb_inventory WHERE collection = ?', ['docs'] ); expect(inventoryRows).to.have.lengthOf(1); expect(inventoryRows[0].doc_id).to.equal('doc1'); // Version should be updated to 2 from updateInventoryItem call expect(inventoryRows[0].version_num).to.equal(2); }); it('should delete with simple ID from inventory', async function() { const config = { collectionConfig: { posts: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Write a document const doc = { id: 'posts/post1', payload: { collection: 'posts', v: 1, type: 'json0', data: { title: 'Test Post' } } }; await new Promise((resolve, reject) => { strategy.writeRecords(db, { docs: [doc] }, (err) => { if (err) return reject(err); resolve(); }); }); // Verify document exists in inventory let inventoryRows = await db.getAllAsync( 'SELECT * FROM sharedb_inventory WHERE collection = ? AND doc_id = ?', ['posts', 'post1'] ); expect(inventoryRows).to.have.lengthOf(1); // Delete the document await new Promise((resolve, reject) => { strategy.deleteRecord(db, 'doc', 'posts', 'post1', (err) => { if (err) return reject(err); resolve(); }); }); // Verify document is removed from inventory inventoryRows = await db.getAllAsync( 'SELECT * FROM sharedb_inventory WHERE collection = ? AND doc_id = ?', ['posts', 'post1'] ); expect(inventoryRows).to.have.lengthOf(0); // Verify document is removed from collection table const docRows = await db.getAllAsync( 'SELECT * FROM posts WHERE id = ?', ['post1'] // Simple ID in table ); expect(docRows).to.have.lengthOf(0); }); }); describe('AttachedCollectionPerTableStrategy', function() { it('should inherit compound ID handling from CollectionPerTableStrategy', async function() { const config = { attachmentAlias: 'sharedb', collectionConfig: { sessions: { indexes: [], encryptedFields: [] } } }; strategy = new AttachedCollectionPerTableStrategy(config); // Remove alias for initialization (as preInitializeDatabase would) strategy.attachmentAlias = null; await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Write a document with compound ID const doc = { id: 'sessions/session123', payload: { collection: 'sessions', v: 1, type: 'json0', data: { startTime: Date.now() } } }; await new Promise((resolve, reject) => { strategy.writeRecords(db, { docs: [doc] }, (err) => { if (err) return reject(err); resolve(); }); }); // Check inventory - should have simple ID const inventoryRows = await db.getAllAsync( 'SELECT * FROM sharedb_inventory WHERE collection = ?', ['sessions'] ); expect(inventoryRows).to.have.lengthOf(1); expect(inventoryRows[0].doc_id).to.equal('session123'); // Simple ID, not compound }); }); describe('Guard Against Compound IDs in Inventory', function() { it('should throw error when trying to directly insert compound ID in inventory via updateInventoryForRecord', async function() { const config = { collectionConfig: { test: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Try to directly insert a compound ID into inventory let errorThrown = false; try { await strategy.updateInventoryForRecord(db, 'test', 'test/doc123', 1, false); } catch (error) { errorThrown = true; expect(error.message).to.include('Document ID cannot contain slashes'); expect(error.message).to.include('test/doc123'); } expect(errorThrown).to.be.true; }); it('should throw error when trying to update inventory with compound ID via updateInventoryItem', async function() { const config = { collectionConfig: { items: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Try to update inventory with compound ID const result = await new Promise((resolve) => { strategy.updateInventoryItem(db, 'items', 'items/item456', 1, 'add', (err) => { resolve(err); }); }); expect(result).to.exist; expect(result.message).to.include('Document ID cannot contain slashes'); expect(result.message).to.include('items/item456'); }); it('should throw error in AttachedCollectionPerTableStrategy when compound ID is used', async function() { const config = { attachmentAlias: 'sharedb', collectionConfig: { data: { indexes: [], encryptedFields: [] } } }; const attachedStrategy = new AttachedCollectionPerTableStrategy(config); // Remove alias for initialization attachedStrategy.attachmentAlias = null; await new Promise((resolve, reject) => { attachedStrategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Try to insert compound ID let errorThrown = false; try { await attachedStrategy.updateInventoryForRecord(db, 'data', 'data/record789', 1, false); } catch (error) { errorThrown = true; expect(error.message).to.include('Document ID cannot contain slashes'); expect(error.message).to.include('data/record789'); } expect(errorThrown).to.be.true; }); it('should reject collection names with slashes', async function() { const config = { collectionConfig: { test: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Try to use a collection name with slashes let errorThrown = false; try { await strategy.updateInventoryForRecord(db, 'bad/collection', 'doc123', 1, false); } catch (error) { errorThrown = true; expect(error.message).to.include('Collection name cannot contain slashes'); expect(error.message).to.include('bad/collection'); } expect(errorThrown).to.be.true; }); it('should prevent accidental storage of compound IDs through writeRecords', async function() { const config = { collectionConfig: { protected: { indexes: [], encryptedFields: [] } } }; strategy = new CollectionPerTableStrategy(config); await new Promise((resolve, reject) => { strategy.initializeSchema(db, (err) => { if (err) return reject(err); resolve(); }); }); // Write a document - the compound ID should be stripped automatically const doc = { id: 'protected/doc999', payload: { collection: 'protected', v: 1, type: 'json0', data: { content: 'test' } } }; await new Promise((resolve, reject) => { strategy.writeRecords(db, { docs: [doc] }, (err) => { if (err) return reject(err); resolve(); }); }); // Verify inventory has simple ID, not compound const inventory = await db.getAllAsync( 'SELECT * FROM sharedb_inventory WHERE collection = ?', ['protected'] ); expect(inventory).to.have.lengthOf(1); expect(inventory[0].doc_id).to.equal('doc999'); // Simple ID expect(inventory[0].doc_id).to.not.include('/'); // No slash in inventory ID }); }); });