Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit baa678f

Browse files
feat(state): introduce rxState asReadOnly API
`RxState.asReadOnly` allows to only expose the readable parts of a given `RxState` instance. This enables more fine grained access control to the state you are exposing. * feat: rx state as read only * feat: review fixes * feat: review fix, building docs fix * feat: review fix, building docs fix
1 parent 189e662 commit baa678f

File tree

3 files changed

+139
-3
lines changed

3 files changed

+139
-3
lines changed

apps/docs/docs/state/api/rx-state.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,33 @@ Use the `$` property if you want to read the state without having applied [state
6565

6666
---
6767

68+
## asReadOnly
69+
70+
##### typeof: Pick<RxState<State>, 'get' | 'select' | 'computed' | 'signal'>
71+
72+
Return RxState in ReadOnly mode that is exposing
73+
get(), select(), computed() and signal() methods.
74+
This can be helpful when you don't want others to write in your state.
75+
76+
```typescript
77+
const readOnlyState = state.asReadOnly();
78+
const getNum = readOnlyState.get('num');
79+
const selectNum$ = readOnlyState.select('num');
80+
```
81+
82+
Trying to call any method that is not exposed in readOnlyState will throw an appropriate error
83+
84+
```typescript
85+
const readOnlyState = state.asReadOnly();
86+
readOnlyState['set']('num', (state) => state.num + 1);
87+
```
88+
89+
```language: none
90+
throwing -> readOnlyState.set is not a function
91+
```
92+
93+
---
94+
6895
## connect
6996

7097
### Signature

libs/state/spec/rx-state.service.spec.ts

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@ import {
88
PrimitiveState,
99
} from '@test-helpers/rx-angular';
1010
import { of, scheduled, Subject } from 'rxjs';
11+
import { ColdObservable } from 'rxjs/internal/testing/ColdObservable';
1112
import { map, switchMap, take, takeUntil } from 'rxjs/operators';
1213
import { TestScheduler } from 'rxjs/testing';
1314
import { RxState } from '../src/lib/rx-state.service';
15+
import { ReadOnly } from '../src/lib/rx-state.service';
1416
import { createStateChecker } from './fixtures';
1517

18+
type ReadOnlyPrimitiveState = Pick<RxState<PrimitiveState>, ReadOnly>;
19+
1620
function setupState<T extends object>(cfg: { initialState?: T } = {}) {
1721
const { initialState } = { ...cfg };
1822
const state = TestBed.inject(RxState);
@@ -139,25 +143,33 @@ describe('RxStateService', () => {
139143
it('should return undefined as initial value', () => {
140144
const state = setupState({ initialState: undefined });
141145
const val = state.get();
146+
const readOnlyVal = state.asReadOnly().get();
142147
expect(val).toEqual(undefined);
148+
expect(readOnlyVal).toEqual(undefined);
143149
});
144150

145151
it('should return undefined for an undefined property', () => {
146152
const state = setupState<{ num: number }>({ initialState: undefined });
147153
const val = state.get('num');
154+
const readOnlyVal = state.asReadOnly().get('num');
148155
expect(val).toEqual(undefined);
156+
expect(readOnlyVal).toEqual(undefined);
149157
});
150158

151159
it('should return value when keys are provided as params', () => {
152160
const state = setupState({ initialState: initialPrimitiveState });
153161
const val = state.get('num');
162+
const readOnlyVal = state.asReadOnly().get('num');
154163
expect(val).toEqual(initialPrimitiveState.num);
164+
expect(readOnlyVal).toEqual(initialPrimitiveState.num);
155165
});
156166

157167
it('should return whole state object when no keys provided', () => {
158168
const state = setupState({ initialState: initialPrimitiveState });
159169
const val = state.get();
170+
const readOnlyVal = state.asReadOnly().get();
160171
expect(val.num).toEqual(initialPrimitiveState.num);
172+
expect(readOnlyVal.num).toEqual(initialPrimitiveState.num);
161173
});
162174
});
163175

@@ -166,6 +178,7 @@ describe('RxStateService', () => {
166178
testScheduler.run(({ expectObservable }) => {
167179
const state = setupState({ initialState: undefined });
168180
expectObservable(state.select()).toBe('-');
181+
expectObservable(state.asReadOnly().select()).toBe('-');
169182
});
170183
});
171184

@@ -175,14 +188,18 @@ describe('RxStateService', () => {
175188
expectObservable(state.select()).toBe('s', {
176189
s: initialPrimitiveState,
177190
});
191+
expectObservable(state.asReadOnly().select()).toBe('s', {
192+
s: initialPrimitiveState,
193+
});
178194
});
179195
});
180196

