This commit is contained in:
crusader 2018-05-25 20:22:05 +09:00
parent bc3f80b726
commit 769594fb9f
19 changed files with 607 additions and 2 deletions

View File

@ -235,6 +235,46 @@
} }
} }
} }
},
"@loafer/ng-entity": {
"root": "projects/loafer/ng-entity",
"sourceRoot": "projects/loafer/ng-entity/src",
"projectType": "library",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"options": {
"tsConfig": "projects/loafer/ng-entity/tsconfig.lib.json",
"project": "projects/loafer/ng-entity/ng-package.json"
},
"configurations": {
"production": {
"project": "projects/loafer/ng-entity/ng-package.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "projects/loafer/ng-entity/src/test.ts",
"tsConfig": "projects/loafer/ng-entity/tsconfig.spec.json",
"karmaConfig": "projects/loafer/ng-entity/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"projects/loafer/ng-entity/tsconfig.lib.json",
"projects/loafer/ng-entity/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
} }
}, },
"defaultProject": "example" "defaultProject": "example"

View File

@ -0,0 +1,31 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

View File

@ -0,0 +1,8 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/loafer/ng-entity",
"deleteDestPath": true,
"lib": {
"entryFile": "src/public_api.ts"
}
}

View File

@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/loafer/ng-entity",
"lib": {
"entryFile": "src/public_api.ts"
}
}

View File

@ -0,0 +1,15 @@
{
"name": "@loafer/ng-entity",
"version": "0.0.1",
"repository": {
"type": "git",
"url": "https://git.loafle.net/loafer/ng.git"
},
"publishConfig": {
"registry": "https://nexus.loafle.net/repository/npm-loafle/"
},
"peerDependencies": {
"@angular/common": "^6.0.0-rc.0 || ^6.0.0",
"@angular/core": "^6.0.0-rc.0 || ^6.0.0"
}
}

View File

@ -0,0 +1,22 @@
import { IDSelector, EntityAdapter } from './type';
import { createInitialStateFactory } from './state';
import { createSelectorsFactory } from './selectors';
import { createUnsortedStateAdapter } from './unsorted_state_adapter';
export function createEntityAdapter<T, E>(selectId?: IDSelector<T>): EntityAdapter<T, E>;
export function createEntityAdapter<T, E>(selectId: IDSelector<T>): EntityAdapter<T, E> {
if (undefined === selectId) {
selectId = (e: any) => e.id;
}
const stateFactory = createInitialStateFactory<T, E>();
const selectorsFactory = createSelectorsFactory<T, E>();
const stateAdapter = createUnsortedStateAdapter<T, E>(selectId);
return {
selectId,
...stateFactory,
...selectorsFactory,
...stateAdapter,
};
}

View File

@ -0,0 +1,62 @@
import { EntityState, EntitySelectors, Dictionary } from './type';
import { createSelector } from '@ngrx/store';
export function createSelectorsFactory<T, E>() {
function getSelectors(): EntitySelectors<T, E, EntityState<T, E>>;
function getSelectors<S>(selectState: (state: S) => EntityState<T, E>): EntitySelectors<T, E, S>;
function getSelectors(selectState?: (state: any) => EntityState<T, E>): EntitySelectors<T, E, any> {
const selectIds = (state: any) => state.ids;
const selectEntities = (state: EntityState<T, E>) => state.entities;
const selectAll = createSelector(
selectIds,
selectEntities,
(ids: T[], entities: Dictionary<T>): any => ids.map((id: any) => (entities as any)[id])
);
const selectTotal = createSelector(selectIds, ids => ids.length);
const selectError = (state: any) => state.error;
if (!selectState) {
return {
selectIds,
selectEntities,
selectOne: (id: string | number) => createSelector(
selectEntities,
(entities: Dictionary<T>): T => {
if (null !== entities) {
return (entities as any)[id];
}
return null;
}
),
selectAll,
selectTotal,
selectError,
};
}
return {
selectIds: createSelector(selectState, selectIds),
selectEntities: createSelector(selectState, selectEntities),
selectOne: (id: string | number) => createSelector(
selectState,
createSelector(
selectEntities,
(entities: Dictionary<T>): T => {
if (null !== entities) {
return (entities as any)[id];
}
return null;
}
)
),
selectAll: createSelector(selectState, selectAll),
selectTotal: createSelector(selectState, selectTotal),
selectError: createSelector(selectState, selectError),
};
}
return {
getSelectors
};
}

View File

@ -0,0 +1,21 @@
import { EntityState } from './type';
export function getInitialEntityState<T, E>(): EntityState<T, E> {
return {
ids: [],
entities: {},
error: null,
};
}
export function createInitialStateFactory<T, E>() {
function getInitialState(): EntityState<T, E>;
function getInitialState<S extends object>(additionalState: S): EntityState<T, E> & S;
function getInitialState(additionalState: any = {}): any {
return Object.assign(getInitialEntityState(), additionalState);
}
return {
getInitialState
};
}

View File

