├── source ├── filters │ ├── index.ts │ ├── filter.ts │ ├── filter.tests.ts │ └── filters.md ├── types │ ├── index.ts │ ├── compareResult.ts │ └── itemList.ts ├── services │ ├── test │ │ ├── index.ts │ │ ├── chaiMoment.tests.ts │ │ ├── chaiMoment.ts │ │ └── angularFixture.ts │ ├── date │ │ ├── index.ts │ │ ├── dateTimeFormatStrings.ts │ │ └── date.md │ ├── fileSize │ │ ├── index.ts │ │ ├── fileSize.pipe.ts │ │ ├── fileSize.service.tests.ts │ │ ├── fileSize.md │ │ └── fileSize.service.ts │ ├── boolean │ │ ├── boolean.md │ │ ├── boolean.service.ts │ │ └── boolean.service.tests.ts │ ├── window │ │ └── window.provider.ts │ ├── logger │ │ ├── logger.service.ts │ │ └── logger.service.tests.ts │ ├── guid │ │ └── guid.service.ts │ ├── dataContracts │ │ ├── dataContractsHelper │ │ │ ├── dataContractsHelper.service.tests.ts │ │ │ └── dataContractsHelper.service.ts │ │ ├── converters │ │ │ ├── dateConverter │ │ │ │ ├── dateConverter.ts │ │ │ │ └── dateConverter.tests.ts │ │ │ ├── enumConverter │ │ │ │ ├── enumConverter.ts │ │ │ │ └── enumConverter.tests.ts │ │ │ ├── timeConverter │ │ │ │ ├── timeConverter.ts │ │ │ │ └── timeConverter.tests.ts │ │ │ ├── aliasConverter │ │ │ │ ├── aliasConverter.ts │ │ │ │ └── aliasConverter.tests.ts │ │ │ ├── converters.ts │ │ │ └── converters.tests.ts │ │ ├── singletonDataService │ │ │ ├── parent │ │ │ │ ├── parentSingletonDataService.md │ │ │ │ └── parentSingletonData.service.ts │ │ │ ├── singletonDataService.md │ │ │ └── singletonData.service.ts │ │ ├── index.ts │ │ ├── contractLibrary │ │ │ ├── dataServiceMocks.ts │ │ │ ├── contractLibrary.md │ │ │ └── contractLibrary.tests.ts │ │ ├── dataService │ │ │ ├── dataService.md │ │ │ ├── parent │ │ │ │ ├── parentData.service.ts │ │ │ │ ├── parentData.service.tests.ts │ │ │ │ └── parentDataService.md │ │ │ └── view │ │ │ │ ├── dataServiceView.md │ │ │ │ ├── parentDataServiceView.md │ │ │ │ └── dataServiceView.ts │ │ ├── resourceBuilder │ │ │ └── resourceBuilder.md │ │ ├── dataContracts.md │ │ └── baseDataService.md │ ├── validation │ │ ├── emailValidation.service.ts │ │ ├── validationTypes.ts │ │ ├── compositeValidator.ts │ │ ├── observableValidator.ts │ │ ├── validator.ts │ │ ├── observableValidator.tests.ts │ │ ├── validation.service.ts │ │ └── emailValidation.service.tests.ts │ ├── object │ │ ├── object.md │ │ └── object.service.ts │ ├── transform │ │ ├── transform.service.ts │ │ └── transform.service.tests.ts │ ├── digest │ │ └── digest.service.ts │ ├── notification │ │ ├── notification.service.ts │ │ └── notification.service.tests.ts │ ├── redirect │ │ ├── redirect.service.ts │ │ └── redirect.service.tests.ts │ ├── string │ │ ├── string.service.ts │ │ └── string.service.tests.ts │ ├── number │ │ ├── number.service.ts │ │ └── number.service.tests.ts │ ├── synchronizedRequests │ │ ├── synchronizedRequests.service.tests.ts │ │ └── synchronizedRequests.service.ts │ ├── genericSearchFilter │ │ ├── genericSearchFilter.md │ │ └── genericSearchFilter.service.ts │ ├── timeout │ │ ├── timeout.service.ts │ │ └── timeout.service.tests.ts │ ├── array │ │ ├── array.md │ │ └── array.service.ts │ ├── search │ │ ├── search.service.ts │ │ └── search.service.tests.ts │ ├── timezone │ │ ├── timezone.service.ts │ │ ├── timezone.enum.tests.ts │ │ ├── timezone.enum.ts │ │ └── timezone.service.tests.ts │ ├── observable │ │ ├── observable.service.ts │ │ └── observable.service.tests.ts │ ├── index.ts │ ├── time │ │ ├── time.service.tests.ts │ │ └── time.service.ts │ └── errorHandler │ │ ├── errorHandler.service.tests.ts │ │ └── errorHandler.md ├── vendor.ts ├── main.ts ├── utilities.module.ts └── utilitiesDowngrade.ts ├── config ├── karma.conf.js ├── karma.full.conf.js ├── karma.debug.conf.js ├── karma.tc.conf.js ├── karma-test-shim.js └── karma.shared.conf.js ├── .editorconfig ├── gulpfile.js ├── typings ├── globals.d.ts ├── jasmine │ └── jasmine.d.ts └── chai │ └── chaiAssertions.d.ts ├── .gitignore ├── .npmignore ├── tsconfig.json ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── system.config.js ├── tslint.json └── package.json /source/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './filter'; 2 | -------------------------------------------------------------------------------- /source/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compareResult'; 2 | export * from './itemList'; 3 | -------------------------------------------------------------------------------- /source/services/test/index.ts: -------------------------------------------------------------------------------- 1 | import './chaiMoment'; 2 | 3 | export * from './angularFixture'; 4 | -------------------------------------------------------------------------------- /source/vendor.ts: -------------------------------------------------------------------------------- 1 | // RxJS 2 | import 'rxjs/add/operator/map'; 3 | import 'rxjs/add/operator/toPromise'; 4 | -------------------------------------------------------------------------------- /source/services/date/index.ts: -------------------------------------------------------------------------------- 1 | export * from './date.service'; 2 | export * from './dateTimeFormatStrings'; 3 | -------------------------------------------------------------------------------- /source/services/fileSize/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fileSize.service'; 2 | export * from './fileSize.pipe'; 3 | -------------------------------------------------------------------------------- /config/karma.conf.js: -------------------------------------------------------------------------------- 1 | var baseConfig = require('./karma.shared.conf'); 2 | 3 | module.exports = function (karma) { 4 | karma.set(baseConfig(karma)); 5 | }; 6 | -------------------------------------------------------------------------------- /source/services/boolean/boolean.md: -------------------------------------------------------------------------------- 1 | ## boolean 2 | Service for interacting with boolean primitives in JS / TypeScript. 3 | 4 | #### `toBool(object: any): boolean` 5 | Converts any object to a boolean value. 6 | -------------------------------------------------------------------------------- /config/karma.full.conf.js: -------------------------------------------------------------------------------- 1 | var baseConfig = require('./karma.shared.conf'); 2 | 3 | module.exports = function (karma) { 4 | var config = baseConfig(karma); 5 | config.browsers = ['ChromeNoSandbox', 'Firefox']; 6 | karma.set(config); 7 | }; 8 | -------------------------------------------------------------------------------- /config/karma.debug.conf.js: -------------------------------------------------------------------------------- 1 | var baseConfig = require('./karma.shared.conf'); 2 | 3 | module.exports = function (karma) { 4 | var config = baseConfig(karma); 5 | config.autoWatch = true; 6 | config.singleRun = false; 7 | karma.set(config); 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = tab 7 | indent_size = 4 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /source/services/window/window.provider.ts: -------------------------------------------------------------------------------- 1 | import { ValueProvider } from '@angular/core'; 2 | 3 | export abstract class WindowWrapper { } 4 | 5 | export const WINDOW_PROVIDER: ValueProvider = { 6 | provide: WindowWrapper, 7 | useValue: window, 8 | }; 9 | -------------------------------------------------------------------------------- /source/services/boolean/boolean.service.ts: -------------------------------------------------------------------------------- 1 | export interface IBooleanUtility { 2 | toBool(object: any): boolean; 3 | } 4 | 5 | export class BooleanUtility implements IBooleanUtility { 6 | toBool(object: any): boolean { 7 | return !!object; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var del = require('del'); 3 | 4 | var utilities = require('@renovolive/gulp-utilities'); 5 | utilities.gulp.clean.config(); 6 | utilities.gulp.version.config(); 7 | 8 | gulp.task('wipe-npm', () => { 9 | return del('node_modules'); 10 | }); 11 | -------------------------------------------------------------------------------- /typings/globals.d.ts: -------------------------------------------------------------------------------- 1 | import { SinonStatic } from 'sinon'; 2 | 3 | declare global { 4 | const sinon: SinonStatic; 5 | } 6 | 7 | import * as moment from "moment"; 8 | 9 | declare module "moment" { 10 | export type MomentFormatSpecification = string; 11 | } 12 | 13 | export = moment; 14 | -------------------------------------------------------------------------------- /source/services/logger/logger.service.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | log(message: any): void; 3 | } 4 | 5 | export class Logger { 6 | private console: Console; 7 | 8 | constructor() { 9 | this.console = console; 10 | } 11 | 12 | log(message: any): void { 13 | this.console.log(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /source/main.ts: -------------------------------------------------------------------------------- 1 | import * as filters from './filters/index'; 2 | import * as services from './services/index'; 3 | import * as types from './types/index'; 4 | import * as downgrade from './utilitiesDowngrade'; 5 | 6 | import './vendor'; 7 | 8 | export { filters, services, types, downgrade }; 9 | export * from './utilities.module'; 10 | -------------------------------------------------------------------------------- /source/services/guid/guid.service.ts: -------------------------------------------------------------------------------- 1 | import { UUID } from 'angular2-uuid'; 2 | 3 | export interface IGuidService { 4 | random(): string; 5 | } 6 | 7 | export class GuidService implements IGuidService { 8 | random(): string { 9 | return UUID.UUID(); 10 | } 11 | } 12 | 13 | export const guid: GuidService = new GuidService(); 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Definitively typed files (from typings) 2 | /typings/globals/ 3 | /typings/modules/ 4 | /typings/index.d.ts 5 | 6 | # Don't commit built files (*.js, *.d.ts) 7 | /source/**/*.js 8 | /source/**/*.d.ts 9 | 10 | # Node deps 11 | npm-debug.log 12 | /node_modules/ 13 | 14 | # Ignore git conflict / diff files 15 | *.orig 16 | 17 | # Ignore test artifacts 18 | /tests/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignore test files 2 | source/**/*.tests.js 3 | source/*.tests.d.ts 4 | karma.* 5 | test-bootstrapper.js 6 | 7 | # NPM 8 | /node_modules/ 9 | npm-debug.log 10 | 11 | # Ignore webpack settings 12 | webpack.* 13 | 14 | # Git 15 | *.orig 16 | 17 | # Don't need external typings on npm 18 | /typings/ 19 | 20 | #exlude the .ts files from dist 21 | *.ts 22 | !*.d.ts -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "declaration": true, 6 | "inlineSourceMap": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "target": "ES5", 10 | "typeRoots": [ 11 | "./node_modules/@types" 12 | ] 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | ".vscode", 17 | "output" 18 | ] 19 | } -------------------------------------------------------------------------------- /config/karma.tc.conf.js: -------------------------------------------------------------------------------- 1 | var teamCityReporter = require('karma-teamcity-reporter'); 2 | var baseConfig = require('./karma.shared.conf'); 3 | 4 | module.exports = function (karma) { 5 | var config = baseConfig(karma); 6 | config.browsers = ['ChromeNoSandbox', 'Firefox']; 7 | config.reporters = ['teamcity']; 8 | 9 | config.plugins = config.plugins || []; 10 | config.plugins.push(teamCityReporter); 11 | karma.set(config); 12 | }; 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.trimTrailingWhitespace": true, 4 | "editor.tabSize": 4, 5 | "editor.formatOnType": true, 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/.DS_Store": true, 9 | "**/output/": true, 10 | "**/tests/": true, 11 | "source/**/*.js": true, 12 | "source/**/*.js.map": true, 13 | "source/**/*.d.ts": true 14 | } 15 | } -------------------------------------------------------------------------------- /source/types/compareResult.ts: -------------------------------------------------------------------------------- 1 | export enum CompareResult { 2 | greater = 1, 3 | equal = 0, 4 | less = -1, 5 | invalid = null, 6 | } 7 | 8 | export function getCompareResult(num: number): CompareResult { 9 | if (num == null) { 10 | return CompareResult.invalid; 11 | } 12 | 13 | if (num === 0) { 14 | return CompareResult.equal; 15 | } else if (num > 0) { 16 | return CompareResult.greater; 17 | } else { 18 | return CompareResult.less; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /source/services/date/dateTimeFormatStrings.ts: -------------------------------------------------------------------------------- 1 | export var dateTimeFormatServiceName: string = 'dateTimeFormatStrings'; 2 | 3 | export interface IDateFormatStrings { 4 | isoFormat: string; 5 | dateTimeFormat: string; 6 | dateFormat: string; 7 | timeFormat: string; 8 | } 9 | 10 | export var defaultFormats: IDateFormatStrings = { 11 | isoFormat: 'YYYY-MM-DDTHH:mm:ssZ', 12 | dateTimeFormat: 'MM/DD/YYYY h:mm A', 13 | dateFormat: 'MM/DD/YYYY', 14 | timeFormat: 'h:mmA', 15 | }; 16 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataContractsHelper/dataContractsHelper.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { helper } from './dataContractsHelper.service'; 2 | 3 | describe('data contracts helper', () => { 4 | it('should version the url', (): void => { 5 | expect(helper.versionEndpoint('/api/test', 2)).to.equal('/api/v2/test'); 6 | }); 7 | 8 | it('should replace the previous version if one is already specified', (): void => { 9 | expect(helper.versionEndpoint('api/v2/test', 3)).to.equal('api/v3/test'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /typings/jasmine/jasmine.d.ts: -------------------------------------------------------------------------------- 1 | declare var describe: { (name: string, specs: { (): any }) }; 2 | declare var it: { (name: string, spec: { (done?: Function): any }) }; 3 | declare var fdescribe: { (name: string, specs: { (): any }) }; 4 | declare var fit: { (name: string, spec: { (done?: Function): any }) }; 5 | declare var xdescribe: { (name: string, specs: { (): any }) }; 6 | declare var xit: { (name: string, spec: { (done?: Function): any }) }; 7 | declare var beforeEach: { (setup: { (): any }) }; 8 | declare var afterEach: { (cleanup: { (): any }) }; 9 | -------------------------------------------------------------------------------- /source/services/logger/logger.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { ILogger, Logger } from './logger.service'; 2 | 3 | describe('logger', () => { 4 | let logger: ILogger; 5 | 6 | beforeEach(() => { 7 | logger = new Logger(); 8 | }); 9 | 10 | it('should log to the console', (): void => { 11 | const originalLog = console.log; 12 | const logSpy: sinon.SinonSpy = sinon.spy(); 13 | console.log = logSpy; 14 | 15 | logger.log('message'); 16 | sinon.assert.calledOnce(logSpy); 17 | sinon.assert.calledWith(logSpy, 'message'); 18 | 19 | console.log = originalLog; 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /typings/chai/chaiAssertions.d.ts: -------------------------------------------------------------------------------- 1 | declare var expect: Chai.ExpectStatic; 2 | declare var assert: Chai.AssertStatic; 3 | 4 | declare module Chai { 5 | interface ChaiStatic { 6 | Assertion: Assertion; 7 | } 8 | 9 | interface Assertion extends MomentAssertion { 10 | addMethod: Function; 11 | } 12 | 13 | interface MomentAssertion { 14 | sameMoment(value: any, message?: string): Assertion; 15 | equalMoment(value: any, message?: string): Assertion; 16 | afterMoment(value: any, message?: string): Assertion; 17 | beforeMoment(value: any, message?: string): Assertion; 18 | } 19 | } -------------------------------------------------------------------------------- /source/services/dataContracts/converters/dateConverter/dateConverter.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | 3 | import { IConverter } from '../converters'; 4 | import { dateUtility, defaultFormats } from '../../../date/index'; 5 | 6 | export { defaultFormats }; 7 | 8 | export const dateConverter: IConverter = { 9 | fromServer(raw: string): moment.Moment { 10 | return dateUtility.getDateFromISOString(raw); 11 | }, 12 | toServer(data: moment.Moment): string { 13 | return data != null 14 | ? moment(data).format(defaultFormats.isoFormat) 15 | : null; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /source/services/validation/emailValidation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | export interface IEmailValidationService { 4 | 5 | /** 6 | * Accepts a string to validate if the format is that of a valid email address 7 | */ 8 | isValidEmailAddress(emailAddress: string): boolean; 9 | } 10 | 11 | @Injectable() 12 | export class EmailValidationService implements IEmailValidationService { 13 | 14 | isValidEmailAddress(emailAddress: string): boolean { 15 | const validEmailPattern: RegExp = /^([^.\@])([\w.\+\.\-]+)@([\w\-]+)(\.([\w\-]+))*((\.(\w){2,3})+)/i; 16 | 17 | return validEmailPattern.test(emailAddress); 18 | } 19 | } -------------------------------------------------------------------------------- /source/services/dataContracts/converters/enumConverter/enumConverter.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | 3 | import { IConverter } from '../converters'; 4 | import { IItemList, IItem } from '../../../../types/itemList'; 5 | 6 | export { IConverter }; 7 | 8 | export class EnumConverter implements IConverter { 9 | constructor(private enumType: IItemList) {} 10 | 11 | fromServer: { (raw: number): TItemType } = (raw: number): TItemType => { 12 | return this.enumType.get(raw); 13 | } 14 | toServer: { (data: TItemType): number } = (data: TItemType): number => { 15 | return data != null 16 | ? data.value 17 | : null; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /source/services/boolean/boolean.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { IBooleanUtility, BooleanUtility } from './boolean.service'; 2 | 3 | describe('booleanUtility', () => { 4 | var booleanUtility: IBooleanUtility; 5 | 6 | beforeEach(() => { 7 | booleanUtility = new BooleanUtility(); 8 | }); 9 | 10 | describe('toBool', (): void => { 11 | it('should convert null and undefined to false', (): void => { 12 | expect(booleanUtility.toBool(null)).to.be.false; 13 | expect(booleanUtility.toBool(undefined)).to.be.false; 14 | }); 15 | 16 | it('should leave bool values unchanged', (): void => { 17 | expect(booleanUtility.toBool(false)).to.be.false; 18 | expect(booleanUtility.toBool(true)).to.be.true; 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /source/services/fileSize/fileSize.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | import { INumberUtility, NumberUtility } from '../number/number.service'; 4 | 5 | import { FileSize } from './fileSize.service'; 6 | 7 | // Formats and optionally truncates and ellipsimogrifies a string for display in a card header 8 | 9 | @Pipe({ name: 'fileSize'}) 10 | export class FileSizePipe implements PipeTransform { 11 | private numberUtility: INumberUtility; 12 | 13 | constructor(numberUtility: NumberUtility) { 14 | this.numberUtility = numberUtility; 15 | } 16 | 17 | transform(bytes: number): string { 18 | var fileSize: FileSize = new FileSize(this.numberUtility, bytes); 19 | return fileSize.display(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /source/utilities.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ValueProvider, ErrorHandler } from '@angular/core'; 2 | import { RLHttpModule } from 'rl-http'; 3 | 4 | import { observableToken, ObservableService, IObservableService } from './services/observable/observable.service'; 5 | import { UTILITY_PROVIDERS } from './services/index'; 6 | 7 | const observableFactoryProvider: ValueProvider = { 8 | provide: observableToken, 9 | useValue: { 10 | deps: [ErrorHandler], 11 | getInstance: (errorHandler: ErrorHandler): IObservableService => new ObservableService(errorHandler), 12 | }, 13 | }; 14 | 15 | @NgModule({ 16 | imports: [RLHttpModule], 17 | providers: [UTILITY_PROVIDERS, observableFactoryProvider], 18 | }) 19 | export class UtilitiesModule { } 20 | -------------------------------------------------------------------------------- /source/services/dataContracts/converters/timeConverter/timeConverter.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | 3 | import { IConverter } from '../converters'; 4 | import { defaultFormats } from '../../../date/index'; 5 | import { timezoneService } from '../../../timezone/timezone.service'; 6 | 7 | export { defaultFormats }; 8 | 9 | export const timeConverter: IConverter = { 10 | fromServer(raw: string): moment.Moment { 11 | return raw != null 12 | ? timezoneService.buildMomentWithTimezone(raw, timezoneService.currentTimezone, defaultFormats.timeFormat) 13 | : null; 14 | }, 15 | toServer(data: moment.Moment): string { 16 | return data != null 17 | ? moment(data).format(defaultFormats.timeFormat) 18 | : null; 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /source/services/object/object.md: -------------------------------------------------------------------------------- 1 | ## object 2 | Contains generic operations for any objects. 3 | 4 | #### `isNullOrEmpty(object: any): boolean` 5 | Returns true if the object is null. 6 | Returns true if the object is an empty array. 7 | Returns true if the object is NaN. 8 | Returns true if the object is an empty string. 9 | 10 | ####`isNullOrWhitespace(object: any): boolean` 11 | If the object is a string, first trims it then passes the object into isNullOrEmpty(). 12 | 13 | ####`areEqual(obj1: any, obj2: any): boolean` 14 | Recursively compares all of the properties on the two objects and returns true if they are equal. 15 | 16 | ####`toString(object: any) string` 17 | Converts any object to a string. 18 | 19 | ####`valueOrDefault(value: any, defaultValue: any): any` 20 | If the value is not null, returns it, otherwise returns the default value. 21 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataContractsHelper/dataContractsHelper.service.ts: -------------------------------------------------------------------------------- 1 | export interface IDataContractsHelper { 2 | versionEndpoint(endpoint: string, versionNumber: number): string; 3 | } 4 | 5 | class DataContractsHelper implements IDataContractsHelper { 6 | versionEndpoint(endpoint: string, versionNumber: number): string { 7 | let versionExpression: RegExp = /\/v\d+\//; 8 | let apiExpression: RegExp = /\/api\//; 9 | 10 | let versionString: string = 'v' + versionNumber; 11 | 12 | let searchResult = endpoint.search(versionExpression); 13 | if (searchResult !== -1) { 14 | return endpoint.replace(versionExpression, '/' + versionString + '/'); 15 | } else { 16 | return endpoint.replace(apiExpression, '/api/' + versionString + '/'); 17 | } 18 | } 19 | } 20 | 21 | export let helper: IDataContractsHelper = new DataContractsHelper(); -------------------------------------------------------------------------------- /source/services/transform/transform.service.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | export type ITransform = string | { (item: TItemType): TReturnType }; 4 | 5 | export interface ITransformService { 6 | getValue(item: TItemType, transform: ITransform); 7 | } 8 | 9 | export class TransformService implements ITransformService { 10 | getValue(item: TItemType, transform: ITransform) { 11 | if (item == null) { 12 | return null; 13 | } 14 | 15 | if (transform == null) { 16 | return item; 17 | } 18 | 19 | return _.isFunction(transform) 20 | ? (<{ (item: TItemType): TReturnType }>transform)(item) 21 | : item[transform]; 22 | } 23 | } 24 | 25 | export const transform: TransformService = new TransformService(); 26 | -------------------------------------------------------------------------------- /source/services/digest/digest.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as angular from 'angular'; 3 | 4 | declare const $: any; 5 | 6 | export interface IDigestService { 7 | runDigestCycle(): void; 8 | } 9 | 10 | @Injectable() 11 | export class DigestService implements IDigestService { 12 | private $applyAsync: Function = null; 13 | 14 | /* 15 | * TODO: Remove when ng1 is dead 16 | */ 17 | runDigestCycle(): void { 18 | if (this.$applyAsync == null) { 19 | let elem: ng.IAugmentedJQuery = angular.element($(".ng-scope")); 20 | let scope: ng.IScope = elem && elem.scope(); 21 | 22 | this.$applyAsync = scope && scope.$root && scope.$root.$applyAsync; 23 | 24 | if (this.$applyAsync == null) { 25 | console.error('Scope not found!') 26 | return; 27 | } 28 | } 29 | this.$applyAsync(); 30 | } 31 | } -------------------------------------------------------------------------------- /source/services/dataContracts/singletonDataService/parent/parentSingletonDataService.md: -------------------------------------------------------------------------------- 1 | ## Parent Singleton Data service 2 | Specifies a data contract that points to a single object on the server that is part of a hierarchy. 3 | 4 | Extends [singletonDataService](../singletonDataService.md). 5 | 6 | ### Options 7 | 8 | `IChildResources`, `resourceDictionaryBuilder` 9 | 10 | See [parentDataService](../../dataService/parent/parentDataService.md) for details on the parent options. 11 | 12 | `endpoint`, `interface`, `useMock`, `logRequests`, `searchParams`, `transform`. 13 | 14 | See [dataContract](../../baseDataService.md) for details on the base options. 15 | 16 | ### Interface 17 | The following functions are available for consumers of the parent singleton data service. 18 | 19 | `childContracts`. See [parentDataService](../../dataService/parent/parentDataService.md). 20 | 21 | `get`, `update`, `version`. 22 | 23 | See [singletonDataService](../singletonDataService.md) for details on the base options. 24 | -------------------------------------------------------------------------------- /source/services/dataContracts/converters/aliasConverter/aliasConverter.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { IConverter } from '../converters'; 4 | 5 | export class AliasConverter implements IConverter { 6 | constructor(private alias: string 7 | , private composedConverter?: IConverter) { } 8 | 9 | fromServer: { (raw: any, parent: any): TDataType } = (raw: any, parent: any): TDataType => { 10 | if (!_.has(parent, this.alias)) { 11 | return null; 12 | } 13 | 14 | raw = parent[this.alias]; 15 | delete parent[this.alias]; 16 | 17 | if (this.composedConverter != null) { 18 | return this.composedConverter.fromServer(raw, parent); 19 | } 20 | 21 | return raw; 22 | } 23 | 24 | toServer: {(data: TDataType, parent: any): any} = (data: TDataType, parent: any): any => { 25 | if (this.composedConverter != null) { 26 | data = this.composedConverter.toServer(data, parent); 27 | } 28 | 29 | parent[this.alias] = data; 30 | 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /source/services/dataContracts/converters/dateConverter/dateConverter.tests.ts: -------------------------------------------------------------------------------- 1 | import { dateConverter, defaultFormats } from './dateConverter'; 2 | import { timezones } from '../../../timezone/timezone.enum'; 3 | 4 | import * as moment from 'moment'; 5 | import 'moment-timezone'; 6 | 7 | describe('dateConverter', (): void => { 8 | it('should get the date from an ISO string', (): void => { 9 | let expectedDate: moment.Moment = moment('2015-11-24T20:12:00-06:00', defaultFormats.isoFormat).tz(timezones.CST.momentName); 10 | expect(dateConverter.fromServer('2015-11-24T20:12:00-06:00')).to.deep.equal(expectedDate); 11 | }); 12 | 13 | it('should convert the date to an ISO string', (): void => { 14 | let date: moment.Moment = moment('2015-11-24T20:12:00-06:00', defaultFormats.isoFormat).tz(timezones.CST.momentName); 15 | expect(dateConverter.toServer(date)).to.equal('2015-11-24T20:12:00-06:00'); 16 | }); 17 | 18 | it('should handle nulls', (): void => { 19 | expect(dateConverter.toServer(null)).to.be.null; 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /source/services/dataContracts/index.ts: -------------------------------------------------------------------------------- 1 | import { ResourceBuilder } from './resourceBuilder/resourceBuilder.service'; 2 | import { DataServiceFactory } from './dataService/data.service'; 3 | import { SingletonDataServiceFactory } from './singletonDataService/singletonData.service'; 4 | 5 | import * as converters from './converters/converters'; 6 | import * as mocks from './contractLibrary/dataServiceMocks'; 7 | 8 | export * from './contractLibrary/contractLibrary'; 9 | export * from './dataService/data.service'; 10 | export * from './dataService/view/dataServiceView'; 11 | export * from './dataService/parent/parentData.service'; 12 | export * from './singletonDataService/singletonData.service'; 13 | export * from './singletonDataService/parent/parentSingletonData.service'; 14 | export * from './resourceBuilder/resourceBuilder.service'; 15 | export { converters, mocks }; 16 | 17 | export const DATA_CONTRACT_PROVIDERS: any[] = [ 18 | ResourceBuilder, 19 | DataServiceFactory, 20 | SingletonDataServiceFactory, 21 | converters.ConverterService, 22 | ]; 23 | -------------------------------------------------------------------------------- /source/services/fileSize/fileSize.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { FileSize } from './fileSize.service'; 2 | import { numberUtility } from '../number/number.service'; 3 | 4 | describe('fileSize', () => { 5 | it('should determine bytes', (): void => { 6 | expect(new FileSize(numberUtility, 1).display()).to.equal('1 bytes'); 7 | expect(new FileSize(numberUtility, 1023).display()).to.equal('1023 bytes'); 8 | }); 9 | 10 | it('should determine kilo bytes', (): void => { 11 | expect(new FileSize(numberUtility, 1024).display()).to.equal('1 KB'); 12 | expect(new FileSize(numberUtility, 1048575).display()).to.equal('1024 KB'); 13 | }); 14 | 15 | it('should determine mega bytes', (): void => { 16 | expect(new FileSize(numberUtility, 1048576).display()).to.equal('1 MB'); 17 | expect(new FileSize(numberUtility, 1073741823).display()).to.equal('1024 MB'); 18 | }); 19 | 20 | it('should determine giga bytes', (): void => { 21 | expect(new FileSize(numberUtility, 1073741824).display()).to.equal('1 GB'); 22 | expect(new FileSize(numberUtility, 1073741825).display()).to.equal('1 GB'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /source/services/dataContracts/singletonDataService/singletonDataService.md: -------------------------------------------------------------------------------- 1 | ## Singleton Data service 2 | Specifies a data contract that points to a single object on the server. Supports get and update requests. 3 | 4 | ### Options 5 | 6 | #### `mockData` 7 | `mockData` functions the same as the default `mockData` option except that only a single object is provided. 8 | 9 | `endpoint`, `interface`, `useMock`, `logRequests`, `searchParams`, `transform`. 10 | 11 | See [dataContract](../baseDataService.md) for details on the base options. 12 | 13 | ### Interface 14 | The following functions are available for consumers of the singleton data service. 15 | 16 | #### `get(): angular.IPromise` 17 | Makes a GET request against the base endpoint. 18 | 19 | #### `update(domainObject: TDataType): angular.IPromise` 20 | Makes a PUT request against the base endpoint to update the item. Returns the updated item. 21 | 22 | #### `version(versionNumber: number): SingletonDataService` 23 | Returns a clone of the data service with the version number injected into the url as `/v{{versionNumber}}` directly after the first `/api`. 24 | -------------------------------------------------------------------------------- /source/services/notification/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { WindowWrapper } from '../window/window.provider'; 3 | import { ILogger, Logger } from '../logger/logger.service'; 4 | 5 | export interface INotificationService { 6 | info(message: string): void; 7 | warning(message: string): void; 8 | error(message: string): void; 9 | success(message: string): void; 10 | } 11 | 12 | @Injectable() 13 | export class NotificationService implements INotificationService { 14 | private window: Window; 15 | private logger: ILogger; 16 | 17 | constructor(window: WindowWrapper 18 | , logger: Logger) { 19 | this.window = window; 20 | this.logger = logger; 21 | } 22 | 23 | info(message: string): void { 24 | this.notify(message); 25 | } 26 | 27 | warning(message: string): void { 28 | this.notify(message); 29 | } 30 | 31 | error(message: string): void { 32 | this.notify(message); 33 | } 34 | 35 | success(message: string): void { 36 | this.notify(message); 37 | } 38 | 39 | private notify(message: string): void { 40 | this.window.alert(message); 41 | this.logger.log(message); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Renovo Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /source/services/redirect/redirect.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { WindowWrapper } from '../window/window.provider'; 3 | 4 | export interface IRedirectService { 5 | getCurrentLocationAsParam(): string; 6 | to(target: string, newTab?: boolean): void; 7 | back(): void; 8 | } 9 | 10 | 11 | // This sevice implementation is browser specific and will NOT work for server side rendering!!!! 12 | 13 | @Injectable() 14 | export class RedirectService implements IRedirectService { 15 | private window: Window; 16 | 17 | constructor(window: WindowWrapper) { 18 | this.window = window; 19 | } 20 | 21 | getCurrentLocationAsParam(): string { 22 | const baseUrl: string = this.window.location.pathname; 23 | const queryString: string = this.window.location.search || ''; 24 | return encodeURIComponent(baseUrl + queryString); 25 | } 26 | 27 | to(target: string, newTab?: boolean): void { 28 | if (!newTab) { 29 | this.window.open(target, '_self'); 30 | } else { 31 | const win: Window = this.window.open(target, '_blank'); 32 | win.focus(); 33 | } 34 | } 35 | 36 | back(): void { 37 | this.window.history.back(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /source/services/dataContracts/converters/timeConverter/timeConverter.tests.ts: -------------------------------------------------------------------------------- 1 | import { timeConverter, defaultFormats } from './timeConverter'; 2 | import { timezones, timezoneService } from '../../../timezone/timezone.service'; 3 | 4 | import * as moment from 'moment'; 5 | import 'moment-timezone'; 6 | 7 | describe('timeConverter', (): void => { 8 | it('should get the time from a time string and set the user timezone', (): void => { 9 | timezoneService.setCurrentTimezone(timezones.CST.offset); 10 | let timeMoment: moment.Moment = timeConverter.fromServer('8:00PM'); 11 | expect(timeMoment.tz()).to.equal(timezones.CST.momentName); 12 | expect(timeMoment.format(defaultFormats.timeFormat)).to.equal('8:00PM'); 13 | }); 14 | 15 | it('should convert the moment back to a time string', (): void => { 16 | let timeMoment: moment.Moment = moment('2016-01-02T20:00:00-07:00', defaultFormats.isoFormat).tz(timezones.MST.momentName); 17 | expect(timeConverter.toServer(timeMoment)).to.equal('8:00PM'); 18 | }); 19 | 20 | it('should handle nulls', (): void => { 21 | expect(timeConverter.fromServer(null)).to.be.null; 22 | expect(timeConverter.toServer(null)).to.be.null; 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /source/services/validation/validationTypes.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | export interface IValidator { 4 | validate(value?: any): boolean; 5 | getErrorCount(): number; 6 | } 7 | 8 | export interface IObservableValidator { 9 | validate(value$?: Observable): Observable; 10 | } 11 | 12 | export interface ISimpleValidator extends IValidator { 13 | registerValidationHandler(handler: IValidationHandler): IUnregisterFunction; 14 | } 15 | 16 | export interface ICompositeValidator extends IValidator { 17 | buildChildValidator(): ISimpleValidator; 18 | unregisterChild(validator: ISimpleValidator): void; 19 | } 20 | 21 | export interface IValidationHandler { 22 | isActive?: {(): boolean} | boolean; 23 | validate(value?: any): boolean; 24 | errorMessage: string | { (): string }; 25 | name?: string; 26 | } 27 | 28 | export interface IObservableValidationHandler { 29 | name?: string; 30 | isActive?: {(): Observable} | boolean; 31 | validate(value$?: Observable): Observable; 32 | } 33 | 34 | export interface IErrorHandler { 35 | (error: string, name?: string): void; 36 | } 37 | 38 | export interface IUnregisterFunction { 39 | (): void; 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "npm", 4 | "isShellCommand": true, 5 | "args": [ 6 | "--no-color" 7 | ], 8 | "tasks": [ 9 | { 10 | "taskName": "build", 11 | "args": ["run"], 12 | "isBuildCommand": true, 13 | "isWatching": false, 14 | "problemMatcher": [ 15 | "$lessCompile", 16 | "$tsc", 17 | "$jshint" 18 | ] 19 | }, 20 | { 21 | "taskName": "test", 22 | "args": [], 23 | "isTestCommand": true 24 | }, 25 | { 26 | "taskName": "update", 27 | "args": ["run"] 28 | }, 29 | { 30 | "taskName": "clean", 31 | "args": ["run"] 32 | }, 33 | { 34 | "taskName": "publish.prep", 35 | "args": ["run"] 36 | }, 37 | { 38 | "taskName": "build.library", 39 | "args": ["run"] 40 | }, 41 | { 42 | "taskName": "test.debug", 43 | "args": ["run"] 44 | } 45 | ], 46 | "problemMatcher": "$tsc" 47 | } -------------------------------------------------------------------------------- /source/services/string/string.service.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | export interface IStringUtility { 4 | toNumber(string: string): number; 5 | contains(str: string, substring?: string): boolean; 6 | substitute(formatString: string, ...params: string[]): string; 7 | replaceAll(str: string, patternToFind: string, replacementString: string): string; 8 | } 9 | 10 | export class StringUtility implements IStringUtility { 11 | toNumber(string: string): number { 12 | return +string; 13 | } 14 | 15 | contains(str: string, substring?: string): boolean { 16 | if (substring) { 17 | return str.indexOf(substring) !== -1; 18 | } 19 | 20 | return true; 21 | } 22 | 23 | substitute(formatString: string, ...params: string[]): string { 24 | _.each(params, (param: string, index: number): void => { 25 | formatString = this.replaceAll(formatString, '\\{' + index + '\\}', param); 26 | }); 27 | return formatString; 28 | } 29 | 30 | replaceAll(str: string, patternToFind: string, replacementString: string): string { 31 | return str.replace(new RegExp(patternToFind, 'gi'), replacementString); 32 | } 33 | } 34 | 35 | export const stringUtility: StringUtility = new StringUtility(); 36 | -------------------------------------------------------------------------------- /source/services/transform/transform.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { TransformService, ITransformService } from './transform.service'; 2 | 3 | describe('transform', (): void => { 4 | let valueTransform: sinon.SinonSpy; 5 | let transform: ITransformService; 6 | 7 | beforeEach((): void => { 8 | transform = new TransformService(); 9 | valueTransform = sinon.spy((item: any): number => { return item.value; }); 10 | }); 11 | 12 | it('should call the transform if a function is provided', (): void => { 13 | let item: any = { value: 4 }; 14 | expect(transform.getValue(item, valueTransform)).to.equal(4); 15 | }); 16 | 17 | it('should use the transform as a key selector if a string is provided', (): void => { 18 | let item: any = { value: 4 }; 19 | expect(transform.getValue(item, 'value')).to.equal(4); 20 | }); 21 | 22 | it('should return null if the item is null', (): void => { 23 | expect(transform.getValue(null, 'value')).to.be.null; 24 | expect(transform.getValue(null, valueTransform)).to.be.null; 25 | }); 26 | 27 | it('should return the item if no transform is provided', (): void => { 28 | let item: any = { value: 4 }; 29 | expect(transform.getValue(item, null)).to.equal(item); 30 | }); 31 | }); -------------------------------------------------------------------------------- /source/types/itemList.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | export interface IItem { 4 | value: number; 5 | name: string; 6 | display: string; 7 | } 8 | 9 | export interface IItemList { 10 | get(value: number | string): TItemType; 11 | all(): TItemType[]; 12 | } 13 | 14 | export class ItemList implements IItemList { 15 | private items: TItemType[]; 16 | 17 | setItems(items: TItemType[]): void { 18 | this.items = items; 19 | } 20 | 21 | get(value: number | string): TItemType { 22 | var predicate: { (item: TItemType): boolean }; 23 | 24 | if (typeof value === 'string') { 25 | predicate = (item: TItemType): boolean => { 26 | return (item.name === value); 27 | }; 28 | } else { 29 | predicate = (item: TItemType): boolean => { 30 | return (item.value === value); 31 | }; 32 | } 33 | 34 | return _.find(this.items, predicate); 35 | } 36 | 37 | all(): TItemType[] { 38 | return this.items; 39 | } 40 | } 41 | 42 | export class SortedItemList extends ItemList { 43 | setItems(items: TItemType[]): void { 44 | super.setItems(_.sortBy(items, x => x.display)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /source/services/fileSize/fileSize.md: -------------------------------------------------------------------------------- 1 | ## fileSize 2 | A simple service for converting byte counts into human friendly storage size strings. 3 | 4 | For example, `2048` is displayed as `2 KB`. 5 | 6 | You can also query fileSize instances to ask for the integer number of GB, MB, KB, that results from the specified number of bytes. 7 | 8 | Example usage: 9 | ``` 10 | import * as angular from 'angular'; 11 | 12 | import { services } from 'typescript-angular-utilities'; 13 | import __fileSize = services.fileSize; 14 | 15 | class MyConsumer { 16 | static $inject: string[] = [__fileSize.factoryName]; 17 | constructor(private fileSizeFactory: __fileSize.IFileSizeFactory) { } 18 | 19 | displayBytes(bytes: number): string { 20 | var instance = this.fileSizeFactory.getInstance(bytes); // Makes a file size instance based on the byte count 21 | return instance.display(); // Generates a pretty string displaying the bytes 22 | } 23 | } 24 | ``` 25 | 26 | ## fileSize filter 27 | The fileSize module also contains an angular filter which will automatically instantiate a fileSize instance for you and print the display result: 28 | ``` 29 | File size: {{ controller.bytes | fileSize }} 30 | ``` 31 | -------------------------------------------------------------------------------- /source/services/number/number.service.ts: -------------------------------------------------------------------------------- 1 | enum Sign { 2 | positive = 1, 3 | negative = -1, 4 | } 5 | 6 | export interface INumberUtility { 7 | preciseRound(num: number, decimals: number): number; 8 | integerDivide(dividend: number, divisor: number): number; 9 | roundToStep(num: number, step: number): number; 10 | isEven(num: number): boolean; 11 | } 12 | 13 | export class NumberUtility implements INumberUtility { 14 | preciseRound(num: number, decimals: number): number { 15 | var sign: Sign = num >= 0 ? Sign.positive : Sign.negative; 16 | return (Math.round((num * Math.pow(10, decimals)) + (sign * 0.001)) / Math.pow(10, decimals)); 17 | } 18 | 19 | integerDivide(dividend: number, divisor: number): number { 20 | return Math.floor(dividend / divisor); 21 | } 22 | 23 | roundToStep(num: number, step: number): number { 24 | if (!step) { 25 | return num; 26 | } 27 | 28 | var remainder: number = num % step; 29 | 30 | if (remainder >= step / 2) { 31 | return num + (step - remainder); 32 | } else { 33 | return num - remainder; 34 | } 35 | } 36 | 37 | isEven(num: number): boolean { 38 | return !(num % 2); 39 | } 40 | } 41 | 42 | export const numberUtility: NumberUtility = new NumberUtility(); 43 | -------------------------------------------------------------------------------- /source/services/synchronizedRequests/synchronizedRequests.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { rlFakeAsync, mock, IMockedPromise } from 'rl-async-testing'; 2 | import { SynchronizedRequestsService } from './synchronizedRequests.service'; 3 | 4 | describe('synchronizedRequests', () => { 5 | let synchronizedRequests: SynchronizedRequestsService; 6 | 7 | it('should accept the results from only the most recent request', rlFakeAsync((): void => { 8 | const firstRequestData: number[] = [1, 2]; 9 | const secondRequestData: number[] = [3, 4]; 10 | const firstRequest: IMockedPromise = mock.promise(firstRequestData); 11 | const secondRequest: IMockedPromise = mock.promise(secondRequestData); 12 | 13 | const callback: sinon.SinonSpy = sinon.spy(); 14 | let get: sinon.SinonSpy = sinon.spy((): Promise => { return firstRequest(); }); 15 | 16 | synchronizedRequests = new SynchronizedRequestsService(get, callback); 17 | 18 | synchronizedRequests.getData(); 19 | 20 | sinon.assert.calledOnce(get); 21 | 22 | get = sinon.spy((): Promise => { return secondRequest(); }); 23 | 24 | synchronizedRequests.dataProvider = get; 25 | synchronizedRequests.getData(); 26 | 27 | sinon.assert.calledOnce(get); 28 | 29 | firstRequest.flush(); 30 | 31 | sinon.assert.notCalled(callback); 32 | 33 | secondRequest.flush(); 34 | 35 | sinon.assert.calledOnce(callback); 36 | sinon.assert.calledWith(callback, secondRequestData); 37 | })); 38 | }); 39 | -------------------------------------------------------------------------------- /source/filters/filter.ts: -------------------------------------------------------------------------------- 1 | import * as Rx from 'rxjs/Rx'; 2 | 3 | import { objectUtility } from '../services/object/object.service'; 4 | 5 | export interface IFilterWithCounts extends IFilter { 6 | updateOptionCounts(data: TItemType[]): void; 7 | } 8 | 9 | export interface ISerializableFilter extends IFilter { 10 | type: string; 11 | serialize(): TFilterData; 12 | subscribe(onValueChange: IValueChangeCallback): Rx.Subscription; 13 | } 14 | 15 | export interface IValueChangeCallback { 16 | (newValue: TFilterData): void; 17 | } 18 | 19 | export interface IFilter { 20 | filter(item: TItemType): boolean; 21 | } 22 | 23 | export abstract class SerializableFilter implements ISerializableFilter { 24 | type: string; 25 | protected subject: Rx.Subject; 26 | private _value: TFilterData; 27 | 28 | constructor() { 29 | this.subject = new Rx.Subject(); 30 | } 31 | 32 | abstract filter(item: any): boolean; 33 | 34 | serialize(): TFilterData { 35 | return this; 36 | } 37 | 38 | subscribe(onValueChange: IValueChangeCallback): Rx.Subscription { 39 | return this.subject.subscribe(onValueChange); 40 | } 41 | 42 | onChange(force: boolean = true): void { 43 | let newValue: TFilterData = this.serialize(); 44 | if (force || !objectUtility.areEqual(newValue, this._value)) { 45 | this._value = newValue; 46 | this.subject.next(this._value); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /config/karma-test-shim.js: -------------------------------------------------------------------------------- 1 | /*global mocha, __karma__, window*/ 2 | Error.stackTraceLimit = Infinity; 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000; 4 | 5 | __karma__.loaded = function () { }; 6 | window.expect = chai.expect; 7 | 8 | var basePath = '/base/'; 9 | 10 | function isJsFile(path) { 11 | return endsWith(path, '.js'); 12 | } 13 | 14 | function isSpecFile(path) { 15 | return endsWith(path, '.tests.js'); 16 | } 17 | 18 | function endsWith(path, ending) { 19 | return path.slice(-ending.length) == ending; 20 | } 21 | 22 | var allSpecFiles = Object.keys(window.__karma__.files) 23 | .filter(isSpecFile); 24 | 25 | // Load our SystemJS configuration. 26 | System.config({ 27 | baseURL: basePath 28 | }); 29 | 30 | System.import('system.config.js').then(function () { 31 | // Load and configure the TestComponentBuilder. 32 | return Promise.all([ 33 | System.import('@angular/core/testing'), 34 | System.import('@angular/platform-browser-dynamic/testing') 35 | ]).then(function (providers) { 36 | var testing = providers[0]; 37 | var testingBrowser = providers[1]; 38 | 39 | testing.TestBed.initTestEnvironment(testingBrowser.BrowserDynamicTestingModule 40 | , testingBrowser.platformBrowserDynamicTesting()); 41 | }); 42 | }).then(function () { 43 | // Finally, load all spec files. 44 | // This will run the tests directly. 45 | return Promise.all( 46 | allSpecFiles.map(function (moduleName) { 47 | return System.import(moduleName); 48 | })); 49 | }).then(__karma__.start, __karma__.error); 50 | -------------------------------------------------------------------------------- /source/services/dataContracts/converters/enumConverter/enumConverter.tests.ts: -------------------------------------------------------------------------------- 1 | import { EnumConverter, IConverter } from './enumConverter'; 2 | import { ItemList, IItem } from '../../../../types/itemList'; 3 | 4 | class TestEnum extends ItemList { 5 | type1: IItem = { 6 | value: 0, 7 | name: 'type1', 8 | display: 'Type 1', 9 | }; 10 | type2: IItem = { 11 | value: 1, 12 | name: 'type2', 13 | display: 'Type 2', 14 | }; 15 | 16 | constructor() { 17 | super(); 18 | 19 | super.setItems([ 20 | this.type1, 21 | this.type2, 22 | ]); 23 | } 24 | } 25 | let testEnum = new TestEnum(); 26 | let enumConverter: IConverter = new EnumConverter(testEnum); 27 | 28 | describe('enumConverter', (): void => { 29 | it('should get the enum type for the specified value', (): void => { 30 | expect(enumConverter.fromServer(0)).to.equal(testEnum.type1); 31 | expect(enumConverter.fromServer(1)).to.equal(testEnum.type2); 32 | }); 33 | 34 | it('should get the value of the enum type', (): void => { 35 | expect(enumConverter.toServer(testEnum.type1)).to.equal(0); 36 | expect(enumConverter.toServer(testEnum.type2)).to.equal(1); 37 | }); 38 | 39 | it('should return undefined if an invalid value is specified', (): void => { 40 | expect(enumConverter.fromServer(10)).to.not.exist; 41 | expect(enumConverter.fromServer(null)).to.not.exist; 42 | }); 43 | 44 | it('should return null if the enum type is null', (): void => { 45 | expect(enumConverter.toServer(null)).to.not.exist; 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /source/services/fileSize/fileSize.service.ts: -------------------------------------------------------------------------------- 1 | import { INumberUtility } from '../number/number.service'; 2 | 3 | export class FileSize { 4 | BYTES_PER_GB: number = 1073741824; 5 | BYTES_PER_MB: number = 1048576; 6 | BYTES_PER_KB: number = 1024; 7 | 8 | bytes: number; 9 | 10 | GB: number; 11 | isGB: boolean; 12 | 13 | MB: number; 14 | isMB: boolean; 15 | 16 | KB: number; 17 | isKB: boolean; 18 | 19 | constructor(numberUtility: INumberUtility, bytes: number) { 20 | this.bytes = bytes; 21 | 22 | if (bytes >= this.BYTES_PER_GB) { 23 | this.isGB = true; 24 | this.GB = bytes / this.BYTES_PER_GB; 25 | this.GB = numberUtility.preciseRound(this.GB, 1); 26 | } else { 27 | this.isGB = false; 28 | 29 | if (bytes >= this.BYTES_PER_MB) { 30 | this.isMB = true; 31 | this.MB = bytes / this.BYTES_PER_MB; 32 | this.MB = numberUtility.preciseRound(this.MB, 1); 33 | } else { 34 | this.isMB = false; 35 | 36 | if (bytes >= this.BYTES_PER_KB) { 37 | this.isKB = true; 38 | this.KB = bytes / this.BYTES_PER_KB; 39 | this.KB = numberUtility.preciseRound(this.KB, 1); 40 | } else { 41 | this.isKB = false; 42 | } 43 | } 44 | } 45 | 46 | this.bytes = Math.round(this.bytes); 47 | } 48 | 49 | display(): string { 50 | if (this.isGB) { 51 | return this.GB + ' GB'; 52 | } else if (this.isMB) { 53 | return this.MB + ' MB'; 54 | } else if (this.isKB) { 55 | return this.KB + ' KB'; 56 | } else { 57 | return this.bytes + ' bytes'; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /source/services/test/chaiMoment.tests.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import 'moment-timezone'; 3 | 4 | import './index'; 5 | 6 | describe('chai moment', () => { 7 | it('should return true if moments are equal', (): void => { 8 | expect(moment('2015-01-01')).to.be.sameMoment(moment('2015-01-01')); 9 | expect(moment('2015-01-01')).to.equalMoment(moment('2015-01-01')); 10 | expect(moment('2015-01-01')).to.not.be.sameMoment(moment('2016-01-01')); 11 | expect(moment('2015-01-01')).to.not.equalMoment(moment('2016-01-01')); 12 | }); 13 | 14 | it('should return true if the first moment is after the second', (): void => { 15 | expect(moment('2015-01-01')).to.be.afterMoment(moment('2014-01-01')); 16 | expect(moment('2015-01-01')).to.not.be.afterMoment(moment('2016-01-01')); 17 | }); 18 | 19 | it('should return true if the first moment is before the second', (): void => { 20 | expect(moment('2015-01-01')).to.be.beforeMoment(moment('2016-01-01')); 21 | expect(moment('2015-01-01')).to.not.be.beforeMoment(moment('2014-01-01')); 22 | }); 23 | 24 | it('should accept strings and dates', (): void => { 25 | expect('2015-01-01').to.equalMoment('2015-01-01'); 26 | expect(new Date(2015, 1, 1)).to.equalMoment(new Date(2015, 1, 1)); 27 | }); 28 | 29 | it('should handle timezones', (): void => { 30 | let est_moment: moment.Moment = moment('2016-01-02T12:00:00.000-05:00').tz('US/Eastern'); 31 | let pst_moment: moment.Moment = moment('2016-01-02T09:00:00.000-08:00').tz('US/Pacific'); 32 | expect(est_moment).to.equalMoment(pst_moment); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /source/services/validation/compositeValidator.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { ICompositeValidator, ISimpleValidator, IErrorHandler } from './validationTypes'; 4 | import { Validator } from './validator'; 5 | 6 | interface IRegisteredValidator extends ISimpleValidator { 7 | key: number; 8 | } 9 | 10 | export class CompositeValidator implements ICompositeValidator { 11 | private childValidators: { [index: string]: ISimpleValidator } = {}; 12 | private nextKey: number = 0; 13 | 14 | constructor(private showError: IErrorHandler) {} 15 | 16 | validate(value?: any): boolean { 17 | let isValid: boolean = true; 18 | 19 | _.each(this.childValidators, (handler: ISimpleValidator): boolean => { 20 | if (!handler.validate(value)) { 21 | isValid = false; 22 | return false; 23 | } 24 | }); 25 | 26 | return isValid; 27 | } 28 | 29 | getErrorCount(): number { 30 | return _.reduce(this.childValidators, (count: number, handler: ISimpleValidator): number => { 31 | return count += handler.getErrorCount(); 32 | }, 0); 33 | } 34 | 35 | buildChildValidator(): ISimpleValidator { 36 | let validator: ISimpleValidator = new Validator((error: string): void => { 37 | this.showError(error); 38 | }); 39 | 40 | var currentKey: number = this.nextKey; 41 | this.nextKey++; 42 | this.childValidators[currentKey] = validator; 43 | (validator).key = currentKey; 44 | 45 | return validator; 46 | } 47 | 48 | unregisterChild(validator: ISimpleValidator): void { 49 | delete this.childValidators[(validator).key]; 50 | } 51 | } -------------------------------------------------------------------------------- /source/filters/filter.tests.ts: -------------------------------------------------------------------------------- 1 | import { SerializableFilter } from './filter'; 2 | 3 | class TestSerializableFilter extends SerializableFilter { 4 | filter(item: any): boolean { 5 | return true; 6 | } 7 | } 8 | 9 | describe('serializable filter', (): void => { 10 | let filter: SerializableFilter; 11 | 12 | beforeEach((): void => { 13 | filter = new TestSerializableFilter(); 14 | }); 15 | 16 | it('should provide a default serialize that returns the filter directly', (): void => { 17 | filter.type = 'myFilter'; 18 | let serializedValue: any = filter.serialize(); 19 | expect(serializedValue).to.be.not.empty; 20 | expect(serializedValue.type).to.equal('myFilter'); 21 | }); 22 | 23 | it('should fire an event every time onChange is called by default', (): void => { 24 | let onValueChange: sinon.SinonSpy = sinon.spy(); 25 | filter.subscribe(onValueChange); 26 | 27 | filter.onChange(); 28 | 29 | sinon.assert.calledOnce(onValueChange); 30 | }); 31 | 32 | it('should deep compare the new seralized value with the old one if force is set to false', (): void => { 33 | let obj1: any = { prop: '1234' }; 34 | let obj2: any = { prop: '5678' }; 35 | filter.serialize = (): any => { return obj1; }; 36 | // set the initial value 37 | filter.onChange(); 38 | let onValueChange: sinon.SinonSpy = sinon.spy(); 39 | filter.subscribe(onValueChange); 40 | 41 | filter.onChange(false); 42 | 43 | sinon.assert.notCalled(onValueChange); 44 | 45 | filter.serialize = (): any => { return obj2; }; 46 | filter.onChange(false); 47 | 48 | sinon.assert.calledOnce(onValueChange); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /source/services/genericSearchFilter/genericSearchFilter.md: -------------------------------------------------------------------------------- 1 | ## genericSearchFilter 2 | A service for performing generic text searching against json objects. Matches the ISerializableFilter interface. 3 | 4 | Implements [ISerializableFilter](../../filters/filters.md#iserializablefilter). 5 | 6 | Example usage: 7 | ``` 8 | import * as angular from 'angular'; 9 | 10 | import { services } from 'typescript-angular-utilities'; 11 | import __genericSearchFilter = services.genericSearchFilter; 12 | 13 | class MyConsumer { 14 | static $inject: string[] = [__genericSearchFilter.factoryName]; 15 | constructor(private searchFilterFactory: __genericSearchFilter.IGenericSearchFilterFactory) { } 16 | 17 | filter(items: any[], search: string): any[] { 18 | var instance = this.searchFilterFactory.getInstance(); // builds a search filter instance 19 | instance.searchText = search; 20 | return _.filter(items, instance.filter); 21 | } 22 | } 23 | ``` 24 | 25 | #### `searchText` 26 | The current search value of the filter. 27 | 28 | #### `minSearchLength (default: 1)` 29 | Sets the minimum number of characters required to perform a search. If the searchText is less than the minimum, no filtering is applied. 30 | 31 | #### `caseSensitive (default: false)` 32 | If true, matches search results only where the casing is the same. 33 | 34 | #### `filter(item: TItemType): boolean` 35 | Returns true if the search text matches against any field on the item. Always returns true if the searchText is empty or below the minimum. 36 | 37 | #### `serialize(): string` 38 | Returns the current search value or null if it's below the minimum allowed. -------------------------------------------------------------------------------- /source/services/dataContracts/contractLibrary/dataServiceMocks.ts: -------------------------------------------------------------------------------- 1 | import { IMockedRequest } from 'rl-async-testing'; 2 | 3 | import { IDataService, IBaseDomainObject } from '../dataService/data.service'; 4 | import { IParentDataService } from '../dataService/parent/parentData.service'; 5 | import { ISingletonDataService } from '../singletonDataService/singletonData.service'; 6 | 7 | export interface IDataTransform { 8 | (data: any): any; 9 | } 10 | 11 | export interface IDataServiceMock extends IDataService { 12 | mockGetList(data: any[]): IMockedRequest; 13 | mockGetDetail(data: any): IMockedRequest; 14 | mockUpdate(dataTransform?: IDataTransform): IMockedRequest; 15 | mockCreate(dataTransform?: IDataTransform): IMockedRequest; 16 | mockDelete(): IMockedRequest; 17 | } 18 | 19 | export interface IParentDataServiceMock extends IParentDataService { 20 | mockGetList(data: any[]): IMockedRequest; 21 | mockGetDetail(data: any): IMockedRequest; 22 | mockChild(mockCallback: { (children: any): void }): void; 23 | mockUpdate(dataTransform?: IDataTransform): IMockedRequest; 24 | mockCreate(dataTransform?: IDataTransform): IMockedRequest; 25 | mockDelete(): IMockedRequest; 26 | } 27 | 28 | export interface ISingletonDataServiceMock extends ISingletonDataService { 29 | mockGet(data: any): IMockedRequest; 30 | mockUpdate(dataTransform?: IDataTransform): IMockedRequest; 31 | } 32 | -------------------------------------------------------------------------------- /source/services/timeout/timeout.service.ts: -------------------------------------------------------------------------------- 1 | import { isFunction } from 'lodash'; 2 | 3 | export interface ITimeout extends Promise { 4 | cancel(): void; 5 | then(onSucces: TValue, onError?: any): ITimeout; 6 | catch(reason: TReason): ITimeout; 7 | } 8 | 9 | export class TimeoutService { 10 | setTimeout(callbackOrDuration: number): ITimeout; 11 | setTimeout(callbackOrDuration: Function, duration?: number): ITimeout; 12 | setTimeout(callbackOrDuration: Function | number, duration?: number): ITimeout { 13 | let useCallback = isFunction(callbackOrDuration); 14 | let callback: Function = useCallback ? callbackOrDuration : () => null; 15 | duration = useCallback ? duration : callbackOrDuration; 16 | 17 | let pending: boolean; 18 | let rejectFunc: Function; 19 | const promise: Promise = new Promise((resolve, reject) => { 20 | pending = true; 21 | rejectFunc = reject; 22 | 23 | setTimeout(() => { 24 | if (pending) { 25 | pending = false; 26 | resolve(); 27 | callback(); 28 | } 29 | }, duration); 30 | }); 31 | return this.wrapPromiseAsTimeout(promise, () => { 32 | pending = false; 33 | rejectFunc(); 34 | }); 35 | } 36 | 37 | private wrapPromiseAsTimeout(promise: Promise, cancel: { (): void }): ITimeout { 38 | const promiseThen = promise.then.bind(promise); 39 | const promiseCatch = promise.catch.bind(promise); 40 | const timeout: ITimeout = promise; 41 | timeout.cancel = cancel; 42 | timeout.then = (onSuccess, onError) => { 43 | const newPromise: Promise = promiseThen(onSuccess, onError); 44 | return this.wrapPromiseAsTimeout(newPromise, cancel); 45 | } 46 | return timeout; 47 | } 48 | } -------------------------------------------------------------------------------- /source/services/validation/observableValidator.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { map, find, isFunction, values } from 'lodash'; 3 | 4 | import { IUnregisterFunction, IObservableValidator, IObservableValidationHandler } from './validationTypes'; 5 | 6 | export class ObservableValidator implements IObservableValidator { 7 | private validationHandlers: { [index: string]: IObservableValidationHandler } = {}; 8 | private nextKey: number = 0; 9 | 10 | validate(value$?: Observable): Observable { 11 | if (!values(this.validationHandlers).length) { 12 | return Observable.of(null); 13 | } 14 | 15 | return Observable.combineLatest(map(this.validationHandlers, handler => this.getValidationResult(handler, value$))) 16 | .map(results => find(results, x => x != null)); 17 | } 18 | 19 | registerValidationHandler(handler: IObservableValidationHandler): IUnregisterFunction { 20 | var currentKey: number = this.nextKey; 21 | this.nextKey++; 22 | this.validationHandlers[currentKey] = handler; 23 | 24 | return (): void => { 25 | this.unregister(currentKey); 26 | }; 27 | } 28 | 29 | private unregister(key: number): void { 30 | delete this.validationHandlers[key]; 31 | } 32 | 33 | private isActive(handler: IObservableValidationHandler): Observable { 34 | return (isFunction(handler.isActive) && (<{(): Observable}>handler.isActive)()) 35 | || Observable.of(handler.isActive == null) 36 | || Observable.of(handler.isActive === true); 37 | } 38 | 39 | private getValidationResult(handler: IObservableValidationHandler, value$: Observable): Observable { 40 | return this.isActive(handler) 41 | .switchMap(active => active ? handler.validate(value$) : Observable.of(null)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /source/services/timeout/timeout.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { rlFakeAsync, rlTick, flushMicrotasks } from 'rl-async-testing'; 2 | import { TimeoutService, ITimeout } from './timeout.service'; 3 | 4 | describe('TimeoutService', (): void => { 5 | let timeoutService: TimeoutService; 6 | 7 | beforeEach(() => { 8 | timeoutService = new TimeoutService(); 9 | }); 10 | 11 | it('should resolve the promise and call the callback when the timeout finishes', rlFakeAsync((): void => { 12 | const promiseSpy: sinon.SinonSpy = sinon.spy(); 13 | const callback: sinon.SinonSpy = sinon.spy(); 14 | 15 | timeoutService.setTimeout(callback, 2000).then(promiseSpy); 16 | 17 | rlTick(2000); 18 | flushMicrotasks(); 19 | 20 | sinon.assert.calledOnce(promiseSpy); 21 | sinon.assert.calledOnce(callback); 22 | })); 23 | 24 | it('should reject the promise without calling the callback if the timeout is canceled', rlFakeAsync((): void => { 25 | const rejectSpy: sinon.SinonSpy = sinon.spy(); 26 | const callback: sinon.SinonSpy = sinon.spy(); 27 | 28 | const timeout: ITimeout = timeoutService.setTimeout(callback, 2000) 29 | .then(assert.fail) 30 | .catch(rejectSpy); 31 | 32 | timeout.cancel(); 33 | rlTick(2000); 34 | flushMicrotasks(); 35 | 36 | sinon.assert.calledOnce(rejectSpy); 37 | sinon.assert.notCalled(callback); 38 | })); 39 | 40 | it('should allow for specifying just a duration for chaining as a promise', rlFakeAsync((): void => { 41 | const promiseSpy: sinon.SinonSpy = sinon.spy(); 42 | 43 | timeoutService.setTimeout(2000).then(promiseSpy); 44 | 45 | flushMicrotasks(); 46 | sinon.assert.notCalled(promiseSpy); 47 | 48 | rlTick(2000); 49 | flushMicrotasks(); 50 | 51 | sinon.assert.calledOnce(promiseSpy); 52 | })); 53 | }); 54 | -------------------------------------------------------------------------------- /source/services/array/array.md: -------------------------------------------------------------------------------- 1 | ## array 2 | Contains generic operations for arrays. Some of these are redundant with libraries like [lodash](https://lodash.com/) and [underscore](http://underscorejs.org/), but available here if needed. 3 | 4 | #### `findIndexOf(array: TDataType[], predicate: { (item: TDataType): boolean }): number` 5 | Pass in the array to search and a predicate to test against each item. Returns the index of the first item that the predicate returns true for, or -1 if the predicate never returns true. 6 | 7 | #### `remove(array: TDataType[], item: TDataType | { (obj: TDataType): boolean }): TDataType` 8 | The first parameter is the array to remove the item from, the second is either the item to remove or a predicate. If you give pass in a predicate the first item that returns true will be removed. 9 | 10 | #### `replace(array: TDataType[], oldItem: TDataType, newItem: TDataType): void` 11 | Takes the array passed in as the first parameter and replaces the second parameter with the third parameter in this array. 12 | 13 | #### `sum(array: TDataType[], transform?: { (item: TDataType): number }): number` 14 | Adds all of the items in the array and returns the sum. The second parameter can optionally transform each item before adding. 15 | ``` 16 | let myArray = [ 17 | { id: 0, num: 1 }, 18 | { id: 1, num: 5 }, 19 | { id: 2, num: 4 }, 20 | { id: 3, num: 8 }, 21 | ]; 22 | let mySum = arrayService.sum(myArray, (item): number => item.num); 23 | expect(mySum).to.equal(18); 24 | ``` 25 | 26 | #### `toDictionary(array: TDataType[], keySelector: { (item: TDataType): string }): { [index: string]: TDataType }` 27 | Converts the array into a dictionary. The second parameter is a function that takes each item and returns a dictionary key. 28 | 29 | #### `last(array: TDataType[]): TDataType` 30 | Returns the last element in the array. 31 | -------------------------------------------------------------------------------- /source/services/synchronizedRequests/synchronizedRequests.service.ts: -------------------------------------------------------------------------------- 1 | import { FactoryProvider } from '@angular/core'; 2 | 3 | export interface ISynchronizedRequestsService { 4 | dataProvider: IRequestGetter; 5 | handleRequest: IRequestCallback; 6 | 7 | getData(...params: any[]): void; 8 | } 9 | 10 | export class SynchronizedRequestsService { 11 | private requestId: number = 0; 12 | dataProvider: IRequestGetter; 13 | handleRequest: IRequestCallback; 14 | 15 | constructor(dataProvider: IRequestGetter 16 | , handleRequest: IRequestCallback) { 17 | this.dataProvider = dataProvider; 18 | this.handleRequest = handleRequest; 19 | } 20 | 21 | getData(...params: any[]): void { 22 | // increment the id first - should match current request id 23 | this.requestId++; 24 | let currentRequestId: number = this.requestId; 25 | Promise.resolve(this.dataProvider(...params)).then((...data: any[]): void => { 26 | if (currentRequestId == this.requestId) { 27 | this.handleRequest(...data); 28 | } 29 | }); 30 | } 31 | } 32 | 33 | export interface IRequestGetter { 34 | (...params: any[]): Promise; 35 | } 36 | 37 | export interface IRequestCallback { 38 | (...data: any[]): void; 39 | } 40 | 41 | export interface ISynchronizedRequestsFactory { 42 | getInstance(dataProvider: IRequestGetter, handleRequest: IRequestCallback): ISynchronizedRequestsService; 43 | } 44 | 45 | export class SynchronizedRequestsFactory { 46 | getInstance(dataProvider: IRequestGetter, handleRequest: IRequestCallback): ISynchronizedRequestsService { 47 | return new SynchronizedRequestsService(dataProvider, handleRequest); 48 | } 49 | } 50 | 51 | export function SynchronizedRequestsProvider(dataProvider: IRequestGetter, handleRequest: IRequestCallback): FactoryProvider { 52 | return { 53 | provide: SynchronizedRequestsService, 54 | useFactory: () => new SynchronizedRequestsService(dataProvider, handleRequest), 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /source/services/search/search.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | 4 | import { IObjectUtility, ObjectUtility, objectUtility } from '../object/object.service'; 5 | import { IStringUtility, StringUtility, stringUtility } from '../string/string.service'; 6 | 7 | export interface ISearchUtility { 8 | search(object: any, search: string, caseSensitive?: boolean): boolean; 9 | tokenizedSearch(object: any, search: string, caseSensitive?: boolean): boolean; 10 | } 11 | 12 | @Injectable() 13 | export class SearchUtility implements ISearchUtility { 14 | private objectUtility: IObjectUtility; 15 | private stringUtility: IStringUtility; 16 | 17 | constructor(objectUtility: ObjectUtility 18 | , stringUtility: StringUtility) { 19 | this.objectUtility = objectUtility; 20 | this.stringUtility = stringUtility; 21 | } 22 | 23 | search(object: any, search: string, caseSensitive?: boolean): boolean { 24 | if (this.objectUtility.isNullOrEmpty(search)) { 25 | return true; 26 | } 27 | 28 | if (_.isObject(object)) { 29 | let values: any = _.values(object); 30 | return _.some(values, (value: any): boolean => { return this.search(value, search, caseSensitive); }); 31 | } else { 32 | let dataString: string = this.objectUtility.toString(object); 33 | 34 | if (!caseSensitive) { 35 | search = search.toLowerCase(); 36 | dataString = dataString.toLowerCase(); 37 | } 38 | 39 | return this.stringUtility.contains(dataString, search); 40 | } 41 | } 42 | 43 | tokenizedSearch(object: any, search: string, caseSensitive?: boolean): boolean { 44 | if (search == null) { 45 | return true; 46 | } 47 | 48 | return _.every(search.split(' '), (subsearch: string): boolean => { 49 | return this.search(object, subsearch, caseSensitive); 50 | }); 51 | } 52 | } 53 | 54 | export const searchUtility: SearchUtility = new SearchUtility(objectUtility, stringUtility); 55 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataService/dataService.md: -------------------------------------------------------------------------------- 1 | ## Data service 2 | This is the base use case for defining a new contract. Simple resources support basic getList, getDetail, create, update, and delete requests. 3 | 4 | ### Options 5 | 6 | #### `useDeepSearch (default: false)` 7 | If true, `getList` will make a POST request to send the search params along with the request body. The return data will be expected to be wrapped in a return object, as specified by generic typing 8 | 9 | `endpoint`, `interface`, `mockData`, `useMock`, `logRequests`, `searchParams`, `transform`. 10 | 11 | See [dataContract](../baseDataService.md) for details on the base options. 12 | 13 | ### Interface 14 | The following functions are available for consumers of the data service. 15 | 16 | #### `getList(params?: TSearchParams): angular.IPromise` 17 | Makes a GET request against the base endpoint with optional search params appended to the query string. If `useDeepSearch` is specified, use a POST request instead. The return signature is typed with `TReturnType` to accomodate indlucing other data, such as a `count`. 18 | 19 | #### `getDetail(id: number): angular.IPromise` 20 | Makes a GET request against `/endpoint/:id` to get a single item. 21 | 22 | #### `create(domainObject: TDataType): angular.IPromise` 23 | Makes a POST request against the base endpoint to create a new item. Returns the new item after creation. 24 | 25 | #### `update(domainObject: TDataType): angular.IPromise` 26 | Makes a PUT request against `/endpoint/:id` to update an existing item. Returns the updated item. 27 | 28 | #### `delete(domainObject: TDataType): angular.IPromise` 29 | Makes a DELETE request against `/endpoint/:id` to delete an existing item. 30 | 31 | #### `version(versionNumber: number): DataService` 32 | Returns a clone of the data service with the version number injected into the url as `/v{{versionNumber}}` directly after the first `/api`. 33 | -------------------------------------------------------------------------------- /source/services/dataContracts/converters/converters.ts: -------------------------------------------------------------------------------- 1 | export * from './aliasConverter/aliasConverter'; 2 | export * from './dateConverter/dateConverter'; 3 | export * from './enumConverter/enumConverter'; 4 | export * from './timeConverter/timeConverter'; 5 | 6 | import * as _ from 'lodash'; 7 | import { objectUtility } from '../../object/object.service'; 8 | 9 | export interface IConverter { 10 | fromServer(raw: any, parent?: any): TDataType; 11 | toServer(data: TDataType, parent?: any): any, 12 | } 13 | 14 | export interface ITransformMapping { 15 | [index: string]: IConverter | ITransformMapping; 16 | } 17 | 18 | export interface IConverterService { 19 | applyTransform(data: any, transform: IConverter | ITransformMapping, toServer: boolean): any; 20 | } 21 | 22 | export class ConverterService { 23 | applyTransform(data: any, transform: IConverter | ITransformMapping, toServer: boolean, parent?: any): any { 24 | if (transform == null || (parent == null && objectUtility.isNullOrEmpty(data))) { 25 | return data; 26 | } 27 | 28 | if (_.isArray(data)) { 29 | return _.map(data, (item: any): any => { return this.applyTransform(item, transform, toServer); }); 30 | } 31 | 32 | if (this.isConverter(transform)) { 33 | let transformFunc: { (data: any, parent?: any): any } = toServer 34 | ? (>transform).toServer 35 | : (>transform).fromServer; 36 | return transformFunc(data, parent); 37 | } else { 38 | let mappedData: any = _.clone(data); 39 | _.each(transform, (childTransform: any, key: string): any => { 40 | mappedData[key] = this.applyTransform(_.get(mappedData, key), childTransform, toServer, mappedData); 41 | }); 42 | return mappedData; 43 | } 44 | } 45 | 46 | private isConverter(object: any): boolean { 47 | return _.isFunction(object.fromServer) 48 | || _.isFunction(object.toServer); 49 | } 50 | } 51 | 52 | export let converterService: IConverterService = new ConverterService(); 53 | -------------------------------------------------------------------------------- /source/services/notification/notification.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { INotificationService, NotificationService } from './notification.service'; 2 | 3 | 4 | interface IMockWindow { 5 | alert: sinon.SinonSpy; 6 | } 7 | 8 | interface IMockLogger { 9 | log: sinon.SinonSpy; 10 | } 11 | 12 | describe('notification', () => { 13 | let notification: INotificationService; 14 | let mockWindow: IMockWindow; 15 | let mockLogger: IMockLogger; 16 | 17 | beforeEach(() => { 18 | mockWindow = { 19 | alert: sinon.spy(), 20 | }; 21 | 22 | mockLogger = { 23 | log: sinon.spy(), 24 | }; 25 | 26 | notification = new NotificationService(mockWindow, mockLogger); 27 | }); 28 | 29 | it('should call notifier to show an info notification', (): void => { 30 | notification.info('message'); 31 | sinon.assert.calledOnce(mockWindow.alert); 32 | sinon.assert.calledWith(mockWindow.alert, 'message'); 33 | sinon.assert.calledOnce(mockLogger.log); 34 | sinon.assert.calledWith(mockLogger.log, 'message'); 35 | }); 36 | 37 | it('should call notifier to show a warning notification', (): void => { 38 | notification.warning('message'); 39 | sinon.assert.calledOnce(mockWindow.alert); 40 | sinon.assert.calledWith(mockWindow.alert, 'message'); 41 | sinon.assert.calledOnce(mockLogger.log); 42 | sinon.assert.calledWith(mockLogger.log, 'message'); 43 | }); 44 | 45 | it('should call notifier to show an error notification', (): void => { 46 | notification.error('message'); 47 | sinon.assert.calledOnce(mockWindow.alert); 48 | sinon.assert.calledWith(mockWindow.alert, 'message'); 49 | sinon.assert.calledOnce(mockLogger.log); 50 | sinon.assert.calledWith(mockLogger.log, 'message'); 51 | }); 52 | 53 | it('should call notifier to show a success notification', (): void => { 54 | notification.success('message'); 55 | sinon.assert.calledOnce(mockWindow.alert); 56 | sinon.assert.calledWith(mockWindow.alert, 'message'); 57 | sinon.assert.calledOnce(mockLogger.log); 58 | sinon.assert.calledWith(mockLogger.log, 'message'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /system.config.js: -------------------------------------------------------------------------------- 1 | var map = { 2 | '@angular': 'node_modules/@angular', 3 | 'angular': 'node_modules/angular', 4 | 'angular-mocks': 'node_modules/angular-mocks/angular-mocks', 5 | 'angular2-uuid': 'node_modules/angular2-uuid', 6 | 'jquery': 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.1/jquery.min.js', 7 | 'lodash': 'node_modules/lodash/index', 8 | 'moment': 'node_modules/moment/moment', 9 | 'moment-timezone': 'node_modules/moment-timezone/builds/moment-timezone-with-data.min', 10 | 'ng-wig': 'node_modules/ng-wig', 11 | 'rl-async-testing': 'node_modules/rl-async-testing', 12 | 'rl-http': 'node_modules/rl-http', 13 | 'rxjs': 'node_modules/rxjs', 14 | }; 15 | 16 | var angularPackageNames = [ 17 | 'core', 18 | 'compiler', 19 | 'common', 20 | 'platform-browser', 21 | 'platform-browser-dynamic', 22 | 'http', 23 | ]; 24 | 25 | var defaultPackages = [ 26 | 'rl-async-testing', 27 | 'rl-http', 28 | ]; 29 | 30 | var meta = { 31 | 'jquery': { 32 | build: false, 33 | }, 34 | 'angular': { 35 | build: false, 36 | }, 37 | }; 38 | 39 | var packages = { 40 | 'source': { 41 | defaultExtension: 'js', 42 | }, 43 | 'node_modules': { 44 | defaultExtension: 'js', 45 | }, 46 | 'angular2-uuid': { 47 | main: 'index.js', 48 | }, 49 | 'rxjs': { 50 | main: 'Rx.js', 51 | }, 52 | 'angular': { 53 | main: 'index.js', 54 | }, 55 | }; 56 | 57 | function setAngularPackage(packageName) { 58 | map[`@angular/${packageName}`] = `node_modules/@angular/${packageName}/bundles/${packageName}.umd.js`; 59 | } 60 | 61 | function setAngularTestingPackage(packageName) { 62 | map[`@angular/${packageName}/testing`] = `node_modules/@angular/${packageName}/bundles/${packageName}-testing.umd.js`; 63 | } 64 | 65 | function setDefaultPackage(packageName) { 66 | packages[packageName] = { main: 'index.js' }; 67 | } 68 | 69 | angularPackageNames.forEach(setAngularPackage); 70 | angularPackageNames.forEach(setAngularTestingPackage); 71 | defaultPackages.forEach(setDefaultPackage); 72 | 73 | System.config({ 74 | meta: meta, 75 | map: map, 76 | packages: packages, 77 | }); 78 | -------------------------------------------------------------------------------- /source/services/string/string.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { IStringUtility, StringUtility } from './string.service'; 2 | 3 | 4 | describe('stringUtility', () => { 5 | let stringUtility: IStringUtility; 6 | 7 | beforeEach(() => { 8 | stringUtility = new StringUtility(); 9 | }); 10 | 11 | describe('toNumber', (): void => { 12 | it('should convert a string to a number', (): void => { 13 | expect(stringUtility.toNumber('5')).to.equal(5); 14 | expect(stringUtility.toNumber('3')).to.equal(3); 15 | expect(stringUtility.toNumber('1.25')).to.equal(1.25); 16 | }); 17 | }); 18 | 19 | describe('contains', (): void => { 20 | it('should return true if the substring is contained within the string', (): void => { 21 | expect(stringUtility.contains('my string', 'my')).to.be.true; 22 | expect(stringUtility.contains('123', '1')).to.be.true; 23 | expect(stringUtility.contains('', null)).to.be.true; 24 | expect(stringUtility.contains('my string', '')).to.be.true; 25 | }); 26 | 27 | it('should return false if the substring is not contained within the string', (): void => { 28 | expect(stringUtility.contains('my string', 'my val')).to.be.false; 29 | expect(stringUtility.contains('123', '4')).to.be.false; 30 | expect(stringUtility.contains('my string', 'my string 1')).to.be.false; 31 | }); 32 | }); 33 | 34 | describe('replaceAll', (): void => { 35 | it('should replace all occurances of some given text with another inside a string', (): void => { 36 | expect(stringUtility.replaceAll('hello world', 'foo', 'bar')).to.equal('hello world'); 37 | expect(stringUtility.replaceAll('fooHellofooWorldfoo', 'foo', 'bar')).to.equal('barHellobarWorldbar'); 38 | }); 39 | }); 40 | 41 | describe('substitute', (): void => { 42 | it('should substitute strings with their positional placeholder value in other strings', (): void => { 43 | expect(stringUtility.substitute('hello world', 'foo')).to.equal('hello world'); 44 | expect(stringUtility.substitute('hello {0} world {1}', 'foo', 'bar')).to.equal('hello foo world bar'); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /source/services/validation/validator.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { ISimpleValidator, IErrorHandler, IUnregisterFunction, IValidationHandler } from './validationTypes'; 4 | 5 | export class Validator implements ISimpleValidator { 6 | private validationHandlers: { [index: string]: IValidationHandler } = {}; 7 | private nextKey: number = 0; 8 | 9 | constructor(private showError: IErrorHandler) {} 10 | 11 | validate(value?: any): boolean { 12 | let isValid: boolean = true; 13 | 14 | _.each(this.validationHandlers, (handler: IValidationHandler): boolean => { 15 | var isActive: boolean = this.isActive(handler); 16 | 17 | if (isActive && !handler.validate(value)) { 18 | isValid = false; 19 | 20 | let error: string = this.errorMessage(handler); 21 | this.showError(error, handler.name); 22 | 23 | return false; 24 | } 25 | }); 26 | 27 | return isValid; 28 | } 29 | 30 | getErrorCount(): number { 31 | return _.reduce(this.validationHandlers, (count: number, handler: IValidationHandler): number => { 32 | var isActive: boolean = this.isActive(handler); 33 | 34 | if (isActive && !handler.validate()) { 35 | count++; 36 | } 37 | 38 | return count; 39 | }, 0); 40 | } 41 | 42 | registerValidationHandler(handler: IValidationHandler): IUnregisterFunction { 43 | var currentKey: number = this.nextKey; 44 | this.nextKey++; 45 | this.validationHandlers[currentKey] = handler; 46 | 47 | return (): void => { 48 | this.unregister(currentKey); 49 | }; 50 | } 51 | 52 | private unregister(key: number): void { 53 | delete this.validationHandlers[key]; 54 | } 55 | 56 | private isActive(handler: IValidationHandler): boolean { 57 | return (_.isFunction(handler.isActive) && (<{(): boolean}>handler.isActive)()) 58 | || handler.isActive == null 59 | || handler.isActive === true; 60 | } 61 | 62 | private errorMessage(handler: IValidationHandler): string { 63 | return _.isFunction(handler.errorMessage) 64 | ? (<{ (): string }>handler.errorMessage)() 65 | : handler.errorMessage; 66 | } 67 | } -------------------------------------------------------------------------------- /source/services/dataContracts/singletonDataService/parent/parentSingletonData.service.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { IHttpUtility } from 'rl-http'; 3 | 4 | import { ISingletonDataService, SingletonDataService } from '../../singletonDataService/singletonData.service'; 5 | import { IDataService } from '../../dataService/data.service'; 6 | import { IDataServiceView } from '../../dataService/view/dataServiceView'; 7 | import { IParentSingletonResourceParams } from '../../resourceBuilder/resourceBuilder.service'; 8 | 9 | export interface IParentSingletonDataService 10 | extends ISingletonDataService{ 11 | childContracts(): TResourceDictionaryType; 12 | } 13 | 14 | export interface IParentSingletonFromViewResourceParams extends IParentSingletonResourceParams { 15 | parentId?: number; 16 | } 17 | 18 | export class ParentSingletonDataService 19 | extends SingletonDataService implements IParentSingletonDataService { 20 | 21 | private resourceDictionaryBuilder: { (): TResourceDictionaryType }; 22 | private parentId: number; 23 | 24 | constructor(http: IHttpUtility, options: IParentSingletonFromViewResourceParams) { 25 | super(http, options); 26 | this.resourceDictionaryBuilder = options.resourceDictionaryBuilder; 27 | this.parentId = options.parentId; 28 | } 29 | 30 | childContracts(): TResourceDictionaryType { 31 | let dictionary: {[index: string]: any} = this.resourceDictionaryBuilder(); 32 | return _.mapValues(dictionary, (dataService: IDataServiceView): ISingletonDataService | IDataService => { 33 | let contract: any; 34 | if (_.isFunction(dataService.AsSingleton)) { 35 | contract = dataService.AsSingleton(this.parentId); 36 | } else { 37 | contract = dataService; 38 | } 39 | 40 | contract.url = this.url + contract.endpoint; 41 | 42 | return contract; 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /source/services/redirect/redirect.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { RedirectService, IRedirectService } from './redirect.service'; 2 | 3 | interface IMockWindow { 4 | location: { 5 | pathname: string; 6 | search: string; 7 | }; 8 | open: sinon.SinonSpy; 9 | focus: sinon.SinonSpy; 10 | history: { back: sinon.SinonSpy }; 11 | } 12 | 13 | describe('RedirectService', () => { 14 | let redirectService: IRedirectService; 15 | let mockWindow: IMockWindow; 16 | 17 | beforeEach(() => { 18 | mockWindow = { 19 | location: null, 20 | open: sinon.spy(() => mockWindow), 21 | focus: sinon.spy(), 22 | history: { back: sinon.spy() }, 23 | }; 24 | 25 | redirectService = new RedirectService(mockWindow); 26 | }); 27 | 28 | describe('getCurrentLocationAsParam', () => { 29 | it('should get location as a query string', () => { 30 | mockWindow.location = { 31 | pathname: '/path', 32 | search: '?search=true&Die=angularUno' 33 | }; 34 | 35 | const locationAsParam: string = redirectService.getCurrentLocationAsParam(); 36 | 37 | expect(locationAsParam).to.equal('%2Fpath%3Fsearch%3Dtrue%26Die%3DangularUno'); 38 | }); 39 | 40 | it('should get location without query string', () => { 41 | mockWindow.location = { 42 | pathname: '/path', 43 | search: null, 44 | }; 45 | 46 | const locationAsParam: string = redirectService.getCurrentLocationAsParam(); 47 | 48 | expect(locationAsParam).to.equal('%2Fpath'); 49 | }); 50 | }); 51 | 52 | describe('to', () => { 53 | it('should redirect to the specified target', () => { 54 | redirectService.to('/some/path'); 55 | 56 | sinon.assert.calledOnce(mockWindow.open); 57 | sinon.assert.calledWith(mockWindow.open, '/some/path', '_self'); 58 | }); 59 | 60 | it('should redirect to the specified target in new window', () => { 61 | redirectService.to('/some/path', true); 62 | 63 | sinon.assert.calledOnce(mockWindow.open); 64 | sinon.assert.calledWith(mockWindow.open, '/some/path', '_blank'); 65 | 66 | sinon.assert.calledOnce(mockWindow.focus); 67 | }); 68 | }); 69 | 70 | describe('back', () => { 71 | it('should trigger back on the window history', () => { 72 | redirectService.back(); 73 | sinon.assert.calledOnce(mockWindow.history.back); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataService/parent/parentData.service.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import { HttpUtility } from 'rl-http'; 3 | 4 | import { IArrayUtility } from '../../../array/array.service'; 5 | import { IDataService, DataService, IBaseDomainObject } from '../data.service'; 6 | import { IDataServiceView } from '../view/dataServiceView'; 7 | import { ISingletonDataService } from '../../singletonDataService/singletonData.service'; 8 | import { IParentResourceParams } from '../../resourceBuilder/resourceBuilder.service'; 9 | 10 | export interface IParentDataService 11 | extends IDataService{ 12 | childContracts(id?: number): TResourceDictionaryType; 13 | } 14 | 15 | export class ParentDataService 16 | extends DataService implements IParentDataService { 17 | 18 | resourceDictionaryBuilder: { (): TResourceDictionaryType }; 19 | 20 | constructor(http: HttpUtility 21 | , array: IArrayUtility 22 | , options: IParentResourceParams) { 23 | super(http, array, options); 24 | this.resourceDictionaryBuilder = options.resourceDictionaryBuilder; 25 | } 26 | 27 | childContracts(id?: number): TResourceDictionaryType { 28 | if (_.isUndefined(id)) { 29 | let dictionary: TResourceDictionaryType = this.resourceDictionaryBuilder(); 30 | _.each(dictionary, (dataService: any): void => { 31 | dataService.url = this.url + dataService.endpoint; 32 | }); 33 | return dictionary; 34 | } else { 35 | let dictionary: {[index: string]: any} = this.resourceDictionaryBuilder(); 36 | return _.mapValues(dictionary, (dataService: IDataServiceView): ISingletonDataService | IDataService => { 37 | let contract: any; 38 | if (_.isFunction(dataService.AsSingleton)) { 39 | contract = dataService.AsSingleton(id); 40 | } else { 41 | contract = dataService; 42 | } 43 | 44 | contract.url = this.url + '/' + id + contract.endpoint; 45 | 46 | return contract; 47 | }); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /source/services/timezone/timezone.service.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import * as _ from 'lodash'; 3 | 4 | import { timezones, ITimezone } from './timezone.enum'; 5 | 6 | export * from './timezone.enum'; 7 | 8 | export interface ITimezoneService { 9 | getTimezone(isoString: string): ITimezone; 10 | getMomentTimezone(isoString: string): string; 11 | setCurrentTimezone(offset: string): void; 12 | currentTimezone: ITimezone; 13 | buildMomentWithTimezone(dateValue: string | moment.Moment, timezone: ITimezone, format?: string): moment.Moment; 14 | } 15 | 16 | export class TimezoneService implements ITimezoneService { 17 | private _currentTimezone: ITimezone; 18 | 19 | get currentTimezone(): ITimezone { 20 | return this._currentTimezone; 21 | } 22 | 23 | setCurrentTimezone(offset: string): void { 24 | let timezone: ITimezone = timezones.get(offset); 25 | this._currentTimezone = timezone; 26 | } 27 | 28 | getTimezone(isoString: string): ITimezone { 29 | if (isoString == null) { 30 | return null; 31 | } 32 | 33 | let offsetText: string = '-' + _.last(isoString.split('-')); 34 | return timezones.get(offsetText); 35 | } 36 | 37 | getMomentTimezone(isoString: string): string { 38 | let timezone: ITimezone = this.getTimezone(isoString); 39 | return timezone != null 40 | ? timezone.momentName 41 | : null; 42 | } 43 | 44 | buildMomentWithTimezone(dateValue: string | moment.Moment, timezone: ITimezone, format?: string): moment.Moment { 45 | let previousTimezone: ITimezone; 46 | let previousOffset: number; 47 | 48 | if (_.isString(dateValue)) { 49 | previousTimezone = this.getTimezone(dateValue); 50 | } 51 | 52 | if (previousTimezone != null) { 53 | previousOffset = previousTimezone.offsetMinutes; 54 | } else { 55 | previousOffset = moment(dateValue, format).utcOffset(); 56 | } 57 | 58 | let dateWithNewTimezone: moment.Moment = moment(dateValue, format).tz(timezone.momentName); 59 | 60 | let offsetDifferenceBetweenOriginalAndNewTimezones: number = previousOffset - dateWithNewTimezone.utcOffset(); 61 | 62 | dateWithNewTimezone.add(offsetDifferenceBetweenOriginalAndNewTimezones, 'minutes'); 63 | return dateWithNewTimezone; 64 | } 65 | } 66 | 67 | export const timezoneService: ITimezoneService = new TimezoneService(); 68 | -------------------------------------------------------------------------------- /source/services/timezone/timezone.enum.tests.ts: -------------------------------------------------------------------------------- 1 | import { defaultFormats } from '../date/index'; 2 | 3 | import * as moment from 'moment'; 4 | import 'moment-timezone'; 5 | 6 | import { timezones } from './timezone.enum'; 7 | 8 | describe('timezone enum', (): void => { 9 | it('should handle daylight savings time', (): void => { 10 | let dateWithoutDaylightSavings: moment.Moment = moment('2016-2-1T12:00:00-07:00', defaultFormats.isoFormat).tz(timezones.PST.momentName); 11 | let dateWithDaylightSavings: moment.Moment = moment('2016-4-1T12:00:00-07:00', defaultFormats.isoFormat).tz(timezones.PST.momentName); 12 | expect(dateWithoutDaylightSavings.format('Z')).to.equal('-08:00'); 13 | expect(dateWithDaylightSavings.format('Z')).to.equal('-07:00'); 14 | }); 15 | 16 | it('should return the appropriate timezone enum', (): void => { 17 | let ast_timezone: moment.Moment = moment('2016-1-2T12:00:00-04:00', defaultFormats.isoFormat).tz(timezones.AST.momentName); 18 | let est_timezone: moment.Moment = moment('2016-1-2T12:00:00-04:00', defaultFormats.isoFormat).tz(timezones.EST.momentName); 19 | let cst_timezone: moment.Moment = moment('2016-1-2T12:00:00-04:00', defaultFormats.isoFormat).tz(timezones.CST.momentName); 20 | let mst_timezone: moment.Moment = moment('2016-1-2T12:00:00-04:00', defaultFormats.isoFormat).tz(timezones.MST.momentName); 21 | let pst_timezone: moment.Moment = moment('2016-1-2T12:00:00-04:00', defaultFormats.isoFormat).tz(timezones.PST.momentName); 22 | let akst_timezone: moment.Moment = moment('2016-1-2T12:00:00-04:00', defaultFormats.isoFormat).tz(timezones.AKST.momentName); 23 | let hast_timezone: moment.Moment = moment('2016-1-2T12:00:00-04:00', defaultFormats.isoFormat).tz(timezones.HAST.momentName); 24 | 25 | expect(ast_timezone.format('Z')).to.equal('-04:00'); 26 | expect(est_timezone.format('Z')).to.equal('-05:00'); 27 | expect(cst_timezone.format('Z')).to.equal('-06:00'); 28 | expect(mst_timezone.format('Z')).to.equal('-07:00'); 29 | expect(pst_timezone.format('Z')).to.equal('-08:00'); 30 | expect(akst_timezone.format('Z')).to.equal('-09:00'); 31 | expect(hast_timezone.format('Z')).to.equal('-10:00'); 32 | }); 33 | 34 | it('should get a timezone from a moment timezone', (): void => { 35 | expect(timezones.get(timezones.MST.momentName)).to.equal(timezones.MST) 36 | }); 37 | }); -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [true, 4 | "parameters", 5 | "arguments", 6 | "statements"], 7 | "ban": false, 8 | "class-name": true, 9 | "curly": true, 10 | "eofline": true, 11 | "forin": true, 12 | "indent": [true, 4], 13 | "interface-name": true, 14 | "jsdoc-format": true, 15 | "label-position": true, 16 | "label-undefined": true, 17 | "max-line-length": [false], 18 | "no-any": false, 19 | "no-arg": true, 20 | "no-bitwise": true, 21 | "no-console": [true, 22 | "debug", 23 | "info", 24 | "time", 25 | "timeEnd", 26 | "trace" 27 | ], 28 | "no-construct": true, 29 | "no-constructor-vars": false, 30 | "no-debugger": true, 31 | "no-duplicate-key": true, 32 | "no-duplicate-variable": true, 33 | "no-empty": true, 34 | "no-eval": true, 35 | "no-string-literal": true, 36 | "no-switch-case-fall-through": true, 37 | "no-trailing-comma": false, 38 | "no-trailing-whitespace": true, 39 | "no-unreachable": true, 40 | "no-unused-expression": true, 41 | "no-unused-variable": true, 42 | "no-use-before-declare": true, 43 | "no-var-requires": true, 44 | "one-line": [true, 45 | "check-open-brace", 46 | "check-catch", 47 | "check-else", 48 | "check-whitespace" 49 | ], 50 | "quotemark": [true, "single"], 51 | "radix": true, 52 | "semicolon": true, 53 | "switch-default": true, 54 | "triple-equals": [true, "allow-null-check"], 55 | "typedef": [true, 56 | "call-signature", 57 | "parameter", 58 | "property-declaration", 59 | "variable-declaration", 60 | "member-variable-declaration" 61 | ], 62 | "typedef-whitespace": [true, { 63 | "call-signature": "nospace", 64 | "index-signature": "nospace", 65 | "parameter": "nospace", 66 | "property-declaration": "nospace", 67 | "variable-declaration": "nospace" 68 | }], 69 | "use-strict": [true, 70 | "check-module", 71 | "check-function" 72 | ], 73 | "variable-name": false, 74 | "whitespace": [true, 75 | "check-branch", 76 | "check-decl", 77 | "check-operator", 78 | "check-separator", 79 | "check-type" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /source/filters/filters.md: -------------------------------------------------------------------------------- 1 | ## Filters 2 | Specifies a generic interface for filters. Also includes an abstract class for SerializableFilters that provides more complex behavior. 3 | 4 | ### IFilter 5 | The base filter type is just a predicate that specifies for a given item whether it should be shown or not. 6 | 7 | #### `filter(item: TItemType): boolean` 8 | Returns true if the item should be shown. 9 | 10 | ### IFilterWithCounts 11 | A slightly more advanced filter that checks for a given data set how many items should be visible if the filter is applied. 12 | 13 | #### `updateOptionCounts(item: TItemType[]): void` 14 | Sets the counts of the filter and/or any possible filter states we want to check. 15 | 16 | ### ISerializableFilter 17 | A serializable filter represents a filter where the current value can be serialized to a json object. The base implementation also allows consumers to subscribe for changes to the serialized filter value. 18 | 19 | #### `type` 20 | The `type` is used as the key when constructing multiple filters into a collection for making a search request against the server. 21 | 22 | #### `serialize(): TFilterData` 23 | Returns the json representation of the current filter value. 24 | 25 | #### `subscribe(onValueChange: { (newValue: TFilterData): void }): Rx.Subscriber` 26 | Subscribes to an observable stream with any changes in the filter values. 27 | 28 | ### SerializableFilter base class 29 | The base class for serializable filters provides functionality for exposing an observable stream of filter values to the consumer. The subclass is responsible for tracking when a change occurs and pushing the change to the stream. He must also provide the filter predicate. 30 | 31 | #### `serialize(): TFilterData` 32 | By default, the base class returns the whole filter class as a filter value object. The consumer can override this to provide a more succinct summary of the current selection value. 33 | 34 | #### `onChange(force: boolean = true): void` 35 | The filter implementation should call onChange to notify for changes to the filter. The base class will then push these changes to the observable stream. By default, all calls to onChange will be pushed to the stream. If the caller specifies `force: false` then the filter will deep compare the current filter value with the last value pushed to the stream. In this case the new value is only pushed if there are changes. -------------------------------------------------------------------------------- /source/services/validation/observableValidator.tests.ts: -------------------------------------------------------------------------------- 1 | import { Subject, Observable } from 'rxjs'; 2 | 3 | import { ObservableValidator } from './observableValidator'; 4 | 5 | describe('ObservableValidator', () => { 6 | let validator: ObservableValidator; 7 | 8 | beforeEach(() => { 9 | validator = new ObservableValidator(); 10 | }); 11 | 12 | describe('validate', () => { 13 | it('should return the error on the first validator', () => { 14 | const errorString = 'error123'; 15 | const firstHandler = { 16 | validate: () => Observable.of(errorString), 17 | }; 18 | const secondHandler = { 19 | validate: () => Observable.of('other'), 20 | }; 21 | validator.registerValidationHandler(firstHandler); 22 | validator.registerValidationHandler(secondHandler); 23 | let result; 24 | 25 | validator.validate().subscribe(valid => result = valid); 26 | 27 | expect(result).to.equal(errorString); 28 | }); 29 | 30 | it('should return null if no errors are found', () => { 31 | const firstHandler = { 32 | validate: () => Observable.of(null), 33 | }; 34 | validator.registerValidationHandler(firstHandler); 35 | let result; 36 | 37 | validator.validate().subscribe(valid => result = valid); 38 | 39 | expect(result).to.not.exist; 40 | }); 41 | 42 | it('should return null if no validators are registered', () => { 43 | let result; 44 | 45 | validator.validate().subscribe(valid => result = valid); 46 | 47 | expect(result).to.be.null; 48 | }); 49 | }); 50 | 51 | describe('isActive', () => { 52 | it('should ignore a validator with isActive set as false', () => { 53 | const firstHandler = { 54 | validate: () => Observable.of('error'), 55 | isActive: false, 56 | }; 57 | validator.registerValidationHandler(firstHandler); 58 | let result; 59 | 60 | validator.validate().subscribe(valid => result = valid); 61 | 62 | expect(result).to.not.exist; 63 | }); 64 | 65 | it('should get the validation error when the validator becomes active', () => { 66 | const error = 'error'; 67 | const firstHandler = { 68 | validate: () => Observable.of(error), 69 | isActive: () => firstHandler.activeStream, 70 | activeStream: new Subject(), 71 | }; 72 | validator.registerValidationHandler(firstHandler); 73 | let result; 74 | 75 | validator.validate().subscribe(valid => result = valid); 76 | 77 | firstHandler.activeStream.next(false); 78 | 79 | expect(result).to.not.exist; 80 | 81 | firstHandler.activeStream.next(true); 82 | 83 | expect(result).to.equal(error); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /source/services/genericSearchFilter/genericSearchFilter.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import * as _ from 'lodash'; 4 | 5 | import { IObjectUtility, ObjectUtility } from '../object/object.service'; 6 | 7 | import { IStringUtility, StringUtility } from '../string/string.service'; 8 | import { searchUtility } from '../search/search.service'; 9 | 10 | import { ISerializableFilter, SerializableFilter } from '../../filters/filter'; 11 | 12 | export var filterName: string = 'search'; 13 | 14 | export interface IGenericSearchFilter extends ISerializableFilter { 15 | type: string; 16 | searchText: string; 17 | minSearchLength: number; 18 | caseSensitive: boolean; 19 | filter(item: TItemType): boolean; 20 | } 21 | 22 | export class GenericSearchFilter extends SerializableFilter implements IGenericSearchFilter { 23 | type: string = filterName; 24 | minSearchLength: number = 1; 25 | caseSensitive: boolean = false; 26 | private _searchText: string; 27 | 28 | constructor(protected object: IObjectUtility 29 | , private string: IStringUtility 30 | , private tokenized: boolean) { 31 | super(); 32 | } 33 | 34 | get searchText(): string { 35 | return this._searchText; 36 | } 37 | 38 | set searchText(value: string) { 39 | this._searchText = value; 40 | this.onChange(false); 41 | } 42 | 43 | serialize(): string { 44 | return this.searchText != null && this.searchText.length >= this.minSearchLength 45 | ? this.searchText 46 | : null; 47 | } 48 | 49 | filter(item: TItemType): boolean { 50 | if (this.object.isNullOrEmpty(this.searchText) || this.searchText.length < this.minSearchLength) { 51 | return true; 52 | } 53 | 54 | if (this.tokenized) { 55 | return searchUtility.tokenizedSearch(item, this.searchText, this.caseSensitive); 56 | } 57 | 58 | return searchUtility.search(item, this.searchText, this.caseSensitive); 59 | } 60 | } 61 | 62 | export interface IGenericSearchFilterFactory { 63 | getInstance(tokenized?: boolean): IGenericSearchFilter; 64 | } 65 | 66 | @Injectable() 67 | export class GenericSearchFilterFactory implements IGenericSearchFilterFactory { 68 | private objectUtility: IObjectUtility; 69 | private stringUtility: IStringUtility; 70 | 71 | constructor(objectUtility: ObjectUtility 72 | , stringUtility: StringUtility) { 73 | this.objectUtility = objectUtility; 74 | this.stringUtility = stringUtility; 75 | } 76 | 77 | getInstance(tokenized?: boolean): IGenericSearchFilter { 78 | return new GenericSearchFilter(this.objectUtility, this.stringUtility, tokenized); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /source/services/test/chaiMoment.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import * as Chai from 'chai'; 3 | 4 | import { defaultFormats } from '../date/dateTimeFormatStrings'; 5 | 6 | let chai: Chai.ChaiStatic = (window).chai; 7 | 8 | if (chai) { 9 | chai.Assertion.addMethod('sameMoment', equalMoment); 10 | chai.Assertion.addMethod('equalMoment', equalMoment); 11 | 12 | chai.Assertion.addMethod('beforeMoment', function(expected, granularity) { 13 | var obj = this._obj 14 | var objMoment = moment(obj) 15 | var expectedMoment = moment(expected) 16 | this.assert( 17 | objMoment.isBefore(expectedMoment, granularity) 18 | , 'expected ' + objMoment.format(defaultFormats.dateTimeFormat + ' z') + ' to be before ' + expectedMoment.format(defaultFormats.dateTimeFormat + ' z') + (granularity ? ' (granularity: ' + granularity + ')' : '') 19 | , 'expected ' + objMoment.format(defaultFormats.dateTimeFormat + ' z') + ' not to be before ' + expectedMoment.format(defaultFormats.dateTimeFormat + ' z') + (granularity ? ' (granularity: ' + granularity + ')' : '') 20 | , expected 21 | , obj 22 | , true 23 | ) 24 | }); 25 | 26 | chai.Assertion.addMethod('afterMoment', function(expected, granularity) { 27 | var obj = this._obj 28 | var objMoment = moment(obj) 29 | var expectedMoment = moment(expected) 30 | this.assert( 31 | objMoment.isAfter(expectedMoment, granularity) 32 | , 'expected ' + objMoment.format(defaultFormats.dateTimeFormat + ' z') + ' to be after ' + expectedMoment.format(defaultFormats.dateTimeFormat + ' z') + (granularity ? ' (granularity: ' + granularity + ')' : '') 33 | , 'expected ' + objMoment.format(defaultFormats.dateTimeFormat + ' z') + ' not to be after ' + expectedMoment.format(defaultFormats.dateTimeFormat + ' z') + (granularity ? ' (granularity: ' + granularity + ')' : '') 34 | , expected 35 | , obj 36 | , true 37 | ) 38 | }); 39 | } 40 | 41 | function equalMoment(expected, granularity) { 42 | var obj = this._obj 43 | var objMoment = moment(obj) 44 | var expectedMoment = moment(expected) 45 | this.assert( 46 | objMoment.isSame(expectedMoment, granularity) 47 | , 'expected ' + objMoment.format(defaultFormats.dateTimeFormat + ' z') + ' not to be the same as ' + expectedMoment.format(defaultFormats.dateTimeFormat + ' z') + (granularity ? ' (granularity: ' + granularity + ')' : '') 48 | , 'expected ' + objMoment.format(defaultFormats.dateTimeFormat + ' z') + ' to be the same as ' + expectedMoment.format(defaultFormats.dateTimeFormat + ' z') + (granularity ? ' (granularity: ' + granularity + ')' : '') 49 | , expected 50 | , obj 51 | , true 52 | ) 53 | } -------------------------------------------------------------------------------- /config/karma.shared.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (karma) { 2 | return { 3 | basePath: '..', 4 | 5 | // frameworks to use 6 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 7 | frameworks: ['jasmine', 'chai', 'sinon'], 8 | 9 | // enable / disable watching file and executing tests whenever any file changes 10 | autoWatch: false, 11 | 12 | // Continuous Integration mode 13 | // if true, Karma captures browsers, runs the tests and exits 14 | singleRun: true, 15 | 16 | // level of logging 17 | // possible values: karma.LOG_DISABLE || karma.LOG_ERROR || karma.LOG_WARN || karma.LOG_INFO || karma.LOG_DEBUG 18 | logLevel: karma.LOG_INFO, 19 | 20 | // start these browsers 21 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 22 | browsers: ['ChromeNoSandbox'], 23 | 24 | // test results reporter to use 25 | // possible values: 'dots', 'progress' 26 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 27 | reporters: ['progress'], 28 | 29 | // enable / disable colors in the output (reporters and logs) 30 | colors: true, 31 | 32 | port: 2000, 33 | 34 | plugins: [ 35 | require('karma-jasmine'), 36 | require('karma-chai'), 37 | require('karma-sinon'), 38 | require('karma-chrome-launcher'), 39 | require('karma-firefox-launcher'), 40 | ], 41 | 42 | customLaunchers: { 43 | ChromeNoSandbox: { 44 | base: 'Chrome', 45 | flags: ['--no-sandbox'] 46 | } 47 | }, 48 | 49 | files: [ 50 | // polyfills 51 | { pattern: 'node_modules/es6-shim/es6-shim.js', included: true, watched: false }, 52 | { pattern: 'node_modules/reflect-metadata/Reflect.js', included: true, watched: false }, 53 | { pattern: 'node_modules/systemjs/dist/system.src.js', included: true, watched: false }, 54 | 'node_modules/zone.js/dist/zone.js', 55 | 'node_modules/zone.js/dist/long-stack-trace-zone.js', 56 | 'node_modules/zone.js/dist/proxy.js', 57 | 'node_modules/zone.js/dist/sync-test.js', 58 | 'node_modules/zone.js/dist/jasmine-patch.js', 59 | 'node_modules/zone.js/dist/async-test.js', 60 | 'node_modules/zone.js/dist/fake-async-test.js', 61 | 62 | // allow for importing these 63 | { pattern: 'node_modules/**/*', included: false, watched: false }, 64 | { pattern: 'system.config.js', included: false, watched: true }, 65 | 66 | // shim to run the unit tests 67 | { pattern: 'config/karma-test-shim.js', included: true, watched: true }, 68 | 69 | { pattern: 'source/**', included: false, watched: true } 70 | ], 71 | 72 | browserNoActivityTimeout: 100000, 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /source/services/observable/observable.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OpaqueToken, ClassProvider, ErrorHandler } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | 4 | // deprecated - use rxjs instead 5 | 6 | export interface IWatcher { 7 | action: IAction; 8 | event?: string; 9 | } 10 | 11 | export interface IAction { 12 | (...params: any[]): TReturnType; 13 | } 14 | 15 | export interface IUnregisterFunction { 16 | (): void; 17 | } 18 | 19 | export interface IObservableService { 20 | allowableEvents?: string[]; 21 | register(action: IAction, event?: string): IUnregisterFunction; 22 | register(action: IAction, event?: string): IUnregisterFunction; 23 | fire(event?: string, ...params: any[]): TReturnType[]; 24 | fire(event?: string, ...params: any[]): void; 25 | } 26 | 27 | @Injectable() 28 | export class ObservableService implements IObservableService { 29 | private errorHandler: ErrorHandler; 30 | private watchers: IWatcher[] = []; 31 | private nextKey: number = 0; 32 | allowableEvents: string[]; 33 | 34 | constructor(errorHandler: ErrorHandler) { 35 | this.errorHandler = errorHandler; 36 | } 37 | 38 | register(action: IAction, event?: string): IUnregisterFunction { 39 | if (!_.isFunction(action)) { 40 | this.errorHandler.handleError(new Error('Watcher must be a function')); 41 | return null; 42 | } 43 | 44 | if (this.allowableEvents != null && !_.find(this.allowableEvents, (e: string): boolean => { return e === event; })) { 45 | this.errorHandler.handleError(new Error('This event is not allowed. Events: ' + this.allowableEvents.join(', '))); 46 | return null; 47 | } 48 | 49 | let currentKey: number = this.nextKey; 50 | this.nextKey++; 51 | this.watchers[currentKey] = { 52 | action: action, 53 | event: event, 54 | }; 55 | 56 | return (): void => { 57 | this.unregister(currentKey); 58 | }; 59 | } 60 | 61 | fire(event?: string, ...params: any[]): TReturnType[] { 62 | return _(this.watchers).filter((watcher: IWatcher): boolean => { 63 | return watcher != null && watcher.event === event; 64 | }) 65 | .map((watcher: IWatcher): TReturnType => { 66 | return watcher.action.apply(this, params); 67 | }).value(); 68 | } 69 | 70 | private unregister(key: number): void { 71 | this.watchers[key] = null; 72 | } 73 | } 74 | 75 | export const observableToken: OpaqueToken = new OpaqueToken('Deprecated - a service for observables'); 76 | 77 | export const OBSERVABLE_PROVIDER: ClassProvider = { 78 | provide: observableToken, 79 | useClass: ObservableService, 80 | }; 81 | -------------------------------------------------------------------------------- /source/services/dataContracts/resourceBuilder/resourceBuilder.md: -------------------------------------------------------------------------------- 1 | ## Resource builder 2 | A service for building data services using data contract definitions. Used in conjunction with a ContractList class. This is primarily used to build functionality into the ContractLibrary base class. It's advisable to use the base class instance when building resources, as this will provide the ability to automatically build in the base url. 3 | 4 | Example usage: 5 | ``` 6 | import * as angular from 'angular'; 7 | 8 | import { services } from 'typescript-angular-utilities'; 9 | import __dataContracts = services.dataContracts; 10 | 11 | class DataServices extends __dataContracts.ContractLibrary { 12 | resource1: __dataContracts.IDataService; 13 | resource2: __dataContracts.IDataService; 14 | 15 | static $inject: string[] = [__dataContracts.builderServiceName]; 16 | constructor(baseResourceBuilder: __dataContracts.IBaseResourceBuilder) { 17 | super(baseResourceBuilder, 'www.example.com/api'); 18 | 19 | // using the builder directly 20 | this.resource1 = baseResourceBuilder.createResource({ 21 | // base url has to be specified by hand 22 | endpoint: 'www.example.com/api' + '/test', 23 | }); 24 | 25 | // using the base class - recommended 26 | this.resource2 = this.createResource({ 27 | // base url is injected automatically 28 | endpoint: '/test', 29 | }); 30 | } 31 | } 32 | ``` 33 | 34 | See [contractLibrary](../contractLibrary/contractLibrary.md) for details on the contract library base class. 35 | 36 | ### Options 37 | 38 | See [dataContract](../baseDataService.md) for details on options for data contracts. 39 | 40 | #### `createResource(options: IBaseResourceParams): IDataService` 41 | Build a standard [dataService](../dataService/dataService.md). 42 | 43 | #### `createResourceView(options: IBaseResourceParams): IDataServiceView` 44 | Build a [dataServiceView](../dataService/view/dataServiceView.md). 45 | 46 | #### `createParentResource(options: IParentResourceParams): IParentDataService` 47 | Build a [parentDataService](../dataService/parent/parentDataService.md). 48 | 49 | #### `createParentResourceView(options: IParentResourceParams): IParentDataService` 50 | Build a [parentDataServiceView](../dataService/view/parentDataServiceView.md). 51 | 52 | #### `createSingletonResource(options: ISingletonResourceParams): IParentDataService` 53 | Build a [singletonDataService](../singletonDataService/singletonDataService.md). 54 | 55 | #### `createParentSingletonResource(options: IParentSingletonResourceParams): IParentDataService` 56 | Build a [parentSingletonDataService](../singletonDataService/parent/parentSingletonDataService.md). 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-angular-utilities", 3 | "version": "4.1.15", 4 | "description": "Typescript utility classes published as angular services", 5 | "author": "Renovo Development Team", 6 | "main": "source/main.js", 7 | "typings": "source/main.d.ts", 8 | "keywords": [ 9 | "typescript", 10 | "utilities" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/RenovoSolutions/TypeScript-Angular-Utilities" 15 | }, 16 | "scripts": { 17 | "prepublish": "npm run build", 18 | "update": "npm install", 19 | "version": "gulp version -v ", 20 | "clean": "gulp clean", 21 | "build": "gulp clean && tsc && echo Build complete", 22 | "build.watch": "tsc -w", 23 | "test": "karma start config/karma.conf.js", 24 | "test.debug": "karma start config/karma.debug.conf.js", 25 | "test.tc": "karma start config/karma.tc.conf.js", 26 | "test.full": "karma start config/karma.full.conf.js", 27 | "build-test": "npm run build && npm run test", 28 | "build-test.watch": "npm run build && npm-run-all --parallel build.watch test.debug", 29 | "all": "npm run update && npm run build && npm run test.full" 30 | }, 31 | "devDependencies": { 32 | "@renovolive/gulp-utilities": "~2.3.0", 33 | "@types/angular": "1.5.16", 34 | "@types/angular-mocks": "~1.5.5", 35 | "@types/chai": "~3.4.33", 36 | "@types/core-js": "0.9.36", 37 | "@types/jquery": "2.0.32", 38 | "@types/lodash": "4.14.50", 39 | "@types/moment-timezone": "~0.2.34", 40 | "@types/sinon": "~1.16.35", 41 | "chai": "~3.5.0", 42 | "del": "~2.2.0", 43 | "gulp": "~3.9.1", 44 | "jasmine": "~2.5.2", 45 | "jasmine-core": "~2.5.2", 46 | "karma": "~0.13.21", 47 | "karma-chai": "~0.1.0", 48 | "karma-chrome-launcher": "~0.2.2", 49 | "karma-firefox-launcher": "~0.1.7", 50 | "karma-ie-launcher": "~0.2.0", 51 | "karma-jasmine": "~1.0.2", 52 | "karma-sinon": "~1.0.4", 53 | "karma-teamcity-reporter": "~0.2.1", 54 | "npm-run-all": "~3.1.0", 55 | "run-sequence": "^1.1.5", 56 | "sinon": "~1.17.3", 57 | "systemjs": "~0.19.36", 58 | "systemjs-builder": "~0.15.16", 59 | "typescript": "~2.1.5" 60 | }, 61 | "dependencies": { 62 | "@angular/common": "~2.4.0", 63 | "@angular/compiler": "~2.4.0", 64 | "@angular/core": "~2.4.0", 65 | "@angular/http": "~2.4.0", 66 | "@angular/platform-browser": "~2.4.0", 67 | "@angular/platform-browser-dynamic": "~2.4.0", 68 | "@angular/upgrade": "~2.4.0", 69 | "angular": "^1.5.3", 70 | "angular-mocks": "~1.5.0", 71 | "angular2-uuid": "~1.0.7", 72 | "es6-shim": "~0.35.0", 73 | "json-loader": "~0.5.4", 74 | "lodash": "~4.5.1", 75 | "moment": "~2.14.1", 76 | "moment-timezone": "~0.5.3", 77 | "reflect-metadata": "0.1.2", 78 | "rl-async-testing": "~1.6.1", 79 | "rl-http": "~1.5.1", 80 | "rxjs": "~5.0.0", 81 | "zone.js": "~0.7.2" 82 | }, 83 | "license": "MIT" 84 | } 85 | -------------------------------------------------------------------------------- /source/services/number/number.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { INumberUtility, NumberUtility } from './number.service'; 2 | 3 | describe('numberUtility', () => { 4 | var numberUtility: INumberUtility; 5 | 6 | beforeEach(() => { 7 | numberUtility = new NumberUtility(); 8 | }); 9 | 10 | describe('preciseRound', (): void => { 11 | it('should round 6 to 6', (): void => { 12 | var roundedNum: number = numberUtility.preciseRound(6, 2); 13 | expect(roundedNum).to.equal(6); 14 | }); 15 | 16 | it('should round 1.275 to 1.28', (): void => { 17 | var roundedNum: number = numberUtility.preciseRound(1.275, 2); 18 | expect(roundedNum).to.equal(1.28); 19 | }); 20 | 21 | it('should round 1.274 to 1.27', (): void => { 22 | var roundedNum: number = numberUtility.preciseRound(1.274, 2); 23 | expect(roundedNum).to.equal(1.27); 24 | }); 25 | 26 | it('should round 1.55555555555555555555 to 1.5555555555555555556', (): void => { 27 | // 20 5's. This is the max precision precise_round is valid for 28 | var roundedNum: number = numberUtility.preciseRound(1.55555555555555555555, 19); 29 | expect(roundedNum).to.equal(1.5555555555555555556); 30 | }); 31 | 32 | it('should round 1.999999999999999999999 to 2', (): void => { 33 | var roundedNum: number = numberUtility.preciseRound(1.999999999999999999999, 20); // 21 9's 34 | expect(roundedNum).to.equal(2); 35 | }); 36 | 37 | it('should not round 1.111111111111111111111', (): void => { 38 | var roundedNum: number = numberUtility.preciseRound(1.111111111111111111111, 20); // 21 1's 39 | expect(roundedNum).to.equal(1.11111111111111111111); // trimmed 1 from the end 40 | }); 41 | }); 42 | 43 | describe('roundToStep', (): void => { 44 | it('should round to the nearest 5', (): void => { 45 | expect(numberUtility.roundToStep(4, 5)).to.equal(5); 46 | expect(numberUtility.roundToStep(23, 5)).to.equal(25); 47 | expect(numberUtility.roundToStep(22, 5)).to.equal(20); 48 | }); 49 | 50 | it('should round to a 365 divisible value', (): void => { 51 | expect(numberUtility.roundToStep(366, 3.65)).to.equal(365); 52 | expect(numberUtility.roundToStep(367, 3.65)).to.equal(368.65); 53 | expect(numberUtility.roundToStep(125, 3.65)).to.equal(124.10); 54 | expect(numberUtility.roundToStep(250, 3.65)).to.equal(248.20); 55 | expect(numberUtility.roundToStep(10.95, 3.65)).to.equal(10.95); 56 | }); 57 | }); 58 | 59 | describe('isEven', (): void => { 60 | it('should return true for even numbers', (): void => { 61 | expect(numberUtility.isEven(0)).to.be.true; 62 | expect(numberUtility.isEven(2)).to.be.true; 63 | expect(numberUtility.isEven(16)).to.be.true; 64 | expect(numberUtility.isEven(36)).to.be.true; 65 | }); 66 | 67 | it('should return false for odd numbers', (): void => { 68 | expect(numberUtility.isEven(1)).to.be.false; 69 | expect(numberUtility.isEven(5)).to.be.false; 70 | expect(numberUtility.isEven(17)).to.be.false; 71 | expect(numberUtility.isEven(33)).to.be.false; 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /source/services/timezone/timezone.enum.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | // functions like an itemList but doesn't want name and value 4 | 5 | export interface ITimezone { 6 | offset: string; 7 | display: string; 8 | momentName: string; 9 | offsetMinutes?: number; 10 | } 11 | 12 | export interface ITimezones { 13 | AST: ITimezone; 14 | EST: ITimezone; 15 | CST: ITimezone; 16 | MST: ITimezone; 17 | PST: ITimezone; 18 | AKST: ITimezone; 19 | HAST: ITimezone; 20 | 21 | get(offset: string): ITimezone; 22 | all(): ITimezone[]; 23 | } 24 | 25 | export class Timezones implements ITimezones { 26 | AST: ITimezone = new Timezone ({ 27 | offset: '-04:00', 28 | display: 'AST', 29 | momentName: 'Canada/Atlantic', 30 | offsetMinutes: -240, 31 | }); 32 | EST: ITimezone = new Timezone ({ 33 | offset: '-05:00', 34 | display: 'EST', 35 | momentName: 'US/Eastern', 36 | offsetMinutes: -300, 37 | }); 38 | CST: ITimezone = new Timezone ({ 39 | offset: '-06:00', 40 | display: 'CST', 41 | momentName: 'US/Central', 42 | offsetMinutes: -360, 43 | }); 44 | MST: ITimezone = new Timezone ({ 45 | offset: '-07:00', 46 | display: 'MST', 47 | momentName: 'US/Mountain', 48 | offsetMinutes: -420, 49 | }); 50 | PST: ITimezone = new Timezone ({ 51 | offset: '-08:00', 52 | display: 'PST', 53 | momentName: 'US/Pacific', 54 | offsetMinutes: -480, 55 | }); 56 | AKST: ITimezone = new Timezone ({ 57 | offset: '-09:00', 58 | display: 'AKST', 59 | momentName: 'US/Alaska', 60 | offsetMinutes: -540, 61 | }); 62 | HAST: ITimezone = new Timezone ({ 63 | offset: '-10:00', 64 | display: 'HAST', 65 | momentName: 'US/Hawaii', 66 | offsetMinutes: -600, 67 | }); 68 | 69 | items: ITimezone[]; 70 | 71 | constructor() { 72 | this.items = [ 73 | this.AST, 74 | this.EST, 75 | this.CST, 76 | this.MST, 77 | this.PST, 78 | this.AKST, 79 | this.HAST, 80 | ]; 81 | } 82 | 83 | get(offsetOrMomentName: string): ITimezone { 84 | return _.find(this.items, (item: ITimezone): boolean => { 85 | return (item.offset === offsetOrMomentName || item.momentName === offsetOrMomentName); 86 | }); 87 | } 88 | 89 | all(): ITimezone[] { 90 | return this.items; 91 | } 92 | } 93 | 94 | class Timezone implements ITimezone { 95 | offset: string; 96 | display: string; 97 | momentName: string; 98 | offsetMinutes: number; 99 | 100 | constructor(data: ITimezone) { 101 | this.offset = data.offset; 102 | this.display = data.display; 103 | this.momentName = data.momentName; 104 | this.offsetMinutes = data.offsetMinutes; 105 | } 106 | } 107 | 108 | export let timezones: ITimezones = new Timezones(); 109 | 110 | // Intentionally not in timezones list as this is not a valid user timezone, 111 | // but is used for server values. 112 | export const UTC: ITimezone = { 113 | offset: '+00:00', 114 | display: 'UTC', 115 | momentName: 'UTC', 116 | offsetMinutes: 0, 117 | }; 118 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataService/view/dataServiceView.md: -------------------------------------------------------------------------------- 1 | ## Data service view 2 | This is a special use case for a child resource that is a `view` of the parent. If a specific parent is selected, the `view` is treated as a singleton. Otherwise, it is treated as a standard resource. 3 | 4 | See [dataService](../dataService.md) and [singletonDataService](../../singletonDataService/singletonDataService.md) for details on each resource type. 5 | 6 | ### Example 7 | Data service view: 8 | ``` 9 | export let endpoint: string = '/settings'; 10 | 11 | export interface IUserSettings { 12 | theme: string; 13 | } 14 | ``` 15 | Definition in the parent (see [parentDataService](../parent/parentDataService.md) for details on how to define a parent data service): 16 | ``` 17 | import { services } from 'typescript-angular-utilities'; 18 | import __dataContracts = services.dataContracts; 19 | 20 | import * as settings from './settings'; 21 | 22 | export { settings }; 23 | 24 | export interface IChildResources { 25 | settings: __dataContracts.IDataServiceView; 26 | } 27 | 28 | export function buildChildResources(baseResourceBuilder: __dataContracts.IBaseResourceBuilder): { (): IChildResources } { 29 | return (): IServiceEventChildResources => { 30 | return { 31 | settings: baseResourceBuilder.createResourceView({ 32 | endpoint: settings.endpoint, 33 | }), 34 | }; 35 | }; 36 | } 37 | ``` 38 | Usage (as a data service): 39 | ``` 40 | import { services } from 'typescript-angular-utilities'; 41 | import __dataContracts = services.dataContracts; 42 | 43 | import { DataServices, serviceName as dataServiceName, user } from '../data/data.service'; 44 | 45 | export class MyConsumer { 46 | settingsResource: __dataContracts.IDataService; 47 | 48 | static $inject: string[] = [dataServiceName]; 49 | constructor(dataServices: DataServices) { 50 | // no parent is selected 51 | this.settingsResource = dataServices.user.childContract(); 52 | } 53 | 54 | action(): void { 55 | this.settingsResource.getList().then((settings: user.settings.IUserSettings[]): void => { 56 | console.log(settings); 57 | }); 58 | } 59 | } 60 | ``` 61 | Usage (as a singleton data service): 62 | ``` 63 | import { services } from 'typescript-angular-utilities'; 64 | import __dataContracts = services.dataContracts; 65 | 66 | import { DataServices, serviceName as dataServiceName, user } from '../data/data.service'; 67 | 68 | export class MyConsumer { 69 | settingsResource: __dataContracts.IDataService; 70 | 71 | static $inject: string[] = [dataServiceName, 'userId']; 72 | constructor(dataServices: DataServices, userId: number) { 73 | // select a parent 74 | this.settingsResource = dataServices.user.childContract(userId).settings; 75 | } 76 | 77 | action(): void { 78 | this.settingsResource.get().then((settings: user.settings.IUserSettings): void => { 79 | console.log(settings); 80 | }); 81 | } 82 | } 83 | ``` -------------------------------------------------------------------------------- /source/services/dataContracts/dataService/parent/parentData.service.tests.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | import { IDataService } from '../data.service'; 4 | import { IDataServiceView } from '../view/dataServiceView'; 5 | import { IParentDataService } from './parentData.service'; 6 | import { IResourceBuilder, ResourceBuilder } from '../../resourceBuilder/resourceBuilder.service'; 7 | 8 | interface ITestResourceDictionaryType { 9 | testView: IDataServiceView; 10 | test: IDataService; 11 | } 12 | 13 | interface ITestMock { 14 | id: number; 15 | prop: string; 16 | } 17 | 18 | describe('parent data service', () => { 19 | let parentDataService: IParentDataService; 20 | let resourceBuilder: IResourceBuilder; 21 | let dataSet: ITestMock[]; 22 | let dataService: IDataService; 23 | let dataServiceView: IDataServiceView; 24 | 25 | beforeEach((): void => { 26 | dataSet = [ 27 | { id: 1, prop: 'item1' }, 28 | { id: 2, prop: 'item2' }, 29 | { id: 3, prop: 'item3' }, 30 | ]; 31 | 32 | resourceBuilder = new ResourceBuilder({}, {}); 33 | 34 | dataService = resourceBuilder.createResource({ 35 | mockData: dataSet, 36 | useMock: true, 37 | }); 38 | dataServiceView = resourceBuilder.createResourceView({ 39 | mockData: dataSet, 40 | useMock: true, 41 | }); 42 | 43 | parentDataService = resourceBuilder.createParentResource({ 44 | resourceDictionaryBuilder(): ITestResourceDictionaryType { 45 | return { 46 | testView: dataServiceView, 47 | test: dataService, 48 | }; 49 | }, 50 | }); 51 | }); 52 | 53 | describe('viewAsSingleton', (): void => { 54 | let children: ITestResourceDictionaryType; 55 | 56 | beforeEach((): void => { 57 | children = parentDataService.childContracts(1); 58 | }); 59 | 60 | it('should expose a get function', done => { 61 | (children.testView).get().subscribe((item: ITestMock): void => { 62 | expect(item).to.equal(dataSet[0]); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should expose an update function and return the result of the update', done => { 68 | (children.testView).update(dataSet[0]).subscribe((item: ITestMock): void => { 69 | expect(item).to.equal(dataSet[0]); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should disable getList, getDetail, and create', (): void => { 75 | expect(children.testView.getList).to.not.exist; 76 | expect(children.testView.getDetail).to.not.exist; 77 | expect(children.testView.create).to.not.exist; 78 | }); 79 | 80 | it('should leave standard data services untouched', (): void => { 81 | expect(_.isFunction(children.test.getList)).to.be.true; 82 | expect(_.isFunction(children.test.getDetail)).to.be.true; 83 | expect(_.isFunction(children.test.create)).to.be.true; 84 | expect(_.isFunction(children.test.update)).to.be.true; 85 | expect((children.test).get).to.not.exist; 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /source/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | 3 | import * as array from './array/array.service'; 4 | import * as boolean from './boolean/boolean.service'; 5 | import * as dataContracts from './dataContracts/index'; 6 | import * as date from './date/index'; 7 | import * as errorHandler from './errorHandler/errorHandler.service'; 8 | import * as fileSize from './fileSize/index'; 9 | import * as genericSearchFilter from './genericSearchFilter/genericSearchFilter.service'; 10 | import * as guid from './guid/guid.service'; 11 | import * as digestService from './digest/digest.service'; 12 | import * as logger from './logger/logger.service'; 13 | import * as notification from './notification/notification.service'; 14 | import * as numberService from './number/number.service'; 15 | import * as objectService from './object/object.service'; 16 | import * as observable from './observable/observable.service'; 17 | import * as redirect from './redirect/redirect.service'; 18 | import * as search from './search/search.service'; 19 | import * as stringService from './string/string.service'; 20 | import * as synchronizedRequests from './synchronizedRequests/synchronizedRequests.service'; 21 | import * as test from './test/index'; 22 | import * as time from './time/time.service'; 23 | import * as timeout from './timeout/timeout.service'; 24 | import * as timezone from './timezone/timezone.service'; 25 | import * as transform from './transform/transform.service'; 26 | import * as validation from './validation/validation.service'; 27 | import * as emailValidation from './validation/emailValidation.service'; 28 | import * as window from './window/window.provider'; 29 | 30 | 31 | export { 32 | array, 33 | boolean, 34 | dataContracts, 35 | date, 36 | errorHandler, 37 | fileSize, 38 | genericSearchFilter, 39 | guid, 40 | digestService, 41 | logger, 42 | notification, 43 | numberService as number, 44 | objectService as object, 45 | observable, 46 | redirect, 47 | search, 48 | stringService as string, 49 | synchronizedRequests, 50 | test, 51 | time, 52 | timeout, 53 | timezone, 54 | transform, 55 | validation, 56 | emailValidation, 57 | window, 58 | }; 59 | 60 | /** 61 | * Providers for utility services. 62 | */ 63 | export const UTILITY_PROVIDERS: (Provider | Provider[] | any)[] = [ 64 | array.ArrayUtility, 65 | boolean.BooleanUtility, 66 | dataContracts.DATA_CONTRACT_PROVIDERS, 67 | date.DateUtility, 68 | errorHandler.ErrorHandlerService, 69 | genericSearchFilter.GenericSearchFilterFactory, 70 | guid.GuidService, 71 | digestService.DigestService, 72 | numberService.NumberUtility, 73 | objectService.ObjectUtility, 74 | search.SearchUtility, 75 | stringService.StringUtility, 76 | synchronizedRequests.SynchronizedRequestsFactory, 77 | time.TimeUtility, 78 | timeout.TimeoutService, 79 | timezone.TimezoneService, 80 | transform.TransformService, 81 | 82 | validation.ObservableValidator, 83 | validation.ValidationService, 84 | emailValidation.EmailValidationService, 85 | logger.Logger, 86 | 87 | errorHandler.DefaultErrors, 88 | errorHandler.DefaultLoginUrlSettings, 89 | 90 | notification.NotificationService, 91 | redirect.RedirectService, 92 | window.WINDOW_PROVIDER, 93 | ]; 94 | -------------------------------------------------------------------------------- /source/services/dataContracts/singletonDataService/singletonData.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, FactoryProvider } from '@angular/core'; 2 | import * as _ from 'lodash'; 3 | import { Observable } from 'rxjs'; 4 | import { IHttpUtility, HttpUtility } from 'rl-http'; 5 | 6 | import { IBaseDataServiceBehavior, BaseDataServiceBehavior } from '../baseDataServiceBehavior'; 7 | import { ISingletonResourceParams } from '../resourceBuilder/resourceBuilder.service'; 8 | import { helper } from '../dataContractsHelper/dataContractsHelper.service'; 9 | 10 | export interface ISingletonDataService { 11 | get(): Observable; 12 | update(domainObject: TDataType): Observable; 13 | version(versionNumber: number): SingletonDataService; 14 | 15 | useMock: boolean; 16 | logRequests: boolean; 17 | } 18 | 19 | export class SingletonDataService implements ISingletonDataService { 20 | private behavior: IBaseDataServiceBehavior; 21 | private mockData: TDataType; 22 | endpoint: string; 23 | url: string; 24 | useMock: boolean; 25 | logRequests: boolean; 26 | 27 | 28 | constructor(http: IHttpUtility, options: ISingletonResourceParams) { 29 | this.behavior = new BaseDataServiceBehavior(http, options.transform); 30 | this.mockData = options.mockData; 31 | this.endpoint = options.endpoint; 32 | this.url = this.endpoint; 33 | this.useMock = options.useMock; 34 | this.logRequests = options.logRequests; 35 | } 36 | 37 | get(): Observable { 38 | return this.behavior.getItem({ 39 | endpoint: this.url, 40 | getMockData: (): TDataType => { return this.mockData; }, 41 | useMock: this.useMock, 42 | logRequests: this.logRequests, 43 | }); 44 | } 45 | 46 | update(domainObject: TDataType): Observable { 47 | return this.behavior.update({ 48 | domainObject: domainObject, 49 | endpoint: this.url, 50 | updateMockData: (data: TDataType): void => { 51 | this.mockData = _.assign(this.mockData, domainObject); 52 | }, 53 | useMock: this.useMock, 54 | logRequests: this.logRequests, 55 | }); 56 | } 57 | 58 | version(versionNumber: number): SingletonDataService { 59 | let dataService: SingletonDataService = _.clone(this); 60 | dataService.url = helper.versionEndpoint(dataService.url, versionNumber); 61 | return dataService; 62 | } 63 | } 64 | 65 | export interface ISingletonDataServiceFactory { 66 | getInstance(options: ISingletonResourceParams): ISingletonDataService; 67 | } 68 | 69 | @Injectable() 70 | export class SingletonDataServiceFactory { 71 | private http: IHttpUtility; 72 | 73 | constructor(http: HttpUtility) { 74 | this.http = http; 75 | } 76 | 77 | getInstance(options: ISingletonResourceParams): ISingletonDataService { 78 | return new SingletonDataService(this.http, options); 79 | } 80 | } 81 | 82 | export function SingletonDataServiceProvider(options: ISingletonResourceParams): FactoryProvider { 83 | return { 84 | provide: SingletonDataService, 85 | deps: [HttpUtility], 86 | useFactory: (http: IHttpUtility) => new SingletonDataService(http, options), 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /source/services/timezone/timezone.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { timezoneService } from './timezone.service'; 2 | import { defaultFormats } from '../date/index'; 3 | 4 | import * as moment from 'moment'; 5 | import 'moment-timezone'; 6 | 7 | import { timezones, ITimezone } from './timezone.enum'; 8 | 9 | describe('timezone service', (): void => { 10 | it('should get the timezone', (): void => { 11 | let date: string = '2016-2-1T12:00:00-07:00'; 12 | let timezone: ITimezone = timezoneService.getTimezone(date); 13 | expect(timezone).to.equal(timezones.MST); 14 | }); 15 | 16 | it('should return moment formatted time zone name from an iso string based on the offset', (): void => { 17 | let pacificOffset: string = '2016-2-1T12:00:00-07:00'; 18 | 19 | expect(timezoneService.getMomentTimezone(pacificOffset)).to.equal(timezones.MST.momentName); 20 | }); 21 | 22 | it('should set the current timezone from an offset', (): void => { 23 | timezoneService.setCurrentTimezone(timezones.MST.offset); 24 | expect(timezoneService.currentTimezone).to.equal(timezones.MST); 25 | }); 26 | 27 | it('should build a moment with the correct time and timezone', (): void => { 28 | let dateTimeString: string = '2016-02-01T12:00'; 29 | let convertedDateTimeString: moment.Moment = timezoneService.buildMomentWithTimezone(dateTimeString, timezones.MST); 30 | expect(convertedDateTimeString.tz()).to.equal(timezones.MST.momentName); 31 | expect(convertedDateTimeString).to.equalMoment('2016-02-01T12:00:00-07:00'); 32 | 33 | let dateTimeStringWithOffset: string = '2016-02-01T12:00:00.000-07:00'; 34 | let convertedDateTimeStringWithOffset: moment.Moment = timezoneService.buildMomentWithTimezone(dateTimeStringWithOffset, timezones.MST); 35 | expect(convertedDateTimeStringWithOffset.tz()).to.equal(timezones.MST.momentName); 36 | expect(convertedDateTimeStringWithOffset).to.equalMoment('2016-02-01T12:00:00-07:00'); 37 | 38 | let momentTime: moment.Moment = moment('2016-02-01T18:05:50.000-06:00').tz(timezones.CST.momentName); 39 | let convertedMomentTime2: moment.Moment = timezoneService.buildMomentWithTimezone(momentTime, timezones.MST); 40 | expect(convertedMomentTime2.tz()).to.equal(timezones.MST.momentName); 41 | expect(convertedMomentTime2).to.equalMoment('2016-02-01T18:05:50-07:00'); 42 | 43 | // can't convert a moment where the original offset doesn't match the client because the original offset information was lost before it gets to us 44 | let momentTime2: moment.Moment = moment('2016-02-01T12:00:00.000-06:00').tz(timezones.EST.momentName); 45 | let convertedMomentTime: moment.Moment = timezoneService.buildMomentWithTimezone(momentTime2, timezones.MST); 46 | expect(convertedMomentTime.tz()).to.equal(timezones.MST.momentName); 47 | expect(convertedMomentTime).to.not.equalMoment('2016-02-01T12:00:00-07:00'); 48 | }); 49 | 50 | it('should also handle times', (): void => { 51 | let timeString: string = '12:00PM'; 52 | let convertedTimeString: moment.Moment = timezoneService.buildMomentWithTimezone(timeString, timezones.MST, defaultFormats.timeFormat); 53 | expect(convertedTimeString.tz()).to.equal(timezones.MST.momentName); 54 | expect(convertedTimeString.format(defaultFormats.timeFormat)).to.equal('12:00PM'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataContracts.md: -------------------------------------------------------------------------------- 1 | ## Data contracts 2 | The data contracts library provides a framework for providing contracts with REST resources on the server. The consumer uses configuration options to specify the expected format for the contract and then uses the resourceBuilder to build the contract into a fully functional data service. 3 | 4 | ### [Resource builder](./resourceBuilder/resourceBuilder.md) 5 | The resource builder is used to build data services from contract definitions. 6 | 7 | ### [Contract library](./contractLibrary/contractLibrary.md) 8 | A base class to provide tools for mocking functions on the data contract library. 9 | 10 | ### [Data contract](./baseDataService.md) 11 | The base configuration options for specifying a data contract. 12 | 13 | ### [Data service](./dataService/dataService.md) 14 | The basic data service type 15 | 16 | ### [Singleton data service](./singletonDataService/singletonDataService.md) 17 | A data service for getting and updating a single entity. 18 | 19 | ### [Data service view](./dataService/view/dataServiceView.md) 20 | A data service type representing a `view` of a parent resource. Functions as a normal data service unless a specific parent is selected, at which point the `view` is treated as a singleton. 21 | 22 | ### [Parent data service](./dataService/parent/parentDataService.md) 23 | A data service that my also have child data services defined under it. 24 | 25 | ### [Parent singleton data service](./singletonDataService/parent/parentSingletonDataService.md) 26 | A parent data service for getting and updating a single entity. 27 | 28 | ### [Parent data service view](./dataService/view/parentDataServiceView.md) 29 | A parent data service that is a `view` of another parent. 30 | 31 | ### Which resource type should I use? 32 | Due to the variety of use cases, it can be difficult to determine which resource type is best suited to the situation. In these cases, use the following decision tree as a guide to assist in the decision. 33 | 34 | * Is the resource a singleton? (Only one instance of that object exists on the server) 35 | * Y 36 | * Does the resource have children? 37 | * Y 38 | * Use [parentSingletonDataService](./singletonDataService/parent/parentSingletonDataService.md) 39 | * N 40 | * Use [singletonDataService](./singletonDataService/singletonDataService.md) 41 | * N 42 | * Is the resource a child of another resource? 43 | * Y 44 | * Does the relationship have a 1-to-1 relationship with the parent? 45 | * Y 46 | * Does the resource have children? 47 | * Y 48 | * Is the parent resource a singleton? 49 | * Y 50 | * Use [parentSingletonDataService](./singletonDataService/parent/parentSingletonDataService.md) 51 | * N 52 | * Use [parentDataServiceView](./dataService/view/parentDataServiceView.md) 53 | * N 54 | * Is the parent resource a singleton? 55 | * Y 56 | * Use [singletonDataService](./singletonDataService/singletonDataService.md) 57 | * N 58 | * Use [dataServiceView](./dataService/view/dataServiceView.md) 59 | * N 60 | * N 61 | * Does the resource have children? 62 | * Y 63 | * Use [parentDataService](./dataService/parent/parentDataService.md) 64 | * N 65 | * Use [dataService](./dataService/dataService.md) 66 | -------------------------------------------------------------------------------- /source/services/dataContracts/contractLibrary/contractLibrary.md: -------------------------------------------------------------------------------- 1 | ## Contract library 2 | A base class that aids in building up resources into a library of data services. A base class that provides tooling for mocking data contracts for unit testing. 3 | 4 | See [resourceBuilder](../resourceBuilder/resourceBuilder.md) for example usage. 5 | 6 | ### Interface 7 | 8 | #### `baseEndpoint` 9 | Configures the base url of the contract library. If a base endpoint is set, the library will handle setting the full url of its resources by default. 10 | 11 | `createResource`, `createResourceView`, `createParentResource`, `createParentResourceView`, `createSingletonResource`, `createParentSingletonResource`. 12 | 13 | See [resourceBuilder](../resourceBuilder/resourceBuilder.md) for details on the options. If the resources are build up through the contract library, the library will automatically handle building the url of the resource based on the base url that is provided for the contract library. 14 | 15 | ### Testing 16 | The following functions are available for building mocks against the contract library. 17 | 18 | #### `mockGet` 19 | `mockGet(resource: any, data: any): sinon.SinonSpy`
20 | `mockGetList(resource: any, data[]: any): sinon.SinonSpy`
21 | `mockGetDetail(resource: any, data: any): sinon.SinonSpy`
22 | Mock the various get actions available for data services and singleton data services. Returns a spy. The mocked function returns the specified data. 23 | 24 | #### `mockChild(parent: any, mockCallback: { (children: any): void }): void` 25 | Wraps the parentDataService `childContracts()` function with an interceptor that allows us to modify the child contracts that are returned. This can be used to substitute in a mock for one of the children. 26 | 27 | #### `createMock` 28 | `createMock(resource?: any): IDataServiceMock`
29 | `createMockParent(resource?: any): IParentDataServiceMock`
30 | `createMockSingleton(resource?: any): ISingletonDataServiceMock`
31 | Wraps the specified resource with functions for mocking functions directly against the resource. See [dataServiceMock](#dataservicemock), [parentDataServiceMock](#parentdataservicemock), or [singletonDataServiceMock](#singletondataservicemock) for details. 32 | 33 | ### dataServiceMock 34 | 35 | `mockGetList(data[]: any): sinon.SinonSpy`
36 | `mockGetDetail(data: any): sinon.SinonSpy`
37 | See [`mockGet`](#mockget). Uses the resource instance so no resource must be specified. 38 | 39 | #### `mockWrite` 40 | `mockUpdate(): sinon.SinonSpy`
41 | `mockCreate(): sinon.SinonSpy`
42 | Mocks the update` and `create` actions with a function that returns the data provided to it. Returns a spy. 43 | 44 | ### parentDataServiceMock 45 | 46 | Extends [dataServiceMock](#dataservicemock) with the ability to mock children. 47 | 48 | #### `mockChild(mockCallback: { (children: any): void }): void` 49 | See [`mockChild`](mockchildparent-any-mockcallback-children-any-void-void). Uses the resource instance so no parent must be specified. 50 | 51 | ### singletonDataServiceMock 52 | 53 | #### `mockGet(data: any): sinon.SinonSpy` 54 | See [`mockGet`](#mockget). Uses the resource instance so no resource must be specified. 55 | 56 | #### `mockUpdate(): sinon.SinonSpy` 57 | See [`mockWrite`](#mockwrite). -------------------------------------------------------------------------------- /source/services/dataContracts/converters/aliasConverter/aliasConverter.tests.ts: -------------------------------------------------------------------------------- 1 | import { AliasConverter } from './aliasConverter'; 2 | import { converterService } from '../converters'; 3 | 4 | describe('aliasConverter', (): void => { 5 | let aliasConverter: AliasConverter; 6 | 7 | beforeEach((): void => { 8 | aliasConverter = new AliasConverter('propValue'); 9 | }); 10 | 11 | it('should get the property from the parent context using the specified property name', (): void => { 12 | let parent: any = { propValue: 'value' }; 13 | expect(aliasConverter.fromServer(undefined, parent)).to.equal('value'); 14 | }); 15 | 16 | it('should set the value on the parent context using the specified property name', (): void => { 17 | let parent: any = {}; 18 | aliasConverter.toServer('value', parent); 19 | expect(parent.propValue).to.equal('value'); 20 | }); 21 | 22 | it('should return null if alias property doesnt exist on the parent', (): void => { 23 | let parent: any = {}; 24 | expect(aliasConverter.fromServer(undefined, parent)).to.be.null; 25 | }); 26 | 27 | it('should apply another converter to the aliased property', (): void => { 28 | let testConverter: any = { 29 | fromServer: sinon.spy(), 30 | toServer: sinon.spy(), 31 | }; 32 | 33 | aliasConverter = new AliasConverter('propValue', testConverter); 34 | 35 | let parent: any = { propValue: 'value' }; 36 | 37 | aliasConverter.fromServer(undefined, parent); 38 | 39 | sinon.assert.calledOnce(testConverter.fromServer); 40 | sinon.assert.calledWith(testConverter.fromServer, 'value'); 41 | 42 | aliasConverter.toServer('value', parent); 43 | 44 | sinon.assert.calledOnce(testConverter.toServer); 45 | sinon.assert.calledWith(testConverter.toServer, 'value'); 46 | }); 47 | 48 | describe('integrationTest', (): void => { 49 | let testConverter: any; 50 | let transform: any; 51 | let serverData: any; 52 | 53 | beforeEach((): void => { 54 | testConverter = { 55 | fromServer(value: number): number { return value + 5; }, 56 | toServer(value: number): number { return value - 5; }, 57 | }; 58 | 59 | transform = { 60 | value: new AliasConverter('valueFromServer'), 61 | number: new AliasConverter('numberFromServer', testConverter), 62 | }; 63 | 64 | serverData = { 65 | valueFromServer: 5, 66 | numberFromServer: 5, 67 | }; 68 | }); 69 | 70 | it('should apply an alias as part of a transform mapping', (): void => { 71 | let transformedData: any = converterService.applyTransform(serverData, transform, false); 72 | 73 | expect(transformedData.value).to.equal(5); 74 | expect(transformedData.number).to.equal(10); 75 | 76 | serverData = converterService.applyTransform(transformedData, transform, true); 77 | 78 | expect(serverData.valueFromServer).to.equal(5); 79 | expect(serverData.numberFromServer).to.equal(5); 80 | }); 81 | 82 | it('should cleanup the value at the original key of the alias', (): void => { 83 | let transformedData: any = converterService.applyTransform(serverData, transform, false); 84 | 85 | expect(transformedData.valueFromServer).to.not.exist; 86 | expect(transformedData.numberFromServer).to.not.exist; 87 | 88 | serverData = converterService.applyTransform(transformedData, transform, true); 89 | 90 | expect(serverData.value).to.not.exist; 91 | expect(serverData.number).to.not.exist; 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /source/services/validation/validation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import * as _ from 'lodash'; 4 | 5 | import { INotificationService, NotificationService } from '../notification/notification.service'; 6 | 7 | import { ISimpleValidator, IErrorHandler, ICompositeValidator } from './validationTypes'; 8 | import { Validator } from './validator'; 9 | import { CompositeValidator } from './compositeValidator'; 10 | import { ObservableValidator } from './observableValidator'; 11 | 12 | export * from './observableValidator'; 13 | export * from './validationTypes'; 14 | 15 | export interface IValidationService { 16 | /** 17 | * Build a validator that uses warning notifications to show errors 18 | */ 19 | buildNotificationWarningValidator(): ISimpleValidator; 20 | 21 | /** 22 | * Build a validator that uses error notifications to show errors 23 | */ 24 | buildNotificationErrorValidator(): ISimpleValidator; 25 | 26 | /** 27 | * Build a validator that uses a custom handler to show errors 28 | * 29 | * @param showError A custom handler for validation errors 30 | */ 31 | buildCustomValidator(showError: IErrorHandler): ISimpleValidator; 32 | 33 | /** 34 | * Build a validator that groups child validators 35 | * and uses warning notifications to show errors 36 | */ 37 | buildCompositeNotificationWarningValidator(): ICompositeValidator; 38 | 39 | /** 40 | * Build a validator that groups child validators 41 | * and uses error notifications to show errors 42 | */ 43 | buildCompositeNotificationErrorValidator(): ICompositeValidator; 44 | 45 | /** 46 | * Build a validator that groups child validators 47 | * and uses a custom handler to show errors 48 | * 49 | * @param showError A custom handler for validation errors 50 | */ 51 | buildCompositeCustomValidator(showError: IErrorHandler): ICompositeValidator; 52 | 53 | /** 54 | * Build a validator that validates against a stream 55 | */ 56 | buildObservableValidator(): ObservableValidator; 57 | } 58 | 59 | @Injectable() 60 | export class ValidationService implements IValidationService { 61 | private notification: INotificationService; 62 | 63 | constructor(notification: NotificationService) { 64 | this.notification = notification; 65 | } 66 | 67 | buildNotificationWarningValidator(): ISimpleValidator { 68 | return new Validator((error: string): void => { 69 | this.notification.warning(error); 70 | }); 71 | } 72 | 73 | buildNotificationErrorValidator(): ISimpleValidator { 74 | return new Validator((error: string): void => { 75 | this.notification.error(error); 76 | }); 77 | } 78 | 79 | buildCustomValidator(showError: IErrorHandler): ISimpleValidator { 80 | return new Validator(showError); 81 | } 82 | 83 | buildCompositeNotificationWarningValidator(): ICompositeValidator { 84 | return new CompositeValidator((error: string): void => { 85 | this.notification.warning(error); 86 | }); 87 | } 88 | 89 | buildCompositeNotificationErrorValidator(): ICompositeValidator { 90 | return new CompositeValidator((error: string): void => { 91 | this.notification.error(error); 92 | }); 93 | } 94 | 95 | buildCompositeCustomValidator(showError: IErrorHandler): ICompositeValidator { 96 | return new CompositeValidator(showError); 97 | } 98 | 99 | buildObservableValidator(): ObservableValidator { 100 | return new ObservableValidator(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /source/services/validation/emailValidation.service.tests.ts: -------------------------------------------------------------------------------- 1 | 2 | import {EmailValidationService} from './emailValidation.service'; 3 | 4 | describe('EmailValidationService', () => { 5 | let emailValidationService = new EmailValidationService(); 6 | 7 | const validCharacterTestCases: string[] = ['_', '+', '.', '+']; 8 | const invalidCharacterTestCases: string[] = ['!', '^', '%', '*', ' ', '~', '&', '$', '#', '?', '`', '=', '/', '\\', ',', ':', ';', '"', '\'', '{', '}', '[', ']', '(', ')', '|', '>', '<']; 9 | const invalidStartCharacterTestCase: string[] = ['@', '.']; 10 | 11 | it('should return true when email is valid', () => { 12 | const fakeEmailToTest = 'fake@email.com'; 13 | 14 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 15 | 16 | expect(isValid).to.be.true; 17 | }); 18 | 19 | it('should return false when email has double at signs', () => { 20 | const fakeEmailToTest = 'fake@@email.com'; 21 | 22 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 23 | 24 | expect(isValid).to.be.false; 25 | }); 26 | 27 | 28 | invalidCharacterTestCases.forEach((invalidCharacter): void => { 29 | it(`should return false when an emailaddress contains ${invalidCharacter}`, () => { 30 | const fakeEmailToTest = `fake${invalidCharacter}invalid@email.org`; 31 | 32 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 33 | 34 | expect(isValid).to.be.false; 35 | }); 36 | }); 37 | 38 | validCharacterTestCases.forEach((validCharacter): void => { 39 | it(`should return true when an emailaddress contains ${validCharacter}`, () => { 40 | const fakeEmailToTest = `fake${validCharacter}invalid@email.org`; 41 | 42 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 43 | 44 | expect(isValid).to.be.true; 45 | }); 46 | }); 47 | 48 | invalidStartCharacterTestCase.forEach((invalidCharacter): void => { 49 | it(`should return false when an emailaddress starts with ${invalidCharacter}`, () => { 50 | const fakeEmailToTest = `${invalidCharacter}invalid@email.org`; 51 | 52 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 53 | 54 | expect(isValid).to.be.false; 55 | }); 56 | }); 57 | 58 | it('should return false when an emailaddress ends with .', () => { 59 | const fakeEmailToTest = 'invalid@emailorg.'; 60 | 61 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 62 | 63 | expect(isValid).to.be.false; 64 | }); 65 | 66 | it('should return false when an emailaddress ends with a @', () => { 67 | const fakeEmailToTest = 'invalidemail.org@'; 68 | 69 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 70 | 71 | expect(isValid).to.be.false; 72 | }); 73 | 74 | it('should not contain a double dot', () => { 75 | const fakeEmailToTest = 'invalid@email..org'; 76 | 77 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 78 | 79 | expect(isValid).to.be.false; 80 | }); 81 | 82 | it('should allow more than one dot contiguously', () => { 83 | const fakeEmailToTest = 'fake@cs.email.org'; 84 | 85 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 86 | 87 | expect(isValid).to.be.true; 88 | }); 89 | 90 | it('should not allow a dot immediately after an at sign', () => { 91 | const fakeEmailToTest = 'fake@.email.org'; 92 | 93 | let isValid = emailValidationService.isValidEmailAddress(fakeEmailToTest); 94 | 95 | expect(isValid).to.be.false; 96 | }); 97 | }); 98 | 99 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataService/view/parentDataServiceView.md: -------------------------------------------------------------------------------- 1 | ## Parent Data service view 2 | Specifies a data contract is a `view` of another resource and also has its own children. 3 | 4 | Extends [dataServiceView](./dataServiceView.md). 5 | 6 | ### Options 7 | 8 | `IChildResources`, `resourceDictionaryBuilder` 9 | 10 | See [parentDataService](../parent/parentDataService.md) for details on the parent options. 11 | 12 | `endpoint`, `interface`, `useMock`, `logRequests`, `searchParams`, `transform`. 13 | 14 | See [dataContract](../baseDataService.md) for details on the base options. 15 | 16 | ### Interface 17 | The following functions are available for consumers of the parent singleton data service. 18 | 19 | `childContracts`. See [parentDataService](../parent/parentDataService.md). 20 | 21 | `getList`, `getDetail`, `create`, `update`, `delete`, `version`. 22 | 23 | See [dataService](../dataService.md) for details on the base options. 24 | 25 | #### `Handling children` 26 | Using a parent data service view adds a little bit of complexity when defining children, since the parent itself could be a singleton or a full resource based on whether a grandparent is selected. If the child is a full resource (where the parent has a one-to-many relationship with it) we can still treat it as before. However, if the child is itself a `view`, then we have to handle it a little different. The main thing to remember here is that if a grandparent is selected, when the parent is converted to a singleton the child is likewise converted. Thus the child `view` will be treated as a singleton in either case of whether a grandparent or a direct parent is selected. 27 | 28 | Example: 29 | In this example, `user` is a parent resource. The user resource has a child `view` called `settings` that is a parent resource view. Settings has it's own child settings called `privacy`, which refers specifically to the user's privacy settings. 30 | 31 | With no parent selected: 32 | ``` 33 | this.privacyResource = dataServices.user.childContracts().settings.childContract().privacy; 34 | 35 | // privacy is treated as a standard data service 36 | this.privacyResource.getList(); 37 | this.privacyResource.getDetail(11); 38 | this.privacyResource.create(object); 39 | this.privacyResource.update(object); 40 | this.privacyResource.delete(object); 41 | this.privacyResource.version(2); 42 | ``` 43 | 44 | With parent selected: 45 | ``` 46 | this.privacyResource = dataServices.user.childContracts().settings.childContracts(11).privacy; 47 | 48 | // privacy is treated as a singleton data service 49 | this.privacyResource.get(); 50 | this.privacyResource.update(object); 51 | this.privacyResource.version(2); 52 | ``` 53 | 54 | With grandparent selected: 55 | ``` 56 | this.privacyResource = dataServices.user.childContracts(11).settings.childContracts().privacy; 57 | 58 | // privacy is treated as a singleton data service 59 | this.privacyResource.get(); 60 | this.privacyResource.update(object); 61 | this.privacyResource.version(2); 62 | ``` 63 | Note that in this case there are three possible urls that could be generated for getting the privacy settings: 64 | `www.example.com/api/user/settings/privacy/11`,
65 | `www.example.com/api/user/settings/11/privacy`, and
66 | `www.example.com/api/user/11/settings/privacy`
67 | You should verify that the API supports all url forms, or be careful to only use the form that is supported by the api. 68 | 69 | See [parentDataService](../parent/parentDataService.md#usecases) for using a `view` against a standard parent resource.
70 | See [dataServiceView](./dataServiceView.md) for details on how to define a `view`. -------------------------------------------------------------------------------- /source/services/time/time.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { ITimeUtility, TimeUtility } from './time.service'; 2 | import { CompareResult } from '../../types/compareResult'; 3 | import { stringUtility } from '../string/string.service'; 4 | import { objectUtility } from '../object/object.service'; 5 | 6 | describe('timeUtility', () => { 7 | let timeUtility: ITimeUtility; 8 | 9 | beforeEach(() => { 10 | timeUtility = new TimeUtility(stringUtility, objectUtility); 11 | }); 12 | 13 | it('should compare times and return a compare result to indicate which is greater', (): void => { 14 | expect(timeUtility.compareTimes('12:00PM', '1:00PM')).to.equal(CompareResult.less); 15 | expect(timeUtility.compareTimes('12:00PM', '12:00PM')).to.equal(CompareResult.equal); 16 | expect(timeUtility.compareTimes('2:00PM', '1:00PM')).to.equal(CompareResult.greater); 17 | }); 18 | 19 | it('should return the rounded number of hours between two times', (): void => { 20 | expect(timeUtility.durationInHours('8:00AM', '5:00PM')).to.equal(9); 21 | expect(timeUtility.durationInHours('8:00AM', '8:00AM')).to.equal(0); 22 | expect(timeUtility.durationInHours('2:45PM', '3:15PM')).to.equal(1); 23 | expect(timeUtility.durationInHours('12:00AM', '12:00AM')).to.equal(0); 24 | expect(timeUtility.durationInHours('12:00AM', '12:00PM')).to.equal(12); 25 | expect(timeUtility.durationInHours('12:00AM', '11:59PM')).to.equal(24); 26 | }); 27 | 28 | it('should return null if the time is null or empty', (): void => { 29 | expect(timeUtility.parseTime(null)).to.be.null; 30 | expect(timeUtility.parseTime('')).to.be.null; 31 | expect(timeUtility.formatTime(null)).to.be.null; 32 | }); 33 | 34 | it('should parse the time string into a time object', (): void => { 35 | expect(timeUtility.parseTime('8:00AM')).to.deep.equal({ hour: 8, minute: 0, period: 'AM' }); 36 | expect(timeUtility.parseTime('12:00PM')).to.deep.equal({ hour: 12, minute: 0, period: 'PM' }); 37 | expect(timeUtility.parseTime('12:00AM')).to.deep.equal({ hour: 12, minute: 0, period: 'AM' }); 38 | expect(timeUtility.parseTime('10:15AM')).to.deep.equal({ hour: 10, minute: 15, period: 'AM' }); 39 | expect(timeUtility.parseTime('1:41PM')).to.deep.equal({ hour: 1, minute: 41, period: 'PM' }); 40 | }); 41 | 42 | it('should format the time object into a time string', (): void => { 43 | expect(timeUtility.formatTime({ hour: 8, minute: 0, period: 'AM' })).to.equal('8:00AM'); 44 | expect(timeUtility.formatTime({ hour: 12, minute: 0, period: 'PM' })).to.equal('12:00PM'); 45 | expect(timeUtility.formatTime({ hour: 12, minute: 0, period: 'AM' })).to.equal('12:00AM'); 46 | expect(timeUtility.formatTime({ hour: 10, minute: 15, period: 'AM' })).to.equal('10:15AM'); 47 | expect(timeUtility.formatTime({ hour: 1, minute: 41, period: 'PM' })).to.equal('1:41PM'); 48 | }); 49 | 50 | it('should hide the period if includePeriod is false', (): void => { 51 | expect(timeUtility.formatTime({ hour: 8, minute: 0, period: 'AM' }, false)).to.equal('8:00'); 52 | expect(timeUtility.formatTime({ hour: 12, minute: 0, period: 'PM' }, false)).to.equal('12:00'); 53 | }); 54 | 55 | it('should handle null hours or minutes', (): void => { 56 | expect(timeUtility.formatTime({ hour: 9, minute: null, period: 'AM' })).to.equal('9:00AM'); 57 | expect(timeUtility.formatTime({ hour: null, minute: 30, period: 'AM' })).to.equal('12:30AM'); 58 | }); 59 | 60 | it('should return the opposite period, defaulting to AM', (): void => { 61 | expect(timeUtility.inversePeriod('AM')).to.equal('PM'); 62 | expect(timeUtility.inversePeriod('PM')).to.equal('AM'); 63 | expect(timeUtility.inversePeriod(null)).to.equal('AM'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /source/services/array/array.service.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | export interface IArrayUtility { 4 | findIndexOf(array: TDataType[], predicate: { (item: TDataType): boolean }): number; 5 | remove(array: TDataType[], item: { (obj: TDataType): boolean }): TDataType; 6 | remove(array: TDataType[], item: TDataType): TDataType; 7 | replace(array: TDataType[], oldItem: TDataType, newItem: TDataType): void; 8 | sum(array: TDataType[], transform: { (item: TDataType): number }): number; 9 | sum(array: number[]): number; 10 | last(array: TDataType[]): TDataType; 11 | toDictionary(array: TDataType[], keySelector: { (item: TDataType): string }): { [index: string]: TDataType }; 12 | has(array: TDataType[], index: number): boolean; 13 | arrayify(maybeArray: TDataType[] | TDataType): TDataType[]; 14 | } 15 | 16 | export class ArrayUtility implements IArrayUtility { 17 | findIndexOf(array: TDataType[], predicate: { (item: TDataType): boolean }): number { 18 | var targetIndex: number; 19 | 20 | _.each(array, (item: TDataType, index: number): boolean => { 21 | if (predicate(item)) { 22 | targetIndex = index; 23 | return false; 24 | } 25 | }); 26 | 27 | return targetIndex != null ? targetIndex : -1; 28 | } 29 | 30 | remove(array: TDataType[], item: TDataType | { (obj: TDataType): boolean }): TDataType { 31 | var index: number; 32 | 33 | if (_.isFunction(item)) { 34 | index = this.findIndexOf(array, <{(obj: TDataType): boolean}>item); 35 | } else { 36 | index = _.indexOf(array, item); 37 | } 38 | 39 | if (index >= 0) { 40 | return array.splice(index, 1)[0]; 41 | } else { 42 | return null; 43 | } 44 | } 45 | 46 | replace(array: TDataType[], oldItem: TDataType, newItem: TDataType): void { 47 | var index: number = _.indexOf(array, oldItem); 48 | 49 | if (index >= 0) { 50 | array.splice(index, 1, newItem); 51 | } 52 | } 53 | 54 | sum(array: TDataType[], transform?: { (item: TDataType): number }): number { 55 | var list: number[]; 56 | 57 | if (transform != null) { 58 | list = _.map(array, (item: TDataType): number => { return transform(item); }); 59 | } else { 60 | list = array; 61 | } 62 | 63 | return _.reduce(list, (sum: number, num: number): number => { return sum + num; }, 0); 64 | } 65 | 66 | toDictionary(array: TDataType[], keySelector: { (item: TDataType): string }) 67 | : { [index: string]: TDataType } { 68 | array = _.reject(array, (item: TDataType): boolean => { 69 | return keySelector(item) == null; 70 | }); 71 | // needs to be seeded with an object or it will be viewed as an array with no items 72 | return _.reduce(array, (dictionary: { [index: string]: TDataType }, item: TDataType): { [index: string]: TDataType } => { 73 | dictionary[keySelector(item)] = item; 74 | return dictionary; 75 | }, {}); 76 | } 77 | 78 | last(array: TDataType[]): TDataType { 79 | if (array != null && array.length > 0) { 80 | return array[array.length - 1]; 81 | } 82 | } 83 | 84 | has(array: TDataType[], index: number): boolean { 85 | if (array == null || index < 0 || index >= array.length) { 86 | return false; 87 | } 88 | 89 | return array[index] != null; 90 | } 91 | 92 | arrayify(maybeArray: TDataType[] | TDataType): TDataType[] { 93 | if (_.isArray(maybeArray)) { 94 | return maybeArray; 95 | } else if (maybeArray) { 96 | return [maybeArray]; 97 | } else { 98 | return []; 99 | } 100 | } 101 | } 102 | 103 | export const arrayUtility: ArrayUtility = new ArrayUtility(); 104 | -------------------------------------------------------------------------------- /source/services/time/time.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as moment from 'moment'; 3 | import { takeRight, dropRight, padStart } from 'lodash'; 4 | 5 | import { CompareResult } from '../../types/compareResult'; 6 | import { defaultFormats } from '../date/index'; 7 | import { IStringUtility, StringUtility, stringUtility } from '../string/string.service'; 8 | import { IObjectUtility, ObjectUtility, objectUtility } from '../object/object.service'; 9 | 10 | export interface ITime { 11 | hour: number; 12 | minute: number; 13 | period: string; 14 | } 15 | 16 | export interface ITimePeriods { 17 | AM: string; 18 | PM: string; 19 | } 20 | 21 | export const timePeriods: ITimePeriods = { 22 | AM: 'AM', 23 | PM: 'PM', 24 | }; 25 | 26 | const defaultHour: number = 12; 27 | const defaultMinute: number = 0; 28 | 29 | export interface ITimeUtility { 30 | compareTimes(start: string, end: string): CompareResult; 31 | durationInHours(start: string, end: string): number; 32 | parseTime(value: string): ITime; 33 | formatTime(time: ITime, includePeriod?: boolean): string; 34 | inversePeriod(period: string): string 35 | } 36 | 37 | @Injectable() 38 | export class TimeUtility { 39 | stringUtility: IStringUtility; 40 | objectUtility: IObjectUtility; 41 | 42 | constructor(stringUtility: StringUtility 43 | , objectUtility: ObjectUtility) { 44 | this.stringUtility = stringUtility; 45 | this.objectUtility = objectUtility; 46 | } 47 | 48 | compareTimes(start: string, end: string): CompareResult { 49 | const format: string = defaultFormats.timeFormat; 50 | 51 | const startMoment: moment.Moment = moment(start, format); 52 | const endMoment: moment.Moment = moment(end, format); 53 | 54 | if (startMoment.hours() === endMoment.hours() 55 | && startMoment.minutes() === endMoment.minutes()) { 56 | return CompareResult.equal; 57 | } else if (startMoment.hours() >= endMoment.hours() 58 | && startMoment.minutes() >= endMoment.minutes()) { 59 | return CompareResult.greater; 60 | } else { 61 | return CompareResult.less; 62 | } 63 | } 64 | 65 | durationInHours(start: string, end: string): number { 66 | const format: string = defaultFormats.timeFormat; 67 | 68 | const startMoment: moment.Moment = moment(start, format); 69 | const endMoment: moment.Moment = moment(end, format); 70 | 71 | let hours: number = endMoment.hours() - startMoment.hours(); 72 | const minutes: number = endMoment.minutes() - startMoment.minutes(); 73 | 74 | if (minutes >= 30) { 75 | hours += 1; 76 | } 77 | return hours; 78 | } 79 | 80 | parseTime(value: string): ITime { 81 | if (this.objectUtility.isNullOrEmpty(value)) { 82 | return null; 83 | } 84 | 85 | let time: ITime = {}; 86 | let [hourString, subsetWithoutHour]: string[] = value.split(':'); 87 | time.hour = this.stringUtility.toNumber(hourString); 88 | time.minute = this.stringUtility.toNumber(dropRight(subsetWithoutHour, 2).join('')); 89 | time.period = takeRight(subsetWithoutHour, 2).join('') === timePeriods.PM 90 | ? timePeriods.PM 91 | : timePeriods.AM; 92 | return time; 93 | } 94 | 95 | formatTime(time: ITime, includePeriod: boolean = true): string { 96 | if (time == null) { 97 | return null; 98 | } 99 | const postfix = includePeriod ? time.period : ''; 100 | return (time.hour || defaultHour) 101 | + ':' 102 | + padStart((time.minute || defaultMinute).toString(), 2, '0') 103 | + postfix; 104 | } 105 | 106 | inversePeriod(period: string): string { 107 | return period === timePeriods.AM 108 | ? timePeriods.PM 109 | : timePeriods.AM; 110 | } 111 | } 112 | 113 | export const timeUtility: TimeUtility = new TimeUtility(stringUtility, objectUtility); 114 | -------------------------------------------------------------------------------- /source/services/dataContracts/contractLibrary/contractLibrary.tests.ts: -------------------------------------------------------------------------------- 1 | import { ContractLibrary } from './contractLibrary'; 2 | import { IResourceBuilder, ResourceBuilder } from '../resourceBuilder/resourceBuilder.service'; 3 | import { DataServiceView } from '../dataService/view/dataServiceView'; 4 | import { DataService } from '../dataService/data.service'; 5 | import { ParentDataService } from '../dataService/parent/parentData.service'; 6 | import { ParentSingletonDataService } from '../singletonDataService/parent/parentSingletonData.service'; 7 | 8 | interface ITestChildResources { 9 | childResource: DataService; 10 | resourceView: DataServiceView; 11 | } 12 | 13 | function testChildBuilder(resourceBuilder: IResourceBuilder): { (): ITestChildResources } { 14 | return (): ITestChildResources => { 15 | return { 16 | childResource: resourceBuilder.createResource({ 17 | endpoint: '/childResource', 18 | }), 19 | resourceView: resourceBuilder.createResourceView({ 20 | endpoint: '/resourceView', 21 | }), 22 | }; 23 | }; 24 | } 25 | 26 | class TestLibrary extends ContractLibrary { 27 | resource1: DataService; 28 | parent1: ParentDataService; 29 | parentSingleton: ParentSingletonDataService; 30 | 31 | constructor(resourceBuilder: IResourceBuilder) { 32 | super(resourceBuilder, 'www.example.com/api'); 33 | 34 | this.resource1 = this.createResource({ 35 | endpoint: '/test', 36 | }); 37 | this.parent1 = this.createParentResource({ 38 | endpoint: '/parent1', 39 | resourceDictionaryBuilder: testChildBuilder(resourceBuilder), 40 | }); 41 | this.parentSingleton = this.createParentResourceView({ 42 | endpoint: '/parentSingleton', 43 | resourceDictionaryBuilder: testChildBuilder(resourceBuilder), 44 | }); 45 | } 46 | } 47 | 48 | describe('contractLibrary', (): void => { 49 | let testLibrary: TestLibrary; 50 | let resourceBuilder: IResourceBuilder; 51 | 52 | beforeEach((): void => { 53 | resourceBuilder = new ResourceBuilder({}, {}); 54 | testLibrary = new TestLibrary(resourceBuilder); 55 | }); 56 | 57 | describe('urls', (): void => { 58 | it('should build the url of the resource using the base endpoint', (): void => { 59 | expect(testLibrary.resource1.endpoint).to.equal('/test'); 60 | expect(testLibrary.resource1.url).to.equal('www.example.com/api/test'); 61 | }); 62 | 63 | it('should default to the endpoint if the builder is used directly', (): void => { 64 | testLibrary.resource1 = resourceBuilder.createResource({ 65 | endpoint: '/test', 66 | }); 67 | expect(testLibrary.resource1.url).to.equal('/test'); 68 | }); 69 | 70 | it('should build the child url from the parent', (): void => { 71 | expect(testLibrary.parent1.childContracts().childResource.url).to.equal('www.example.com/api/parent1/childResource'); 72 | expect(testLibrary.parent1.childContracts().resourceView.url).to.equal('www.example.com/api/parent1/resourceView'); 73 | expect(testLibrary.parentSingleton.childContracts().childResource.url).to.equal('www.example.com/api/parentSingleton/childResource'); 74 | expect(testLibrary.parentSingleton.childContracts().resourceView.url).to.equal('www.example.com/api/parentSingleton/resourceView'); 75 | }); 76 | 77 | it('should append the id of the parent if a parent is selected', (): void => { 78 | expect(testLibrary.parent1.childContracts(11).childResource.url).to.equal('www.example.com/api/parent1/11/childResource'); 79 | expect(testLibrary.parent1.childContracts(11).resourceView.url).to.equal('www.example.com/api/parent1/11/resourceView'); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataService/view/dataServiceView.ts: -------------------------------------------------------------------------------- 1 | import { IHttpUtility, HttpUtility } from 'rl-http'; 2 | 3 | import { IArrayUtility } from '../../../array/array.service'; 4 | import { IConverter } from '../../converters/converters'; 5 | import { IDataService, DataService, IBaseDomainObject } from '../data.service'; 6 | import { IParentDataService, ParentDataService } from '../parent/parentData.service'; 7 | import { ISingletonDataService, SingletonDataService } from '../../singletonDataService/singletonData.service'; 8 | import { IParentSingletonDataService, ParentSingletonDataService } from '../../singletonDataService/parent/parentSingletonData.service'; 9 | import { IBaseResourceParams, IParentResourceParams } from '../../resourceBuilder/resourceBuilder.service'; 10 | 11 | import * as _ from 'lodash'; 12 | 13 | export interface IDataServiceView extends IDataService { 14 | AsSingleton(parentId: number): ISingletonDataService; 15 | } 16 | 17 | export interface IParentDataServiceView 18 | extends IParentDataService { 19 | AsSingleton(parentId: number): IParentSingletonDataService; 20 | } 21 | 22 | export class DataServiceView 23 | extends DataService 24 | implements IDataServiceView { 25 | 26 | private http: IHttpUtility; 27 | private transform: IConverter | { [index: string]: IConverter }; 28 | 29 | constructor(http: HttpUtility 30 | , array: IArrayUtility 31 | , options: IBaseResourceParams) { 32 | super(http, array, options); 33 | this.http = http; 34 | this.transform = options.transform; 35 | } 36 | 37 | AsSingleton(parentId: number): ISingletonDataService { 38 | let mockData: TDataType = _.find(this.mockData, (item: TDataType): boolean => { 39 | return item.id === parentId; 40 | }); 41 | let singleton: SingletonDataService = new SingletonDataService(this.http, { 42 | endpoint: this.endpoint, 43 | mockData: mockData, 44 | transform: this.transform, 45 | useMock: this.useMock, 46 | logRequests: this.logRequests, 47 | }); 48 | singleton.url = this.url; 49 | return singleton; 50 | } 51 | } 52 | 53 | export class ParentDataServiceView 54 | extends ParentDataService 55 | implements IParentDataServiceView { 56 | 57 | private http: IHttpUtility; 58 | private transform: IConverter | { [index: string]: IConverter }; 59 | 60 | constructor(http: HttpUtility 61 | , array: IArrayUtility 62 | , options: IParentResourceParams) { 63 | super(http, array, options); 64 | this.http = http; 65 | } 66 | 67 | AsSingleton(parentId: number): IParentSingletonDataService { 68 | let mockData: TDataType = _.find(this.mockData, (item: TDataType): boolean => { 69 | return item.id === parentId; 70 | }); 71 | let singleton: ParentSingletonDataService = new ParentSingletonDataService(this.http, { 72 | endpoint: this.endpoint, 73 | mockData: mockData, 74 | resourceDictionaryBuilder: this.resourceDictionaryBuilder, 75 | transform: this.transform, 76 | useMock: this.useMock, 77 | logRequests: this.logRequests, 78 | parentId: parentId, 79 | }); 80 | singleton.url = this.url; 81 | return singleton; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /source/services/observable/observable.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { ObservableService } from './observable.service'; 2 | 3 | interface IMockExceptionHandler { 4 | handleError: sinon.SinonSpy; 5 | } 6 | 7 | describe('observable', () => { 8 | let observable: ObservableService; 9 | let exceptionHandler: IMockExceptionHandler; 10 | 11 | beforeEach(() => { 12 | exceptionHandler = { 13 | handleError: sinon.spy(), 14 | }; 15 | 16 | observable = new ObservableService(exceptionHandler); 17 | }); 18 | 19 | it('should register a watcher and call the action when fire is called', (): void => { 20 | let func: sinon.SinonSpy = sinon.spy(); 21 | 22 | observable.register(func); 23 | observable.fire(); 24 | 25 | sinon.assert.calledOnce(func); 26 | }); 27 | 28 | it('should unregister only the indicated watcher', (): void => { 29 | let registeredFunc1: sinon.SinonSpy = sinon.spy(); 30 | let unregisteredFunc: sinon.SinonSpy = sinon.spy(); 31 | let registeredFunc2: sinon.SinonSpy = sinon.spy(); 32 | 33 | observable.register(registeredFunc1); 34 | let cancel: () => void = observable.register(unregisteredFunc); 35 | observable.register(registeredFunc2); 36 | 37 | cancel(); 38 | 39 | observable.fire(); 40 | 41 | sinon.assert.calledOnce(registeredFunc1); 42 | sinon.assert.notCalled(unregisteredFunc); 43 | sinon.assert.calledOnce(registeredFunc2); 44 | }); 45 | 46 | it('should only call watcher registered with the specified event if fire is called with an event', (): void => { 47 | let funcWithEvent: sinon.SinonSpy = sinon.spy(); 48 | let funcWithoutEvent: sinon.SinonSpy = sinon.spy(); 49 | 50 | observable.register(funcWithEvent, 'myEvent'); 51 | observable.register(funcWithoutEvent); 52 | observable.fire('myEvent'); 53 | 54 | sinon.assert.notCalled(funcWithoutEvent); 55 | sinon.assert.calledOnce(funcWithEvent); 56 | }); 57 | 58 | it('should not call watchers registered with a different event', (): void => { 59 | let func: sinon.SinonSpy = sinon.spy(); 60 | 61 | observable.register(func, 'myEvent'); 62 | observable.fire('otherEvent'); 63 | 64 | sinon.assert.notCalled(func); 65 | }); 66 | 67 | it('should call the registered watchers with the additional params passed into the fire function', (): void => { 68 | let func: sinon.SinonSpy = sinon.spy(); 69 | 70 | observable.register(func, 'myEvent'); 71 | observable.fire('myEvent', 1, 2, 3, 4, 5); 72 | 73 | sinon.assert.calledOnce(func); 74 | 75 | let args: number[] = func.firstCall.args; 76 | expect(args[0]).to.equal(1); 77 | expect(args[1]).to.equal(2); 78 | expect(args[2]).to.equal(3); 79 | expect(args[3]).to.equal(4); 80 | expect(args[4]).to.equal(5); 81 | }); 82 | 83 | // skipped because it is failing, has been for some time, and no one knows why 84 | // the observable service has been deprecated, use rxjs instead 85 | // not going to waste resources investigating a failing test on a deprecated service 86 | xit('should return with an error if no function is provided', (): void => { 87 | let cancel: Function = observable.register(null); 88 | 89 | sinon.assert.calledOnce(exceptionHandler.handleError); 90 | sinon.assert.calledWith(exceptionHandler.handleError, new Error('Watcher must be a function')); 91 | 92 | expect(cancel).to.be.null; 93 | }); 94 | 95 | // same scenario as previous test 96 | xit('should return with an error if the event is not allowed', (): void => { 97 | observable.allowableEvents = ['event1', 'event2']; 98 | 99 | let cancel: Function = observable.register((): void => { return; }, 'event3'); 100 | 101 | sinon.assert.calledOnce(exceptionHandler.handleError); 102 | sinon.assert.calledWith(exceptionHandler.handleError, new Error('This event is not allowed. Events: event1, event2')); 103 | 104 | expect(cancel).to.be.null; 105 | 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /source/services/date/date.md: -------------------------------------------------------------------------------- 1 | ## date 2 | Contains a collection of helper functions, primarily focused around dates and less on time. 3 | 4 | Strings are parsed using momentjs. For more details on how parsing works and how to formulate format strings, go here: http://momentjs.com/docs/#/parsing/string-format/ 5 | 6 | Format strings are usually optional. If they are not specified, they default to `'YYYY-MM-DDTHH:mm:ssZ'` 7 | 8 | #### `getFullString(month: number): string` 9 | Take the zero based month number (December = 11) and convert it to the string name of the month. 10 | 11 | #### `subtractDates(start: string | Date | moment.Moment, end: string | Date | moment.Moment, dateFormat?: string): IDateValue` 12 | Calculates the difference between two dates. Returns the difference in years, months, and days. If the inputs are strings, will use the format string to parse them. 13 | 14 | ``` 15 | export interface IDateValue { 16 | years: number; 17 | months: number; 18 | days: number; 19 | } 20 | ``` 21 | 22 | ``` 23 | let difference = dateUtility.subtractDates('01-01-2000', '02-03-2002'); 24 | // difference.years === 2 25 | // difference.months === 1 26 | // difference.days === 2 27 | ``` 28 | 29 | #### `subtractDateInDays(start: string | Date | moment.Moment, end: string | Date | moment.Moment, dateFormat?: string): number` 30 | Calculates the number of days between two dates. If the inputs are strings, will parse them and then calculate the difference. 31 | 32 | ``` 33 | let difference = dateUtility.subtractDateInDays('01-01-2001', '01-05-2003'); 34 | // difference === 734 35 | ``` 36 | 37 | #### `subtractDateInMilliseconds(start: string | Date | moment.Moment, end: string | Date | moment.Moment, dateFormat?: string): number` 38 | Calculates the difference between two dates in milliseconds. If the inputs are strings, will parse them and then calculate the difference. 39 | 40 | #### `compareDates(date1: string | Date | moment.Moment, date2: string | Date | moment.Moment, dateFormat?: string): CompareResult` 41 | Compares two dates and indicates if the first date is less than, equal to, or greater than the second date. If the inputs are strings they will be parsed before comparing. 42 | 43 | CompareResult: https://github.com/RenovoSolutions/TypeScript-Angular-Utilities/blob/master/README.md#compareresult 44 | 45 | #### `dateInRange(date: string | Date | moment.Moment, rangeStart: string | Date | moment.Moment, rangeEnd: string | Date): boolean` 46 | Tests to see if a date is in between two other dates. Any strings are parsed before comparing. 47 | 48 | #### `getDateFromISOString(date: string): moment.Moment` 49 | Converts an [ISO format date string](https://www.w3.org/TR/NOTE-datetime) into a moment object. 50 | 51 | #### `isDate(date: string | Date | moment.Moment, dateFormat?: string): boolean` 52 | Tests the input to see if it is a valid date. For Date inputs, asserts that `Date.prototype.getTime()` does not return NaN. For string inputs, verifies that the string parses into a valid date. 53 | 54 | #### `getNow(): moment.Moment` 55 | Returns a date representing "now." Can be useful for getting now as this allows tests to mock "now" instead of relying on `new Date()` directly. 56 | 57 | #### `formatDate(date: string | Date | moment.Moment, dateFormat?: string): string` 58 | Pretty print a Date using the given format string. 59 | 60 | #### `sameDate(date1: string | Date | moment.Moment, date2: string | Date | moment.Moment, date1Format?: string, date2Format?: string): boolean` 61 | Returns true if the two dates specify the same DAY without regard to hours, minutes, seconds, or milliseconds. Attempts to parse strings inputs into dates. 62 | 63 | #### `sameDateTime(date1: string | Date | moment.Moment, date2: string | Date | moment.Moment, date1Format?: string, date2Format?: string): boolean` 64 | Returns true if the two dates specify the same DAY/HOUR/MINUTE without regard to seconds or milliseconds. Attempts to parse strings inputs into dates. 65 | -------------------------------------------------------------------------------- /source/services/dataContracts/dataService/parent/parentDataService.md: -------------------------------------------------------------------------------- 1 | ## Parent Data service 2 | Specifies a data contract that points to a resource that is part of a hierarchy. 3 | 4 | Extends [dataService](../dataService.md). 5 | 6 | ### Options 7 | 8 | #### `IChildResources` 9 | An interface that specifies the data model of the children. This provides strong typing for accessing child resources. 10 | 11 | #### `resourceDictionaryBuilder` 12 | A function that tells the resourceBuilder how to build up the children of the parent resource. 13 | 14 | `endpoint`, `interface`, `useMock`, `logRequests`, `searchParams`, `transform`. 15 | 16 | See [dataContract](../../baseDataService.md) for details on the base options. 17 | 18 | ### Interface 19 | The following functions are available for consumers of the parent data service. 20 | 21 | `getList`, `getDetail`, `create`, `update`, `delete`, `version`. 22 | 23 | See [dataService](../dataService.md) for details on the base options. 24 | 25 | #### `childContracts(id?: number): TResourceDictionaryType` 26 | Returns the children of the data service. If an id is provided, the children are scoped to a single parent object. 27 | 28 | ##### **Handling urls** 29 | Unlike the base library itself, parent data services handle the details of building up the url of the children. The format of the child url depends on whether a parent was selected by passing an id to the `childContracts()` function, or the child is scoped against the whole parent resource. If a parent is selected, the url is constructed as `/parentUrl/:id/childEndpoint`. Otherwise the url is constructed as `/parentUrl/childEndpoint`. 30 | 31 | Example: 32 | For this example, the parent is a user resource, with endpoint `/user` appended to a base url of `www.example.com/api`. The child is a settings resource with endpoint `/settings`. 33 | 34 | With no parent selected: 35 | ``` 36 | this.settingsResource = dataServices.user.childContracts().settings; 37 | ``` 38 | The url of the settings resource in this case is `www.example.com/api/user/settings`. 39 | 40 | With parent selected: 41 | ``` 42 | this.settingsResource = dataServices.user.childContracts(11).settings; 43 | ``` 44 | The url of the settings resource in this case is `www.example.com/api/user/11/settings`. 45 | 46 | ##### **Use cases** 47 | There are two main use cases for child resources. The first is where the parent has a one-to-many relationship with the child. In this case, the child resource is a fully qualified rest resource whether it is scoped against a single parent, or the parent resource as a whole. In the other case, the child resource is a subset or `view` of the parent resource. In this case, if the the resource is scoped against a single parent, the child resource is treated as a singleton. 48 | 49 | Example: 50 | The parent is a `user` resource, with a child resource representing `settings` for that user. 51 | 52 | With no parent selected: 53 | ``` 54 | this.settingsResource = dataServices.user.childContracts().settings; 55 | 56 | // settings is treated as a standard data service 57 | this.settingsResource.getList(); 58 | this.settingsResource.getDetail(11); 59 | this.settingsResource.create(object); 60 | this.settingsResource.update(object); 61 | this.settingsResource.delete(object); 62 | this.settingsResource.version(2); 63 | ``` 64 | 65 | With parent selected: 66 | ``` 67 | this.settingsResource = dataServices.user.childContracts(11).settings; 68 | 69 | // settings is treated as a singleton data service 70 | this.settingsResource.get(); 71 | this.settingsResource.update(object); 72 | this.settingsResource.version(2); 73 | ``` 74 | Note that in this case a single settings entry may be accessed either via: 75 | ``` 76 | dataServices.user.childContracts().settings.getDetail(11) 77 | // url www.example.com/api/user/settings/11 78 | ``` 79 | Or: 80 | ``` 81 | dataServices.user.childContracts(11).settings.get() 82 | // url www.example.com/api/users/11/settings 83 | ``` 84 | You should verify that the API supports both url forms, and if not, be careful to only use the form that is supported by the API. 85 | 86 | See [dataServiceView](../view/dataServiceView.md) for details on how to define a `view`. -------------------------------------------------------------------------------- /source/services/errorHandler/errorHandler.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultErrors, 3 | DefaultLoginUrlSettings, 4 | ErrorHandlerService, 5 | IErrorHandlerService, 6 | IRejection, 7 | HttpStatusCode, 8 | } from './errorHandler.service'; 9 | 10 | import { IRedirectService } from '../redirect/redirect.service'; 11 | 12 | interface INotificationMock { 13 | error: sinon.SinonSpy; 14 | } 15 | 16 | describe('errorHandler', () => { 17 | var errorHandler: IErrorHandlerService; 18 | var redirect: IRedirectService; 19 | var notification: INotificationMock; 20 | var returnUrl: string; 21 | 22 | beforeEach(() => { 23 | redirect = { 24 | getCurrentLocationAsParam: () => { 25 | return returnUrl; 26 | }, 27 | to: sinon.spy(), 28 | }; 29 | 30 | notification = { 31 | error: sinon.spy(), 32 | }; 33 | 34 | const exceptionHandler: any = { 35 | call: sinon.spy(), 36 | }; 37 | 38 | errorHandler = new ErrorHandlerService( 39 | redirect, 40 | exceptionHandler, 41 | notification, 42 | new DefaultErrors(), 43 | new DefaultLoginUrlSettings() 44 | ); 45 | }); 46 | 47 | it('should redirect the user to the login page with a redirect url on an unauthorized error', (): void => { 48 | var rejection: IRejection = { 49 | status: HttpStatusCode.unauthorized 50 | }; 51 | returnUrl = 'myReturnUrl'; 52 | 53 | errorHandler.httpResponseError(rejection); 54 | 55 | sinon.assert.calledOnce(redirect.to); 56 | sinon.assert.calledWith(redirect.to, '/login?returnUrl=myReturnUrl'); 57 | }); 58 | 59 | it('should show an error for insufficient permissions', (): void => { 60 | var rejection: IRejection = { 61 | status: HttpStatusCode.forbidden 62 | }; 63 | 64 | errorHandler.httpResponseError(rejection); 65 | 66 | sinon.assert.calledOnce(notification.error); 67 | sinon.assert.calledWith(notification.error, 'You have insufficient permissions to perform this action'); 68 | }); 69 | 70 | it('should show an error for invalid url', (): void => { 71 | var rejection: IRejection = { 72 | status: HttpStatusCode.invalidUrl 73 | }; 74 | 75 | errorHandler.httpResponseError(rejection); 76 | 77 | sinon.assert.calledOnce(notification.error); 78 | sinon.assert.calledWith(notification.error, 'Resource not found. This issue has been logged'); 79 | }); 80 | 81 | it('should show an error for gone resource', (): void => { 82 | var rejection: IRejection = { 83 | status: HttpStatusCode.gone 84 | }; 85 | 86 | errorHandler.httpResponseError(rejection); 87 | 88 | sinon.assert.calledOnce(notification.error); 89 | sinon.assert.calledWith(notification.error, 'The requested resource is no longer available.'); 90 | }); 91 | 92 | it('should show an error for timeout', (): void => { 93 | var rejection: IRejection = { 94 | status: HttpStatusCode.timeout 95 | }; 96 | 97 | errorHandler.httpResponseError(rejection); 98 | 99 | sinon.assert.calledOnce(notification.error); 100 | sinon.assert.calledWith(notification.error, 'Request timed out. Check your network connection or contact your administrator for issues'); 101 | }); 102 | 103 | it('should show an error for system error', (): void => { 104 | var rejection: IRejection = { 105 | status: HttpStatusCode.internalServerError 106 | }; 107 | 108 | errorHandler.httpResponseError(rejection); 109 | 110 | sinon.assert.calledOnce(notification.error); 111 | sinon.assert.calledWith(notification.error, 'The system has encountered an error. This issue has been logged.' + 112 | ' Please contact support if you are unable to complete critical tasks'); 113 | }); 114 | 115 | it('should show a custom error for bad request error', (): void => { 116 | var errorMessage: string = 'An error occurred'; 117 | 118 | var rejection: IRejection = { 119 | status: HttpStatusCode.badRequest, 120 | data: errorMessage 121 | }; 122 | 123 | errorHandler.httpResponseError(rejection); 124 | 125 | sinon.assert.calledOnce(notification.error); 126 | sinon.assert.calledWith(notification.error, errorMessage); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /source/services/test/angularFixture.ts: -------------------------------------------------------------------------------- 1 | import * as angular from 'angular'; 2 | import 'angular-mocks'; 3 | 4 | import * as _ from 'lodash'; 5 | 6 | export interface IControllerResult { 7 | controller: TControllerType; 8 | scope: angular.IScope; 9 | } 10 | 11 | export interface IDirectiveResult { 12 | directive: angular.IDirective; 13 | scope: angular.IScope; 14 | controller: TControllerType; 15 | } 16 | 17 | export interface IAngularFixture { 18 | inject: (...serviceNames: string[]) => any; 19 | mock: (mocks: any) => void; 20 | componentController(componentName: string, bindings?: any, locals?: any): IControllerResult; 21 | controllerWithBindings(controllerName: string, bindings?: any, locals?: any, scope?: any) 22 | : IControllerResult; 23 | directive(directiveName: string, dom: string, scope: angular.IScope): IDirectiveResult; 24 | } 25 | 26 | class AngularFixture implements IAngularFixture { 27 | inject(...serviceNames: string[]): Object { 28 | // object that will contain all of the services requested 29 | var services: Object = {}; 30 | 31 | // clone the array and add a function that iterates over the original array 32 | // this avoids iterating over the function itself 33 | var injectParameters: any[] = _.clone(serviceNames); 34 | injectParameters.push((...injectedServices: any[]) => { 35 | // should get called with the services injected by angular 36 | // we'll add these to services using the serviceName as the key 37 | _.each(serviceNames, (service: string, index: number) => { 38 | services[service] = injectedServices[index]; 39 | }); 40 | }); 41 | 42 | angular.mock.inject(injectParameters); 43 | 44 | return services; 45 | } 46 | 47 | mock(mocks: any): void { 48 | angular.mock.module(($provide: angular.auto.IProvideService) => { 49 | _.each(mocks, (value: any, key: number) => { 50 | $provide.value(key.toString(), value); 51 | }); 52 | }); 53 | } 54 | 55 | componentController(componentName: string, bindings?: any, locals?: any, scope?: any): IControllerResult { 56 | const services: any = this.inject('$rootScope', '$componentController'); 57 | const $rootScope: angular.IRootScopeService = services.$rootScope; 58 | const $componentController: angular.IComponentControllerService = services.$componentController; 59 | 60 | scope = _.extend($rootScope.$new(), scope); 61 | 62 | if (locals == null) { 63 | locals = {}; 64 | } 65 | 66 | locals.$scope = scope; 67 | 68 | return { 69 | scope: scope, 70 | controller: $componentController(componentName, locals, bindings), 71 | }; 72 | } 73 | 74 | controllerWithBindings(controllerName: string, bindings?: any, locals?: any, scope?: any) 75 | : IControllerResult { 76 | var services: any = this.inject('$rootScope', '$controller'); 77 | var $rootScope: angular.IRootScopeService = services.$rootScope; 78 | var $controller: angular.IControllerService = services.$controller; 79 | 80 | scope = _.extend($rootScope.$new(), scope); 81 | 82 | if (locals == null) { 83 | locals = {}; 84 | } 85 | 86 | locals.$scope = scope; 87 | 88 | return { 89 | scope: scope, 90 | controller: $controller(controllerName, locals, bindings), 91 | }; 92 | } 93 | 94 | directive(directiveName: string, dom: string, scope: any): IDirectiveResult { 95 | var services: any = this.inject('$rootScope', '$compile'); 96 | scope = _.extend(services.$rootScope.$new(), scope); 97 | 98 | var $compile: angular.ICompileService = services.$compile; 99 | 100 | var component: angular.IAugmentedJQuery = $compile(dom)(scope); 101 | scope.$digest(); 102 | 103 | return { 104 | directive: component, 105 | scope: component.isolateScope(), 106 | controller: component.controller(directiveName), 107 | }; 108 | } 109 | } 110 | 111 | export var angularFixture: IAngularFixture = new AngularFixture(); 112 | -------------------------------------------------------------------------------- /source/services/dataContracts/baseDataService.md: -------------------------------------------------------------------------------- 1 | ## Data contract 2 | Defines a contract with the server for a single REST resource. Contracts can come in several different types, but they all share a set of base options. 3 | 4 | ### Options 5 | 6 | #### `endpoint` 7 | The endpoint of the resource. Generally this should be of the form `/entity`. The rest of the url will be filled in when the contract is wired up. If no endpoint is specified, `useMock` defaults to true. 8 | 9 | #### `interface` 10 | The contract should provide a typescript interface representing the data model expected from the server. This can be specified as a generic type against the contract to provide strong typing. 11 | 12 | #### `mockData (optional)` 13 | The contract can provide a json mock representing a sample return value from the server. This can be used in place of an actual request to the server for development purposes or for unit tests. 14 | 15 | #### `useMock (default: false)` 16 | If true, the contract will return the json mock data instead of making a request to the server. 17 | 18 | #### `logRequests (default: false)` 19 | If true, the contract logs all requests to the console, whether mocked or actual. This is useful for debugging, and for using mocks in particular, since no requests will be picked up by the browser. 20 | 21 | #### `searchParams` 22 | Another interface that can be used to specify what search parameters can be provided with a GET request to the server. 23 | 24 | #### `transform` 25 | An object map specifying how to transform data going to and from the server. Each key may specify a [converters](./converters/converters.md) that can be used to apply the transformation, or a converter for the whole object may be specified. See [converters](./converters/converters.md) for details. 26 | 27 | ### Example 28 | An example contract definition: 29 | ``` 30 | import { services } from 'typescript-angular-utilities'; 31 | import __converters = services.dataContracts.converters; 32 | 33 | export let endpoint: string = '/user'; 34 | 35 | export interface IUser { 36 | username: string; 37 | lastLogin: Date; 38 | } 39 | 40 | export interface ISearchParams { 41 | username?: string; 42 | isAdmin?: string; 43 | } 44 | 45 | export let transform: any = { 46 | lastLogin: __converters.dateConverter, 47 | }; 48 | 49 | // alternatively import from a json doc 50 | export let mockData: ICar[] = [ 51 | { username: 'bobsmith', lastLogin: '2016-3-30T10:30:30' }, 52 | { username: 'johnjones', lastLogin: '2016-3-15T3:15:45' }, 53 | ]; 54 | ``` 55 | Implementation: 56 | ``` 57 | import { services } from 'typescript-angular-utilities'; 58 | import __dataContracts = services.dataContracts; 59 | 60 | import * as user from './myContract'; 61 | 62 | export { user }; 63 | 64 | export class DataServices extends __dataContracts.ContractLibrary { 65 | user: __dataContracts.IDataService; 66 | 67 | static $inject: string[] = [__dataContracts.builderServiceName]; 68 | constructor(baseResourceBuilder: __dataContracts.IBaseResourceBuilder) { 69 | super(baseResourceBuilder); 70 | // as yet we don't have a way to plug in the base endpoint 71 | let baseEndpoint: string = 'www.example.com/api'; 72 | 73 | this.user = this.createResource({ 74 | endpoint: baseEndpoint + user.endpoint, 75 | mockData: user.mockData, 76 | transform: user.transform, 77 | useMock: true, 78 | logRequests: true, 79 | }); 80 | } 81 | } 82 | ``` 83 | Usage: 84 | ``` 85 | import { services } from 'typescript-angular-utilities'; 86 | import __dataContracts = services.dataContracts; 87 | 88 | import { DataServices, serviceName as dataServiceName, user } from '../data/data.service'; 89 | 90 | export class MyConsumer { 91 | userResource: __dataContracts.IDataService; 92 | 93 | static $inject: string[] = [dataServiceName]; 94 | constructor(dataServices: DataServices) { 95 | this.userResource = dataServices.user; 96 | } 97 | 98 | action(search: user.ISearchParams): void { 99 | this.userResource.getList(search).then((users: IUser[]): void => { 100 | console.log(users); 101 | }); 102 | } 103 | } 104 | ``` -------------------------------------------------------------------------------- /source/services/object/object.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import * as _ from 'lodash'; 4 | import * as moment from 'moment'; 5 | 6 | import { IArrayUtility, ArrayUtility, arrayUtility } from '../array/array.service'; 7 | 8 | import { IDateUtility, DateUtility, dateUtility } from '../date/index'; 9 | 10 | export interface IObjectUtility { 11 | isNullOrEmpty(object: any[]): boolean; 12 | isNullOrEmpty(object: number): boolean; 13 | isNullOrEmpty(object: string): boolean; 14 | isNullOrEmpty(object: any): boolean; 15 | isNullOrWhitespace(object: any[]): boolean; 16 | isNullOrWhitespace(object: number): boolean; 17 | isNullOrWhitespace(object: string): boolean; 18 | isNullOrWhitespace(object: any): boolean; 19 | areEqual(obj1: any, obj2: any): boolean; 20 | toString(object: any): string; 21 | valueOrDefault(value: any, defaultValue: any): any; 22 | propertyNameToString(propertyFunction: () => any): string; 23 | 24 | } 25 | 26 | @Injectable() 27 | export class ObjectUtility implements IObjectUtility { 28 | private array: IArrayUtility; 29 | private dateUtility: IDateUtility; 30 | 31 | constructor(array: ArrayUtility, dateUtility: DateUtility) { 32 | this.array = array; 33 | this.dateUtility = dateUtility; 34 | } 35 | 36 | isNullOrEmpty(object: any): boolean { 37 | if (object == null) { 38 | return true; 39 | } else if (_.isArray(object)) { 40 | return object.length <= 0; 41 | } else if (_.isNumber(object)) { 42 | return _.isNaN(object); 43 | } else { 44 | return object === ''; 45 | } 46 | } 47 | 48 | isNullOrWhitespace(object: any): boolean { 49 | if (_.isString(object)) { 50 | object = (object).trim(); 51 | } 52 | 53 | return this.isNullOrEmpty(object); 54 | } 55 | 56 | areEqual(obj1: any, obj2: any): boolean { 57 | var type1: string = typeof obj1; 58 | var type2: string = typeof obj2; 59 | 60 | if (obj1 == null && obj2 == null) { 61 | return true; 62 | } else if (obj1 == null || obj2 == null) { 63 | return false; 64 | } 65 | 66 | if (type1 !== type2) { 67 | return false; 68 | } else if (obj1 instanceof Array) { 69 | if (obj1.length !== obj2.length) { 70 | return false; 71 | } 72 | 73 | for (var i: number = 0; i < obj1.length; i++) { 74 | if (this.areEqual(obj1[i], obj2[i]) === false) { 75 | return false; 76 | } 77 | } 78 | } else if (this.areDates(obj1, obj2)) { 79 | return this.dateUtility.sameDateTime(obj1, obj2); 80 | } else if (type1 === 'object') { 81 | //init an object with the keys from obj2 82 | var keys2: string[] = _.keys(obj2); 83 | _.forIn(obj1, (value: any, key: string): boolean => { 84 | if (_.has(obj2, key)) { 85 | //compare value against the value with the same key in obj2, then remove the key 86 | if (this.areEqual(value, obj2[key]) === false) { 87 | return false; 88 | } else { 89 | this.array.remove(keys2, key); 90 | } 91 | } else { 92 | return false; 93 | } 94 | }); 95 | //if there are still keys left in keys2, we know they are not equal (obj2 has more properties) 96 | if (_.some(keys2)) { 97 | return false; 98 | } 99 | } else { 100 | //if types are primitive, do a simple comparison 101 | return obj1 === obj2; 102 | } 103 | 104 | return true; 105 | } 106 | 107 | private areDates(obj1: any, obj2: any): boolean { 108 | if ((_.isDate(obj1) && _.isDate(obj2)) 109 | || (moment.isMoment(obj1) && moment.isMoment(obj2))) { 110 | return true; 111 | } 112 | 113 | return false; 114 | } 115 | 116 | toString(object: any): string { 117 | return object + ''; 118 | } 119 | 120 | valueOrDefault(value: any, defaultValue: any): any { 121 | if (value != null) { 122 | return value; 123 | } else { 124 | return defaultValue; 125 | } 126 | } 127 | 128 | propertyNameToString(propertyFunction: () => any): string { 129 | let stringValue = propertyFunction.toString(); 130 | let regExpLiteral = /\.([^\.;]+);?\s*\}$/; 131 | let propertyName = regExpLiteral.exec(stringValue)[1]; 132 | return propertyName; 133 | } 134 | } 135 | 136 | export const objectUtility: ObjectUtility = new ObjectUtility(arrayUtility, dateUtility); 137 | -------------------------------------------------------------------------------- /source/services/search/search.service.tests.ts: -------------------------------------------------------------------------------- 1 | import { searchUtility } from './search.service'; 2 | 3 | interface ITestObject { 4 | prop: string; 5 | } 6 | 7 | interface ITestObject2 { 8 | prop1?: number; 9 | prop2?: string; 10 | } 11 | 12 | interface INestedTestObject { 13 | prop?: string; 14 | nestedObject: ITestObject2; 15 | } 16 | 17 | describe('search utility', () => { 18 | it('should return true for all items if search is null or empty', (): void => { 19 | let object1: ITestObject = { 20 | prop: 'some string', 21 | }; 22 | 23 | let object2: ITestObject = { 24 | prop: 'another value', 25 | }; 26 | 27 | expect(searchUtility.search(object1, null)).to.be.true; 28 | expect(searchUtility.search(object2, null)).to.be.true; 29 | expect(searchUtility.search(object1, '')).to.be.true; 30 | expect(searchUtility.search(object2, '')).to.be.true; 31 | }); 32 | 33 | it('should search the actual data values if they arent objects', (): void => { 34 | expect(searchUtility.search(1, '2')).to.be.false; 35 | expect(searchUtility.search(2, '2')).to.be.true; 36 | expect(searchUtility.search(3, '2')).to.be.false; 37 | expect(searchUtility.search(4, '2')).to.be.false; 38 | expect(searchUtility.search(5, '2')).to.be.false; 39 | }); 40 | 41 | it('should include items that contain the search string', (): void => { 42 | let searchText: string = 'my'; 43 | let caseSensitive: boolean = true; 44 | 45 | let matchingObject1: ITestObject2 = { 46 | prop2: 'my string', 47 | }; 48 | 49 | let matchingObject2: ITestObject2 = { 50 | prop1: 5, 51 | prop2: 'some string with my', 52 | }; 53 | 54 | let objectWithoutSearchString: ITestObject2 = { 55 | prop1: 2, 56 | }; 57 | 58 | let objectWithDifferentCase: ITestObject2 = { 59 | prop1: 5, 60 | prop2: 'MY string', 61 | }; 62 | 63 | expect(searchUtility.search(matchingObject1, searchText, caseSensitive)).to.be.true; 64 | expect(searchUtility.search(objectWithoutSearchString, searchText, caseSensitive)).to.be.false; 65 | expect(searchUtility.search(matchingObject2, searchText, caseSensitive)).to.be.true; 66 | expect(searchUtility.search(objectWithDifferentCase, searchText, caseSensitive)).to.be.false; 67 | }); 68 | 69 | it('should include items that contain the search string, case insensitive', (): void => { 70 | let searchText: string = 'my'; 71 | let caseInsensitive: boolean = false; 72 | let lowercaseMatch: ITestObject2 = { 73 | prop2: 'my string', 74 | }; 75 | 76 | let uppercaseMatch: ITestObject2 = { 77 | prop1: 2.2, 78 | prop2: 'MY string', 79 | }; 80 | 81 | expect(searchUtility.search(lowercaseMatch, searchText, caseInsensitive)).to.be.true; 82 | expect(searchUtility.search(uppercaseMatch, searchText, caseInsensitive)).to.be.true; 83 | }); 84 | 85 | it('should recursively search the properties of an object', (): void => { 86 | let searchText: string = 'my'; 87 | let caseInsensitive: boolean = false; 88 | let objectWithNestedObject: INestedTestObject = { 89 | nestedObject: { 90 | prop2: 'my string', 91 | }, 92 | }; 93 | 94 | expect(searchUtility.search(objectWithNestedObject, searchText, caseInsensitive)).to.be.true; 95 | }); 96 | 97 | describe('tokenized', (): void => { 98 | it('should match against objects where each token appears at least once', (): void => { 99 | let searchText: string = 'some text'; 100 | 101 | let object1: ITestObject = { 102 | prop: 'some values ended up with text', 103 | }; 104 | 105 | let objectWithNesting: INestedTestObject = { 106 | prop: 'some', 107 | nestedObject: { 108 | prop2: 'with text', 109 | }, 110 | }; 111 | 112 | let object2: ITestObject = { 113 | prop: 'some other something', 114 | }; 115 | 116 | expect(searchUtility.tokenizedSearch(object1, searchText)).to.be.true; 117 | expect(searchUtility.tokenizedSearch(objectWithNesting, searchText)).to.be.true; 118 | expect(searchUtility.tokenizedSearch(object2, searchText)).to.be.false; 119 | }); 120 | 121 | it('should handle a null search', (): void => { 122 | let object: ITestObject = { 123 | prop: 'some values ended up with text', 124 | }; 125 | expect(searchUtility.tokenizedSearch(object, null)).to.be.true; 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /source/services/errorHandler/errorHandler.md: -------------------------------------------------------------------------------- 1 | ## errorHandler 2 | An http intercepter that will intercept http error responses and display error messages to the notification service when errors occur. A one stop way to handle common http error responses. 3 | 4 | ### Full demonstration 5 | 6 | To wire up the handler, add the following code to an Angular 1 application: 7 | 8 | ``` 9 | import * as angular from 'angular'; 10 | 11 | import { services } from 'typescript-angular-utilities'; 12 | import __errorHandler = services.errorHandler; 13 | 14 | var moduleName = 'myCustomAngularModule'; 15 | var interceptorName: string = 'httpErrorInterceptor'; 16 | 17 | angular.module(moduleName, [__errorHandler.moduleName]) 18 | .config(httpConfiguration); 19 | 20 | httpConfiguration.$inject = ['$httpProvider']; 21 | function httpConfiguration($httpProvider: angular.IHttpProvider): void { 22 | $httpProvider.interceptors.push(interceptorName); // Add your custom interceptor by name 23 | } 24 | 25 | httpErrorInterceptor.$inject = [__errorHandler.serviceName, '$q']; 26 | function httpErrorInterceptor(errorHandler: __errorHandler.IErrorHandlerService, $q: angular.IQService): any { 27 | return { 28 | responseError: (rejection: __errorHandler.IRejection): angular.IPromise => { 29 | errorHandler.httpResponseError(rejection); 30 | return $q.reject(rejection); 31 | }, 32 | }; 33 | } 34 | 35 | ``` 36 | 37 | ### `loginUrl` 38 | The http error handler will redirect to a login URL if a 401 response code is encountered. By default, this URL is `/login`. You can override this behavior if desired by setting it on the `errorHandlerProvider`: 39 | 40 | ``` 41 | angular.module(moduleName, [__errorHandler.moduleName]) 42 | .config(httpConfiguration); 43 | 44 | httpConfiguration.$inject = [__errorHandler.serviceName + 'Provider']; 45 | function httpConfiguration(errorHandlerProvider: __errorHandler.IErrorHandlerServiceProvider): void { 46 | errorHandlerProvider.loginUrl = '/login.aspx'; 47 | } 48 | ``` 49 | 50 | ### `errorMessages` 51 | You can customize the error messages that are displayed when an http error code is received if desired. Set the errorMessages property on the errorHandlerProvider to an object implementing the following interface: 52 | ``` 53 | export interface IErrorMessages { 54 | badRequestError: string; 55 | forbiddenError: string; 56 | invalidUrlError: string; 57 | timeoutError: string; 58 | internalServerError: string; 59 | defaultError: string; 60 | } 61 | ``` 62 | 63 | * `badRequestError` is displayed when a 400 is received 64 | * `forbiddenError` is displayed when a 403 is received 65 | * `invalidUrlError` is displayed when a 404 is received 66 | * `timeoutError` is displayed when a 408 is received 67 | * `internalServerError` is displayed when 500 is received 68 | * `defaultError` is displayed for any other error status code 69 | 70 | Example: 71 | ``` 72 | angular.module(moduleName, [__errorHandler.moduleName]) 73 | .config(httpConfiguration); 74 | 75 | httpConfiguration.$inject = [__errorHandler.serviceName + 'Provider']; 76 | function httpConfiguration(errorHandlerProvider: __errorHandler.IErrorHandlerServiceProvider): void { 77 | errorHandlerProvider.errorMessages = { 78 | badRequestError: 'Your reqest failed one or more validation checks.', 79 | forbiddenError: 'You have insufficient permissions to perform this action', 80 | invalidUrlError: 'Resource not found. This issue has been logged', 81 | timeoutError: 'Request timed out. Check your network connection or contact your administrator for issues', 82 | internalServerError: 'The system has encountered an error. This issue has been logged. Please contact support if you are unable to complete critical tasks', 83 | defaultError: 'An unknown error occurred', 84 | }; 85 | } 86 | ``` 87 | 88 | ### `returnUrlParam` 89 | When a 401 is received, the browser is redirected to the login url, but the current url is appended to the login url as a return url param. This allows makes it possible for the login page to redirect back to the previous page after successfully logging in. If desired, the name of the return url param can be customized. The default value is `returnUrl`. To change the name of the return url param: 90 | ``` 91 | angular.module(moduleName, [__errorHandler.moduleName]) 92 | .config(httpConfiguration); 93 | 94 | httpConfiguration.$inject = [__errorHandler.serviceName + 'Provider']; 95 | function httpConfiguration(errorHandlerProvider: __errorHandler.IErrorHandlerServiceProvider): void { 96 | errorHandlerProvider.returnUrlParam = 'redirectTo'; 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /source/services/dataContracts/converters/converters.tests.ts: -------------------------------------------------------------------------------- 1 | import { converterService } from './converters'; 2 | 3 | interface ITestMock { 4 | id?: number; 5 | prop?: string; 6 | } 7 | 8 | interface ITestMock2 { 9 | id?: number; 10 | prop1?: number; 11 | prop2?: number; 12 | } 13 | 14 | interface IComplexTestMock { 15 | id?: number; 16 | obj: ITestMock2; 17 | } 18 | 19 | describe('Converters', () => { 20 | let transform: any; 21 | let numberConverter: any; 22 | 23 | beforeEach((): void => { 24 | transform = { 25 | fromServer: sinon.spy((rawData: ITestMock): string => { 26 | return rawData.prop; 27 | }), 28 | toServer: sinon.spy((data: string): ITestMock => { 29 | return { 30 | prop: data, 31 | }; 32 | }), 33 | }; 34 | 35 | numberConverter = { 36 | fromServer: sinon.spy((rawData: number): number => { 37 | return rawData + 1; 38 | }), 39 | toServer: sinon.spy((data: number): number => { 40 | return data - 1; 41 | }), 42 | }; 43 | }); 44 | 45 | it('should transform each entry in the list', (): void => { 46 | let dataSet: ITestMock = [ 47 | { id: 1, prop: 'item1' }, 48 | { id: 2, prop: 'item2' }, 49 | { id: 3, prop: 'item3' }, 50 | ]; 51 | 52 | let transformedData: string[] = converterService.applyTransform(dataSet, transform, false); 53 | 54 | expect(transformedData).to.have.length(3); 55 | expect(transformedData[0]).to.equal(dataSet[0].prop); 56 | expect(transformedData[1]).to.equal(dataSet[1].prop); 57 | expect(transformedData[2]).to.equal(dataSet[2].prop); 58 | sinon.assert.calledThrice(transform.fromServer); 59 | }); 60 | 61 | it('should transform a single item', (): void => { 62 | let item: ITestMock = { prop: 'item1' }; 63 | let transformedItem: string = converterService.applyTransform(item, transform, false); 64 | expect(transformedItem).to.equal(item.prop); 65 | sinon.assert.calledOnce(transform.fromServer); 66 | }); 67 | 68 | it('should reverse the transformation if toServer is specified', (): void => { 69 | let item: string = 'item1'; 70 | let transformedItem: ITestMock = converterService.applyTransform(item, transform, true); 71 | expect(transformedItem.prop).to.equal(item); 72 | sinon.assert.calledOnce(transform.toServer); 73 | }); 74 | 75 | it('should use a an object map to transform properties', (): void => { 76 | let map: any = { 77 | prop1: numberConverter, 78 | }; 79 | 80 | let item: ITestMock2 = { 81 | prop1: 4, 82 | prop2: 4, 83 | }; 84 | 85 | let transformedItem: ITestMock2 = converterService.applyTransform(item, map, false); 86 | 87 | expect(transformedItem.prop1).to.equal(5); 88 | expect(transformedItem.prop2).to.equal(4); 89 | sinon.assert.calledOnce(numberConverter.fromServer); 90 | }); 91 | 92 | it('should transform properties in reverse if toServer is specified', (): void => { 93 | let map: any = { 94 | prop1: numberConverter, 95 | }; 96 | 97 | let item: ITestMock2 = { 98 | prop1: 5, 99 | prop2: 4, 100 | }; 101 | 102 | let transformedItem: ITestMock2 = converterService.applyTransform(item, map, true); 103 | 104 | expect(transformedItem.prop1).to.equal(4); 105 | expect(transformedItem.prop2).to.equal(4); 106 | sinon.assert.calledOnce(numberConverter.toServer); 107 | }); 108 | 109 | it('should recursively transform nested object properties', (): void => { 110 | let map: any = { 111 | obj: { 112 | prop1: numberConverter, 113 | }, 114 | }; 115 | 116 | let item: IComplexTestMock = { 117 | obj: { 118 | prop1: 4, 119 | prop2: 4, 120 | }, 121 | }; 122 | 123 | let transformedItem: IComplexTestMock = converterService.applyTransform(item, map, false); 124 | 125 | expect(transformedItem.obj.prop1).to.equal(5); 126 | expect(transformedItem.obj.prop2).to.equal(4); 127 | sinon.assert.calledOnce(numberConverter.fromServer); 128 | }); 129 | 130 | it('should provide the parent object to a property transform', (): void => { 131 | let converter: any = { 132 | fromServer: sinon.spy((prop: any): any => { return prop; }), 133 | toServer: sinon.spy((prop: any): any => { return prop; }), 134 | }; 135 | 136 | let map: any = { 137 | prop: converter, 138 | }; 139 | 140 | let item: ITestMock = { 141 | prop: 'value', 142 | }; 143 | 144 | converterService.applyTransform(item, map, false); 145 | 146 | sinon.assert.calledOnce(converter.fromServer); 147 | let args: any = converter.fromServer.firstCall.args; 148 | expect(args[0]).to.equal('value'); 149 | expect(args[1]).to.deep.equal(item); 150 | 151 | converterService.applyTransform(item, map, true); 152 | 153 | sinon.assert.calledOnce(converter.toServer); 154 | args = converter.toServer.firstCall.args; 155 | expect(args[0]).to.equal('value'); 156 | expect(args[1]).to.deep.equal(item); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /source/utilitiesDowngrade.ts: -------------------------------------------------------------------------------- 1 | import { downgradeInjectable } from '@angular/upgrade/static'; 2 | import { HttpUtility } from 'rl-http'; 3 | 4 | import * as angular from 'angular'; 5 | 6 | import { ArrayUtility } from './services/array/array.service'; 7 | import { BooleanUtility } from './services/boolean/boolean.service'; 8 | import { ResourceBuilder } from './services/dataContracts/resourceBuilder/resourceBuilder.service'; 9 | import { DateUtility } from './services/date/date.service'; 10 | import { ErrorHandlerService } from './services/errorHandler/errorHandler.service'; 11 | import { GenericSearchFilterFactory } from './services/genericSearchFilter/genericSearchFilter.service'; 12 | import { GuidService } from './services/guid/guid.service'; 13 | import { DigestService } from './services/digest/digest.service'; 14 | import { NotificationService } from './services/notification/notification.service'; 15 | import { NumberUtility } from './services/number/number.service'; 16 | import { ObjectUtility } from './services/object/object.service'; 17 | import { observableToken, IObservableService } from './services/observable/observable.service'; 18 | import { StringUtility } from './services/string/string.service'; 19 | import { SynchronizedRequestsFactory } from './services/synchronizedRequests/synchronizedRequests.service'; 20 | import { TimeUtility } from './services/time/time.service'; 21 | import { TimeoutService } from './services/timeout/timeout.service'; 22 | import { TimezoneService } from './services/timezone/timezone.service'; 23 | import { TransformService } from './services/transform/transform.service'; 24 | import { ValidationService } from './services/validation/validation.service'; 25 | import { EmailValidationService } from './services/validation/emailValidation.service'; 26 | 27 | export const arrayServiceName: string = 'rlArrayService'; 28 | export const booleanServiceName: string = 'rlBooleanService'; 29 | export const resourceBuilderServiceName: string = 'rlResourceBuilderService'; 30 | export const dateServiceName: string = 'rlDateService'; 31 | export const errorHandlerServiceName: string = 'rlErrorHandlerService'; 32 | export const digestServiceName: string = 'rlDigestService'; 33 | export const genericSearchFilterServiceName: string = 'rlGenericSearchFilterService'; 34 | export const guidServiceName: string = 'rlGuidService'; 35 | export const httpServiceName: string = 'rlHttpService'; 36 | export const notificationServiceName: string = 'rlNotificationService'; 37 | export const numberServiceName: string = 'rlNumberService'; 38 | export const objectServiceName: string = 'rlObjectService'; 39 | export const observableServiceName: string = 'rlObservableService'; 40 | export const stringServiceName: string = 'rlStringService'; 41 | export const synchronizedRequestsServiceName: string = 'rlSynchronizedRequestsService'; 42 | export const timeServiceName: string = 'rlTimeService'; 43 | export const timeoutServiceName: string = 'rlTimeoutService'; 44 | export const timezoneServiceName: string = 'rlTimezoneService'; 45 | export const transformServiceName: string = 'rlTransformService'; 46 | export const validationServiceName: string = 'rlValidationService'; 47 | export const emailValidationServiceName: string = 'rlEmailValidationService'; 48 | 49 | export const moduleName: string = 'rl.utilities'; 50 | 51 | angular.module(moduleName, []) 52 | .factory(arrayServiceName, downgradeInjectable(ArrayUtility)) 53 | .factory(booleanServiceName, downgradeInjectable(BooleanUtility)) 54 | .factory(resourceBuilderServiceName, downgradeInjectable(ResourceBuilder)) 55 | .factory(dateServiceName, downgradeInjectable(DateUtility)) 56 | .factory(errorHandlerServiceName, downgradeInjectable(ErrorHandlerService)) 57 | .factory(digestServiceName, downgradeInjectable(DigestService)) 58 | .factory(genericSearchFilterServiceName, downgradeInjectable(GenericSearchFilterFactory)) 59 | .factory(guidServiceName, downgradeInjectable(GuidService)) 60 | .factory(httpServiceName, downgradeInjectable(HttpUtility)) 61 | .factory(notificationServiceName, downgradeInjectable(NotificationService)) 62 | .factory(numberServiceName, downgradeInjectable(NumberUtility)) 63 | .factory(objectServiceName, downgradeInjectable(ObjectUtility)) 64 | .factory(observableServiceName, downgradeInjectable(observableToken)) 65 | .factory(stringServiceName, downgradeInjectable(StringUtility)) 66 | .factory(synchronizedRequestsServiceName, downgradeInjectable(SynchronizedRequestsFactory)) 67 | .factory(timeServiceName, downgradeInjectable(TimeUtility)) 68 | .factory(timeoutServiceName, downgradeInjectable(TimeoutService)) 69 | .factory(timezoneServiceName, downgradeInjectable(TimezoneService)) 70 | .factory(transformServiceName, downgradeInjectable(TransformService)) 71 | .factory(validationServiceName, downgradeInjectable(ValidationService)) 72 | .factory(emailValidationServiceName, downgradeInjectable(EmailValidationService)); 73 | 74 | export interface IObservableFactory { 75 | getInstance(): IObservableService; 76 | } 77 | --------------------------------------------------------------------------------