181197
it('should throw with wrong params', () => {
182198
const state = setupState({ initialState: initialPrimitiveState });
183-
184-
expect(() => state.select(true as any)).toThrowError(
185-
'wrong params passed to select'
199+
const errorMessage = 'wrong params passed to select';
200+
expect(() => state.select(true as any)).toThrowError(errorMessage);
201+
expect(() => state.asReadOnly().select(true as any)).toThrowError(
202+
errorMessage
186203
);
187204
});
188205

@@ -191,6 +208,7 @@ describe('RxStateService', () => {
191208
testScheduler.run(({ expectObservable }) => {
192209
const state = setupState({});
193210
expectObservable(state.select()).toBe('');
211+
expectObservable(state.asReadOnly().select()).toBe('');
194212
});
195213
});
196214

@@ -202,6 +220,9 @@ describe('RxStateService', () => {
202220

203221
state.set({ num: 42 });
204222
expectObservable(state.select('num')).toBe('s', { s: 42 });
223+
expectObservable(state.asReadOnly().select('num')).toBe('s', {
224+
s: 42,
225+
});
205226
});
206227
});
207228
});
@@ -356,6 +377,16 @@ describe('RxStateService', () => {
356377
state.select().subscribe((s) => expect(s).toBe({ num: 43 }));
357378
});
358379
});
380+
describe('with read only state', () => {
381+
it('should throw error when trying to call set from readOnlyState', () => {
382+
const readOnlyState: ReadOnlyPrimitiveState = setupState({
383+
initialState: initialPrimitiveState,
384+
}).asReadOnly();
385+
expect((): void => {
386+
readOnlyState['set']('num', (state: PrimitiveState) => state.num + 1);
387+
}).toThrowError('readOnlyState.set is not a function');
388+
});
389+
});
359390
});
360391

361392
describe('connect', () => {
@@ -580,6 +611,24 @@ describe('RxStateService', () => {
580611
state.ngOnDestroy();
581612
});
582613
});
614+
615+
it('should throw error when trying to call connect from readOnlyState', () => {
616+
testScheduler.run(() => {
617+
const s: { num: number | undefined } = { num: 0 };
618+
const readOnlyState: ReadOnlyPrimitiveState = setupState({
619+
initialState: s,
620+
}).asReadOnly();
621+
expect((): void => {
622+
readOnlyState['connect'](
623+
scheduled(
624+
[{ num: undefined }, { num: 43 }, { num: undefined }],
625+
testScheduler
626+
),
627+
(o, n) => n
628+
);
629+
}).toThrowError('readOnlyState.connect is not a function');
630+
});
631+
});
583632
});
584633

585634
describe('setAccumulator', () => {
@@ -640,6 +689,22 @@ describe('RxStateService', () => {
640689
expect(numAcc1Calls).toBe(1);
641690
expect(numAcc2Calls).toBe(1);
642691
});
692+
it('should throw error when trying to call setAccumulator from readOnlyState', () => {
693+
let numAccCalls = 0;
694+
const customAcc = <T>(s: T, sl: Partial<T>) => {
695+
++numAccCalls;
696+
return {
697+
...s,
698+
...sl,
699+
};
700+
};
701+
const readOnlyState: ReadOnlyPrimitiveState = setupState({
702+
initialState: initialPrimitiveState,
703+
}).asReadOnly();
704+
expect((): void => {
705+
readOnlyState['setAccumulator'](customAcc);
706+
}).toThrowError('readOnlyState.setAccumulator is not a function');
707+
});
643708
});
644709

645710
describe('hold', () => {
@@ -665,5 +730,22 @@ describe('RxStateService', () => {
665730
state.hold(of(1, 2, 3), effect);
666731
expect(calls).toBe(3);
667732
}));
733+
734+
it('should throw error when trying to call hold from readOnlyState', () => {
735+
testScheduler.run(({ cold, expectSubscriptions }) => {
736+
const readOnlyState: ReadOnlyPrimitiveState = setupState({
737+
initialState: initialPrimitiveState,
738+
}).asReadOnly();
739+
const test$: ColdObservable<number> = cold('(abc)', {
740+
a: 1,
741+
b: 2,
742+
c: 3,
743+
});
744+
const stop: Subject<void> = new Subject();
745+
expect((): void => {
746+
readOnlyState['hold'](test$.pipe(takeUntil(stop)));
747+
}).toThrowError('readOnlyState.hold is not a function');
748+
});
749+
});
668750
});
669751
});

libs/state/src/lib/rx-state.service.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export type ProjectValueReducer<Type, Key extends keyof Type, Value> = (
4646
value: Value
4747
) => Type[Key];
4848

49+
export type ReadOnly = 'get' | 'select' | 'computed' | 'signal';
50+
4951
/**
5052
* @description
5153
* RxState is a light-weight reactive state management service for managing local state in angular.
@@ -99,6 +101,31 @@ export class RxState<State extends object>
99101
this.subscription.unsubscribe();
100102
}
101103

104+
/**
105+
* @description
106+
*
107+
* Return RxState in ReadOnly mode exposing only methods for reading state
108+
* get(), select(), computed() and signal() methods.
109+
* This can be helpful when you don't want others to write in your state.
110+
*
111+
* @example
112+
* ```typescript
113+
* const readOnlyState = state.asReadOnly();
114+
* const getNum = state.get('num');
115+
* const selectNum$ = state.select('num');
116+
* ```
117+
*
118+
* @return Pick<RxState<State>, ReadOnly>
119+
*/
120+
asReadOnly(): Pick<RxState<State>, ReadOnly> {
121+
return {
122+
get: this.get.bind(this),
123+
select: this.select.bind(this),
124+
computed: this.computed.bind(this),
125+
signal: this.signal.bind(this),
126+
};
127+
}
128+
102129
/**
103130
* @description
104131
*

0 commit comments

Comments
 (0)