diff --git a/README.md b/README.md index 75350b3..5fbe63c 100644 --- a/README.md +++ b/README.md @@ -66,17 +66,37 @@ following options: - `autoKey` (boolean): If set to `true`, this option tells data stores to automatically generate an unique identifier for new objects without a key. + - `logging` (`IStoreLogConfig`): By using this option you can enable automatic logging for the store's entities. + The required `mode`-field specifies the mode, whereas `LogMode.Disabled` is used to disable logging at all, + `LogMode.Simple` enables logging but includes only some meta information on the change including store, primary key, + type of change (e.g. `created` or `removed`) and `LogMode.Full` basically does the same as `Simple` but it includes + the changed object. So deciding whether to use `Simple` or `Full` is equal to making a trade-off between loss of + information and a large logging store. + By default, logging is disabled for each store which does not explicitly enable it or does not derive another + preference from one of its parents. This of course can be changed by setting another mode in the `_defaults`-store + (as it can be seen in the example below). + The `eventSelection` can be optionally used to filter the events which should be logged. + `IStoreLogConfig`-instances can be built by using a `StoreLogBuilder`. + Logging is used by the `data-store`-module only. + An example for such a `ISchemaConfig`-object for a simple blog could look like this: ```typescript const schemaConfig: ISchemaConfig = { _defaults: { key: 'id', - autoKey: true + autoKey: true, + logging: { + mode: LogMode.Simple, + } }, _authored: { targets: { author: 'user' + }, + logging: { + mode: LogMode.Full, + eventSelection: ['created', 'updated', 'removed', 'cleared'] } }, role: true, @@ -85,6 +105,9 @@ const schemaConfig: ISchemaConfig = { autoKey: false, targets: { role: 'role' + }, + logging: { + eventSelection: ['created', 'removed'] } }, article: { diff --git a/package.json b/package.json index 2670f09..2498d4a 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "build-prod": "tslint ./src/*.ts ./src/**/*.ts && WEBPACK_ENV=prod webpack", "build-all": "npm run build && npm run build-prod", "clean-build": "rm -rf lib/ && npm run build-all", - "test": "npm run build && ./node_modules/karma/bin/karma start ./karma.conf.js", + "test-mocha": "npm run build && ./node_modules/.bin/mocha --compilers ts:ts-node/register ./test/**/*.spec.ts", + "test-karma": "npm run build && ./node_modules/karma/bin/karma start ./karma.conf.js", "build-and-publish": "npm run clean-build && npm publish" }, "devDependencies": { diff --git a/src/model/event-selection-type.ts b/src/model/event-selection-type.ts new file mode 100644 index 0000000..e78889c --- /dev/null +++ b/src/model/event-selection-type.ts @@ -0,0 +1,3 @@ +import { EventType } from './index'; + +export type EventSelection = EventType | EventType[]; diff --git a/src/model/event-type.ts b/src/model/event-type.ts new file mode 100644 index 0000000..86e9515 --- /dev/null +++ b/src/model/event-type.ts @@ -0,0 +1 @@ +export type EventType = 'created' | 'updated' | 'removed' | 'cleared'; diff --git a/src/model/index.ts b/src/model/index.ts index 624790b..d63e0b5 100644 --- a/src/model/index.ts +++ b/src/model/index.ts @@ -1,6 +1,9 @@ import { Depth } from './depth'; +import { EventSelection } from './event-selection-type'; +import { EventType } from './event-type'; import { FetchCallback } from './fetch-callback'; import { KeyMap } from './key-map'; +import { LogMode } from './log-mode.enum'; import { NdbDocument } from './ndb-document'; import { NormalizedData } from './normalized-data'; import { ReverseReferences } from './reverse-references'; @@ -9,8 +12,11 @@ import { ValidKey } from './valid-key'; export { Depth, + EventSelection, + EventType, FetchCallback, KeyMap, + LogMode, NdbDocument, NormalizedData, ReverseReferences, diff --git a/src/model/log-mode.enum.ts b/src/model/log-mode.enum.ts new file mode 100644 index 0000000..832b6aa --- /dev/null +++ b/src/model/log-mode.enum.ts @@ -0,0 +1,5 @@ +export enum LogMode { + Disabled = 0, // disable logging + Simple = 1, // `LogEntry` except `item`-field + Full = 2 // include `item`-field +} diff --git a/src/schema/builder/index.ts b/src/schema/builder/index.ts index 67ed170..4aebf16 100644 --- a/src/schema/builder/index.ts +++ b/src/schema/builder/index.ts @@ -1,7 +1,9 @@ import { SchemaBuilder } from './schema-builder'; import { StoreBuilder } from './store-builder'; +import { StoreLogBuilder } from './store-log-builder'; export { StoreBuilder, - SchemaBuilder + SchemaBuilder, + StoreLogBuilder }; diff --git a/src/schema/builder/store-builder.ts b/src/schema/builder/store-builder.ts index c928498..a08b855 100644 --- a/src/schema/builder/store-builder.ts +++ b/src/schema/builder/store-builder.ts @@ -1,5 +1,6 @@ import { isNull } from '../../utility/object'; import { IStoreConfig } from '../model/store-config-interface'; +import { IStoreLogConfig } from '../model/store-log-config-interface'; import { IStoreTargetConfig } from '../model/store-target-config-interface'; import { IStoreTargetItem } from '../model/store-target-item-interface'; @@ -8,7 +9,8 @@ export class StoreBuilder { constructor(private parent?: string, private key?: string, private autoKey?: boolean, - private targets?: IStoreTargetConfig) { + private targets?: IStoreTargetConfig, + private logging?: IStoreLogConfig) { } public setKey(key: string): StoreBuilder { @@ -61,6 +63,11 @@ export class StoreBuilder { return this; } + public setLogging(logging: IStoreLogConfig): StoreBuilder { + this.logging = logging; + return this; + } + public get build(): boolean | string | IStoreConfig { if (this.hasConfiguration) { const result: IStoreConfig = {}; @@ -80,6 +87,10 @@ export class StoreBuilder { result.targets = this.targets; } + if (this.logging) { + result.logging = this.logging; + } + return result; } else if (this.parent) { @@ -91,7 +102,7 @@ export class StoreBuilder { } private get hasConfiguration(): boolean { - return !isNull(this.key) || !isNull(this.autoKey) || this.hasTargets; + return !isNull(this.key) || !isNull(this.autoKey) || this.hasTargets || !isNull(this.logging); } private get hasTargets(): boolean { diff --git a/src/schema/builder/store-log-builder.ts b/src/schema/builder/store-log-builder.ts new file mode 100644 index 0000000..b06a0bc --- /dev/null +++ b/src/schema/builder/store-log-builder.ts @@ -0,0 +1,26 @@ +import { EventSelection, LogMode } from '../../model'; +import { IStoreLogConfig } from '../model'; + +export class StoreLogBuilder { + + constructor(private mode?: LogMode, + private eventSelection?: EventSelection) { + } + + public setMode(value: LogMode): StoreLogBuilder { + this.mode = value; + return this; + } + + public setEventSelection(value: EventSelection): StoreLogBuilder { + this.eventSelection = value; + return this; + } + + public build(): IStoreLogConfig { + return { + mode: this.mode || LogMode.Disabled, + eventSelection: this.eventSelection + }; + } +} diff --git a/src/schema/model/index.ts b/src/schema/model/index.ts index 2c2d3ed..e38b7c7 100644 --- a/src/schema/model/index.ts +++ b/src/schema/model/index.ts @@ -2,16 +2,20 @@ import { ISchemaConfig } from './schema-config-interface'; import { ISchemaExpanded } from './schema-expanded-interface'; import { IStoreConfig } from './store-config-interface'; import { IStore } from './store-interface'; +import { IStoreLogConfig } from './store-log-config-interface'; +import { IStoreLog } from './store-log-interface'; import { IStoreTargetConfig } from './store-target-config-interface'; import { IStoreTarget } from './store-target-interface'; import { IStoreTargetItem } from './store-target-item-interface'; export { - ISchemaExpanded, ISchemaConfig, - IStore, + ISchemaExpanded, IStoreConfig, - IStoreTarget, + IStore, + IStoreLogConfig, + IStoreLog, IStoreTargetConfig, + IStoreTarget, IStoreTargetItem }; diff --git a/src/schema/model/store-config-interface.ts b/src/schema/model/store-config-interface.ts index 4adcd15..98261e1 100644 --- a/src/schema/model/store-config-interface.ts +++ b/src/schema/model/store-config-interface.ts @@ -1,3 +1,4 @@ +import { IStoreLogConfig } from './store-log-config-interface'; import { IStoreTargetConfig } from './store-target-config-interface'; export interface IStoreConfig { @@ -5,4 +6,5 @@ export interface IStoreConfig { key?: string; autoKey?: boolean; targets?: IStoreTargetConfig; + logging?: IStoreLogConfig; } diff --git a/src/schema/model/store-interface.ts b/src/schema/model/store-interface.ts index 29bbdff..3d41ad1 100644 --- a/src/schema/model/store-interface.ts +++ b/src/schema/model/store-interface.ts @@ -1,3 +1,4 @@ +import { IStoreLog } from './store-log-interface'; import { IStoreTarget } from './store-target-interface'; export interface IStore { @@ -5,4 +6,5 @@ export interface IStore { key?: string; autoKey?: boolean; targets?: IStoreTarget; + logging?: IStoreLog; } diff --git a/src/schema/model/store-log-config-interface.ts b/src/schema/model/store-log-config-interface.ts new file mode 100644 index 0000000..724ed85 --- /dev/null +++ b/src/schema/model/store-log-config-interface.ts @@ -0,0 +1,6 @@ +import { EventSelection, LogMode } from '../../model'; + +export interface IStoreLogConfig { + mode: LogMode; + eventSelection?: EventSelection; +} diff --git a/src/schema/model/store-log-interface.ts b/src/schema/model/store-log-interface.ts new file mode 100644 index 0000000..79aa24e --- /dev/null +++ b/src/schema/model/store-log-interface.ts @@ -0,0 +1,6 @@ +import { EventSelection, LogMode } from '../../model'; + +export interface IStoreLog { + mode: LogMode; + eventSelection?: EventSelection; +} diff --git a/src/schema/schema-interface.ts b/src/schema/schema-interface.ts index 6cf6d8c..90db0db 100644 --- a/src/schema/schema-interface.ts +++ b/src/schema/schema-interface.ts @@ -1,7 +1,8 @@ -import { IStore } from './model/store-interface'; -import { IStoreTargetItem } from './model/store-target-item-interface'; +import { IStore, IStoreTargetItem } from './model'; +import { SchemaLogConfig } from './schema-log-config'; export interface ISchema { + getLogConfig(): SchemaLogConfig; hasType(type: string): boolean; getTypes(): string[]; getConfig(type: string): IStore; diff --git a/src/schema/schema-log-config.ts b/src/schema/schema-log-config.ts new file mode 100644 index 0000000..f4c3515 --- /dev/null +++ b/src/schema/schema-log-config.ts @@ -0,0 +1,49 @@ +import { EventType, LogMode } from '../model'; +import { StoreLogBuilder } from './builder/store-log-builder'; +import { IStoreLogConfig } from './model/store-log-config-interface'; +import { ISchema } from './schema-interface'; + +export class SchemaLogConfig { + + constructor(private readonly _schema: ISchema) { + } + + public getConfig(type: string, orDefault?: IStoreLogConfig): IStoreLogConfig { + let config = orDefault || new StoreLogBuilder().build(); + if (this._schema.hasType(type)) { + config = Object.assign(config, this._schema.getConfig(type).logging); + } + return config; + } + + public getLogMode(type: string, orDefault = LogMode.Disabled): LogMode { + return this._schema.hasType(type) + ? this._schema.getConfig(type).logging.mode + : orDefault; + } + + public getEventTypes(type: string, orDefault?: EventType[]): EventType[] { + let types: EventType[]; + if (this._schema.hasType(type)) { + const eventSelection = this._schema.getConfig(type).logging.eventSelection; + types = Array.isArray(eventSelection) ? eventSelection : [eventSelection]; + } + return types && types.length > 0 ? types : orDefault; + } + + public isLoggingEnabled(type: string, eventType?: EventType): boolean { + let isEnabled = this._schema.hasType(type); + if (isEnabled) { + const logConfig = this._schema.getConfig(type).logging; + isEnabled = logConfig.mode !== LogMode.Disabled; + + if (isEnabled && eventType) { + isEnabled = Array.isArray(logConfig.eventSelection) + ? logConfig.eventSelection.indexOf(eventType) >= 0 + : logConfig.eventSelection === eventType; + } + } + + return isEnabled; + } +} diff --git a/src/schema/schema.ts b/src/schema/schema.ts index 13b473a..7ec25e7 100644 --- a/src/schema/schema.ts +++ b/src/schema/schema.ts @@ -1,13 +1,16 @@ -import { deepClone } from '../utility/deep-clone'; -import { isNull } from '../utility/object'; -import { ISchemaConfig } from './model/schema-config-interface'; -import { ISchemaExpanded } from './model/schema-expanded-interface'; -import { IStoreConfig } from './model/store-config-interface'; -import { IStore } from './model/store-interface'; -import { IStoreTargetConfig } from './model/store-target-config-interface'; -import { IStoreTarget } from './model/store-target-interface'; -import { IStoreTargetItem } from './model/store-target-item-interface'; +import { deepClone, isNull } from '../utility'; +import { StoreLogBuilder } from './builder/store-log-builder'; +import { + ISchemaConfig, + ISchemaExpanded, + IStore, + IStoreConfig, + IStoreTarget, + IStoreTargetConfig, + IStoreTargetItem +} from './model'; import { ISchema } from './schema-interface'; +import { SchemaLogConfig } from './schema-log-config'; export class Schema implements ISchema { @@ -16,6 +19,8 @@ export class Schema implements ISchema { private readonly config: ISchemaExpanded; private readonly userConfig: ISchemaConfig; + private readonly logConfig: SchemaLogConfig; + constructor(userConfig: ISchemaConfig) { this.config = {}; this.userConfig = deepClone(userConfig); @@ -24,7 +29,13 @@ export class Schema implements ISchema { const keys = Object.keys(this.userConfig); keys.filter(type => !type.startsWith('_')) - .forEach(type => this.config[type] = this.expandSchemaForType(type)); + .forEach(type => this.config[type] = this.expandSchemaForType(type)); + + this.logConfig = new SchemaLogConfig(this); + } + + public getLogConfig(): SchemaLogConfig { + return this.logConfig; } public hasType(type: string): boolean { @@ -90,10 +101,13 @@ export class Schema implements ISchema { const typeSchema = this.userConfig[type]; if (type === Schema.TYPE_DEFAULTS) { - const defaults: IStoreConfig = this.userConfig[Schema.TYPE_DEFAULTS]; + const defaults: IStoreConfig = this.userConfig[Schema.TYPE_DEFAULTS] || {}; if (defaults.targets) { defaults.targets = this.expandTargets(defaults.targets); } + if (!defaults.logging) { + defaults.logging = new StoreLogBuilder().build(); + } return deepClone(defaults); @@ -118,16 +132,17 @@ export class Schema implements ISchema { } private mergeConfigs(type: string, typeConfig: IStoreConfig, parent: IStore): IStore { - const targets = Object.assign(parent.targets, typeConfig.targets); const autoKey = isNull(typeConfig.autoKey || null) ? parent.autoKey : typeConfig.autoKey; - const result: IStore = { + const targets = Object.assign(parent.targets, typeConfig.targets); + const logging = Object.assign(parent.logging, typeConfig.logging); + + return { type: type, key: typeConfig.key || parent.key, autoKey: autoKey || false, - targets: this.expandTargets(targets) + targets: this.expandTargets(targets), + logging: logging }; - - return result; } private expandTargets(targetConfig: IStoreTargetConfig): IStoreTarget { @@ -135,8 +150,8 @@ export class Schema implements ISchema { Object.keys(targetConfig).forEach(field => { const item = targetConfig[field]; result[field] = typeof item === 'string' - ? { type: item } - : item; + ? { type: item } + : item; }); return result; diff --git a/test/data/blog-post/schema-expanded.ts b/test/data/blog-post/schema-expanded.ts index 31b45e6..7e035a5 100755 --- a/test/data/blog-post/schema-expanded.ts +++ b/test/data/blog-post/schema-expanded.ts @@ -1,10 +1,11 @@ -import { ISchemaExpanded } from '../../../lib/index'; +import { ISchemaExpanded, LogMode } from '../../../lib/index'; export const SCHEMA_EXPANDED: ISchemaExpanded = { role: { key: 'id', autoKey: true, targets: {}, + logging: { mode: LogMode.Disabled }, type: 'role' }, user: { @@ -13,6 +14,10 @@ export const SCHEMA_EXPANDED: ISchemaExpanded = { autoKey: true, targets: { role: { type: 'role' } + }, + logging: { + mode: LogMode.Disabled, + eventSelection: 'created' } }, article: { @@ -26,6 +31,10 @@ export const SCHEMA_EXPANDED: ISchemaExpanded = { cascadeRemoval: true, isArray: true } + }, + logging: { + mode: LogMode.Full, + eventSelection: ['created', 'updated', 'removed'] } }, comment: { @@ -34,6 +43,9 @@ export const SCHEMA_EXPANDED: ISchemaExpanded = { autoKey: true, targets: { author: { type: 'user' } + }, + logging: { + mode: LogMode.Simple } } }; diff --git a/test/data/blog-post/schema.ts b/test/data/blog-post/schema.ts index b714a05..f459aba 100755 --- a/test/data/blog-post/schema.ts +++ b/test/data/blog-post/schema.ts @@ -1,4 +1,4 @@ -import { ISchemaConfig } from '../../../lib/index'; +import { ISchemaConfig, LogMode } from '../../../lib/index'; export const SCHEMA: ISchemaConfig = { _defaults: { @@ -8,6 +8,9 @@ export const SCHEMA: ISchemaConfig = { _authored: { targets: { author: 'user' + }, + logging: { + mode: LogMode.Simple } }, role: true, @@ -15,6 +18,10 @@ export const SCHEMA: ISchemaConfig = { key: 'userName', targets: { role: 'role' + }, + logging: { + mode: LogMode.Disabled, + eventSelection: 'created' } }, article: { @@ -25,6 +32,10 @@ export const SCHEMA: ISchemaConfig = { cascadeRemoval: true, isArray: true } + }, + logging: { + mode: LogMode.Full, + eventSelection: ['created', 'updated', 'removed'] } }, comment: '_authored' diff --git a/test/data/user/schema-expanded.ts b/test/data/user/schema-expanded.ts index d0b8b30..9dd3e6b 100755 --- a/test/data/user/schema-expanded.ts +++ b/test/data/user/schema-expanded.ts @@ -1,4 +1,4 @@ -import { ISchemaExpanded } from '../../../lib/index'; +import { ISchemaExpanded, LogMode } from '../../../lib/index'; export const SCHEMA_EXPANDED: ISchemaExpanded = { user: { @@ -7,12 +7,14 @@ export const SCHEMA_EXPANDED: ISchemaExpanded = { autoKey: true, targets: { role: { type: 'role' } - } + }, + logging: { mode: LogMode.Disabled } }, role: { key: 'id', autoKey: true, targets: {}, + logging: { mode: LogMode.Disabled }, type: 'role' } }; diff --git a/test/schema/schema-builder.spec.ts b/test/schema/schema-builder.spec.ts index db2760e..0bd9d2a 100644 --- a/test/schema/schema-builder.spec.ts +++ b/test/schema/schema-builder.spec.ts @@ -1,5 +1,5 @@ import { assert } from 'chai'; -import { Schema, SchemaBuilder } from '../../lib/index'; +import { LogMode, Schema, SchemaBuilder, StoreLogBuilder } from '../../lib/index'; import * as Blog from '../data/blog-post'; import * as User from '../data/user'; @@ -14,11 +14,11 @@ describe('Schema-Builder', function () { it('User', function () { const schemaBuilder = new SchemaBuilder() - .setDefaultKey('id') - .setDefaultAutoKey(true); + .setDefaultKey('id') + .setDefaultAutoKey(true); schemaBuilder.setStore('user') - .setTarget('role', 'role'); + .setTarget('role', 'role'); schemaBuilder.setStore('role'); @@ -27,20 +27,33 @@ describe('Schema-Builder', function () { it('Blog Posts', function () { const schemaBuilder = new SchemaBuilder() - .setDefaultKey('id') - .setDefaultAutoKey(true); + .setDefaultKey('id') + .setDefaultAutoKey(true); schemaBuilder.setAbstractStore('authored') - .setTarget('author', 'user'); + .setTarget('author', 'user') + .setLogging(new StoreLogBuilder(LogMode.Simple).build()); schemaBuilder.setStore('role'); + const userLogConfig = new StoreLogBuilder() + .setMode(LogMode.Disabled) + .setEventSelection( 'created') + .build(); + schemaBuilder.setStore('user') - .setKey('userName') - .setTarget('role', 'role'); + .setKey('userName') + .setTarget('role', 'role') + .setLogging(userLogConfig); + + const articleLogConfig = new StoreLogBuilder() + .setMode(LogMode.Full) + .setEventSelection(['created', 'updated', 'removed']) + .build(); schemaBuilder.setStore('article', '_authored') - .setArrayTarget('comments', 'comment', true); + .setArrayTarget('comments', 'comment', true) + .setLogging(articleLogConfig); schemaBuilder.setStore('comment', '_authored');