Browse Source

Merge branch 'apollo-link-state'

Michael Bromley 7 years ago
parent
commit
386babf873
47 changed files with 1567 additions and 1567 deletions
  1. 2 2
      admin-ui/README.md
  2. 88 38
      admin-ui/generate-graphql-types.ts
  3. 6 5
      admin-ui/package.json
  4. 4 5
      admin-ui/src/app/app.component.ts
  5. 2 2
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts
  6. 15 9
      admin-ui/src/app/catalog/components/product-list/product-list.component.ts
  7. 0 19
      admin-ui/src/app/common/queries/get-product-by-id.ts
  8. 0 16
      admin-ui/src/app/common/queries/get-product-list.ts
  9. 0 785
      admin-ui/src/app/common/types/gql-generated-types.ts
  10. 9 7
      admin-ui/src/app/core/components/app-shell/app-shell.component.ts
  11. 4 5
      admin-ui/src/app/core/components/breadcrumb/breadcrumb.component.ts
  12. 4 31
      admin-ui/src/app/core/core.module.ts
  13. 71 0
      admin-ui/src/app/core/providers/auth/auth.service.ts
  14. 0 30
      admin-ui/src/app/core/providers/data/product-data.service.ts
  15. 9 15
      admin-ui/src/app/core/providers/guard/auth.guard.ts
  16. 14 0
      admin-ui/src/app/data/client-state/client-defaults.ts
  17. 71 0
      admin-ui/src/app/data/client-state/client-resolvers.ts
  18. 61 0
      admin-ui/src/app/data/data.module.ts
  19. 33 0
      admin-ui/src/app/data/mutations/local-mutations.ts
  20. 16 7
      admin-ui/src/app/data/providers/base-data.service.ts
  21. 39 0
      admin-ui/src/app/data/providers/client-data.service.ts
  22. 3 0
      admin-ui/src/app/data/providers/data.service.ts
  23. 6 6
      admin-ui/src/app/data/providers/interceptor.ts
  24. 24 0
      admin-ui/src/app/data/providers/product-data.service.ts
  25. 2 2
      admin-ui/src/app/data/providers/user-data.service.ts
  26. 19 0
      admin-ui/src/app/data/queries/local-queries.ts
  27. 34 0
      admin-ui/src/app/data/queries/product-queries.ts
  28. 21 0
      admin-ui/src/app/data/types/client-types.graphql
  29. 370 0
      admin-ui/src/app/data/types/gql-generated-types.ts
  30. 35 0
      admin-ui/src/app/data/types/query-result.ts
  31. 0 0
      admin-ui/src/app/data/types/response.ts
  32. 5 8
      admin-ui/src/app/login/components/login/login.component.ts
  33. 0 34
      admin-ui/src/app/state/action-logger.ts
  34. 0 19
      admin-ui/src/app/state/api/api-actions.ts
  35. 0 26
      admin-ui/src/app/state/api/api-reducer.ts
  36. 0 31
      admin-ui/src/app/state/api/api-state.ts
  37. 0 10
      admin-ui/src/app/state/app-state.ts
  38. 0 24
      admin-ui/src/app/state/handle-error.ts
  39. 0 85
      admin-ui/src/app/state/state-store.service.ts
  40. 0 44
      admin-ui/src/app/state/state.module.ts
  41. 0 61
      admin-ui/src/app/state/user/user-actions.ts
  42. 0 65
      admin-ui/src/app/state/user/user-reducer.spec.ts
  43. 0 41
      admin-ui/src/app/state/user/user-reducer.ts
  44. 0 60
      admin-ui/src/app/state/user/user-state.ts
  45. 598 57
      admin-ui/yarn.lock
  46. 2 18
      graphql.config.json
  47. 0 0
      schema.json

+ 2 - 2
admin-ui/README.md

@@ -11,7 +11,7 @@ The UI is powered by the [Clarity Design System](https://vmware.github.io/clarit
 [apollo-codegen](https://github.com/apollographql/apollo-codegen) is used to automatically create TypeScript interfaces
 for all GraphQL queries used in the application.
 
-All queries should be located in the [`./src/app/common/queries`](./src/app/common/queries) directory. 
+All queries should be located in the [`./src/app/data/queries`](src/app/data/queries) directory. 
 
 Run `yarn generate-gql-types` to generate TypeScript interfaces based on these queries. The generated
-types are located at [`./src/app/common/types/gql-generated-types.ts`](./src/app/common/types/gql-generated-types.ts).
+types are located at [`./src/app/data/types/gql-generated-types.ts`](src/app/data/types/gql-generated-types.ts).

+ 88 - 38
admin-ui/generate-graphql-types.ts

@@ -1,55 +1,105 @@
 import { spawn } from 'child_process';
-import * as rimraf from 'rimraf';
+import * as fs from 'fs';
+import { execute, GraphQLSchema, IntrospectionQuery, IntrospectionSchema, parse } from 'graphql';
+import { makeExecutableSchema } from 'graphql-tools';
+import { buildClientSchema, introspectionQuery, printSchema } from 'graphql/utilities';
+import { fileLoader, mergeTypes } from 'merge-graphql-schemas';
 import { API_PATH, API_PORT } from '../shared/shared-constants';
 
-/*
+// tslint:disable:no-console
+const API_URL = `http://localhost:${API_PORT}/${API_PATH}`;
+const SCHEMA_JSON_FILE = '../schema.json';
+const CLIENT_SCHEMA_FILES = './src/app/data/types/**/*.graphql';
+const CLIENT_QUERY_FILES = '"./src/app/data/(queries|mutations)/**/*.ts"';
+const TYPESCRIPT_DEFINITIONS_FILE = './src/app/data/types/gql-generated-types.ts';
+
+main().catch(e => {
+    console.log('Could not generate types!', e);
+    process.exitCode = 1;
+});
+
+/**
  * This script uses apollo-codegen to generate TypeScript interfaces for all
  * GraphQL queries defined in the admin-ui app. Run it via the package.json
  * script "generate-gql-types".
  */
+async function main(): Promise<void> {
+    const introspectionQueryFromApi = await downloadSchemaFromApi(API_URL);
+    const combinedSchema = await combineSchemas(introspectionQueryFromApi, CLIENT_SCHEMA_FILES);
 
-const API_URL = `http://localhost:${API_PORT}/${API_PATH}`;
-const SCHEMA_DUMP = 'schema.temp.json';
+    fs.writeFileSync(SCHEMA_JSON_FILE, JSON.stringify(combinedSchema));
+    console.log(`Generated schema file: ${SCHEMA_JSON_FILE}`);
 
-// tslint:disable:no-console
-runApolloCodegen([
-    'introspect-schema',
-    API_URL,
-    '--add-typename',
-    `--output ${SCHEMA_DUMP}`,
-])
-    .then(() => {
-        console.log('Generated schema dump...');
-        return runApolloCodegen([
-            'generate',
-            './src/app/common/queries/**/*.ts',
-            `--schema ${SCHEMA_DUMP}`,
-            '--target typescript',
-            '--output ./src/app/common/types/gql-generated-types.ts',
-        ]);
-    })
-    .then(() => {
-        console.log('Generated TypeScript definitions!');
-    })
-    .then(() => {
-        rimraf(SCHEMA_DUMP, (err) => {
-            if (err) {
-                console.log('Could not delete schema dump');
-            }
-            console.log('Deleted schema dump');
-        });
-    })
-    .catch(() => {
-        console.log('Could not generate types!');
-        process.exitCode = 1;
+    await generateTypeScriptTypesFromSchema(SCHEMA_JSON_FILE, CLIENT_QUERY_FILES, TYPESCRIPT_DEFINITIONS_FILE);
+
+    console.log('Generated TypeScript definitions!');
+}
+
+/**
+ * Downloads the schema from the provided GraphQL endpoint using the `apollo schema:download`
+ * cli command and returns the result as an IntrospectionQuery object.
+ */
+async function downloadSchemaFromApi(apiEndpoint: string): Promise<IntrospectionQuery> {
+    const TEMP_API_SCHEMA = '../schema.temp.json';
+    await runCommand('yarn', [
+        'apollo',
+        'schema:download',
+        TEMP_API_SCHEMA,
+        `--endpoint=${API_URL}`,
+    ]);
+
+    console.log(`Downloaded schema from ${API_URL}`);
+
+    const schemaFromApi = fs.readFileSync(TEMP_API_SCHEMA, { encoding: 'utf8' });
+    fs.unlinkSync(TEMP_API_SCHEMA);
+    const introspectionSchema: IntrospectionSchema = JSON.parse(schemaFromApi);
+    return {
+        __schema: introspectionSchema,
+    };
+}
+
+async function introspectionFromSchema(schema: GraphQLSchema): Promise<IntrospectionQuery> {
+    const queryAST = parse(introspectionQuery);
+    const result = await execute(schema, queryAST);
+    return result.data as IntrospectionQuery;
+}
+
+/**
+ * Combines the IntrospectionQuery from the GraphQL API with any client-side schemas as defined by the
+ * clientSchemaFiles glob.
+ */
+async function combineSchemas(introspectionQueryFromApi: IntrospectionQuery, clientSchemaFiles: string): Promise<IntrospectionQuery> {
+    const schemaFromApi = buildClientSchema(introspectionQueryFromApi);
+    const clientSchemas = fileLoader(clientSchemaFiles);
+    const remoteSchema = printSchema(schemaFromApi);
+    const typeDefs = mergeTypes([...clientSchemas, remoteSchema], {
+        all: true,
     });
+    const executableSchema = makeExecutableSchema({ typeDefs, resolverValidationOptions: { requireResolversForResolveType: false } });
+    const introspection = await introspectionFromSchema(executableSchema);
+    return introspection;
+}
+
+/**
+ * Generates TypeScript definitions from the provided schema json file sing the `apollo codegen:generate` cli command.
+ */
+async function generateTypeScriptTypesFromSchema(schemaFile: string, queryFiles: string, outputFile: string): Promise<number> {
+    return runCommand('yarn', [
+        'apollo',
+        'codegen:generate',
+        outputFile,
+        '--addTypename',
+        `--queries=${queryFiles}`,
+        `--schema ${schemaFile}`,
+    ]);
+}
 
 /**
- * Run the apollo-codegen script and wrap in a Promise.
+ * Runs a command-line command and resolves when completed.
  */
-function runApolloCodegen(args: string[]): Promise<any> {
+function runCommand(command: string, args: string[]): Promise<number> {
     return new Promise((resolve, reject) => {
-        const cp = spawn('yarn', ['apollo-codegen', ...args], { shell: true });
+        const cp = spawn(command, args, { shell: true });
 
         cp.on('error', reject);
         cp.stdout.on('data', (data) => {

+ 6 - 5
admin-ui/package.json

@@ -8,7 +8,7 @@
     "test": "ng test",
     "lint": "ng lint vendure-admin --fix",
     "e2e": "ng e2e",
-    "apollo-codegen": "apollo-codegen",
+    "apollo": "apollo",
     "generate-gql-types": "ts-node generate-graphql-types.ts"
   },
   "private": true,
@@ -25,18 +25,17 @@
     "@clr/angular": "^0.11.21",
     "@clr/icons": "^0.11.21",
     "@clr/ui": "^0.11.21",
-    "@ngrx/store": "^6.0.1",
     "@webcomponents/custom-elements": "1.0.0",
     "apollo-angular": "^1.1.1",
     "apollo-angular-cache-ngrx": "^1.0.0-beta.0",
     "apollo-angular-link-http": "^1.1.0",
     "apollo-cache-inmemory": "^1.2.4",
     "apollo-client": "^2.3.4",
+    "apollo-link": "^1.2.2",
+    "apollo-link-state": "^0.4.1",
     "core-js": "^2.5.4",
     "graphql": "^0.13.2",
     "graphql-tag": "^2.9.2",
-    "immer": "^1.3.1",
-    "ngrx-store-logger": "^0.2.2",
     "rxjs": "^6.0.0",
     "rxjs-compat": "^6.2.1",
     "zone.js": "^0.8.26"
@@ -49,8 +48,9 @@
     "@types/jasmine": "~2.8.6",
     "@types/jasminewd2": "~2.0.3",
     "@types/node": "~8.9.4",
-    "apollo-codegen": "^0.20.1",
+    "apollo": "^1.1.1",
     "codelyzer": "~4.2.1",
+    "graphql-tools": "^3.0.4",
     "jasmine-core": "~2.99.1",
     "jasmine-spec-reporter": "~4.2.1",
     "karma": "~1.7.1",
@@ -58,6 +58,7 @@
     "karma-coverage-istanbul-reporter": "~2.0.0",
     "karma-jasmine": "~1.1.1",
     "karma-jasmine-html-reporter": "^0.2.2",
+    "merge-graphql-schemas": "^1.5.2",
     "protractor": "~5.3.0",
     "rimraf": "^2.6.2",
     "ts-node": "~5.0.1",

+ 4 - 5
admin-ui/src/app/app.component.ts

@@ -1,8 +1,7 @@
 import { Component, OnInit } from '@angular/core';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
-
-import { StateStore } from './state/state-store.service';
+import { DataService } from './data/providers/data.service';
 
 @Component({
     selector: 'vdr-root',
@@ -12,12 +11,12 @@ import { StateStore } from './state/state-store.service';
 export class AppComponent implements OnInit {
     loading$: Observable<boolean>;
 
-    constructor(private store: StateStore) {
+    constructor(private dataService: DataService) {
     }
 
     ngOnInit() {
-        this.loading$ = this.store.select(state => state.api.inFlightRequests).pipe(
-            map(count => 0 < count),
+        this.loading$ = this.dataService.client.getNetworkStatus().stream$.pipe(
+            map(data => 0 < data.networkStatus.inFlightRequests),
         );
     }
 }

+ 2 - 2
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 import { Observable } from 'rxjs';
 
-import { DataService } from '../../../core/providers/data/data.service';
+import { DataService } from '../../../data/providers/data.service';
 
 @Component({
     selector: 'vdr-product-detail',
@@ -17,7 +17,7 @@ export class ProductDetailComponent implements OnInit {
                 private route: ActivatedRoute) { }
 
     ngOnInit() {
-        this.product$ = this.dataService.product.getProduct(this.route.snapshot.paramMap.get('id'));
+        this.product$ = this.dataService.product.getProduct(this.route.snapshot.paramMap.get('id')).single$;
     }
 
 }

+ 15 - 9
admin-ui/src/app/catalog/components/product-list/product-list.component.ts

@@ -1,34 +1,40 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
 import { QueryRef } from 'apollo-angular';
-import { Observable } from 'rxjs';
-import { map, tap } from 'rxjs/operators';
-
-import { GetProductListQuery, GetProductListQueryVariables } from '../../../common/types/gql-generated-types';
-import { DataService } from '../../../core/providers/data/data.service';
+import { Observable, Subject } from 'rxjs';
+import { map, takeUntil, tap } from 'rxjs/operators';
+import { DataService } from '../../../data/providers/data.service';
+import { GetProductList, GetProductListVariables } from '../../../data/types/gql-generated-types';
 
 @Component({
     selector: 'vdr-products-list',
     templateUrl: './product-list.component.html',
     styleUrls: ['./product-list.component.scss'],
 })
-export class ProductListComponent implements OnInit {
+export class ProductListComponent implements OnInit, OnDestroy {
 
     products$: Observable<any[]>;
     totalItems: number;
     itemsPerPage = 25;
     currentPage = 1;
-    private productsQuery: QueryRef<GetProductListQuery, GetProductListQueryVariables>;
+    private productsQuery: QueryRef<GetProductList, GetProductListVariables>;
+    private destroy$ = new Subject<void>();
 
     constructor(private dataService: DataService) { }
 
     ngOnInit() {
-        this.productsQuery = this.dataService.product.getProducts(this.itemsPerPage, 0);
+        this.productsQuery = this.dataService.product.getProducts(this.itemsPerPage, 0).ref;
         this.products$ = this.productsQuery.valueChanges.pipe(
+            takeUntil(this.destroy$),
             tap(val => { this.totalItems = val.data.products.totalItems; }),
             map(val => val.data.products.items),
         );
     }
 
+    ngOnDestroy() {
+        this.destroy$.next();
+        this.destroy$.complete();
+    }
+
     getPage(pageNumber: number): void {
         const take = this.itemsPerPage;
         const skip = (pageNumber - 1) * this.itemsPerPage;

+ 0 - 19
admin-ui/src/app/common/queries/get-product-by-id.ts

@@ -1,19 +0,0 @@
-import gql from 'graphql-tag';
-
-export const getProductById = gql`
-    query GetProductById($id: ID!, $languageCode: LanguageCode){
-        product(languageCode: $languageCode, id: $id) {
-            id
-            languageCode
-            name
-            slug
-            description
-            translations {
-                languageCode
-                name
-                slug
-                description
-            }
-        }
-    }
-`;

+ 0 - 16
admin-ui/src/app/common/queries/get-product-list.ts

@@ -1,16 +0,0 @@
-import gql from 'graphql-tag';
-
-export const getProductList = gql`
-    query GetProductList($take: Int, $skip: Int, $languageCode: LanguageCode){
-        products(languageCode: $languageCode, take: $take, skip: $skip) {
-            items {
-                id
-                languageCode
-                name
-                slug
-                description
-            }
-            totalItems
-        }
-    }
-`;

+ 0 - 785
admin-ui/src/app/common/types/gql-generated-types.ts

@@ -1,785 +0,0 @@
-/* tslint:disable */
-//  This file was automatically generated and should not be edited.
-
-/**
- * ISO 639-1 language code
- */
-export enum LanguageCode {
-  /**
-   * Afar
-   */
-  aa = "aa",
-  /**
-   * Abkhazian
-   */
-  ab = "ab",
-  /**
-   * Avestan
-   */
-  ae = "ae",
-  /**
-   * Afrikaans
-   */
-  af = "af",
-  /**
-   * Akan
-   */
-  ak = "ak",
-  /**
-   * Amharic
-   */
-  am = "am",
-  /**
-   * Aragonese
-   */
-  an = "an",
-  /**
-   * Arabic
-   */
-  ar = "ar",
-  /**
-   * Assamese
-   */
-  as = "as",
-  /**
-   * Avaric
-   */
-  av = "av",
-  /**
-   * Aymara
-   */
-  ay = "ay",
-  /**
-   * Azerbaijani
-   */
-  az = "az",
-  /**
-   * Bashkir
-   */
-  ba = "ba",
-  /**
-   * Belarusian
-   */
-  be = "be",
-  /**
-   * Bulgarian
-   */
-  bg = "bg",
-  /**
-   * Bihari languages
-   */
-  bh = "bh",
-  /**
-   * Bislama
-   */
-  bi = "bi",
-  /**
-   * Bambara
-   */
-  bm = "bm",
-  /**
-   * Bengali
-   */
-  bn = "bn",
-  /**
-   * Tibetan
-   */
-  bo = "bo",
-  /**
-   * Breton
-   */
-  br = "br",
-  /**
-   * Bosnian
-   */
-  bs = "bs",
-  /**
-   * Catalan; Valencian
-   */
-  ca = "ca",
-  /**
-   * Chechen
-   */
-  ce = "ce",
-  /**
-   * Chamorro
-   */
-  ch = "ch",
-  /**
-   * Corsican
-   */
-  co = "co",
-  /**
-   * Cree
-   */
-  cr = "cr",
-  /**
-   * Czech
-   */
-  cs = "cs",
-  /**
-   * Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic
-   */
-  cu = "cu",
-  /**
-   * Chuvash
-   */
-  cv = "cv",
-  /**
-   * Welsh
-   */
-  cy = "cy",
-  /**
-   * Danish
-   */
-  da = "da",
-  /**
-   * German
-   */
-  de = "de",
-  /**
-   * Divehi; Dhivehi; Maldivian
-   */
-  dv = "dv",
-  /**
-   * Dzongkha
-   */
-  dz = "dz",
-  /**
-   * Ewe
-   */
-  ee = "ee",
-  /**
-   * Greek, Modern (1453-)
-   */
-  el = "el",
-  /**
-   * English
-   */
-  en = "en",
-  /**
-   * Esperanto
-   */
-  eo = "eo",
-  /**
-   * Spanish; Castilian
-   */
-  es = "es",
-  /**
-   * Estonian
-   */
-  et = "et",
-  /**
-   * Basque
-   */
-  eu = "eu",
-  /**
-   * Persian
-   */
-  fa = "fa",
-  /**
-   * Fulah
-   */
-  ff = "ff",
-  /**
-   * Finnish
-   */
-  fi = "fi",
-  /**
-   * Fijian
-   */
-  fj = "fj",
-  /**
-   * Faroese
-   */
-  fo = "fo",
-  /**
-   * French
-   */
-  fr = "fr",
-  /**
-   * Western Frisian
-   */
-  fy = "fy",
-  /**
-   * Irish
-   */
-  ga = "ga",
-  /**
-   * Gaelic; Scottish Gaelic
-   */
-  gd = "gd",
-  /**
-   * Galician
-   */
-  gl = "gl",
-  /**
-   * Guarani
-   */
-  gn = "gn",
-  /**
-   * Gujarati
-   */
-  gu = "gu",
-  /**
-   * Manx
-   */
-  gv = "gv",
-  /**
-   * Hausa
-   */
-  ha = "ha",
-  /**
-   * Hebrew
-   */
-  he = "he",
-  /**
-   * Hindi
-   */
-  hi = "hi",
-  /**
-   * Hiri Motu
-   */
-  ho = "ho",
-  /**
-   * Croatian
-   */
-  hr = "hr",
-  /**
-   * Haitian; Haitian Creole
-   */
-  ht = "ht",
-  /**
-   * Hungarian
-   */
-  hu = "hu",
-  /**
-   * Armenian
-   */
-  hy = "hy",
-  /**
-   * Herero
-   */
-  hz = "hz",
-  /**
-   * Interlingua (International Auxiliary Language Association)
-   */
-  ia = "ia",
-  /**
-   * Indonesian
-   */
-  id = "id",
-  /**
-   * Interlingue; Occidental
-   */
-  ie = "ie",
-  /**
-   * Igbo
-   */
-  ig = "ig",
-  /**
-   * Sichuan Yi; Nuosu
-   */
-  ii = "ii",
-  /**
-   * Inupiaq
-   */
-  ik = "ik",
-  /**
-   * Ido
-   */
-  io = "io",
-  /**
-   * Icelandic
-   */
-  is = "is",
-  /**
-   * Italian
-   */
-  it = "it",
-  /**
-   * Inuktitut
-   */
-  iu = "iu",
-  /**
-   * Japanese
-   */
-  ja = "ja",
-  /**
-   * Javanese
-   */
-  jv = "jv",
-  /**
-   * Georgian
-   */
-  ka = "ka",
-  /**
-   * Kongo
-   */
-  kg = "kg",
-  /**
-   * Kikuyu; Gikuyu
-   */
-  ki = "ki",
-  /**
-   * Kuanyama; Kwanyama
-   */
-  kj = "kj",
-  /**
-   * Kazakh
-   */
-  kk = "kk",
-  /**
-   * Kalaallisut; Greenlandic
-   */
-  kl = "kl",
-  /**
-   * Central Khmer
-   */
-  km = "km",
-  /**
-   * Kannada
-   */
-  kn = "kn",
-  /**
-   * Korean
-   */
-  ko = "ko",
-  /**
-   * Kanuri
-   */
-  kr = "kr",
-  /**
-   * Kashmiri
-   */
-  ks = "ks",
-  /**
-   * Kurdish
-   */
-  ku = "ku",
-  /**
-   * Komi
-   */
-  kv = "kv",
-  /**
-   * Cornish
-   */
-  kw = "kw",
-  /**
-   * Kirghiz; Kyrgyz
-   */
-  ky = "ky",
-  /**
-   * Latin
-   */
-  la = "la",
-  /**
-   * Luxembourgish; Letzeburgesch
-   */
-  lb = "lb",
-  /**
-   * Ganda
-   */
-  lg = "lg",
-  /**
-   * Limburgan; Limburger; Limburgish
-   */
-  li = "li",
-  /**
-   * Lingala
-   */
-  ln = "ln",
-  /**
-   * Lao
-   */
-  lo = "lo",
-  /**
-   * Lithuanian
-   */
-  lt = "lt",
-  /**
-   * Luba-Katanga
-   */
-  lu = "lu",
-  /**
-   * Latvian
-   */
-  lv = "lv",
-  /**
-   * Malagasy
-   */
-  mg = "mg",
-  /**
-   * Marshallese
-   */
-  mh = "mh",
-  /**
-   * Maori
-   */
-  mi = "mi",
-  /**
-   * Macedonian
-   */
-  mk = "mk",
-  /**
-   * Malayalam
-   */
-  ml = "ml",
-  /**
-   * Mongolian
-   */
-  mn = "mn",
-  /**
-   * Marathi
-   */
-  mr = "mr",
-  /**
-   * Malay
-   */
-  ms = "ms",
-  /**
-   * Maltese
-   */
-  mt = "mt",
-  /**
-   * Burmese
-   */
-  my = "my",
-  /**
-   * Nauru
-   */
-  na = "na",
-  /**
-   * Bokmål, Norwegian; Norwegian Bokmål
-   */
-  nb = "nb",
-  /**
-   * Ndebele, North; North Ndebele
-   */
-  nd = "nd",
-  /**
-   * Nepali
-   */
-  ne = "ne",
-  /**
-   * Ndonga
-   */
-  ng = "ng",
-  /**
-   * Dutch; Flemish
-   */
-  nl = "nl",
-  /**
-   * Norwegian Nynorsk; Nynorsk, Norwegian
-   */
-  nn = "nn",
-  /**
-   * Norwegian
-   */
-  no = "no",
-  /**
-   * Ndebele, South; South Ndebele
-   */
-  nr = "nr",
-  /**
-   * Navajo; Navaho
-   */
-  nv = "nv",
-  /**
-   * Chichewa; Chewa; Nyanja
-   */
-  ny = "ny",
-  /**
-   * Occitan (post 1500); Provençal
-   */
-  oc = "oc",
-  /**
-   * Ojibwa
-   */
-  oj = "oj",
-  /**
-   * Oromo
-   */
-  om = "om",
-  /**
-   * Oriya
-   */
-  or = "or",
-  /**
-   * Ossetian; Ossetic
-   */
-  os = "os",
-  /**
-   * Panjabi; Punjabi
-   */
-  pa = "pa",
-  /**
-   * Pali
-   */
-  pi = "pi",
-  /**
-   * Polish
-   */
-  pl = "pl",
-  /**
-   * Pushto; Pashto
-   */
-  ps = "ps",
-  /**
-   * Portuguese
-   */
-  pt = "pt",
-  /**
-   * Quechua
-   */
-  qu = "qu",
-  /**
-   * Romansh
-   */
-  rm = "rm",
-  /**
-   * Rundi
-   */
-  rn = "rn",
-  /**
-   * Romanian; Moldavian; Moldovan
-   */
-  ro = "ro",
-  /**
-   * Russian
-   */
-  ru = "ru",
-  /**
-   * Kinyarwanda
-   */
-  rw = "rw",
-  /**
-   * Sanskrit
-   */
-  sa = "sa",
-  /**
-   * Sardinian
-   */
-  sc = "sc",
-  /**
-   * Sindhi
-   */
-  sd = "sd",
-  /**
-   * Northern Sami
-   */
-  se = "se",
-  /**
-   * Sango
-   */
-  sg = "sg",
-  /**
-   * Sinhala; Sinhalese
-   */
-  si = "si",
-  /**
-   * Slovak
-   */
-  sk = "sk",
-  /**
-   * Slovenian
-   */
-  sl = "sl",
-  /**
-   * Samoan
-   */
-  sm = "sm",
-  /**
-   * Shona
-   */
-  sn = "sn",
-  /**
-   * Somali
-   */
-  so = "so",
-  /**
-   * Albanian
-   */
-  sq = "sq",
-  /**
-   * Serbian
-   */
-  sr = "sr",
-  /**
-   * Swati
-   */
-  ss = "ss",
-  /**
-   * Sotho, Southern
-   */
-  st = "st",
-  /**
-   * Sundanese
-   */
-  su = "su",
-  /**
-   * Swedish
-   */
-  sv = "sv",
-  /**
-   * Swahili
-   */
-  sw = "sw",
-  /**
-   * Tamil
-   */
-  ta = "ta",
-  /**
-   * Telugu
-   */
-  te = "te",
-  /**
-   * Tajik
-   */
-  tg = "tg",
-  /**
-   * Thai
-   */
-  th = "th",
-  /**
-   * Tigrinya
-   */
-  ti = "ti",
-  /**
-   * Turkmen
-   */
-  tk = "tk",
-  /**
-   * Tagalog
-   */
-  tl = "tl",
-  /**
-   * Tswana
-   */
-  tn = "tn",
-  /**
-   * Tonga (Tonga Islands)
-   */
-  to = "to",
-  /**
-   * Turkish
-   */
-  tr = "tr",
-  /**
-   * Tsonga
-   */
-  ts = "ts",
-  /**
-   * Tatar
-   */
-  tt = "tt",
-  /**
-   * Twi
-   */
-  tw = "tw",
-  /**
-   * Tahitian
-   */
-  ty = "ty",
-  /**
-   * Uighur; Uyghur
-   */
-  ug = "ug",
-  /**
-   * Ukrainian
-   */
-  uk = "uk",
-  /**
-   * Urdu
-   */
-  ur = "ur",
-  /**
-   * Uzbek
-   */
-  uz = "uz",
-  /**
-   * Venda
-   */
-  ve = "ve",
-  /**
-   * Vietnamese
-   */
-  vi = "vi",
-  /**
-   * Volapük
-   */
-  vo = "vo",
-  /**
-   * Walloon
-   */
-  wa = "wa",
-  /**
-   * Wolof
-   */
-  wo = "wo",
-  /**
-   * Xhosa
-   */
-  xh = "xh",
-  /**
-   * Yiddish
-   */
-  yi = "yi",
-  /**
-   * Yoruba
-   */
-  yo = "yo",
-  /**
-   * Zhuang; Chuang
-   */
-  za = "za",
-  /**
-   * Chinese
-   */
-  zh = "zh",
-  /**
-   * Zulu
-   */
-  zu = "zu",
-}
-
-
-export interface GetProductByIdQueryVariables {
-  id: string,
-  languageCode?: LanguageCode | null,
-};
-
-export interface GetProductByIdQuery {
-  product:  {
-    id: string,
-    languageCode: LanguageCode | null,
-    name: string | null,
-    slug: string | null,
-    description: string | null,
-    translations:  Array< {
-      languageCode: LanguageCode,
-      name: string,
-      slug: string,
-      description: string | null,
-    } | null > | null,
-  } | null,
-};
-
-export interface GetProductListQueryVariables {
-  take?: number | null,
-  skip?: number | null,
-  languageCode?: LanguageCode | null,
-};
-
-export interface GetProductListQuery {
-  products:  {
-    items:  Array< {
-      id: string,
-      languageCode: LanguageCode | null,
-      name: string | null,
-      slug: string | null,
-      description: string | null,
-    } >,
-    totalItems: number,
-  } | null,
-};

+ 9 - 7
admin-ui/src/app/core/components/app-shell/app-shell.component.ts

@@ -1,9 +1,9 @@
 import { Component, OnInit } from '@angular/core';
 import { Router } from '@angular/router';
 import { Observable } from 'rxjs';
-
-import { StateStore } from '../../../state/state-store.service';
-import { UserActions } from '../../../state/user/user-actions';
+import { map } from 'rxjs/operators';
+import { DataService } from '../../../data/providers/data.service';
+import { AuthService } from '../../providers/auth/auth.service';
 
 @Component({
     selector: 'vdr-app-shell',
@@ -14,16 +14,18 @@ export class AppShellComponent implements OnInit {
 
     userName$: Observable<string>;
 
-    constructor(private store: StateStore,
-                private userActions: UserActions,
+    constructor(private authService: AuthService,
+                private dataService: DataService,
                 private router: Router) { }
 
     ngOnInit() {
-        this.userName$ = this.store.select(state => state.user.username);
+        this.userName$ = this.dataService.client.userStatus().single$.pipe(
+            map(data => data.userStatus.username),
+        );
     }
 
     logOut() {
-        this.userActions.logOut();
+        this.authService.logOut();
         this.router.navigate(['/login']);
     }
 

+ 4 - 5
admin-ui/src/app/core/components/breadcrumb/breadcrumb.component.ts

@@ -3,8 +3,7 @@ import { ActivatedRoute, Data, NavigationEnd, Params, PRIMARY_OUTLET, Router } f
 import { flatten } from 'lodash';
 import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subject } from 'rxjs';
 import { filter, map, takeUntil } from 'rxjs/operators';
-
-import { StateStore } from '../../../state/state-store.service';
+import { DataService } from '../../../data/providers/data.service';
 
 export type BreadcrumbString = string;
 export interface BreadcrumbLabelLinkPair {
@@ -12,7 +11,7 @@ export interface BreadcrumbLabelLinkPair {
     link: any[];
 }
 export type BreadcrumbValue = BreadcrumbString | BreadcrumbLabelLinkPair | BreadcrumbLabelLinkPair[];
-export type BreadcrumbFunction = (data: Data, params: Params, store: StateStore) => BreadcrumbValue | Observable<BreadcrumbValue>;
+export type BreadcrumbFunction = (data: Data, params: Params, dataService: DataService) => BreadcrumbValue | Observable<BreadcrumbValue>;
 export type BreadcrumbDefinition = BreadcrumbValue | BreadcrumbFunction | Observable<BreadcrumbValue>;
 
 /**
@@ -36,7 +35,7 @@ export class BreadcrumbComponent implements OnDestroy {
 
     constructor(private router: Router,
                 private route: ActivatedRoute,
-                private store: StateStore) {
+                private dataService: DataService) {
         this.router.events.pipe(
             filter(event => event instanceof NavigationEnd),
             takeUntil(this.destroy$))
@@ -99,7 +98,7 @@ export class BreadcrumbComponent implements OnDestroy {
 
                     if (breadcrumbDef) {
                         if (isBreadcrumbFunction(breadcrumbDef)) {
-                            breadcrumbDef = breadcrumbDef(routeSnapshot.data, routeSnapshot.params, this.store);
+                            breadcrumbDef = breadcrumbDef(routeSnapshot.data, routeSnapshot.params, this.dataService);
                         }
                         const observableValue = isObservable(breadcrumbDef) ? breadcrumbDef : observableOf(breadcrumbDef);
                         breadcrumbParts.push({ value$: observableValue, path: segmentPaths.slice() });

+ 4 - 31
admin-ui/src/app/core/core.module.ts

@@ -1,58 +1,31 @@
-import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
 import { NgModule } from '@angular/core';
-import { Apollo, APOLLO_OPTIONS, ApolloModule } from 'apollo-angular';
-import { HttpLink, HttpLinkModule } from 'apollo-angular-link-http';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-
-import { API_PATH } from '../../../../shared/shared-constants';
-import { API_URL } from '../app.config';
+import { DataModule } from '../data/data.module';
 import { SharedModule } from '../shared/shared.module';
-import { APOLLO_NGRX_CACHE, StateModule } from '../state/state.module';
-
 import { AppShellComponent } from './components/app-shell/app-shell.component';
 import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
 import { MainNavComponent } from './components/main-nav/main-nav.component';
 import { NotificationComponent } from './components/notification/notification.component';
 import { OverlayHostComponent } from './components/overlay-host/overlay-host.component';
 import { UserMenuComponent } from './components/user-menu/user-menu.component';
-import { BaseDataService } from './providers/data/base-data.service';
-import { DataService } from './providers/data/data.service';
-import { DefaultInterceptor } from './providers/data/interceptor';
+import { AuthService } from './providers/auth/auth.service';
 import { AuthGuard } from './providers/guard/auth.guard';
 import { LocalStorageService } from './providers/local-storage/local-storage.service';
 import { NotificationService } from './providers/notification/notification.service';
 import { OverlayHostService } from './providers/overlay-host/overlay-host.service';
 
-export function createApollo(httpLink: HttpLink, ngrxCache: InMemoryCache) {
-  return {
-    link: httpLink.create({ uri: `${API_URL}/${API_PATH}` }),
-    cache: ngrxCache,
-  };
-}
-
 @NgModule({
     imports: [
+        DataModule,
         SharedModule,
-        HttpClientModule,
-        ApolloModule,
-        HttpLinkModule,
-        StateModule,
     ],
     exports: [
         SharedModule,
         OverlayHostComponent,
     ],
     providers: [
-        {
-            provide: APOLLO_OPTIONS,
-            useFactory: createApollo,
-            deps: [HttpLink, APOLLO_NGRX_CACHE],
-        },
-        { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
-        BaseDataService,
         LocalStorageService,
-        DataService,
         AuthGuard,
+        AuthService,
         OverlayHostService,
         NotificationService,
     ],

+ 71 - 0
admin-ui/src/app/core/providers/auth/auth.service.ts

@@ -0,0 +1,71 @@
+import { Injectable } from '@angular/core';
+import { Observable, of } from 'rxjs';
+import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
+import { DataService } from '../../../data/providers/data.service';
+import { LogIn } from '../../../data/types/gql-generated-types';
+import { LocalStorageService } from '../local-storage/local-storage.service';
+
+/**
+ * This service handles logic relating to authentication of the current user.
+ */
+@Injectable()
+export class AuthService {
+
+    constructor(private localStorageService: LocalStorageService,
+                private dataService: DataService) {}
+
+    /**
+     * Attempts to log in via the REST login endpoint and updates the app
+     * state on success.
+     */
+    logIn(username: string, password: string): Observable<LogIn> {
+        return this.dataService.user.attemptLogin(username, password).pipe(
+            switchMap(response => {
+                this.localStorageService.set('authToken', response.token);
+                return this.dataService.client.loginSuccess(username);
+            }),
+        );
+    }
+
+    /**
+     * Update the user status to being logged out.
+     */
+    logOut(): void {
+        this.dataService.client.logOut();
+        this.localStorageService.remove('authToken');
+    }
+
+    /**
+     * Checks the app state to see if the user is already logged in,
+     * and if not, attempts to validate any auth token found.
+     */
+    checkAuthenticatedStatus(): Observable<boolean> {
+        return this.dataService.client.userStatus().single$.pipe(
+            mergeMap(data => {
+                if (!data.userStatus.isLoggedIn) {
+                    return this.validateAuthToken();
+                } else {
+                    return of(true);
+                }
+            }),
+        );
+    }
+
+    /**
+     * Checks for an auth token and if found, attempts to validate
+     * that token against the API.
+     */
+    validateAuthToken(): Observable<boolean> {
+        if (!this.localStorageService.get('authToken')) {
+            return of(false);
+        }
+
+        return this.dataService.user.checkLoggedIn().pipe(
+            map(result => {
+                this.dataService.client.loginSuccess(result.identifier);
+                return true;
+            }),
+            catchError(err => of(false)),
+        );
+    }
+}

+ 0 - 30
admin-ui/src/app/core/providers/data/product-data.service.ts

@@ -1,30 +0,0 @@
-import { QueryRef } from 'apollo-angular';
-
-import { ID } from '../../../../../../shared/shared-types';
-import { getProductById } from '../../../common/queries/get-product-by-id';
-import { getProductList } from '../../../common/queries/get-product-list';
-import {
-    GetProductByIdQuery,
-    GetProductByIdQueryVariables,
-    GetProductListQuery,
-    GetProductListQueryVariables,
-    LanguageCode,
-} from '../../../common/types/gql-generated-types';
-
-import { BaseDataService } from './base-data.service';
-
-export class ProductDataService {
-
-    constructor(private baseDataService: BaseDataService) {}
-
-    getProducts(take: number = 10, skip: number = 0): QueryRef<GetProductListQuery, GetProductListQueryVariables> {
-        return this.baseDataService
-            .query<GetProductListQuery, GetProductListQueryVariables>(getProductList, { take, skip, languageCode: LanguageCode.en });
-    }
-
-    getProduct(id: ID): any {
-        const stringId = id.toString();
-        return this.baseDataService.query<GetProductByIdQuery, GetProductByIdQueryVariables>(getProductById, { id: stringId });
-    }
-
-}

+ 9 - 15
admin-ui/src/app/core/providers/guard/auth.guard.ts

@@ -1,28 +1,22 @@
 import { Injectable } from '@angular/core';
 import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
-import { Observable, of } from 'rxjs';
-import { flatMap, mergeMap, tap } from 'rxjs/operators';
-
-import { StateStore } from '../../../state/state-store.service';
-import { UserActions } from '../../../state/user/user-actions';
+import { Observable } from 'rxjs';
+import { tap } from 'rxjs/operators';
+import { AuthService } from '../auth/auth.service';
 
+/**
+ * This guard prevents unauthorized users from accessing any routes which require
+ * authorization.
+ */
 @Injectable()
 export class AuthGuard implements CanActivate {
 
     constructor(private router: Router,
-                private userActions: UserActions,
-                private store: StateStore) {}
+                private authService: AuthService) {}
 
     canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
 
-        return this.store.select(state => state.user.isLoggedIn).pipe(
-            mergeMap(loggedIn => {
-                if (!loggedIn) {
-                    return this.userActions.checkAuth();
-                } else {
-                    return of(true);
-                }
-            }),
+        return this.authService.checkAuthenticatedStatus().pipe(
             tap(authenticated => {
                 if (authenticated) {
                     return true;

+ 14 - 0
admin-ui/src/app/data/client-state/client-defaults.ts

@@ -0,0 +1,14 @@
+import { GetNetworkStatus, GetUserStatus } from '../types/gql-generated-types';
+
+export const clientDefaults: GetNetworkStatus & GetUserStatus = {
+    networkStatus: {
+        inFlightRequests: 0,
+        __typename: 'NetworkStatus',
+    },
+    userStatus: {
+        username: '',
+        isLoggedIn: false,
+        loginTime: '',
+        __typename: 'UserStatus',
+    },
+};

+ 71 - 0
admin-ui/src/app/data/client-state/client-resolvers.ts

@@ -0,0 +1,71 @@
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import { GraphQLFieldResolver } from 'graphql';
+import { GET_NEWTORK_STATUS } from '../queries/local-queries';
+import {
+    GetNetworkStatus,
+    GetUserStatus,
+    GetUserStatus_userStatus,
+    LogInVariables,
+    RequestStarted,
+} from '../types/gql-generated-types';
+
+export type ResolverContext = {
+    cache: InMemoryCache;
+    optimisticResponse: any;
+    getCacheKey: (storeObj: any) => string;
+};
+
+export type ResolverDefinition = {
+    Mutation: {
+        [name: string]: GraphQLFieldResolver<any, ResolverContext>;
+    },
+};
+
+export const clientResolvers: ResolverDefinition = {
+    Mutation: {
+        requestStarted: (_, args, { cache }): number => {
+            return updateRequestsInFlight(cache, 1);
+        },
+        requestCompleted: (_, args, { cache }): number => {
+            return updateRequestsInFlight(cache, -1);
+        },
+        logIn: (_, args: LogInVariables, { cache }): GetUserStatus_userStatus => {
+            const { username, loginTime } = args;
+            const data: GetUserStatus = {
+                userStatus: {
+                    __typename: 'UserStatus',
+                    username,
+                    loginTime,
+                    isLoggedIn: true,
+                },
+            };
+            cache.writeData({ data });
+            return data.userStatus;
+        },
+        logOut: (_, args, { cache }): GetUserStatus_userStatus => {
+            const data: GetUserStatus = {
+                userStatus: {
+                    __typename: 'UserStatus',
+                    username: '',
+                    loginTime: '',
+                    isLoggedIn: false,
+                },
+            };
+            cache.writeData({ data });
+            return data.userStatus;
+        },
+    },
+};
+
+function updateRequestsInFlight(cache: InMemoryCache, increment: 1 | -1): number {
+    const previous = cache.readQuery<GetNetworkStatus>({ query: GET_NEWTORK_STATUS });
+    const inFlightRequests = previous.networkStatus.inFlightRequests + increment;
+    const data: GetNetworkStatus = {
+        networkStatus: {
+            __typename: 'NetworkStatus',
+            inFlightRequests,
+        },
+    };
+    cache.writeData({ data });
+    return inFlightRequests;
+}

+ 61 - 0
admin-ui/src/app/data/data.module.ts

@@ -0,0 +1,61 @@
+import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
+import { NgModule } from '@angular/core';
+import { APOLLO_OPTIONS, ApolloModule } from 'apollo-angular';
+import { HttpLink, HttpLinkModule } from 'apollo-angular-link-http';
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import { ApolloLink } from 'apollo-link';
+import { withClientState } from 'apollo-link-state';
+import { API_PATH } from '../../../../shared/shared-constants';
+import { environment } from '../../environments/environment';
+import { API_URL } from '../app.config';
+import { clientDefaults } from './client-state/client-defaults';
+import { clientResolvers } from './client-state/client-resolvers';
+import { BaseDataService } from './providers/base-data.service';
+import { DataService } from './providers/data.service';
+import { DefaultInterceptor } from './providers/interceptor';
+
+const apolloCache = new InMemoryCache();
+
+if (!environment.production) {
+    // make the Apollo Cache inspectable in the console for debug purposes
+    (window as any)['apolloCache'] = apolloCache;
+}
+
+const stateLink = withClientState({
+    cache: apolloCache,
+    resolvers: clientResolvers,
+    defaults: clientDefaults,
+});
+
+export function createApollo(httpLink: HttpLink) {
+    return {
+        link:  ApolloLink.from([stateLink, httpLink.create({ uri: `${API_URL}/${API_PATH}` })]),
+        cache: apolloCache,
+    };
+}
+
+/**
+ * The DataModule is responsible for all API calls *and* serves as the source of truth for global app
+ * state via the apollo-link-state package.
+ */
+@NgModule({
+    imports: [
+        ApolloModule,
+        HttpLinkModule,
+        HttpClientModule,
+    ],
+    exports: [],
+    declarations: [],
+    providers: [
+        BaseDataService,
+        DataService,
+        {
+            provide: APOLLO_OPTIONS,
+            useFactory: createApollo,
+            deps: [HttpLink],
+        },
+        { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
+    ],
+})
+export class DataModule {
+}

+ 33 - 0
admin-ui/src/app/data/mutations/local-mutations.ts

@@ -0,0 +1,33 @@
+import gql from 'graphql-tag';
+
+export const REQUEST_STARTED = gql`
+    mutation RequestStarted {
+        requestStarted @client
+    }
+`;
+
+export const REQUEST_COMPLETED = gql`
+    mutation RequestCompleted {
+        requestCompleted @client
+    }
+`;
+
+export const LOG_IN = gql`
+    mutation LogIn($username: String!, $loginTime: String!) {
+        logIn(username: $username, loginTime: $loginTime) @client {
+            username
+            isLoggedIn
+            loginTime
+        }
+    }
+`;
+
+export const LOG_OUT = gql`
+    mutation LogOut {
+        logOut @client {
+            username
+            isLoggedIn
+            loginTime
+        }
+    }
+`;

+ 16 - 7
admin-ui/src/app/core/providers/data/base-data.service.ts → admin-ui/src/app/data/providers/base-data.service.ts

@@ -1,12 +1,12 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
-import { Apollo, QueryRef } from 'apollo-angular';
+import { Apollo } from 'apollo-angular';
 import { DocumentNode } from 'graphql/language/ast';
 import { Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
-
-import { API_URL } from '../../../app.config';
-import { LocalStorageService } from '../local-storage/local-storage.service';
+import { API_URL } from '../../app.config';
+import { LocalStorageService } from '../../core/providers/local-storage/local-storage.service';
+import { QueryResult } from '../types/query-result';
 
 @Injectable()
 export class BaseDataService {
@@ -16,10 +16,10 @@ export class BaseDataService {
                 private localStorageService: LocalStorageService) {}
 
     /**
-     * Performs a GraphQL query
+     * Performs a GraphQL watch query
      */
-    query<T, V = Record<string, any>>(query: DocumentNode, variables?: V): QueryRef<T> {
-        return this.apollo.watchQuery<T, V>({
+    query<T, V = Record<string, any>>(query: DocumentNode, variables?: V): QueryResult<T, V> {
+        const queryRef = this.apollo.watchQuery<T, V>({
             query,
             variables,
             context: {
@@ -28,6 +28,15 @@ export class BaseDataService {
                 },
             },
         });
+        return new QueryResult<T, any>(queryRef);
+    }
+
+    /**
+     * Performs a GraphQL mutation
+     */
+    mutate<T, V = Record<string, any>>(mutation: DocumentNode, variables?: V): Observable<T> {
+        return this.apollo.mutate<T, V>({ mutation, variables }).pipe(
+            map(result => result.data as T));
     }
 
     /**

+ 39 - 0
admin-ui/src/app/data/providers/client-data.service.ts

@@ -0,0 +1,39 @@
+import { Observable } from 'rxjs';
+import { LOG_IN, LOG_OUT, REQUEST_COMPLETED, REQUEST_STARTED } from '../mutations/local-mutations';
+import { GET_NEWTORK_STATUS, GET_USER_STATUS } from '../queries/local-queries';
+import { GetNetworkStatus, GetUserStatus, LogIn, LogInVariables, LogOut, RequestCompleted, RequestStarted } from '../types/gql-generated-types';
+import { QueryResult } from '../types/query-result';
+import { BaseDataService } from './base-data.service';
+
+export class ClientDataService {
+
+    constructor(private baseDataService: BaseDataService) {}
+
+    startRequest(): Observable<RequestStarted> {
+        return this.baseDataService.mutate<RequestStarted>(REQUEST_STARTED);
+    }
+
+    completeRequest(): Observable<RequestCompleted> {
+        return this.baseDataService.mutate<RequestCompleted>(REQUEST_COMPLETED);
+    }
+
+    getNetworkStatus(): QueryResult<GetNetworkStatus> {
+        return this.baseDataService.query<GetNetworkStatus>(GET_NEWTORK_STATUS);
+    }
+
+    loginSuccess(username: string): Observable<LogIn> {
+        return this.baseDataService.mutate<LogIn, LogInVariables>(LOG_IN, {
+            username,
+            loginTime: Date.now().toString(),
+        });
+    }
+
+    logOut(): Observable<LogOut> {
+        return this.baseDataService.mutate(LOG_OUT);
+    }
+
+    userStatus(): QueryResult<GetUserStatus> {
+        return this.baseDataService.query<GetUserStatus>(GET_USER_STATUS);
+    }
+
+}

+ 3 - 0
admin-ui/src/app/core/providers/data/data.service.ts → admin-ui/src/app/data/providers/data.service.ts

@@ -1,6 +1,7 @@
 import { Injectable } from '@angular/core';
 
 import { BaseDataService } from './base-data.service';
+import { ClientDataService } from './client-data.service';
 import { ProductDataService } from './product-data.service';
 import { UserDataService } from './user-data.service';
 
@@ -8,10 +9,12 @@ import { UserDataService } from './user-data.service';
 export class DataService {
     user: UserDataService;
     product: ProductDataService;
+    client: ClientDataService;
 
     constructor(baseDataService: BaseDataService) {
         this.user = new UserDataService(baseDataService);
         this.product = new ProductDataService(baseDataService);
+        this.client = new ClientDataService(baseDataService);
     }
 
 }

+ 6 - 6
admin-ui/src/app/core/providers/data/interceptor.ts → admin-ui/src/app/data/providers/interceptor.ts

@@ -4,8 +4,8 @@ import { Injectable } from '@angular/core';
 import { Observable, throwError } from 'rxjs';
 import { catchError, tap } from 'rxjs/operators';
 
-import { ApiActions } from '../../../state/api/api-actions';
-import { NotificationService } from '../notification/notification.service';
+import { NotificationService } from '../../core/providers/notification/notification.service';
+import { DataService } from './data.service';
 
 /**
  * The default interceptor examines all HTTP requests & responses and automatically updates the requesting state
@@ -14,23 +14,23 @@ import { NotificationService } from '../notification/notification.service';
 @Injectable()
 export class DefaultInterceptor implements HttpInterceptor {
 
-    constructor(private apiActions: ApiActions,
+    constructor(private dataService: DataService,
                 private notification: NotificationService) {}
 
     intercept(req: HttpRequest<any>, next: HttpHandler):
         Observable<HttpEvent<any>> {
-        this.apiActions.startRequest();
+        this.dataService.client.startRequest().subscribe();
         return next.handle(req).pipe(
             tap(event => {
                     if (event instanceof HttpResponse) {
                         this.notifyOnGraphQLErrors(event);
-                        this.apiActions.requestCompleted();
+                        this.dataService.client.completeRequest().subscribe();
                     }
                 },
                 err => {
                     if (err instanceof HttpErrorResponse) {
                         this.notification.error(err.message);
-                        this.apiActions.requestCompleted();
+                        this.dataService.client.completeRequest().subscribe();
                     }
                 }),
         );

+ 24 - 0
admin-ui/src/app/data/providers/product-data.service.ts

@@ -0,0 +1,24 @@
+import { ID } from '../../../../../shared/shared-types';
+import { GET_PRODUCT_BY_ID, GET_PRODUCT_LIST } from '../queries/product-queries';
+import { GetProductById, GetProductByIdVariables, GetProductList, GetProductListVariables, LanguageCode } from '../types/gql-generated-types';
+import { QueryResult } from '../types/query-result';
+import { BaseDataService } from './base-data.service';
+
+export class ProductDataService {
+
+    constructor(private baseDataService: BaseDataService) {}
+
+    getProducts(take: number = 10, skip: number = 0): QueryResult<GetProductList, GetProductListVariables> {
+        return this.baseDataService
+            .query<GetProductList, GetProductListVariables>(GET_PRODUCT_LIST, { take, skip, languageCode: LanguageCode.en });
+    }
+
+    getProduct(id: ID): QueryResult<GetProductById> {
+        const stringId = id.toString();
+        return this.baseDataService.query<GetProductById, GetProductByIdVariables>(GET_PRODUCT_BY_ID, {
+            id: stringId,
+            languageCode: LanguageCode.en,
+        });
+    }
+
+}

+ 2 - 2
admin-ui/src/app/core/providers/data/user-data.service.ts → admin-ui/src/app/data/providers/user-data.service.ts

@@ -1,6 +1,6 @@
 import { Observable } from 'rxjs';
 
-import { LoginResponse, UserResponse } from '../../../common/types/response';
+import { LoginResponse, UserResponse } from '../types/response';
 
 import { BaseDataService } from './base-data.service';
 
@@ -12,7 +12,7 @@ export class UserDataService {
         return this.baseDataService.get('auth/me');
     }
 
-    logIn(username: string, password: string): Observable<LoginResponse> {
+    attemptLogin(username: string, password: string): Observable<LoginResponse> {
         return this.baseDataService.post('auth/login', {
             username,
             password,

+ 19 - 0
admin-ui/src/app/data/queries/local-queries.ts

@@ -0,0 +1,19 @@
+import gql from 'graphql-tag';
+
+export const GET_NEWTORK_STATUS = gql`
+    query GetNetworkStatus {
+        networkStatus @client {
+            inFlightRequests
+        }
+    }
+`;
+
+export const GET_USER_STATUS = gql`
+    query GetUserStatus {
+        userStatus @client {
+            username
+            isLoggedIn
+            loginTime
+        }
+    }
+`;

+ 34 - 0
admin-ui/src/app/data/queries/product-queries.ts

@@ -0,0 +1,34 @@
+import gql from 'graphql-tag';
+
+export const GET_PRODUCT_BY_ID = gql`
+    query GetProductById($id: ID!, $languageCode: LanguageCode){
+        product(languageCode: $languageCode, id: $id) {
+            id
+            languageCode
+            name
+            slug
+            description
+            translations {
+                languageCode
+                name
+                slug
+                description
+            }
+        }
+    }
+`;
+
+export const GET_PRODUCT_LIST = gql`
+    query GetProductList($take: Int, $skip: Int, $languageCode: LanguageCode){
+        products(languageCode: $languageCode, take: $take, skip: $skip) {
+            items {
+                id
+                languageCode
+                name
+                slug
+                description
+            }
+            totalItems
+        }
+    }
+`;

+ 21 - 0
admin-ui/src/app/data/types/client-types.graphql

@@ -0,0 +1,21 @@
+type Query {
+    networkStatus: NetworkStatus!
+    userStatus: UserStatus!
+}
+
+type Mutation {
+    requestStarted: Int!
+    requestCompleted: Int!
+    logIn(username: String!, loginTime: String!): UserStatus
+    logOut: UserStatus
+}
+
+type NetworkStatus {
+    inFlightRequests: Int!
+}
+
+type UserStatus {
+    username: String!
+    isLoggedIn: Boolean!
+    loginTime: String!
+}

+ 370 - 0
admin-ui/src/app/data/types/gql-generated-types.ts

@@ -0,0 +1,370 @@
+
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: RequestStarted
+// ====================================================
+
+export interface RequestStarted {
+  requestStarted: number;
+}
+
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: RequestCompleted
+// ====================================================
+
+export interface RequestCompleted {
+  requestCompleted: number;
+}
+
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: LogIn
+// ====================================================
+
+export interface LogIn_logIn {
+  __typename: "UserStatus";
+  username: string;
+  isLoggedIn: boolean;
+  loginTime: string;
+}
+
+export interface LogIn {
+  logIn: LogIn_logIn | null;
+}
+
+export interface LogInVariables {
+  username: string;
+  loginTime: string;
+}
+
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: LogOut
+// ====================================================
+
+export interface LogOut_logOut {
+  __typename: "UserStatus";
+  username: string;
+  isLoggedIn: boolean;
+  loginTime: string;
+}
+
+export interface LogOut {
+  logOut: LogOut_logOut | null;
+}
+
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetProductById
+// ====================================================
+
+export interface GetProductById_product_translations {
+  __typename: "ProductTranslation";
+  languageCode: LanguageCode;
+  name: string;
+  slug: string;
+  description: string | null;
+}
+
+export interface GetProductById_product {
+  __typename: "Product";
+  id: string;
+  languageCode: LanguageCode | null;
+  name: string | null;
+  slug: string | null;
+  description: string | null;
+  translations: (GetProductById_product_translations | null)[] | null;
+}
+
+export interface GetProductById {
+  product: GetProductById_product | null;
+}
+
+export interface GetProductByIdVariables {
+  id: string;
+  languageCode?: LanguageCode | null;
+}
+
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetProductList
+// ====================================================
+
+export interface GetProductList_products_items {
+  __typename: "Product";
+  id: string;
+  languageCode: LanguageCode | null;
+  name: string | null;
+  slug: string | null;
+  description: string | null;
+}
+
+export interface GetProductList_products {
+  __typename: "ProductList";
+  items: GetProductList_products_items[];
+  totalItems: number;
+}
+
+export interface GetProductList {
+  products: GetProductList_products | null;
+}
+
+export interface GetProductListVariables {
+  take?: number | null;
+  skip?: number | null;
+  languageCode?: LanguageCode | null;
+}
+
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetNetworkStatus
+// ====================================================
+
+export interface GetNetworkStatus_networkStatus {
+  __typename: "NetworkStatus";
+  inFlightRequests: number;
+}
+
+export interface GetNetworkStatus {
+  networkStatus: GetNetworkStatus_networkStatus;
+}
+
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: GetUserStatus
+// ====================================================
+
+export interface GetUserStatus_userStatus {
+  __typename: "UserStatus";
+  username: string;
+  isLoggedIn: boolean;
+  loginTime: string;
+}
+
+export interface GetUserStatus {
+  userStatus: GetUserStatus_userStatus;
+}
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+//==============================================================
+// START Enums and Input Objects
+//==============================================================
+
+// ISO 639-1 language code
+export enum LanguageCode {
+  aa = "aa",
+  ab = "ab",
+  ae = "ae",
+  af = "af",
+  ak = "ak",
+  am = "am",
+  an = "an",
+  ar = "ar",
+  as = "as",
+  av = "av",
+  ay = "ay",
+  az = "az",
+  ba = "ba",
+  be = "be",
+  bg = "bg",
+  bh = "bh",
+  bi = "bi",
+  bm = "bm",
+  bn = "bn",
+  bo = "bo",
+  br = "br",
+  bs = "bs",
+  ca = "ca",
+  ce = "ce",
+  ch = "ch",
+  co = "co",
+  cr = "cr",
+  cs = "cs",
+  cu = "cu",
+  cv = "cv",
+  cy = "cy",
+  da = "da",
+  de = "de",
+  dv = "dv",
+  dz = "dz",
+  ee = "ee",
+  el = "el",
+  en = "en",
+  eo = "eo",
+  es = "es",
+  et = "et",
+  eu = "eu",
+  fa = "fa",
+  ff = "ff",
+  fi = "fi",
+  fj = "fj",
+  fo = "fo",
+  fr = "fr",
+  fy = "fy",
+  ga = "ga",
+  gd = "gd",
+  gl = "gl",
+  gn = "gn",
+  gu = "gu",
+  gv = "gv",
+  ha = "ha",
+  he = "he",
+  hi = "hi",
+  ho = "ho",
+  hr = "hr",
+  ht = "ht",
+  hu = "hu",
+  hy = "hy",
+  hz = "hz",
+  ia = "ia",
+  id = "id",
+  ie = "ie",
+  ig = "ig",
+  ii = "ii",
+  ik = "ik",
+  io = "io",
+  is = "is",
+  it = "it",
+  iu = "iu",
+  ja = "ja",
+  jv = "jv",
+  ka = "ka",
+  kg = "kg",
+  ki = "ki",
+  kj = "kj",
+  kk = "kk",
+  kl = "kl",
+  km = "km",
+  kn = "kn",
+  ko = "ko",
+  kr = "kr",
+  ks = "ks",
+  ku = "ku",
+  kv = "kv",
+  kw = "kw",
+  ky = "ky",
+  la = "la",
+  lb = "lb",
+  lg = "lg",
+  li = "li",
+  ln = "ln",
+  lo = "lo",
+  lt = "lt",
+  lu = "lu",
+  lv = "lv",
+  mg = "mg",
+  mh = "mh",
+  mi = "mi",
+  mk = "mk",
+  ml = "ml",
+  mn = "mn",
+  mr = "mr",
+  ms = "ms",
+  mt = "mt",
+  my = "my",
+  na = "na",
+  nb = "nb",
+  nd = "nd",
+  ne = "ne",
+  ng = "ng",
+  nl = "nl",
+  nn = "nn",
+  no = "no",
+  nr = "nr",
+  nv = "nv",
+  ny = "ny",
+  oc = "oc",
+  oj = "oj",
+  om = "om",
+  or = "or",
+  os = "os",
+  pa = "pa",
+  pi = "pi",
+  pl = "pl",
+  ps = "ps",
+  pt = "pt",
+  qu = "qu",
+  rm = "rm",
+  rn = "rn",
+  ro = "ro",
+  ru = "ru",
+  rw = "rw",
+  sa = "sa",
+  sc = "sc",
+  sd = "sd",
+  se = "se",
+  sg = "sg",
+  si = "si",
+  sk = "sk",
+  sl = "sl",
+  sm = "sm",
+  sn = "sn",
+  so = "so",
+  sq = "sq",
+  sr = "sr",
+  ss = "ss",
+  st = "st",
+  su = "su",
+  sv = "sv",
+  sw = "sw",
+  ta = "ta",
+  te = "te",
+  tg = "tg",
+  th = "th",
+  ti = "ti",
+  tk = "tk",
+  tl = "tl",
+  tn = "tn",
+  to = "to",
+  tr = "tr",
+  ts = "ts",
+  tt = "tt",
+  tw = "tw",
+  ty = "ty",
+  ug = "ug",
+  uk = "uk",
+  ur = "ur",
+  uz = "uz",
+  ve = "ve",
+  vi = "vi",
+  vo = "vo",
+  wa = "wa",
+  wo = "wo",
+  xh = "xh",
+  yi = "yi",
+  yo = "yo",
+  za = "za",
+  zh = "zh",
+  zu = "zu",
+}
+
+//==============================================================
+// END Enums and Input Objects
+//==============================================================

+ 35 - 0
admin-ui/src/app/data/types/query-result.ts

@@ -0,0 +1,35 @@
+import { QueryRef } from 'apollo-angular';
+import { Observable } from 'rxjs';
+import { map, take } from 'rxjs/operators';
+
+/**
+ * This class wraps the Apollo Angular QueryRef object and exposes some getters
+ * for convenience.
+ */
+export class QueryResult<T, V = Record<string, any>> {
+    constructor(private queryRef: QueryRef<T, V>) {}
+
+    /**
+     * Returns an Observable which emits a single result and then completes.
+     */
+    get single$(): Observable<T> {
+        return this.queryRef.valueChanges.pipe(
+            take(1),
+            map(result => result.data),
+        );
+    }
+
+    /**
+     * Returns an Observable which emits until unsubscribed.
+     */
+    get stream$(): Observable<T> {
+        return this.queryRef.valueChanges.pipe(
+            map(result => result.data),
+        );
+    }
+
+    get ref(): QueryRef<T, V> {
+        return this.queryRef;
+    }
+
+}

+ 0 - 0
admin-ui/src/app/common/types/response.ts → admin-ui/src/app/data/types/response.ts


+ 5 - 8
admin-ui/src/app/login/components/login/login.component.ts

@@ -1,9 +1,8 @@
 import { HttpErrorResponse } from '@angular/common/http';
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
 import { Router } from '@angular/router';
-
 import { API_URL } from '../../../app.config';
-import { UserActions } from '../../../state/user/user-actions';
+import { AuthService } from '../../../core/providers/auth/auth.service';
 
 @Component({
     selector: 'vdr-login',
@@ -16,15 +15,13 @@ export class LoginComponent {
     password = '';
     lastError = '';
 
-    constructor(private userActions: UserActions,
+    constructor(private authService: AuthService,
                 private router: Router) { }
 
     logIn(): void {
-        this.userActions.logIn(this.username, this.password)
+        this.authService.logIn(this.username, this.password)
             .subscribe(
-                () => {
-                    this.router.navigate(['/']);
-                },
+                () => this.router.navigate(['/']),
                 (err: HttpErrorResponse) => {
                     switch (err.status) {
                         case 401:

+ 0 - 34
admin-ui/src/app/state/action-logger.ts

@@ -1,34 +0,0 @@
-import { ActionReducer } from '@ngrx/store';
-import { storeLogger } from 'ngrx-store-logger';
-
-import { AppState } from './app-state';
-
-/**
- * A meta reducer which logs actions to the console for debugging purposes.
- */
-export function actionLogger(reducer: ActionReducer<AppState>): any {
-    const key = 'vdr_logToConsole';
-    const localSetting = sessionStorage.getItem(key);
-    let logToConsole = localSetting === 'true';
-
-    Object.defineProperty(window, 'vdr_log_actions', {
-        get: () => {
-            return logToConsole ? 'Logging actions enabled' : 'No longer logging actions';
-        },
-        set: (active: any) => {
-            logToConsole = !!active;
-            sessionStorage.setItem(key, logToConsole.toString());
-        },
-    });
-
-    return (state, action) => {
-        if (logToConsole) {
-            return storeLogger({
-                collapsed: true,
-            })(reducer)(state, action);
-        } else {
-            return reducer(state, action);
-        }
-    };
-
-}

+ 0 - 19
admin-ui/src/app/state/api/api-actions.ts

@@ -1,19 +0,0 @@
-import { Injectable } from '@angular/core';
-
-import { StateStore } from '../state-store.service';
-
-import {Actions} from './api-state';
-
-@Injectable()
-export class ApiActions {
-
-    constructor(private store: StateStore) {}
-
-    startRequest(): void {
-        this.store.dispatch(new Actions.StartRequest());
-    }
-
-    requestCompleted(): void {
-        this.store.dispatch(new Actions.RequestCompleted());
-    }
-}

+ 0 - 26
admin-ui/src/app/state/api/api-reducer.ts

@@ -1,26 +0,0 @@
-import produce from 'immer';
-
-import {reducerCaseNever} from '../../common/utilities/common';
-
-import {Actions, ActionType, ApiState, initialApiState} from './api-state';
-
-/**
- * Reducer for user (auth state etc.)
- */
-export function api(state: ApiState = initialApiState, action: Actions): ApiState {
-    return produce(state, draft => {
-        switch (action.type) {
-
-            case ActionType.START_REQUEST:
-                draft.inFlightRequests++;
-                return draft;
-
-            case ActionType.REQUEST_COMPLETED:
-                draft.inFlightRequests--;
-                return draft;
-
-            default:
-                reducerCaseNever(action);
-        }
-    });
-}

+ 0 - 31
admin-ui/src/app/state/api/api-state.ts

@@ -1,31 +0,0 @@
-import { Action } from '@ngrx/store';
-
-export interface ApiState {
-    inFlightRequests: number;
-}
-
-export const initialApiState: ApiState = {
-    inFlightRequests: 0,
-};
-
-export enum ActionType  {
-    START_REQUEST = 'api/START_REQUEST',
-    REQUEST_COMPLETED = 'api/REQUEST_COMPLETED',
-}
-
-export class StartRequest implements Action {
-    readonly type = ActionType.START_REQUEST;
-}
-
-export class RequestCompleted implements Action {
-    readonly type = ActionType.REQUEST_COMPLETED;
-}
-
-export const Actions = {
-    StartRequest,
-    RequestCompleted,
-};
-
-export type Actions =
-    StartRequest |
-    RequestCompleted;

+ 0 - 10
admin-ui/src/app/state/app-state.ts

@@ -1,10 +0,0 @@
-import { NormalizedCacheObject } from 'apollo-cache-inmemory/src/types';
-
-import { ApiState } from './api/api-state';
-import { UserState } from './user/user-state';
-
-export interface AppState {
-    api: ApiState;
-    entities: NormalizedCacheObject;
-    user: UserState;
-}

+ 0 - 24
admin-ui/src/app/state/handle-error.ts

@@ -1,24 +0,0 @@
-import {EMPTY, Observable} from 'rxjs';
-import {catchError} from 'rxjs/operators';
-
-/**
- * An error-handling operator which will execute the onErrorSideEffect() function and then
- * catch the error and complete the stream, so that any further operators are bypassed.
- *
- * Designed to be used with the .let() operator or in the pipable style.
- *
- * @example
- * ```
- * this.dataService.fetchData()
- *  .let(handleError(err => this.store.dispatch(new Actions.FetchError(err)))
- *  .do(data => this.store.dispatch(new Actions.FetchSuccess(data))
- *  .subscribe();
- * ```
- */
-export function handleError<T>(onErrorSideEffect: (err: any) => void): (observable: Observable<T>) => Observable<T> {
-    return (observable: Observable<T>) => observable.pipe(
-        catchError(err => {
-            onErrorSideEffect(err);
-            return EMPTY;
-        }));
-}

+ 0 - 85
admin-ui/src/app/state/state-store.service.ts

@@ -1,85 +0,0 @@
-import { Injectable } from '@angular/core';
-import { Action, Store } from '@ngrx/store';
-import { Observable, Subscription } from 'rxjs';
-import { distinctUntilChanged, take } from 'rxjs/operators';
-
-import { AppState } from './app-state';
-
-/**
- * Wrapper class which wraps the @ngrx/store Store object, and also provides additional
- * convenience methods for accessing data.
- */
-@Injectable()
-export class StateStore {
-
-    observeStateSubscriptions: Subscription[] = [];
-
-    constructor(public _internalStore: Store<AppState>) {
-        // expose the AppStore on the window for debug purposes
-        Object.defineProperty(window, 'vdr_app_state', {
-            get: () => this.getState(),
-        });
-        // allow observing of particular state for debug purposes
-        Object.defineProperty(window, 'vdr_observe_state', {
-            get: () => this.observeState.bind(this),
-        });
-    }
-
-    /**
-     * The store object can be manually set (mainly for testing purposes).
-     */
-    public setStore(store): void {
-        this._internalStore = store;
-    }
-
-    public getState(): AppState {
-        // Hacky typing here because TypeScript does not know that .take().subscribe() is a
-        // synchronous operation, and therefore complains that "state" has not been assigned
-        // when it is returned.
-        let state = {} as any;
-        this._internalStore.pipe(take(1)).subscribe(s => state = s);
-        return state;
-    }
-
-    public select<T>(selector: (state: AppState) => T): Observable<T> {
-        return this._internalStore.select<T>(selector).pipe(
-            distinctUntilChanged());
-    }
-
-    public dispatch(action: Action): void {
-        return this._internalStore.dispatch(action);
-    }
-
-    public subscribe(callback: (_) => any): Subscription {
-        return this._internalStore.subscribe(callback);
-    }
-
-    /**
-     * Allows the creation of ad-hoc subscriptions to the app state, and logs the value whenever that part of the state changes.
-     *
-     * Returns an unsubscribe function.
-     */
-    public observeState(selector: (state: AppState) => any): (() => void) | void  {
-        let hasError = false;
-
-        // tslint:disable:no-console
-        const sub = this.select(selector)
-            .subscribe(
-                value => console.log(`%c ${selector.toString()}:`, 'color: #0d35a9;', value),
-                err => {
-                    console.log(`%c "${selector.toString()}" is an invalid selector function:`, 'color: #f00', err.message);
-                    hasError = true;
-                },
-            );
-        // tslint:enable:no-console
-
-        if (!hasError) {
-            this.observeStateSubscriptions.push(sub);
-            return () => {
-                sub.unsubscribe();
-                const index = this.observeStateSubscriptions.indexOf(sub);
-                this.observeStateSubscriptions.splice(index, 1);
-            };
-        }
-    }
-}

+ 0 - 44
admin-ui/src/app/state/state.module.ts

@@ -1,44 +0,0 @@
-import { InjectionToken, NgModule } from '@angular/core';
-import { Store, StoreModule } from '@ngrx/store';
-import { apolloReducer, NgrxCache, NgrxCacheModule } from 'apollo-angular-cache-ngrx';
-import { InMemoryCache } from 'apollo-cache-inmemory';
-
-import { environment } from '../../environments/environment';
-
-import { actionLogger } from './action-logger';
-import { ApiActions } from './api/api-actions';
-import { api } from './api/api-reducer';
-import { StateStore } from './state-store.service';
-import { UserActions } from './user/user-actions';
-import { user } from './user/user-reducer';
-
-export const APOLLO_NGRX_CACHE = new InjectionToken<InMemoryCache>('APOLLO_NGRX_CACHE');
-
-export function createApolloNgrxCache(ngrxCache: NgrxCache, store: Store<any>): InMemoryCache {
-    return ngrxCache.create();
-}
-
-export const metaReducers = environment.production ? [] : [actionLogger];
-
-@NgModule({
-    imports: [
-        NgrxCacheModule,
-        StoreModule.forRoot({
-            entities: apolloReducer,
-            api,
-            user,
-        }, { metaReducers }),
-        NgrxCacheModule.forRoot('entities'),
-    ],
-    providers: [
-        {
-            provide: APOLLO_NGRX_CACHE,
-            useFactory: createApolloNgrxCache,
-            deps: [NgrxCache, Store],
-        },
-        UserActions,
-        StateStore,
-        ApiActions,
-    ],
-})
-export class StateModule {}

+ 0 - 61
admin-ui/src/app/state/user/user-actions.ts

@@ -1,61 +0,0 @@
-import {Injectable} from '@angular/core';
-import { EMPTY, Observable, of, throwError } from 'rxjs';
-import { catchError, map } from 'rxjs/operators';
-
-import { DataService } from '../../core/providers/data/data.service';
-import { LocalStorageService } from '../../core/providers/local-storage/local-storage.service';
-import { handleError } from '../handle-error';
-import { StateStore } from '../state-store.service';
-
-import {Actions} from './user-state';
-
-@Injectable()
-export class UserActions {
-
-    constructor(private store: StateStore,
-                private localStorageService: LocalStorageService,
-                private dataService: DataService) {}
-
-    checkAuth(): Observable<boolean> {
-        if (!this.localStorageService.get('authToken')) {
-            return of(false);
-        }
-
-        this.store.dispatch(new Actions.Login());
-
-        return this.dataService.user.checkLoggedIn().pipe(
-            map(result => {
-                this.store.dispatch(new Actions.LoginSuccess({
-                    username: result.identifier,
-                    loginTime: Date.now(),
-                }));
-                return true;
-            }),
-            catchError(err => of(false)),
-        );
-    }
-
-    logIn(username: string, password: string): Observable<any> {
-        this.store.dispatch(new Actions.Login());
-
-        return this.dataService.user.logIn(username, password).pipe(
-            map(result => {
-                this.store.dispatch(new Actions.LoginSuccess({
-                    username,
-                    loginTime: Date.now(),
-                }));
-                this.localStorageService.set('authToken', result.token);
-            }),
-            catchError(err => {
-                this.store.dispatch(new Actions.LoginError(err));
-                return throwError(err);
-            }),
-        );
-    }
-
-    logOut(): void {
-        this.localStorageService.remove('authToken');
-        this.store.dispatch(new Actions.Logout());
-    }
-
-}

+ 0 - 65
admin-ui/src/app/state/user/user-reducer.spec.ts

@@ -1,65 +0,0 @@
-import { user } from './user-reducer';
-import { Actions, UserState } from './user-state';
-
-describe('user reducer', () => {
-
-    it('should handle LOGIN', () => {
-        const state = {} as UserState;
-        const action = new Actions.Login();
-        const newState = user(state, action);
-
-        expect(newState).toEqual({
-            loggingIn: true,
-        } as any);
-    });
-
-    it('should handle LOGIN_SUCCESS', () => {
-        const state = {
-            loggingIn: true,
-            loginTime: -1,
-        } as UserState;
-        const action = new Actions.LoginSuccess({
-            username: 'test',
-            loginTime: 12345,
-        });
-        const newState = user(state, action);
-
-        expect(newState).toEqual({
-            username: 'test',
-            loggingIn: false,
-            isLoggedIn: true,
-            loginTime: 12345,
-        } as any);
-    });
-
-    it('should handle LOGIN_ERROR', () => {
-        const state = {
-            loggingIn: true,
-        } as UserState;
-        const action = new Actions.LoginError({ message: 'an error message' });
-        const newState = user(state, action);
-
-        expect(newState).toEqual({
-            loggingIn: false,
-            isLoggedIn: false,
-        } as any);
-    });
-
-    it('should handle LOGOUT', () => {
-        const state = {
-            username: 'test',
-            isLoggedIn: true,
-            loginTime: 12345,
-        } as UserState;
-        const action = new Actions.Logout();
-        const newState = user(state, action);
-
-        expect(newState).toEqual({
-            username: '',
-            loggingIn: false,
-            isLoggedIn: false,
-            loginTime: -1,
-        } as any);
-    });
-
-});

+ 0 - 41
admin-ui/src/app/state/user/user-reducer.ts

@@ -1,41 +0,0 @@
-import produce from 'immer';
-
-import {reducerCaseNever} from '../../common/utilities/common';
-
-import {Actions, ActionType, initialUserState, UserState} from './user-state';
-
-/**
- * Reducer for user (auth state etc.)
- */
-export function user(state: UserState = initialUserState, action: Actions): UserState {
-    return produce(state, draft => {
-        switch (action.type) {
-
-            case ActionType.LOGIN:
-                draft.loggingIn = true;
-                return;
-
-            case ActionType.LOGIN_SUCCESS:
-                draft.username = action.payload.username;
-                draft.loggingIn = false;
-                draft.isLoggedIn = true;
-                draft.loginTime = action.payload.loginTime;
-                return;
-
-            case ActionType.LOGIN_ERROR:
-                draft.loggingIn = false;
-                draft.isLoggedIn = false;
-                return;
-
-            case ActionType.LOGOUT:
-                draft.username = '';
-                draft.loggingIn = false;
-                draft.isLoggedIn = false;
-                draft.loginTime = -1;
-                return;
-
-            default:
-                reducerCaseNever(action);
-        }
-    });
-}

+ 0 - 60
admin-ui/src/app/state/user/user-state.ts

@@ -1,60 +0,0 @@
-import {Action} from '@ngrx/store';
-
-export interface UserState {
-    username: any;
-    loggingIn: boolean;
-    isLoggedIn: boolean;
-    loginTime: number;
-}
-
-export const initialUserState = {
-    username: '',
-    loggingIn: false,
-    isLoggedIn: false,
-    loginTime: -1,
-};
-
-export enum ActionType  {
-    CHECK_LOGGED_IN = 'user/CHECK_LOGGED_IN',
-    LOGIN = 'user/LOGIN',
-    LOGIN_SUCCESS = 'user/LOGIN_SUCCESS',
-    LOGIN_ERROR = 'user/LOGIN_ERROR',
-    LOGOUT = 'user/LOGOUT',
-}
-
-export class CheckLoggedIn implements Action {
-    readonly type = ActionType.LOGIN;
-}
-
-export class Login implements Action {
-    readonly type = ActionType.LOGIN;
-}
-
-export class LoginSuccess implements Action {
-    readonly type = ActionType.LOGIN_SUCCESS;
-    constructor(public payload: { username: string; loginTime: number }) {}
-}
-
-export class LoginError implements Action {
-    readonly type = ActionType.LOGIN_ERROR;
-    constructor(public payload: any) {}
-}
-
-export class Logout implements Action {
-    readonly type = ActionType.LOGOUT;
-}
-
-export const Actions = {
-    CheckLoggedIn,
-    Login,
-    LoginSuccess,
-    LoginError,
-    Logout,
-};
-
-export type Actions =
-    CheckLoggedIn |
-    Login |
-    LoginSuccess |
-    LoginError |
-    Logout;

File diff suppressed because it is too large
+ 598 - 57
admin-ui/yarn.lock


+ 2 - 18
graphql.config.json

@@ -2,30 +2,14 @@
 
   "README_schema" : "Specifies how to load the GraphQL schema that completion, error highlighting, and documentation is based on in the IDE",
   "schema": {
-
     "README_file" : "Remove 'file' to use request url below. A relative or absolute path to the JSON from a schema introspection query, e.g. '{ data: ... }' or a .graphql/.graphqls file describing the schema using GraphQL Schema Language. Changes to the file are watched.",
-
-
-    "README_request" : "To request the schema from a url instead, remove the 'file' JSON property above (and optionally delete the default graphql.schema.json file).",
-    "request": {
-      "url" : "http://localhost:3000/api",
-      "method" : "POST",
-      "README_postIntrospectionQuery" : "Whether to POST an introspectionQuery to the url. If the url always returns the schema JSON, set to false and consider using GET",
-      "postIntrospectionQuery" : true,
-      "README_options" : "See the 'Options' section at https://github.com/then/then-request",
-      "options" : {
-        "headers": {
-          "user-agent" : "JS GraphQL"
-        }
-      }
-    }
-
+    "file": "./schema.json"
   },
 
   "README_endpoints": "A list of GraphQL endpoints that can be queried from '.graphql' files in the IDE",
   "endpoints" : [
     {
-      "name": "Vendure Dev (http://localhost:8080/api)",
+      "name": "Vendure Dev",
       "url": "http://localhost:3000/api",
       "options" : {
         "headers": {

File diff suppressed because it is too large
+ 0 - 0
schema.json


Some files were not shown because too many files changed in this diff