Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Part 5.1 First Use of Amplify Storage in Webapp #4

Merged
merged 3 commits into from
Oct 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 70 additions & 60 deletions src/app/services/persistence.service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import {Injectable} from "@angular/core";
import {environment} from '../../environments/environment';
import {UserSettings, UserSettingsJSON} from "../models/user-settings";
import {Collection, CollectionJSON} from "../models/collection";
import {AbstractStorableModel, AbstractStorableModelJSON} from "../models/abstract-storable-model";
import * as localForage from "localforage";
import {AuthUser} from "../models/auth-user";
import {SyncService} from "./sync.service";
import {AmplifyService} from 'aws-amplify-angular';
import {AuthClass} from 'aws-amplify';
import {AuthClass, StorageClass} from 'aws-amplify';

/// Exception thrown by failures in the [PersistenceService].
export class PersistenceException {

constructor(readonly statusCode: number, readonly statusText: string) {
constructor(readonly statusCode: number,
readonly statusText: string,
public readonly error?: Error) {
}

toString(): string {
Expand All @@ -25,17 +25,21 @@ export class PersistenceException {
*
* There are two levels of persistence:
* 1. local - Changes are periodically saved to browser storage via LocalForage.
* 2. cloud - Permanent storage to sqac-server, via Feathers.js. (https://www.feathersjs.com)
* 2. cloud - Permanent storage to AWS S3 via Amplify.
*/
@Injectable()
export class PersistenceService {
/** API to the cloud storage */
private readonly cloud: StorageClass;

constructor(private readonly syncSvc: SyncService,
private readonly amplifySvc: AmplifyService,
) {
this.cloud = this.amplifySvc.storage();

localForage.config({
name: 'SqAC',
size: 20000000,
size: 20000000, // 20 MB
storeName: 'SqAC',
version: 2,
description: 'Stores your login session and data for editing and offline use.'
Expand All @@ -55,40 +59,45 @@ export class PersistenceService {
* Load [UserSettings] for the authenticated user.
* Throws [PersistenceException] upon unhandled failure.
*/
async loadUser(userId: string): Promise<UserSettings> {
async loadUser(): Promise<UserSettings> {
try {
// Don't care about userId - always load 'settings', which the server translates to current user.
userId = 'settings';
const settingsKey = 'settings';

// First get local copy
let json: UserSettingsJSON = (await localForage.getItem(userId)) as UserSettingsJSON;
let json: UserSettingsJSON | null = (await localForage.getItem(settingsKey)) as UserSettingsJSON;

// TODO: Then ask the server for a newer one
// Then ask the server for a newer one
if (this.syncSvc.isOnline()) {
// let params: feathers.Params = json ? {query: {ifModifiedSince: json.modified}} : {};
// try {
// let response = await this.server.service(DATA_API_PATH).get(userId, params);
// // response will be blank (empty string?) if server copy is not newer vis-a-vi isModifiedSince
// if (response) {
// // Use and local save updated version
// json = response as UserSettingsJSON;
// json.isCloudBacked = true;
// localForage.setItem(userId, json);
// console.log("Loaded settings", JSON.stringify(json));
// }
// else {
// console.debug("No change in user settings");
// }
// }
// catch (err) {
// return this.translateError(err);
// }
const downloadedObj = await this.cloud.get(settingsKey, {level: 'private', download: true});
try {
const downloadedStr = (downloadedObj as any).Body.toString('utf-8');
const downloadedJson = JSON.parse(downloadedStr) as UserSettingsJSON;
/// let response = await this.getJSON<UserSettingsJSON>(settingsUrl as string).toPromise();
if (!json || downloadedJson.revision > json.revision) {
// Use and local save updated version
json = downloadedJson;
json.isCloudBacked = true;
localForage.setItem(settingsKey, json).then();
console.log("Loaded settings", JSON.stringify(json));
} else {
console.debug("No change in user settings");
}
}
catch (badCloudUpdate) {
console.error("Fetch of updated settings from cloud failure", badCloudUpdate);
}
}

const user = UserSettings.fromJSON(json);
user.isAvailable = true;
this.syncSvc.reset();
return user;
}
catch (error) {
throw this.translateError(error);
}
}

/**
* Save/update UserSettings to the cloud for the authenticated user.
Expand All @@ -97,7 +106,7 @@ export class PersistenceService {
* @throws {PersistenceException} upon unhandled failure.
*/
cloudSaveUser(userSettings: UserSettings): Promise<UserSettings> {
return this.saveModelToCloud(userSettings, 'settings');
return this.saveModelToCloud(userSettings, 'settings', 'private');
}

/**
Expand Down Expand Up @@ -165,7 +174,7 @@ export class PersistenceService {
* @throws {PersistenceException} upon unhandled failure.
*/
cloudSaveCollection(collection: Collection): Promise<Collection> {
return this.saveModelToCloud(collection, collection.id);
return this.saveModelToCloud(collection, collection.id, collection.isPublic ? 'protected' : 'private');
}

/**
Expand Down Expand Up @@ -225,7 +234,7 @@ export class PersistenceService {
model.isDirty = false;

let json = model.toJSON() as AbstractStorableModelJSON;
localForage.setItem(id, json);
localForage.setItem(id, json).then();
console.log("Local stored " + id);
}

Expand All @@ -235,62 +244,63 @@ export class PersistenceService {
/**
* Save an {AbstractStorableModel}.
*
* @param {AbstractStorableModel} model
* @param {string} id
* @returns {Promise<AbstractStorableModel>} the modified model, or rejects with {PersistenceException}
* @param model
* @param id
* @param level store as private or protected?
* @returns the modified model, or rejects with {PersistenceException}
*/
private async saveModelToCloud<T extends AbstractStorableModel>(model: T, id: string): Promise<T> {
private async saveModelToCloud<T extends AbstractStorableModel>(model: T, id: string, level: 'private'|'protected'): Promise<T> {

if (model.isDirty) {
model = await this.saveModelToLocal(model, id);
}

if (model.isCloudBacked) {
// Already saved.
return Promise.resolve(model);
return model;
}

// Save to cloud
model.revision = model.revision ? model.revision + 1 : 1;
model.isCloudBacked = true;
let json = model.toJSON() as AbstractStorableModelJSON;

// TODO:
throw new PersistenceException(501, 'Not implemented');
// return this.server.service(DATA_API_PATH).update(id, json)
// .then(() => {
// // Update local copy
// localForage.setItem(id, json);
// return model;
// })
// .catch(error => {
// // Unset changed values in model
// model.revision--;
// model.isCloudBacked = false;
//
// // Return Error
// return this.translateError(error);
// });
try {
await this.cloud.put(
id, JSON.stringify(json),
{level, contentType: "application/json"}
);

// Update local copy
localForage.setItem(id, json).then();
return model;
}
catch(error) {
// Unset changed values in model
model.revision--;
model.isCloudBacked = false;

// Return Error
throw this.translateError(error);
}
}

/**
* Translate a Response failure to a PersistenceException.
*/
private translateError(error: any) {
private translateError(error: any): PersistenceException {
let errMsg;
let status = 0;
let status = error.statusCode || error.status || error.code || 0;

// Is it a proper feathers error object?
if (error.name && error.code) {
if (error.name) {
errMsg = error.name + ': ' + error.message;
status = error.code;
}
else {
errMsg = error.message ? error.message : error.toString();
}

const ex = new PersistenceException(status, errMsg);
console.error(ex.toString());
return Promise.reject(ex);
const ex = new PersistenceException(status, errMsg, error);
console.error(ex.toString(), error);
return ex;
}
}
14 changes: 5 additions & 9 deletions src/app/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,7 @@ export class UserService {
return this.settings;
}
else {
// FIXME: Load actual data from cloud
return this.createNewSettings();
/*
return this.persistSvc.loadUser(this.authUser.id)
return this.persistSvc.loadUser()
.then(settings => {
console.log("User settings loaded");
this.settings = settings;
Expand Down Expand Up @@ -218,7 +215,6 @@ export class UserService {
this.syncSvc.setDirty();
})
.then(() => this.settings);
*/
}
}

Expand Down Expand Up @@ -259,7 +255,7 @@ export class UserService {
}
else if (!sendOnly) {
console.log("Check for updated content on server");
let tmp = await this.persistSvc.loadUser(this.authUser.id);
let tmp = await this.persistSvc.loadUser();

if (tmp.modified.getTime() > this.settings.modified.getTime()) {
await this.toastr.warning("Updated from cloud", "Account");
Expand Down Expand Up @@ -290,9 +286,9 @@ export class UserService {
this.settings.name = user.name;
this.settings.email = user.email;

// Default collections
["callerlab-basic","callerlab-mainstream","callerlab-plus","adam-classics"]
.forEach(c => this.settings.collections.add(c));
// FIXME: Default collections
// ["callerlab-basic","callerlab-mainstream","callerlab-plus","adam-classics"]
// .forEach(c => this.settings.collections.add(c));

return this.settings;
}
Expand Down