Skip to content

Commit c6d1c1e

Browse files
author
Gérard Collin
committed
fix: working signalstore sort & filter
1 parent 4bcf3e6 commit c6d1c1e

2 files changed

Lines changed: 151 additions & 23 deletions

File tree

libs/xt-store/projects/store/src/store-entity/store-entity-feature.spec.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { withXtStoreProvider, XtSignalStore } from './store-entity-feature';
88
import { XtMemoryStoreProvider } from '../store-provider/xt-memory-store-provider';
99
import { ManagedData, xtTypeManager } from 'xt-type';
1010
import { StoreTestBed } from '../test/store-test-bed';
11-
import { XtStoreCriteria } from '../xt-store-parameters';
11+
import { XtSortByDirection, XtStoreCriteria } from '../xt-store-parameters';
1212
import { firstValueFrom } from 'rxjs';
1313

1414
describe('StoreEntityFeature', () => {
@@ -153,6 +153,116 @@ describe('StoreEntityFeature', () => {
153153
expect(loadedProvenanceBook.author._id).toEqual(annLeckieAuthor._id);
154154
});
155155

156+
it('should support sorted & filtered store', async () => {
157+
StoreTestBed.ensureMemoryProviderOnly();
158+
const typeMgr = xtTypeManager();
159+
typeMgr.addRootType('AuthorType2', {
160+
name: 'string',
161+
birthDate: 'date',
162+
birthCity: 'string'
163+
});
164+
typeMgr.addRootType('BookType2', {
165+
children: {
166+
title: 'string',
167+
author: {
168+
toType: 'AuthorType2',
169+
field: 'name',
170+
referenceType: 'MANY-TO-ONE'
171+
},
172+
genre: 'string',
173+
cost: 'number'
174+
}
175+
});
176+
177+
const storeMgr = xtStoreManager();
178+
179+
const philipKDickAuthor = await storeMgr.getProviderSafe<AuthorType>('AuthorType2').storeEntity('AuthorType2', {
180+
name: 'Philip K. Dick',
181+
birthCity: 'Chicago',
182+
birthDate: new Date(1928, 12, 16)
183+
});
184+
185+
const annLeckieAuthor = await storeMgr.getProviderSafe<AuthorType>('AuthorType2').storeEntity('AuthorType2', {
186+
name: 'Ann Leckie',
187+
birthCity: 'Toledo',
188+
birthDate: new Date(1966, 3, 2)
189+
190+
});
191+
192+
await storeMgr.getProviderSafe<StoredBookType2>('BookType2').storeEntity('BookType2', {
193+
title: 'Ubik',
194+
author: 'Philip K. Dick',
195+
genre: 'SF',
196+
cost:50
197+
});
198+
199+
await storeMgr.getProviderSafe<StoredBookType2>('BookType2').storeEntity('BookType2', {
200+
title: 'Ancillaire',
201+
author: 'Ann Leckie',
202+
genre: 'Space Opera',
203+
cost:70
204+
});
205+
206+
await storeMgr.getProviderSafe<StoredBookType2>('BookType2').storeEntity('BookType2', {
207+
title: 'Sheep',
208+
author: 'Philip K. Dick',
209+
genre: 'SF',
210+
cost:40
211+
});
212+
213+
// Let's try first the simple sort / filter
214+
const storeType = signalStore(withXtStoreProvider<BookType2>("BookType2", undefined, storeMgr, typeMgr, {
215+
sort: [{
216+
by: 'title',
217+
direction:XtSortByDirection.Ascending
218+
} ],
219+
filter: [new XtStoreCriteria('genre', 'SF', '=')
220+
]
221+
}));
222+
const store = new storeType() as unknown as XtSignalStore<BookType2>;
223+
await store.fetchEntities();
224+
let result = store.entities();
225+
expect(result.length).toEqual(2);
226+
expect(result[0].title).toEqual('Sheep');
227+
228+
// Now adds an item, and ensure it is put at the right place, and genre is automatically filled
229+
let added = await store.storeEntity({
230+
title: 'Tad Book',
231+
author: annLeckieAuthor,
232+
cost: 40
233+
} as BookType2);
234+
expect(added.genre).toBe('SF');
235+
result = store.entities();
236+
expect(result.length).toEqual(3);
237+
expect(result[1].title).toEqual(added.title);
238+
239+
let error=false;
240+
// Storing an entity that doesn't match the filter should not update the list
241+
const notAdded = await store.storeEntity({
242+
title: 'Tad Book',
243+
author: annLeckieAuthor,
244+
cost: 40,
245+
genre:'NotGood'
246+
});
247+
result = store.entities();
248+
expect(result).toHaveLength(3);
249+
250+
// Updating an element should re-trigger sort
251+
added.title='Aaaa';
252+
const addedId=added._id;
253+
added = await store.storeEntity(added);
254+
expect(added._id).toBe(addedId);
255+
result = store.entities();
256+
expect(result.map(val => val.title)).toEqual(['Aaaa', 'Sheep', 'Ubik']);
257+
258+
// If the updated element is not in the filter anymore, it should be removed from the list
259+
added.genre='Other';
260+
added = await store.storeEntity(added);
261+
result = store.entities();
262+
expect(result.length).toBe(2);
263+
expect(result.map(val => val.title)).toEqual(['Sheep', 'Ubik']);
264+
265+
});
156266
});
157267

