From 21d03818c6fd07445197dea04fc4cb1806716ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Tue, 13 May 2025 10:53:04 +0200 Subject: [PATCH 1/2] Add the possibility to filter with an ObjectID a stream publication --- lib/publish.js | 13 ++++++++-- lib/utils/server.js | 4 ++- tests.js | 59 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/lib/publish.js b/lib/publish.js index a1acd33..6640a79 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -5,7 +5,16 @@ import { check } from 'meteor/check'; import { Cache } from './cache'; import { config } from './config'; import { actions } from './mongo'; -import { getCursor, formatId, convertFilter, buildProjection, removeValue, overrideLowLevelPublishAPI, mergeDocIntoFetchResult } from './utils/server'; +import { + getCursor, + formatId, + convertFilter, + convertObjectId, + buildProjection, + removeValue, + overrideLowLevelPublishAPI, + mergeDocIntoFetchResult, +} from './utils/server'; import { isEmpty, createKey } from './utils/shared'; let onces = []; @@ -127,7 +136,7 @@ function publishOnce(name, handler) { // change streams const formatDoc = streamDoc => { const _id = formatId(streamDoc.documentKey._id); - const doc = streamDoc.fullDocument || {}; + const doc = convertObjectId(streamDoc.fullDocument) || {}; doc._id = _id; if (streamDoc.operationType !== 'update') { diff --git a/lib/utils/server.js b/lib/utils/server.js index f92fda6..76a0e2f 100644 --- a/lib/utils/server.js +++ b/lib/utils/server.js @@ -144,7 +144,9 @@ export const convertFilter = filter => { if (key === '$or' || key === '$and' || key === '$nor') { result[key] = convertFilter(filter[key]); } else { - result[`fullDocument.${key}`] = filter[key]; + result[`fullDocument.${key}`] = filter[key]._str + ? new ObjectId(filter[key]._str) + : filter[key]; } } return result; diff --git a/tests.js b/tests.js index 42943d8..c6ebf89 100644 --- a/tests.js +++ b/tests.js @@ -27,6 +27,9 @@ const Notes = new Mongo.Collection('notes'); const Items = new Mongo.Collection('items'); const Books = new Mongo.Collection('books'); const Dogs = new Mongo.Collection('dogs'); +const Cats = new Mongo.Collection('cats', { + idGeneration: 'MONGO' // Mongo.ObjectID +}); const Markers = new Mongo.Collection('markers', { idGeneration: 'MONGO' // Mongo.ObjectID }); @@ -72,6 +75,17 @@ const resetDogs = async () => { return; } +const resetCats = async () => { + await Cats.removeAsync({}); + await Cats.insertAsync({ name: 'fluffy', something: new Mongo.ObjectID('123456789012345678901234') }); + await Cats.insertAsync({ name: 'Phantom', something: new Mongo.ObjectID('123456789012345678901234') }); + await Cats.insertAsync({ + name: 'Mittens', + something: new Mongo.ObjectID('012341234567890123456789'), + }); + return; +} + const insertThing = async ({ text, num }) => { return Things.insertAsync({ text, num }); } @@ -148,6 +162,10 @@ const insertDog = async ({ text }) => { return Dogs.insertAsync({ text, ...(Meteor.isServer && { something: 1 }) }); } +const insertCat = async ({ name, id }) => { + return Cats.insertAsync({ name, something: id }); +} + const updateDog = async ({ _id, text }) => { return Dogs.updateAsync({ _id }, { $set: { text, ...(Meteor.isServer && { something: 2 }) }}); } @@ -168,6 +186,7 @@ if (Meteor.isServer) { await resetBooks(); await resetMarkers(); await resetDogs(); + await resetCats(); }) Meteor.publish('notes.all', function() { @@ -218,11 +237,15 @@ if (Meteor.isServer) { return Dogs.find({}, { fields: { text: 1 }}); }); - Meteor.methods({ reset, resetNotes, resetItems, resetBooks, resetMarkers, resetDogs, updateThing, updateThingWithUnset, updateThings, updateThingsWithUnset, updateThingUpsert, updateThingUpsertMulti, upsertThing, replaceThing, removeThing, fetchThings, updateItem, fetchItems }) + Meteor.publish.stream('cats.stream.something', function(filterObjectId) { + return Cats.find({something: filterObjectId}, { fields: { name: 1, something: 1 }}); + }); + + Meteor.methods({ reset, resetNotes, resetItems, resetBooks, resetMarkers, resetDogs, resetCats, updateThing, updateThingWithUnset, updateThings, updateThingsWithUnset, updateThingUpsert, updateThingUpsertMulti, upsertThing, replaceThing, removeThing, fetchThings, updateItem, fetchItems }) } // isomorphic methods -Meteor.methods({ insertThing, insertItem, insertBook, insertMarker, updateMarker, updateMarkers, insertDog, updateDog, replaceDog, removeDog }); +Meteor.methods({ insertThing, insertItem, insertBook, insertMarker, updateMarker, updateMarkers, insertDog, updateDog, insertCat, replaceDog, removeDog }); function createConnection() { @@ -1069,6 +1092,38 @@ if (Meteor.isClient) { computation.stop(); }); + Tinytest.addAsync( + 'subscribe - .stream - successful with Mongo.ObjectID as a filter', + async (test) => { + await Meteor.callAsync('resetCats'); + + let sub; + Tracker.autorun(() => { + sub = Meteor.subscribe('cats.stream.something', new Mongo.ObjectID('123456789012345678901234'), { cacheDuration: 0.1 }); + }); + + let cats; + const computation = Tracker.autorun(() => { + if (sub.ready()) { + cats = Cats.find().fetch(); + sub.stop(); + } + }); + + + + await wait(101); + console.log('cats', cats); + test.equal(cats.length, 2); + + await Meteor.callAsync('insertCat', { name: 'sup', id: new Mongo.ObjectID('123456789012345678901234') }); + await wait(100); + test.equal(cats.length, 3); + + computation.stop(); + } + ); + Tinytest.addAsync('cache - regular pubsub - successful', async (test) => { let sub; From 7cb33ff5fc56d14f60e188e30f07e5cd733e2786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Fri, 19 Sep 2025 09:26:37 +0200 Subject: [PATCH 2/2] remove usage of isSubsetOf that it is not widely supported. tests provided --- lib/publish.js | 13 +--- lib/utils/client.js | 10 ++- lib/utils/server.js | 4 +- tests.js | 181 ++++++++++++++++++++++++++++++-------------- 4 files changed, 134 insertions(+), 74 deletions(-) diff --git a/lib/publish.js b/lib/publish.js index 6640a79..a1acd33 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -5,16 +5,7 @@ import { check } from 'meteor/check'; import { Cache } from './cache'; import { config } from './config'; import { actions } from './mongo'; -import { - getCursor, - formatId, - convertFilter, - convertObjectId, - buildProjection, - removeValue, - overrideLowLevelPublishAPI, - mergeDocIntoFetchResult, -} from './utils/server'; +import { getCursor, formatId, convertFilter, buildProjection, removeValue, overrideLowLevelPublishAPI, mergeDocIntoFetchResult } from './utils/server'; import { isEmpty, createKey } from './utils/shared'; let onces = []; @@ -136,7 +127,7 @@ function publishOnce(name, handler) { // change streams const formatDoc = streamDoc => { const _id = formatId(streamDoc.documentKey._id); - const doc = convertObjectId(streamDoc.fullDocument) || {}; + const doc = streamDoc.fullDocument || {}; doc._id = _id; if (streamDoc.operationType !== 'update') { diff --git a/lib/utils/client.js b/lib/utils/client.js index 70bc4a1..14e022f 100644 --- a/lib/utils/client.js +++ b/lib/utils/client.js @@ -38,7 +38,13 @@ export const isEqual = (first, second) => { // Check for Set if (first instanceof Set && second instanceof Set) { - return first.size === second.size && first.isSubsetOf(second); + if (first.size !== second.size) return false; + + // Check if all elements in first are also in second + for (const item of first) { + if (!second.has(item)) return false; + } + return true; } if (typeof first === 'object') { @@ -52,7 +58,7 @@ export const isEqual = (first, second) => { const keys = Object.keys(first); if (keys.length !== Object.keys(second).length) return false; - return keys.every(key => isEqual(first[key], second[key])); + return keys.every((key) => isEqual(first[key], second[key])); } return false; diff --git a/lib/utils/server.js b/lib/utils/server.js index 76a0e2f..f92fda6 100644 --- a/lib/utils/server.js +++ b/lib/utils/server.js @@ -144,9 +144,7 @@ export const convertFilter = filter => { if (key === '$or' || key === '$and' || key === '$nor') { result[key] = convertFilter(filter[key]); } else { - result[`fullDocument.${key}`] = filter[key]._str - ? new ObjectId(filter[key]._str) - : filter[key]; + result[`fullDocument.${key}`] = filter[key]; } } return result; diff --git a/tests.js b/tests.js index 6af63e8..b5a7b81 100644 --- a/tests.js +++ b/tests.js @@ -4,7 +4,7 @@ import { Mongo, MongoInternals } from 'meteor/mongo'; import { MongoID } from 'meteor/mongo-id'; import { DDP } from 'meteor/ddp-client'; import { Tracker } from 'meteor/tracker'; -import { merge, extractSubscribeArguments } from './lib/utils/client'; +import { merge, extractSubscribeArguments, isEqual } from './lib/utils/client'; import { convertFilter, removeValue, trim, matchesFilter, convertObjectId, createProjection } from './lib/utils/server'; import { createKey } from './lib/utils/shared'; import { subsCache } from './lib/subs-cache'; @@ -28,9 +28,6 @@ const Notes = new Mongo.Collection('notes'); const Items = new Mongo.Collection('items'); const Books = new Mongo.Collection('books'); const Dogs = new Mongo.Collection('dogs'); -const Cats = new Mongo.Collection('cats', { - idGeneration: 'MONGO' // Mongo.ObjectID -}); const Markers = new Mongo.Collection('markers', { idGeneration: 'MONGO' // Mongo.ObjectID }); @@ -76,17 +73,6 @@ const resetDogs = async () => { return; } -const resetCats = async () => { - await Cats.removeAsync({}); - await Cats.insertAsync({ name: 'fluffy', something: new Mongo.ObjectID('123456789012345678901234') }); - await Cats.insertAsync({ name: 'Phantom', something: new Mongo.ObjectID('123456789012345678901234') }); - await Cats.insertAsync({ - name: 'Mittens', - something: new Mongo.ObjectID('012341234567890123456789'), - }); - return; -} - const insertThing = async ({ text, num }) => { return Things.insertAsync({ text, num }); } @@ -163,10 +149,6 @@ const insertDog = async ({ text }) => { return Dogs.insertAsync({ text, ...(Meteor.isServer && { something: 1 }) }); } -const insertCat = async ({ name, id }) => { - return Cats.insertAsync({ name, something: id }); -} - const updateDog = async ({ _id, text }) => { return Dogs.updateAsync({ _id }, { $set: { text, ...(Meteor.isServer && { something: 2 }) }}); } @@ -187,7 +169,6 @@ if (Meteor.isServer) { await resetBooks(); await resetMarkers(); await resetDogs(); - await resetCats(); }) Meteor.publish('notes.all', function() { @@ -238,15 +219,11 @@ if (Meteor.isServer) { return Dogs.find({}, { fields: { text: 1 }}); }); - Meteor.publish.stream('cats.stream.something', function(filterObjectId) { - return Cats.find({something: filterObjectId}, { fields: { name: 1, something: 1 }}); - }); - - Meteor.methods({ reset, resetNotes, resetItems, resetBooks, resetMarkers, resetDogs, resetCats, updateThing, updateThingWithUnset, updateThings, updateThingsWithUnset, updateThingUpsert, updateThingUpsertMulti, upsertThing, replaceThing, removeThing, fetchThings, updateItem, fetchItems }) + Meteor.methods({ reset, resetNotes, resetItems, resetBooks, resetMarkers, resetDogs, updateThing, updateThingWithUnset, updateThings, updateThingsWithUnset, updateThingUpsert, updateThingUpsertMulti, upsertThing, replaceThing, removeThing, fetchThings, updateItem, fetchItems }) } // isomorphic methods -Meteor.methods({ insertThing, insertItem, insertBook, insertMarker, updateMarker, updateMarkers, insertDog, updateDog, insertCat, replaceDog, removeDog }); +Meteor.methods({ insertThing, insertItem, insertBook, insertMarker, updateMarker, updateMarkers, insertDog, updateDog, replaceDog, removeDog }); function createConnection() { @@ -1093,38 +1070,6 @@ if (Meteor.isClient) { computation.stop(); }); - Tinytest.addAsync( - 'subscribe - .stream - successful with Mongo.ObjectID as a filter', - async (test) => { - await Meteor.callAsync('resetCats'); - - let sub; - Tracker.autorun(() => { - sub = Meteor.subscribe('cats.stream.something', new Mongo.ObjectID('123456789012345678901234'), { cacheDuration: 0.1 }); - }); - - let cats; - const computation = Tracker.autorun(() => { - if (sub.ready()) { - cats = Cats.find().fetch(); - sub.stop(); - } - }); - - - - await wait(101); - console.log('cats', cats); - test.equal(cats.length, 2); - - await Meteor.callAsync('insertCat', { name: 'sup', id: new Mongo.ObjectID('123456789012345678901234') }); - await wait(100); - test.equal(cats.length, 3); - - computation.stop(); - } - ); - Tinytest.addAsync('cache - regular pubsub - successful', async (test) => { let sub; @@ -2124,6 +2069,126 @@ if (Meteor.isServer) { test.equal(_isEqual(lodashMerged, merged), true); }) + + // Tests for isEqual function with Set compatibility + Tinytest.add('isEqual - Set equality - identical sets', function (test) { + const set1 = new Set([1, 2, 3]); + const set2 = new Set([1, 2, 3]); + test.isTrue(isEqual(set1, set2), 'Identical sets should be equal'); + }); + + Tinytest.add('isEqual - Set equality - same elements different order', function (test) { + const set1 = new Set([1, 2, 3]); + const set2 = new Set([3, 1, 2]); + test.isTrue( + isEqual(set1, set2), + 'Sets with same elements in different order should be equal' + ); + } + ); + + Tinytest.add('isEqual - Set inequality - different sizes', function (test) { + const set1 = new Set([1, 2, 3]); + const set2 = new Set([1, 2]); + test.isFalse( + isEqual(set1, set2), + 'Sets with different sizes should not be equal' + ); + }); + + Tinytest.add('isEqual - Set inequality - different elements', function (test) { + const set1 = new Set([1, 2, 3]); + const set2 = new Set([1, 2, 4]); + test.isFalse( + isEqual(set1, set2), + 'Sets with different elements should not be equal' + ); + } + ); + + Tinytest.add('isEqual - Set equality - empty sets', function (test) { + const set1 = new Set(); + const set2 = new Set(); + test.isTrue(isEqual(set1, set2), 'Empty sets should be equal'); + }); + + Tinytest.add('isEqual - Set inequality - one empty', function (test) { + const set1 = new Set(); + const set2 = new Set([1, 2, 3]); + test.isFalse( + isEqual(set1, set2), + 'Empty set should not equal non-empty set' + ); + }); + + Tinytest.add('isEqual - Set equality - with strings', function (test) { + const set1 = new Set(['a', 'b', 'c']); + const set2 = new Set(['c', 'a', 'b']); + test.isTrue( + isEqual(set1, set2), + 'Sets with same string elements should be equal' + ); + }); + + Tinytest.add('isEqual - Set equality - with objects', function (test) { + const obj1 = { id: 1, name: 'test' }; + const obj2 = { id: 2, name: 'test2' }; + const set1 = new Set([obj1, obj2]); + const set2 = new Set([obj2, obj1]); + test.isTrue( + isEqual(set1, set2), + 'Sets with same object elements should be equal' + ); + }); + + Tinytest.add('isEqual - Set inequality - with objects', function (test) { + const obj1 = { id: 1, name: 'test' }; + const obj2 = { id: 2, name: 'test2' }; + const obj3 = { id: 3, name: 'test3' }; + const set1 = new Set([obj1, obj2]); + const set2 = new Set([obj1, obj3]); + test.isFalse( + isEqual(set1, set2), + 'Sets with different object elements should not be equal' + ); + }); + + Tinytest.add('isEqual - Set vs non-Set', function (test) { + const set1 = new Set([1, 2, 3]); + const array1 = [1, 2, 3]; + test.isFalse(isEqual(set1, array1), 'Set should not equal array'); + }); + + Tinytest.add('isEqual - non-Set vs Set', function (test) { + const array1 = [1, 2, 3]; + const set1 = new Set([1, 2, 3]); + test.isFalse(isEqual(array1, set1), 'Array should not equal Set'); + }); + + Tinytest.add('isEqual - Set with duplicate values', function (test) { + // Note: Sets don't have duplicates, but testing edge case + const set1 = new Set([1, 2, 3]); + const set2 = new Set([1, 2, 3, 3]); // This will be the same as [1, 2, 3] + test.isTrue( + isEqual(set1, set2), + 'Sets should handle duplicate values correctly' + ); + }); + + Tinytest.add('isEqual - Set with undefined and null', function (test) { + const set1 = new Set([undefined, null, 0]); + const set2 = new Set([null, undefined, 0]); + test.isTrue( + isEqual(set1, set2), + 'Sets with undefined and null should be equal' + ); + }); + + Tinytest.add('isEqual - Set with NaN', function (test) { + const set1 = new Set([NaN, 1, 2]); + const set2 = new Set([1, NaN, 2]); + test.isTrue(isEqual(set1, set2), 'Sets with NaN should be equal'); + }); } Tinytest.addAsync('subscribe - .once - current user is not removed', async (test) => {