diff --git a/package-lock.json b/package-lock.json index 55b46eb..e5971d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.2.0.tgz", "integrity": "sha512-oouEibCbHMVdZSDlJBO6bZmID/zA/G/Qx3H1d3rSNPTD+L8UNKvCat7aKWSJ74zYbm5zWGh0GQN0hKj8zYFTCg==", + "dev": true, "requires": { "regenerator-runtime": "^0.12.0" } @@ -788,13 +789,7 @@ "dev": true }, "@rondo/client": { - "version": "file:packages/client", - "requires": { - "react": "^16.7.0", - "react-dom": "^16.7.0", - "react-redux": "^6.0.0", - "redux": "^4.0.1" - } + "version": "file:packages/client" }, "@rondo/common": { "version": "file:packages/common" @@ -1489,6 +1484,16 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, + "axios": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", + "dev": true, + "requires": { + "follow-redirects": "^1.3.0", + "is-buffer": "^1.1.5" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -4887,6 +4892,15 @@ "readable-stream": "^2.0.4" } }, + "follow-redirects": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.6.1.tgz", + "integrity": "sha512-t2JCjbzxQpWvbhts3l6SH1DKzSrx8a+SsaVf4h6bG4kOXUuPYS/kg2Lr4gQSb7eemaHqJkOThF1BGyjlUkO1GQ==", + "dev": true, + "requires": { + "debug": "=3.1.0" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -6134,6 +6148,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz", "integrity": "sha512-TFsu3TV3YLY+zFTZDrN8L2DTFanObwmBLpWvJs1qfUuEQ5bTAdFcwfx2T/bsCXfM9QHSLvjfP+nihEl0yvozxw==", + "dev": true, "requires": { "react-is": "^16.3.2" } @@ -6456,6 +6471,7 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, "requires": { "loose-envify": "^1.0.0" } @@ -8135,7 +8151,8 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "js-yaml": { "version": "3.12.1", @@ -8652,6 +8669,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -10231,6 +10249,7 @@ "version": "15.6.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz", "integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==", + "dev": true, "requires": { "loose-envify": "^1.3.1", "object-assign": "^4.1.1" @@ -10458,6 +10477,7 @@ "version": "16.7.0", "resolved": "https://registry.npmjs.org/react/-/react-16.7.0.tgz", "integrity": "sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -10469,6 +10489,7 @@ "version": "16.7.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.7.0.tgz", "integrity": "sha512-D0Ufv1ExCAmF38P2Uh1lwpminZFRXEINJe53zRAbm4KPwSyd6DY/uDoS0Blj9jvPpn1+wivKpZYc8aAAN/nAkg==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -10479,12 +10500,14 @@ "react-is": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.7.0.tgz", - "integrity": "sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==" + "integrity": "sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g==", + "dev": true }, "react-redux": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-6.0.0.tgz", "integrity": "sha512-EmbC3uLl60pw2VqSSkj6HpZ6jTk12RMrwXMBdYtM6niq0MdEaRq9KYCwpJflkOZj349BLGQm1MI/JO1W96kLWQ==", + "dev": true, "requires": { "@babel/runtime": "^7.2.0", "hoist-non-react-statics": "^3.2.1", @@ -10673,6 +10696,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "dev": true, "requires": { "loose-envify": "^1.4.0", "symbol-observable": "^1.2.0" @@ -10686,7 +10710,8 @@ "regenerator-runtime": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", - "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true }, "regex-cache": { "version": "0.4.4", @@ -10979,6 +11004,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.12.0.tgz", "integrity": "sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -11781,7 +11807,8 @@ "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true }, "symbol-tree": { "version": "3.2.2", diff --git a/package.json b/package.json index 091279d..a42be7b 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,9 @@ { + "dependencies": { + "@rondo/client": "file:packages/client", + "@rondo/common": "file:packages/common", + "@rondo/server": "file:packages/server" + }, "devDependencies": { "@types/bcrypt": "^3.0.0", "@types/body-parser": "^1.17.0", @@ -20,6 +25,11 @@ "@types/std-mocks": "^1.0.0", "@types/supertest": "^2.0.7", "@types/uuid": "^3.4.4", + "axios": "^0.18.0", + "react": "^16.7.0", + "react-dom": "^16.7.0", + "react-redux": "^6.0.0", + "redux": "^4.0.1", "browserify": "^16.2.3", "buildfile": "^1.2.17", "jest": "^23.6.0", @@ -38,10 +48,5 @@ "typescript-tslint-plugin": "^0.2.1", "watchify": "^3.11.0" }, - "name": "node", - "dependencies": { - "@rondo/client": "file:packages/client", - "@rondo/common": "file:packages/common", - "@rondo/server": "file:packages/server" - } + "name": "node" } diff --git a/packages/client/package.json b/packages/client/package.json index b440d61..1b9b8c5 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,7 +1,8 @@ { "name": "@rondo/client", "private": true, - "dependencies": { + "peerDependencies": { + "axios": "^0.18.0", "react": "^16.7.0", "react-dom": "^16.7.0", "react-redux": "^6.0.0", diff --git a/packages/client/src/actions/ActionTypes.ts b/packages/client/src/actions/ActionTypes.ts new file mode 100644 index 0000000..4f7a854 --- /dev/null +++ b/packages/client/src/actions/ActionTypes.ts @@ -0,0 +1,19 @@ +// Get the type of promise +// https://www.typescriptlang.org/docs/handbook/advanced-types.html +// section: Type inference in conditional types +type Unpacked = T extends Promise ? U : T +import {IAction} from './IAction' + +// Also from TypeScript handbook: +// https://www.typescriptlang.org/docs/handbook/advanced-types.html +type FunctionProperties = { + [K in keyof T]: T[K] extends (...args: any[]) => IAction ? { + payload: Unpacked['payload']>, + type: Unpacked['type']>, + }: never +} + +// https://stackoverflow.com/questions/48305190/ +// Is there an automatic way to create a discriminated union for all interfaces +// in a namespace? +export type ActionTypes = FunctionProperties[keyof FunctionProperties] diff --git a/packages/client/src/actions/IAction.ts b/packages/client/src/actions/IAction.ts new file mode 100644 index 0000000..0383d1f --- /dev/null +++ b/packages/client/src/actions/IAction.ts @@ -0,0 +1,7 @@ +// Maybe this won't be necessary after this is merged: +// https://github.com/Microsoft/TypeScript/pull/29478 + +export interface IAction { + payload: Promise | T, + type: ActionType +} diff --git a/packages/client/src/actions/UnionType.ts b/packages/client/src/actions/UnionType.ts new file mode 100644 index 0000000..45e6fe9 --- /dev/null +++ b/packages/client/src/actions/UnionType.ts @@ -0,0 +1,13 @@ +// Get the type of promise +// https://www.typescriptlang.org/docs/handbook/advanced-types.html +// section: Type inference in conditional types +type Unpacked = T extends Promise ? U : T + +type FunctionProperties = { + [K in keyof T]: T[K] extends (...args: any[]) => any ? { + payload: Unpacked['payload']>, + type: Unpacked['type']>, + }: never +} + +export type UnionType = FunctionProperties[keyof FunctionProperties] diff --git a/packages/client/src/actions/UserActions.ts b/packages/client/src/actions/UserActions.ts new file mode 100644 index 0000000..7354734 --- /dev/null +++ b/packages/client/src/actions/UserActions.ts @@ -0,0 +1,39 @@ +import {ActionTypes} from './ActionTypes' +import {IAPIDef, ICredentials, IUser} from '@rondo/common' +import {IHTTPClient} from '../http/IHTTPClient' + +export enum UserActionKeys { + USER_LOG_IN = 'USER_LOG_IN', + USER_LOG_IN_FULFILLED = 'USER_LOG_IN_FULFILLED', + USER_LOG_IN_REJECTED = 'USER_LOG_IN_REJECTED', + + USER_LOG_OUT = 'USER_LOG_OUT', + USER_LOG_OUT_FULFILLED = 'USER_LOG_OUT_FULFILLED', + USER_LOG_OUT_REJECTED = 'USER_LOG_OUT_REJECTED', +} + +interface IAction { + payload: Promise | PayloadType, + type: ActionType +} + +export class UserActions { + constructor(protected readonly http: IHTTPClient) {} + + logIn(credentials: ICredentials): IAction { + return { + payload: this.http.post('/auth/login', credentials), + type: UserActionKeys.USER_LOG_IN, + } + } + + logOut(): IAction { + return { + payload: this.http.get('/auth/logout'), + type: UserActionKeys.USER_LOG_OUT, + } + } +} + +// This makes it very easy to write reducer code. +export type UserActionType = ActionTypes diff --git a/packages/client/src/actions/index.ts b/packages/client/src/actions/index.ts new file mode 100644 index 0000000..3fa9d76 --- /dev/null +++ b/packages/client/src/actions/index.ts @@ -0,0 +1,3 @@ +export * from './IAction' +export * from './UnionType' +export * from './UserActions' diff --git a/packages/client/src/http/HTTPClient.ts b/packages/client/src/http/HTTPClient.ts new file mode 100644 index 0000000..0b33e06 --- /dev/null +++ b/packages/client/src/http/HTTPClient.ts @@ -0,0 +1,128 @@ +import assert from 'assert' +import axios, {AxiosInstance} from 'axios' +import {IHTTPClient} from './IHTTPClient' +import {IHeader} from './IHeader' +import {IMethod, IRoutes} from '@rondo/common' +import {ITypedRequestParams} from './ITypedRequestParams' + +export class HTTPClient implements IHTTPClient { + protected readonly axios: AxiosInstance + + constructor(baseURL = '', headers?: IHeader) { + this.axios = axios.create({ + baseURL, + headers, + }) + } + + async request< + P extends keyof T & string, + M extends IMethod, + >(params: ITypedRequestParams): Promise { + + const url = params.path.replace(/:[a-zA-Z0-9-]+/g, (match) => { + const key = match.substring(1) + assert(params.params, 'Params is required, but not defined') + assert(params.params!.hasOwnProperty(key)) + return params.params![key] + }) + + return this.axios.request({ + method: params.method, + url, + params: params.query, + data: params.body, + }) + } + + get

( + path: P, + query?: T[P]['get']['query'], + params?: T[P]['get']['params'], + ) { + return this.request({ + method: 'get', + path, + query, + params, + }) + } + + post

( + path: P, + body: T[P]['post']['body'], + params?: T[P]['post']['params'], + ) { + return this.request({ + method: 'post', + path, + body, + params, + }) + } + + put

( + path: P, + body: T[P]['put']['body'], + params?: T[P]['put']['params'], + ) { + return this.request({ + method: 'put', + path, + body, + params, + }) + } + + delete

( + path: P, + body: T[P]['delete']['body'], + params?: T[P]['delete']['params'], + ) { + return this.request({ + method: 'delete', + path, + body, + params, + }) + } + + head

( + path: P, + query?: T[P]['head']['query'], + params?: T[P]['head']['params'], + ) { + return this.request({ + method: 'head', + path, + params, + query, + }) + } + + options

( + path: P, + query?: T[P]['options']['query'], + params?: T[P]['options']['params'], + ) { + return this.request({ + method: 'options', + path, + params, + query, + }) + } + + patch

( + path: P, + body: T[P]['patch']['body'], + params?: T[P]['patch']['params'], + ) { + return this.request({ + method: 'patch', + path, + body, + params, + }) + } +} diff --git a/packages/client/src/http/IHTTPClient.ts b/packages/client/src/http/IHTTPClient.ts new file mode 100644 index 0000000..4ba3c4e --- /dev/null +++ b/packages/client/src/http/IHTTPClient.ts @@ -0,0 +1,51 @@ +import {IMethod, IRoutes} from '@rondo/common' +import {ITypedRequestParams} from './ITypedRequestParams' + +export interface IHTTPClient { + request< + P extends keyof T & string, + M extends IMethod, + >(params: ITypedRequestParams): Promise + + get

( + path: P, + query?: T[P]['get']['query'], + params?: T[P]['get']['params'], + ): Promise + + post

( + path: P, + body: T[P]['post']['body'], + params?: T[P]['post']['params'], + ): Promise + + put

( + path: P, + body: T[P]['put']['body'], + params?: T[P]['put']['params'], + ): Promise + + delete

( + path: P, + body: T[P]['delete']['body'], + params?: T[P]['delete']['params'], + ): Promise + + head

( + path: P, + query?: T[P]['head']['query'], + params?: T[P]['head']['params'], + ): Promise + + options

( + path: P, + query?: T[P]['options']['query'], + params?: T[P]['options']['params'], + ): Promise + + patch

( + path: P, + body: T[P]['patch']['body'], + params?: T[P]['patch']['params'], + ): Promise +} diff --git a/packages/client/src/http/IHeader.ts b/packages/client/src/http/IHeader.ts new file mode 100644 index 0000000..6c7cedc --- /dev/null +++ b/packages/client/src/http/IHeader.ts @@ -0,0 +1,3 @@ +export interface IHeader { + [key: string]: string +} diff --git a/packages/client/src/http/ITypedRequestParams.ts b/packages/client/src/http/ITypedRequestParams.ts new file mode 100644 index 0000000..e403ce1 --- /dev/null +++ b/packages/client/src/http/ITypedRequestParams.ts @@ -0,0 +1,13 @@ +import {IRoutes, IMethod} from '@rondo/common' + +export interface ITypedRequestParams< + T extends IRoutes, + P extends keyof T & string, + M extends IMethod, +> { + method: M, + path: P, + params?: T[P][M]['params'], + query?: T[P][M]['query'], + body?: T[P][M]['body'], +} diff --git a/packages/client/src/http/index.ts b/packages/client/src/http/index.ts new file mode 100644 index 0000000..57ed147 --- /dev/null +++ b/packages/client/src/http/index.ts @@ -0,0 +1,2 @@ +export * from './HTTPClient' +export * from './IHeader' diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx index 1dc25e6..d210aab 100644 --- a/packages/client/src/index.tsx +++ b/packages/client/src/index.tsx @@ -1,11 +1,5 @@ -import React from 'react' -import ReactDOM from 'react-dom' - export * from './renderer' -export {React} -export {ReactDOM} - // import ReactDOM from 'react-dom' // import React from 'react' // import {CComponent} from './components/Component' diff --git a/packages/client/src/reducers/user.ts b/packages/client/src/reducers/user.ts new file mode 100644 index 0000000..baac40d --- /dev/null +++ b/packages/client/src/reducers/user.ts @@ -0,0 +1,19 @@ +import {IUser} from '@rondo/common' +import {UserActionKeys, UserActionType} from '../actions/UserActions' + +interface IState { + user?: IUser +} + +const defaultState: IState = { + user: undefined, +} + +export function user(state = defaultState, action: UserActionType): IState { + switch (action.type) { + case UserActionKeys.USER_LOG_IN: + return {user: action.payload} + case UserActionKeys.USER_LOG_OUT: + return {user: undefined} + } +} diff --git a/packages/common/src/IAPIDef.ts b/packages/common/src/IAPIDef.ts index d115a97..f3af39e 100644 --- a/packages/common/src/IAPIDef.ts +++ b/packages/common/src/IAPIDef.ts @@ -1,18 +1,16 @@ +import {ICredentials} from './ICredentials' +import {IUser} from './IUser' + export interface IAPIDef { '/auth/register': { 'post': { - body: { - username: string - password: string - } + body: ICredentials } } '/auth/login': { 'post': { - body: { - username: string - password: string - } + body: ICredentials + response: IUser } } '/auth/logout': { diff --git a/packages/server/src/services/ICredentials.ts b/packages/common/src/ICredentials.ts similarity index 100% rename from packages/server/src/services/ICredentials.ts rename to packages/common/src/ICredentials.ts diff --git a/packages/server/src/services/IUser.ts b/packages/common/src/IUser.ts similarity index 100% rename from packages/server/src/services/IUser.ts rename to packages/common/src/IUser.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 5e7f455..20f1a79 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,2 +1,4 @@ -export * from './IRoutes' export * from './IAPIDef' +export * from './ICredentials' +export * from './IRoutes' +export * from './IUser' diff --git a/packages/server/src/middleware/Authenticator.test.ts b/packages/server/src/middleware/Authenticator.test.ts index b821c3a..b44b749 100644 --- a/packages/server/src/middleware/Authenticator.test.ts +++ b/packages/server/src/middleware/Authenticator.test.ts @@ -1,7 +1,8 @@ import express, {Application} from 'express' import request from 'supertest' import {Authenticator} from './Authenticator' -import {ICredentials, IUserService} from '../services' +import {ICredentials} from '@rondo/common' +import {IUserService} from '../services' import {handlePromise} from './handlePromise' import {urlencoded} from 'body-parser' diff --git a/packages/server/src/services/IUserService.ts b/packages/server/src/services/IUserService.ts index 80d5261..42dcc21 100644 --- a/packages/server/src/services/IUserService.ts +++ b/packages/server/src/services/IUserService.ts @@ -1,5 +1,5 @@ -import {ICredentials} from './ICredentials' -import {IUser} from './IUser' +import {ICredentials} from '@rondo/common' +import {IUser} from '@rondo/common' export interface IUserService { createUser(credentials: ICredentials): Promise diff --git a/packages/server/src/services/UserService.ts b/packages/server/src/services/UserService.ts index 0244428..e6b0c2a 100644 --- a/packages/server/src/services/UserService.ts +++ b/packages/server/src/services/UserService.ts @@ -1,6 +1,6 @@ import createError from 'http-errors' import {BaseService} from './BaseService' -import {ICredentials} from './ICredentials' +import {ICredentials} from '@rondo/common' import {IUserService} from './IUserService' import {UserEmail} from '../entities/UserEmail' import {User} from '../entities/User' diff --git a/packages/server/src/services/index.ts b/packages/server/src/services/index.ts index 58ad54e..4012b9e 100644 --- a/packages/server/src/services/index.ts +++ b/packages/server/src/services/index.ts @@ -1,4 +1,2 @@ export * from './UserService' export * from './IUserService' -export * from './IUser' -export * from './ICredentials'