import DataCollection = require('./DataCollection') import RootDataCollection = require('./RootDataCollection') import DataProviderConfig = require('./DataProviderConfig') import DataCollectionChangeProvider = require('./DataCollectionChangeProvider') import DataCollectionChangeListener = require('./DataCollectionChangeListener') import DataModel = require('./DataModel') import DataCollectionOptions = require('./DataCollectionOptions'); import FilterCollection = require('./FilterCollection'); import DataCollectionFactory = require('./DataCollectionFactory'); import ObjectMap = require('./ObjectMap'); import DataProviderState = require('./DataProviderState'); import ActionUrl = require('./ActionUrl'); const DEFAULT_SCOPE_NAME = 'index' const CLIENT_ID_ATTRIBUTE = 'bitpuls_client_id' class DataProvider implements DataCollectionChangeProvider, DataCollectionChangeListener { private state:DataProviderState = 'not_inited' private config:DataProviderConfig private rootDataCollectionsByScope:{[scope:string]: RootDataCollection} = {} private dataCollectionFactory:DataCollectionFactory private allEntities:{[s: string]: T} = {} private changeListeners:Array> = new Array() private dataModelClass:{ new(properties:ObjectMap, dataProvider:DataProvider, isNewInstance?:boolean): T, computeIdentityHashCode(dataModel:DataModel|ObjectMap, dataProviderConfig:DataProviderConfig):string } constructor(dataCollectionFactory:DataCollectionFactory, config:DataProviderConfig, dataModelClass:{ new(properties:ObjectMap): T, computeIdentityHashCode(dataModel:DataModel|ObjectMap, dataProviderConfig:DataProviderConfig):string }) { this.dataCollectionFactory = dataCollectionFactory this.config = config this.dataModelClass = dataModelClass } public getConfig() { return this.config } dataCollectionChanged(dataCollection:DataCollection) { this.changeListeners.forEach((listener) => { listener.dataCollectionChanged(dataCollection) }) } getState():DataProviderState { return this.state } public addChangeListener(listener:DataCollectionChangeListener) { this.changeListeners.push(listener) } public removeChangeListener(listener:DataCollectionChangeListener) { let index = this.changeListeners.indexOf(listener) if (index > -1) { this.changeListeners.splice(index, 1 ); } } private loadIndexData(scopeName:string) { let scopeOrUrl = this.config.getScopes()[scopeName] let indexUrl if (typeof scopeOrUrl == 'string') { indexUrl = scopeOrUrl as string } else { indexUrl = (scopeOrUrl as {url:string})['url'] let initialEntities = (scopeOrUrl as {initialEntities:[ObjectMap]})['initialEntities'] if (initialEntities) { initialEntities.forEach((objectMap) => { this.createDataModel(objectMap) }) // TODO: Wie kriegen diese initialen Einträge aus dem JSON die Verbindung zu einem Scope? } } if (indexUrl) { // // FIXME: Muss über DataProviderConfig realisiert werden // let todayMidnight = DateHelper.dateToMidnight(new Date()) // let dateBegin = DateHelper.getCopyAddDays(todayMidnight, 0) // let dateEnd = DateHelper.getCopyAddDays(todayMidnight, 1) // indexUrl = indexUrl.replace('${date_begin}', dateBegin.toString()).replace('${date_end}', dateEnd.toString()) this.config.getBackendConnector().get(indexUrl).done((objectMaps:ObjectMap[]) => { let entities = new Array() if (objectMaps) { objectMaps.forEach((objectMap) => { entities.push(this.createDataModel(objectMap)) }) } // FIXME: Instead mergeEntities this.buildRootDataCollection(scopeName).setEntities(entities) }) } } public delete(dataModel:DataModel) { let identityHashCode = dataModel.computeIdentityHashCode() this.deleteIntern(identityHashCode) let destroyUrl:ActionUrl = this.config.getActionUrlSet().getActionUrl('destroy', dataModel) if (destroyUrl) { this.doServerRequest(destroyUrl, {}, (objectMap:ObjectMap) => { this.deleteIntern(identityHashCode) }) // TODO: What to do, if deletion fails on server side? } else { throw new Error(`There is no destroy possible for ${this.config.getDataProviderName()}`) } } private deleteIntern(identityHashCode:string) { if (this.allEntities[identityHashCode]) { delete this.allEntities[identityHashCode] Object.keys(this.rootDataCollectionsByScope).forEach((key) => { // FIXME: This MUST be done by using remove instead writing ALL entities to every scope this.rootDataCollectionsByScope[key].setEntities(this.getAllEntities()) }) } } getAllEntities():T[] { let dataModels:T[] = [] Object.keys(this.allEntities).forEach((key) => { dataModels.push(this.allEntities[key]) }) return dataModels } public find(dataProviderName:string, searchMap:{[s:string]: Object}|string):T { let collection = this.dataCollectionFactory.createCollection(dataProviderName) return collection.find(searchMap) as T } private writeInstanceToServer(dataModel:T) { let createUrl:ActionUrl = this.config.getActionUrlSet().getActionUrl('create', dataModel) if (createUrl) { let payload = this.config.prepareForServer(dataModel) this.doServerRequest(createUrl, payload, (objectMaps:ObjectMap) => { dataModel.mergeChanges(objectMaps) delete this.allEntities[CLIENT_ID_ATTRIBUTE] dataModel.removeProperty(CLIENT_ID_ATTRIBUTE) this.allEntities[dataModel.computeIdentityHashCode()] = dataModel }) } else { throw new Error(`There is no update possible for ${this.config.getDataProviderName()}`) } } private tryWriteChangeToServer(dataModel:DataModel) { let updateUrl:ActionUrl = this.config.getActionUrlSet().getActionUrl('update', dataModel) if (updateUrl) { let payload = this.config.prepareForServer(dataModel) this.doServerRequest(updateUrl, payload, (objectMaps:ObjectMap|ObjectMap[]) => { if (objectMaps.constructor == Array) { (objectMaps as ObjectMap[]).forEach((objectMap:ObjectMap) => { this.createDataModel(objectMap) }) } else { this.createDataModel(objectMaps as ObjectMap) } }) } else { throw new Error(`There is no update possible for ${this.config.getDataProviderName()}`) } } public doServerRequest(actionUrl:ActionUrl, payload:ObjectMap, callback:(object:Object) => void) { let backendConnector = this.config.getBackendConnector() let apiMethod = backendConnector.getConcreteRequestMethod(actionUrl.method) apiMethod.apply(backendConnector, [actionUrl.url, payload]).done(callback) } public createDataModel(objectMap:ObjectMap, isBuild?:boolean):T { let identityHashCode = this.dataModelClass.computeIdentityHashCode(objectMap, this.config) let dataModel = this.allEntities[identityHashCode] if (!dataModel) { dataModel = new this.dataModelClass(objectMap, this, isBuild) identityHashCode = dataModel.computeIdentityHashCode() this.allEntities[identityHashCode] = dataModel if (isBuild) { this.getConfig().getNewInstanceDataModelScopeNames().forEach((scopeName) => { this.buildRootDataCollection(scopeName).addDataModel(dataModel) }) } } else { dataModel.mergeChanges(objectMap) } return dataModel } dataModelAsksForSave(dataModel:T) { if (dataModel.isMarkedForDeletion()) { this.delete(dataModel) } else if (dataModel.hasProperty(CLIENT_ID_ATTRIBUTE)) { // debugger this.writeInstanceToServer(dataModel) } else { this.tryWriteChangeToServer(dataModel) } } private buildRootDataCollection(scopeName:string) { if (!this.hasRootDataCollection(scopeName)) { this.rootDataCollectionsByScope[scopeName] = new RootDataCollection({ dataProvider: this, changeProvider: this, scope: scopeName }) } return this.rootDataCollectionsByScope[scopeName] } private hasRootDataCollection(scopeName:string):boolean { return this.rootDataCollectionsByScope.hasOwnProperty(scopeName) } createCollection(options:DataCollectionOptions):DataCollection { if (!options.scope) { options.scope = DEFAULT_SCOPE_NAME } let shouldLoad = (this.config.getInitialRequestMoment() == 'on_first_collection') && !this.hasRootDataCollection(options.scope) let rootDataCollection = this.buildRootDataCollection(options.scope) let dataCollection:DataCollection = new DataCollection({ dataProvider: this, changeProvider: rootDataCollection, scope: options.scope, initialEntities: rootDataCollection.getFilteredDataModels(), changeListener: options.dataCollectionChangeListener }) if (shouldLoad) { this.loadIndexData(options.scope) } return dataCollection } filtersChanged(scope:string, filtersChanged:FilterCollection) { // TODO: Look into data and decide, wether there should be a new query against the server //this.loadIndexData(scope) } } export = DataProvider