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:
parent
a10ffae469
commit
477b900530
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -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
|
||||
|
@ -71,7 +94,7 @@
|
|||
* Updated external packages with help from `npm audit`.
|
||||
* Upgraded to Realm Sync v3.9.1 (to match the devtoolset-6 upgrade).
|
||||
* Upgraded to devtoolset-6 on Centos for Linux builds.
|
||||
|
||||
|
||||
|
||||
2.14.2 Release notes (2018-8-8)
|
||||
=============================================================
|
||||
|
@ -380,7 +403,7 @@
|
|||
The feature known as Partial synchronization has been renamed to Query-based synchronization and is now the default mode for synchronized Realms. This has impacted a number of APIs. See below for the details.
|
||||
|
||||
### Deprecated
|
||||
* [Sync] `Realm.Configuration.SyncConfiguration.partial` has been deprecated in favor of `Realm.Configuration.SyncConfiguration.fullSynchronization`.
|
||||
* [Sync] `Realm.Configuration.SyncConfiguration.partial` has been deprecated in favor of `Realm.Configuration.SyncConfiguration.fullSynchronization`.
|
||||
* [Sync] `Realm.automaticSyncConfiguration()` has been deprecated in favor of `Realm.Sync.User.createConfiguration()`.
|
||||
|
||||
### Breaking changes
|
||||
|
|
32
docs/sync.js
32
docs/sync.js
|
@ -344,22 +344,22 @@ class User {
|
|||
/**
|
||||
* Request a password reset email to be sent to a user's email.
|
||||
* This will not throw an exception, even if the email doesn't belong to a Realm Object Server user.
|
||||
*
|
||||
*
|
||||
* This can only be used for users who authenticated with the 'password' provider, and passed a valid email address as a username.
|
||||
*
|
||||
*
|
||||
* @param {string} server - authentication server
|
||||
* @param {string} email - The email that corresponds to the user's username.
|
||||
* @return {Promise<void>} A promise which is resolved when the request has been sent.
|
||||
*/
|
||||
static requestPasswordReset(server, email) {}
|
||||
|
||||
|
||||
/**
|
||||
* Complete the password reset flow by using the reset token sent to the user's email as a one-time authorization token to change the password.
|
||||
*
|
||||
*
|
||||
* By default, Realm Object Server will send a link to the user's email that will redirect to a webpage where they can enter their new password.
|
||||
* If you wish to provide a native UX, you may wish to modify the password authentication provider to use a custom URL with deep linking, so you can
|
||||
* open the app, extract the token, and navigate to a view that allows to change the password within the app.
|
||||
*
|
||||
*
|
||||
* @param {string} server - authentication server
|
||||
* @param {string} reset_token - The token that was sent to the user's email address.
|
||||
* @param {string} new_password - The user's new password.
|
||||
|
@ -370,20 +370,20 @@ class User {
|
|||
/**
|
||||
* Request an email confirmation email to be sent to a user's email.
|
||||
* This will not throw an exception, even if the email doesn't belong to a Realm Object Server user.
|
||||
*
|
||||
*
|
||||
* @param {string} server - authentication server
|
||||
* @param {string} email - The email that corresponds to the user's username.
|
||||
* @return {Promise<void>} A promise which is resolved when the request has been sent.
|
||||
*/
|
||||
static requestEmailConfirmation(server, email) {}
|
||||
|
||||
|
||||
/**
|
||||
* Complete the email confirmation flow by using the confirmation token sent to the user's email as a one-time authorization token to confirm their email.
|
||||
*
|
||||
*
|
||||
* By default, Realm Object Server will send a link to the user's email that will redirect to a webpage where they can enter their new password.
|
||||
* If you wish to provide a native UX, you may wish to modify the password authentication provider to use a custom URL with deep linking, so you can
|
||||
* open the app, extract the token, and navigate to a view that allows to confirm the email within the app.
|
||||
*
|
||||
*
|
||||
* @param {string} server - authentication server
|
||||
* @param {string} confirmation_token - The token that was sent to the user's email address.
|
||||
* @return {Promise<void>} A promise which is resolved when the request has been sent.
|
||||
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 }
|
||||
|
@ -318,13 +325,16 @@ declare namespace Realm.Sync {
|
|||
static requestPasswordReset(server: string, email: string): Promise<void>;
|
||||
|
||||
static completePasswordReset(server:string, reset_token:string, new_password:string): Promise<void>;
|
||||
|
||||
|
||||
static requestEmailConfirmation(server:string, email:string): Promise<void>;
|
||||
|
||||
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>;
|
||||
|
|
|
@ -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() {
|
||||
|
@ -296,7 +315,7 @@ function _updateAccount(userConstructor, server, json) {
|
|||
body,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
return response.json().then((body) => Promise.reject(new AuthError(body)));
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -419,8 +449,8 @@ const staticMethods = {
|
|||
|
||||
completePasswordReset(server, reset_token, new_password) {
|
||||
checkTypes(arguments, ['string', 'string']);
|
||||
const json = {
|
||||
data: {
|
||||
const json = {
|
||||
data: {
|
||||
action: 'complete_reset',
|
||||
token: reset_token,
|
||||
new_password: new_password
|
||||
|
@ -442,8 +472,8 @@ const staticMethods = {
|
|||
|
||||
confirmEmail(server, confirmation_token) {
|
||||
checkTypes(arguments, ['string', 'string']);
|
||||
const json = {
|
||||
data: {
|
||||
const json = {
|
||||
data: {
|
||||
action: 'confirm_email',
|
||||
token: confirmation_token
|
||||
}
|
||||
|
@ -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);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "realm",
|
||||
"version": "2.15.2",
|
||||
"version": "2.15.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -343,18 +343,18 @@ module.exports = {
|
|||
const username = uuid();
|
||||
return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => {
|
||||
TestCase.assertThrowsContaining(() => {
|
||||
let config = {
|
||||
sync: {
|
||||
let config = {
|
||||
sync: {
|
||||
url: 'http://localhost:9080/~/default',
|
||||
partial: true,
|
||||
fullSynchronization: false
|
||||
}
|
||||
partial: true,
|
||||
fullSynchronization: false
|
||||
}
|
||||
};
|
||||
user.createConfiguration(config);
|
||||
}, "'partial' and 'fullSynchronization' were both set. 'partial' has been deprecated, use only 'fullSynchronization'");
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
testOpen_partialAndFullSynchronizationSetThrows() {
|
||||
const username = uuid();
|
||||
return Realm.Sync.User.register('http://localhost:9080', username, 'password').then((user) => {
|
||||
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue