From 58e56c1ced5409160d6a1778f86b1f7422782eef Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Sat, 1 Sep 2018 16:40:32 +0200 Subject: [PATCH] More advanced (#19) --- examples/games/package.json | 42 +++--- examples/games/src/app/app.module.ts | 1 - .../games/src/app/games/games.component.ts | 11 ++ examples/games/src/app/games/games.state.ts | 74 ++++++---- .../src/app/games/graphql/count.query.ts | 7 + examples/todo-angular/.editorconfig | 13 ++ examples/todo-angular/.gitignore | 39 ++++++ examples/todo-angular/README.md | 27 ++++ examples/todo-angular/angular.json | 98 +++++++++++++ examples/todo-angular/package.json | 47 +++++++ .../todo-angular/src/app/app.component.ts | 70 ++++++++++ examples/todo-angular/src/app/app.module.ts | 31 +++++ .../todo-angular/src/app/graphql.module.ts | 30 ++++ .../src/app/todos/add-todo.component.ts | 36 +++++ .../src/app/todos/todos-list.component.ts | 34 +++++ .../src/app/todos/todos.actions.ts | 21 +++ .../src/app/todos/todos.graphql.ts | 47 +++++++ .../todo-angular/src/app/todos/todos.state.ts | 106 ++++++++++++++ examples/todo-angular/src/assets/.gitkeep | 0 examples/todo-angular/src/browserslist | 9 ++ .../src/environments/environment.prod.ts | 3 + .../src/environments/environment.ts | 15 ++ examples/todo-angular/src/favicon.ico | Bin 0 -> 5430 bytes examples/todo-angular/src/index.html | 14 ++ examples/todo-angular/src/main.ts | 12 ++ examples/todo-angular/src/polyfills.ts | 80 +++++++++++ examples/todo-angular/src/styles.scss | 1 + examples/todo-angular/src/test.ts | 1 + examples/todo-angular/src/tsconfig.app.json | 12 ++ examples/todo-angular/src/tslint.json | 17 +++ examples/todo-angular/tsconfig.json | 22 +++ examples/todo-angular/tslint.json | 130 ++++++++++++++++++ package.json | 3 +- packages/angular/package.json | 35 ++--- packages/angular/src/decorators/action.ts | 7 +- packages/angular/src/decorators/mutation.ts | 3 +- packages/angular/src/decorators/resolve.ts | 12 ++ packages/angular/src/decorators/update.ts | 27 ++++ packages/angular/src/index.ts | 4 +- .../src/internal/transform-metadata.ts | 25 +++- packages/angular/src/internal/utils.ts | 33 ++++- packages/angular/src/metadata/metadata.ts | 8 ++ packages/angular/src/metadata/mutation.ts | 14 +- packages/angular/src/metadata/resolve.ts | 8 ++ packages/angular/src/metadata/update.ts | 17 +++ packages/angular/src/module.ts | 19 ++- packages/angular/src/types/action.ts | 7 + packages/angular/src/types/metadata.ts | 11 ++ packages/angular/src/types/mutation.ts | 6 + packages/angular/src/types/resolve.ts | 7 + packages/angular/src/types/update.ts | 3 + packages/core/package.json | 11 +- packages/core/src/helpers.ts | 98 +++++++++---- packages/core/src/index.ts | 10 +- packages/core/src/internal/context.ts | 15 ++ packages/core/src/internal/mutation.ts | 45 ++++-- packages/core/src/internal/query.ts | 16 --- packages/core/src/internal/resolvers.ts | 25 ++++ packages/core/src/internal/update.ts | 23 ++++ packages/core/src/link.ts | 10 +- packages/core/src/manager.ts | 11 +- packages/core/src/query.ts | 14 -- packages/core/src/resolvers.ts | 26 ++++ packages/core/src/types/common.ts | 21 ++- packages/core/src/types/mutation.ts | 9 +- packages/core/src/types/options.ts | 2 + packages/core/src/types/query.ts | 8 +- packages/core/src/types/resolver.ts | 12 ++ packages/core/src/types/update.ts | 16 +++ packages/core/src/update.ts | 13 ++ rollup.config.js | 2 + 71 files changed, 1508 insertions(+), 178 deletions(-) create mode 100644 examples/games/src/app/games/graphql/count.query.ts create mode 100644 examples/todo-angular/.editorconfig create mode 100644 examples/todo-angular/.gitignore create mode 100644 examples/todo-angular/README.md create mode 100644 examples/todo-angular/angular.json create mode 100644 examples/todo-angular/package.json create mode 100644 examples/todo-angular/src/app/app.component.ts create mode 100644 examples/todo-angular/src/app/app.module.ts create mode 100644 examples/todo-angular/src/app/graphql.module.ts create mode 100644 examples/todo-angular/src/app/todos/add-todo.component.ts create mode 100644 examples/todo-angular/src/app/todos/todos-list.component.ts create mode 100644 examples/todo-angular/src/app/todos/todos.actions.ts create mode 100644 examples/todo-angular/src/app/todos/todos.graphql.ts create mode 100644 examples/todo-angular/src/app/todos/todos.state.ts create mode 100644 examples/todo-angular/src/assets/.gitkeep create mode 100644 examples/todo-angular/src/browserslist create mode 100644 examples/todo-angular/src/environments/environment.prod.ts create mode 100644 examples/todo-angular/src/environments/environment.ts create mode 100644 examples/todo-angular/src/favicon.ico create mode 100644 examples/todo-angular/src/index.html create mode 100644 examples/todo-angular/src/main.ts create mode 100644 examples/todo-angular/src/polyfills.ts create mode 100644 examples/todo-angular/src/styles.scss create mode 100644 examples/todo-angular/src/test.ts create mode 100644 examples/todo-angular/src/tsconfig.app.json create mode 100644 examples/todo-angular/src/tslint.json create mode 100644 examples/todo-angular/tsconfig.json create mode 100644 examples/todo-angular/tslint.json create mode 100644 packages/angular/src/decorators/resolve.ts create mode 100644 packages/angular/src/decorators/update.ts create mode 100644 packages/angular/src/metadata/resolve.ts create mode 100644 packages/angular/src/metadata/update.ts create mode 100644 packages/angular/src/types/action.ts create mode 100644 packages/angular/src/types/mutation.ts create mode 100644 packages/angular/src/types/resolve.ts create mode 100644 packages/angular/src/types/update.ts create mode 100644 packages/core/src/internal/context.ts delete mode 100644 packages/core/src/internal/query.ts create mode 100644 packages/core/src/internal/resolvers.ts create mode 100644 packages/core/src/internal/update.ts delete mode 100644 packages/core/src/query.ts create mode 100644 packages/core/src/resolvers.ts create mode 100644 packages/core/src/types/resolver.ts create mode 100644 packages/core/src/types/update.ts create mode 100644 packages/core/src/update.ts diff --git a/examples/games/package.json b/examples/games/package.json index f4488655..63ea8d5e 100644 --- a/examples/games/package.json +++ b/examples/games/package.json @@ -9,37 +9,37 @@ "lint": "ng lint" }, "dependencies": { - "@angular/animations": "6.0.3", - "@angular/common": "6.0.3", - "@angular/compiler": "6.0.3", - "@angular/core": "6.0.3", - "@angular/forms": "6.0.3", - "@angular/http": "6.0.3", - "@angular/platform-browser": "6.0.3", - "@angular/platform-browser-dynamic": "6.0.3", - "@angular/router": "6.0.3", - "@loona/angular": "^1.0.0-alpha.1", - "apollo-angular": "1.1.2", - "apollo-angular-link-http": "1.1.1", - "apollo-cache-inmemory": "1.2.5", - "apollo-client": "2.3.5", + "@angular/animations": "6.1.4", + "@angular/common": "6.1.4", + "@angular/compiler": "6.1.4", + "@angular/core": "6.1.4", + "@angular/forms": "6.1.4", + "@angular/http": "6.1.4", + "@angular/platform-browser": "6.1.4", + "@angular/platform-browser-dynamic": "6.1.4", + "@angular/router": "6.1.4", + "@loona/angular": "1.0.0-alpha.1", + "apollo-angular": "1.2.0", + "apollo-angular-link-http": "1.2.0", + "apollo-cache-inmemory": "1.2.9", + "apollo-client": "2.4.1", "apollo-link": "1.2.2", - "core-js": "2.5.4", + "core-js": "2.5.7", "graphql": "0.13.2", "graphql-tag": "2.9.2", - "rxjs": "6.2.1", - "tachyons": "^4.10.0", + "rxjs": "6.2.2", + "tachyons": "4.11.1", "zone.js": "0.8.26" }, "devDependencies": { - "@angular-devkit/build-angular": "0.6.1", + "@angular-devkit/build-angular": "0.6.8", "@angular/cli": "6.0.1", - "@angular/compiler-cli": "6.0.3", - "@angular/language-service": "6.0.3", + "@angular/compiler-cli": "6.1.4", + "@angular/language-service": "6.1.4", "@types/node": "8.9.4", "codelyzer": "4.2.1", "ts-node": "5.0.1", "tslint": "5.9.1", - "typescript": "2.7.2" + "typescript": "2.9.2" } } diff --git a/examples/games/src/app/app.module.ts b/examples/games/src/app/app.module.ts index f2eab644..99043ab0 100644 --- a/examples/games/src/app/app.module.ts +++ b/examples/games/src/app/app.module.ts @@ -21,7 +21,6 @@ import { ErrorComponent } from './shared/error.component'; ErrorComponent, ], imports: [BrowserModule, HttpClientModule, AppRoutingModule, GraphQLModule], - providers: [], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/examples/games/src/app/games/games.component.ts b/examples/games/src/app/games/games.component.ts index 49ee6237..13b07a6e 100644 --- a/examples/games/src/app/games/games.component.ts +++ b/examples/games/src/app/games/games.component.ts @@ -5,6 +5,7 @@ import { pluck, share } from 'rxjs/operators'; import { Game } from './interfaces'; import { allGamesQuery } from './graphql/all-games.query'; +import { countQuery } from './graphql/count.query'; @Component({ selector: 'app-games', @@ -37,12 +38,16 @@ import { allGamesQuery } from './graphql/all-games.query'; +
+ All games: {{count$ | async}} +
`, }) export class GamesComponent implements OnInit { games$: Observable; + count$: Observable; loading$: Observable; constructor(private loona: Loona) {} @@ -56,6 +61,12 @@ export class GamesComponent implements OnInit { }) .valueChanges.pipe(share()); + this.count$ = this.loona + .query({ + query: countQuery, + }) + .valueChanges.pipe(share(), pluck('data', 'count')); + // I used pluck since it's the easiest way extract properties in that case this.games$ = games$.pipe(pluck('data', 'allGames')); this.loading$ = games$.pipe(pluck('loading')); diff --git a/examples/games/src/app/games/games.state.ts b/examples/games/src/app/games/games.state.ts index 91e93cd2..f9954398 100644 --- a/examples/games/src/app/games/games.state.ts +++ b/examples/games/src/app/games/games.state.ts @@ -1,9 +1,9 @@ -import { State, Mutation, Action, Update } from '@loona/angular'; -import { of } from 'rxjs'; -import { mapTo, catchError } from 'rxjs/operators'; +import {State, Mutation, Action, Resolve, Context} from '@loona/angular'; +import {of, Observable} from 'rxjs'; +import {mapTo, catchError, map} from 'rxjs/operators'; -import { currentGameQuery } from './graphql/current-game.query'; -import { currentGameStatusQuery } from './graphql/current-game-status.query'; +import {currentGameQuery} from './graphql/current-game.query'; +import {currentGameStatusQuery} from './graphql/current-game-status.query'; import { GameCreationSuccess, GameCreationFailure, @@ -35,39 +35,55 @@ const defaultState = { export class GamesState { // registers and creates a resolver for the mutation @Mutation(UpdateName) - // wrapper to update a query based on mutation's arguments - @Update(currentGameQuery) - // state holds the result of currentGameQuery - // second params are arguments - updateName(state, { team, name }) { - // since it uses immer, you can mutate an object directly - state.currentGame[`team${team}Name`] = name; + updateName({team, name}, ctx: Context) { + return ctx.patchQuery(currentGameQuery, data => { + // since it uses immer, you can mutate an object directly + data.currentGame[`team${team}Name`] = name; + }); + } + + @Resolve('Query.count') + count() { + return new Observable(observer => { + setTimeout(() => { + observer.next(10) + + setTimeout(() => { + observer.next(20); + observer.complete(); + }, 2000); + }, 2000); + }); } @Mutation(Goal) - @Update(currentGameQuery) - goal(state, { team }) { - state.currentGame[`team${team}Score`] += 1; + goal({team}, ctx: Context) { + console.log(ctx); + return ctx.patchQuery(currentGameQuery, data => { + data.currentGame[`team${team}Score`] += 1; + }); } @Mutation(UpdateGameStatus) - @Update(currentGameStatusQuery) - updateGameStatus(state, { created, error }) { - if (typeof created !== 'undefined') { - state.currentGameStatus.created = created; - } + updateGameStatus({created, error}, ctx: Context) { + return ctx.patchQuery(currentGameStatusQuery, data => { + if (typeof created !== 'undefined') { + data.currentGameStatus.created = created; + } - if (typeof error !== 'undefined') { - state.currentGameStatus.error = error; - } + if (typeof error !== 'undefined') { + data.currentGameStatus.error = error; + } + }); } @Mutation(ResetCurrentGame) - @Update(currentGameQuery) - resetCurrentGame() { - return { - currentGame: defaultState.currentGame, - }; + resetCurrentGame(args, ctx: Context) { + return ctx.writeData({ + data: { + currentGame: defaultState.currentGame, + } + }); } // Action handler - similar to NGRX Effects @@ -105,7 +121,7 @@ export class GamesState { // Action handler that returns an action based on the result @Action(CreateGame) - onCreateGame(_action, action$) { + onCreateGame(payload, action$) { return action$.pipe( mapTo(new GameCreationSuccess()), catchError(() => of(new GameCreationFailure())), diff --git a/examples/games/src/app/games/graphql/count.query.ts b/examples/games/src/app/games/graphql/count.query.ts new file mode 100644 index 00000000..17955a9e --- /dev/null +++ b/examples/games/src/app/games/graphql/count.query.ts @@ -0,0 +1,7 @@ +import gql from 'graphql-tag'; + +export const countQuery = gql` + query CountGames { + count @client + } +`; diff --git a/examples/todo-angular/.editorconfig b/examples/todo-angular/.editorconfig new file mode 100644 index 00000000..6e87a003 --- /dev/null +++ b/examples/todo-angular/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/examples/todo-angular/.gitignore b/examples/todo-angular/.gitignore new file mode 100644 index 00000000..ee5c9d83 --- /dev/null +++ b/examples/todo-angular/.gitignore @@ -0,0 +1,39 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git a/examples/todo-angular/README.md b/examples/todo-angular/README.md new file mode 100644 index 00000000..24d6e13d --- /dev/null +++ b/examples/todo-angular/README.md @@ -0,0 +1,27 @@ +# Games + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.1. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/examples/todo-angular/angular.json b/examples/todo-angular/angular.json new file mode 100644 index 00000000..7a794470 --- /dev/null +++ b/examples/todo-angular/angular.json @@ -0,0 +1,98 @@ +{ + "$schema": "../../node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "todo": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "app", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "spec": false + }, + "@schematics/angular:class": { + "spec": false + }, + "@schematics/angular:directive": { + "spec": false + }, + "@schematics/angular:guard": { + "spec": false + }, + "@schematics/angular:module": { + "spec": false + }, + "@schematics/angular:pipe": { + "spec": false + }, + "@schematics/angular:service": { + "spec": false + } + }, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/todo", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [{ + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + }], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "todo:build" + }, + "configurations": { + "production": { + "browserTarget": "todo:build:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.app.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + } + }, + "defaultProject": "todo" +} diff --git a/examples/todo-angular/package.json b/examples/todo-angular/package.json new file mode 100644 index 00000000..5e9c7e7c --- /dev/null +++ b/examples/todo-angular/package.json @@ -0,0 +1,47 @@ +{ + "name": "examples-todo-angular", + "private": true, + "version": "1.0.0-alpha.1", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "lint": "ng lint" + }, + "dependencies": { + "@angular/animations": "6.1.4", + "@angular/cdk": "6.3.1", + "@angular/common": "6.1.4", + "@angular/compiler": "6.1.4", + "@angular/core": "6.1.4", + "@angular/forms": "6.1.4", + "@angular/http": "6.1.4", + "@angular/material": "6.3.1", + "@angular/platform-browser": "6.1.4", + "@angular/platform-browser-dynamic": "6.1.4", + "@angular/router": "6.1.4", + "@loona/angular": "1.0.0-alpha.1", + "apollo-angular": "1.2.0", + "apollo-angular-link-http": "1.2.0", + "apollo-cache-inmemory": "1.2.9", + "apollo-client": "2.4.1", + "apollo-link": "1.2.2", + "core-js": "2.5.7", + "graphql": "0.13.2", + "graphql-tag": "2.9.2", + "rxjs": "6.2.2", + "tachyons": "4.11.1", + "zone.js": "0.8.26" + }, + "devDependencies": { + "@angular-devkit/build-angular": "0.6.5", + "@angular/cli": "6.0.1", + "@angular/compiler-cli": "6.1.4", + "@angular/language-service": "6.1.4", + "@types/node": "8.9.4", + "codelyzer": "4.2.1", + "ts-node": "5.0.1", + "tslint": "5.9.1", + "typescript": "2.9.2" + } +} diff --git a/examples/todo-angular/src/app/app.component.ts b/examples/todo-angular/src/app/app.component.ts new file mode 100644 index 00000000..1468e60c --- /dev/null +++ b/examples/todo-angular/src/app/app.component.ts @@ -0,0 +1,70 @@ +import {Component} from '@angular/core'; +import {Loona} from '@loona/angular'; +import {Observable} from 'rxjs'; +import {pluck} from 'rxjs/operators'; + +import {AddTodo, ToggleTodo} from './todos/todos.actions'; +import {activeTodos, completedTodos} from './todos/todos.graphql'; + +@Component({ + selector: 'app-root', + template: ` +
+ +
+
+ +
+
+ +
+
+
+ `, + styles: [ + ` + .container { + display: block; + max-width: 600px; + margin: 0 auto; + } + + .split { + display: flex; + justify-content: space-between; + + .into { + display: flex; + flex-direction: column; + flex: 1; + } + } + `, + ], +}) +export class AppComponent { + active: Observable; + completed: Observable; + + constructor(private loona: Loona) { + this.active = this.loona + .query({ + query: activeTodos, + }) + .valueChanges.pipe(pluck('data', 'active')); + + this.completed = this.loona + .query({ + query: completedTodos, + }) + .valueChanges.pipe(pluck('data', 'completed')); + } + + add(text: string): void { + this.loona.dispatch(new AddTodo(text)); + } + + toggle(id: string): void { + this.loona.dispatch(new ToggleTodo(id)); + } +} diff --git a/examples/todo-angular/src/app/app.module.ts b/examples/todo-angular/src/app/app.module.ts new file mode 100644 index 00000000..00b04de2 --- /dev/null +++ b/examples/todo-angular/src/app/app.module.ts @@ -0,0 +1,31 @@ +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import { + MatListModule, + MatFormFieldModule, + MatInputModule, +} from '@angular/material'; + +import {GraphQLModule} from './graphql.module'; +import {AppComponent} from './app.component'; +import {TodosListComponent} from './todos/todos-list.component'; +import {AddTodoComponent} from './todos/add-todo.component'; + +@NgModule({ + declarations: [AppComponent, TodosListComponent, AddTodoComponent], + imports: [ + BrowserModule, + FormsModule, + ReactiveFormsModule, + BrowserAnimationsModule, + MatListModule, + MatFormFieldModule, + MatInputModule, + GraphQLModule, + ], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/examples/todo-angular/src/app/graphql.module.ts b/examples/todo-angular/src/app/graphql.module.ts new file mode 100644 index 00000000..2833a7a3 --- /dev/null +++ b/examples/todo-angular/src/app/graphql.module.ts @@ -0,0 +1,30 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ApolloClientOptions} from 'apollo-client'; +import {ApolloModule, APOLLO_OPTIONS} from 'apollo-angular'; +import {InMemoryCache} from 'apollo-cache-inmemory'; +import {LoonaModule, LoonaLink} from '@loona/angular'; + +import {TodosState} from './todos/todos.state'; + +const cache = new InMemoryCache(); + +export function apolloFactory(loonaLink: LoonaLink): ApolloClientOptions { + return { + link: loonaLink, + cache, + }; +} + +@NgModule({ + imports: [CommonModule, LoonaModule.forRoot(cache, [TodosState])], + exports: [ApolloModule, LoonaModule], + providers: [ + { + provide: APOLLO_OPTIONS, + useFactory: apolloFactory, + deps: [LoonaLink], + }, + ], +}) +export class GraphQLModule {} diff --git a/examples/todo-angular/src/app/todos/add-todo.component.ts b/examples/todo-angular/src/app/todos/add-todo.component.ts new file mode 100644 index 00000000..c70ebe2a --- /dev/null +++ b/examples/todo-angular/src/app/todos/add-todo.component.ts @@ -0,0 +1,36 @@ +import {Component, Output, EventEmitter} from '@angular/core'; +import {FormControl, Validators} from '@angular/forms'; + +@Component({ + selector: 'app-add-todo', + template: ` +
+ + + +
+ `, + styles: [ + ` + form { + display: flex; + flex-direction: column; + } + + form > * { + width: 100%; + } + `, + ], +}) +export class AddTodoComponent { + @Output() todo = new EventEmitter(); + text = new FormControl('', Validators.required); + + submit() { + if (this.text.valid) { + this.todo.next(this.text.value); + this.text.reset(); + } + } +} diff --git a/examples/todo-angular/src/app/todos/todos-list.component.ts b/examples/todo-angular/src/app/todos/todos-list.component.ts new file mode 100644 index 00000000..6c58bbb3 --- /dev/null +++ b/examples/todo-angular/src/app/todos/todos-list.component.ts @@ -0,0 +1,34 @@ +import {Component, Input, Output, EventEmitter} from '@angular/core'; + +@Component({ + selector: 'app-todos-list', + template: ` +

{{name}}

+ + + {{todo.text}} + + + `, + styles: [ + ` + .name { + font-family: Roboto, 'Helvetica Neue', sans-serif; + } + + .rtl { + text-align: right; + } + `, + ], +}) +export class TodosListComponent { + @Input() position = 'before'; + @Input() todos = []; + @Input() name: string; + @Output() toggle = new EventEmitter(); + + toggled(todo) { + this.toggle.next(todo.id); + } +} diff --git a/examples/todo-angular/src/app/todos/todos.actions.ts b/examples/todo-angular/src/app/todos/todos.actions.ts new file mode 100644 index 00000000..3fcffe0f --- /dev/null +++ b/examples/todo-angular/src/app/todos/todos.actions.ts @@ -0,0 +1,21 @@ +import {toggleTodo, addTodo} from './todos.graphql'; + +export class AddTodo { + static mutation = addTodo; + variables: any; + constructor(text: string) { + this.variables = { + text, + }; + } +} + +export class ToggleTodo { + static mutation = toggleTodo; + variables: any; + constructor(id: string) { + this.variables = { + id, + }; + } +} diff --git a/examples/todo-angular/src/app/todos/todos.graphql.ts b/examples/todo-angular/src/app/todos/todos.graphql.ts new file mode 100644 index 00000000..3841fd54 --- /dev/null +++ b/examples/todo-angular/src/app/todos/todos.graphql.ts @@ -0,0 +1,47 @@ +import gql from 'graphql-tag'; + +export const todoFragment = gql` + fragment todoFragment on Todo { + id + text + completed + } +`; + +export const completedTodos = gql` + query completed { + completed @client { + ...todoFragment + } + } + ${todoFragment} +`; + +export const activeTodos = gql` + query active { + active @client { + ...todoFragment + } + } + ${todoFragment} +`; + +export const addTodo = gql` + mutation add($text: String!) { + addTodo(text: $text) @client { + ...todoFragment + } + } + + ${todoFragment} +`; + +export const toggleTodo = gql` + mutation toggle($id: ID!) { + toggleTodo(id: $id) @client { + ...todoFragment + } + } + + ${todoFragment} +`; diff --git a/examples/todo-angular/src/app/todos/todos.state.ts b/examples/todo-angular/src/app/todos/todos.state.ts new file mode 100644 index 00000000..4ed4f3a7 --- /dev/null +++ b/examples/todo-angular/src/app/todos/todos.state.ts @@ -0,0 +1,106 @@ +import {State, Mutation, Update, Context} from '@loona/angular'; + +import {AddTodo, ToggleTodo} from './todos.actions'; +import {todoFragment, activeTodos, completedTodos} from './todos.graphql'; + +@State({ + defaults: { + completed: [], + active: [], + }, +}) +export class TodosState { + @Mutation(AddTodo) + add(args) { + const todo = { + id: Math.random() + .toString(32) + .substr(2), + text: args.text, + completed: false, + __typename: 'Todo', + }; + + return todo; + } + + @Mutation(ToggleTodo) + toggle(args, ctx: Context) { + return ctx.patchFragment(todoFragment, {id: args.id}, data => { + data.completed = !data.completed; + }); + } + + @Update(ToggleTodo) + popTodoFromActive(mutation, ctx: Context) { + const todo = mutation.result; + + if (todo.completed) { + ctx.patchQuery(activeTodos, data => { + if (!data.active) { + data.active = []; + } + + data.active = data.active.filter(o => o.id !== todo.id); + }); + } + } + + @Update(ToggleTodo) + pushTodoFromActive(mutation, ctx: Context) { + const todo = mutation.result; + + if (!todo.completed) { + ctx.patchQuery(activeTodos, data => { + if (!data.active) { + data.active = []; + } + + data.active = data.active.concat([todo]); + }); + } + } + + @Update(ToggleTodo) + popTodoFromCompleted(mutation, ctx: Context) { + const todo = mutation.result; + + if (!todo.completed) { + ctx.patchQuery(completedTodos, data => { + if (!data.completed) { + data.completed = []; + } + + data.completed = data.completed.filter(o => o.id !== todo.id); + }); + } + } + + @Update(ToggleTodo) + pushTodoFromCompleted(mutation, ctx: Context) { + const todo = mutation.result; + + if (todo.completed) { + ctx.patchQuery(completedTodos, data => { + if (!data.completed) { + data.completed = []; + } + + data.completed = data.completed.concat([todo]); + }); + } + } + + @Update(AddTodo) + updateActiveOnAdd(mutation, ctx: Context) { + const todo = mutation.result; + + ctx.patchQuery(activeTodos, data => { + if (!data.active) { + data.active = []; + } + + data.active = data.active.concat([todo]); + }); + } +} diff --git a/examples/todo-angular/src/assets/.gitkeep b/examples/todo-angular/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/examples/todo-angular/src/browserslist b/examples/todo-angular/src/browserslist new file mode 100644 index 00000000..8e09ab49 --- /dev/null +++ b/examples/todo-angular/src/browserslist @@ -0,0 +1,9 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# For IE 9-11 support, please uncomment the last line of the file and adjust as needed +> 0.5% +last 2 versions +Firefox ESR +not dead +# IE 9-11 \ No newline at end of file diff --git a/examples/todo-angular/src/environments/environment.prod.ts b/examples/todo-angular/src/environments/environment.prod.ts new file mode 100644 index 00000000..3612073b --- /dev/null +++ b/examples/todo-angular/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/examples/todo-angular/src/environments/environment.ts b/examples/todo-angular/src/environments/environment.ts new file mode 100644 index 00000000..012182ef --- /dev/null +++ b/examples/todo-angular/src/environments/environment.ts @@ -0,0 +1,15 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * In development mode, to ignore zone related error stack frames such as + * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can + * import the following file, but please comment it out in production mode + * because it will have performance impact when throw error + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/examples/todo-angular/src/favicon.ico b/examples/todo-angular/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8081c7ceaf2be08bf59010158c586170d9d2d517 GIT binary patch literal 5430 zcmc(je{54#6vvCoAI3i*G5%$U7!sA3wtMZ$fH6V9C`=eXGJb@R1%(I_{vnZtpD{6n z5Pl{DmxzBDbrB>}`90e12m8T*36WoeDLA&SD_hw{H^wM!cl_RWcVA!I+x87ee975; z@4kD^=bYPn&pmG@(+JZ`rqQEKxW<}RzhW}I!|ulN=fmjVi@x{p$cC`)5$a!)X&U+blKNvN5tg=uLvuLnuqRM;Yc*swiexsoh#XPNu{9F#c`G zQLe{yWA(Y6(;>y|-efAy11k<09(@Oo1B2@0`PtZSkqK&${ zgEY}`W@t{%?9u5rF?}Y7OL{338l*JY#P!%MVQY@oqnItpZ}?s z!r?*kwuR{A@jg2Chlf0^{q*>8n5Ir~YWf*wmsh7B5&EpHfd5@xVaj&gqsdui^spyL zB|kUoblGoO7G(MuKTfa9?pGH0@QP^b#!lM1yHWLh*2iq#`C1TdrnO-d#?Oh@XV2HK zKA{`eo{--^K&MW66Lgsktfvn#cCAc*(}qsfhrvOjMGLE?`dHVipu1J3Kgr%g?cNa8 z)pkmC8DGH~fG+dlrp(5^-QBeEvkOvv#q7MBVLtm2oD^$lJZx--_=K&Ttd=-krx(Bb zcEoKJda@S!%%@`P-##$>*u%T*mh+QjV@)Qa=Mk1?#zLk+M4tIt%}wagT{5J%!tXAE;r{@=bb%nNVxvI+C+$t?!VJ@0d@HIyMJTI{vEw0Ul ze(ha!e&qANbTL1ZneNl45t=#Ot??C0MHjjgY8%*mGisN|S6%g3;Hlx#fMNcL<87MW zZ>6moo1YD?P!fJ#Jb(4)_cc50X5n0KoDYfdPoL^iV`k&o{LPyaoqMqk92wVM#_O0l z09$(A-D+gVIlq4TA&{1T@BsUH`Bm=r#l$Z51J-U&F32+hfUP-iLo=jg7Xmy+WLq6_tWv&`wDlz#`&)Jp~iQf zZP)tu>}pIIJKuw+$&t}GQuqMd%Z>0?t%&BM&Wo^4P^Y z)c6h^f2R>X8*}q|bblAF?@;%?2>$y+cMQbN{X$)^R>vtNq_5AB|0N5U*d^T?X9{xQnJYeU{ zoZL#obI;~Pp95f1`%X3D$Mh*4^?O?IT~7HqlWguezmg?Ybq|7>qQ(@pPHbE9V?f|( z+0xo!#m@Np9PljsyxBY-UA*{U*la#8Wz2sO|48_-5t8%_!n?S$zlGe+NA%?vmxjS- zHE5O3ZarU=X}$7>;Okp(UWXJxI%G_J-@IH;%5#Rt$(WUX?6*Ux!IRd$dLP6+SmPn= z8zjm4jGjN772R{FGkXwcNv8GBcZI#@Y2m{RNF_w8(Z%^A*!bS*!}s6sh*NnURytky humW;*g7R+&|Ledvc- + + + + Todo + + + + + + + + + diff --git a/examples/todo-angular/src/main.ts b/examples/todo-angular/src/main.ts new file mode 100644 index 00000000..91ec6da5 --- /dev/null +++ b/examples/todo-angular/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.log(err)); diff --git a/examples/todo-angular/src/polyfills.ts b/examples/todo-angular/src/polyfills.ts new file mode 100644 index 00000000..d310405a --- /dev/null +++ b/examples/todo-angular/src/polyfills.ts @@ -0,0 +1,80 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +import 'core-js/es7/reflect'; + + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + + // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + + /* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/examples/todo-angular/src/styles.scss b/examples/todo-angular/src/styles.scss new file mode 100644 index 00000000..314271e1 --- /dev/null +++ b/examples/todo-angular/src/styles.scss @@ -0,0 +1 @@ +@import "~@angular/material/prebuilt-themes/indigo-pink.css" diff --git a/examples/todo-angular/src/test.ts b/examples/todo-angular/src/test.ts new file mode 100644 index 00000000..8337712e --- /dev/null +++ b/examples/todo-angular/src/test.ts @@ -0,0 +1 @@ +// diff --git a/examples/todo-angular/src/tsconfig.app.json b/examples/todo-angular/src/tsconfig.app.json new file mode 100644 index 00000000..c7cd7bca --- /dev/null +++ b/examples/todo-angular/src/tsconfig.app.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "es2015", + "types": [] + }, + "exclude": [ + "sr/test.ts", + "**/*.spec.ts" + ] +} diff --git a/examples/todo-angular/src/tslint.json b/examples/todo-angular/src/tslint.json new file mode 100644 index 00000000..52e2c1a5 --- /dev/null +++ b/examples/todo-angular/src/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ] + } +} diff --git a/examples/todo-angular/tsconfig.json b/examples/todo-angular/tsconfig.json new file mode 100644 index 00000000..55a6c38e --- /dev/null +++ b/examples/todo-angular/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "typeRoots": [ + "../../node_modules/@types/", + "node_modules/@types/" + ], + "lib": [ + "es2017", + "esnext.asynciterable", + "dom" + ] + } +} diff --git a/examples/todo-angular/tslint.json b/examples/todo-angular/tslint.json new file mode 100644 index 00000000..3ea984c7 --- /dev/null +++ b/examples/todo-angular/tslint.json @@ -0,0 +1,130 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-output-on-prefix": true, + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} diff --git a/package.json b/package.json index aebca661..973ae4df 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "packages": [ "packages/core", "packages/angular", - "examples/games" + "examples/games", + "examples/todo-angular" ], "nohoist": [ "@loona/angular/ng-packagr", diff --git a/packages/angular/package.json b/packages/angular/package.json index 695ce44f..bf975cf2 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -13,14 +13,16 @@ "test": "exit 0", "bundle": "rollup -c rollup.config.js", "build": "ngc -p tsconfig.json", + "clean": "rimraf build/", + "prebuild": "yarn clean", "postbuild": "yarn bundle", "prepublishOnly": "yarn build", "prettier": "prettier --config ../../.prettierrc --write {src,tests}/**/*.ts" }, "peerDependencies": { - "@angular/core": "^6.0.0", - "apollo-angular": "^1.1.0", - "apollo-client": "^2.0.0", + "@angular/core": "^6.1.0", + "apollo-angular": "^1.2.0", + "apollo-client": "^2.3.0", "graphql": "^0.13.2", "rxjs": "^6.0.0" }, @@ -28,25 +30,26 @@ "@loona/core": "^1.0.0-alpha.1" }, "devDependencies": { - "@angular/common": "6.0.3", - "@angular/compiler": "6.0.3", - "@angular/compiler-cli": "6.0.3", - "@angular/core": "6.0.3", - "@angular/platform-browser": "6.0.3", - "@angular/platform-browser-dynamic": "6.0.3", + "@angular/common": "6.1.4", + "@angular/compiler": "6.1.4", + "@angular/compiler-cli": "6.1.4", + "@angular/core": "6.1.4", + "@angular/platform-browser": "6.1.4", + "@angular/platform-browser-dynamic": "6.1.4", "@types/jest": "22.2.3", - "apollo-angular": "1.1.2", - "apollo-cache-inmemory": "1.2.5", - "apollo-client": "2.3.5", + "apollo-angular": "1.2.0", + "apollo-cache-inmemory": "1.2.9", + "apollo-client": "2.4.1", "graphql": "0.13.2", "graphql-tag": "2.9.2", "jest": "22.4.4", - "ng-packagr": "4.0.0-rc.1", - "prettier": "1.13.6", + "ng-packagr": "4.1.1", + "prettier": "1.14.2", + "rimraf": "2.6.2", "rollup": "0.61.2", - "rxjs": "6.2.1", + "rxjs": "6.2.2", "ts-jest": "22.4.6", - "typescript": "2.7.2", + "typescript": "2.9.2", "zone.js": "0.8.26" }, "jest": { diff --git a/packages/angular/src/decorators/action.ts b/packages/angular/src/decorators/action.ts index 3b0e7cf0..d3f8bda8 100644 --- a/packages/angular/src/decorators/action.ts +++ b/packages/angular/src/decorators/action.ts @@ -1,14 +1,11 @@ -import {Observable} from 'rxjs'; - import {setActionMetadata} from '../metadata/action'; +import {ActionMethod} from '../types/action'; export function Action(actions: any | any[], options?: any) { return function( target: any, name: string, - _descriptor: TypedPropertyDescriptor< - (action: any, action$: Observable) => Observable | Promise - >, + _descriptor: TypedPropertyDescriptor, ) { setActionMetadata( target, diff --git a/packages/angular/src/decorators/mutation.ts b/packages/angular/src/decorators/mutation.ts index be296f19..b142514c 100644 --- a/packages/angular/src/decorators/mutation.ts +++ b/packages/angular/src/decorators/mutation.ts @@ -2,12 +2,13 @@ import {DocumentNode} from 'graphql'; import {setMutationMetadata} from '../metadata/mutation'; import {isMutation} from '../internal/mutation'; +import {MutationMethod} from '../types/mutation'; export function Mutation(mutation: DocumentNode | any, options?: any) { return function( target: any, name: string, - _descriptor: TypedPropertyDescriptor, + _descriptor: TypedPropertyDescriptor, ) { setMutationMetadata( target, diff --git a/packages/angular/src/decorators/resolve.ts b/packages/angular/src/decorators/resolve.ts new file mode 100644 index 00000000..324c180c --- /dev/null +++ b/packages/angular/src/decorators/resolve.ts @@ -0,0 +1,12 @@ +import {setResolveMetadata} from '../metadata/resolve'; +import {ResolveMethod} from '../types/resolve'; + +export function Resolve(path: string) { + return function( + target: any, + name: string, + _descriptor: TypedPropertyDescriptor, + ) { + setResolveMetadata(target, name, path); + }; +} diff --git a/packages/angular/src/decorators/update.ts b/packages/angular/src/decorators/update.ts new file mode 100644 index 00000000..54f2a261 --- /dev/null +++ b/packages/angular/src/decorators/update.ts @@ -0,0 +1,27 @@ +import {UpdateMatchFn, getNameOfMutation} from '@loona/core'; + +import {setUpdateMetadata} from '../metadata/update'; +import {getMutation} from '../internal/mutation'; +import {UpdateMethod} from '../types/update'; + +export function Update(mutation: any) { + return function( + target: any, + name: string, + _descriptor: TypedPropertyDescriptor, + ) { + const document = getMutation(mutation); + + if (!document) { + throw new Error( + `Mutation ${ + (mutation as any).name + } is missing a static property 'mutation'`, + ); + } + const mutationName = getNameOfMutation(document); + const match: UpdateMatchFn = info => info.name === mutationName; + + setUpdateMetadata(target, name, match); + }; +} diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index 8ec63f88..a01a57d6 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -1,9 +1,11 @@ -export {Manager, LoonaLink, Update} from '@loona/core'; +export {Manager, LoonaLink, Context} from '@loona/core'; // decorators export {State} from './decorators/state'; export {Action} from './decorators/action'; export {Mutation} from './decorators/mutation'; export {Query} from './decorators/query'; +export {Update} from './decorators/update'; +export {Resolve} from './decorators/resolve'; // providers export {Actions, getActionType} from './actions'; export {Loona} from './client'; diff --git a/packages/angular/src/internal/transform-metadata.ts b/packages/angular/src/internal/transform-metadata.ts index 752ad633..8a0bb629 100644 --- a/packages/angular/src/internal/transform-metadata.ts +++ b/packages/angular/src/internal/transform-metadata.ts @@ -1,4 +1,4 @@ -import {MutationDef, QueryDef} from '@loona/core'; +import {MutationDef, QueryDef, UpdateDef, ResolverDef} from '@loona/core'; import {Metadata} from '../types/metadata'; import {createResolver} from './utils'; @@ -13,6 +13,29 @@ export function transformMutations( })); } +export function transformUpdates( + instance: any, + meta: Metadata, +): UpdateDef[] | undefined { + if (meta.updates) { + return meta.updates.map(({propName, match}) => ({ + match, + resolve: instance[propName].bind(instance), + })); + } +} + +export function transformResolvers( + instance: any, + meta: Metadata +): ResolverDef[] | undefined { + return meta.resolvers.map(({propName, path}) => ({ + name: propName, + path, + resolve: createResolver(instance, propName), + })); +} + export function transformQueries(instance: any, meta: Metadata): QueryDef[] { return meta.queries.map(({propName}) => ({ name: propName, diff --git a/packages/angular/src/internal/utils.ts b/packages/angular/src/internal/utils.ts index b5effa28..07cb8a42 100644 --- a/packages/angular/src/internal/utils.ts +++ b/packages/angular/src/internal/utils.ts @@ -1,5 +1,5 @@ import {Observable, from} from 'rxjs'; -import {first} from 'rxjs/operators'; +import {DocumentNode, OperationDefinitionNode} from 'graphql'; function wrapObservable(instance: any, propName: string) { return (...args: any[]) => @@ -13,11 +13,17 @@ function wrapObservable(instance: any, propName: string) { } if (isPromise(result) || isObservable(result)) { + let last: any; + from(result) - .pipe(first()) .subscribe({ - next: resolve, + next(emitted) { + last = emitted; + }, error: reject, + complete() { + resolve(last) + } }); } else { resolve(result); @@ -37,4 +43,25 @@ export function isString(val: any): val is string { return typeof val === 'string'; } +export function getKind(doc: DocumentNode): 'fragment' | 'query' | undefined { + if (isFragment(doc)) { + return 'fragment'; + } + + if (isQuery(doc)) { + return 'query'; + } +} + +export function isFragment(doc: DocumentNode): boolean { + return doc.definitions[0].kind === 'FragmentDefinition'; +} + +export function isQuery(doc: DocumentNode): boolean { + return ( + doc.definitions[0].kind === 'OperationDefinition' && + (doc.definitions[0] as OperationDefinitionNode).operation === 'query' + ); +} + export const createResolver = wrapObservable; diff --git a/packages/angular/src/metadata/metadata.ts b/packages/angular/src/metadata/metadata.ts index 75543b70..c375c792 100644 --- a/packages/angular/src/metadata/metadata.ts +++ b/packages/angular/src/metadata/metadata.ts @@ -2,11 +2,19 @@ import {Metadata} from '../types/metadata'; export const METADATA_KEY = '@@loona'; +export function readMetadata(target: any): Metadata { + const constructor = target.constructor; + + return constructor[METADATA_KEY]; +} + export function ensureMetadata(target: any): Metadata { if (!target.hasOwnProperty(METADATA_KEY)) { const defaultValue: Metadata = { defaults: {}, mutations: [], + resolvers: [], + updates: [], actions: {}, queries: [], typeDefs: [], diff --git a/packages/angular/src/metadata/mutation.ts b/packages/angular/src/metadata/mutation.ts index 068b93ad..af9a4189 100644 --- a/packages/angular/src/metadata/mutation.ts +++ b/packages/angular/src/metadata/mutation.ts @@ -1,6 +1,6 @@ import {DocumentNode} from 'graphql'; -import {ensureMetadata} from './metadata'; +import {ensureMetadata, readMetadata} from './metadata'; export function setMutationMetadata( proto: any, @@ -17,3 +17,15 @@ export function setMutationMetadata( options, }); } + +export function hasMutation(target: any, propName: string): boolean { + const meta = readMetadata(target); + + if (meta) { + return meta.mutations.some(def => { + return def.propName === propName; + }); + } + + return false; +} diff --git a/packages/angular/src/metadata/resolve.ts b/packages/angular/src/metadata/resolve.ts new file mode 100644 index 00000000..d73556c3 --- /dev/null +++ b/packages/angular/src/metadata/resolve.ts @@ -0,0 +1,8 @@ +import {ensureMetadata} from './metadata'; + +export function setResolveMetadata(proto: any, propName: string, path: string) { + const constructor = proto.constructor; + const meta = ensureMetadata(constructor); + + meta.resolvers.push({propName, path}); +} diff --git a/packages/angular/src/metadata/update.ts b/packages/angular/src/metadata/update.ts new file mode 100644 index 00000000..bfb5dd48 --- /dev/null +++ b/packages/angular/src/metadata/update.ts @@ -0,0 +1,17 @@ +import {UpdateMatchFn} from '@loona/core'; + +import {ensureMetadata} from './metadata'; + +export function setUpdateMetadata( + proto: any, + propName: string, + match: UpdateMatchFn, +) { + const constructor = proto.constructor; + const meta = ensureMetadata(constructor); + + meta.updates.push({ + propName, + match, + }); +} diff --git a/packages/angular/src/module.ts b/packages/angular/src/module.ts index 465899c3..ab3347db 100644 --- a/packages/angular/src/module.ts +++ b/packages/angular/src/module.ts @@ -1,6 +1,13 @@ import {NgModule, ModuleWithProviders, Injector} from '@angular/core'; import {ApolloCache} from 'apollo-cache'; -import {Manager, QueryDef, MutationDef, LoonaLink} from '@loona/core'; +import { + Manager, + QueryDef, + MutationDef, + ResolverDef, + UpdateDef, + LoonaLink, +} from '@loona/core'; import {Loona} from './client'; import {Actions} from './actions'; @@ -12,6 +19,8 @@ import {METADATA_KEY} from './metadata/metadata'; import { transformQueries, transformMutations, + transformUpdates, + transformResolvers, } from './internal/transform-metadata'; import {isString} from './internal/utils'; @@ -79,6 +88,8 @@ export function managerFactory( ): Manager { let queries: QueryDef[] = []; let mutations: MutationDef[] = []; + let resolvers: ResolverDef[] = []; + let updates: UpdateDef[] = []; let defaults: any = {}; let typeDefs: Array = []; @@ -88,6 +99,8 @@ export function managerFactory( queries = queries.concat(transformQueries(instance, meta)); mutations = mutations.concat(transformMutations(instance, meta)); + updates = updates.concat(transformUpdates(instance, meta) || []); + resolvers = resolvers.concat(transformResolvers(instance, meta) || []); defaults = { ...defaults, ...meta.defaults, @@ -102,12 +115,16 @@ export function managerFactory( queries = queries.filter(Boolean); mutations = mutations.filter(Boolean); + resolvers = resolvers.filter(Boolean); + updates = updates.filter(Boolean); typeDefs = typeDefs.filter(Boolean); return new Manager({ cache, queries, + resolvers, mutations, + updates, defaults, typeDefs: [...typeDefs], }); diff --git a/packages/angular/src/types/action.ts b/packages/angular/src/types/action.ts new file mode 100644 index 00000000..097edbda --- /dev/null +++ b/packages/angular/src/types/action.ts @@ -0,0 +1,7 @@ +import {MutationInfo} from '@loona/core'; +import {Observable} from 'rxjs'; + +export type ActionMethod = ( + action: any | MutationInfo, + action$: Observable, +) => Observable | Promise; diff --git a/packages/angular/src/types/metadata.ts b/packages/angular/src/types/metadata.ts index 1cc8c64e..87ca384a 100644 --- a/packages/angular/src/types/metadata.ts +++ b/packages/angular/src/types/metadata.ts @@ -1,12 +1,21 @@ +import {UpdateMatchFn} from '@loona/core'; import {DocumentNode} from 'graphql'; export namespace Metadata { export type Queries = Array<{propName: string}>; + export type Resolvers = Array<{ + propName: string, + path: string, + }>; export type Mutations = Array<{ propName: string; mutation: DocumentNode; options: any; }>; + export type Updates = Array<{ + propName: string; + match: UpdateMatchFn; + }>; export type Actions = Record< string, Array<{propName: string; type: string; options: any}> @@ -17,7 +26,9 @@ export namespace Metadata { export interface Metadata { queries: Metadata.Queries; + resolvers: Metadata.Resolvers; mutations: Metadata.Mutations; + updates: Metadata.Updates; actions: Metadata.Actions; defaults?: Metadata.Defaults; typeDefs?: Metadata.TypeDefs; diff --git a/packages/angular/src/types/mutation.ts b/packages/angular/src/types/mutation.ts new file mode 100644 index 00000000..8723aac6 --- /dev/null +++ b/packages/angular/src/types/mutation.ts @@ -0,0 +1,6 @@ +import {Context} from '@loona/core'; + +export type MutationMethod = ( + args: Record, + context: Context, +) => any; diff --git a/packages/angular/src/types/resolve.ts b/packages/angular/src/types/resolve.ts new file mode 100644 index 00000000..d600a194 --- /dev/null +++ b/packages/angular/src/types/resolve.ts @@ -0,0 +1,7 @@ +import {Context} from '@loona/core'; + +export type ResolveMethod = ( + parent: any, + args: Record, + context: Context, +) => any; diff --git a/packages/angular/src/types/update.ts b/packages/angular/src/types/update.ts new file mode 100644 index 00000000..7aebdbca --- /dev/null +++ b/packages/angular/src/types/update.ts @@ -0,0 +1,3 @@ +import {MutationInfo, Context} from '@loona/core'; + +export type UpdateMethod = (action: MutationInfo, context: Context) => void; diff --git a/packages/core/package.json b/packages/core/package.json index b2b6ceab..3f3a68ab 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,6 +13,8 @@ "test": "jest", "bundle": "rollup -c rollup.config.js", "build": "tsc -p tsconfig.json", + "clean": "rimraf build/", + "prebuild": "yarn clean", "postbuild": "yarn bundle", "prepublishOnly": "yarn build", "prettier": "prettier --config ../../.prettierrc --write {src,tests}/**/*.ts" @@ -29,14 +31,15 @@ }, "devDependencies": { "@types/jest": "22.2.3", - "apollo-cache-inmemory": "1.2.2", + "apollo-cache-inmemory": "1.2.9", "graphql": "0.13.2", "graphql-tag": "2.9.2", "jest": "22.4.4", - "prettier": "1.13.6", + "prettier": "1.14.2", + "rimraf": "2.6.2", "rollup": "0.61.2", "ts-jest": "22.4.6", - "typescript": "2.9.1" + "typescript": "2.9.2" }, "jest": { "globals": { @@ -55,4 +58,4 @@ "js" ] } -} \ No newline at end of file +} diff --git a/packages/core/src/helpers.ts b/packages/core/src/helpers.ts index aa104ff1..b7ba942c 100644 --- a/packages/core/src/helpers.ts +++ b/packages/core/src/helpers.ts @@ -1,44 +1,88 @@ -import {DocumentNode} from 'graphql'; -import {DataProxy} from 'apollo-cache'; +import {DocumentNode, FragmentDefinitionNode} from 'graphql'; import produce from 'immer'; +import {ReceivedContext} from './types/common'; import {getMutationDefinition, getFirstField} from './internal/utils'; -export function updateQuery( - query: DocumentNode, - fn: (val: S, args: A, ctx: C) => S | void, +export function getNameOfMutation(mutation: DocumentNode): string { + const def = getMutationDefinition(mutation); + const field = getFirstField(def); + + return field.name.value; +} + +export function getFragmentTypename(fragment: DocumentNode): string { + const def = fragment.definitions.find(def => def.kind === 'FragmentDefinition') as FragmentDefinitionNode; + + return def.typeCondition.name.value; +} + +export function writeFragment( + fragment: DocumentNode, + obj: any, + context: ReceivedContext, ) { - return (_root: any, args: A, context: C) => { - const cache: DataProxy = (context as any).cache; - const previous = cache.readQuery({query}) as S; + const __typename = getFragmentTypename(fragment); + const data = {...obj, __typename}; + + context.cache.writeFragment({ + fragment, + id: context.getCacheKey(data), + data, + }); +} - const data = produce(previous, draft => fn(draft, args, context)); +export function readFragment( + fragment: DocumentNode, + obj: any, + context: ReceivedContext, +) { + return context.cache.readFragment({ + fragment, + id: context.getCacheKey({ + ...obj, + __typename: getFragmentTypename(fragment) + }), + }); +} - cache.writeQuery({query, data}); +export function writeQuery(obj: any, context: ReceivedContext) { + context.cache.writeData({ + data: obj, + }); +} - return null; - }; +export function readQuery( + query: DocumentNode, + context: ReceivedContext, +): R | null { + return context.cache.readQuery({ + query, + }); } -export function Update(query: DocumentNode) { - return ( - _target: any, - _propName: string, - descriptor: TypedPropertyDescriptor, - ) => { - const fn = descriptor.value; +export function patchQuery(context: ReceivedContext) { + return (query: DocumentNode, patchFn: (data: R) => any): R => { + const obj = readQuery(query, context); + const data = produce(obj, patchFn); - if (fn) { - descriptor.value = updateQuery(query, fn); - } + writeQuery(data, context); - return descriptor; + return data; }; } -export function getNameOfMutation(mutation: DocumentNode): string { - const def = getMutationDefinition(mutation); - const field = getFirstField(def); +export function patchFragment(context: ReceivedContext) { + return ( + fragment: DocumentNode, + obj: any, + patchFn: (data: R) => any, + ): R => { + const frgmt: any = readFragment(fragment, obj, context); + const data = produce(frgmt, data => patchFn(data)); - return field.name.value; + writeFragment(fragment, data, context); + + return data; + }; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 051f42eb..34409bfb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,5 +2,13 @@ export {LoonaLink} from './link'; export {Manager} from './manager'; export {QueryDef} from './types/query'; export {MutationDef} from './types/mutation'; +export {ResolverDef, Resolvers} from './types/resolver'; +export { + UpdateDef, + UpdateMatchFn, + UpdateResolveFn, + MutationInfo, +} from './types/update'; +export {ResolveFn, Context} from './types/common'; export {Options} from './types/options'; -export {Update, updateQuery, getNameOfMutation} from './helpers'; +export {getNameOfMutation} from './helpers'; diff --git a/packages/core/src/internal/context.ts b/packages/core/src/internal/context.ts new file mode 100644 index 00000000..72175c54 --- /dev/null +++ b/packages/core/src/internal/context.ts @@ -0,0 +1,15 @@ +import {DataProxy} from 'apollo-cache'; + +import {ReceivedContext, Context} from '../types/common'; +import {patchQuery, patchFragment} from '../helpers'; + +export function buildContext(context: ReceivedContext): Context { + return { + ...context, + patchQuery: patchQuery(context), + patchFragment: patchFragment(context), + writeData(options: DataProxy.WriteDataOptions) { + return context.cache.writeData(options); + }, + }; +} diff --git a/packages/core/src/internal/mutation.ts b/packages/core/src/internal/mutation.ts index 6da2f375..e7a56d34 100644 --- a/packages/core/src/internal/mutation.ts +++ b/packages/core/src/internal/mutation.ts @@ -1,25 +1,42 @@ -import { - MutationSchema, - MutationDef, - MutationResolveFn, -} from '../types/mutation'; -import {MutationManager} from '../mutation'; +import {MutationSchema, MutationDef} from '../types/mutation'; +import {ResolveFn} from '../types/common'; +import {MutationInfo} from '../types/update'; +import {runUpdates} from './update'; +import {UpdateManager} from '../update'; +import {Manager} from '../manager'; +import {getNameOfMutation} from '../helpers'; +import {buildContext} from './context'; -export function createMutationSchema( - mutationManager: MutationManager, -): MutationSchema { +export function createMutationSchema(manager: Manager): MutationSchema { const schema: MutationSchema = {}; - mutationManager.forEach((def, name) => { - schema[name] = createMutationResolver(def); + manager.mutations.forEach((def, name) => { + schema[name] = createMutationResolver(def, manager.updates); }); return schema; } -function createMutationResolver(def: MutationDef): MutationResolveFn { - return async (_, args, ctx) => { - const result = await def.resolve(_, args, ctx); +function createMutationResolver( + def: MutationDef, + updates: UpdateManager, +): ResolveFn { + return async (_, args, context) => { + const enhancedContext = buildContext(context); + const result = await def.resolve(args, enhancedContext); + + const info: MutationInfo = { + name: getNameOfMutation(def.mutation), + variables: args, + result, + }; + + runUpdates({ + updates, + info, + context: enhancedContext, + }); + return result; }; } diff --git a/packages/core/src/internal/query.ts b/packages/core/src/internal/query.ts deleted file mode 100644 index 63c69cef..00000000 --- a/packages/core/src/internal/query.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {QuerySchema, QueryDef, QueryResolveFn} from '../types/query'; -import {QueryManager} from '../query'; - -export function createQuerySchema(queryManager: QueryManager): QuerySchema { - const schema: QuerySchema = {}; - - queryManager.forEach((def, name) => { - schema[name] = createQueryResolver(def); - }); - - return schema; -} - -function createQueryResolver(def: QueryDef): QueryResolveFn { - return def.resolve; -} diff --git a/packages/core/src/internal/resolvers.ts b/packages/core/src/internal/resolvers.ts new file mode 100644 index 00000000..9c2ec5b1 --- /dev/null +++ b/packages/core/src/internal/resolvers.ts @@ -0,0 +1,25 @@ +import {Resolvers, ResolverDef} from '../types/resolver'; +import {Manager} from '../manager'; +import {ResolveFn} from '../types/common'; +import {buildContext} from './context'; + +export function createResolvers(manager: Manager): Resolvers { + const schema: Resolvers = {}; + + manager.resolvers.forEach(def => { + const [typeName, fieldName] = def.path.split('.'); + + if (!schema[typeName]) { + schema[typeName] = {}; + } + + schema[typeName][fieldName] = createResolver(def); + }); + + return schema; +} + +function createResolver(def: ResolverDef): ResolveFn { + return (parent, args, context) => + def.resolve(parent, args, buildContext(context)); +} diff --git a/packages/core/src/internal/update.ts b/packages/core/src/internal/update.ts new file mode 100644 index 00000000..bad746d8 --- /dev/null +++ b/packages/core/src/internal/update.ts @@ -0,0 +1,23 @@ +import {Context} from '../types/common'; +import {MutationInfo} from '../types/update'; +import {UpdateManager} from '../update'; + +export function runUpdates({ + updates, + info, + context, +}: { + updates: UpdateManager; + info: MutationInfo; + context: Context; +}): void { + if (!updates) { + return; + } + + updates.get().forEach(update => { + if (update.match(info)) { + update.resolve(info, context); + } + }); +} diff --git a/packages/core/src/link.ts b/packages/core/src/link.ts index 347167b8..34655593 100644 --- a/packages/core/src/link.ts +++ b/packages/core/src/link.ts @@ -9,7 +9,7 @@ import {withClientState} from 'apollo-link-state'; import {Manager} from './manager'; import {createMutationSchema} from './internal/mutation'; -import {createQuerySchema} from './internal/query'; +import {createResolvers} from './internal/resolvers'; import {Options} from './types/options'; function isManager(obj: any): obj is Manager { @@ -32,6 +32,7 @@ export class LoonaLink extends ApolloLink { defaults: optionsOrManager.defaults, queries: optionsOrManager.queries, mutations: optionsOrManager.mutations, + updates: optionsOrManager.updates, resolvers: optionsOrManager.resolvers, }); } @@ -39,11 +40,10 @@ export class LoonaLink extends ApolloLink { this.stateLink = withClientState({ cache: this.manager.cache, // TODO: make it as a function + // TODO: it's called once :( resolvers: { - // TODO: there's need to be a place for Type resolvers - Query: createQuerySchema(this.manager.queries), - Mutation: createMutationSchema(this.manager.mutations), - ...this.manager.resolvers, + Mutation: createMutationSchema(this.manager), + ...createResolvers(this.manager), }, defaults: this.manager.defaults, typeDefs: this.manager.typeDefs, diff --git a/packages/core/src/manager.ts b/packages/core/src/manager.ts index 6be830d1..d5215d6f 100644 --- a/packages/core/src/manager.ts +++ b/packages/core/src/manager.ts @@ -1,22 +1,23 @@ import {ApolloCache} from 'apollo-cache'; import {MutationManager} from './mutation'; -import {QueryManager} from './query'; +import {UpdateManager} from './update'; +import {ResolversManager} from './resolvers'; import {Options} from './types/options'; export class Manager { cache: ApolloCache; - queries: QueryManager; mutations: MutationManager; - resolvers: any; + updates: UpdateManager; + resolvers: ResolversManager; defaults: any; typeDefs: string | string[] | undefined; constructor(options: Options) { this.cache = options.cache; this.defaults = options.defaults; - this.resolvers = options.resolvers; this.typeDefs = options.typeDefs; - this.queries = new QueryManager(options.queries); + this.resolvers = new ResolversManager(options.resolvers, options.queries); + this.updates = new UpdateManager(options.updates); this.mutations = new MutationManager(options.mutations); } } diff --git a/packages/core/src/query.ts b/packages/core/src/query.ts deleted file mode 100644 index 41297883..00000000 --- a/packages/core/src/query.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {Store} from './internal/store'; -import {QueryDef} from './types/query'; - -export class QueryManager extends Store { - constructor(defs?: QueryDef[]) { - super(); - - if (defs) { - defs.forEach(def => { - this.set(def.name, def); - }); - } - } -} diff --git a/packages/core/src/resolvers.ts b/packages/core/src/resolvers.ts new file mode 100644 index 00000000..08931ed3 --- /dev/null +++ b/packages/core/src/resolvers.ts @@ -0,0 +1,26 @@ +import {Store} from './internal/store'; +import {ResolverDef} from './types/resolver'; +import {QueryDef} from './types/query'; + +export class ResolversManager extends Store { + constructor(defs?: ResolverDef[], queries?: QueryDef[]) { + super(); + + if (defs) { + defs.forEach(def => { + this.set(def.path, def); + }); + } + + if (queries) { + queries.forEach(def => { + const path = `Query.${def.name}`; + + this.set(path, { + path, + resolve: def.resolve, + }); + }); + } + } +} diff --git a/packages/core/src/types/common.ts b/packages/core/src/types/common.ts index 68a0528b..a0e33b84 100644 --- a/packages/core/src/types/common.ts +++ b/packages/core/src/types/common.ts @@ -1,7 +1,26 @@ import {DataProxy} from 'apollo-cache'; +import {DocumentNode} from 'graphql'; export type ResolveFn = ( _: any, args: Record, - context: {cache: DataProxy} & Record, + context: Context & Record, ) => Promise | any; + +export interface ReceivedContext { + cache: DataProxy; + getCacheKey(obj: any): string; +} + +export interface Context extends ReceivedContext { + // reads and writes + patchQuery(query: DocumentNode, producer: (data: any) => any): any; + // reads and writes + patchFragment( + fragment: DocumentNode, + obj: any, + producer: (data: any) => any, + ): any; + // writes + writeData(options: DataProxy.WriteDataOptions): any; +} diff --git a/packages/core/src/types/mutation.ts b/packages/core/src/types/mutation.ts index 80abb21e..0716100f 100644 --- a/packages/core/src/types/mutation.ts +++ b/packages/core/src/types/mutation.ts @@ -1,6 +1,6 @@ import {DocumentNode} from 'graphql'; -import {ResolveFn} from './common'; +import {Context, ResolveFn} from './common'; export interface Mutation { name: string; @@ -8,7 +8,7 @@ export interface Mutation { } export interface MutationSchema { - [key: string]: MutationResolveFn; + [key: string]: ResolveFn; } export interface MutationDef { @@ -16,4 +16,7 @@ export interface MutationDef { resolve: MutationResolveFn; } -export type MutationResolveFn = ResolveFn; +export type MutationResolveFn = ( + args: Record, + context: Context & Record, +) => Promise | any; diff --git a/packages/core/src/types/options.ts b/packages/core/src/types/options.ts index 3fee2bbc..21099be4 100644 --- a/packages/core/src/types/options.ts +++ b/packages/core/src/types/options.ts @@ -2,11 +2,13 @@ import {ApolloCache} from 'apollo-cache'; import {MutationDef} from './mutation'; import {QueryDef} from './query'; +import {UpdateDef} from './update'; export interface Options { cache: ApolloCache; mutations?: MutationDef[]; queries?: QueryDef[]; + updates?: UpdateDef[]; defaults?: any; resolvers?: any; typeDefs?: string | string[]; diff --git a/packages/core/src/types/query.ts b/packages/core/src/types/query.ts index 2b9d4da6..4f52f6a9 100644 --- a/packages/core/src/types/query.ts +++ b/packages/core/src/types/query.ts @@ -1,12 +1,6 @@ import {ResolveFn} from './common'; -export interface QuerySchema { - [key: string]: QueryResolveFn; -} - export type QueryDef = { name: string; - resolve: QueryResolveFn; + resolve: ResolveFn; }; - -export type QueryResolveFn = ResolveFn; diff --git a/packages/core/src/types/resolver.ts b/packages/core/src/types/resolver.ts new file mode 100644 index 00000000..51d26e81 --- /dev/null +++ b/packages/core/src/types/resolver.ts @@ -0,0 +1,12 @@ +import {ResolveFn} from './common'; + +export interface Resolvers { + [key: string]: { + [key: string]: ResolveFn; + }; +} + +export interface ResolverDef { + path: string; + resolve: ResolveFn; +} diff --git a/packages/core/src/types/update.ts b/packages/core/src/types/update.ts new file mode 100644 index 00000000..88777ebe --- /dev/null +++ b/packages/core/src/types/update.ts @@ -0,0 +1,16 @@ +import {Context} from './common'; + +export interface MutationInfo { + name: string; + variables?: Record; + result?: R; +} + +export type UpdateResolveFn = (info: MutationInfo, context: Context) => void; + +export interface UpdateDef { + match: UpdateMatchFn; + resolve: UpdateResolveFn; +} + +export type UpdateMatchFn = (info: MutationInfo) => boolean; diff --git a/packages/core/src/update.ts b/packages/core/src/update.ts new file mode 100644 index 00000000..f0b76330 --- /dev/null +++ b/packages/core/src/update.ts @@ -0,0 +1,13 @@ +import {UpdateDef} from './types/update'; + +export class UpdateManager { + constructor(private defs: UpdateDef[] = []) {} + + add(def: UpdateDef): void { + this.defs.push(def); + } + + get(): UpdateDef[] { + return this.defs; + } +} diff --git a/rollup.config.js b/rollup.config.js index c6dce0d9..fcf1e880 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -14,6 +14,8 @@ export const globals = { // RxJS rxjs: 'rxjs', 'rxjs/operators': 'rxjs.operators', + // Others + immer: 'immer', }; export default name => ({