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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import {IJSONRPCReturnType} from './express'
import {TAsyncified, TReduxed} from './types'
import {Contextual, TAsyncified, TReduxed} from './types'
import {createActions} from './redux'
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>
}
type BulkLocalClient<T> = {[K in keyof T & string]: LocalClient<T[K]>}
type BulkActions<T> = {[K in keyof T & string]: TReduxed<T[K], K>}
type BulkRemoteClient<T> = {[K in keyof T & string]: TAsyncified<T[K]>}
export type BulkServices<T, Cx> = {
[K in keyof T & string]: Contextual<T[K], Cx>
}
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>(
src: T,
@ -22,7 +25,7 @@ function bulkCreate<T, R>(
}, {} as any)
}
export function bulkCreateLocalClient<T, Cx>(
export function bulkCreateLocalClient<Cx, T extends Contextual<{}, Cx>>(
src: T,
context: Cx,
): BulkLocalClient<T> {

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ const MIN_PASSWORD_LENGTH = 10
export class UserService implements RPC<u.IUserService> {
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 {oldPassword, newPassword} = params
const userRepository = this.db.getRepository(User)
@ -27,12 +27,12 @@ export class UserService implements RPC<u.IUserService> {
if (!(user && isValid)) {
throw createError(400, 'Passwords do not match')
}
const password = await this.hash(newPassword)
const password = await this._hash(newPassword)
await userRepository
.update(userId, { password })
}
async findOne(id: number) {
async findOne(context: IContext, id: number) {
const user = await this.db.getRepository(User).findOne(id, {
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)
.findOne({ email }, {
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)
}
}