Enhancements of subscription api (#2060)

* Adding Realm.subscriptions() and Realm.unsubscribe()
* Update changelog
* RN support
This commit is contained in:
Kenneth Geisshirt 2018-10-10 12:00:04 +02:00 committed by GitHub
parent d6c1c62a47
commit ffb48ec2ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 188 additions and 35 deletions

View File

@ -1,7 +1,8 @@
x.x.x Release notes (yyyy-MM-dd)
=============================================================
### Enhancements
* None.
* Added `Realm.subscriptions()` to query active query-based sync subscriptions. This method is in beta and might change in future releases. ([#2052](https://github.com/realm/realm-js/issues/2052))
* Added `Realm.unsubscribe()` to unsubscribe by name an active query-based sync subscription. This method is in beta and might change in future releases. ([#2052](https://github.com/realm/realm-js/issues/2052))
### Fixed
* <How to hit and notice issue? what was the impact?> ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?)
@ -15,7 +16,7 @@ x.x.x Release notes (yyyy-MM-dd)
### Internal
* None.
2.19.0 Release notes (2018-10-9)
2.19.0-rc.1 Release notes (2018-10-9)
=============================================================
### Enhancements
* Added `SyncConfig.customQueryBasedSyncIdentifier` to allow customizing the identifier appended to the realm path when opening a query based Realm. This identifier is used to distinguish between query based Realms opened on different devices and by default Realm builds it as a combination of a user's id and a random string, allowing the same user to subscribe to different queries on different devices. In very rare cases, you may want to share query based Realms between devices and specifying the `customQueryBasedSyncIdentifier` allows you to do that.

View File

@ -331,6 +331,22 @@ class Realm {
* @throws {Error} If anything in the provided `config` is invalid.
*/
static deleteFile(config) {}
/**
* Get a list of subscriptions. THIS METHOD IS IN BETA AND MAY CHANGE IN FUTURE VERSIONS.
* @param {string} name - Optional parameter to query for either a specific name or pattern (using
* cards `?` and `*`).
* @throws {Error} If `name` is not a string.
* @returns an array of objects of (`name`, `objectType`, `query`).
*/
subscriptions(name) {}
/**
* Unsubscribe a named subscription. THIS METHOD IS IN BETA AND MAY CHANGE IN FUTURE VERSIONS.
* @param {string} name - The name of the subscription.
* @throws {Error} If `name` is not a string or an empty string.
*/
unsubscribe(name) {}
}
/**
* This describes the different options used to create a {@link Realm} instance.

View File

@ -27,10 +27,11 @@ export default class Subscription {
Object.defineProperties(Subscription.prototype, {
error: { get: getterForProperty('error') },
state: { get: getterForProperty('state') }
state: { get: getterForProperty('state') },
name: { get: getterForProperty('name') }
});
// // Non-mutating methods:
// Non-mutating methods:
createMethods(Subscription.prototype, objectTypes.SUBSCRIPTION, [
'unsubscribe',
'addListener',

View File

@ -54,7 +54,7 @@ function waitForDownloadConfig(config) {
/**
* Finds the permissions associated with a given Role or create them as needed.
*
*
* @param {RealmObject} Container RealmObject holding the permission list.
* @param {List<Realm.Permissions.Permission>} list of permissions.
* @param {string} name of the role to find or create permissions for.
@ -363,6 +363,30 @@ module.exports = function(realmConstructor) {
});
}
const ResultSets = function() {};
ResultSets.schema = Object.freeze({
name: '__ResultSets',
properties: {
name: { type: 'string', indexed: true },
query: 'string',
matches_property: 'string',
status: 'int',
error_message: 'string',
query_parse_counter: 'int'
}
});
const subscriptionSchema = {
'ResultSets': ResultSets
};
if (!realmConstructor.Subscription) {
Object.defineProperty(realmConstructor, 'Subscription', {
value: subscriptionSchema,
configurable: false,
});
}
// Add instance methods to the Realm object that are only applied if Sync is
Object.defineProperties(realmConstructor.prototype, getOwnPropertyDescriptors({
permissions(arg) {
@ -379,6 +403,55 @@ module.exports = function(realmConstructor) {
return classPermissions[0];
}
},
subscriptions(name) {
let all_subscriptions = this.objects('__ResultSets');
if (name) {
if (typeof(name) !== 'string') {
throw new Error(`string expected - got ${typeof(name)}.`);
}
if (name.includes('*') || name.includes('?')) {
all_subscriptions = all_subscriptions.filtered(`name LIKE '${name}'`);
} else {
all_subscriptions = all_subscriptions.filtered(`name == '${name}'`);
}
}
let listOfSubscriptions = [];
for (var subscription of all_subscriptions) {
let matches_property = subscription['matches_property'];
let sub = {
name: subscription['name'],
objectType: matches_property.substr(0, matches_property.length-8), // remove _matches
query: subscription['query'],
}
listOfSubscriptions.push(sub);
}
return listOfSubscriptions;
},
unsubscribe(name) {
if (typeof(name) === 'string') {
if (name !== '') {
let named_subscriptions = this.objects('__ResultSets').filtered(`name == '${name}'`);
if (named_subscriptions.length === 0) {
return;
}
let doCommit = false;
if (!this.isInTransaction) {
this.beginTransaction();
doCommit = true;
}
this.delete(named_subscriptions);
if (doCommit) {
this.commitTransaction();
}
} else {
throw new Error('Non-empty string expected.');
}
} else {
throw new Error(`string expected - got ${typeof(name)}.`);
}
}
}));
}
@ -391,8 +464,8 @@ module.exports = function(realmConstructor) {
* but still need to modify any input config.
*/
_constructor(config) {
// Even though this runs code only available for Sync it requires some serious misconfiguration
// for this to happen
// Even though this runs code only available for Sync it requires some serious misconfiguration
// for this to happen
if (config && config.sync) {
if (!Realm.Sync) {
throw new Error("Realm is not compiled with Sync, but the configuration contains sync features.");
@ -408,6 +481,7 @@ module.exports = function(realmConstructor) {
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Realm);
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Role);
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.User);
addSchemaIfNeeded(config.schema, realmConstructor.Subscription.ResultSets);
}
}
return config;

8
lib/index.d.ts vendored
View File

@ -649,6 +649,12 @@ interface ProgressPromise extends Promise<Realm> {
progress(callback: Realm.Sync.ProgressNotificationCallback): Promise<Realm>
}
interface NamedSubscription {
name: string,
objectType: string,
query: string
}
declare class Realm {
static defaultPath: string;
@ -819,6 +825,8 @@ declare class Realm {
permissions() : Realm.Permissions.Realm;
permissions(objectType: string | Realm.ObjectSchema | Function) : Realm.Permissions.Class;
subscriptions(name?: string): NamedSubscription[];
unsubscribe(name: string): void;
}
declare module 'realm' {

View File

@ -308,7 +308,7 @@ void ResultsClass<T>::subscribe(ContextType ctx, ObjectType this_object, Argumen
}
auto subscription = partial_sync::subscribe(*results, subscription_name);
return_value.set(SubscriptionClass<T>::create_instance(ctx, std::move(subscription)));
return_value.set(SubscriptionClass<T>::create_instance(ctx, std::move(subscription), subscription_name));
}
#endif

View File

@ -688,9 +688,10 @@ void SessionClass<T>::override_server(ContextType ctx, ObjectType this_object, A
template<typename T>
class Subscription : public partial_sync::Subscription {
public:
Subscription(partial_sync::Subscription s) : partial_sync::Subscription(std::move(s)) {}
Subscription(partial_sync::Subscription s, util::Optional<std::string> name) : partial_sync::Subscription(std::move(s)), m_name(name) {}
Subscription(Subscription &&) = default;
util::Optional<std::string> m_name;
std::vector<std::pair<Protected<typename T::Function>, partial_sync::SubscriptionNotificationToken>> m_notification_tokens;
};
@ -712,10 +713,11 @@ public:
std::string const name = "Subscription";
static FunctionType create_constructor(ContextType);
static ObjectType create_instance(ContextType, partial_sync::Subscription);
static ObjectType create_instance(ContextType, partial_sync::Subscription, util::Optional<std::string>);
static void get_state(ContextType, ObjectType, ReturnValue &);
static void get_error(ContextType, ObjectType, ReturnValue &);
static void get_name(ContextType, ObjectType, ReturnValue &);
static void unsubscribe(ContextType, ObjectType, Arguments, ReturnValue &);
static void add_listener(ContextType, ObjectType, Arguments, ReturnValue &);
@ -724,7 +726,8 @@ public:
PropertyMap<T> const properties = {
{"state", {wrap<get_state>, nullptr}},
{"error", {wrap<get_error>, nullptr}}
{"error", {wrap<get_error>, nullptr}},
{"name", {wrap<get_name>, nullptr}},
};
MethodMap<T> const methods = {
@ -736,8 +739,8 @@ public:
};
template<typename T>
typename T::Object SubscriptionClass<T>::create_instance(ContextType ctx, partial_sync::Subscription subscription) {
return create_object<T, SubscriptionClass<T>>(ctx, new Subscription<T>(std::move(subscription)));
typename T::Object SubscriptionClass<T>::create_instance(ContextType ctx, partial_sync::Subscription subscription, util::Optional<std::string> name) {
return create_object<T, SubscriptionClass<T>>(ctx, new Subscription<T>(std::move(subscription), name));
}
template<typename T>
@ -762,6 +765,18 @@ void SubscriptionClass<T>::get_error(ContextType ctx, ObjectType object, ReturnV
}
}
template<typename T>
void SubscriptionClass<T>::get_name(ContextType ctx, ObjectType object, ReturnValue &return_value) {
auto subscription = get_internal<T, SubscriptionClass<T>>(object);
if (subscription->m_name == util::none) {
// FIXME: should we reconstruct the name to match the one stored in __ResultSets?
return_value.set_undefined();
}
else {
return_value.set(subscription->m_name);
}
}
template<typename T>
void SubscriptionClass<T>::unsubscribe(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(0);

@ -1 +1 @@
Subproject commit b0fc2814d9e6061ce5ba1da887aab6cfba4755ca
Subproject commit 85cc4f3fc78fb905060489d6e51c2503d770393b

View File

@ -289,7 +289,7 @@ module.exports = {
let realm = new Realm(config);
TestCase.assertTrue(realm.empty);
TestCase.assertEqual(realm.schema.length, 5);
TestCase.assertEqual(realm.schema.length, 5 + 1); // 5 = see below, 1 = __ResultSets
TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Class').length, 1);
TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Permission').length, 1);
TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Realm').length, 1);
@ -354,7 +354,7 @@ module.exports = {
Realm.deleteFile(config);
// connecting with an empty schema should be possible, permission is added implicitly
Realm.open(user.createConfiguration()).then((realm) => {
return waitForUpload(realm);
return waitForUpload(realm);
}).then((realm) => {
return waitForDownload(realm);
}).then((realm) => {
@ -381,7 +381,7 @@ module.exports = {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
let realmPermissions = realm.permissions();
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>" ]
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>" ]
realm.write(() => {
let permissions = realmPermissions.findOrCreate("foo");
TestCase.assertEqual("foo", permissions.role.name);
@ -393,7 +393,7 @@ module.exports = {
TestCase.assertFalse(permissions.canQuery);
TestCase.assertFalse(permissions.canModifySchema);
TestCase.assertFalse(permissions.canSetPermissions);
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>", "foo" ]
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>", "foo" ]
});
resolve();
});
@ -406,7 +406,7 @@ module.exports = {
realm.write(() => {
realm.create('__Role', {'name':'foo'});
});
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
let realmPermissions = realm.permissions();
realm.write(() => {
@ -419,7 +419,7 @@ module.exports = {
TestCase.assertFalse(permissions.canQuery);
TestCase.assertFalse(permissions.canModifySchema);
TestCase.assertFalse(permissions.canSetPermissions);
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
});
resolve();
});
@ -430,7 +430,7 @@ module.exports = {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
let classPermissions = realm.permissions('__Class');
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:xxx" ]
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:xxx" ]
realm.write(() => {
let permissions = classPermissions.findOrCreate("foo");
TestCase.assertEqual("foo", permissions.role.name);
@ -442,7 +442,7 @@ module.exports = {
TestCase.assertFalse(permissions.canQuery);
TestCase.assertFalse(permissions.canModifySchema);
TestCase.assertFalse(permissions.canSetPermissions);
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
});
resolve();
});

View File

@ -1264,6 +1264,7 @@ module.exports = {
// FIXME: We need to test adding a property also calls the listener
testSchemaUpdatesNewClass: function() {
return new Promise((resolve, reject) => {
let called = false;
let realm1 = new Realm({ _cache: false });
TestCase.assertTrue(realm1.empty);
TestCase.assertEqual(realm1.schema.length, 0); // empty schema
@ -1274,6 +1275,7 @@ module.exports = {
TestCase.assertEqual(schema[0].name, 'TestObject');
TestCase.assertEqual(realm1.schema.length, 1);
TestCase.assertEqual(realm.schema[0].name, 'TestObject');
called = true;
});
const schema = [{
@ -1291,7 +1293,11 @@ module.exports = {
// in real world, a Realm will not be closed just after its
// schema has been updated
setTimeout(() => {
resolve();
if (called) {
resolve();
} else {
reject();
}
}, 1000);
});
},

View File

@ -870,6 +870,12 @@ module.exports = {
Realm.deleteFile(config);
realm = new Realm(config);
let listener_called = false;
let schema_listener = function (realm, msg, newSchema) {
listener_called = true;
};
realm.addListener('schema', schema_listener);
const session = realm.syncSession;
TestCase.assertInstanceOf(session, Realm.Sync.Session);
TestCase.assertEqual(session.user.identity, user.identity);
@ -881,7 +887,7 @@ module.exports = {
var subscription1 = results1.subscribe();
TestCase.assertEqual(subscription1.state, Realm.Sync.SubscriptionState.Creating);
var subscription2 = results2.subscribe();
var subscription2 = results2.subscribe('foobar');
TestCase.assertEqual(subscription2.state, Realm.Sync.SubscriptionState.Creating);
let called1 = false;
@ -893,15 +899,11 @@ module.exports = {
TestCase.assertEqual(collection.length, 1);
TestCase.assertTrue(collection[0].name === 'Lassy 1', "The object is not synced correctly");
results1.removeAllListeners();
subscription1.unsubscribe();
TestCase.assertUndefined(subscription1.name);
called1 = true;
});
} else if (state === Realm.Sync.SubscriptionState.Invalidated) {
subscription1.removeAllListeners();
if (called1 && called2) {
realm.close();
resolve('Done');
}
}
});
@ -911,20 +913,50 @@ module.exports = {
TestCase.assertEqual(collection.length, 1);
TestCase.assertTrue(collection[0].name === 'Lassy 2', "The object is not synced correctly");
results2.removeAllListeners();
subscription2.unsubscribe();
TestCase.assertEqual(subscription2.name, 'foobar');
called2 = true;
});
} else if (state === Realm.Sync.SubscriptionState.Invalidated) {
subscription2.removeAllListeners();
if (called1 && called2) {
realm.close();
resolve('Done');
}
}
});
setTimeout(() => {
reject("listeners never called");
if (called1 && called2 && listener_called) {
let listOfSubscriptions = realm.subscriptions();
TestCase.assertArrayLength(listOfSubscriptions, 2 + 5); // 2 = the two subscriptions, 5 = the permissions classes
TestCase.assertEqual(listOfSubscriptions[0]['name'], '[Dog] name == "Lassy 1" '); // the query is the default name; notice the trailing whitespace!
TestCase.assertEqual(listOfSubscriptions[0]['query'], 'name == "Lassy 1" '); // notice the trailing whitespace!
TestCase.assertEqual(listOfSubscriptions[0]['objectType'], 'Dog');
TestCase.assertEqual(listOfSubscriptions[1]['name'], 'foobar');
TestCase.assertEqual(listOfSubscriptions[1]['query'], 'name == "Lassy 2" '); // notice the trailing whitespace!
TestCase.assertEqual(listOfSubscriptions[1]['objectType'], 'Dog');
listOfSubscriptions = realm.subscriptions('foobar');
TestCase.assertArrayLength(listOfSubscriptions, 1);
listOfSubscriptions = realm.subscriptions('*bar');
TestCase.assertArrayLength(listOfSubscriptions, 1);
listOfSubscriptions = realm.subscriptions('RABOOF');
TestCase.assertArrayLength(listOfSubscriptions, 0);
subscription1.unsubscribe();
realm.unsubscribe('foobar');
realm.removeAllListeners();
// check if subscriptions have been removed
// subscription1.unsubscribe() requires a server round-trip so it might take a while
setTimeout(() => {
listOfSubscriptions = realm.subscriptions();
TestCase.assertArrayLength(listOfSubscriptions, 5); // the 5 permissions classes
realm.close();
resolve();
}, 10000);
} else {
reject("listeners never called");
}
}, 15000);
});
});