Skip to content

Commit 02d1909

Browse files
committed
feat(store): allow auto action generation per state using a service
1 parent dc541db commit 02d1909

File tree

7 files changed

+65
-56
lines changed

7 files changed

+65
-56
lines changed

demo/src/app/app.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class AppComponent implements OnInit {
3131
});
3232
break;
3333
case 'log':
34-
this.store.dispatch('App', 'APP_LOG', { age: 123 }).subscribe((data) => {
34+
this.store.dispatch('App', 'APP_LOG', { name: 'John Smith', age: 37 }).subscribe((data) => {
3535
this.logs.unshift({ action: 'log', type: 'subscribe', data });
3636
});
3737
break;

demo/src/app/app.service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { HttpClient } from '@angular/common/http';
2-
import { Injectable } from '@angular/core';
2+
import { inject, Injectable } from '@angular/core';
33

44
export type LogObject = { name: string, age: number };
55
export type DispatchObject = {
@@ -14,7 +14,7 @@ export type DispatchObject = {
1414

1515
@Injectable({ providedIn: 'root' })
1616
export class AppService {
17-
constructor(private http: HttpClient) { }
17+
http = inject(HttpClient);
1818

1919
appLog(args: LogObject) {
2020
console.log('###', 'appLog', args);

demo/src/app/app.store.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,8 @@ export const AppStoreStates = [
3434
name: 'App',
3535
fallback: ['Shared'],
3636
initial: <AppState>{},
37+
service: AppService,
3738
actions: [
38-
new StoreAction({
39-
name: 'APP_LOG',
40-
service: AppService,
41-
method: 'appLog',
42-
}),
43-
new StoreAction({
44-
name: 'APP_DISPATCH',
45-
service: AppService,
46-
method: 'appDispatch',
47-
}),
4839
new StoreAction({
4940
name: 'APP_DEPRECATED',
5041
deprecated: true,

src/models/store.action.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import { ActionStatus, DefaultActions } from "../shared/store.enums";
2-
import { ActionMethod, ActionPayload, ActionService, RequireOnly } from "../shared/store.types";
2+
import { MethodArguments, RequireOnly, Service, ServiceClass, ServiceMethod } from "../shared/store.types";
33
import { UpdateFlag } from "./store.options";
44

55

66

7-
type StoreActionInput<K extends string, S extends ActionService, M extends ActionMethod<S>, D extends boolean = boolean, F extends string = string> =
7+
type StoreActionInput<K extends string, S extends Service, M extends ServiceMethod<S>, D extends boolean = boolean, F extends string = string> =
88
| RequireOnly<StoreAction<K, S, M, D, F>, 'name' | 'fallback'>
99
| RequireOnly<StoreAction<K, S, M, D, F>, 'name' | 'service' | 'method'>
1010
| Partial<StoreAction<K, S, M, D, F>>;
1111

1212

13-
export class StoreAction<K extends string = string, S extends ActionService = ActionService, M extends ActionMethod<S> = ActionMethod<S>, D extends boolean = boolean, F extends string = string> {
13+
export class StoreAction<K extends string = string, S extends Service = Service, M extends ServiceMethod<S> = ServiceMethod<S>, D extends boolean = boolean, F extends string = string> {
1414
name!: K;
1515
state?: string;
1616

17-
service!: new (...args: any[]) => S;
17+
service!: ServiceClass<S>;
1818
method!: M;
1919
fallback?: F[];
20-
payload?: ActionPayload<S, M>;
20+
payload?: MethodArguments<S, M>;
2121
uuid?: string;
2222
status?: ActionStatus = undefined as any;
2323

src/models/store.state.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Provider } from "@angular/core";
22
import { DefaultActions } from "../shared/store.enums";
33
import { provideStoreStates } from "../shared/store.providers";
4+
import { Service, ServiceClass } from "../shared/store.types";
45
import { MergeReducer } from "../variations/merge.reducer";
56
import { StorageReducer } from "../variations/storage.reducer";
67
import { StoreAction } from "./store.action";
@@ -9,27 +10,47 @@ import { StoreReducer } from "./store.reducer";
910

1011
const DefaultReducers = [StoreReducer, MergeReducer, StorageReducer];
1112

12-
export class StoreState<T extends any = any, A extends any[] = any[], K extends string = string, F extends string = string> {
13+
export class StoreState<M extends any = any, N extends string = string, S extends Service = Service, A extends any[] = any[], F extends string = string> {
1314
app?: string;
14-
name: K;
15-
initial: T;
15+
name: N;
16+
initial: M;
17+
service: ServiceClass<S>;
1618
actions: A;
1719
fallback: F[] = [];
1820
options: StoreOptions;
1921
reducers: StoreReducer[] = [];
2022

21-
constructor(state?: Partial<StoreState<T, A, K, F>>, autoExtend = true) {
23+
constructor(state?: Partial<StoreState<M, N, S, A, F>>, autoExtend = true) {
2224
this.app = state?.app;
2325
this.name = !state || typeof (state) === 'string' ? state as any : state.name;
24-
this.initial = state?.initial || {} as T;
26+
this.initial = state?.initial || {} as M;
27+
this.service = state?.service as ServiceClass<S>;
2528
this.fallback = state?.fallback || [] as F[];
2629
this.options = state?.options || {};
2730
this.reducers = state?.reducers || [];
2831
this.actions = state?.actions as A || [];
2932

33+
if (this.service) {
34+
Object.getOwnPropertyNames(this.service.prototype)
35+
.forEach(key => {
36+
const isFunction = key !== 'constructor' && typeof this.service.prototype[key] === 'function';
37+
const name = key.split(/(?=[A-Z])/).join('_').toUpperCase();
38+
const isNew = !this.actions.find(x => x.name == name);
39+
if (isFunction && isNew) {
40+
const action = new StoreAction({
41+
name,
42+
service: this.service,
43+
method: key as any
44+
}, this.name);
45+
this.actions.push(action);
46+
}
47+
});
48+
}
49+
50+
3051
Object.keys(DefaultActions).forEach(action => {
3152
if (!this.actions.find(x => x.name == action)) {
32-
this.actions.push(new StoreAction(action as any, this.name));
53+
this.actions.push(new StoreAction(action as any, this.name) as any);
3354
}
3455
});
3556

@@ -41,30 +62,20 @@ export class StoreState<T extends any = any, A extends any[] = any[], K extends
4162

4263
}
4364

44-
update(state: any, action: StoreAction): T {
45-
this.reducers?.forEach(reducer => {
46-
state = reducer.prePopulate(this as any, state, action as StoreAction);
47-
});
48-
this.reducers?.forEach(reducer => {
49-
state = reducer.onPopulate(this as any, state, action as StoreAction);
50-
});
51-
this.reducers?.forEach(reducer => {
52-
state = reducer.postPopulate(this as any, state, action as StoreAction);
53-
});
54-
return state;
65+
update(state: any, action: StoreAction): M {
66+
this.reducers?.forEach(reducer => {
67+
state = reducer.prePopulate(this as any, state, action as StoreAction);
68+
});
69+
this.reducers?.forEach(reducer => {
70+
state = reducer.onPopulate(this as any, state, action as StoreAction);
71+
});
72+
this.reducers?.forEach(reducer => {
73+
state = reducer.postPopulate(this as any, state, action as StoreAction);
74+
});
75+
return state;
5576
}
5677

5778
provideState(): Provider[] {
5879
return provideStoreStates([this], { app: this.app })
5980
}
60-
61-
private addAction() {
62-
63-
}
64-
private addReducer(reducer: any) {
65-
if (!this.reducers.find(x => x instanceof reducer)) {
66-
this.reducers.push(new reducer());
67-
}
68-
69-
}
7081
}

src/services/store.facade.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { exhaustMap, Observable, of } from 'rxjs';
33
import { UpdateFlag } from '../models/store.options';
44
import { StoreState } from '../models/store.state';
55
import { DefaultActions } from '../shared/store.enums';
6-
import { DeepPartial, DispatchPayload, DispatchResponse, State, StateActionNames, StateData, StateFormatter, StateKey } from '../shared/store.types';
6+
import { ActionKey, DeepPartial, DispatchArguments, DispatchPayload, DispatchResponse, State, StateAction, StateActionNames, StateActionPayload, StateData, StateFormatter, StateKey } from '../shared/store.types';
77
import { isEmpty } from '../shared/store.utils';
88
import { StoreDispatcher } from './store.dispatcher';
99
import { StoreManager } from './store.manager';
@@ -36,7 +36,7 @@ export class StoreFacade<States extends StoreState[], Keys extends string = Stat
3636
}
3737
}
3838

39-
dispatch<K extends Keys, S extends StoreState = State<States, K>, N extends string = StateActionNames<S>, P = DispatchPayload<S, N>>(stateKey: K, actionKey: N, payload?: P, flag?: UpdateFlag): Observable<DispatchResponse<S, N>> {
39+
dispatch<K extends Keys, S extends StoreState = State<States, K>, N extends string = StateActionNames<S>>(stateKey: K, actionKey: ActionKey<S, N>, ...[payload, flag]: DispatchArguments<S,N>): Observable<DispatchResponse<S, N>> {
4040
const action = this.dispatcher.dispatch(stateKey, actionKey, payload, flag);
4141
return this.manager.observable(stateKey, action);
4242
}

src/shared/store.types.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,37 @@
11
import { Observable } from "rxjs";
22
import { StoreAction } from "../models/store.action";
3+
import { UpdateFlag } from "../models/store.options";
34
import { StoreState } from "../models/store.state";
45
import { DefaultActions } from "./store.enums";
56

67
export type StateKey<S extends StoreState[]> = S[number]['name'];
78
export type State<S extends StoreState[], K extends StateKey<S>> = Extract<S[number], { name: K; }>;
89
export type StateData<S extends StoreState> = S['initial'];
9-
export type StateActions<S extends StoreState> = S['actions'];
10-
export type StateActionNames<S extends StoreState> = S['actions'][number]['name'] | keyof typeof DefaultActions;
10+
export type StateActions<S extends StoreState, SV = InstanceType<S['service']>> = SV extends never ? S['actions'] : ServiceActions<SV> | S['actions'];
11+
export type StateActionNames<S extends StoreState> = StateActions<S>[number]['name'];
12+
export type ActionKey<S extends StoreState, N extends string> = N extends (StateActionNames<S> | DefaultActions) ? N : never;
1113
export type StateAction<S extends StoreState, N extends StateActionNames<S>> = Extract<StateActions<S>[number], { name: N }>;
1214

13-
export type StateActionPayload<A extends StoreAction> = A['payload'];
14-
export type StateActionReturn<A extends StoreAction> = ActionResponse<InstanceType<A['service']>, A['method']>;
15+
export type StateActionPayload<A extends StoreAction> = MethodArguments<InstanceType<A['service']>, A['method']>;
16+
export type StateActionReturn<A extends StoreAction> = MethodReturnType<InstanceType<A['service']>, A['method']>;
1517
export type DeprecatedActions<S extends StoreState> = Extract<StateActions<S>[number], { deprecated: true; }>['name'];
1618
export type ActiveActions<S extends StoreState> = Exclude<StateActions<S>[number]['name'], DeprecatedActions<S>>;
1719

18-
export type DispatchPayload<S extends StoreState, N extends StateActionNames<S>, A extends StoreAction = StateAction<S, N>> = StateActionPayload<A> extends undefined ? DeepPartial<StateData<S>> | string | number | boolean : DeepPartial<StateActionPayload<A>>;
20+
export type DispatchArguments<S extends StoreState, N extends StateActionNames<S>, A extends StoreAction = StateAction<S, N>> = StateActionPayload<A> extends undefined ? [DispatchPayload<S, N>?, UpdateFlag?] : [DispatchPayload<S, N>, UpdateFlag?];
21+
export type DispatchPayload<S extends StoreState, N extends StateActionNames<S>, A extends StoreAction = StateAction<S, N>> = StateActionPayload<A> extends undefined ? DeepPartial<StateData<S>> | string | number | boolean : StateActionPayload<A>;
1922
export type DispatchResponse<S extends StoreState, N extends StateActionNames<S>, A extends StoreAction = StateAction<S, N>, R = StateActionReturn<A>> = N extends keyof typeof DefaultActions ? StateData<S> : `Object` extends R ? StateData<S> : StateData<S> & R;
2023

2124
export type StateFormatter<S extends StoreState> = (payload: StateData<S>) => StateData<S>;
2225

2326

24-
export type ActionService = any;
25-
export type ActionMethod<S extends ActionService> = keyof S;
26-
export type ActionPayload<S extends ActionService, M extends keyof S> = S[M] extends (...args: any[]) => any ? Parameters<S[M]>[0] : never;
27-
export type ActionResponse<S extends ActionService, M extends keyof S> = S[M] extends (...args: any[]) => any ? (ReturnType<S[M]> extends Observable<infer R> ? R : ReturnType<S[M]>) : never;
27+
export type Service = unknown;
28+
export type ServiceClass<S extends Service> = new (...args: any[]) => S;
29+
export type ServiceMethod<S extends Service> = { [K in keyof S]: S[K] extends (...args: any[]) => any ? K : never; }[keyof S];
30+
export type ServiceActions<S extends Service> = { [K in ServiceMethod<S>]: StoreAction<CamelToSnakeCase<string & K>, S, string & K> }[ServiceMethod<S>][];
31+
export type MethodArguments<S extends Service, M extends keyof S> = S[M] extends (...args: any[]) => any ? Parameters<S[M]>[0] : never;
32+
export type MethodReturnType<S extends Service, M extends keyof S> = S[M] extends (...args: any[]) => any ? (ReturnType<S[M]> extends Observable<infer R> ? R : ReturnType<S[M]>) : never;
2833

2934
export type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; };
3035
export type RequireOnly<T, R extends keyof T> = Required<Pick<T, R>> & Partial<Omit<T, R>>;
36+
37+
export type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}` ? `${T extends Capitalize<T> ? "_" : ""}${Uppercase<T>}${CamelToSnakeCase<U>}` : S;

0 commit comments

Comments
 (0)