-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* adding types * adding the rest * adding demo * adding comments * forcing uppercase * camelcase id * update lockfile * swapping demo to comments
- Loading branch information
Showing
10 changed files
with
43,943 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Body, Query } from "../api/client/types"; | ||
import { | ||
ActionGenerator, | ||
APILifecycleActionTypesInterface, | ||
CreateRequestAction, | ||
LoadRequestAction, | ||
ModifyRequestAction, | ||
Registration | ||
} from "./types"; | ||
|
||
class APILifeCycleActionType implements APILifecycleActionTypesInterface { | ||
constructor(stage: string) { | ||
this.stage = stage; | ||
} | ||
|
||
stage: string; | ||
request = (name: string) => `${this.stage}_${name.toUpperCase()}_REQUEST`; | ||
success = (name: string) => `${this.stage}_${name.toUpperCase()}_SUCCESS`; | ||
failure = (name: string) => `${this.stage}_${name.toUpperCase()}_FAILURE`; | ||
} | ||
|
||
export const loadActionTypes = new APILifeCycleActionType("LOAD"); | ||
export const createActionTypes = new APILifeCycleActionType("CREATE"); | ||
export const modifyActionTypes = new APILifeCycleActionType("MODIFY"); | ||
|
||
export const deepUpdateObjectsActionType: ActionGenerator = name => | ||
`DEEP_UPDATE_${name.toUpperCase()}`; | ||
export const replaceUpdateObjectsActionType: ActionGenerator = name => | ||
`REPLACE_UPDATE_${name.toUpperCase()}`; | ||
export const replaceIdListActionType: ActionGenerator = name => | ||
`REPLACE_${name.toUpperCase()}_ID_LIST`; | ||
|
||
// These functions are designed to be called directly from inside a dispatch, | ||
// and they will return an appropriately shaped action for the given Rest API... action? (load, create, modify) | ||
export const apiActions = { | ||
load: function<CB, MB>( | ||
reg: Registration<CB, MB>, | ||
path?: number, | ||
query?: Query | ||
): LoadRequestAction { | ||
return { | ||
type: loadActionTypes.request(reg.name), | ||
payload: { path, query } | ||
}; | ||
}, | ||
|
||
create: function<CB, MB>( | ||
reg: Registration<CB, MB>, | ||
body?: CB, | ||
query?: Query | ||
): CreateRequestAction { | ||
return { | ||
type: createActionTypes.request(reg.name), | ||
payload: { query, body } | ||
}; | ||
}, | ||
|
||
modify: function<CB, MB>( | ||
reg: Registration<CB, MB>, | ||
path: number, | ||
body?: MB, | ||
query?: Query | ||
): ModifyRequestAction { | ||
return { | ||
type: modifyActionTypes.request(reg.name), | ||
payload: { path, query, body } | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { apiClient } from "../api/client/apiClient"; | ||
import { ApiClientType } from "../api/client/types"; | ||
|
||
export type GetRequest = Pick<ApiClientType, "url" | "query" | "path">; | ||
export type PutRequest = Pick<ApiClientType, "url" | "query" | "path" | "body">; | ||
export type PostRequest = Pick<ApiClientType, "url" | "query" | "body">; | ||
export const genericGetAPI = ({ url, query, path }: GetRequest) => | ||
apiClient({ | ||
method: "GET", | ||
url: `/${url}/`, | ||
query: query, | ||
path: path | ||
}); | ||
export const genericPutAPI = ({ url, query, path, body }: PutRequest) => | ||
apiClient({ | ||
method: "PUT", | ||
url: `/${url}/`, | ||
query: query, | ||
path: path, | ||
body: body | ||
}); | ||
export const genericPostAPI = ({ url, query, body }: PostRequest) => | ||
apiClient({ | ||
method: "POST", | ||
url: `/${url}/`, | ||
query: query, | ||
body: body | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import { | ||
createActionTypes, | ||
loadActionTypes, | ||
modifyActionTypes | ||
} from "./actions"; | ||
import { sagaFactory } from "./sagas"; | ||
import { combineReducers, ReducersMapObject } from "redux"; | ||
import { | ||
byIdReducerFactory, | ||
idListReducerFactory, | ||
lifecycleReducerFactory | ||
} from "./reducers"; | ||
import { | ||
EndpointedRegistration, | ||
RegistrationMapping, | ||
Registration, | ||
EndpointedRegistrationMapping | ||
} from "./types"; | ||
|
||
export { apiActions } from "./actions"; | ||
export { | ||
Registration, | ||
LoadRequestAction, | ||
CreateRequestAction, | ||
ModifyRequestAction | ||
} from "./types"; | ||
export { Body } from "../api/client/types"; | ||
|
||
function hasEndpoint( | ||
reg: Registration | EndpointedRegistration | ||
): reg is EndpointedRegistration { | ||
return (reg as EndpointedRegistration).endpoint !== undefined; | ||
} | ||
|
||
/** | ||
* Creates a set of reducers for each object defined in the registration mapping. | ||
* By default creates a map of each instance of an object indexed by the instance's ID. This map is deep-merged | ||
* whenever a REST API call triggered by _any_ of the registered objects results in updated data for that object | ||
* Additionally if an API endpoint is defined, will also create lifecycle reducers corresponding | ||
* to loading, creating and modifying an object, as well as an ordered ID list that is updated when new data is loaded | ||
* | ||
* @param {RegistrationMapping} registrations - a mapping of each object to be registered | ||
* @returns An array of reducers | ||
*/ | ||
export const createReducers = <R extends RegistrationMapping>( | ||
registrations: R | ||
): ReducersMapObject<R> => { | ||
const base: ReducersMapObject = {}; | ||
|
||
Object.keys(registrations).map(key => { | ||
let reg = { ...registrations[key], name: key }; | ||
|
||
let reducers = { | ||
byId: byIdReducerFactory(reg) | ||
}; | ||
|
||
if (hasEndpoint(reg)) { | ||
reducers = { | ||
...reducers, | ||
...{ | ||
loadStatus: lifecycleReducerFactory(loadActionTypes, reg), | ||
createStatus: lifecycleReducerFactory(createActionTypes, reg), | ||
modifyStatus: lifecycleReducerFactory(modifyActionTypes, reg), | ||
idList: idListReducerFactory(reg) | ||
} | ||
}; | ||
} | ||
|
||
base[reg.name] = combineReducers(reducers); | ||
}); | ||
|
||
return base; | ||
}; | ||
|
||
/** | ||
* Creates REST API sagas for loading, creating and modifying objects. An object must have an endpoint defined for | ||
* a saga to be created | ||
* @param {RegistrationMapping} registrations - a mapping of each object to be registered | ||
* @returns {any[]} | ||
*/ | ||
export const createSagas = (registrations: RegistrationMapping) => { | ||
let sagaList: any[] = []; | ||
|
||
let endpointedRegistrations: EndpointedRegistrationMapping = {}; | ||
|
||
//First ensure all registrations have a name and filter out all non-endpointed registrations | ||
//Done in that order rather than using .filter for type safety | ||
Object.keys(registrations).map(key => { | ||
let reg = { ...registrations[key], name: key }; | ||
if (hasEndpoint(reg)) { | ||
endpointedRegistrations[key] = reg; | ||
} | ||
}); | ||
|
||
Object.keys(endpointedRegistrations).map(key => { | ||
sagaList.push( | ||
sagaFactory(endpointedRegistrations[key], endpointedRegistrations)() | ||
); | ||
}); | ||
|
||
return sagaList; | ||
}; | ||
|
||
/** | ||
* ~~~~~For example~~~~~ | ||
* | ||
* | ||
* interface CreateUserBody { | ||
* first_name: string; | ||
* last_name: string; | ||
* public_serial_number: string; | ||
* // And so on | ||
* } | ||
* | ||
* interface ModifyUserBody { | ||
* //Defaults to the same as create body, but can be different | ||
* first_name: string; | ||
* last_name: string; | ||
* some_other_thing: boolean; | ||
* // And so on | ||
* } | ||
* | ||
* interface SempoObjects extends RegistrationMapping { | ||
* UserExample: Registration<CreateUserBody, ModifyUserBody>; | ||
* } | ||
* | ||
* export const sempoObjects: SempoObjects = { | ||
* UserExample: { | ||
* name: "UserExample", | ||
* endpoint: "user", | ||
* schema: userSchema | ||
* } | ||
* | ||
* let baseReducers = createReducers(sempoObjects); | ||
* let sagalist = createSagas(sempoObjects); | ||
* | ||
* const appReducer = combineReducers({ | ||
* ...baseReducers | ||
* }); | ||
* | ||
* export default function* rootSaga() { | ||
* yield all([ | ||
* generatedSagas() | ||
* ]); | ||
* } | ||
* | ||
* Then for dispatch in component: | ||
* const mapDispatchToProps = (dispatch: any): DispatchProps => { | ||
* return { | ||
* loadUsers: () => dispatch(apiActions.load(sempoObjects.UserExample)) | ||
* } | ||
* } | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { DEEEEEEP } from "../utils"; | ||
import { | ||
APILifecycleActionTypesInterface, | ||
byIdState, | ||
IdListState, | ||
Registration, | ||
RequestingState | ||
} from "./types"; | ||
import { | ||
deepUpdateObjectsActionType, | ||
replaceIdListActionType, | ||
replaceUpdateObjectsActionType | ||
} from "./actions"; | ||
|
||
export const lifecycleReducerFactory = ( | ||
actionType: APILifecycleActionTypesInterface, | ||
reg: Registration | ||
): ((state: RequestingState | undefined, action: any) => RequestingState) => { | ||
const initialLoaderState = { | ||
isRequesting: false, | ||
success: false, | ||
error: null | ||
}; | ||
return ( | ||
state: RequestingState | undefined = initialLoaderState, | ||
action: any | ||
): RequestingState => { | ||
switch (action.type) { | ||
case actionType.request(reg.name): | ||
return { ...state, isRequesting: true }; | ||
|
||
case actionType.success(reg.name): | ||
return { ...state, isRequesting: false, success: true }; | ||
|
||
case actionType.failure(reg.name): | ||
return { | ||
...state, | ||
isRequesting: false, | ||
success: false, | ||
error: action.error | ||
}; | ||
|
||
default: | ||
return state; | ||
} | ||
}; | ||
}; | ||
|
||
export const byIdReducerFactory = ( | ||
reg: Registration | ||
): ((state: byIdState | undefined, action: any) => byIdState) => { | ||
return (state: byIdState | undefined = {}, action: any): byIdState => { | ||
switch (action.type) { | ||
case deepUpdateObjectsActionType(reg.name): | ||
return DEEEEEEP(state, action.objects); | ||
|
||
case replaceUpdateObjectsActionType(reg.name): | ||
return action.objects; | ||
|
||
default: | ||
return state; | ||
} | ||
}; | ||
}; | ||
|
||
export const idListReducerFactory = ( | ||
reg: Registration | ||
): ((state: IdListState | undefined, action: any) => IdListState) => { | ||
return (state: IdListState | undefined = [], action: any): IdListState => { | ||
switch (action.type) { | ||
case replaceIdListActionType(reg.name): | ||
return (state = action.idList); | ||
default: | ||
return state; | ||
} | ||
}; | ||
}; |
Oops, something went wrong.