fix(storage): neutral queryConstraints honored by both adapters
Bug: server adapter accepted queryConstraints (orderBy/limit) but ignored them.
Combat log [orderBy('timestamp','desc'), limit(500)] returned ALL logs unordered
in server mode — unbounded growth. No test caught it: shared contract tested
subscribeCollection with 0 constraints; the firebase-only queryConstraint test
never touched the server adapter. Parity gap.
Fix (Plan A — neutral shape):
- index.js: orderBy()/limit() now NEUTRAL builders returning {__type}. Removed
SDK orderBy/limit re-export.
- firebase.js: subscribeCollection translates neutral -> SDK query constraints.
- server.js: applyConstraints() helper sorts/limits client-side (backend returns
all). Constraints stored per collection, applied on initial fetch + WS change.
- contract.js: added 3 queryConstraint tests (orderBy desc, limit, no-constraints)
to SHARED contract — both adapters now run identical assertions.
- firebase.contract.test.js: removed now-redundant firebase-only constraint
block (covered by shared contract).
Data impact: ZERO. Constraints are query-time only, never stored. Combat log
docs keep timestamp field. No Firebase data migration needed.
208 tests green.
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
// Firebase adapter contract test.
|
||||
// Runs the SAME storage contract as ws (src/storage/contract.js)
|
||||
// Runs the SAME storage contract as server (src/storage/contract.js)
|
||||
// against createFirebaseStorage. The SDK is mocked via src/__mocks__/firebase/*,
|
||||
// so this proves the adapter translates contract calls -> SDK calls correctly
|
||||
// (path handling, merge semantics, subscribe) without hitting network.
|
||||
// (path handling, merge semantics, subscribe, queryConstraints) without network.
|
||||
//
|
||||
// queryConstraints (orderBy + limit) now tested in shared contract for BOTH
|
||||
// adapters — no separate firebase-only block needed here.
|
||||
|
||||
import { initFirebase, createFirebaseStorage } from '../storage/firebase';
|
||||
import { runStorageContract } from '../storage/contract';
|
||||
import { resetMockDb, MOCK_DB } from '../__mocks__/firebase/_mock-db';
|
||||
import { orderBy, limit } from '../__mocks__/firebase/firestore';
|
||||
import { resetMockDb } from '../__mocks__/firebase/_mock-db';
|
||||
|
||||
// Firebase mock DB is shared global state. Reset before each factory so
|
||||
// contract tests are isolated.
|
||||
@@ -17,39 +19,3 @@ runStorageContract('firebase', () => {
|
||||
if (!ok) throw new Error('initFirebase failed under mocked env');
|
||||
return createFirebaseStorage();
|
||||
});
|
||||
|
||||
// Firebase-only: queryConstraints (orderBy + limit) must flow through
|
||||
// subscribeCollection. main App passes LOG_QUERY = [orderBy('timestamp','desc'),
|
||||
// limit(500)] for the combat log. Adapter forwards them to the SDK; this proves
|
||||
// the whole chain sorts + caps, not just dumps raw.
|
||||
describe('firebase adapter: queryConstraints', () => {
|
||||
let storage;
|
||||
beforeEach(() => {
|
||||
resetMockDb();
|
||||
initFirebase();
|
||||
storage = createFirebaseStorage();
|
||||
});
|
||||
|
||||
test('subscribeCollection honors orderBy desc', async () => {
|
||||
MOCK_DB.set('logs/a', { message: 'A', timestamp: 100 });
|
||||
MOCK_DB.set('logs/b', { message: 'B', timestamp: 300 });
|
||||
MOCK_DB.set('logs/c', { message: 'C', timestamp: 200 });
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
storage.subscribeCollection('logs', resolve, [orderBy('timestamp', 'desc')]);
|
||||
});
|
||||
expect(result.map(d => d.message)).toEqual(['B', 'C', 'A']);
|
||||
});
|
||||
|
||||
test('subscribeCollection honors limit', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
MOCK_DB.set(`logs/l${i}`, { message: `L${i}`, timestamp: i });
|
||||
}
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
storage.subscribeCollection('logs', resolve, [orderBy('timestamp', 'desc'), limit(3)]);
|
||||
});
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map(d => d.message)).toEqual(['L4', 'L3', 'L2']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user