Add ability to configure rpc services. Needs further testing

This commit is contained in:
Jerko Steiner 2019-08-31 09:12:07 +07:00
parent fbd7a2229b
commit 67e3da3246
13 changed files with 88 additions and 101 deletions

View File

@ -9,5 +9,7 @@ export * from './middleware'
export * from './redux' export * from './redux'
export * from './renderer' export * from './renderer'
export * from './store' export * from './store'
export * from './team'
export * from './test-utils' export * from './test-utils'
import * as team from './team'
export {team}

View File

@ -10,33 +10,27 @@ import {Router} from 'react-router-dom'
import {Store} from 'redux' import {Store} from 'redux'
import {createBrowserHistory} from 'history' import {createBrowserHistory} from 'history'
export interface IClientRendererParams<A extends Action, D extends IAPIDef> { export interface IClientRendererParams<Props> {
readonly RootComponent: React.ComponentType<{ readonly RootComponent: React.ComponentType<Props>
config: IClientConfig,
http: IHTTPClient<D>
}>,
readonly target?: HTMLElement readonly target?: HTMLElement
readonly hydrate: boolean // TODO make this better readonly hydrate: boolean // TODO make this better
} }
export class ClientRenderer<A extends Action, D extends IAPIDef> export class ClientRenderer<Props>
implements IRenderer { implements IRenderer<Props> {
constructor(readonly params: IClientRendererParams<A, D>) {} constructor(readonly params: IClientRendererParams<Props>) {}
render<State>( render<State>(
url: string, url: string,
store: Store<State>, store: Store<State>,
config = (window as any).__APP_CONFIG__ as IClientConfig, props: Props,
config: IClientConfig,
) { ) {
const { const {
RootComponent, RootComponent,
target = document.getElementById('container'), target = document.getElementById('container'),
} = this.params } = this.params
const http = new HTTPClient<D>(config.baseUrl + '/api', {
'x-csrf-token': config.csrfToken,
})
const history = createBrowserHistory({ const history = createBrowserHistory({
basename: config.baseUrl, basename: config.baseUrl,
}) })
@ -45,7 +39,7 @@ export class ClientRenderer<A extends Action, D extends IAPIDef>
ReactDOM.hydrate( ReactDOM.hydrate(
<Provider store={store}> <Provider store={store}>
<Router history={history}> <Router history={history}>
<RootComponent config={config} http={http} /> <RootComponent {...props} />
</Router> </Router>
</Provider>, </Provider>,
target, target,
@ -54,7 +48,7 @@ export class ClientRenderer<A extends Action, D extends IAPIDef>
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<Router history={history}> <Router history={history}>
<RootComponent config={config} http={http} /> <RootComponent {...props} />
</Router> </Router>
</Provider>, </Provider>,
target, target,

View File

@ -2,10 +2,11 @@ import {IAPIDef} from '@rondo.dev/common'
import {IClientConfig} from './IClientConfig' import {IClientConfig} from './IClientConfig'
import {Store} from 'redux' import {Store} from 'redux'
export interface IRenderer { export interface IRenderer<Props> {
render<State>( render<State>(
url: string, url: string,
store: Store<State>, store: Store<State>,
props: Props,
config: IClientConfig, config: IClientConfig,
): any ): any
} }

View File

@ -11,27 +11,20 @@ import {StaticRouter} from 'react-router-dom'
import {Store} from 'redux' import {Store} from 'redux'
import {renderToNodeStream} from 'react-dom/server' import {renderToNodeStream} from 'react-dom/server'
export class ServerRenderer<A extends Action, D extends IAPIDef> export class ServerRenderer<Props> implements IRenderer<Props> {
implements IRenderer {
constructor( constructor(
readonly RootComponent: React.ComponentType<{ readonly RootComponent: React.ComponentType<Props>,
config: IClientConfig,
http: IHTTPClient<D>
}>,
) {} ) {}
async render<State>( async render<State>(
url: string, url: string,
store: Store<State>, store: Store<State>,
props: Props,
config: IClientConfig, config: IClientConfig,
host: string = '', host: string = '',
headers: Record<string, string> = {}, headers: Record<string, string> = {},
) { ) {
const {RootComponent} = this const {RootComponent} = this
// TODO set cookie in headers here... // TODO set cookie in headers here...
const http = new HTTPClient<D>(
'http://' + host + config.baseUrl + '/api',
headers,
)
const context: StaticRouterContext = {} const context: StaticRouterContext = {}
@ -42,7 +35,7 @@ export class ServerRenderer<A extends Action, D extends IAPIDef>
location={url} location={url}
context={context} context={context}
> >
<RootComponent config={config} http={http} /> <RootComponent {...props} />
</StaticRouter> </StaticRouter>
</Provider> </Provider>
) )

View File

@ -40,10 +40,7 @@ describe('TeamConnector', () => {
reducers: {Team: Feature.Team}, reducers: {Team: Feature.Team},
select: state => state.Team, select: state => state.Team,
}) })
.withComponent(select => .withComponent(select => Feature.configure(teamActions, select))
new Feature
.TeamConnector(teamActions)
.connect(select))
.withJSX((Component, props) => .withJSX((Component, props) =>
<MemoryRouter initialEntries={historyEntries}> <MemoryRouter initialEntries={historyEntries}>
<Component {...props} /> <Component {...props} />

View File

@ -1,37 +1,32 @@
import {Connector} from '../redux/Connector' import {Connector} from '../redux/Connector'
import {TStateSelector} from '../redux' import {pack, TStateSelector} from '../redux'
import {ITeamState} from './TeamReducer' import {ITeamState} from './TeamReducer'
import {TeamActions} from './TeamActions' import {TeamActions} from './TeamActions'
import {TeamManager} from './TeamManager' import {TeamManager} from './TeamManager'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import {withRouter} from 'react-router-dom' import {withRouter} from 'react-router-dom'
export class TeamConnector extends Connector<ITeamState> { export function configure<State>(
constructor(protected readonly teamActions: TeamActions) { teamActions: TeamActions,
super() getLocalState: TStateSelector<State, ITeamState>,
} ) {
const Component = pack(
connect<State>(getLocalState: TStateSelector<State, ITeamState>) {
const Component = this.wrap(
getLocalState, getLocalState,
state => ({ state => ({...state}),
...state,
}),
d => ({ d => ({
addUser: bindActionCreators(this.teamActions.addUser, d), addUser: bindActionCreators(teamActions.addUser, d),
removeUser: bindActionCreators(this.teamActions.removeUser, d), removeUser: bindActionCreators(teamActions.removeUser, d),
createTeam: bindActionCreators(this.teamActions.createTeam, d), createTeam: bindActionCreators(teamActions.createTeam, d),
updateTeam: bindActionCreators(this.teamActions.updateTeam, d), updateTeam: bindActionCreators(teamActions.updateTeam, d),
removeTeam: bindActionCreators(this.teamActions.removeTeam, d), removeTeam: bindActionCreators(teamActions.removeTeam, d),
fetchMyTeams: bindActionCreators(this.teamActions.fetchMyTeams, d), fetchMyTeams: bindActionCreators(teamActions.fetchMyTeams, d),
fetchUsersInTeam: fetchUsersInTeam:
bindActionCreators(this.teamActions.fetchUsersInTeam, d), bindActionCreators(teamActions.fetchUsersInTeam, d),
findUserByEmail: findUserByEmail:
bindActionCreators(this.teamActions.findUserByEmail, d), bindActionCreators(teamActions.findUserByEmail, d),
}), }),
TeamManager, TeamManager,
) )
return Component return Component
} }
}

View File

@ -0,0 +1,8 @@
import {IContext} from './IContext'
import {ITeamService} from './team'
import {IUserService} from './user'
export interface IRPCServices {
userService: IUserService
teamService: ITeamService
}

View File

@ -1,4 +1,4 @@
import * as util from './util' import * as util from './bulk'
import express from 'express' import express from 'express'
import {Contextual} from './types' import {Contextual} from './types'
import {jsonrpc} from './express' import {jsonrpc} from './express'

View File

@ -1,5 +1,5 @@
import {IJSONRPCReturnType} from './express' import {IJSONRPCReturnType} from './express'
import {TAsyncified, TReduxed} from './types' import {Contextual, TAsyncified, TReduxed} from './types'
import {createActions} from './redux' import {createActions} from './redux'
import {createLocalClient, LocalClient} from './local' import {createLocalClient, LocalClient} from './local'
@ -7,9 +7,12 @@ function keys<T>(obj: T): Array<keyof T & string> {
return Object.keys(obj) as Array<keyof T & string> return Object.keys(obj) as Array<keyof T & string>
} }
type BulkLocalClient<T> = {[K in keyof T & string]: LocalClient<T[K]>} export type BulkServices<T, Cx> = {
type BulkActions<T> = {[K in keyof T & string]: TReduxed<T[K], K>} [K in keyof T & string]: Contextual<T[K], Cx>
type BulkRemoteClient<T> = {[K in keyof T & string]: TAsyncified<T[K]>} }
export type BulkLocalClient<T> = {[K in keyof T & string]: LocalClient<T[K]>}
export type BulkActions<T> = {[K in keyof T & string]: TReduxed<T[K], K>}
export type BulkClient<T> = {[K in keyof T & string]: TAsyncified<T[K]>}
function bulkCreate<T, R>( function bulkCreate<T, R>(
src: T, src: T,
@ -22,7 +25,7 @@ function bulkCreate<T, R>(
}, {} as any) }, {} as any)
} }
export function bulkCreateLocalClient<T, Cx>( export function bulkCreateLocalClient<Cx, T extends Contextual<{}, Cx>>(
src: T, src: T,
context: Cx, context: Cx,
): BulkLocalClient<T> { ): BulkLocalClient<T> {

View File

@ -1,3 +1,4 @@
export * from './bulk'
export * from './ensure' export * from './ensure'
export * from './error' export * from './error'
export * from './express' export * from './express'
@ -6,4 +7,3 @@ export * from './local'
export * from './redux' export * from './redux'
export * from './remote' export * from './remote'
export * from './types' export * from './types'
export * from './util'

View File

@ -12,7 +12,7 @@ import {IApplication} from './IApplication'
import {IConfig} from './IConfig' import {IConfig} from './IConfig'
import {IDatabase} from '../database/IDatabase' import {IDatabase} from '../database/IDatabase'
import {ILogger} from '../logger/ILogger' import {ILogger} from '../logger/ILogger'
import {IRoutes} from '@rondo.dev/common' import {IRoutes, IContext} from '@rondo.dev/common'
import {IServices} from './IServices' import {IServices} from './IServices'
import {ITransactionManager} from '../database/ITransactionManager' import {ITransactionManager} from '../database/ITransactionManager'
import {loggerFactory} from '../logger' import {loggerFactory} from '../logger'
@ -112,9 +112,13 @@ export class Application implements IApplication {
).handle) ).handle)
} }
protected getContext(req: express.Request): IContext {
return {user: req.user}
}
protected jsonrpc() { protected jsonrpc() {
return jsonrpc( return jsonrpc(
req => ({user: req.user}), req => this.getContext(req),
this.getApiLogger(), this.getApiLogger(),
(path, service, callback) => this (path, service, callback) => this
.database .database
@ -124,17 +128,7 @@ export class Application implements IApplication {
} }
protected configureRPC(router: express.Router) { protected configureRPC(router: express.Router) {
router.use( // Override this method
'/rpc',
this.jsonrpc()
.addService('/team',
new rpc.TeamService(this.database, this.services.userPermissions),
keys<rpc.TeamService>())
.addService('/user',
new rpc.UserService(this.database),
keys<rpc.UserService>())
.router(),
)
} }
protected configureApiErrorHandling(router: express.Router) { protected configureApiErrorHandling(router: express.Router) {

View File

@ -18,7 +18,7 @@ export class TeamService implements RPC<t.ITeamService> {
protected readonly permissions: IUserPermissions, protected readonly permissions: IUserPermissions,
) {} ) {}
async create(params: t.ITeamCreateParams, context: IContext) { async create(context: IContext, params: t.ITeamCreateParams) {
const userId = context.user!.id const userId = context.user!.id
const name = trim(params.name) const name = trim(params.name)
@ -32,17 +32,17 @@ export class TeamService implements RPC<t.ITeamService> {
userId, userId,
}) })
await this.addUser({ await this.addUser(context, {
teamId: team.id, teamId: team.id,
userId, userId,
// ADMIN role // ADMIN role
roleId: 1, roleId: 1,
}, context) })
return team return team
} }
async remove({id}: t.ITeamRemoveParams, context: IContext) { async remove(context: IContext, {id}: t.ITeamRemoveParams) {
const userId = context.user!.id const userId = context.user!.id
await this.permissions.belongsToTeam({ await this.permissions.belongsToTeam({
@ -59,7 +59,7 @@ export class TeamService implements RPC<t.ITeamService> {
return {id} return {id}
} }
async update({id, name}: t.ITeamUpdateParams, context: IContext) { async update(context: IContext, {id, name}: t.ITeamUpdateParams) {
const userId = context.user!.id const userId = context.user!.id
await this.permissions.belongsToTeam({ await this.permissions.belongsToTeam({
@ -74,15 +74,15 @@ export class TeamService implements RPC<t.ITeamService> {
name, name,
}) })
return (await this.findOne(id))! return (await this.findOne(context, id))!
} }
async addUser(params: t.ITeamAddUserParams, context: IContext) { async addUser(context: IContext, params: t.ITeamAddUserParams) {
const {userId, teamId, roleId} = params const {userId, teamId, roleId} = params
await this.db.getRepository(UserTeam) await this.db.getRepository(UserTeam)
.save({userId, teamId, roleId}) .save({userId, teamId, roleId})
const userTeam = await this.createFindUserInTeamQuery() const userTeam = await this._createFindUserInTeamQuery()
.where({ .where({
userId, userId,
teamId, teamId,
@ -90,10 +90,10 @@ export class TeamService implements RPC<t.ITeamService> {
}) })
.getOne() .getOne()
return this.mapUserInTeam(userTeam!) return this._mapUserInTeam(userTeam!)
} }
async removeUser(params: t.ITeamAddUserParams, context: IContext) { async removeUser(context: IContext, params: t.ITeamAddUserParams) {
const {teamId, userId, roleId} = params const {teamId, userId, roleId} = params
await this.permissions.belongsToTeam({ await this.permissions.belongsToTeam({
@ -108,7 +108,7 @@ export class TeamService implements RPC<t.ITeamService> {
return {teamId, userId, roleId} return {teamId, userId, roleId}
} }
async findOne(id: number) { async findOne(context: IContext, id: number) {
return this.db.getRepository(Team).findOne(id) return this.db.getRepository(Team).findOne(id)
} }
@ -123,7 +123,7 @@ export class TeamService implements RPC<t.ITeamService> {
.getMany() .getMany()
} }
async findUsers(teamId: number, context: IContext) { async findUsers(context: IContext, teamId: number) {
const userId = context.user!.id const userId = context.user!.id
await this.permissions.belongsToTeam({ await this.permissions.belongsToTeam({
@ -131,16 +131,16 @@ export class TeamService implements RPC<t.ITeamService> {
userId, userId,
}) })
const userTeams = await this.createFindUserInTeamQuery() const userTeams = await this._createFindUserInTeamQuery()
.where('ut.teamId = :teamId', { .where('ut.teamId = :teamId', {
teamId, teamId,
}) })
.getMany() .getMany()
return userTeams.map(this.mapUserInTeam) return userTeams.map(this._mapUserInTeam)
} }
protected mapUserInTeam(ut: UserTeam): IUserInTeam { protected _mapUserInTeam(ut: UserTeam): IUserInTeam {
return { return {
teamId: ut.teamId, teamId: ut.teamId,
userId: ut.userId, userId: ut.userId,
@ -150,7 +150,7 @@ export class TeamService implements RPC<t.ITeamService> {
} }
} }
protected createFindUserInTeamQuery() { protected _createFindUserInTeamQuery() {
return this.db.getRepository(UserTeam) return this.db.getRepository(UserTeam)
.createQueryBuilder('ut') .createQueryBuilder('ut')
.select('ut') .select('ut')

View File

@ -13,7 +13,7 @@ const MIN_PASSWORD_LENGTH = 10
export class UserService implements RPC<u.IUserService> { export class UserService implements RPC<u.IUserService> {
constructor(protected readonly db: IDatabase) {} constructor(protected readonly db: IDatabase) {}
async changePassword(params: u.IChangePasswordParams, context: IContext) { async changePassword(context: IContext, params: u.IChangePasswordParams) {
const userId = context.user!.id const userId = context.user!.id
const {oldPassword, newPassword} = params const {oldPassword, newPassword} = params
const userRepository = this.db.getRepository(User) const userRepository = this.db.getRepository(User)
@ -27,12 +27,12 @@ export class UserService implements RPC<u.IUserService> {
if (!(user && isValid)) { if (!(user && isValid)) {
throw createError(400, 'Passwords do not match') throw createError(400, 'Passwords do not match')
} }
const password = await this.hash(newPassword) const password = await this._hash(newPassword)
await userRepository await userRepository
.update(userId, { password }) .update(userId, { password })
} }
async findOne(id: number) { async findOne(context: IContext, id: number) {
const user = await this.db.getRepository(User).findOne(id, { const user = await this.db.getRepository(User).findOne(id, {
relations: ['emails'], relations: ['emails'],
}) })
@ -49,7 +49,7 @@ export class UserService implements RPC<u.IUserService> {
} }
} }
async findUserByEmail(email: string) { async findUserByEmail(context: IContext, email: string) {
const userEmail = await this.db.getRepository(UserEmail) const userEmail = await this.db.getRepository(UserEmail)
.findOne({ email }, { .findOne({ email }, {
relations: ['user'], relations: ['user'],
@ -69,7 +69,7 @@ export class UserService implements RPC<u.IUserService> {
} }
} }
protected async hash(password: string): Promise<string> { protected async _hash(password: string): Promise<string> {
return hash(password, SALT_ROUNDS) return hash(password, SALT_ROUNDS)
} }
} }