@ -0,0 +1,45 @@
import { EntityState } from './type';
export enum DidMutate {
EntitiesOnly,
Both,
Error,
None,
}
export function createStateOperator<T, E, R>(mutator: (arg: R, state: EntityState<T, E>) => DidMutate): EntityState<T, E>;
export function createStateOperator<T, E, R>(mutator: (arg: any, state: any) => DidMutate): any {
return function operation<S extends EntityState<T, E>>(arg: R, state: any): S {
const clonedEntityState: EntityState<T, E> = {
ids: [...state.ids],
entities: { ...state.entities },
error: state.error,
};
const didMutate = mutator(arg, clonedEntityState);
if (didMutate === DidMutate.Both) {
clonedEntityState.error = null;
return Object.assign({}, state, clonedEntityState);
}
if (didMutate === DidMutate.EntitiesOnly) {
return {
...state,
entities: clonedEntityState.entities,
error: null,
};
}
if (didMutate === DidMutate.Error) {
return {
...state,
ids: null,
entities: null,
error: clonedEntityState.error,
};
}
return state;
};
}

View File

@ -0,0 +1,67 @@
export type IDSelectorStr<T> = (model: T) => string;
export type IDSelectorNum<T> = (model: T) => number;
export type IDSelector<T> = IDSelectorStr<T> | IDSelectorNum<T>;
export interface DictionaryNum<T> {
[id: number]: T;
}
export abstract class Dictionary<T> implements DictionaryNum<T> {
[id: string]: T;
}
export interface UpdateStr<T> {
id: string;
changes: Partial<T>;
}
export interface UpdateNum<T> {
id: number;
changes: Partial<T>;
}
export type Update<T> = UpdateStr<T> | UpdateNum<T>;
export interface EntityState<T, E> {
ids: string[] | number[];
entities: Dictionary<T>;
error: E | null;
}
export interface EntityStateAdapter<T, E> {
setOne<S extends EntityState<T, E>>(entity: T, state: S): S;
setAll<S extends EntityState<T, E>>(entities: T[], state: S): S;
addOne<S extends EntityState<T, E>>(entity: T, state: S): S;
addMany<S extends EntityState<T, E>>(entities: T[], state: S): S;
updateOne<S extends EntityState<T, E>>(entity: T, state: S): S;
updateMany<S extends EntityState<T, E>>(entities: T[], state: S): S;
upsertOne<S extends EntityState<T, E>>(entity: T, state: S): S;
upsertMany<S extends EntityState<T, E>>(entities: T[], state: S): S;
removeOne<S extends EntityState<T, E>>(entity: T, state: S): S;
removeMany<S extends EntityState<T, E>>(entities: T[], state: S): S;
removeAll<S extends EntityState<T, E>>(state: S): S;
setError<S extends EntityState<T, E>>(error: E, state: S): S;
}
export interface EntitySelectors<T, E, S> {
selectIds: (state: S) => string[] | number[];
selectEntities: (state: S) => Dictionary<T>;
selectOne: (id: string | number) => (state: S) => T;
selectAll: (state: S) => T[];
selectTotal: (state: S) => number;
selectError: (state: S) => E;
}
export interface EntityAdapter<T, E> extends EntityStateAdapter<T, E> {
selectId: IDSelector<T>;
getInitialState(): EntityState<T, E>;
getInitialState<S extends object>(state: S): EntityState<T, E> & S;
getSelectors(): EntitySelectors<T, E, EntityState<T, E>>;
getSelectors<S>(
selectState: (state: S) => EntityState<T, E>
): EntitySelectors<T, E, S>;
}

View File

