Expose user serialize and deserialize methods (#1996)

* Expose serialize and deserialize methods

* Changelog and docs

* Forgot to save changelog 🤦‍♂️

* Add input validation
This commit is contained in:
Nikola Irinchev 2018-08-28 15:01:32 +02:00 committed by GitHub
parent a10ffae469
commit 477b900530
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 276 additions and 110 deletions

View File

@ -1,3 +1,26 @@
X.Y.Z Release notes
=============================================================
### Compatibility
* Sync protocol: 24
* Server-side history format: 4
* File format: 7
* Realm Object Server: 3.0.0 or later
### Breaking changes
* None.
### Enhancements
* Exposed `User.serialize` to create a persistable representation of a user instance, as well as
`User.deserialize` to later inflate a `User` instance that can be used to connect to Realm Object
Server and open synchronized Realms (#1276).
### Bug fixes
* Removed a false negative warning when using `User.createConfiguration`.
### Internal
* Realm Core v5.7.2.
* Realm Sync v3.9.1.
2.15.3 Release notes (2018-8-24)
=============================================================
### Compatibility

View File

@ -398,6 +398,12 @@ class User {
*/
static adminUser(adminToken, server) {}
/**
* Creates a new sync user instance from the serialized representation.
* @param {object} serialized - the serialized version of the user, obtained by calling {@link User#serialize}.
*/
static deserialize(serialized) {}
/**
* A dictionary containing users that are currently logged in.
* The keys in the dictionary are user identities, values are corresponding User objects.
@ -452,6 +458,14 @@ class User {
*/
createConfiguration(config) {}
/**
* Serializes a user to an object, that can be persisted or passed to another component to create a new instance
* by calling {@link User.deserialize}. The serialized user instance includes the user's refresh token and should
* be treated as sensitive data.
* @returns {object} an object, containing the user identity, server url, and refresh token.
*/
serialize() {}
/**
* Logs out the user from the Realm Object Server.
*/

10
lib/index.d.ts vendored
View File

@ -283,6 +283,13 @@ declare namespace Realm.Sync {
user: UserInfo
}
interface SerializedUser {
server: string;
refreshToken: string;
identity: string;
isAdmin: boolean;
}
/**
* User
* @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.User.html }
@ -323,8 +330,11 @@ declare namespace Realm.Sync {
static confirmEmail(server:string, confirmation_token:string): Promise<void>;
static deserialize(serialized: SerializedUser): Realm.Sync.User;
createConfiguration(config?: Realm.PartialConfiguration): Realm.Configuration
authenticate(server: string, provider: string, options: any): Promise<Realm.Sync.User>;
serialize(): SerializedUser;
logout(): void;
openManagementRealm(): Realm;
retrieveAccount(provider: string, username: string): Promise<Account>;

View File

@ -38,6 +38,25 @@ function checkTypes(args, types) {
}
}
function checkObjectTypes(obj, types) {
for (const name of Object.getOwnPropertyNames(types)) {
const actualType = typeof obj[name];
let targetType = types[name];
const isOptional = targetType[targetType.length - 1] === '?';
if (isOptional) {
targetType = targetType.slice(0, -1);
}
if (!isOptional && actualType === 'undefined') {
throw new Error(`${name} is required, but a value was not provided.`);
}
if (actualType !== targetType) {
throw new TypeError(`${name} must be of type '${targetType}' but was of type '${actualType}' instead.`);
}
}
}
// Perform a HTTP request, enqueuing it if too many requests are already in
// progress to avoid hammering the server.
const performFetch = (function() {
@ -407,6 +426,17 @@ const staticMethods = {
return _authenticate(this, server, json)
},
deserialize(serialized) {
checkObjectTypes(serialized, {
server: 'string',
identity: 'string',
refreshToken: 'string',
isAdmin: 'boolean',
});
return this.createUser(serialized.server, serialized.identity, serialized.refreshToken, false, serialized.isAdmin || false);
},
requestPasswordReset(server, email) {
checkTypes(arguments, ['string', 'string']);
const json = {
@ -478,98 +508,106 @@ const instanceMethods = {
.then(() => console.log('User is logged out'))
.catch((e) => print_error(e));
},
openManagementRealm() {
let url = url_parse(this.server);
if (url.protocol === 'http:') {
url.set('protocol', 'realm:');
} else if (url.protocol === 'https:') {
url.set('protocol', 'realms:');
} else {
throw new Error(`Unexpected user auth url: ${this.server}`);
serialize() {
return {
server: this.server,
refreshToken: this.token,
identity: this.identity,
isAdmin: this.isAdmin,
};
},
openManagementRealm() {
let url = url_parse(this.server);
if (url.protocol === 'http:') {
url.set('protocol', 'realm:');
} else if (url.protocol === 'https:') {
url.set('protocol', 'realms:');
} else {
throw new Error(`Unexpected user auth url: ${this.server}`);
}
url.set('pathname', '/~/__management');
return new this.constructor._realmConstructor({
schema: require('./management-schema'),
sync: {
user: this,
url: url.href
}
url.set('pathname', '/~/__management');
return new this.constructor._realmConstructor({
schema: require('./management-schema'),
sync: {
user: this,
url: url.href
});
},
retrieveAccount(provider, provider_id) {
checkTypes(arguments, ['string', 'string']);
const url = url_parse(this.server);
url.set('pathname', `/auth/users/${provider}/${provider_id}`);
const headers = {
Authorization: this.token
};
const options = {
method: 'GET',
headers,
open_timeout: 5000
};
return performFetch(url.href, options)
.then((response) => {
if (response.status !== 200) {
return response.json()
.then(body => {
throw new AuthError(body);
});
} else {
return response.json();
}
});
},
retrieveAccount(provider, provider_id) {
checkTypes(arguments, ['string', 'string']);
const url = url_parse(this.server);
url.set('pathname', `/auth/users/${provider}/${provider_id}`);
const headers = {
Authorization: this.token
};
const options = {
method: 'GET',
headers,
open_timeout: 5000
};
return performFetch(url.href, options)
.then((response) => {
if (response.status !== 200) {
return response.json()
.then(body => {
throw new AuthError(body);
});
} else {
return response.json();
}
});
},
createConfiguration(config) {
},
createConfiguration(config) {
if (config && config.sync) {
if (console.warn !== undefined) {
console.warn(`'user' property will be overridden by ${this.identity}`);
}
if (config.sync.partial !== undefined && config.sync.fullSynchronization !== undefined) {
throw new Error("'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'");
}
if (config && config.sync) {
if (config.sync.user && console.warn !== undefined) {
console.warn(`'user' property will be overridden by ${this.identity}`);
}
// Create default config
let url = new URL(this.server);
let secure = (url.protocol === 'https:')?'s':'';
let port = (url.port === undefined)?'9080':url.port;
let realmUrl = `realm${secure}://${url.hostname}:${port}/default`;
let defaultConfig = {
sync: {
user: this,
url: realmUrl,
},
schema: [],
};
// Set query-based as the default setting if the user doesn't specified any other behaviour.
if (!(config && config.sync && config.sync.partial)) {
defaultConfig.sync.fullSynchronization = false;
if (config.sync.partial !== undefined && config.sync.fullSynchronization !== undefined) {
throw new Error("'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'");
}
}
// Automatically add Permission classes to the schema if Query-based sync is enabled
if (defaultConfig.sync.fullSynchronization === false || (config && config.sync && config.sync.partial === true)) {
defaultConfig.schema = [
Realm.Permissions.Class,
Realm.Permissions.Permission,
Realm.Permissions.Role,
Realm.Permissions.User,
];
}
// Create default config
let url = new URL(this.server);
let secure = (url.protocol === 'https:')?'s':'';
let port = (url.port === undefined)?'9080':url.port;
let realmUrl = `realm${secure}://${url.hostname}:${port}/default`;
// Merge default configuration with user provided config. User defined properties should aways win.
// Doing the naive merge in JS break objects that are backed by native objects, so these needs to
// be merged manually. This is currently only `sync.user`.
let mergedConfig = (config === undefined) ? defaultConfig : merge(defaultConfig, config);
mergedConfig.sync.user = this;
return mergedConfig;
},
};
let defaultConfig = {
sync: {
user: this,
url: realmUrl,
},
schema: [],
};
// Set query-based as the default setting if the user doesn't specified any other behaviour.
if (!(config && config.sync && config.sync.partial)) {
defaultConfig.sync.fullSynchronization = false;
}
// Automatically add Permission classes to the schema if Query-based sync is enabled
if (defaultConfig.sync.fullSynchronization === false || (config && config.sync && config.sync.partial === true)) {
defaultConfig.schema = [
Realm.Permissions.Class,
Realm.Permissions.Permission,
Realm.Permissions.Role,
Realm.Permissions.User,
];
}
// Merge default configuration with user provided config. User defined properties should aways win.
// Doing the naive merge in JS break objects that are backed by native objects, so these needs to
// be merged manually. This is currently only `sync.user`.
let mergedConfig = (config === undefined) ? defaultConfig : merge(defaultConfig, config);
mergedConfig.sync.user = this;
return mergedConfig;
},
};
// Append the permission apis
Object.assign(instanceMethods, permissionApis);

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "realm",
"version": "2.15.2",
"version": "2.15.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -369,6 +369,87 @@ module.exports = {
})
}, "'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'");
});
},
testSerialize() {
const username = uuid();
return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => {
const serialized = user.serialize();
TestCase.assertFalse(serialized.isAdmin);
TestCase.assertEqual(serialized.identity, user.identity);
TestCase.assertEqual(serialized.server, 'http://localhost:9080');
TestCase.assertEqual(serialized.refreshToken, user.token);
});
},
testDeserialize() {
const username = uuid();
return Realm.Sync.User.register('http://localhost:9080', username, 'password')
.then((user) => {
const userConfig = user.createConfiguration({
schema: [{ name: 'Dog', properties: { name: 'string' } }],
sync: {
url: 'realm://localhost:9080/~/foo',
fullSynchronization: true,
}
});
const realm = new Realm(userConfig);
realm.write(() => {
realm.create('Dog', {
name: 'Doggo'
});
});
const session = realm.syncSession;
return new Promise((resolve, reject) => {
let callback = (transferred, total) => {
if (transferred >= total) {
session.removeProgressNotification(callback);
realm.close();
Realm.deleteFile(userConfig);
resolve(user.serialize());
}
}
session.addProgressNotification('upload', 'forCurrentlyOutstandingWork', callback);
});
}).then((serialized) => {
const deserialized = Realm.Sync.User.deserialize(serialized);
const config = deserialized.createConfiguration({
schema: [{ name: 'Dog', properties: { name: 'string' } }],
sync: {
url: 'realm://localhost:9080/~/foo',
fullSynchronization: true,
}
});
return Realm.open(config);
}).then((realm) => {
const dogs = realm.objects('Dog');
TestCase.assertEqual(dogs.length, 1);
TestCase.assertEqual(dogs[0].name, 'Doggo');
});
},
testDeserializeInvalidInput() {
const dummy = {
server: '123',
identity: '123',
refreshToken: '123',
isAdmin: false,
};
for (const name of Object.getOwnPropertyNames(dummy)) {
const clone = Object.assign({}, dummy);
// Set to invalid type
clone[name] = 123;
TestCase.assertThrowsContaining(() => Realm.Sync.User.deserialize(clone), `${name} must be of type '${typeof dummy[name]}'`);
// Set to undefined
clone[name] = undefined;
TestCase.assertThrowsContaining(() => Realm.Sync.User.deserialize(clone), `${name} is required, but a value was not provided.`);
}
}
/* This test fails because of realm-object-store #243 . We should use 2 users.

View File

@ -15,8 +15,8 @@
"typescript": "^2.5.2"
},
"scripts": {
"check-typescript" : "tsc --types --noEmit --alwaysStrict ./../lib/index.d.ts",
"js-tests" : "jasmine spec/unit_tests.js",
"check-typescript": "tsc --types --noEmit --alwaysStrict ./../lib/index.d.ts",
"js-tests": "jasmine spec/unit_tests.js",
"test": "npm run check-typescript && npm run js-tests"
}
}