301 lines
6.6 KiB
TypeScript
301 lines
6.6 KiB
TypeScript
import { indexBy, without } from '@rondo.dev/common'
|
|
import { CRUDAction } from './CRUDAction'
|
|
import { CRUDMethod } from './CRUDMethod'
|
|
|
|
export interface CRUDEntity {
|
|
readonly id: number
|
|
}
|
|
|
|
export interface CRUDMethodStatus {
|
|
readonly isLoading: boolean
|
|
readonly error: string
|
|
}
|
|
|
|
export interface CRUDState<T extends CRUDEntity> {
|
|
readonly ids: ReadonlyArray<number>
|
|
readonly byId: Record<number, T>
|
|
readonly status: CRUDStatus
|
|
readonly form: CRUDFormState<T>
|
|
}
|
|
|
|
export interface CRUDFormState<T extends CRUDEntity> {
|
|
readonly createItem: Pick<T, Exclude<keyof T, 'id'>>
|
|
readonly createErrors: Partial<Record<keyof T, string>>
|
|
|
|
readonly itemsById: Record<number, T>
|
|
readonly errorsById: Record<number, Partial<Record<keyof T, string>>>
|
|
}
|
|
|
|
export interface CRUDStatus {
|
|
readonly save: CRUDMethodStatus
|
|
readonly update: CRUDMethodStatus
|
|
readonly remove: CRUDMethodStatus
|
|
readonly findOne: CRUDMethodStatus
|
|
readonly findMany: CRUDMethodStatus
|
|
}
|
|
|
|
export class CRUDReducer<
|
|
T extends CRUDEntity,
|
|
ActionType extends string,
|
|
> {
|
|
readonly defaultState: CRUDState<T>
|
|
|
|
constructor(
|
|
readonly actionName: ActionType,
|
|
readonly newItem: Pick<T, Exclude<keyof T, 'id'>>,
|
|
) {
|
|
|
|
const defaultMethodStatus = this.getDefaultMethodStatus()
|
|
this.defaultState = {
|
|
ids: [],
|
|
byId: {},
|
|
form: {
|
|
itemsById: {},
|
|
errorsById: {},
|
|
createItem: newItem,
|
|
createErrors: {},
|
|
},
|
|
|
|
status: {
|
|
save: defaultMethodStatus,
|
|
update: defaultMethodStatus,
|
|
remove: defaultMethodStatus,
|
|
findOne: defaultMethodStatus,
|
|
findMany: defaultMethodStatus,
|
|
},
|
|
}
|
|
}
|
|
|
|
getDefaultMethodStatus(): CRUDMethodStatus {
|
|
return {
|
|
error: '',
|
|
isLoading: false,
|
|
}
|
|
}
|
|
|
|
protected getSuccessStatus(): CRUDMethodStatus {
|
|
return {
|
|
isLoading: false,
|
|
error: '',
|
|
}
|
|
}
|
|
|
|
handleRejected = (
|
|
state: CRUDState<T>,
|
|
method: CRUDMethod,
|
|
error: Error,
|
|
): CRUDState<T> => {
|
|
return {
|
|
...state,
|
|
status: {
|
|
...state.status,
|
|
[method]: {
|
|
isLoading: false,
|
|
error: error.message,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
handleLoading = (
|
|
state: CRUDState<T>,
|
|
method: CRUDMethod,
|
|
): CRUDState<T> => {
|
|
return {
|
|
...state,
|
|
status: {
|
|
...state.status,
|
|
[method]: {
|
|
isLoading: true,
|
|
error: '',
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
handleFindOne = (state: CRUDState<T>, payload: T): CRUDState<T> => {
|
|
const ids = !state.byId[payload.id]
|
|
? [...state.ids, payload.id]
|
|
: state.ids
|
|
return {
|
|
...state,
|
|
ids,
|
|
byId: {
|
|
...state.byId,
|
|
[payload.id]: payload,
|
|
},
|
|
status: {
|
|
...state.status,
|
|
findOne: this.getSuccessStatus(),
|
|
},
|
|
}
|
|
}
|
|
|
|
handleSave = (state: CRUDState<T>, payload: T): CRUDState<T> => {
|
|
return {
|
|
...state,
|
|
ids: [...state.ids, payload.id],
|
|
byId: {
|
|
...state.byId,
|
|
[payload.id]: payload,
|
|
},
|
|
status: {
|
|
...state.status,
|
|
save: this.getSuccessStatus(),
|
|
},
|
|
}
|
|
}
|
|
|
|
handleUpdate = (state: CRUDState<T>, payload: T): CRUDState<T> => {
|
|
return {
|
|
...state,
|
|
byId: {
|
|
[payload.id]: payload,
|
|
},
|
|
status: {
|
|
...state.status,
|
|
update: this.getSuccessStatus(),
|
|
},
|
|
}
|
|
}
|
|
|
|
handleRemove = (state: CRUDState<T>, payload: T): CRUDState<T> => {
|
|
// FIXME site does not get removed because payload looks different!
|
|
return {
|
|
...state,
|
|
ids: state.ids.filter(id => id !== payload.id),
|
|
byId: without(state.byId, payload.id),
|
|
status: {
|
|
...state.status,
|
|
remove: this.getSuccessStatus(),
|
|
},
|
|
}
|
|
}
|
|
|
|
handleFindMany = (state: CRUDState<T>, payload: T[]): CRUDState<T> => {
|
|
return {
|
|
...state,
|
|
ids: payload.map(item => item.id),
|
|
byId: indexBy(payload, 'id' as any), // eslint-disable-line
|
|
status: {
|
|
...state.status,
|
|
findMany: this.getSuccessStatus(),
|
|
},
|
|
}
|
|
}
|
|
|
|
handleCreate = (state: CRUDState<T>, payload: Partial<T>): CRUDState<T> => {
|
|
return {
|
|
...state,
|
|
form: {
|
|
...state.form,
|
|
createItem: {
|
|
...this.newItem,
|
|
...payload,
|
|
},
|
|
createErrors: {},
|
|
},
|
|
}
|
|
}
|
|
|
|
handleEdit = (state: CRUDState<T>, id: number): CRUDState<T> => {
|
|
return {
|
|
...state,
|
|
form: {
|
|
...state.form,
|
|
itemsById: {
|
|
...state.form.itemsById,
|
|
[id]: state.byId[id],
|
|
},
|
|
errorsById: {
|
|
...state.form.errorsById,
|
|
[id]: {},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
handleChange = (state: CRUDState<T>, payload: {
|
|
id?: number
|
|
key: keyof T
|
|
value: string
|
|
}): CRUDState<T> => {
|
|
const {id, key, value} = payload
|
|
|
|
if (!id) {
|
|
return {
|
|
...state,
|
|
form: {
|
|
...state.form,
|
|
createItem: {
|
|
...state.form.createItem,
|
|
[key]: value,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
form: {
|
|
...state.form,
|
|
itemsById: {
|
|
...state.form.itemsById,
|
|
[id]: {
|
|
...state.form.itemsById[id],
|
|
[key]: value,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
reduce = (
|
|
state: CRUDState<T> | undefined,
|
|
action: CRUDAction<T, ActionType>,
|
|
): CRUDState<T> => {
|
|
const {defaultState} = this
|
|
state = state || defaultState
|
|
|
|
if (action.type !== this.actionName) {
|
|
return state
|
|
}
|
|
|
|
if (!('status' in action)) {
|
|
switch (action.method) {
|
|
case 'change':
|
|
return this.handleChange(state, action.payload)
|
|
case 'edit':
|
|
return this.handleEdit(state, action.payload.id)
|
|
case 'create':
|
|
return this.handleCreate(state, action.payload)
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
switch (action.status) {
|
|
case 'pending':
|
|
return this.handleLoading(state, action.method)
|
|
case 'rejected':
|
|
return this.handleRejected(state, action.method, action.payload)
|
|
case 'resolved':
|
|
switch (action.method) {
|
|
case 'save':
|
|
return this.handleSave(state, action.payload)
|
|
case 'update':
|
|
return this.handleUpdate(state, action.payload)
|
|
case 'remove':
|
|
return this.handleRemove(state, action.payload)
|
|
case 'findOne':
|
|
return this.handleFindOne(state, action.payload)
|
|
case 'findMany':
|
|
return this.handleFindMany(state, action.payload)
|
|
}
|
|
return state
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
}
|