158268
type AuthorType=ManagedData&{
@@ -167,8 +277,16 @@ type BookType =ManagedData&{
167277
genre:string
168278
}
169279

280+
type BookType2 = BookType & {
281+
cost:number
282+
}
283+
170284
type StoredBookType =ManagedData&{
171285
title:string,
172286
author:string,
173287
genre:string
174288
}
289+
290+
type StoredBookType2 = StoredBookType & {
291+
cost:number
292+
}

libs/xt-store/projects/store/src/store-entity/store-entity-feature.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ManagedData, XtTypeResolver } from 'xt-type';
1616
import { XtStoreManager } from '../store-manager/xt-store-manager';
1717
import { XtSortBy, XtStoreCriteria } from '../xt-store-parameters';
1818
import { XtStoreProviderHelper } from '../store-provider/xt-store-provider-helper';
19+
import { XtStoreSortBy } from '../xt-reporting';
1920

2021
export function selectId<T extends ManagedData=ManagedData>(): SelectEntityId<T> { return (data) => {
2122
if (data._id==null) throw new Error("ManagedData with no entity Id used in the store.", { cause: data });
@@ -62,9 +63,9 @@ export type XtSignalStore<T> = {
6263
* @param storeMgr
6364
* @param typeRegistry
6465
*/
65-
export function withXtStoreProvider<T extends ManagedData = ManagedData> (entityName:string, storeProvider?:XtStoreProvider<T>, storeMgr?:XtStoreManager, typeRegistry?: XtTypeResolver) {
66+
export function withXtStoreProvider<T extends ManagedData = ManagedData> (entityName:string, storeProvider?:XtStoreProvider<T>, storeMgr?:XtStoreManager, typeRegistry?: XtTypeResolver, options?: XtStoreEntityFeatureOptions) {
6667
return signalStoreFeature(
67-
withState ({ entityName, loading:false} as StoreState),
68+
withState ({ entityName, loading:false, sort:options?.sort, filter:options?.filter} as StoreState),
6869
withEntities(xtStoreEntityConfig<T> ()),
6970
withProps ( () => ({
7071
_storeProvider:storeProvider??storeMgr!.getProviderSafe<T>(entityName),
@@ -100,21 +101,6 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
100101
})));
101102
},
102103

103-
_callProviderSearchEntities ():Observable<T[]> {
104-
const sort=this._safeSort();
105-
if( sort.length>0) {
106-
// We call the full function but only needs the sorted & filtered data (no need for grouping)
107-
return store._storeProvider.searchAndPrepareEntities(entityName, this._safeSort(),undefined,undefined,...this._safeFilter()).pipe(
108-
map((value) => {
109-
return value.sortedData;
110-
})
111-
)
112-
} else {
113-
// If we don't need sorting, then a simpler function can be called
114-
return store._storeProvider.searchEntities(entityName, ...this._safeFilter());
115-
}
116-
},
117-
118104
/* listEntities (): Observable<ManagedData[]> {
119105
patchState(store, {loading:true});
120106
return store._storeProvider.searchEntities(entityName).pipe (map( (entities: ManagedData[]) => {
@@ -169,7 +155,7 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
169155
const listEntities = store.entities();
170156
const ret = new Array<T>();
171157
for (const entity of listEntities) {
172-
if (this._isPassingFilter(entity, criteria)){
158+
if (this._isPassingFilter(entity, false, criteria)){
173159
ret.push(entity);
174160
}
175161
}
@@ -238,7 +224,7 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
238224

239225
_patchStateSetEntity (stored:T) {
240226
// Ensure the entity is still part of the list
241-
if (this._isPassingFilter (stored)) {
227+
if (this._isPassingFilter (stored, true)) {
242228
// It's still in the list, now check if sort needs to be applied
243229
if (this._safeSort().length>0) {
244230
// We remove it from the old sort
@@ -258,15 +244,20 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
258244
}
259245

260246
},
261-
_isPassingFilter (element:T, criteria?:XtStoreCriteria<T>[]):boolean {
247+
_isPassingFilter (element:T, applyFilters:boolean=false, criteria?:XtStoreCriteria<T>[]):boolean {
262248
if (criteria===undefined) {
263249
criteria=this._safeFilter();
264250
}
265251
for (const crit of criteria) {
266252
if (!crit.filter(element)) {
267-
return false;
253+
// One filter is not met, but we may be allowed to set it automatically
254+
if ((applyFilters)&& (crit.operator=='=') && (element[crit.name]==null)) {
255+
// We enforce the criteria
256+
element[crit.name]=crit.value;
257+
}else {
258+
return false;
259+
}
268260
}
269-
270261
}
271262
return true;
272263
},
@@ -283,8 +274,27 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
283274
return ret;
284275
}
285276
return [];
277+
},
278+
_callProviderSearchEntities ():Observable<T[]> {
279+
const sort=this._safeSort();
280+
if( sort.length>0) {
281+
// We call the full function but only needs the sorted & filtered data (no need for grouping)
282+
return store._storeProvider.searchAndPrepareEntities(entityName, this._safeSort(),undefined,undefined,...this._safeFilter()).pipe(
283+
map((value) => {
284+
return value.sortedData;
285+
})
286+
)
287+
} else {
288+
// If we don't need sorting, then a simpler function can be called
289+
return store._storeProvider.searchEntities(entityName, ...this._safeFilter());
290+
}
286291
}
287292
})
288293
)
289294
);
290295
}
296+
297+
export type XtStoreEntityFeatureOptions<T extends ManagedData=ManagedData>= {
298+
sort?:XtSortBy<T>[];
299+
filter?:XtStoreCriteria<T>[];
300+
}

0 commit comments

Comments
 (0)