Merge remote-tracking branch 'artlowel/rest-relationships' into w2p-40416_simple-item-page

Conflicts:
	src/app/core/shared/dspace-object.model.ts
This commit is contained in:
Lotte Hofstede
2017-04-27 13:12:23 +02:00
90 changed files with 2537 additions and 1664 deletions

367
README.md
View File

@@ -1,4 +1,8 @@
# dspace-angular
[![Build Status](https://travis-ci.org/DSpace/dspace-angular.svg?branch=master)](https://travis-ci.org/DSpace/dspace-angular) [![Universal Angular](https://img.shields.io/badge/universal-angular2-brightgreen.svg?style=flat)](https://github.com/angular/universal)
dspace-angular
==============
> The next UI for DSpace, based on Angular 2 Universal.
This project is currently in pre-alpha.
@@ -7,7 +11,9 @@ You can find additional information on the [wiki](https://wiki.duraspace.org/dis
If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [here](https://github.com/DSpace-Labs/angular2-ui-prototype)
## Quick start
Quick start
-----------
**Ensure you're running [Node](https://nodejs.org) >= `v5.x`, [npm](https://www.npmjs.com/) >= `v3.x` and [yarn](https://yarnpkg.com) >= `v0.20.x`**
```bash
@@ -26,72 +32,95 @@ yarn install
# start the server
yarn start
```
Then go to [http://localhost:3000](http://localhost:3000) in your browser
NOTE: currently there's not much to see at that URL. We really do need your help. If you're interested in jumping in, and you've made it this far, please look at the [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular), grab a card, and get to work. Thanks!
Not sure where to start? watch the training videos linked in the [Introduction to the technology](#introduction-to-the-technology) section below.
## Table of Contents
* [Introduction to the technology](#introduction-to-the-technology)
* [Requirements](#requirements)
* [Installing](#installing)
* [Configuring](#configuring)
* [Running the app](#running-the-app)
* [Running in production mode](#running-in-production-mode)
* [Cleaning](#cleaning)
* [Testing](#testing)
* [Documentation](#documentation)
* [Other commands](#other-commands)
* [Recommended Editors/IDEs](#recommended-editorsides)
* [Collaborating](#collaborating)
* [File Structure](#file-structure)
* [3rd Party Library Installation](#3rd-party-library-installation)
* [Frequently asked questions](#frequently-asked-questions)
* [License](#license)
Table of Contents
-----------------
- [Introduction to the technology](#introduction-to-the-technology)
- [Requirements](#requirements)
- [Installing](#installing)
- [Configuring](#configuring)
- [Running the app](#running-the-app)
- [Running in production mode](#running-in-production-mode)
- [Cleaning](#cleaning)
- [Testing](#testing)
- [Documentation](#documentation)
- [Other commands](#other-commands)
- [Recommended Editors/IDEs](#recommended-editorsides)
- [Collaborating](#collaborating)
- [File Structure](#file-structure)
- [3rd Party Library Installation](#3rd-party-library-installation)
- [Frequently asked questions](#frequently-asked-questions)
- [License](#license)
Introduction to the technology
------------------------------
## Introduction to the technology
You can find more information on the technologies used in this project (Angular 2, Typescript, Angular Universal, RxJS, etc) on the [DuraSpace wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+UI+Technology+Stack)
## Requirements
* [Node.js](https://nodejs.org), [npm](https://www.npmjs.com/), and [yarn](https://yarnpkg.com)
* Ensure you're running node >= `v5.x`, npm >= `v3.x` and yarn >= `v0.20.x`
Requirements
------------
- [Node.js](https://nodejs.org), [npm](https://www.npmjs.com/), and [yarn](https://yarnpkg.com)
- Ensure you're running node >= `v5.x`, npm >= `v3.x` and yarn >= `v0.20.x`
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
## Installing
* `yarn run global` to install the required global dependencies
* `yarn install` to install the local dependencies
Installing
----------
- `yarn run global` to install the required global dependencies
- `yarn install` to install the local dependencies
Configuring
-----------
## Configuring
Default configuration file is located in `config/` folder.
To change the default configuration values, create local files that override the parameters you need to change:
* Create a new `environment.dev.js` file in `config/` for `devel` environment;
* Create a new `environment.prod.js` file in `config/` for `production` environment;
- Create a new `environment.dev.js` file in `config/` for `devel` environment;
- Create a new `environment.prod.js` file in `config/` for `production` environment;
To use the configuration parameters in your component:
```bash
import { GlobalConfig } from "../config";
```
## Running the app
Running the app
---------------
After you have installed all dependencies you can now run the app. Run `yarn run watch:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:3000`.
## Running in production mode
Running in production mode
--------------------------
When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload.
To build the app for production and start the server run:
To build the app for production and start the server run:
```bash
yarn start
```
If you only want to build for production, without starting, run:
```bash
yarn run build:prod:ngc:json
```
This will build the application and put the result in the `dist` folder
## Cleaning
Cleaning
--------
```bash
# clean everything, including node_modules. You'll need to run yarn install again afterwards.
yarn run clean
@@ -103,129 +132,160 @@ yarn run clean:prod
yarn run clean:dist
```
## Testing
### Unit Test
Unit tests use Karma. You can find the configuration file at the same level of this README file:
`./karma.conf.js`
If you are going to use a remote test enviroment you need to edit the './karma.conf.js'. Follow the instructions you will find inside it.
To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'.
A coverage report is also available at:
http://localhost:9876/
after you run:
`yarn run coverage`.
Testing
-------
To correctly run the tests you need to run the build once with:
`yarn run build`.
### Unit Test
Unit tests use Karma. You can find the configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test enviroment you need to edit the './karma.conf.js'. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run:`yarn run coverage`.
To correctly run the tests you need to run the build once with:`yarn run build`.
The default browser is Google Chrome.
Place your tests in the same location of the application source code files that they test.
and run:
`yarn run test`
and run:`yarn run test`
### E2E test
E2E tests use Protractor + Selenium server + browsers. You can find the configuration file at the same level of this README file:
`./protractor.conf.js`
Protractor is installed as 'local' as a dev dependency.
E2E tests use Protractor + Selenium server + browsers. You can find the configuration file at the same level of this README file:`./protractor.conf.js` Protractor is installed as 'local' as a dev dependency.
If you are going to use a remote test enviroment you need to edit the './protractor.conf.js'. Follow the instructions you will find inside it.
The default browser is Google Chrome.
Protractor needs a functional instance of the DSpace interface to run the E2E tests, so you need to run:
`yarn run watch:dev`
Protractor needs a functional instance of the DSpace interface to run the E2E tests, so you need to run:`yarn run watch:dev`
or any command that bring up the DSpace interface.
Place your tests at the following path:
`./e2e`
Place your tests at the following path:`./e2e`
and run:
`yarn run e2e`
and run:`yarn run e2e`
### Continuous Integration (CI) Test
To run all the tests (e.g.: to run tests with Continuous Integration software) you can execute:
`yarn run ci`
Keep in mind that this command prerequisites are the sum of unit test and E2E tests.
##Documentation
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects.
It extracts informations from properly formatted comments that can be written within the code files.
Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
To run all the tests (e.g.: to run tests with Continuous Integration software) you can execute:`yarn run ci` Keep in mind that this command prerequisites are the sum of unit test and E2E tests.
Run:
`yarn run docs`
to produce the documentation that will be available in the 'doc' folder.
##Documentation To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts informations from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder.
Other commands
--------------
## Other commands
There are many more commands in the `scripts` section of `package.json`. Most of these are executed by one of the commands mentioned above.
A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `yarn run start` the `prestart` script will run first, then the `start` script will trigger.
Recommended Editors/IDEs
------------------------
## Recommended Editors/IDEs
To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've had good experiences using these editors:
* Free
* [Visual Studio Code](https://code.visualstudio.com/)
* [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
* [Atom](https://atom.io/)
* [TypeScript plugin](https://atom.io/packages/atom-typescript)
* Paid
* [Webstorm](https://www.jetbrains.com/webstorm/download/) or [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/)
* [Sublime Text](http://www.sublimetext.com/3)
* [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation)
- Free
- [Visual Studio Code](https://code.visualstudio.com/)
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
- [Atom](https://atom.io/)
- [TypeScript plugin](https://atom.io/packages/atom-typescript)
- Paid
- [Webstorm](https://www.jetbrains.com/webstorm/download/) or [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/)
- [Sublime Text](http://www.sublimetext.com/3)
- [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation)
Collaborating
-------------
## Collaborating
See [the guide on the wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+2+UI#DSpace7-Angular2UI-Howtocontribute)
## File Structure
File Structure
--------------
```
dspace-angular
├── README.md * This document
├── app.json * Application manifest file
├── config * Folder for configuration files
│   └── environment.default.js * Default configuration files
├── e2e * Folder for e2e test files
├── karma.conf.js * Unit Test configuration file
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
├── postcss.config.json * PostCSS (http://postcss.org/) configuration file
├── protractor.conf.js * E2E tests configuration file
├── resources * Folder for static resources
│   ── i18n * Folder for i18n translations
│   └── images * Folder for images
├── rollup-client.js * Rollup (http://rollupjs.org/) configuration for the client
├── rollup-server.js * Rollup (http://rollupjs.org/) configuration for the server
├── src * The source of the application
│   ├── app * The location of the app module, and root of the application shared by client and server
│   ├── backend * Folder containing a mock of the REST API, hosted by the express server
│   ├── browser.module.ts * The root module for the client
│   ├── client.aot.ts * The bootstrap file for the client, in production
│   ├── client.ts * The bootstrap file for the client, during development
│   ├── config.ts * File that loads environmental and shareable settings and makes them available to app components
│   ── index-aot.html * The index.html file, for production
│   ├── index.html * The index.html file, for development
│   ├── node.module.ts * The root module for the server
│   ├── server.aot.ts * The express (http://expressjs.com/) config and bootstrap file for the server, in production
│   ├── server.routes.ts * The routes file for the server
│   ├── server.ts * The express (http://expressjs.com/) config and bootstrap file for the server, during development
│   ├── styles * Folder containing global styles.
│   │   ├── main.scss * Global scss file
│   │   ── variables.scss * Global sass variables file
│   └── typings.d.ts * File that allows you to add custom typings for libraries without TypeScript support
├── tsconfig.aot.json * TypeScript config for production builds
├── tsconfig.json * TypeScript config for development build
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
├── typedoc.json * TYPEDOC configuration
├── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
├── webpack.config.ts * Webpack (https://webpack.github.io/) config for development builds
├── webpack.test.config.ts * Webpack (https://webpack.github.io/) config for testing
└── webpack.prod.config.ts * Webpack (https://webpack.github.io/) config for production builds
├── README.md * This document
├── app.json * Application manifest file
├── config * Folder for configuration files
│   └── environment.default.js * Default configuration files
├── dist * Folder for e2e test files
├── e2e *
│   ├── app.e2e-spec.ts *
│   ├── app.po.ts *
│   ├── pagenotfound *
│   │   ├── pagenotfound.e2e-spec.ts *
│   │   └── pagenotfound.po.ts *
│   ── tsconfig.json *
├── empty.js *
├── helpers.js *
├── karma.conf.js * Unit Test configuration file
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
├── postcss.config.json * PostCSS (http://postcss.org/) configuration file
├── protractor.conf.js * E2E tests configuration file
├── resources * Folder for static resources
│   ├── i18n * Folder for i18n translations
│   │   └── en.json *
│   ── images * Folder for images
│   └── dspace_logo.png *
├── rollup-client.js * Rollup (http://rollupjs.org/) configuration for the client
├── rollup-server.js * Rollup (http://rollupjs.org/) configuration for the server
├── spec-bundle.js *
├── src * The source of the application
│   ├── app * The location of the app module, and root of the application shared by client and server
│   │   ├── app-routing.module.ts *
│   │   ── app.component.html *
│   │   ├── app.component.scss *
│   │   ├── app.component.spec.ts *
│   │   ├── app.component.ts *
│   │   ├── app.effects.ts *
│   │   ├── app.module.ts *
│   │   ├── app.reducers.ts *
│   │   ├── core *
│   │   ├── header *
│   │   ├── home *
│   │   ├── pagenotfound *
│   │   ├── shared *
│   │   └── store.actions.ts *
│   ├── backend * Folder containing a mock of the REST API, hosted by the express server
│   │   ├── api.ts *
│   │   ├── bitstreams.ts *
│   │   ├── bundles.ts *
│   │   ├── cache.ts *
│   │   ├── collections.ts *
│   │   ├── db.ts *
│   │   ├── items.ts *
│   │   └── metadata.ts *
│   ├── client.aot.ts * The bootstrap file for the client, in production
│   ├── client.ts * The bootstrap file for the client, during development
│   ├── config.ts * File that loads environmental and shareable settings and makes them available to app components
│   ├── index.html * The index.html file
│   ├── platform *
│   │   ├── angular2-meta.ts *
│   │   ├── modules *
│   │   │   ├── browser.module.ts * The root module for the client
│   │   │   └── node.module.ts * The root module for the server
│   │   └── workarounds *
│   │   ├── __workaround.browser.ts *
│   │   └── __workaround.node.ts *
│   ├── server.aot.ts * The express (http://expressjs.com/) config and bootstrap file for the server, in production
│   ├── server.routes.ts * The routes file for the server
│   ├── server.ts * The express (http://expressjs.com/) config and bootstrap file for the server, during development
│   ├── styles * Folder containing global styles.
│   │   ├── main.scss * Global scss file
│   │   └── variables.scss * Global sass variables file
│   └── typings.d.ts * File that allows you to add custom typings for libraries without TypeScript support
├── tsconfig.aot.json * TypeScript config for production builds
├── tsconfig.json * TypeScript config for development build
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
├── typedoc.json * TYPEDOC configuration
├── webpack.config.ts * Webpack (https://webpack.github.io/) config for development builds
├── webpack.prod.config.ts * Webpack (https://webpack.github.io/) config for production builds
├── webpack.test.config.js * Webpack (https://webpack.github.io/) config for testing
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
```
## 3rd Party Library Installation
3rd Party Library Installation
------------------------------
Install your library via `yarn add lib-name --save` and import it in your code. `--save` will add it to `package.json`.
@@ -236,57 +296,60 @@ yarn add d3 --save
yarn add @types/d3 --save-dev
```
If the library doesn't have typings available at `@types/`, you can still use it by
manually adding typings for it:
If the library doesn't have typings available at `@types/`, you can still use it by manually adding typings for it:
1. In `src/typings.d.ts`, add the following code:
1. In `src/typings.d.ts`, add the following code:
```typescript
declare module 'typeless-package';
```
```typescript
declare module 'typeless-package';
```
2. Then, in the component or file that uses the library, add the following code:
2. Then, in the component or file that uses the library, add the following code:
```typescript
import * as typelessPackage from 'typeless-package';
typelessPackage.method();
```
```typescript
import * as typelessPackage from 'typeless-package';
typelessPackage.method();
```
Done. Note: you might need or find useful to define more typings for the library that you're trying to use.
If you're importing a module that uses CommonJS you need to import as
```typescript
import * as _ from 'lodash';
```
## yarn lockfile
yarn lockfile
-------------
This project makes use of yarn to ensure that the exact same dependency versions are used every time you install it.
yarn creates the file [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically every time you install a new dependency from the commandline (by using `yarn add some-lib --save` or `yarn add some-lib --save-dev`).
If you manually add a package or change a version in `package.json` you'll have to update yarn's lock file as well. You can do so by running `yarn upgrade`
## Frequently asked questions
* Why is my service, aka provider, is not injecting a parameter correctly?
* Please use `@Injectable()` for your service for typescript to correctly attach the metadata
* Where do I write my tests?
* You can write your tests next to your component files. e.g. for `src/app/home/home.component.ts` call it `src/app/home/home.component.spec.ts`
* How do I start the app when I get `EACCES` and `EADDRINUSE` errors?
* The `EADDRINUSE` error means the port `3000` is currently being used and `EACCES` is lack of permission to build files to `./dist/`
* What are the naming conventions for Angular 2?
* See [the official angular 2 style guide](https://angular.io/styleguide)
* Why is the size of my app larger in development?
* The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed.
* node-pre-gyp ERR in yarn install (Windows)
* install Python x86 version between 2.5 and 3.0 on windows. See [this issue](https://github.com/AngularClass/angular2-webpack-starter/issues/626)
* How do I handle merge conflicts in yarn.lock?
* first check out the yarn.lock file from the branch you're merging in to yours: e.g. `git checkout --theirs yarn.lock`
* now run `yarn install` again. Yarn will create a new lockfile that contains both sets of changes.
* then run `git add yarn.lock` to stage the lockfile for commit
* and `git commit` to conclude the merge
Frequently asked questions
--------------------------
- Why is my service, aka provider, is not injecting a parameter correctly?
- Please use `@Injectable()` for your service for typescript to correctly attach the metadata
- Where do I write my tests?
- You can write your tests next to your component files. e.g. for `src/app/home/home.component.ts` call it `src/app/home/home.component.spec.ts`
- How do I start the app when I get `EACCES` and `EADDRINUSE` errors?
- The `EADDRINUSE` error means the port `3000` is currently being used and `EACCES` is lack of permission to build files to `./dist/`
- What are the naming conventions for Angular 2?
- See [the official angular 2 style guide](https://angular.io/styleguide)
- Why is the size of my app larger in development?
- The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed.
- node-pre-gyp ERR in yarn install (Windows)
- install Python x86 version between 2.5 and 3.0 on windows. See [this issue](https://github.com/AngularClass/angular2-webpack-starter/issues/626)
- How do I handle merge conflicts in yarn.lock?
- first check out the yarn.lock file from the branch you're merging in to yours: e.g. `git checkout --theirs yarn.lock`
- now run `yarn install` again. Yarn will create a new lockfile that contains both sets of changes.
- then run `git add yarn.lock` to stage the lockfile for commit
- and `git commit` to conclude the merge
License
-------
## License
http://www.dspace.org/license

View File

@@ -1,22 +1,28 @@
module.exports = {
"production": false,
// The REST API Location.
// The REST API server settings.
"rest": {
"ssl": false,
"address": "localhost",
"port": 3000,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
"nameSpace": "/api",
"baseURL": "http://localhost:3000"
"nameSpace": "/api"
},
// Path and Port in use for this Angular2 UI
// Angular2 UI server settings.
"ui": {
"nameSpace": "/",
"baseURL": "http://localhost:3000"
"ssl": false,
"address": "localhost",
"port": 3000,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
"nameSpace": "/"
},
"cache": {
// how long should objects be cached for by default
"msToLive": 15 * 60 * 1000, //15 minutes
"msToLive": 15 * 60 * 1000, // 15 minute
"control": "max-age=60" // revalidate browser
},
"universal": {
//on the client: start with the state on the server
"shouldRehydrate": true
// Angular Universal settings
"preboot": true,
"async": true
}
};

View File

@@ -151,6 +151,7 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webdriver-launcher": "^1.0.4",
"karma-webpack": "1.8.0",
"ngrx-store-freeze": "^0.1.9",
"node-sass": "4.0.0",
"nodemon": "1.11.0",
"npm-run-all": "4.0.2",

View File

@@ -6,7 +6,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
imports: [
RouterModule.forChild([
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent},
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
])
],
})

View File

@@ -6,8 +6,10 @@
<div class="container-fluid">
<p>{{ 'example.with.data' | translate:data }}</p>
<p>{{ example }}</p>
<h2 *ngIf="!env" style="color:green">development</h2>
<h2 *ngIf="env" style="color:red">production</h2>
<h2 [ngClass]="{ 'red': EnvConfig.production, 'green': !EnvConfig.production }">
<span *ngIf="!EnvConfig.production">development</span>
<span *ngIf="EnvConfig.production">production</span>
</h2>
<router-outlet></router-outlet>
</div>
</main>

View File

@@ -1,5 +1,4 @@
// Sticky Footer
.outer-wrapper {
display: flex;
margin: 0;
@@ -16,3 +15,11 @@
.main-content {
flex: 1 0 auto;
}
h2.red {
color: red;
}
h2.green {
color: green;
}

View File

@@ -21,6 +21,8 @@ import { HostWindowState } from "./shared/host-window.reducer";
import { HostWindowResizeAction } from "./shared/host-window.actions";
import { MockTranslateLoader } from "./shared/testing/mock-translate-loader";
import { GLOBAL_CONFIG, EnvConfig } from '../config';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let de: DebugElement;
@@ -37,6 +39,7 @@ describe('App component', () => {
})],
declarations: [AppComponent], // declare the test component
providers: [
{ provide: GLOBAL_CONFIG, useValue: EnvConfig },
AppComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@@ -1,6 +1,7 @@
import {
Component,
ChangeDetectionStrategy,
Inject,
ViewEncapsulation,
OnDestroy,
OnInit, HostListener
@@ -9,7 +10,8 @@ import { TranslateService } from "ng2-translate";
import { HostWindowState } from "./shared/host-window.reducer";
import { Store } from "@ngrx/store";
import { HostWindowResizeAction } from "./shared/host-window.actions";
import { GlobalConfig } from "../config";
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
@Component({
changeDetection: ChangeDetectionStrategy.Default,
@@ -28,13 +30,8 @@ export class AppComponent implements OnDestroy, OnInit {
recipient: 'World'
};
env: string = GlobalConfig.production;
styles = {
color: 'red'
};
constructor(
@Inject(GLOBAL_CONFIG) public EnvConfig: GlobalConfig,
private translate: TranslateService,
private store: Store<HostWindowState>
) {

View File

@@ -3,8 +3,12 @@ import { routerReducer, RouterState } from "@ngrx/router-store";
import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from "./shared/host-window.reducer";
import { CoreState, coreReducer } from "./core/core.reducers";
import { storeFreeze } from 'ngrx-store-freeze';
import { compose } from "@ngrx/core";
import { StoreActionTypes } from "./store.actions";
import { EnvConfig } from '../config';
export interface AppState {
core: CoreState;
router: RouterState;
@@ -20,10 +24,16 @@ export const reducers = {
};
export function rootReducer(state: any, action: any) {
let output;
if (action.type === StoreActionTypes.REHYDRATE) {
state = action.payload;
}
return combineReducers(reducers)(state, action);
if (EnvConfig.production) {
output = combineReducers(reducers)(state, action);
} else {
output = compose(storeFreeze, combineReducers)(reducers)(state, action);
}
return output;
}
export const NGRX_CACHE_KEY = "NGRX_STORE";

View File

@@ -1,14 +1,14 @@
import { combineReducers } from "@ngrx/store";
import { RequestCacheState, requestCacheReducer } from "./request-cache.reducer";
import { ResponseCacheState, responseCacheReducer } from "./response-cache.reducer";
import { ObjectCacheState, objectCacheReducer } from "./object-cache.reducer";
export interface CacheState {
request: RequestCacheState,
response: ResponseCacheState,
object: ObjectCacheState
}
export const reducers = {
request: requestCacheReducer,
response: responseCacheReducer,
object: objectCacheReducer
};

View File

@@ -0,0 +1,65 @@
import { Bitstream } from "../../shared/bitstream.model";
import { ObjectCacheService } from "../object-cache.service";
import { ResponseCacheService } from "../response-cache.service";
import { RequestService } from "../../data/request.service";
import { Store } from "@ngrx/store";
import { CoreState } from "../../core.reducers";
import { NormalizedBitstream } from "./normalized-bitstream.model";
import { ListRemoteDataBuilder, SingleRemoteDataBuilder } from "./remote-data-builder";
import { Request } from "../../data/request.models";
import { hasValue } from "../../../shared/empty.util";
import { RequestConfigureAction, RequestExecuteAction } from "../../data/request.actions";
export class BitstreamBuilder {
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>,
protected href: string,
protected normalized: NormalizedBitstream
) {
}
build(): Bitstream {
let links: any = {};
//TODO
return Object.assign(new Bitstream(), this.normalized, links);
}
}
export class BitstreamRDBuilder extends SingleRemoteDataBuilder<Bitstream, NormalizedBitstream> {
constructor(
objectCache: ObjectCacheService,
responseCache: ResponseCacheService,
requestService: RequestService,
store: Store<CoreState>,
href: string
) {
super(objectCache, responseCache, requestService, store, href, NormalizedBitstream);
}
protected normalizedToDomain(normalized: NormalizedBitstream): Bitstream {
return new BitstreamBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build();
}
}
export class BitstreamListRDBuilder extends ListRemoteDataBuilder<Bitstream, NormalizedBitstream> {
constructor(
objectCache: ObjectCacheService,
responseCache: ResponseCacheService,
requestService: RequestService,
store: Store<CoreState>,
href: string
) {
super(objectCache, responseCache, requestService, store, href, NormalizedBitstream);
}
protected normalizedToDomain(normalized: NormalizedBitstream): Bitstream {
return new BitstreamBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build();
}
}

View File

@@ -0,0 +1,93 @@
import { Bundle } from "../../shared/bundle.model";
import { ObjectCacheService } from "../object-cache.service";
import { ResponseCacheService } from "../response-cache.service";
import { RequestService } from "../../data/request.service";
import { Store } from "@ngrx/store";
import { CoreState } from "../../core.reducers";
import { NormalizedBundle } from "./normalized-bundle.model";
import { ListRemoteDataBuilder, SingleRemoteDataBuilder } from "./remote-data-builder";
import { Request } from "../../data/request.models";
import { hasValue } from "../../../shared/empty.util";
import { RequestConfigureAction, RequestExecuteAction } from "../../data/request.actions";
import { BitstreamRDBuilder } from "./bitstream-builder";
import { NormalizedBitstream } from "./normalized-bitstream.model";
export class BundleBuilder {
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>,
protected href: string,
protected normalized: NormalizedBundle
) {
}
build(): Bundle {
let links: any = {};
if (hasValue(this.normalized.bitstreams)) {
//for some reason the dispatches in the forEach don't
//fire without this timeout. A zone issue?
setTimeout(() => {
this.normalized.bitstreams.forEach((href: string) => {
const isCached = this.objectCache.hasBySelfLink(href);
const isPending = this.requestService.isPending(href);
if (!(isCached || isPending)) {
const request = new Request(href, NormalizedBitstream);
this.store.dispatch(new RequestConfigureAction(request));
this.store.dispatch(new RequestExecuteAction(href));
}
});
}, 0);
links.bitstreams = this.normalized.bitstreams.map((href: string) => {
return new BitstreamRDBuilder(
this.objectCache,
this.responseCache,
this.requestService,
this.store,
href
).build();
});
}
return Object.assign(new Bundle(), this.normalized, links);
}
}
export class BundleRDBuilder extends SingleRemoteDataBuilder<Bundle, NormalizedBundle> {
constructor(
objectCache: ObjectCacheService,
responseCache: ResponseCacheService,
requestService: RequestService,
store: Store<CoreState>,
href: string
) {
super(objectCache, responseCache, requestService, store, href, NormalizedBundle);
}
protected normalizedToDomain(normalized: NormalizedBundle): Bundle {
return new BundleBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build();
}
}
export class BundleListRDBuilder extends ListRemoteDataBuilder<Bundle, NormalizedBundle> {
constructor(
objectCache: ObjectCacheService,
responseCache: ResponseCacheService,
requestService: RequestService,
store: Store<CoreState>,
href: string
) {
super(objectCache, responseCache, requestService, store, href, NormalizedBundle);
}
protected normalizedToDomain(normalized: NormalizedBundle): Bundle {
return new BundleBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build();
}
}

View File

@@ -0,0 +1,92 @@
import { Collection } from "../../shared/collection.model";
import { hasValue } from "../../../shared/empty.util";
import { Item } from "../../shared/item.model";
import { RequestConfigureAction, RequestExecuteAction } from "../../data/request.actions";
import { ObjectCacheService } from "../object-cache.service";
import { ResponseCacheService } from "../response-cache.service";
import { RequestService } from "../../data/request.service";
import { Store } from "@ngrx/store";
import { CoreState } from "../../core.reducers";
import { NormalizedCollection } from "./normalized-collection.model";
import { Request } from "../../data/request.models";
import { ListRemoteDataBuilder, SingleRemoteDataBuilder } from "./remote-data-builder";
import { ItemRDBuilder } from "./item-builder";
import { NormalizedItem } from "./normalized-item.model";
export class CollectionBuilder {
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>,
protected href: string,
protected normalized: NormalizedCollection
) {
}
build(): Collection {
let links: any = {};
if (hasValue(this.normalized.items)) {
this.normalized.items.forEach((href: string) => {
const isCached = this.objectCache.hasBySelfLink(href);
const isPending = this.requestService.isPending(href);
if (!(isCached || isPending)) {
const request = new Request(href, NormalizedItem);
this.store.dispatch(new RequestConfigureAction(request));
this.store.dispatch(new RequestExecuteAction(href));
}
});
links.items = this.normalized.items.map((href: string) => {
return new ItemRDBuilder(
this.objectCache,
this.responseCache,
this.requestService,
this.store,
href
).build();
});
}
return Object.assign(new Collection(), this.normalized, links);
}
}
export class CollectionRDBuilder extends SingleRemoteDataBuilder<Collection, NormalizedCollection> {
constructor(
objectCache: ObjectCacheService,
responseCache: ResponseCacheService,
requestService: RequestService,
store: Store<CoreState>,
href: string
) {
super(objectCache, responseCache, requestService, store, href, NormalizedCollection);
}
protected normalizedToDomain(normalized: NormalizedCollection): Collection {
return new CollectionBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build();
}
}
export class CollectionListRDBuilder extends ListRemoteDataBuilder<Collection, NormalizedCollection> {
constructor(
objectCache: ObjectCacheService,
responseCache: ResponseCacheService,
requestService: RequestService,
store: Store<CoreState>,
href: string
) {
super(objectCache, responseCache, requestService, store, href, NormalizedCollection);
}
protected normalizedToDomain(normalized: NormalizedCollection): Collection {
return new CollectionBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build();
}
}

View File

@@ -0,0 +1,89 @@
import { Item } from "../../shared/item.model";
import { ObjectCacheService } from "../object-cache.service";
import { ResponseCacheService } from "../response-cache.service";
import { RequestService } from "../../data/request.service";
import { Store } from "@ngrx/store";
import { CoreState } from "../../core.reducers";
import { NormalizedItem } from "./normalized-item.model";
import { ListRemoteDataBuilder, SingleRemoteDataBuilder } from "./remote-data-builder";
import { Request } from "../../data/request.models";
import { hasValue } from "../../../shared/empty.util";
import { RequestConfigureAction, RequestExecuteAction } from "../../data/request.actions";
import { BundleRDBuilder } from "./bundle-builder";
import { NormalizedBundle } from "./normalized-bundle.model";
export class ItemBuilder {
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>,
protected href: string,
protected normalized: NormalizedItem
) {
}
build(): Item {
let links: any = {};
if (hasValue(this.normalized.bundles)) {
this.normalized.bundles.forEach((href: string) => {
const isCached = this.objectCache.hasBySelfLink(href);
const isPending = this.requestService.isPending(href);
if (!(isCached || isPending)) {
const request = new Request(href, NormalizedBundle);
this.store.dispatch(new RequestConfigureAction(request));
this.store.dispatch(new RequestExecuteAction(href));
}
});
links.bundles = this.normalized.bundles.map((href: string) => {
return new BundleRDBuilder(
this.objectCache,
this.responseCache,
this.requestService,
this.store,
href
).build();
});
}
return Object.assign(new Item(), this.normalized, links);
}
}
export class ItemRDBuilder extends SingleRemoteDataBuilder<Item, NormalizedItem> {
constructor(
objectCache: ObjectCacheService,
responseCache: ResponseCacheService,
requestService: RequestService,
store: Store<CoreState>,
href: string
) {
super(objectCache, responseCache, requestService, store, href, NormalizedItem);
}
protected normalizedToDomain(normalized: NormalizedItem): Item {
return new ItemBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build();
}
}
export class ItemListRDBuilder extends ListRemoteDataBuilder<Item, NormalizedItem> {
constructor(
objectCache: ObjectCacheService,
responseCache: ResponseCacheService,
requestService: RequestService,
store: Store<CoreState>,
href: string
) {
super(objectCache, responseCache, requestService, store, href, NormalizedItem);
}
protected normalizedToDomain(normalized: NormalizedItem): Item {
return new ItemBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build();
}
}

View File

@@ -0,0 +1,36 @@
import { inheritSerialization } from "cerialize";
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedBitstream extends NormalizedDSpaceObject {
/**
* The size of this bitstream in bytes(?)
*/
size: number;
/**
* The relative path to this Bitstream's file
*/
url: string;
/**
* The mime type of this Bitstream
*/
mimetype: string;
/**
* The description of this Bitstream
*/
description: string;
/**
* An array of Bundles that are direct parents of this Bitstream
*/
parents: Array<string>;
/**
* The Bundle that owns this Bitstream
*/
owner: string;
}

View File

@@ -0,0 +1,24 @@
import { autoserialize, inheritSerialization } from "cerialize";
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedBundle extends NormalizedDSpaceObject {
/**
* The primary bitstream of this Bundle
*/
@autoserialize
primaryBitstream: string;
/**
* An array of Items that are direct parents of this Bundle
*/
parents: Array<string>;
/**
* The Item that owns this Bundle
*/
owner: string;
@autoserialize
bitstreams: Array<string>;
}

View File

@@ -0,0 +1,31 @@
import { autoserialize, inheritSerialization, autoserializeAs } from "cerialize";
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedCollection extends NormalizedDSpaceObject {
/**
* A string representing the unique handle of this Collection
*/
@autoserialize
handle: string;
/**
* The Bitstream that represents the logo of this Collection
*/
logo: string;
/**
* An array of Collections that are direct parents of this Collection
*/
parents: Array<string>;
/**
* The Collection that owns this Collection
*/
owner: string;
@autoserialize
items: Array<string>;
}

View File

@@ -0,0 +1,51 @@
import { autoserialize, autoserializeAs } from "cerialize";
import { CacheableObject } from "../object-cache.reducer";
import { Metadatum } from "../../shared/metadatum.model";
/**
* An abstract model class for a DSpaceObject.
*/
export abstract class NormalizedDSpaceObject implements CacheableObject {
@autoserialize
self: string;
/**
* The human-readable identifier of this DSpaceObject
*/
@autoserialize
id: string;
/**
* The universally unique identifier of this DSpaceObject
*/
@autoserialize
uuid: string;
/**
* A string representing the kind of DSpaceObject, e.g. community, item, …
*/
type: string;
/**
* The name for this DSpaceObject
*/
@autoserialize
name: string;
/**
* An array containing all metadata of this DSpaceObject
*/
@autoserializeAs(Metadatum)
metadata: Array<Metadatum>;
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
*/
parents: Array<string>;
/**
* The DSpaceObject that owns this DSpaceObject
*/
owner: string;
}

View File

@@ -0,0 +1,40 @@
import { inheritSerialization, autoserialize } from "cerialize";
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedItem extends NormalizedDSpaceObject {
/**
* A string representing the unique handle of this Item
*/
@autoserialize
handle: string;
/**
* The Date of the last modification of this Item
*/
lastModified: Date;
/**
* A boolean representing if this Item is currently archived or not
*/
isArchived: boolean;
/**
* A boolean representing if this Item is currently withdrawn or not
*/
isWithdrawn: boolean;
/**
* An array of Collections that are direct parents of this Item
*/
parents: Array<string>;
/**
* The Collection that owns this Item
*/
owner: string;
@autoserialize
bundles: Array<string>;
}

View File

@@ -0,0 +1,131 @@
import { RemoteData } from "../../data/remote-data";
import { Observable } from "rxjs/Observable";
import { RequestEntry } from "../../data/request.reducer";
import { ResponseCacheEntry } from "../response-cache.reducer";
import { ErrorResponse, SuccessResponse } from "../response-cache.models";
import { Store } from "@ngrx/store";
import { CoreState } from "../../core.reducers";
import { ResponseCacheService } from "../response-cache.service";
import { ObjectCacheService } from "../object-cache.service";
import { RequestService } from "../../data/request.service";
import { CacheableObject } from "../object-cache.reducer";
import { GenericConstructor } from "../../shared/generic-constructor";
import { hasValue, isNotEmpty } from "../../../shared/empty.util";
export interface RemoteDataBuilder<T> {
build(): RemoteData<T>
}
export abstract class SingleRemoteDataBuilder<TDomain, TNormalized extends CacheableObject> implements RemoteDataBuilder<TDomain> {
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>,
protected href: string,
protected normalizedType: GenericConstructor<TNormalized>
) {
}
protected abstract normalizedToDomain(normalized: TNormalized): TDomain;
build(): RemoteData<TDomain> {
const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', this.href);
const responseCacheObs = this.responseCache.get(this.href);
const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged();
const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged();
const isSuccessFul = responseCacheObs
.map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged();
const errorMessage = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
.distinctUntilChanged();
const payload =
Observable.race(
this.objectCache.getBySelfLink<TNormalized>(this.href, this.normalizedType),
responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => {
if (isNotEmpty(resourceUUIDs)) {
return this.objectCache.get(resourceUUIDs[0], this.normalizedType);
}
else {
return Observable.of(undefined);
}
})
.distinctUntilChanged()
).map((normalized: TNormalized) => this.normalizedToDomain(normalized));
return new RemoteData(
this.href,
requestPending,
responsePending,
isSuccessFul,
errorMessage,
payload
);
}
}
export abstract class ListRemoteDataBuilder<TDomain, TNormalized extends CacheableObject> implements RemoteDataBuilder<TDomain[]> {
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>,
protected href: string,
protected normalizedType: GenericConstructor<TNormalized>
) {
}
protected abstract normalizedToDomain(normalized: TNormalized): TDomain;
build(): RemoteData<TDomain[]> {
const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', this.href);
const responseCacheObs = this.responseCache.get(this.href);
const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged();
const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged();
const isSuccessFul = responseCacheObs
.map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged();
const errorMessage = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
.distinctUntilChanged();
const payload = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => {
return this.objectCache.getList(resourceUUIDs, this.normalizedType)
.map((normList: TNormalized[]) => {
return normList.map((normalized: TNormalized) => {
return this.normalizedToDomain(normalized);
});
});
})
.distinctUntilChanged();
return new RemoteData(
this.href,
requestPending,
responsePending,
isSuccessFul,
errorMessage,
payload
);
}
}

View File

@@ -0,0 +1,10 @@
import { autoserialize } from "cerialize";
export class SelfLink {
@autoserialize
self: string;
@autoserialize
uuid: string;
}

View File

@@ -12,6 +12,7 @@ import { CacheEntry } from "./cache-entry";
*/
export interface CacheableObject {
uuid: string;
self?: string;
}
/**

View File

@@ -60,6 +60,11 @@ export class ObjectCacheService {
.map((entry: ObjectCacheEntry) => <T> Object.assign(new type(), entry.data));
}
getBySelfLink<T extends CacheableObject>(href: string, type: GenericConstructor<T>): Observable<T> {
return this.store.select<string>('core', 'index', 'href', href)
.flatMap((uuid: string) => this.get(uuid, type))
}
/**
* Get an observable for an array of objects of the same type
* with the specified UUIDs
@@ -104,6 +109,25 @@ export class ObjectCacheService {
return result;
}
/**
* Check whether the object with the specified self link is cached
*
* @param href
* The self link of the object to check
* @return boolean
* true if the object with the specified self link is cached,
* false otherwise
*/
hasBySelfLink(href: string): boolean {
let result: boolean = false;
this.store.select<string>('core', 'index', 'href', href)
.take(1)
.subscribe((uuid: string) => result = this.has(uuid));
return result;
}
/**
* Check whether an ObjectCacheEntry should still be cached
*

View File

@@ -1,205 +0,0 @@
import { OpaqueToken } from "@angular/core";
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
import { PaginationOptions } from "../shared/pagination-options.model";
import { SortOptions } from "../shared/sort-options.model";
/**
* The list of RequestCacheAction type definitions
*/
export const RequestCacheActionTypes = {
FIND_BY_ID: type('dspace/core/cache/request/FIND_BY_ID'),
FIND_ALL: type('dspace/core/cache/request/FIND_ALL'),
SUCCESS: type('dspace/core/cache/request/SUCCESS'),
ERROR: type('dspace/core/cache/request/ERROR'),
REMOVE: type('dspace/core/cache/request/REMOVE'),
RESET_TIMESTAMPS: type('dspace/core/cache/request/RESET_TIMESTAMPS')
};
/**
* An ngrx action to find all objects of a certain type
*/
export class RequestCacheFindAllAction implements Action {
type = RequestCacheActionTypes.FIND_ALL;
payload: {
key: string,
service: OpaqueToken,
scopeID: string,
paginationOptions: PaginationOptions,
sortOptions: SortOptions
};
/**
* Create a new RequestCacheFindAllAction
*
* @param key
* the key under which to cache this request, should be unique
* @param service
* the name of the service that initiated the action
* @param scopeID
* the id of an optional scope object
* @param paginationOptions
* the pagination options
* @param sortOptions
* the sort options
*/
constructor(
key: string,
service: OpaqueToken,
scopeID?: string,
paginationOptions: PaginationOptions = new PaginationOptions(),
sortOptions: SortOptions = new SortOptions()
) {
this.payload = {
key,
service,
scopeID,
paginationOptions,
sortOptions
}
}
}
/**
* An ngrx action to find objects by id
*/
export class RequestCacheFindByIDAction implements Action {
type = RequestCacheActionTypes.FIND_BY_ID;
payload: {
key: string,
service: OpaqueToken,
resourceID: string
};
/**
* Create a new RequestCacheFindByIDAction
*
* @param key
* the key under which to cache this request, should be unique
* @param service
* the name of the service that initiated the action
* @param resourceID
* the ID of the resource to find
*/
constructor(
key: string,
service: OpaqueToken,
resourceID: string
) {
this.payload = {
key,
service,
resourceID
}
}
}
/**
* An ngrx action to indicate a request was returned successful
*/
export class RequestCacheSuccessAction implements Action {
type = RequestCacheActionTypes.SUCCESS;
payload: {
key: string,
resourceUUIDs: Array<string>,
timeAdded: number,
msToLive: number
};
/**
* Create a new RequestCacheSuccessAction
*
* @param key
* the key under which cache this request is cached,
* should be identical to the one used in the corresponding
* find action
* @param resourceUUIDs
* the UUIDs returned from the backend
* @param timeAdded
* the time it was returned
* @param msToLive
* the amount of milliseconds before it should expire
*/
constructor(key: string, resourceUUIDs: Array<string>, timeAdded, msToLive: number) {
this.payload = {
key,
resourceUUIDs,
timeAdded,
msToLive
};
}
}
/**
* An ngrx action to indicate a request failed
*/
export class RequestCacheErrorAction implements Action {
type = RequestCacheActionTypes.ERROR;
payload: {
key: string,
errorMessage: string
};
/**
* Create a new RequestCacheErrorAction
*
* @param key
* the key under which cache this request is cached,
* should be identical to the one used in the corresponding
* find action
* @param errorMessage
* A message describing the reason the request failed
*/
constructor(key: string, errorMessage: string) {
this.payload = {
key,
errorMessage
};
}
}
/**
* An ngrx action to remove a request from the cache
*/
export class RequestCacheRemoveAction implements Action {
type = RequestCacheActionTypes.REMOVE;
payload: string;
/**
* Create a new RequestCacheRemoveAction
* @param key
* The key of the request to remove
*/
constructor(key: string) {
this.payload = key;
}
}
/**
* An ngrx action to reset the timeAdded property of all cached objects
*/
export class ResetRequestCacheTimestampsAction implements Action {
type = RequestCacheActionTypes.RESET_TIMESTAMPS;
payload: number;
/**
* Create a new ResetObjectCacheTimestampsAction
*
* @param newTimestamp
* the new timeAdded all objects should get
*/
constructor(newTimestamp: number) {
this.payload = newTimestamp;
}
}
/**
* A type to encompass all RequestCacheActions
*/
export type RequestCacheAction
= RequestCacheFindAllAction
| RequestCacheFindByIDAction
| RequestCacheSuccessAction
| RequestCacheErrorAction
| RequestCacheRemoveAction
| ResetRequestCacheTimestampsAction;

View File

@@ -1,227 +0,0 @@
import { requestCacheReducer, RequestCacheState } from "./request-cache.reducer";
import {
RequestCacheRemoveAction, RequestCacheFindByIDAction,
RequestCacheFindAllAction, RequestCacheSuccessAction, RequestCacheErrorAction,
ResetRequestCacheTimestampsAction
} from "./request-cache.actions";
import deepFreeze = require("deep-freeze");
import { OpaqueToken } from "@angular/core";
class NullAction extends RequestCacheRemoveAction {
type = null;
payload = null;
constructor() {
super(null);
}
}
describe("requestCacheReducer", () => {
const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
const services = [new OpaqueToken('service1'), new OpaqueToken('service2')];
const msToLive = 900000;
const uuids = [
"9e32a2e2-6b91-4236-a361-995ccdc14c60",
"598ce822-c357-46f3-ab70-63724d02d6ad",
"be8325f7-243b-49f4-8a4b-df2b793ff3b5"
];
const resourceID = "9978";
const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
const sortOptions = { "field": "id", "direction": 0 };
const testState = {
[keys[0]]: {
"key": keys[0],
"service": services[0],
"resourceUUIDs": [uuids[0], uuids[1]],
"isLoading": false,
"paginationOptions": paginationOptions,
"sortOptions": sortOptions,
"timeAdded": new Date().getTime(),
"msToLive": msToLive
},
[keys[1]]: {
"key": keys[1],
"service": services[1],
"resourceID": resourceID,
"resourceUUIDs": [uuids[2]],
"isLoading": false,
"timeAdded": new Date().getTime(),
"msToLive": msToLive
}
};
deepFreeze(testState);
const errorState: {} = {
[keys[0]]: {
errorMessage: 'error',
resourceUUIDs: uuids
}
};
deepFreeze(errorState);
it("should return the current state when no valid actions have been made", () => {
const action = new NullAction();
const newState = requestCacheReducer(testState, action);
expect(newState).toEqual(testState);
});
it("should start with an empty cache", () => {
const action = new NullAction();
const initialState = requestCacheReducer(undefined, action);
expect(initialState).toEqual(Object.create(null));
});
describe("FIND_BY_ID", () => {
const action = new RequestCacheFindByIDAction(keys[0], services[0], resourceID);
it("should perform the action without affecting the previous state", () => {
//testState has already been frozen above
requestCacheReducer(testState, action);
});
it("should add the request to the cache", () => {
const state = Object.create(null);
const newState = requestCacheReducer(state, action);
expect(newState[keys[0]].key).toBe(keys[0]);
expect(newState[keys[0]].service).toEqual(services[0]);
expect(newState[keys[0]].resourceID).toBe(resourceID);
});
it("should set isLoading to true", () => {
const state = Object.create(null);
const newState = requestCacheReducer(state, action);
expect(newState[keys[0]].isLoading).toBe(true);
});
it("should remove any previous error message or resourceUUID for the request", () => {
const newState = requestCacheReducer(errorState, action);
expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
expect(newState[keys[0]].errorMessage).toBeUndefined();
});
});
describe("FIND_ALL", () => {
const action = new RequestCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions);
it("should perform the action without affecting the previous state", () => {
//testState has already been frozen above
requestCacheReducer(testState, action);
});
it("should add the request to the cache", () => {
const state = Object.create(null);
const newState = requestCacheReducer(state, action);
expect(newState[keys[0]].key).toBe(keys[0]);
expect(newState[keys[0]].service).toEqual(services[0]);
expect(newState[keys[0]].scopeID).toBe(resourceID);
expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions);
expect(newState[keys[0]].sortOptions).toEqual(sortOptions);
});
it("should set isLoading to true", () => {
const state = Object.create(null);
const newState = requestCacheReducer(state, action);
expect(newState[keys[0]].isLoading).toBe(true);
});
it("should remove any previous error message or resourceUUIDs for the request", () => {
const newState = requestCacheReducer(errorState, action);
expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
expect(newState[keys[0]].errorMessage).toBeUndefined();
});
});
describe("SUCCESS", () => {
const successUUIDs = [uuids[0], uuids[2]];
const successTimeAdded = new Date().getTime();
const successMsToLive = 5;
const action = new RequestCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive);
it("should perform the action without affecting the previous state", () => {
//testState has already been frozen above
requestCacheReducer(testState, action);
});
it("should add the response to the cached request", () => {
const newState = requestCacheReducer(testState, action);
expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs);
expect(newState[keys[0]].timeAdded).toBe(successTimeAdded);
expect(newState[keys[0]].msToLive).toBe(successMsToLive);
});
it("should set isLoading to false", () => {
const newState = requestCacheReducer(testState, action);
expect(newState[keys[0]].isLoading).toBe(false);
});
it("should remove any previous error message for the request", () => {
const newState = requestCacheReducer(errorState, action);
expect(newState[keys[0]].errorMessage).toBeUndefined();
});
});
describe("ERROR", () => {
const errorMsg = 'errorMsg';
const action = new RequestCacheErrorAction(keys[0], errorMsg);
it("should perform the action without affecting the previous state", () => {
//testState has already been frozen above
requestCacheReducer(testState, action);
});
it("should set an error message for the request", () => {
const newState = requestCacheReducer(errorState, action);
expect(newState[keys[0]].errorMessage).toBe(errorMsg);
});
it("should set isLoading to false", () => {
const newState = requestCacheReducer(testState, action);
expect(newState[keys[0]].isLoading).toBe(false);
});
});
describe("REMOVE", () => {
it("should perform the action without affecting the previous state", () => {
const action = new RequestCacheRemoveAction(keys[0]);
//testState has already been frozen above
requestCacheReducer(testState, action);
});
it("should remove the specified request from the cache", () => {
const action = new RequestCacheRemoveAction(keys[0]);
const newState = requestCacheReducer(testState, action);
expect(testState[keys[0]]).not.toBeUndefined();
expect(newState[keys[0]]).toBeUndefined();
});
it("shouldn't do anything when the specified key isn't cached", () => {
const wrongKey = "this isn't cached";
const action = new RequestCacheRemoveAction(wrongKey);
const newState = requestCacheReducer(testState, action);
expect(testState[wrongKey]).toBeUndefined();
expect(newState).toEqual(testState);
});
});
describe("RESET_TIMESTAMPS", () => {
const newTimeStamp = new Date().getTime();
const action = new ResetRequestCacheTimestampsAction(newTimeStamp);
it("should perform the action without affecting the previous state", () => {
//testState has already been frozen above
requestCacheReducer(testState, action);
});
it("should set the timestamp of all requests in the cache", () => {
const newState = requestCacheReducer(testState, action);
Object.keys(newState).forEach((key) => {
expect(newState[key].timeAdded).toEqual(newTimeStamp);
});
});
});
});

View File

@@ -1,212 +0,0 @@
import { PaginationOptions } from "../shared/pagination-options.model";
import { SortOptions } from "../shared/sort-options.model";
import {
RequestCacheAction, RequestCacheActionTypes, RequestCacheFindAllAction,
RequestCacheSuccessAction, RequestCacheErrorAction, RequestCacheFindByIDAction,
RequestCacheRemoveAction, ResetRequestCacheTimestampsAction
} from "./request-cache.actions";
import { OpaqueToken } from "@angular/core";
import { CacheEntry } from "./cache-entry";
import { hasValue } from "../../shared/empty.util";
/**
* An entry in the RequestCache
*/
export class RequestCacheEntry implements CacheEntry {
service: OpaqueToken;
key: string;
scopeID: string;
resourceID: string;
resourceUUIDs: Array<String>;
resourceType: String;
isLoading: boolean;
errorMessage: string;
paginationOptions: PaginationOptions;
sortOptions: SortOptions;
timeAdded: number;
msToLive: number;
}
/**
* The RequestCache State
*/
export interface RequestCacheState {
[key: string]: RequestCacheEntry
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null);
/**
* The RequestCache Reducer
*
* @param state
* the current state
* @param action
* the action to perform on the state
* @return RequestCacheState
* the new state
*/
export const requestCacheReducer = (state = initialState, action: RequestCacheAction): RequestCacheState => {
switch (action.type) {
case RequestCacheActionTypes.FIND_ALL: {
return findAllRequest(state, <RequestCacheFindAllAction> action);
}
case RequestCacheActionTypes.FIND_BY_ID: {
return findByIDRequest(state, <RequestCacheFindByIDAction> action);
}
case RequestCacheActionTypes.SUCCESS: {
return success(state, <RequestCacheSuccessAction> action);
}
case RequestCacheActionTypes.ERROR: {
return error(state, <RequestCacheErrorAction> action);
}
case RequestCacheActionTypes.REMOVE: {
return removeFromCache(state, <RequestCacheRemoveAction> action);
}
case RequestCacheActionTypes.RESET_TIMESTAMPS: {
return resetRequestCacheTimestamps(state, <ResetRequestCacheTimestampsAction>action)
}
default: {
return state;
}
}
};
/**
* Add a FindAll request to the cache
*
* @param state
* the current state
* @param action
* a RequestCacheFindAllAction
* @return RequestCacheState
* the new state, with the request added, or overwritten
*/
function findAllRequest(state: RequestCacheState, action: RequestCacheFindAllAction): RequestCacheState {
return Object.assign({}, state, {
[action.payload.key]: {
key: action.payload.key,
service: action.payload.service,
scopeID: action.payload.scopeID,
resourceUUIDs: [],
isLoading: true,
errorMessage: undefined,
paginationOptions: action.payload.paginationOptions,
sortOptions: action.payload.sortOptions
}
});
}
/**
* Add a FindByID request to the cache
*
* @param state
* the current state
* @param action
* a RequestCacheFindByIDAction
* @return RequestCacheState
* the new state, with the request added, or overwritten
*/
function findByIDRequest(state: RequestCacheState, action: RequestCacheFindByIDAction): RequestCacheState {
return Object.assign({}, state, {
[action.payload.key]: {
key: action.payload.key,
service: action.payload.service,
resourceID: action.payload.resourceID,
resourceUUIDs: [],
isLoading: true,
errorMessage: undefined,
}
});
}
/**
* Update a cached request with a successful response
*
* @param state
* the current state
* @param action
* a RequestCacheSuccessAction
* @return RequestCacheState
* the new state, with the response added to the request
*/
function success(state: RequestCacheState, action: RequestCacheSuccessAction): RequestCacheState {
return Object.assign({}, state, {
[action.payload.key]: Object.assign({}, state[action.payload.key], {
isLoading: false,
resourceUUIDs: action.payload.resourceUUIDs,
errorMessage: undefined,
timeAdded: action.payload.timeAdded,
msToLive: action.payload.msToLive
})
});
}
/**
* Update a cached request with an error
*
* @param state
* the current state
* @param action
* a RequestCacheSuccessAction
* @return RequestCacheState
* the new state, with the error added to the request
*/
function error(state: RequestCacheState, action: RequestCacheErrorAction): RequestCacheState {
return Object.assign({}, state, {
[action.payload.key]: Object.assign({}, state[action.payload.key], {
isLoading: false,
errorMessage: action.payload.errorMessage
})
});
}
/**
* Remove a request from the cache
*
* @param state
* the current state
* @param action
* an RequestCacheRemoveAction
* @return RequestCacheState
* the new state, with the request removed if it existed.
*/
function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAction): RequestCacheState {
if (hasValue(state[action.payload])) {
let newCache = Object.assign({}, state);
delete newCache[action.payload];
return newCache;
}
else {
return state;
}
}
/**
* Set the timeAdded timestamp of every cached request to the specified value
*
* @param state
* the current state
* @param action
* a ResetRequestCacheTimestampsAction
* @return RequestCacheState
* the new state, with all timeAdded timestamps set to the specified value
*/
function resetRequestCacheTimestamps(state: RequestCacheState, action: ResetRequestCacheTimestampsAction): RequestCacheState {
let newState = Object.create(null);
Object.keys(state).forEach(key => {
newState[key] = Object.assign({}, state[key], {
timeAdded: action.payload
});
});
return newState;
}

View File

@@ -1,147 +0,0 @@
import { RequestCacheService } from "./request-cache.service";
import { Store } from "@ngrx/store";
import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer";
import { OpaqueToken } from "@angular/core";
import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "./request-cache.actions";
import { Observable } from "rxjs";
describe("RequestCacheService", () => {
let service: RequestCacheService;
let store: Store<RequestCacheState>;
const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')];
const resourceID = "9978";
const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
const sortOptions = { "field": "id", "direction": 0 };
const timestamp = new Date().getTime();
const validCacheEntry = (key) => {
return {
key: key,
timeAdded: timestamp,
msToLive: 24 * 60 * 60 * 1000 // a day
}
};
const invalidCacheEntry = (key) => {
return {
key: key,
timeAdded: 0,
msToLive: 0
}
};
beforeEach(() => {
store = new Store<RequestCacheState>(undefined, undefined, undefined);
spyOn(store, 'dispatch');
service = new RequestCacheService(store);
spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
});
describe("findAll", () => {
beforeEach(() => {
spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
});
describe("if the key isn't cached", () => {
beforeEach(() => {
spyOn(service, "has").and.returnValue(false);
});
it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => {
service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
expect(store.dispatch).toHaveBeenCalledWith(new RequestCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions))
});
it("should return an observable of the newly cached request with the specified key", () => {
let result: RequestCacheEntry;
service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
expect(result.key).toEqual(keys[0]);
});
});
describe("if the key is already cached", () => {
beforeEach(() => {
spyOn(service, "has").and.returnValue(true);
});
it("shouldn't dispatch anything", () => {
service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
expect(store.dispatch).not.toHaveBeenCalled();
});
it("should return an observable of the existing cached request with the specified key", () => {
let result: RequestCacheEntry;
service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
expect(result.key).toEqual(keys[0]);
});
});
});
describe("findById", () => {
beforeEach(() => {
spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
});
describe("if the key isn't cached", () => {
beforeEach(() => {
spyOn(service, "has").and.returnValue(false);
});
it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => {
service.findById(keys[0], serviceTokens[0], resourceID);
expect(store.dispatch).toHaveBeenCalledWith(new RequestCacheFindByIDAction(keys[0], serviceTokens[0], resourceID))
});
it("should return an observable of the newly cached request with the specified key", () => {
let result: RequestCacheEntry;
service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
expect(result.key).toEqual(keys[0]);
});
});
describe("if the key is already cached", () => {
beforeEach(() => {
spyOn(service, "has").and.returnValue(true);
});
it("shouldn't dispatch anything", () => {
service.findById(keys[0], serviceTokens[0], resourceID);
expect(store.dispatch).not.toHaveBeenCalled();
});
it("should return an observable of the existing cached request with the specified key", () => {
let result: RequestCacheEntry;
service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
expect(result.key).toEqual(keys[0]);
});
});
});
describe("get", () => {
it("should return an observable of the cached request with the specified key", () => {
spyOn(store, "select").and.callFake((...args:Array<any>) => {
return Observable.of(validCacheEntry(args[args.length - 1]));
});
let testObj: RequestCacheEntry;
service.get(keys[1]).take(1).subscribe(entry => testObj = entry);
expect(testObj.key).toEqual(keys[1]);
});
it("should not return a cached request that has exceeded its time to live", () => {
spyOn(store, "select").and.callFake((...args:Array<any>) => {
return Observable.of(invalidCacheEntry(args[args.length - 1]));
});
let getObsHasFired = false;
const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true);
expect(getObsHasFired).toBe(false);
subscription.unsubscribe();
});
});
describe("has", () => {
it("should return true if the request with the supplied key is cached and still valid", () => {
spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1])));
expect(service.has(keys[1])).toBe(true);
});
it("should return false if the request with the supplied key isn't cached", () => {
spyOn(store, 'select').and.returnValue(Observable.of(undefined));
expect(service.has(keys[1])).toBe(false);
});
it("should return false if the request with the supplied key is cached but has exceeded its time to live", () => {
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
expect(service.has(keys[1])).toBe(false);
});
});
});

View File

@@ -1,138 +0,0 @@
import { Injectable, OpaqueToken } from "@angular/core";
import { Store } from "@ngrx/store";
import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer";
import { Observable } from "rxjs";
import { hasNoValue } from "../../shared/empty.util";
import {
RequestCacheRemoveAction, RequestCacheFindAllAction,
RequestCacheFindByIDAction
} from "./request-cache.actions";
import { SortOptions } from "../shared/sort-options.model";
import { PaginationOptions } from "../shared/pagination-options.model";
/**
* A service to interact with the request cache
*/
@Injectable()
export class RequestCacheService {
constructor(
private store: Store<RequestCacheState>
) {}
/**
* Start a new findAll request
*
* This will send a new findAll request to the backend,
* and store the request parameters and the fact that
* the request is pending
*
* @param key
* the key should be a unique identifier for the request and its parameters
* @param service
* the service that initiated the request
* @param scopeID
* the id of an optional scope object
* @param paginationOptions
* the pagination options (optional)
* @param sortOptions
* the sort options (optional)
* @return Observable<RequestCacheEntry>
* an observable of the RequestCacheEntry for this request
*/
findAll(
key: string,
service: OpaqueToken,
scopeID?: string,
paginationOptions?: PaginationOptions,
sortOptions?: SortOptions
): Observable<RequestCacheEntry> {
if (!this.has(key)) {
this.store.dispatch(new RequestCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
}
return this.get(key);
}
/**
* Start a new findById request
*
* This will send a new findById request to the backend,
* and store the request parameters and the fact that
* the request is pending
*
* @param key
* the key should be a unique identifier for the request and its parameters
* @param service
* the service that initiated the request
* @param resourceID
* the ID of the resource to find
* @return Observable<RequestCacheEntry>
* an observable of the RequestCacheEntry for this request
*/
findById(
key: string,
service: OpaqueToken,
resourceID: string
): Observable<RequestCacheEntry> {
if (!this.has(key)) {
this.store.dispatch(new RequestCacheFindByIDAction(key, service, resourceID));
}
return this.get(key);
}
/**
* Get an observable of the request with the specified key
*
* @param key
* the key of the request to get
* @return Observable<RequestCacheEntry>
* an observable of the RequestCacheEntry with the specified key
*/
get(key: string): Observable<RequestCacheEntry> {
return this.store.select<RequestCacheEntry>('core', 'cache', 'request', key)
.filter(entry => this.isValid(entry))
.distinctUntilChanged()
}
/**
* Check whether the request with the specified key is cached
*
* @param key
* the key of the request to check
* @return boolean
* true if the request with the specified key is cached,
* false otherwise
*/
has(key: string): boolean {
let result: boolean;
this.store.select<RequestCacheEntry>('core', 'cache', 'request', key)
.take(1)
.subscribe(entry => result = this.isValid(entry));
return result;
}
/**
* Check whether a RequestCacheEntry should still be cached
*
* @param entry
* the entry to check
* @return boolean
* false if the entry is null, undefined, or its time to
* live has been exceeded, true otherwise
*/
private isValid(entry: RequestCacheEntry): boolean {
if (hasNoValue(entry)) {
return false;
}
else {
const timeOutdated = entry.timeAdded + entry.msToLive;
const isOutDated = new Date().getTime() > timeOutdated;
if (isOutDated) {
this.store.dispatch(new RequestCacheRemoveAction(entry.key));
}
return !isOutDated;
}
}
}

View File

@@ -0,0 +1,69 @@
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
import { Response } from "./response-cache.models";
/**
* The list of ResponseCacheAction type definitions
*/
export const ResponseCacheActionTypes = {
ADD: type('dspace/core/cache/response/ADD'),
REMOVE: type('dspace/core/cache/response/REMOVE'),
RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS')
};
export class ResponseCacheAddAction implements Action {
type = ResponseCacheActionTypes.ADD;
payload: {
key: string,
response: Response
timeAdded: number;
msToLive: number;
};
constructor(key: string, response: Response, timeAdded: number, msToLive: number) {
this.payload = { key, response, timeAdded, msToLive };
}
}
/**
* An ngrx action to remove a request from the cache
*/
export class ResponseCacheRemoveAction implements Action {
type = ResponseCacheActionTypes.REMOVE;
payload: string;
/**
* Create a new ResponseCacheRemoveAction
* @param key
* The key of the request to remove
*/
constructor(key: string) {
this.payload = key;
}
}
/**
* An ngrx action to reset the timeAdded property of all cached objects
*/
export class ResetResponseCacheTimestampsAction implements Action {
type = ResponseCacheActionTypes.RESET_TIMESTAMPS;
payload: number;
/**
* Create a new ResetObjectCacheTimestampsAction
*
* @param newTimestamp
* the new timeAdded all objects should get
*/
constructor(newTimestamp: number) {
this.payload = newTimestamp;
}
}
/**
* A type to encompass all ResponseCacheActions
*/
export type ResponseCacheAction
= ResponseCacheAddAction
| ResponseCacheRemoveAction
| ResetResponseCacheTimestampsAction;

View File

@@ -0,0 +1,16 @@
export class Response {
constructor(public isSuccessful: boolean) {}
}
export class SuccessResponse extends Response {
constructor(public resourceUUIDs: Array<String>) {
super(true);
}
}
export class ErrorResponse extends Response {
constructor(public errorMessage: string) {
super(false);
}
}

View File

@@ -0,0 +1,225 @@
import { responseCacheReducer, ResponseCacheState } from "./response-cache.reducer";
import {
ResponseCacheRemoveAction,
ResetResponseCacheTimestampsAction
} from "./response-cache.actions";
import deepFreeze = require("deep-freeze");
class NullAction extends ResponseCacheRemoveAction {
type = null;
payload = null;
constructor() {
super(null);
}
}
// describe("responseCacheReducer", () => {
// const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
// const services = [new OpaqueToken('service1'), new OpaqueToken('service2')];
// const msToLive = 900000;
// const uuids = [
// "9e32a2e2-6b91-4236-a361-995ccdc14c60",
// "598ce822-c357-46f3-ab70-63724d02d6ad",
// "be8325f7-243b-49f4-8a4b-df2b793ff3b5"
// ];
// const resourceID = "9978";
// const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
// const sortOptions = { "field": "id", "direction": 0 };
// const testState = {
// [keys[0]]: {
// "key": keys[0],
// "service": services[0],
// "resourceUUIDs": [uuids[0], uuids[1]],
// "isLoading": false,
// "paginationOptions": paginationOptions,
// "sortOptions": sortOptions,
// "timeAdded": new Date().getTime(),
// "msToLive": msToLive
// },
// [keys[1]]: {
// "key": keys[1],
// "service": services[1],
// "resourceID": resourceID,
// "resourceUUIDs": [uuids[2]],
// "isLoading": false,
// "timeAdded": new Date().getTime(),
// "msToLive": msToLive
// }
// };
// deepFreeze(testState);
// const errorState: {} = {
// [keys[0]]: {
// errorMessage: 'error',
// resourceUUIDs: uuids
// }
// };
// deepFreeze(errorState);
//
//
// it("should return the current state when no valid actions have been made", () => {
// const action = new NullAction();
// const newState = responseCacheReducer(testState, action);
//
// expect(newState).toEqual(testState);
// });
//
// it("should start with an empty cache", () => {
// const action = new NullAction();
// const initialState = responseCacheReducer(undefined, action);
//
// expect(initialState).toEqual(Object.create(null));
// });
//
// describe("FIND_BY_ID", () => {
// const action = new ResponseCacheFindByIDAction(keys[0], services[0], resourceID);
//
// it("should perform the action without affecting the previous state", () => {
// //testState has already been frozen above
// responseCacheReducer(testState, action);
// });
//
// it("should add the request to the cache", () => {
// const state = Object.create(null);
// const newState = responseCacheReducer(state, action);
// expect(newState[keys[0]].key).toBe(keys[0]);
// expect(newState[keys[0]].service).toEqual(services[0]);
// expect(newState[keys[0]].resourceID).toBe(resourceID);
// });
//
// it("should set responsePending to true", () => {
// const state = Object.create(null);
// const newState = responseCacheReducer(state, action);
// expect(newState[keys[0]].responsePending).toBe(true);
// });
//
// it("should remove any previous error message or resourceUUID for the request", () => {
// const newState = responseCacheReducer(errorState, action);
// expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
// expect(newState[keys[0]].errorMessage).toBeUndefined();
// });
// });
//
// describe("FIND_ALL", () => {
// const action = new ResponseCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions);
//
// it("should perform the action without affecting the previous state", () => {
// //testState has already been frozen above
// responseCacheReducer(testState, action);
// });
//
// it("should add the request to the cache", () => {
// const state = Object.create(null);
// const newState = responseCacheReducer(state, action);
// expect(newState[keys[0]].key).toBe(keys[0]);
// expect(newState[keys[0]].service).toEqual(services[0]);
// expect(newState[keys[0]].scopeID).toBe(resourceID);
// expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions);
// expect(newState[keys[0]].sortOptions).toEqual(sortOptions);
// });
//
// it("should set responsePending to true", () => {
// const state = Object.create(null);
// const newState = responseCacheReducer(state, action);
// expect(newState[keys[0]].responsePending).toBe(true);
// });
//
// it("should remove any previous error message or resourceUUIDs for the request", () => {
// const newState = responseCacheReducer(errorState, action);
// expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
// expect(newState[keys[0]].errorMessage).toBeUndefined();
// });
// });
//
// describe("SUCCESS", () => {
// const successUUIDs = [uuids[0], uuids[2]];
// const successTimeAdded = new Date().getTime();
// const successMsToLive = 5;
// const action = new ResponseCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive);
//
// it("should perform the action without affecting the previous state", () => {
// //testState has already been frozen above
// responseCacheReducer(testState, action);
// });
//
// it("should add the response to the cached request", () => {
// const newState = responseCacheReducer(testState, action);
// expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs);
// expect(newState[keys[0]].timeAdded).toBe(successTimeAdded);
// expect(newState[keys[0]].msToLive).toBe(successMsToLive);
// });
//
// it("should set responsePending to false", () => {
// const newState = responseCacheReducer(testState, action);
// expect(newState[keys[0]].responsePending).toBe(false);
// });
//
// it("should remove any previous error message for the request", () => {
// const newState = responseCacheReducer(errorState, action);
// expect(newState[keys[0]].errorMessage).toBeUndefined();
// });
// });
//
// describe("ERROR", () => {
// const errorMsg = 'errorMsg';
// const action = new ResponseCacheErrorAction(keys[0], errorMsg);
//
// it("should perform the action without affecting the previous state", () => {
// //testState has already been frozen above
// responseCacheReducer(testState, action);
// });
//
// it("should set an error message for the request", () => {
// const newState = responseCacheReducer(errorState, action);
// expect(newState[keys[0]].errorMessage).toBe(errorMsg);
// });
//
// it("should set responsePending to false", () => {
// const newState = responseCacheReducer(testState, action);
// expect(newState[keys[0]].responsePending).toBe(false);
// });
// });
//
// describe("REMOVE", () => {
// it("should perform the action without affecting the previous state", () => {
// const action = new ResponseCacheRemoveAction(keys[0]);
// //testState has already been frozen above
// responseCacheReducer(testState, action);
// });
//
// it("should remove the specified request from the cache", () => {
// const action = new ResponseCacheRemoveAction(keys[0]);
// const newState = responseCacheReducer(testState, action);
// expect(testState[keys[0]]).not.toBeUndefined();
// expect(newState[keys[0]]).toBeUndefined();
// });
//
// it("shouldn't do anything when the specified key isn't cached", () => {
// const wrongKey = "this isn't cached";
// const action = new ResponseCacheRemoveAction(wrongKey);
// const newState = responseCacheReducer(testState, action);
// expect(testState[wrongKey]).toBeUndefined();
// expect(newState).toEqual(testState);
// });
// });
//
// describe("RESET_TIMESTAMPS", () => {
// const newTimeStamp = new Date().getTime();
// const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
//
// it("should perform the action without affecting the previous state", () => {
// //testState has already been frozen above
// responseCacheReducer(testState, action);
// });
//
// it("should set the timestamp of all requests in the cache", () => {
// const newState = responseCacheReducer(testState, action);
// Object.keys(newState).forEach((key) => {
// expect(newState[key].timeAdded).toEqual(newTimeStamp);
// });
// });
//
// });
//
//
// });

View File

@@ -0,0 +1,112 @@
import {
ResponseCacheAction, ResponseCacheActionTypes,
ResponseCacheRemoveAction, ResetResponseCacheTimestampsAction,
ResponseCacheAddAction
} from "./response-cache.actions";
import { CacheEntry } from "./cache-entry";
import { hasValue } from "../../shared/empty.util";
import { Response } from "./response-cache.models";
/**
* An entry in the ResponseCache
*/
export class ResponseCacheEntry implements CacheEntry {
key: string;
response: Response;
timeAdded: number;
msToLive: number;
}
/**
* The ResponseCache State
*/
export interface ResponseCacheState {
[key: string]: ResponseCacheEntry
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null);
/**
* The ResponseCache Reducer
*
* @param state
* the current state
* @param action
* the action to perform on the state
* @return ResponseCacheState
* the new state
*/
export const responseCacheReducer = (state = initialState, action: ResponseCacheAction): ResponseCacheState => {
switch (action.type) {
case ResponseCacheActionTypes.ADD: {
return addToCache(state, <ResponseCacheAddAction> action);
}
case ResponseCacheActionTypes.REMOVE: {
return removeFromCache(state, <ResponseCacheRemoveAction> action);
}
case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
return resetResponseCacheTimestamps(state, <ResetResponseCacheTimestampsAction>action)
}
default: {
return state;
}
}
};
function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState {
return Object.assign({}, state, {
[action.payload.key]: {
key: action.payload.key,
response: action.payload.response,
timeAdded: action.payload.timeAdded,
msToLive: action.payload.msToLive
}
});
}
/**
* Remove a request from the cache
*
* @param state
* the current state
* @param action
* an ResponseCacheRemoveAction
* @return ResponseCacheState
* the new state, with the request removed if it existed.
*/
function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState {
if (hasValue(state[action.payload])) {
let newCache = Object.assign({}, state);
delete newCache[action.payload];
return newCache;
}
else {
return state;
}
}
/**
* Set the timeAdded timestamp of every cached request to the specified value
*
* @param state
* the current state
* @param action
* a ResetResponseCacheTimestampsAction
* @return ResponseCacheState
* the new state, with all timeAdded timestamps set to the specified value
*/
function resetResponseCacheTimestamps(state: ResponseCacheState, action: ResetResponseCacheTimestampsAction): ResponseCacheState {
let newState = Object.create(null);
Object.keys(state).forEach(key => {
newState[key] = Object.assign({}, state[key], {
timeAdded: action.payload
});
});
return newState;
}

View File

@@ -0,0 +1,146 @@
import { ResponseCacheService } from "./response-cache.service";
import { Store } from "@ngrx/store";
import { ResponseCacheState, ResponseCacheEntry } from "./response-cache.reducer";
import { OpaqueToken } from "@angular/core";
import { Observable } from "rxjs";
// describe("ResponseCacheService", () => {
// let service: ResponseCacheService;
// let store: Store<ResponseCacheState>;
//
// const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
// const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')];
// const resourceID = "9978";
// const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
// const sortOptions = { "field": "id", "direction": 0 };
// const timestamp = new Date().getTime();
// const validCacheEntry = (key) => {
// return {
// key: key,
// timeAdded: timestamp,
// msToLive: 24 * 60 * 60 * 1000 // a day
// }
// };
// const invalidCacheEntry = (key) => {
// return {
// key: key,
// timeAdded: 0,
// msToLive: 0
// }
// };
//
// beforeEach(() => {
// store = new Store<ResponseCacheState>(undefined, undefined, undefined);
// spyOn(store, 'dispatch');
// service = new ResponseCacheService(store);
// spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
// });
//
// describe("findAll", () => {
// beforeEach(() => {
// spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
// });
// describe("if the key isn't cached", () => {
// beforeEach(() => {
// spyOn(service, "has").and.returnValue(false);
// });
// it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => {
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
// expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions))
// });
// it("should return an observable of the newly cached request with the specified key", () => {
// let result: ResponseCacheEntry;
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
// expect(result.key).toEqual(keys[0]);
// });
// });
// describe("if the key is already cached", () => {
// beforeEach(() => {
// spyOn(service, "has").and.returnValue(true);
// });
// it("shouldn't dispatch anything", () => {
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
// expect(store.dispatch).not.toHaveBeenCalled();
// });
// it("should return an observable of the existing cached request with the specified key", () => {
// let result: ResponseCacheEntry;
// service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
// expect(result.key).toEqual(keys[0]);
// });
// });
// });
//
// describe("findById", () => {
// beforeEach(() => {
// spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
// });
// describe("if the key isn't cached", () => {
// beforeEach(() => {
// spyOn(service, "has").and.returnValue(false);
// });
// it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => {
// service.findById(keys[0], serviceTokens[0], resourceID);
// expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindByIDAction(keys[0], serviceTokens[0], resourceID))
// });
// it("should return an observable of the newly cached request with the specified key", () => {
// let result: ResponseCacheEntry;
// service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
// expect(result.key).toEqual(keys[0]);
// });
// });
// describe("if the key is already cached", () => {
// beforeEach(() => {
// spyOn(service, "has").and.returnValue(true);
// });
// it("shouldn't dispatch anything", () => {
// service.findById(keys[0], serviceTokens[0], resourceID);
// expect(store.dispatch).not.toHaveBeenCalled();
// });
// it("should return an observable of the existing cached request with the specified key", () => {
// let result: ResponseCacheEntry;
// service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
// expect(result.key).toEqual(keys[0]);
// });
// });
// });
//
// describe("get", () => {
// it("should return an observable of the cached request with the specified key", () => {
// spyOn(store, "select").and.callFake((...args:Array<any>) => {
// return Observable.of(validCacheEntry(args[args.length - 1]));
// });
//
// let testObj: ResponseCacheEntry;
// service.get(keys[1]).take(1).subscribe(entry => testObj = entry);
// expect(testObj.key).toEqual(keys[1]);
// });
//
// it("should not return a cached request that has exceeded its time to live", () => {
// spyOn(store, "select").and.callFake((...args:Array<any>) => {
// return Observable.of(invalidCacheEntry(args[args.length - 1]));
// });
//
// let getObsHasFired = false;
// const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true);
// expect(getObsHasFired).toBe(false);
// subscription.unsubscribe();
// });
// });
//
// describe("has", () => {
// it("should return true if the request with the supplied key is cached and still valid", () => {
// spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1])));
// expect(service.has(keys[1])).toBe(true);
// });
//
// it("should return false if the request with the supplied key isn't cached", () => {
// spyOn(store, 'select').and.returnValue(Observable.of(undefined));
// expect(service.has(keys[1])).toBe(false);
// });
//
// it("should return false if the request with the supplied key is cached but has exceeded its time to live", () => {
// spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
// expect(service.has(keys[1])).toBe(false);
// });
// });
// });

View File

@@ -0,0 +1,89 @@
import { Injectable } from "@angular/core";
import { Store } from "@ngrx/store";
import {
ResponseCacheState, ResponseCacheEntry
} from "./response-cache.reducer";
import { Observable } from "rxjs";
import { hasNoValue } from "../../shared/empty.util";
import {
ResponseCacheRemoveAction,
ResponseCacheAddAction
} from "./response-cache.actions";
import { Response } from "./response-cache.models";
/**
* A service to interact with the response cache
*/
@Injectable()
export class ResponseCacheService {
constructor(
private store: Store<ResponseCacheState>
) {}
add(key: string, response: Response, msToLive: number): Observable<ResponseCacheEntry> {
if (!this.has(key)) {
// this.store.dispatch(new ResponseCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
}
return this.get(key);
}
/**
* Get an observable of the response with the specified key
*
* @param key
* the key of the response to get
* @return Observable<ResponseCacheEntry>
* an observable of the ResponseCacheEntry with the specified key
*/
get(key: string): Observable<ResponseCacheEntry> {
return this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
.filter(entry => this.isValid(entry))
.distinctUntilChanged()
}
/**
* Check whether the response with the specified key is cached
*
* @param key
* the key of the response to check
* @return boolean
* true if the response with the specified key is cached,
* false otherwise
*/
has(key: string): boolean {
let result: boolean;
this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
.take(1)
.subscribe(entry => {
result = this.isValid(entry);
});
return result;
}
/**
* Check whether a ResponseCacheEntry should still be cached
*
* @param entry
* the entry to check
* @return boolean
* false if the entry is null, undefined, or its time to
* live has been exceeded, true otherwise
*/
private isValid(entry: ResponseCacheEntry): boolean {
if (hasNoValue(entry)) {
return false;
}
else {
const timeOutdated = entry.timeAdded + entry.msToLive;
const isOutDated = new Date().getTime() > timeOutdated;
if (isOutDated) {
this.store.dispatch(new ResponseCacheRemoveAction(entry.key));
}
return !isOutDated;
}
}
}

View File

@@ -1,12 +1,11 @@
import { EffectsModule } from "@ngrx/effects";
import { CollectionDataEffects } from "./data-services/collection-data.effects";
import { ItemDataEffects } from "./data-services/item-data.effects";
import { ObjectCacheEffects } from "./data-services/object-cache.effects";
import { RequestCacheEffects } from "./data-services/request-cache.effects";
import { ObjectCacheEffects } from "./data/object-cache.effects";
import { RequestCacheEffects } from "./data/request-cache.effects";
import { HrefIndexEffects } from "./index/href-index.effects";
import { RequestEffects } from "./data/request.effects";
export const coreEffects = [
EffectsModule.run(CollectionDataEffects),
EffectsModule.run(ItemDataEffects),
EffectsModule.run(RequestCacheEffects),
EffectsModule.run(RequestEffects),
EffectsModule.run(ObjectCacheEffects),
EffectsModule.run(HrefIndexEffects),
];

View File

@@ -5,9 +5,10 @@ import { isNotEmpty } from "../shared/empty.util";
import { FooterComponent } from "./footer/footer.component";
import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service";
import { ObjectCacheService } from "./cache/object-cache.service";
import { RequestCacheService } from "./cache/request-cache.service";
import { CollectionDataService } from "./data-services/collection-data.service";
import { ItemDataService } from "./data-services/item-data.service";
import { ResponseCacheService } from "./cache/response-cache.service";
import { CollectionDataService } from "./data/collection-data.service";
import { ItemDataService } from "./data/item-data.service";
import { RequestService } from "./data/request.service";
const IMPORTS = [
CommonModule,
@@ -27,7 +28,8 @@ const PROVIDERS = [
ItemDataService,
DSpaceRESTv2Service,
ObjectCacheService,
RequestCacheService
ResponseCacheService,
RequestService
];
@NgModule({

View File

@@ -1,12 +1,18 @@
import { combineReducers } from "@ngrx/store";
import { CacheState, cacheReducer } from "./cache/cache.reducers";
import { IndexState, indexReducer } from "./index/index.reducers";
import { DataState, dataReducer } from "./data/data.reducers";
export interface CoreState {
cache: CacheState
cache: CacheState,
index: IndexState,
data: DataState
}
export const reducers = {
cache: cacheReducer
cache: cacheReducer,
index: indexReducer,
data: dataReducer
};
export function coreReducer(state: any, action: any) {

View File

@@ -1,38 +0,0 @@
import { Injectable } from "@angular/core";
import { DataEffects } from "./data.effects";
import { Serializer } from "../serializer";
import { Collection } from "../shared/collection.model";
import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
import { ObjectCacheService } from "../cache/object-cache.service";
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
import { Actions, Effect } from "@ngrx/effects";
import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions";
import { CollectionDataService } from "./collection-data.service";
@Injectable()
export class CollectionDataEffects extends DataEffects<Collection> {
constructor(
actions$: Actions,
restApi: DSpaceRESTv2Service,
cache: ObjectCacheService,
dataService: CollectionDataService
) {
super(actions$, restApi, cache, dataService);
}
protected getFindAllEndpoint(action: RequestCacheFindAllAction): string {
return '/collections';
}
protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string {
return `/collections/${action.payload.resourceID}`;
}
protected getSerializer(): Serializer<Collection> {
return new DSpaceRESTv2Serializer(Collection);
}
@Effect() findAll$ = this.findAll;
@Effect() findById$ = this.findById;
}

View File

@@ -1,18 +0,0 @@
import { Injectable, OpaqueToken } from "@angular/core";
import { DataService } from "./data.service";
import { Collection } from "../shared/collection.model";
import { ObjectCacheService } from "../cache/object-cache.service";
import { RequestCacheService } from "../cache/request-cache.service";
@Injectable()
export class CollectionDataService extends DataService<Collection> {
serviceName = new OpaqueToken('CollectionDataService');
constructor(
protected objectCache: ObjectCacheService,
protected requestCache: RequestCacheService,
) {
super(Collection);
}
}

View File

@@ -1,65 +0,0 @@
import { Actions } from "@ngrx/effects";
import { Observable } from "rxjs";
import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
import { ObjectCacheService } from "../cache/object-cache.service";
import { GlobalConfig } from "../../../config";
import { CacheableObject } from "../cache/object-cache.reducer";
import { Serializer } from "../serializer";
import {
RequestCacheActionTypes, RequestCacheFindAllAction, RequestCacheSuccessAction,
RequestCacheErrorAction, RequestCacheFindByIDAction
} from "../cache/request-cache.actions";
import { DataService } from "./data.service";
import { hasNoValue } from "../../shared/empty.util";
export abstract class DataEffects<T extends CacheableObject> {
protected abstract getFindAllEndpoint(action: RequestCacheFindAllAction): string;
protected abstract getFindByIdEndpoint(action: RequestCacheFindByIDAction): string;
protected abstract getSerializer(): Serializer<T>;
constructor(
private actions$: Actions,
private restApi: DSpaceRESTv2Service,
private objectCache: ObjectCacheService,
private dataService: DataService<T>
) {}
// TODO, results of a findall aren't retrieved from cache yet
protected findAll = this.actions$
.ofType(RequestCacheActionTypes.FIND_ALL)
.filter((action: RequestCacheFindAllAction) => action.payload.service === this.dataService.serviceName)
.flatMap((action: RequestCacheFindAllAction) => {
//TODO scope, pagination, sorting -> when we know how that works in rest
return this.restApi.get(this.getFindAllEndpoint(action))
.map((data: DSpaceRESTV2Response) => this.getSerializer().deserializeArray(data))
.do((ts: T[]) => {
ts.forEach((t) => {
if (hasNoValue(t) || hasNoValue(t.uuid)) {
throw new Error('The server returned an invalid object');
}
this.objectCache.add(t, GlobalConfig.cache.msToLive);
});
})
.map((ts: Array<T>) => ts.map(t => t.uuid))
.map((ids: Array<string>) => new RequestCacheSuccessAction(action.payload.key, ids, new Date().getTime(), GlobalConfig.cache.msToLive))
.catch((error: Error) => Observable.of(new RequestCacheErrorAction(action.payload.key, error.message)));
});
protected findById = this.actions$
.ofType(RequestCacheActionTypes.FIND_BY_ID)
.filter((action: RequestCacheFindAllAction) => action.payload.service === this.dataService.serviceName)
.flatMap((action: RequestCacheFindByIDAction) => {
return this.restApi.get(this.getFindByIdEndpoint(action))
.map((data: DSpaceRESTV2Response) => this.getSerializer().deserialize(data))
.do((t: T) => {
if (hasNoValue(t) || hasNoValue(t.uuid)) {
throw new Error('The server returned an invalid object');
}
this.objectCache.add(t, GlobalConfig.cache.msToLive);
})
.map((t: T) => new RequestCacheSuccessAction(action.payload.key, [t.uuid], new Date().getTime(), GlobalConfig.cache.msToLive))
.catch((error: Error) => Observable.of(new RequestCacheErrorAction(action.payload.key, error.message)));
});
}

View File

@@ -1,54 +0,0 @@
import { OpaqueToken } from "@angular/core";
import { Observable } from "rxjs";
import { ObjectCacheService } from "../cache/object-cache.service";
import { RequestCacheService } from "../cache/request-cache.service";
import { CacheableObject } from "../cache/object-cache.reducer";
import { ParamHash } from "../shared/param-hash";
import { isNotEmpty } from "../../shared/empty.util";
import { GenericConstructor } from "../shared/generic-constructor";
import { RemoteData } from "./remote-data";
export abstract class DataService<T extends CacheableObject> {
abstract serviceName: OpaqueToken;
protected abstract objectCache: ObjectCacheService;
protected abstract requestCache: RequestCacheService;
constructor(private modelType: GenericConstructor<T>) {
}
findAll(scopeID?: string): RemoteData<Array<T>> {
const key = new ParamHash(this.serviceName, 'findAll', scopeID).toString();
const requestCacheObs = this.requestCache.findAll(key, this.serviceName, scopeID);
return new RemoteData(
requestCacheObs.map(entry => entry.isLoading).distinctUntilChanged(),
requestCacheObs.map(entry => entry.errorMessage).distinctUntilChanged(),
requestCacheObs
.map(entry => entry.resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => {
// use those IDs to fetch the actual objects from the ObjectCache
return this.objectCache.getList<T>(resourceUUIDs, this.modelType);
}).distinctUntilChanged()
);
}
findById(id: string): RemoteData<T> {
const key = new ParamHash(this.serviceName, 'findById', id).toString();
const requestCacheObs = this.requestCache.findById(key, this.serviceName, id);
return new RemoteData(
requestCacheObs.map(entry => entry.isLoading).distinctUntilChanged(),
requestCacheObs.map(entry => entry.errorMessage).distinctUntilChanged(),
requestCacheObs
.map(entry => entry.resourceUUIDs)
.flatMap((resourceUUIDs: Array<string>) => {
if (isNotEmpty(resourceUUIDs)) {
return this.objectCache.get<T>(resourceUUIDs[0], this.modelType);
}
else {
return Observable.of(undefined);
}
}).distinctUntilChanged()
);
}
}

View File

@@ -1,38 +0,0 @@
import { Injectable } from "@angular/core";
import { DataEffects } from "./data.effects";
import { Serializer } from "../serializer";
import { Item } from "../shared/item.model";
import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
import { ObjectCacheService } from "../cache/object-cache.service";
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
import { Actions, Effect } from "@ngrx/effects";
import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions";
import { ItemDataService } from "./item-data.service";
@Injectable()
export class ItemDataEffects extends DataEffects<Item> {
constructor(
actions$: Actions,
restApi: DSpaceRESTv2Service,
cache: ObjectCacheService,
dataService: ItemDataService
) {
super(actions$, restApi, cache, dataService);
}
protected getFindAllEndpoint(action: RequestCacheFindAllAction): string {
return '/items';
}
protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string {
return `/items/${action.payload.resourceID}`;
}
protected getSerializer(): Serializer<Item> {
return new DSpaceRESTv2Serializer(Item);
}
@Effect() findAll$ = this.findAll;
@Effect() findById$ = this.findById;
}

View File

@@ -1,18 +0,0 @@
import { Injectable, OpaqueToken } from "@angular/core";
import { DataService } from "./data.service";
import { Item } from "../shared/item.model";
import { ObjectCacheService } from "../cache/object-cache.service";
import { RequestCacheService } from "../cache/request-cache.service";
@Injectable()
export class ItemDataService extends DataService<Item> {
serviceName = new OpaqueToken('ItemDataService');
constructor(
protected objectCache: ObjectCacheService,
protected requestCache: RequestCacheService,
) {
super(Item);
}
}

View File

@@ -0,0 +1,45 @@
import { Injectable } from "@angular/core";
import { DataService } from "./data.service";
import { Collection } from "../shared/collection.model";
import { ObjectCacheService } from "../cache/object-cache.service";
import { ResponseCacheService } from "../cache/response-cache.service";
import { Store } from "@ngrx/store";
import { NormalizedCollection } from "../cache/models/normalized-collection.model";
import { CoreState } from "../core.reducers";
import { RequestService } from "./request.service";
import { CollectionListRDBuilder, CollectionRDBuilder } from "../cache/models/collection-builder";
@Injectable()
export class CollectionDataService extends DataService<Collection, NormalizedCollection> {
protected endpoint = '/collections';
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>
) {
super(NormalizedCollection);
}
protected getListDataBuilder(href: string): CollectionListRDBuilder {
return new CollectionListRDBuilder (
this.objectCache,
this.responseCache,
this.requestService,
this.store,
href,
);
}
protected getSingleDataBuilder(href: string): CollectionRDBuilder {
return new CollectionRDBuilder (
this.objectCache,
this.responseCache,
this.requestService,
this.store,
href,
);
}
}

View File

@@ -0,0 +1,14 @@
import { combineReducers } from "@ngrx/store";
import { RequestState, requestReducer } from "./request.reducer";
export interface DataState {
request: RequestState
}
export const reducers = {
request: requestReducer
};
export function dataReducer(state: any, action: any) {
return combineReducers(reducers)(state, action);
}

View File

@@ -0,0 +1,69 @@
import { ObjectCacheService } from "../cache/object-cache.service";
import { ResponseCacheService } from "../cache/response-cache.service";
import { CacheableObject } from "../cache/object-cache.reducer";
import { hasValue } from "../../shared/empty.util";
import { GenericConstructor } from "../shared/generic-constructor";
import { RemoteData } from "./remote-data";
import { FindAllRequest, FindByIDRequest, Request } from "./request.models";
import { Store } from "@ngrx/store";
import { RequestConfigureAction, RequestExecuteAction } from "./request.actions";
import { CoreState } from "../core.reducers";
import { RemoteDataBuilder } from "../cache/models/remote-data-builder";
import { RequestService } from "./request.service";
export abstract class DataService<T, U extends CacheableObject> {
protected abstract objectCache: ObjectCacheService;
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService;
protected abstract store: Store<CoreState>;
protected abstract endpoint: string;
constructor(private normalizedResourceType: GenericConstructor<U>) {
}
protected abstract getListDataBuilder(href: string): RemoteDataBuilder<T[]>;
protected abstract getSingleDataBuilder(href: string): RemoteDataBuilder<T>;
protected getFindAllHref(scopeID?): string {
let result = this.endpoint;
if (hasValue(scopeID)) {
result += `?scope=${scopeID}`
}
return result;
}
findAll(scopeID?: string): RemoteData<Array<T>> {
const href = this.getFindAllHref(scopeID);
if (!this.responseCache.has(href) && !this.requestService.isPending(href)) {
const request = new FindAllRequest(href, this.normalizedResourceType, scopeID);
this.store.dispatch(new RequestConfigureAction(request));
this.store.dispatch(new RequestExecuteAction(href));
}
return this.getListDataBuilder(href).build();
}
protected getFindByIDHref(resourceID): string {
return `${this.endpoint}/${resourceID}`;
}
findById(id: string): RemoteData<T> {
const href = this.getFindByIDHref(id);
if (!this.objectCache.hasBySelfLink(href) && !this.requestService.isPending(href)) {
const request = new FindByIDRequest(href, this.normalizedResourceType, id);
this.store.dispatch(new RequestConfigureAction(request));
this.store.dispatch(new RequestExecuteAction(href));
}
return this.getSingleDataBuilder(href).build();
}
findByHref(href: string): RemoteData<T> {
if (!this.objectCache.hasBySelfLink(href) && !this.requestService.isPending(href)) {
const request = new Request(href, this.normalizedResourceType);
this.store.dispatch(new RequestConfigureAction(request));
this.store.dispatch(new RequestExecuteAction(href));
}
return this.getSingleDataBuilder(href).build();
}
}

View File

@@ -0,0 +1,44 @@
import { Injectable } from "@angular/core";
import { DataService } from "./data.service";
import { Item } from "../shared/item.model";
import { ObjectCacheService } from "../cache/object-cache.service";
import { ResponseCacheService } from "../cache/response-cache.service";
import { Store } from "@ngrx/store";
import { CoreState } from "../core.reducers";
import { NormalizedItem } from "../cache/models/normalized-item.model";
import { RequestService } from "./request.service";
import { ItemListRDBuilder, ItemRDBuilder } from "../cache/models/item-builder";
@Injectable()
export class ItemDataService extends DataService<Item, NormalizedItem> {
protected endpoint = '/items';
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>
) {
super(NormalizedItem);
}
protected getListDataBuilder(href: string): ItemListRDBuilder {
return new ItemListRDBuilder(
this.objectCache,
this.responseCache,
this.requestService,
this.store,
href,
);
}
protected getSingleDataBuilder(href: string): ItemRDBuilder {
return new ItemRDBuilder(
this.objectCache,
this.responseCache,
this.requestService,
this.store,
href,
);
}
}

View File

@@ -2,15 +2,12 @@ import { Injectable } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { StoreActionTypes } from "../../store.actions";
import { ResetObjectCacheTimestampsAction } from "../cache/object-cache.actions";
import { Store } from "@ngrx/store";
import { ObjectCacheState } from "../cache/object-cache.reducer";
@Injectable()
export class ObjectCacheEffects {
constructor(
private actions$: Actions,
private store: Store<ObjectCacheState>
private actions$: Actions
) { }
/**

View File

@@ -1,8 +1,6 @@
import { Observable } from "rxjs";
import { hasValue } from "../../shared/empty.util";
export enum RemoteDataState {
//TODO RequestPending will never happen: implement it in the store & DataEffects.
RequestPending,
ResponsePending,
Failed,
@@ -10,12 +8,14 @@ export enum RemoteDataState {
}
/**
* A class to represent the state of
* A class to represent the state of a remote resource
*/
export class RemoteData<T> {
constructor(
private storeLoading: Observable<boolean>,
public self: string,
private requestPending: Observable<boolean>,
private responsePending: Observable<boolean>,
private isSuccessFul: Observable<boolean>,
public errorMessage: Observable<string>,
public payload: Observable<T>
) {
@@ -23,13 +23,17 @@ export class RemoteData<T> {
get state(): Observable<RemoteDataState> {
return Observable.combineLatest(
this.storeLoading,
this.errorMessage.map(msg => hasValue(msg)),
(storeLoading, hasMsg) => {
if (storeLoading) {
this.requestPending,
this.responsePending,
this.isSuccessFul,
(requestPending, responsePending, isSuccessFul) => {
if (requestPending) {
return RemoteDataState.RequestPending
}
else if (responsePending) {
return RemoteDataState.ResponsePending
}
else if (hasMsg) {
else if (!isSuccessFul) {
return RemoteDataState.Failed
}
else {

View File

@@ -1,16 +1,15 @@
import { Injectable } from "@angular/core";
import { Injectable, Inject } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { ResetRequestCacheTimestampsAction } from "../cache/request-cache.actions";
import { Store } from "@ngrx/store";
import { RequestCacheState } from "../cache/request-cache.reducer";
import { ObjectCacheActionTypes } from "../cache/object-cache.actions";
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
import { ResetResponseCacheTimestampsAction } from "../cache/response-cache.actions";
@Injectable()
export class RequestCacheEffects {
constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
private actions$: Actions,
private store: Store<RequestCacheState>
) { }
/**
@@ -31,6 +30,5 @@ export class RequestCacheEffects {
*/
@Effect() fixTimestampsOnRehydrate = this.actions$
.ofType(ObjectCacheActionTypes.RESET_TIMESTAMPS)
.map(() => new ResetRequestCacheTimestampsAction(new Date().getTime()));
.map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()));
}

View File

@@ -0,0 +1,59 @@
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
import { CacheableObject } from "../cache/object-cache.reducer";
import { Request } from "./request.models";
/**
* The list of RequestAction type definitions
*/
export const RequestActionTypes = {
CONFIGURE: type('dspace/core/data/request/CONFIGURE'),
EXECUTE: type('dspace/core/data/request/EXECUTE'),
COMPLETE: type('dspace/core/data/request/COMPLETE')
};
export class RequestConfigureAction implements Action {
type = RequestActionTypes.CONFIGURE;
payload: Request<CacheableObject>;
constructor(
request: Request<CacheableObject>
) {
this.payload = request;
}
}
export class RequestExecuteAction implements Action {
type = RequestActionTypes.EXECUTE;
payload: string;
constructor(key: string) {
this.payload = key
}
}
/**
* An ngrx action to indicate a response was returned
*/
export class RequestCompleteAction implements Action {
type = RequestActionTypes.COMPLETE;
payload: string;
/**
* Create a new RequestCompleteAction
*
* @param key
* the key under which this request is stored,
*/
constructor(key: string) {
this.payload = key;
}
}
/**
* A type to encompass all RequestActions
*/
export type RequestAction
= RequestConfigureAction
| RequestExecuteAction
| RequestCompleteAction;

View File

@@ -0,0 +1,71 @@
import { Injectable, Inject } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { Store } from "@ngrx/store";
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
import { ObjectCacheService } from "../cache/object-cache.service";
import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
import { CacheableObject } from "../cache/object-cache.reducer";
import { Observable } from "rxjs";
import { Response, SuccessResponse, ErrorResponse } from "../cache/response-cache.models";
import { hasNoValue } from "../../shared/empty.util";
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
import { RequestState, RequestEntry } from "./request.reducer";
import {
RequestActionTypes, RequestExecuteAction,
RequestCompleteAction
} from "./request.actions";
import { ResponseCacheService } from "../cache/response-cache.service";
import { RequestService } from "./request.service";
@Injectable()
export class RequestEffects {
constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
private actions$: Actions,
private restApi: DSpaceRESTv2Service,
private objectCache: ObjectCacheService,
private responseCache: ResponseCacheService,
protected requestService: RequestService,
private store: Store<RequestState>
) { }
@Effect() execute = this.actions$
.ofType(RequestActionTypes.EXECUTE)
.flatMap((action: RequestExecuteAction) => {
return this.requestService.get(action.payload)
.take(1);
})
.flatMap((entry: RequestEntry) => {
const [ifArray, ifNotArray] = this.restApi.get(entry.request.href)
.share() // share ensures restApi.get() doesn't get called twice when the partitions are used below
.partition((data: DSpaceRESTV2Response) => Array.isArray(data._embedded));
return Observable.merge(
ifArray.map((data: DSpaceRESTV2Response) => {
return new DSpaceRESTv2Serializer(entry.request.resourceType).deserializeArray(data);
}).do((cos: CacheableObject[]) => cos.forEach((t) => this.addToObjectCache(t)))
.map((cos: Array<CacheableObject>): Array<string> => cos.map(t => t.uuid)),
ifNotArray.map((data: DSpaceRESTV2Response) => {
return new DSpaceRESTv2Serializer(entry.request.resourceType).deserialize(data);
}).do((co: CacheableObject) => this.addToObjectCache(co))
.map((co: CacheableObject): Array<string> => [co.uuid])
).map((ids: Array<string>) => new SuccessResponse(ids))
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href))
.catch((error: Error) => Observable.of(new ErrorResponse(error.message))
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href)));
});
protected addToObjectCache(co: CacheableObject): void {
if (hasNoValue(co) || hasNoValue(co.uuid)) {
throw new Error('The server returned an invalid object');
}
this.objectCache.add(co, this.EnvConfig.cache.msToLive);
}
}

View File

@@ -0,0 +1,32 @@
import { SortOptions } from "../cache/models/sort-options.model";
import { PaginationOptions } from "../cache/models/pagination-options.model";
import { GenericConstructor } from "../shared/generic-constructor";
export class Request<T> {
constructor(
public href: string,
public resourceType: GenericConstructor<T>
) {}
}
export class FindByIDRequest<T> extends Request<T> {
constructor(
href: string,
resourceType: GenericConstructor<T>,
public resourceID: string
) {
super(href, resourceType);
}
}
export class FindAllRequest<T> extends Request<T> {
constructor(
href: string,
resourceType: GenericConstructor<T>,
public scopeID?: string,
public paginationOptions?: PaginationOptions,
public sortOptions?: SortOptions
) {
super(href, resourceType);
}
}

View File

@@ -0,0 +1,81 @@
import { CacheableObject } from "../cache/object-cache.reducer";
import {
RequestActionTypes, RequestAction, RequestConfigureAction,
RequestExecuteAction, RequestCompleteAction
} from "./request.actions";
import { Request } from "./request.models";
export class RequestEntry {
request: Request<CacheableObject>;
requestPending: boolean;
responsePending: boolean;
completed: boolean;
}
export interface RequestState {
[key: string]: RequestEntry
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null);
export const requestReducer = (state = initialState, action: RequestAction): RequestState => {
switch (action.type) {
case RequestActionTypes.CONFIGURE: {
return configureRequest(state, <RequestConfigureAction> action);
}
case RequestActionTypes.EXECUTE: {
return executeRequest(state, <RequestExecuteAction> action);
}
case RequestActionTypes.COMPLETE: {
return completeRequest(state, <RequestCompleteAction> action);
}
default: {
return state;
}
}
};
function configureRequest(state: RequestState, action: RequestConfigureAction): RequestState {
return Object.assign({}, state, {
[action.payload.href]: {
request: action.payload,
requestPending: true,
responsePending: false,
completed: false
}
});
}
function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState {
return Object.assign({}, state, {
[action.payload]: Object.assign({}, state[action.payload], {
requestPending: false,
responsePending: true
})
});
}
/**
* Update a request with the response
*
* @param state
* the current state
* @param action
* a RequestCompleteAction
* @return RequestState
* the new state, with the response added to the request
*/
function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState {
return Object.assign({}, state, {
[action.payload]: Object.assign({}, state[action.payload], {
responsePending: false,
completed: true
})
});
}

View File

@@ -0,0 +1,27 @@
import { Injectable } from "@angular/core";
import { RequestEntry, RequestState } from "./request.reducer";
import { Store } from "@ngrx/store";
import { hasValue } from "../../shared/empty.util";
import { Observable } from "rxjs/Observable";
@Injectable()
export class RequestService {
constructor(private store: Store<RequestState>) {
}
isPending(href: string): boolean {
let isPending = false;
this.store.select<RequestEntry>('core', 'data', 'request', href)
.take(1)
.subscribe((re: RequestEntry) => {
isPending = (hasValue(re) && !re.completed)
});
return isPending;
}
get(href: string): Observable<RequestEntry> {
return this.store.select<RequestEntry>('core', 'data', 'request', href);
}
}

View File

@@ -140,19 +140,20 @@ describe("DSpaceRESTv2Serializer", () => {
describe("deserializeArray", () => {
it("should turn a valid document describing a collection of objects in to an array of valid models", () => {
const serializer = new DSpaceRESTv2Serializer(TestModel);
const doc = {
"_embedded": testResponses
};
const models = serializer.deserializeArray(doc);
expect(models[0].id).toBe(doc._embedded[0].id);
expect(models[0].name).toBe(doc._embedded[0].name);
expect(models[1].id).toBe(doc._embedded[1].id);
expect(models[1].name).toBe(doc._embedded[1].name);
});
//TODO rewrite to incorporate normalisation.
// it("should turn a valid document describing a collection of objects in to an array of valid models", () => {
// const serializer = new DSpaceRESTv2Serializer(TestModel);
// const doc = {
// "_embedded": testResponses
// };
//
// const models = serializer.deserializeArray(doc);
//
// expect(models[0].id).toBe(doc._embedded[0].id);
// expect(models[0].name).toBe(doc._embedded[0].name);
// expect(models[1].id).toBe(doc._embedded[1].id);
// expect(models[1].name).toBe(doc._embedded[1].name);
// });
//TODO cant implement/test this yet - depends on how relationships
// will be handled in the rest api

View File

@@ -55,7 +55,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
if (Array.isArray(response._embedded)) {
throw new Error('Expected a single model, use deserializeArray() instead');
}
return <T> Deserialize(response._embedded, this.modelType);
let normalized = Object.assign({}, response._embedded, this.normalizeLinks(response._embedded._links));
return <T> Deserialize(normalized, this.modelType);
}
/**
@@ -70,7 +71,26 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
if (!Array.isArray(response._embedded)) {
throw new Error('Expected an Array, use deserialize() instead');
}
return <Array<T>> Deserialize(response._embedded, this.modelType);
let normalized = response._embedded.map((resource) => {
return Object.assign({}, resource, this.normalizeLinks(resource._links));
});
return <Array<T>> Deserialize(normalized, this.modelType);
}
private normalizeLinks(links:any): any {
let normalizedLinks = links;
for (let link in normalizedLinks) {
if (Array.isArray(normalizedLinks[link])) {
normalizedLinks[link] = normalizedLinks[link].map(linkedResource => {
return linkedResource.href;
});
}
else {
normalizedLinks[link] = normalizedLinks[link].href;
}
}
return normalizedLinks;
}
}

View File

@@ -1,14 +1,16 @@
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { Http, RequestOptionsArgs } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
/**
* Service to access DSpace's REST API
*/
@Injectable()
export class DSpaceRESTv2Service {
constructor(public _http: Http) {
constructor(private http: Http, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
}
@@ -23,7 +25,7 @@ export class DSpaceRESTv2Service {
* An Observablse<string> containing the response from the server
*/
get(relativeURL: string, options?: RequestOptionsArgs): Observable<string> {
return this._http.get(new RESTURLCombiner(relativeURL).toString(), options)
return this.http.get(new RESTURLCombiner(this.EnvConfig, relativeURL).toString(), options)
.map(res => res.json())
.catch(err => {
console.log('Error: ', err);

View File

@@ -0,0 +1,58 @@
import { Action } from "@ngrx/store";
import { type } from "../../shared/ngrx/type";
/**
* The list of HrefIndexAction type definitions
*/
export const HrefIndexActionTypes = {
ADD: type('dspace/core/index/href/ADD'),
REMOVE_UUID: type('dspace/core/index/href/REMOVE_UUID')
};
/**
* An ngrx action to add an href to the index
*/
export class AddToHrefIndexAction implements Action {
type = HrefIndexActionTypes.ADD;
payload: {
href: string;
uuid: string;
};
/**
* Create a new AddToHrefIndexAction
*
* @param href
* the href to add
* @param uuid
* the uuid of the resource the href links to
*/
constructor(href: string, uuid: string) {
this.payload = { href, uuid };
}
}
/**
* An ngrx action to remove an href from the index
*/
export class RemoveUUIDFromHrefIndexAction implements Action {
type = HrefIndexActionTypes.REMOVE_UUID;
payload: string;
/**
* Create a new RemoveUUIDFromHrefIndexAction
*
* @param uuid
* the uuid to remove all hrefs for
*/
constructor(uuid: string) {
this.payload = uuid;
}
}
/**
* A type to encompass all HrefIndexActions
*/
export type HrefIndexAction
= AddToHrefIndexAction
| RemoveUUIDFromHrefIndexAction;

View File

@@ -0,0 +1,32 @@
import { Injectable } from "@angular/core";
import { Effect, Actions } from "@ngrx/effects";
import {
ObjectCacheActionTypes, AddToObjectCacheAction,
RemoveFromObjectCacheAction
} from "../cache/object-cache.actions";
import { AddToHrefIndexAction, RemoveUUIDFromHrefIndexAction } from "./href-index.actions";
import { hasValue } from "../../shared/empty.util";
@Injectable()
export class HrefIndexEffects {
constructor(
private actions$: Actions
) { }
@Effect() add$ = this.actions$
.ofType(ObjectCacheActionTypes.ADD)
.filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.self))
.map((action: AddToObjectCacheAction) => {
return new AddToHrefIndexAction(
action.payload.objectToCache.self,
action.payload.objectToCache.uuid
);
});
@Effect() remove$ = this.actions$
.ofType(ObjectCacheActionTypes.REMOVE)
.map((action: RemoveFromObjectCacheAction) => {
return new RemoveUUIDFromHrefIndexAction(action.payload);
});
}

View File

@@ -0,0 +1,43 @@
import {
HrefIndexAction, HrefIndexActionTypes, AddToHrefIndexAction,
RemoveUUIDFromHrefIndexAction
} from "./href-index.actions";
export interface HrefIndexState {
[href: string]: string
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState: HrefIndexState = Object.create(null);
export const hrefIndexReducer = (state = initialState, action: HrefIndexAction): HrefIndexState => {
switch (action.type) {
case HrefIndexActionTypes.ADD: {
return addToHrefIndex(state, <AddToHrefIndexAction>action);
}
case HrefIndexActionTypes.REMOVE_UUID: {
return removeUUIDFromHrefIndex(state, <RemoveUUIDFromHrefIndexAction>action)
}
default: {
return state;
}
}
};
function addToHrefIndex(state: HrefIndexState, action: AddToHrefIndexAction): HrefIndexState {
return Object.assign({}, state, {
[action.payload.href]: action.payload.uuid
});
}
function removeUUIDFromHrefIndex(state: HrefIndexState, action: RemoveUUIDFromHrefIndexAction): HrefIndexState {
let newState = Object.create(null);
for (let href in state) {
if (state[href] !== action.payload) {
newState[href] = state[href];
}
}
return newState;
}

View File

@@ -0,0 +1,14 @@
import { combineReducers } from "@ngrx/store";
import { HrefIndexState, hrefIndexReducer } from "./href-index.reducer";
export interface IndexState {
href: HrefIndexState
}
export const reducers = {
href: hrefIndexReducer
};
export function indexReducer(state: any, action: any) {
return combineReducers(reducers)(state, action);
}

View File

@@ -2,13 +2,14 @@ import { inheritSerialization } from "cerialize";
import { DSpaceObject } from "./dspace-object.model";
import { Bitstream } from "./bitstream.model";
import { Item } from "./item.model";
import { RemoteData } from "../data/remote-data";
@inheritSerialization(DSpaceObject)
export class Bundle extends DSpaceObject {
/**
* The primary bitstream of this Bundle
*/
primaryBitstream: Bitstream;
primaryBitstream: RemoteData<Bitstream>;
/**
* An array of Items that are direct parents of this Bundle
@@ -20,4 +21,6 @@ export class Bundle extends DSpaceObject {
*/
owner: Item;
bitstreams: Array<RemoteData<Bitstream>>
}

View File

@@ -1,14 +1,13 @@
import { autoserialize, inheritSerialization } from "cerialize";
import { DSpaceObject } from "./dspace-object.model";
import { Bitstream } from "./bitstream.model";
import { Item } from "./item.model";
import { RemoteData } from "../data/remote-data";
@inheritSerialization(DSpaceObject)
export class Collection extends DSpaceObject {
/**
* A string representing the unique handle of this Collection
*/
@autoserialize
handle: string;
/**
@@ -54,7 +53,7 @@ export class Collection extends DSpaceObject {
/**
* The Bitstream that represents the logo of this Collection
*/
logo: Bitstream;
logo: RemoteData<Bitstream>;
/**
* An array of Collections that are direct parents of this Collection
@@ -66,4 +65,6 @@ export class Collection extends DSpaceObject {
*/
owner: Collection;
items: Array<RemoteData<Item>>;
}

View File

@@ -8,75 +8,77 @@ import { CacheableObject } from "../cache/object-cache.reducer";
*/
export abstract class DSpaceObject implements CacheableObject {
/**
* The human-readable identifier of this DSpaceObject
*/
@autoserialize
id: string;
@autoserialize
self: string;
/**
* The universally unique identifier of this DSpaceObject
*/
@autoserialize
uuid: string;
/**
* The human-readable identifier of this DSpaceObject
*/
@autoserialize
id: string;
/**
* A string representing the kind of DSpaceObject, e.g. community, item, …
*/
type: string;
/**
* The universally unique identifier of this DSpaceObject
*/
@autoserialize
uuid: string;
/**
* The name for this DSpaceObject
*/
@autoserialize
name: string;
/**
* A string representing the kind of DSpaceObject, e.g. community, item, …
*/
type: string;
/**
* An array containing all metadata of this DSpaceObject
*/
@autoserializeAs(Metadatum)
metadata: Array<Metadatum>;
/**
* The name for this DSpaceObject
*/
@autoserialize
name: string;
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
*/
parents: Array<DSpaceObject>;
/**
* An array containing all metadata of this DSpaceObject
*/
@autoserializeAs(Metadatum)
metadata: Array<Metadatum>;
/**
* The DSpaceObject that owns this DSpaceObject
*/
owner: DSpaceObject;
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
*/
parents: Array<DSpaceObject>;
/**
* Find a metadata field by key and language
*
* This method returns the value of the first element
* in the metadata array that matches the provided
* key and language
*
* @param key
* @param language
* @return string
*/
findMetadata(key: string, language?: string): string {
const metadatum = this.metadata
.find((metadatum: Metadatum) => {
return metadatum.key === key &&
(isEmpty(language) || metadatum.language === language)
});
if (isNotEmpty(metadatum)) {
return metadatum.value;
}
else {
return undefined;
}
}
/**
* The DSpaceObject that owns this DSpaceObject
*/
owner: DSpaceObject;
filterMetadata(keys: string[]): Array<Metadatum> {
return this.metadata
.filter((metadatum: Metadatum) => {
return keys.some(key => key === metadatum.key);
});
}
/**
* Find a metadata field by key and language
*
* This method returns the value of the first element
* in the metadata array that matches the provided
* key and language
*
* @param key
* @param language
* @return string
*/
findMetadata(key: string, language?: string): string {
const metadatum = this.metadata
.find((metadatum: Metadatum) => {
return metadatum.key === key &&
(isEmpty(language) || metadatum.language === language)
});
if (isNotEmpty(metadatum)) {
return metadatum.value;
}
else {
return undefined;
}
}
filterMetadata(keys: string[]): Array<Metadatum> {
return this.metadata
.filter((metadatum: Metadatum) => {
return keys.some(key => key === metadatum.key);
});
}
}

View File

@@ -1,6 +1,8 @@
import { inheritSerialization, autoserialize, autoserializeAs } from "cerialize";
import { DSpaceObject } from "./dspace-object.model";
import { Collection } from "./collection.model";
import { RemoteData } from "../data/remote-data";
import { Bundle } from "./bundle.model";
@inheritSerialization(DSpaceObject)
export class Item extends DSpaceObject {
@@ -37,4 +39,6 @@ export class Item extends DSpaceObject {
*/
owner: Collection;
bundles: Array<RemoteData<Bundle>>
}

View File

@@ -1,58 +0,0 @@
import { ParamHash } from "./param-hash";
describe("ParamHash", () => {
it("should return a hash for a set of parameters", () => {
const hash = new ParamHash('azerty', true, 23).toString();
expect(hash).not.toBeNull();
expect(hash).not.toBe('');
});
it("should work with both simple and complex objects as parameters", () => {
const hash = new ParamHash('azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }).toString();
expect(hash).not.toBeNull();
expect(hash).not.toBe('');
});
it("should work with null or undefined as parameters", () => {
const hash1 = new ParamHash(undefined).toString();
const hash2 = new ParamHash(null).toString();
const hash3 = new ParamHash(undefined, null).toString();
expect(hash1).not.toBeNull();
expect(hash1).not.toBe('');
expect(hash2).not.toBeNull();
expect(hash2).not.toBe('');
expect(hash3).not.toBeNull();
expect(hash3).not.toBe('');
expect(hash1).not.toEqual(hash2);
expect(hash1).not.toEqual(hash3);
expect(hash2).not.toEqual(hash3);
});
it("should work if created without parameters", () => {
const hash1 = new ParamHash().toString();
const hash2 = new ParamHash().toString();
expect(hash1).not.toBeNull();
expect(hash1).not.toBe('');
expect(hash1).toEqual(hash2);
});
it("should create the same hash if created with the same set of parameters in the same order", () => {
const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }];
const hash1 = new ParamHash(...params).toString();
const hash2 = new ParamHash(...params).toString();
expect(hash1).toEqual(hash2);
});
it("should create a different hash if created with the same set of parameters in a different order", () => {
const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }];
const hash1 = new ParamHash(...params).toString();
const hash2 = new ParamHash(...params.reverse()).toString();
expect(hash1).not.toEqual(hash2);
});
});

View File

@@ -1,35 +0,0 @@
import { Md5 } from "ts-md5/dist/md5";
/**
* Creates a hash of a set of parameters
*/
export class ParamHash {
private params: Array<any>;
constructor(...params) {
this.params = params;
}
/**
* Returns an md5 hash based on the
* params passed to the constructor
*
* If you hash the same set of params in the
* same order the hashes will be identical
*
* @return {string}
* an md5 hash
*/
toString(): string {
let hash = new Md5();
this.params.forEach((param) => {
if (param === Object(param)) {
hash.appendStr(JSON.stringify(param));
}
else {
hash.appendStr('' + param);
}
});
return hash.end().toString();
}
}

View File

@@ -1,5 +1,6 @@
import { URLCombiner } from "./url-combiner";
import { GlobalConfig } from "../../../config";
import { GlobalConfig } from '../../../config';
/**
* Combines a variable number of strings representing parts
@@ -7,8 +8,8 @@ import { GlobalConfig } from "../../../config";
*
* TODO write tests once GlobalConfig becomes injectable
*/
export class RESTURLCombiner extends URLCombiner{
constructor(...parts:Array<string>) {
super(GlobalConfig.rest.baseURL, GlobalConfig.rest.nameSpace, ...parts);
export class RESTURLCombiner extends URLCombiner {
constructor(EnvConfig: GlobalConfig, ...parts: Array<string>) {
super(EnvConfig.rest.baseUrl, EnvConfig.rest.nameSpace, ...parts);
}
}

View File

@@ -8,7 +8,7 @@ import { GlobalConfig } from "../../../config";
* TODO write tests once GlobalConfig becomes injectable
*/
export class UIURLCombiner extends URLCombiner{
constructor(...parts:Array<string>) {
super(GlobalConfig.ui.baseURL, GlobalConfig.ui.nameSpace, ...parts);
constructor(EnvConfig: GlobalConfig, ...parts: Array<string>) {
super(EnvConfig.ui.baseUrl, EnvConfig.ui.nameSpace, ...parts);
}
}

View File

@@ -2,9 +2,11 @@ import { NgModule } from '@angular/core';
import { HomeComponent } from './home.component';
import { HomeRoutingModule } from './home-routing.module';
import { CommonModule } from "@angular/common";
@NgModule({
imports: [
CommonModule,
HomeRoutingModule
],
declarations: [

View File

@@ -1,9 +1,9 @@
<div class="page-not-found">
<h1>404</h1>
<h2><small>{{"404.page-not-found" | translate}}</small></h2>
<br>
<br/>
<p>{{"404.help" | translate}}</p>
<br>
<br/>
<p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a>
</p>

View File

@@ -1,6 +1,5 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-pagenotfound',
styleUrls: ['./pagenotfound.component.css'],

View File

@@ -1,7 +1,7 @@
export const BITSTREAMS = [
{
"_links": {
"self": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" },
"self": { "href": "/bitstreams/3678" },
"bundle": { "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9" },
"retrieve": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa/retrieve" }
},
@@ -22,7 +22,7 @@ export const BITSTREAMS = [
},
{
"_links": {
"self": { "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" },
"self": { "href": "/bitstreams/8842" },
"bundle": { "href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd" },
"retrieve": { "href": "/rest/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632/retrieve" }
},

View File

@@ -1,12 +1,12 @@
export const BUNDLES = [
{
"_links": {
"self": { "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9" },
"self": { "href": "/bundles/2355" },
"items": [
{ "href": "/items/8871" }
],
"bitstreams": [
{ "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" },
{ "href": "/bitstreams/3678" },
],
"primaryBitstream": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" }
},
@@ -19,12 +19,12 @@ export const BUNDLES = [
},
{
"_links": {
"self": { "href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd" },
"self": { "href": "/bundles/5687" },
"items": [
{ "href": "/items/8871" }
],
"bitstreams": [
{ "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" },
{ "href": "/bitstreams/8842" },
],
"primaryBitstream": { "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" }
},

View File

@@ -14,10 +14,10 @@ export const ITEMS = [
],
"bundles": [
{
"href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9"
"href": "/bundles/2355"
},
{
"href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd"
"href": "/bundles/5687"
}
]
},
@@ -106,10 +106,10 @@ export const ITEMS = [
],
"bundles": [
{
"href": "/bundles/b0176baa-d52e-4c20-a8e6-d586f2c70c76"
"href": "/bundles/2355"
},
{
"href": "/bundles/40b1cd3f-07ad-4ca6-9716-132671f93a15"
"href": "/bundles/5687"
}
]
},

View File

@@ -1,7 +1,7 @@
// the polyfills must be the first thing imported
import 'angular2-universal-polyfills';
import 'ts-helpers';
import './__workaround.browser'; // temporary until 2.1.1 things are patched in Core
import './platform/workarounds/__workaround.browser'; // temporary until 2.1.1 things are patched in Core
// Angular 2
import { enableProdMode } from '@angular/core';
@@ -15,7 +15,7 @@ import { load as loadWebFont } from 'webfontloader';
// enable prod for faster renders
enableProdMode();
import { MainModuleNgFactory } from './browser.module.ngfactory';
import { MainModuleNgFactory } from './platform/modules/browser.module.ngfactory';
export const platformRef = platformBrowser();

View File

@@ -1,7 +1,7 @@
// the polyfills must be the first thing imported
import 'angular2-universal-polyfills';
import 'ts-helpers';
import './__workaround.browser'; // temporary until 2.1.1 things are patched in Core
import './platform/workarounds/__workaround.browser'; // temporary until 2.1.1 things are patched in Core
// Angular 2
import { enableProdMode } from '@angular/core';
@@ -10,10 +10,14 @@ import { bootloader } from '@angularclass/bootloader';
import { load as loadWebFont } from 'webfontloader';
// enable prod for faster renders
// enableProdMode();
import { EnvConfig } from './config';
import { MainModule } from './browser.module';
if (EnvConfig.production) {
// enable prod for faster renders
enableProdMode();
}
import { MainModule } from './platform/modules/browser.module';
export const platformRef = platformUniversalDynamic();

View File

@@ -1,34 +1,78 @@
// Look in ./config folder for config
import { OpaqueToken } from '@angular/core';
const path = require('path');
let configContext = require.context("../config", false, /js$/);
let EnvConfig : any = {};
let EnvConfigFile : string;
let DefaultConfig : any = {};
try {
DefaultConfig = configContext('./environment.default.js');
} catch (e) {
throw new Error(`Cannot find file "${path.resolve('config', './environment.default.js')}"`);
interface ServerConfig {
"ssl": boolean;
"address": string;
"port": number;
"nameSpace": string;
"baseUrl": string;
}
interface GlobalConfig {
"production": boolean;
"rest": ServerConfig;
"ui": ServerConfig;
"cache": {
"msToLive": number,
"control": string
};
"universal": {
"preboot": boolean,
"async": boolean
};
}
const GLOBAL_CONFIG = new OpaqueToken('config');
let configContext = require.context("../config", false, /js$/);
let EnvConfig: GlobalConfig;
let EnvConfigFile: string;
let production: boolean = false;
// check process.env.NODE_ENV to determine which environment config to use
// process.env.NODE_ENV is defined by webpack, else assume development
switch (process.env.NODE_ENV) {
case 'prod':
case 'production':
// webpack.prod.config.ts defines process.env.NODE_ENV = 'production'
EnvConfigFile = './environment.prod.js';
production = true;
break;
case 'test':
// webpack.test.config.ts defines process.env.NODE_ENV = 'test'
EnvConfigFile = './environment.test.js';
break;
case 'dev':
case 'development':
default:
// if not using webpack.prod.config.ts or webpack.test.config.ts, it must be development
EnvConfigFile = './environment.dev.js';
}
try {
EnvConfig = configContext(EnvConfigFile);
EnvConfig = configContext('./environment.default.js');
} catch (e) {
EnvConfig = {};
throw new Error("Cannot find file environment.default.js");
}
const GlobalConfig = Object.assign(DefaultConfig, EnvConfig);
// if EnvConfigFile set try to get configs
if (EnvConfigFile) {
try {
EnvConfig = <GlobalConfig>Object.assign(EnvConfig, configContext(EnvConfigFile));
} catch (e) {
console.warn("Cannot find file " + EnvConfigFile.substring(2, EnvConfigFile.length), "Using default environment.");
}
}
export {GlobalConfig}
// set base url if property is object with ssl, address, and port. i.e. ServerConfig
for (let key in EnvConfig) {
if (EnvConfig[key].ssl !== undefined && EnvConfig[key].address && EnvConfig[key].port) {
EnvConfig[key].baseUrl = [EnvConfig[key].ssl ? 'https://' : 'http://', EnvConfig[key].address, (EnvConfig[key].port !== 80) ? ':' + EnvConfig[key].port : ''].join('');
}
}
// set config for whether running in production
EnvConfig.production = production;
export { GLOBAL_CONFIG, GlobalConfig, EnvConfig }

View File

@@ -1,4 +1,4 @@
import { NgModule } from '@angular/core';
import { Inject, NgModule } from '@angular/core';
import { Http } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
@@ -8,21 +8,22 @@ import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-translate';
import { AppModule, AppComponent } from './app/app.module';
import { SharedModule } from './app/shared/shared.module';
import { CoreModule } from "./app/core/core.module";
import { AppModule, AppComponent } from '../../app/app.module';
import { SharedModule } from '../../app/shared/shared.module';
import { CoreModule } from '../../app/core/core.module';
import { StoreModule, Store } from "@ngrx/store";
import { RouterStoreModule } from "@ngrx/router-store";
import { StoreDevtoolsModule } from "@ngrx/store-devtools";
import { rootReducer, NGRX_CACHE_KEY, AppState } from './app/app.reducers';
import { effects } from './app/app.effects';
import { rootReducer, NGRX_CACHE_KEY, AppState } from '../../app/app.reducers';
import { effects } from '../../app/app.effects';
// Will be merged into @angular/platform-browser in a later release
// see https://github.com/angular/angular/pull/12322
import { Meta } from './angular2-meta';
import { RehydrateStoreAction } from "./app/store.actions";
import { GlobalConfig } from "./config";
import { Meta } from '../angular2-meta';
import { RehydrateStoreAction } from "../../app/store.actions";
import { GLOBAL_CONFIG, GlobalConfig, EnvConfig } from '../../config';
// import * as LRU from 'modern-lru';
@@ -44,7 +45,6 @@ export function getResponse() {
return {};
}
export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
@NgModule({
@@ -72,6 +72,8 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
effects
],
providers: [
{ provide: GLOBAL_CONFIG, useValue: EnvConfig },
{ provide: 'isBrowser', useValue: isBrowser },
{ provide: 'isNode', useValue: isNode },
@@ -80,21 +82,21 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
{ provide: 'LRU', useFactory: getLRU, deps: [] },
Meta,
Meta
// { provide: AUTO_PREBOOT, useValue: false } // turn off auto preboot complete
]
})
export class MainModule {
constructor(public store: Store<AppState>) {
constructor( @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, public store: Store<AppState>) {
// TODO(gdi2290): refactor into a lifecycle hook
this.doRehydrate();
}
doRehydrate() {
if (GlobalConfig.universal.shouldRehydrate) {
let defaultValue = {};
let serverCache = this._getCacheValue(NGRX_CACHE_KEY, defaultValue);
let defaultValue = {};
let serverCache = this._getCacheValue(NGRX_CACHE_KEY, defaultValue);
if (this.EnvConfig.universal.preboot) {
this.store.dispatch(new RehydrateStoreAction(serverCache));
}
}

View File

@@ -7,19 +7,20 @@ import { UniversalModule, isBrowser, isNode } from 'angular2-universal/node'; //
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-translate';
import { AppModule, AppComponent } from './app/app.module';
import { SharedModule } from './app/shared/shared.module';
import { CoreModule } from "./app/core/core.module";
import { AppModule, AppComponent } from '../../app/app.module';
import { SharedModule } from '../../app/shared/shared.module';
import { CoreModule } from "../../app/core/core.module";
import { StoreModule, Store } from "@ngrx/store";
import { RouterStoreModule } from "@ngrx/router-store";
import { StoreDevtoolsModule } from "@ngrx/store-devtools";
import { rootReducer, AppState, NGRX_CACHE_KEY } from './app/app.reducers';
import { effects } from './app/app.effects';
import { rootReducer, AppState, NGRX_CACHE_KEY } from '../../app/app.reducers';
import { effects } from '../../app/app.effects';
// Will be merged into @angular/platform-browser in a later release
// see https://github.com/angular/angular/pull/12322
import { Meta } from './angular2-meta';
import { Meta } from '../angular2-meta';
import { GLOBAL_CONFIG, EnvConfig } from '../../config';
export function createTranslateLoader(http: Http) {
return new TranslateStaticLoader(http, './assets/i18n', '.json');
@@ -57,10 +58,11 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
AppModule,
StoreModule.provideStore(rootReducer),
RouterStoreModule.connectRouter(),
StoreDevtoolsModule.instrumentOnlyWithExtension(),
effects
],
providers: [
{ provide: GLOBAL_CONFIG, useValue: EnvConfig },
{ provide: 'isBrowser', useValue: isBrowser },
{ provide: 'isNode', useValue: isNode },
@@ -69,7 +71,8 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
{ provide: 'LRU', useFactory: getLRU, deps: [] },
Meta,
Meta
]
})
export class MainModule {

View File

@@ -4,7 +4,7 @@
// if you are including modules that modify Promise, such as NewRelic,, you must include them before polyfills
import 'angular2-universal-polyfills';
import 'ts-helpers';
import './__workaround.node'; // temporary until 2.1.1 things are patched in Core
import './platform/workarounds/__workaround.node'; // temporary until 2.1.1 things are patched in Core
import * as fs from 'fs';
import * as path from 'path';
@@ -20,11 +20,13 @@ import { enableProdMode } from '@angular/core';
import { createEngine } from 'angular2-express-engine';
// App
import { MainModuleNgFactory } from './node.module.ngfactory';
import { MainModuleNgFactory } from './platform/modules/node.module.ngfactory';
// Routes
import { routes } from './server.routes';
import { EnvConfig } from './config';
// enable prod for faster renders
enableProdMode();
@@ -42,8 +44,9 @@ app.engine('.html', createEngine({
// stateless providers only since it's shared
]
}));
app.set('port', process.env.PORT || 3000);
app.set('address', process.env.ADDRESS || '127.0.0.1');
app.set('port', process.env.PORT || EnvConfig.ui.port || 3000);
app.set('address', process.env.ADDRESS || EnvConfig.ui.address || '127.0.0.1');
app.set('views', __dirname);
app.set('view engine', 'html');
app.set('json spaces', 2);
@@ -61,16 +64,16 @@ app.use(morgan('common', {
function cacheControl(req, res, next) {
// instruct browser to revalidate in 60 seconds
res.header('Cache-Control', 'max-age=60');
res.header('Cache-Control', EnvConfig.cache.control || 'max-age=60');
next();
}
// Serve static files
app.use('/assets', cacheControl, express.static(path.join(__dirname, 'assets'), { maxAge: 30 }));
app.use('/styles', cacheControl, express.static(path.join(__dirname, 'styles'), { maxAge: 30 }));
app.use(cacheControl, express.static(path.join(ROOT, 'dist/client'), { index: false }));
//
/////////////////////////
// ** Example API
// Notice API should be in a separate process
@@ -93,11 +96,11 @@ function ngApp(req, res) {
res,
// use this to determine what part of your app is slow only in development
// time: true,
async: true,
preboot: true,
baseUrl: '/',
async: EnvConfig.universal.async,
preboot: EnvConfig.universal.preboot,
baseUrl: EnvConfig.ui.nameSpace,
requestUrl: req.originalUrl,
originUrl: `http://${app.get('address')}:${app.get('port')}`
originUrl: EnvConfig.ui.baseUrl
});
});
@@ -112,7 +115,6 @@ routes.forEach(route => {
app.get(`/${route}/*`, ngApp);
});
app.get('*', function(req, res) {
res.setHeader('Content-Type', 'application/json');
var pojo = { status: 404, message: 'No Content' };
@@ -121,6 +123,6 @@ app.get('*', function(req, res) {
});
// Server
let server = app.listen(app.get('port'), () => {
console.log(`Listening on: http://${server.address().address}:${server.address().port}`);
let server = app.listen(app.get('port'), app.get('address'), () => {
console.log(`[${new Date().toTimeString()}] Listening on ${EnvConfig.ui.ssl ? 'https://' : 'http://'}${server.address().address}:${server.address().port}`);
});

View File

@@ -4,7 +4,7 @@
// if you are including modules that modify Promise, such as NewRelic,, you must include them before polyfills
import 'angular2-universal-polyfills';
import 'ts-helpers';
import './__workaround.node'; // temporary until 2.1.1 things are patched in Core
import './platform/workarounds/__workaround.node'; // temporary until 2.1.1 things are patched in Core
import * as path from 'path';
import * as morgan from 'morgan';
@@ -19,13 +19,17 @@ import { enableProdMode } from '@angular/core';
import { createEngine } from 'angular2-express-engine';
// App
import { MainModule } from './node.module';
import { MainModule } from './platform/modules/node.module';
// Routes
import { routes } from './server.routes';
// enable prod for faster renders
enableProdMode();
import { EnvConfig } from './config';
if (EnvConfig.production) {
// enable prod for faster renders
enableProdMode();
}
const app = express();
const ROOT = path.join(path.resolve(__dirname, '..'));
@@ -40,8 +44,9 @@ app.engine('.html', createEngine({
// stateless providers only since it's shared
]
}));
app.set('port', process.env.PORT || 3000);
app.set('address', process.env.ADDRESS || '127.0.0.1');
app.set('port', process.env.PORT || EnvConfig.ui.port || 3000);
app.set('address', process.env.ADDRESS || EnvConfig.ui.address || '127.0.0.1');
app.set('views', __dirname);
app.set('view engine', 'html');
app.set('json spaces', 2);
@@ -54,16 +59,16 @@ app.use(morgan('dev'));
function cacheControl(req, res, next) {
// instruct browser to revalidate in 60 seconds
res.header('Cache-Control', 'max-age=60');
res.header('Cache-Control', EnvConfig.cache.control || 'max-age=60');
next();
}
// Serve static files
app.use('/assets', cacheControl, express.static(path.join(__dirname, 'assets'), { maxAge: 30 }));
app.use('/styles', cacheControl, express.static(path.join(__dirname, 'styles'), { maxAge: 30 }));
app.use(cacheControl, express.static(path.join(ROOT, 'dist/client'), { index: false }));
//
/////////////////////////
// ** Example API
// Notice API should be in aseparate process
@@ -85,11 +90,11 @@ function ngApp(req, res) {
req,
res,
// time: true, // use this to determine what part of your app is slow only in development
async: true,
preboot: true,
baseUrl: '/',
async: EnvConfig.universal.async,
preboot: EnvConfig.universal.preboot,
baseUrl: EnvConfig.ui.nameSpace,
requestUrl: req.originalUrl,
originUrl: `http://${app.get('address')}:${app.get('port')}`
originUrl: EnvConfig.ui.baseUrl
});
});
@@ -112,6 +117,6 @@ app.get('*', function(req, res) {
});
// Server
let server = app.listen(app.get('port'), () => {
console.log(`Listening on: http://${server.address().address}:${server.address().port}`);
let server = app.listen(app.get('port'), app.get('address'), () => {
console.log(`[${new Date().toTimeString()}] Listening on ${EnvConfig.ui.ssl ? 'https://' : 'http://'}${server.address().address}:${server.address().port}`);
});

View File

@@ -94,7 +94,6 @@ export var clientConfig = {
}
};
// Server.
export var serverPlugins = [
@@ -133,9 +132,6 @@ export default [
webpackMerge(clone(commonConfig), serverConfig, { plugins: serverPlugins.concat(commonPlugins) })
];
// Helpers
export function includeClientPackages(packages, localModule?: string[]) {
return function(context, request, cb) {

View File

@@ -1455,6 +1455,10 @@ deep-extend@~0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253"
deep-freeze-strict@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0"
deep-freeze@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84"
@@ -2104,15 +2108,12 @@ fs-extra@^0.26.4:
path-is-absolute "^1.0.0"
rimraf "^2.2.8"
fs-extra@^0.30.0:
version "0.30.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
fs-extra@^2.0.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-2.1.2.tgz#046c70163cef9aad46b0e4a7fa467fb22d71de35"
dependencies:
graceful-fs "^4.1.2"
jsonfile "^2.1.0"
klaw "^1.0.0"
path-is-absolute "^1.0.0"
rimraf "^2.2.8"
fs-extra@~1.0.0:
version "1.0.0"
@@ -3659,6 +3660,12 @@ ng2-translate@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/ng2-translate/-/ng2-translate-4.2.0.tgz#83bc8feca329b5fc56a636e36073241c6280c659"
ngrx-store-freeze@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/ngrx-store-freeze/-/ngrx-store-freeze-0.1.9.tgz#b20f18f21fd5efc4e1b1e05f6f279674d0f70c81"
dependencies:
deep-freeze-strict "^1.1.1"
no-case@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.1.tgz#7aeba1c73a52184265554b7dc03baf720df80081"
@@ -5654,13 +5661,13 @@ typedarray@~0.0.5:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typedoc-default-themes@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.4.2.tgz#640b854fd7ef19e6774496ea7741ec31a0dcaddc"
typedoc-default-themes@^0.4.2:
version "0.4.3"
resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.4.3.tgz#39014c515585f27e59773d29e8921a5b8b89d4c0"
typedoc@0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.5.1.tgz#150f92bdc4a36d8e114b27f1d296329d27e837dd"
typedoc@0.5.7:
version "0.5.7"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.5.7.tgz#f2998dbb5909cb3f02db5fecb85e2a4377189e2a"
dependencies:
"@types/fs-extra" "0.0.33"
"@types/handlebars" "^4.0.31"
@@ -5669,7 +5676,7 @@ typedoc@0.5.1:
"@types/marked" "0.0.28"
"@types/minimatch" "^2.0.29"
"@types/shelljs" "^0.3.32"
fs-extra "^0.30.0"
fs-extra "^2.0.0"
handlebars "4.0.5"
highlight.js "^9.0.0"
lodash "^4.13.1"
@@ -5677,16 +5684,16 @@ typedoc@0.5.1:
minimatch "^3.0.0"
progress "^1.1.8"
shelljs "^0.7.0"
typedoc-default-themes "^0.4.0"
typescript "2.0.6"
typedoc-default-themes "^0.4.2"
typescript "2.1.6"
typescript@2.0.10, typescript@~2.0.3:
version "2.0.10"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.0.10.tgz#ccdd4ed86fd5550a407101a0814012e1b3fac3dd"
typescript@2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.0.6.tgz#5385499ac9811508c2c43e0ea07a1ddca435e111"
typescript@2.1.6:
version "2.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.1.6.tgz#40c7e6e9e5da7961b7718b55505f9cac9487a607"
uglify-js@2.7.x, uglify-js@^2.6, uglify-js@^2.6.1, uglify-js@~2.7.3:
version "2.7.5"