ngxs-labs / entity-state Goto Github PK
View Code? Open in Web Editor NEW⏱ WIP: Entity adapter
⏱ WIP: Entity adapter
First of all thanks for the great work !
As pagination is very specific to every application, I'd suggest that this library should best not assume any kind of implementation. It'd be easier to add your own implementation than work around the provided one.
We are using this in a project close to production and are currently building this ourselves.
Is it possible to provide it as NPM package with beta/alpha tag?
Is it useful to add a flag in the super
call of your store that enforces immutable entities? The object containing the data is immutable but the entities itself aren't required to be immutable.
This could simply be checked by adding current !== updated
after this.onUpdate
and throwing an error depending on the result.
Also: what should the default value for the flag be, if it's wanted. Or no default value?
A kind of state decorator to implement a standardized CRUD action mutators and selectors for an entity collection.
The decorator would receive the action classes for create, update, destroy and would map them to internal methods, and would expose some selectors for retrieving one, many and all entities and maybe a helper for filters
export interface Student {
name: string;
grade: number;
}
@EntityState<Student>({
name: 'students',
actions: {
create: [StudentAdd, StudentFullLoad],
destroy: [StudentDelete]
},
defaults: {
// Decorator injects automatically the collection (array) of entities, and so on ...
selectedId: number,
},
})
class StudentState {}
export class StudentAdd {
static readonly type = '[Student] Add'
constructor (public payload: Student) {}
}
export class StudentFullLoad {
static readonly type = '[Student] Full Load'
constructor (public payload: Student[]) {}
}
export class StudentDelete {
static readonly type = '[Student] Delete'
constructor (public id: number) {}
}
const all = this.store.select(StudentState.retrieveAll)
const one = this.store.select(StudentState.retrieve(5))
or
const all = this.store.select(EntityState.retrieveAll<StudentState>)
const one = this.store.select(EntityState.retrieve<StudentState>(5))
I'm not sure if the current version of typescript could implement that... Any ideas will be welcome!
Well i was trying to mix the states in a selector, i try Static and Dynamic but neither of them work.
I get as first value an empty array, which is correct, and after i get undefined. Both states after update have all the values.
I compiled the current master of the entity-state to include it as library.
// AuthorState extends EntityState<AuthorDTO>
@Selector([LocationState])
static entities2(
state: EntityStateModel<AuthorDTO>,
locationState: EntityStateModel<LocationDTO>
) {
return Object.values(state.entities)
.map(v => {
v.bornLocation = locationState.entities[v.bornIdlocation];
return v;
});
}
static entities3() {
return createSelector([this, LocationState], (state: EntityStateModel<AuthorDTO>, location: EntityStateModel<LocationDTO>) => {
return Object.values(state.entities)
.map(v => {
v.bornLocation = location.entities[v.bornIdlocation];
return v;
});
});
}
//And later in the component
@Select(AuthorState.entities2)
author1$: Observable<AuthorDTO[]>;
@Select(AuthorState.entities3())
author2$: Observable<AuthorDTO[]>;
@markwhitfeld @splincode @arturovt @JanMalch
Now that version 3.4.0 is out, we might want to create a new version for this plugin using State Operators
. And then make the finish round to publish this plugin.
What do you think?
I am getting an array from Get Api call but I am unable to add them in the entities attribute, How can I add in that attribute please let me know?
Hi guys. Many thanks for making this lib. I just want to ask: how are you supposed to get an entity by its ID? Have I missed the interface for that or is it not possible? I can only find the interface for getting the nth entity or all entities as an array.
But I want to associate multiple entities (forum-threads and posts) as a graph-relationship and do not want to filter an array for the id every time. I think others have this use-case too.
As far as I understand the implementation of Entity it is a simple JS object with keys and values, where the values are also kept as a sorted array. So (unless it is possible already) why not give read access to the key-value-object?
There seems to be an issue with running ngcc
postinstall
Angular version: 10.2.3
Ivy compiler enabled in tsconfig.json with:
"angularCompilerOptions": {
"enableIvy": true
},
Error Reported:
Error: Error on worker #2: Error: Attempted to get members of a non-class: "class Add {
/**
* Generates an action that will add the given entities to the state.
* The entities given by the payload will be added.
* For certain ID strategies this might fail, if it provides an existing ID.
* In all other cases it will overwrite the ID value in the entity with the calculated ID.
* @param target The targeted state class
* @param payload An entity or an array of entities to be added
* @see CreateOrReplace#constructor
*/
constructor(target, payload) {
return generateActionObject(exports.EntityActionType.Add, target, payload);
}
}"
at UmdReflectionHost.Esm2015ReflectionHost.getMembersOfClass (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\host\esm2015_host.js:178:23)
at DelegatingReflectionHost.getMembersOfClass (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\host\delegating_host.js:103:34)
at C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\analysis\module_with_providers_analyzer.js:77:32
at Map.forEach (<anonymous>)
at ModuleWithProvidersAnalyzer.getModuleWithProvidersFunctions (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\analysis\module_with_providers_analyzer.js:72:21)
at C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\analysis\module_with_providers_analyzer.js:39:33
at Array.forEach (<anonymous>)
at ModuleWithProvidersAnalyzer.analyzeProgram (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\analysis\module_with_providers_analyzer.js:38:23)
at Transformer.analyzeProgram (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\packages\transformer.js:133:45)
at Transformer.transform (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\packages\transformer.js:76:27)
at ClusterMaster.onWorkerMessage (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\execution\cluster\master.js:195:27)
at C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\execution\cluster\master.js:55:95
at ClusterMaster.<anonymous> (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\execution\cluster\master.js:293:57)
at step (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\node_modules\tslib\tslib.js:140:27)
at Object.next (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\node_modules\tslib\tslib.js:121:57)
at C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\node_modules\tslib\tslib.js:114:75
at new Promise (<anonymous>)
at Object.__awaiter (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\node_modules\tslib\tslib.js:110:16)
at EventEmitter.<anonymous> (C:\Users\marley.powell\source\repos\exclaimercloud-ui\src\ExclaimerCloud.UI\ClientApp\node_modules\@angular\compiler-cli\ngcc\src\execution\cluster\master.js:287:32)
at EventEmitter.emit (events.js:315:20)
A while back I prototyped something like John Papas NGRX-DATA, https://gist.github.com/amcdnl/cafd37bc9a99e2cd7653ebc023e06ffe#file-readme-md
Something we might want to think about one day.
I have created a LIB to be used as follows. I'm using it on mine.
projects:
How to use:
export class UserStateModel extends NgxsEntityStateModel<UserModel> {}
@State<UserStateModel>({
name: 'user',
defaults: UserStateModel.InitialState()
})
export class UserState implements NgxsOnInit {}
Methods Available:
NgxsEntityAdapter.addAll( payload, ctx );
NgxsEntityAdapter.addOne( payload, ctx );
NgxsEntityAdapter.updateOne( payload, ctx );
NgxsEntityAdapter.removeOne( payload, ctx );
NgxsEntityStateModel Class:
export class NgxsEntityStateModel<T> {
public ids: string[];
public entities: { [id: string]: T };
public selected: T | null;
public isLoading: boolean;
static InitialState() {
return {
ids: [],
entities: {},
selected: null,
isLoading: false
};
}
}
When I set developmentMode: true
I get an object is not extensible
error at https://github.com/ngxs-labs/entity-state/blob/master/src/lib/entity-state.ts#L451
Is that the expected behavior or am I doing something wrong?
Hello I tried "npm i @ngxs-labs/entity-state" to run it, it downloads all the dependencies but 'src' folder did not create which contains all library methods. Please let me know how can I get that?
It's possible to build the library but the integration app is unable to compile. (npm run lib && ng build
)
ERROR in src/app/app.component.ts(3,125): error TS2307: Cannot find module 'entity-store'.
src/app/app.component.ts(14,21): error TS2339: Property 'size' does not exist on type 'typeof TodoState'.
src/app/app.component.ts(15,21): error TS2339: Property 'entities' does not exist on type 'typeof TodoState'.
src/app/app.component.ts(16,21): error TS2339: Property 'active' does not exist on type 'typeof TodoState'.
src/app/app.component.ts(17,21): error TS2339: Property 'activeId' does not exist on type 'typeof TodoState'.
src/app/app.component.ts(18,21): error TS2339: Property 'keys' does not exist on type 'typeof TodoState'.
src/app/app.component.ts(19,21): error TS2339: Property 'loading' does not exist on type 'typeof TodoState'.
src/app/app.component.ts(20,21): error TS2339: Property 'error' does not exist on type 'typeof TodoState'.
src/app/app.component.ts(36,39): error TS2339: Property 'remove' does not exist on type 'typeof TodoState'.
src/app/app.component.ts(40,39): error TS2339: Property 'remove' does not exist on type 'typeof TodoState'.
src/app/store/todo/store.ts(2,65): error TS2307: Cannot find module 'entity-store'.
I am getting an error when trying to use ngcc in conjunction with this package, and according to the below post it has to do with the way the package is being bundled. This package is currently bundled in ES2015 format and ngcc uses ES5. Is there a reason this is configured for ES2015, or can it be changed to ES5?
I've gone through the code in this to try and see if I can use it to replace (or get some better ideas on) our homegrown version of doing the same thing (ours has WAY too much boilerplate though and probably much worse performance).
There are a couple of things which most APIs have in common, some of which this repo addresses, however there are use cases I can't see addressed so far. Could I get some feedback on whether these are currently possible, or whether this could be planned for a future update? I'm interested in helping contribute where I can as I would like to use this.
Currently I have a state for each API endpoint which contains the following keys:
table: Map<string, T>;
collections: Map<Params, string[]>;
I'm assuming this isn't the most perfect method, however, when requesting a single resource, it is checked first in the table, and then loaded via the service (being saved in the table). When a collection is requested, the parameters for that request are mapped to the identities returned. The individual resources are then saved in the table.
Would this implementation be required on top of this repository, or is this something this repository could accomplish? At the very least the discussion here may produce fruit for those searching for a similar answer, if not provide grounds for documentation.
As for entities being related, this leads into ORM which I'm not sure is a good idea client side, however I can see the benefits.
Thanks.
Discuss and decide on one of the following syntax for dispatching actions:
// 1. Misses new keyword but has type information for payload
this.store.dispatch(UpdateActive(TodoState, { done: true }));
// 2. Looks more like the usual ngxs way but no type information for payload
this.store.dispatch(new TodoState.updateActive({ done: true }));
Also consider: Should the action be upper or lowercase? Change order of parameters?
I added some filters like this, they could be provided by the library.
import { Type } from "@angular/core";
import { EntityState } from "@ngxs-labs/entity-state";
import { ofActionSuccessful } from "@ngxs/store";
export enum EntityActionType {
SetActive = "setActive"
...
}
export const ofEntityActionSuccessful = (state: Type<EntityState<any>>, actionType: EntityActionType) => {
const type = `[${(state as any).NGXS_META.path}] ${actionType}`;
return ofActionSuccessful({
type: type
});
};
...
I like Akitas approach of having few actions and allowing different parameter types, like updating by a single ID, an array of IDs or a selecting by function. This is also the current implementation.
https://netbasal.gitbook.io/akita/entity-store/entity-store/api
One particular interesting action is createOrReplace
vs add
. The former takes an ID and an entity, while the latter only takes one or more entities.
Moved from ngxs/store#321
A base class that you can implement to get methods on your state class to simplify handling crud operations.
It looks something like this.
class Project {
id: string;
name: string;
}
class AddOne {
static readonly type = '[Project] AddProject';
constructor(public project: Project) {}
}
class AddMany {
static readonly type = '[Project] AddProjects';
constructor(public projects: Project[]) {}
}
interface ProjectStateModel extends EntityState<Project> {}
@State<ProjectStateModel>({
name: 'projects',
defaults: {
...EntityBase.defaults
}
})
class ProjectState extends EntityBase<Project, ProjectStateModel> {
// pass injector to base class so that we can get and set state
// @todo, can we work around this?
constructor(injector: Injector) {
super(injector);
}
// will automatically do a ctx.setState if needed
@Action(AddOne)
addProject(ctx: StateContext<ProjectStateModel>, action: AddOne) {
this.addOne(action.project);
}
@Action(AddMany)
addProjects(ctx: StateContext<ProjectStateModel>, action: AddMany) {
this.addMany(action.projects);
}
}
for now I've only implemented these two, but it shouldn't take me too long to implement the rest of the methods.
Though I would like some input on the api of it.
I would like to be able to get at the StateContext
from within the class.
At the moment I'm hacking it by getting this.constructor[META_KEY]
and then calling .next
on the stateStream.
Please let me know what you think, and if you have any suggestions to improve it even further :)
This matches the correct ngxs concepts and also matches the library name.
When upgrading to angular 9 the next incompatible peer dependency error appears:
Package "@ngxs-labs/entity-state" has an incompatible peer dependency to "@angular/core" (requires "^6.0.0 || ^7.0.0" (extended), would install "9.1.12").
Could you please update it?
🚨 You need to enable Continuous Integration on Greenkeeper branches of this repository. 🚨
To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.
Since we didn’t receive a CI status on the greenkeeper/initial
branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.
If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/
.
Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please click the 'fix repo' button on account.greenkeeper.io.
Steps to reproduce: set
NgxsModule.forRoot(states, {
developmentMode: true
}),
in the example project and you will get
ERROR TypeError: Cannot add property VALUE_OF_ID, object is not extensible
at entity-state.ts:471
(if you use AddOrReplace action), eg at entities[id] = entity;
It would be nice to have an error message suggesting to disable development mode if entity doesn't work with immutability (?)
Instead of repro steps I added test
import { EntityState, EntityStateModel, defaultEntityState, IdStrategy } from '@ngxs-labs/entity-state';
import { State, NgxsModule, Store, Action, StateContext } from '@ngxs/store';
import { TestBed } from '@angular/core/testing';
fdescribe('EntityState selectors recalculate when not needed', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NgxsModule.forRoot([
SomeEntityState,
AnotherState
])
]
});
});
it('get entities once when updating different state', () => {
const spy = jasmine.createSpy('callback');
const store = TestBed.get(Store) as Store;
store.select(SomeEntityState.entities).subscribe(spy);
store.dispatch(new UpdateAnotherState);
store.dispatch(new UpdateAnotherState);
expect(spy).toHaveBeenCalledTimes(1); // Expected spy callback to have been called once. It was called 3 times.
});
});
@State<EntityStateModel<any>>({
name: 'someEntity',
defaults: defaultEntityState<any>()
})
class SomeEntityState extends EntityState<any> {
constructor() {
super(SomeEntityState, 'id', IdStrategy.EntityIdGenerator);
}
}
class UpdateAnotherState {
static type = 'UpdateAnotherState';
}
@State<number>({
name: 'another',
defaults: 999
})
class AnotherState {
constructor() {
}
@Action(UpdateAnotherState)
update(ctx: StateContext<number>) {
ctx.setState(Math.random());
}
}
Switch to Jest test framework to be in line with the main library.
I generalize some CRUD operations extending the ngxs-labs/entity-state plugin.
It's easy to extend this library, but i would like to use internal.ts methods so i don't need to copy them in my application.
Also i hope to get a Stable release soon available on NPM. 😉
It would be nice to be able to generate id from some nested object of the entity.
I saw in some other similar implementations of entity adapters the possibility to pass a function for generating the id.
Is this possible?
An entity state should preserve the order in which the entities were added.
Updating an entity doesn't update order.
See also #16 and https://redux.js.org/recipes/structuringreducers/normalizingstateshape#designing-a-normalized-state
First of all sorry for the complete lack of communication.
As you can already tell this library is basically abandoned at this point. I'm completely out of the loop with what's going on with ngxs-labs & ngxs, and even most of Angular. Unfortunately I don't have the time to properly work on this library.
While I was still working on it, I had massive issues with the project setup and build process, which blocks any kind of updates. Looking at the considerable amount of downloads, I'm happy to review any PRs that help keep the library somewhat useable. Again, I don't have the time or knowledge to fix it myself or create a new maintainable setup.
So unless someone else is willing to take over this library you shouldn't be using it. If nothing changes, I'll archive this repository and deprecate the npm package in a few weeks or months.
I think this should be controlled at the entity state level.
[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report
[ ] Performance issue
[x ] Feature request
[ ] Documentation issue or request
[ ] Support request => https://github.com/ngxs/store/blob/master/CONTRIBUTING.md
[ ] Other... Please describe:
Currently you can't create a SubState within an array or map.
I would like to be able to use a Map of RowStates in a StateModel:
import {State} from '@ngxs/store';
export interface RowStateModel {
id: number; // Or string
}
export interface GridStateModel {
id: number; // Or string
rows: Map<number, RowState>;
}
/**
* GridCollectionStateModel has a map of GridStates,
* where the key refers to the dataname of the GridStateModel.
*/
export interface GridCollectionStateModel {
grids: Map<number, GridState>;
}
@State<RowStateModel>({
name: 'row',
defaults: {
id: -1
}
})
export class RowState {}
@State<GridStateModel>({
name: 'grid',
defaults: {
id: -1,
rows: new Map<number, RowState>()
}
})
export class GridState {}
@State<GridCollectionStateModel>({
name: 'grid-collection',
defaults: {
grids: new Map<number, GridState>()
}
})
export class GridCollectionState {}
This should create a state that looks like this:
{
"grids": {
"GridState_0": {
"id": 0,
"rows": {
"RowState_0": {
"id": 0
},
"RowState_1": {
"id": 1
}
}
},
"GridState_1": {
"id": 1,
"rows": {
"RowState_0": {
"id": 0
}
}
}
}
}
In my application I have a use case where I have a complex data tree which would need to have a SubState. I have a collection of grids that can be displayed at the same time. Each grid has a dataname (unique identifier), which it uses to fetch the corresponding data. On top of that, each grid has a set of hierarchical rows, for which I also could use the SubStates.
As dictionaries with numbers are a lot faster, and I could not think of any advantages, I use numbers as keys.
However, entity-state seems to allow only strings as keys. For instance, EntitySelector<T>
is restricted to string-types (except when you use a custom lambda).
/**
* An EntitySelector determines which entities will be affected.
* Can be one of the following:
* - a single ID in form of a string
* - multiple IDs in form of an array of strings
* - a predicate function that returns `true` for entities to be selected
*/
export declare type EntitySelector<T> = string | string[] | ((entity: T) => boolean);
It would be great if the restrictions could be just a little bit relaxed by extending such types to numbers.
I noticed two contradicting thoughts: I made the onUpdate
method abstract
, because I thought maybe someone wants a state with just strings or something. But on the other hand I expect them to pass a idKey
in the super
call and all the code works based on entity[idKey]
. Also: what if the entities can't be used properly with the entity[idKey]
syntax. What if a function should be called instead?
Which raises the following questions:
<T>
in EntityState<T>
have constraints like <T extends {}>
?onUpdate
method be non-abstract and provide a default implementation that returns {...current, ...updated}
, to further reduce boilerplate? The user would still have the possibility to overwrite this, if spreading does not work for the entity type.idOf
method?So far I only used object literals for my states, so I'm not sure if the current implementation fits all needs. For example would this work with ImmutableJS data structures?
Would love to hear some opinions and experiences.
Should the remove
action remove all entities, if the payload is strictly === null
or should this functionality be moved to a separate removeAll
action?
🚨 You need to enable Continuous Integration on Greenkeeper branches of this repository. 🚨
To enable Greenkeeper, you need to make sure that a commit status is reported on all branches. This is required by Greenkeeper because it uses your CI build statuses to figure out when to notify you about breaking changes.
Since we didn’t receive a CI status on the greenkeeper/initial
branch, it’s possible that you don’t have CI set up yet. We recommend using Travis CI, but Greenkeeper will work with every other CI service as well.
If you have already set up a CI for this repository, you might need to check how it’s configured. Make sure it is set to run on all new branches. If you don’t want it to run on absolutely every branch, you can whitelist branches starting with greenkeeper/
.
Once you have installed and configured CI on this repository correctly, you’ll need to re-trigger Greenkeeper’s initial pull request. To do this, please click the 'fix repo' button on account.greenkeeper.io.
{
"name": "entity-state",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^7.0.0",
"@angular/core": "^7.0.0",
"@ngxs/store": "^3.3.0"
}
}
There's a reason why not support angular 6 and older ngxs versions?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.