@ -0,0 +1,189 @@
import { IDSelector, EntityStateAdapter, EntityState, Update } from './type';
import { DidMutate, createStateOperator } from './state_adapter';
export function createUnsortedStateAdapter<T, E>(selectId: IDSelector<T>): EntityStateAdapter<T, E>;
export function createUnsortedStateAdapter<T, E>(selectId: IDSelector<T>): any {
type R = EntityState<T, E>;
function addOneMutably(entity: T, state: R): DidMutate;
function addOneMutably(entity: any, state: any): DidMutate {
const id = selectId(entity);
if (id in state.entities) {
return DidMutate.None;
}
state.ids.push(id);
state.entities[id] = entity;
return DidMutate.Both;
}
function addManyMutably(entities: T[], state: R): DidMutate;
function addManyMutably(entities: any[], state: any): DidMutate {
let didMutate = false;
for (const entity of entities) {
didMutate = addOneMutably(entity, state) !== DidMutate.None || didMutate;
}
return didMutate ? DidMutate.Both : DidMutate.None;
}
function setOneMutably(entity: T, state: R): DidMutate;
function setOneMutably(entity: any, state: any): DidMutate {
state.ids = [];
state.entities = {};
addManyMutably([entity], state);
return DidMutate.Both;
}
function setAllMutably(entities: T[], state: R): DidMutate;
function setAllMutably(entities: any[], state: any): DidMutate {
state.ids = [];
state.entities = {};
addManyMutably(entities, state);
return DidMutate.Both;
}
function removeOneMutably(id: string | number, state: R): DidMutate;
function removeOneMutably(id: any, state: any): DidMutate {
return removeManyMutably([id], state);
}
function removeManyMutably(ids: string[] | number[], state: R): DidMutate;
function removeManyMutably(ids: any[], state: any): DidMutate {
const didMutate =
ids
.filter(id => id in state.entities)
.map(id => delete state.entities[id]).length > 0;
if (didMutate) {
state.ids = state.ids.filter((_id: any) => _id in state.entities);
}
return didMutate ? DidMutate.Both : DidMutate.None;
}
function removeAllMutably<S extends R>(state: S): DidMutate;
function removeAllMutably<S extends R>(state: any): DidMutate {
state.ids = [];
state.entities = {};
return DidMutate.Both;
}
function takeNewKey(
keys: { [id: string]: string },
update: Update<T>,
state: R
): void;
function takeNewKey(
keys: { [id: string]: any },
update: Update<T>,
state: any
): boolean {
const original = state.entities[update.id];
const updated: T = Object.assign({}, original, update.changes);
const newKey = selectId(updated);
const hasNewKey = newKey !== update.id;
if (hasNewKey) {
keys[update.id] = newKey;
delete state.entities[update.id];
}
state.entities[newKey] = updated;
return hasNewKey;
}
function updateOneMutably(update: Update<T>, state: R): DidMutate;
function updateOneMutably(update: any, state: any): DidMutate {
return updateManyMutably([update], state);
}
function updateManyMutably(updates: Update<T>[], state: R): DidMutate;
function updateManyMutably(updates: any[], state: any): DidMutate {
const newKeys: { [id: string]: string } = {};
updates = updates.filter(update => update.id in state.entities);
const didMutateEntities = updates.length > 0;
if (didMutateEntities) {
const didMutateIds =
updates.filter(update => takeNewKey(newKeys, update, state)).length > 0;
if (didMutateIds) {
state.ids = state.ids.map((id: any) => newKeys[id] || id);
return DidMutate.Both;
} else {
return DidMutate.EntitiesOnly;
}
}
return DidMutate.None;
}
function upsertOneMutably(entity: T, state: R): DidMutate;
function upsertOneMutably(entity: any, state: any): DidMutate {
return upsertManyMutably([entity], state);
}
function upsertManyMutably(entities: T[], state: R): DidMutate;
function upsertManyMutably(entities: any[], state: any): DidMutate {
const added: any[] = [];
const updated: any[] = [];
for (const entity of entities) {
const id = selectId(entity);
if (id in state.entities) {
updated.push({ id, changes: entity });
} else {
added.push(entity);
}
}
const didMutateByUpdated = updateManyMutably(updated, state);
const didMutateByAdded = addManyMutably(added, state);
switch (true) {
case didMutateByAdded === DidMutate.None &&
didMutateByUpdated === DidMutate.None:
return DidMutate.None;
case didMutateByAdded === DidMutate.Both ||
didMutateByUpdated === DidMutate.Both:
return DidMutate.Both;
default:
return DidMutate.EntitiesOnly;
}
}
function setErrorMutably<S extends R>(error: E, state: S): DidMutate;
function setErrorMutably<S extends R>(error: any, state: any): DidMutate {
state.error = error;
return DidMutate.Error;
}
return {
setOne: createStateOperator(setOneMutably),
setAll: createStateOperator(setAllMutably),
addOne: createStateOperator(addOneMutably),
addMany: createStateOperator(addManyMutably),
updateOne: createStateOperator(updateOneMutably),
updateMany: createStateOperator(updateManyMutably),
upsertOne: createStateOperator(upsertOneMutably),
upsertMany: createStateOperator(upsertManyMutably),
removeOne: createStateOperator(removeOneMutably),
removeMany: createStateOperator(removeManyMutably),
removeAll: createStateOperator(removeAllMutably),
setError: createStateOperator(setErrorMutably),
};
}

View File

@ -0,0 +1,6 @@
/*
* Public API Surface of ng-entity
*/
export { createEntityAdapter } from './lib/adapter';
export { EntityState, EntityAdapter } from './lib/type';

View File

@ -0,0 +1,22 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -0,0 +1,33 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"target": "es2015",
"module": "es2015",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"inlineSources": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"types": [],
"lib": [
"dom",
"es2015"
]
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"strictMetadataEmit": true,
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"flatModuleId": "AUTOGENERATED",
"flatModuleOutFile": "AUTOGENERATED"
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,17 @@
{
"extends": "../../../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"lib",
"camelCase"
],
"component-selector": [
true,
"element",
"lib",
"kebab-case"
]
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@loafer/ng-rpc", "name": "@loafer/ng-rpc",
"version": "0.0.6", "version": "0.0.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://git.loafle.net/loafer/ng.git" "url": "https://git.loafle.net/loafer/ng.git"

View File

@ -25,6 +25,9 @@
], ],
"@loafer/ng-rpc": [ "@loafer/ng-rpc": [
"dist/loafer/ng-rpc" "dist/loafer/ng-rpc"
],
"@loafer/ng-entity": [
"dist/loafer/ng-entity"
] ]
} }
} }