diff --git a/README.md b/README.md index d753a6cd..dd2aff6b 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,6 @@ this.localStorage.getItem('user').subscribe((user) => { }); ``` -As any data can be stored, you can type your data. - Not finding an item is not an error, it succeeds but returns `null`. ```typescript @@ -136,7 +134,7 @@ Starting with *version 5*, you can use a [JSON Schema](http://json-schema.org/) A [migration guide](./docs/MIGRATION_TO_V7.md) is available. ```typescript -this.localStorage.getItem('test', { schema: { type: 'string' } }) +this.localStorage.getItem('test', { schema: { type: 'string' } }) .subscribe((user) => { // Called if data is valid or null }, (error) => { diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 78ca84b7..71080079 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -1,5 +1,10 @@ # Validation guide +## Version + +This is the up to date guide about validation for version >= 8. +The old guide for validation in versions < 8 is available [here](./VALIDATION_BEFORE_V8.md). + ## Why validation? Any client-side storage (cookies, `localStorage`, `indexedDb`...) is not secure by nature, @@ -27,52 +32,52 @@ as you'll see the more complex it is, the more complex is validation too. ### Boolean ```typescript -this.localStorage.getItem('test', { schema: { type: 'boolean' } }) +this.localStorage.getItem('test', { schema: { type: 'boolean' } }) ``` ### Integer ```typescript -this.localStorage.getItem('test', { schema: { type: 'integer' } }) +this.localStorage.getItem('test', { schema: { type: 'integer' } }) ``` ### Number ```typescript -this.localStorage.getItem('test', { schema: { type: 'number' } }) +this.localStorage.getItem('test', { schema: { type: 'number' } }) ``` ### String ```typescript -this.localStorage.getItem('test', { schema: { type: 'string' } }) +this.localStorage.getItem('test', { schema: { type: 'string' } }) ``` ### Arrays ```typescript -this.localStorage.getItem('test', { schema: { +this.localStorage.getItem('test', { schema: { type: 'array', items: { type: 'boolean' } } }) ``` ```typescript -this.localStorage.getItem('test', { schema: { +this.localStorage.getItem('test', { schema: { type: 'array', items: { type: 'integer' } } }) ``` ```typescript -this.localStorage.getItem('test', { schema: { +this.localStorage.getItem('test', { schema: { type: 'array', items: { type: 'number' } } }) ``` ```typescript -this.localStorage.getItem('test', { schema: { +this.localStorage.getItem('test', { schema: { type: 'array', items: { type: 'string' } } }) @@ -107,7 +112,7 @@ this.localStorage.getItem('test', { schema }) What's expected for each property is another JSON schema. -## Why a schema *and* a cast? +### Why a schema *and* a cast? You may ask why we have to define a TypeScript cast with `getItem()` *and* a JSON schema with `{ schema }`. @@ -117,22 +122,24 @@ It's because they happen at different steps: So they each serve a different purpose: - casting allow you to retrieve the data if the good type instead of `any` -- the schema allow the lib to validate the data at runtime +- the schema allow the lib to validate the data at + +For previous basic types, as they are static, we can infer automatically. +But as objects properties are dynamic, we can't do the same for objects. Be aware **you are responsible the casted type (`User`) describes the same structure as the JSON schema**. The lib can't check that. -## How to validate fixed values - -Temporarily, documentation for constants and enums is removed, -as there will be a breaking change in v8. - ## Additional validation -Some types have additional validation options since version >= 6. +### Options for booleans + +- `const` ### Options for integers and numbers +- `const` +- `enum` - `multipleOf` - `maximum` - `exclusiveMaximum` @@ -140,7 +147,6 @@ Some types have additional validation options since version >= 6. - `exclusiveMinimum` For example: - ```typescript this.localStorage.getItem('test', { schema: { type: 'number', @@ -150,6 +156,8 @@ this.localStorage.getItem('test', { schema: { ### Options for strings +- `const` +- `enum` - `maxLength` - `minLength` - `pattern` @@ -210,7 +218,7 @@ this.localStorage.getItem('test', { schema }) If validation fails, it'll go in the error callback: ```typescript -this.localStorage.getItem('existing', { schema: { type: 'string' } }) +this.localStorage.getItem('existing', { schema: { type: 'string' } }) .subscribe((result) => { // Called if data is valid or null }, (error) => { @@ -221,7 +229,7 @@ this.localStorage.getItem('existing', { schema: { type: 'string' } }) But as usual (like when you do a database request), not finding an item is not an error. It succeeds but returns `null`. ```typescript -this.localStorage.getItem('notExisting', { schema: { type: 'string' } }) +this.localStorage.getItem('notExisting', { schema: { type: 'string' } }) .subscribe((result) => { result; // null }, (error) => { diff --git a/docs/VALIDATION_BEFORE_V8.md b/docs/VALIDATION_BEFORE_V8.md new file mode 100644 index 00000000..0569a01f --- /dev/null +++ b/docs/VALIDATION_BEFORE_V8.md @@ -0,0 +1,288 @@ +# Validation guide + +## Version + +This is old guide for validation in versions < 8. +The up to date guide about validation for version >= 8 is available [here](./VALIDATION.md). + +## Why validation? + +Any client-side storage (cookies, `localStorage`, `indexedDb`...) is not secure by nature, +as the client can forge the value (intentionally to attack your app, or unintentionally because it is affected by a virus or a XSS attack). + +It can cause obvious **security issues**, but also **errors** and thus crashes (as the received data type may not be what you expected). + +Then, **any data coming from client-side storage should be checked before used**. + +It was allowed since v5 of the lib, and is **now required since v7** (see the [migration guide](./MIGRATION_TO_V7.md)). + +## Why JSON schemas? + +[JSON Schema](https://json-schema.org/) is a standard to describe the structure of a JSON data. +You can see this as an equivalent to the DTD in XML, the Doctype in HTML or interfaces in TypeScript. + +It can have many uses (it's why you have autocompletion in some JSON files in Visual Studio Code). +**In this lib, JSON schemas are used to validate the data retrieved from local storage.** + +## How to validate simple data + +As a general recommendation, we recommend to keep your data structures as simple as possible, +as you'll see the more complex it is, the more complex is validation too. + +### Boolean + +```typescript +this.localStorage.getItem('test', { schema: { type: 'boolean' } }) +``` + +### Integer + +```typescript +this.localStorage.getItem('test', { schema: { type: 'integer' } }) +``` + +### Number + +```typescript +this.localStorage.getItem('test', { schema: { type: 'number' } }) +``` + +### String + +```typescript +this.localStorage.getItem('test', { schema: { type: 'string' } }) +``` + +### Arrays + +```typescript +this.localStorage.getItem('test', { schema: { + type: 'array', + items: { type: 'boolean' } +} }) +``` + +```typescript +this.localStorage.getItem('test', { schema: { + type: 'array', + items: { type: 'integer' } +} }) +``` + +```typescript +this.localStorage.getItem('test', { schema: { + type: 'array', + items: { type: 'number' } +} }) +``` + +```typescript +this.localStorage.getItem('test', { schema: { + type: 'array', + items: { type: 'string' } +} }) +``` + +What's expected in `items` is another JSON schema. + +## How to validate objects + +For example: +```typescript +import { JSONSchema } from '@ngx-pwa/local-storage'; + +interface User { + firstName: string; + lastName: string; + age?: number; +} + +const schema: JSONSchema = { + type: 'object', + properties: { + firstName: { type: 'string' }, + lastName: { type: 'string' }, + age: { type: 'number' }, + }, + required: ['firstName', 'lastName'] +}; + +this.localStorage.getItem('test', { schema }) +``` + +What's expected for each property is another JSON schema. + +## Why a schema *and* a cast? + +You may ask why we have to define a TypeScript cast with `getItem()` *and* a JSON schema with `{ schema }`. + +It's because they happen at different steps: +- a cast (`getItem()`) just says "TypeScript, trust me, I'm telling you it will be a `User`", but it only happens at *compilation* time (it won't be checked at runtime) +- the JSON schema (`{ schema }`) will be used at *runtime* when getting data in local storage for real. + +So they each serve a different purpose: +- casting allow you to retrieve the data if the good type instead of `any` +- the schema allow the lib to validate the data at runtime + +Be aware **you are responsible the casted type (`User`) describes the same structure as the JSON schema**. +The lib can't check that. + +## How to validate fixed values + +Temporarily, documentation for constants and enums is removed, +as there will be a breaking change in v8. + +## Additional validation + +Some types have additional validation options since version >= 6. + +### Options for integers and numbers + +- `multipleOf` +- `maximum` +- `exclusiveMaximum` +- `minimum` +- `exclusiveMinimum` + +For example: + +```typescript +this.localStorage.getItem('test', { schema: { + type: 'number', + maximum: 5 +} }) +``` + +### Options for strings + +- `maxLength` +- `minLength` +- `pattern` + +For example: +```typescript +this.localStorage.getItem('test', { schema: { + type: 'string', + maxLength: 10 +} }) +``` + +### Options for arrays + +- `maxItems` +- `minItems` +- `uniqueItems` + +For example: +```typescript +this.localStorage.getItem('test', { schema: { + type: 'array', + items: { type: 'string' }, + maxItems: 5 +} }) +``` + +## How to validate nested types + +```typescript +import { JSONSchema } from '@ngx-pwa/local-storage'; + +interface User { + firstName: string; + lastName: string; +} + +const schema: JSONSchema = { + type: 'array', + items: { + type: 'object', + properties: { + firstName: { + type: 'string', + maxLength: 10 + }, + lastName: { type: 'string' } + }, + required: ['firstName', 'lastName'] + } +}; + +this.localStorage.getItem('test', { schema }) +``` + +## Errors vs. `null` + +If validation fails, it'll go in the error callback: + +```typescript +this.localStorage.getItem('existing', { schema: { type: 'string' } }) +.subscribe((result) => { + // Called if data is valid or null +}, (error) => { + // Called if data is invalid +}); +``` + +But as usual (like when you do a database request), not finding an item is not an error. It succeeds but returns `null`. + +```typescript +this.localStorage.getItem('notExisting', { schema: { type: 'string' } }) +.subscribe((result) => { + result; // null +}, (error) => { + // Not called +}); +``` + +## Differences from the standard + +The role of the validation feature in this lib is to check the data against corruption, +so it needs to be a strict checking. Then there are important differences with the JSON schema standards. + +### Restrictions + +Types are enforced: each value MUST have a `type`. + +### Unsupported features + +The following features available in the JSON schema standard +are *not* available in this lib: +- `additionalItems` +- `additionalProperties` +- `propertyNames` +- `maxProperties` +- `minProperties` +- `patternProperties` +- `not` +- `contains` +- `allOf` +- `anyOf` +- `oneOf` +- array for `type` + +## ES6 shortcut + +In EcmaScript >= 6, this: + +```typescript +const schema: JSONSchemaBoolean = { type: 'boolean' }; + +this.localStorage.getItem('test', { schema }); +``` + +is a shortcut for this: +```typescript +const schema: JSONSchemaBoolean = { type: 'boolean' }; + +this.localStorage.getItem('test', { schema: schema }); +``` + +which works only if the property and the variable have the same name. +So if your variable has another name, you can't use the shortcut: +```typescript +const customSchema: JSONSchemaBoolean = { type: 'boolean' }; + +this.localStorage.getItem('test', { schema: customSchema }); +``` + +[Back to general documentation](../README.md) diff --git a/package-lock.json b/package-lock.json index 85ffed68..9ec29b29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12045,9 +12045,9 @@ "dev": true }, "typescript": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.1.6.tgz", - "integrity": "sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz", + "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==", "dev": true }, "uglify-es": { diff --git a/package.json b/package.json index 2a86181e..2e514095 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,6 @@ "tsickle": "^0.34.0", "tslib": "^1.7.1", "tslint": "^5.12.1", - "typescript": "^3.1.6" + "typescript": "^3.2.4" } } diff --git a/projects/ngx-pwa/local-storage/src/lib/get-item-overloads.spec.ts b/projects/ngx-pwa/local-storage/src/lib/get-item-overloads.spec.ts new file mode 100644 index 00000000..b913df43 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/get-item-overloads.spec.ts @@ -0,0 +1,317 @@ +import { LocalStorage } from './lib.service'; +import { IndexedDBDatabase } from './databases/indexeddb-database'; +import { JSONValidator } from './validation/json-validator'; +import { JSONSchemaString, JSONSchema, JSONSchemaArrayOf } from './validation/json-schema'; + +describe('getItem() overload signature', () => { + + let localStorageService: LocalStorage; + + beforeEach((done: DoneFn) => { + + localStorageService = new LocalStorage(new IndexedDBDatabase(), new JSONValidator()); + + localStorageService.clear().subscribe(() => { + + done(); + + }); + + }); + + it('should compile with no schema and without cast', (done: DoneFn) => { + + localStorageService.getItem('test').subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile with no schema and with cast', (done: DoneFn) => { + + localStorageService.getItem('test').subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile with literal basic schema and without type param', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { type: 'string' } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile with literal basic schema and with type param', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { type: 'string' } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile with literal basic schema and extra options', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { type: 'string', maxLength: 10 } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile with prepared basic schema and with specific interface', (done: DoneFn) => { + + const schema: JSONSchemaString = { type: 'string' }; + + localStorageService.getItem('test', { schema }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile with prepared basic schema and with generic interface', (done: DoneFn) => { + + const schema: JSONSchema = { type: 'string' }; + + localStorageService.getItem('test', { schema }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for string type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { type: 'string' } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for number type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { type: 'number' } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for integer type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { type: 'integer' } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for boolean type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { type: 'boolean' } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of strings', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { + type: 'array', + items: { type: 'string' } + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of numbers', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { + type: 'array', + items: { type: 'number' } + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of integers', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { + type: 'array', + items: { type: 'integer' } + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of booleans', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { + type: 'array', + items: { type: 'boolean' } + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array with extra options', (done: DoneFn) => { + + const schema: JSONSchemaArrayOf = { + type: 'array', + items: { type: 'string' }, + maxItems: 5 + }; + + localStorageService.getItem('test', { schema }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of objects', (done: DoneFn) => { + + interface Test { + test: string; + } + + localStorageService.getItem('test', { schema: { + type: 'array', + items: { + type: 'object', + properties: { + test: { type: 'string' } + } + } + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for objects without param type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { + type: 'object', + properties: { + test: { type: 'string' } + } + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for objects with param type', (done: DoneFn) => { + + interface Test { + test: string; + } + + localStorageService.getItem('test', { schema: { + type: 'object', + properties: { + test: { type: 'string' } + } + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile with schema with unsupported options coming from JSON schema standard', (done: DoneFn) => { + + // TODO: check this in TS >= 3.3 as it seems weird unknown properties are allowed + localStorageService.getItem('test', { schema: { + type: 'object', + properties: { + test: { type: 'string' } + }, + ddd: 'ddd' + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + +}); diff --git a/projects/ngx-pwa/local-storage/src/lib/lib.service.spec.ts b/projects/ngx-pwa/local-storage/src/lib/lib.service.spec.ts index 5ae078e2..be0bf393 100755 --- a/projects/ngx-pwa/local-storage/src/lib/lib.service.spec.ts +++ b/projects/ngx-pwa/local-storage/src/lib/lib.service.spec.ts @@ -895,7 +895,7 @@ describe('LocalStorage with IndexedDB', () => { } - function testGetCompatibilityWithNativeAPI(done: DoneFn, value: any, schema: JSONSchema) { + function testGetCompatibilityWithNativeAPI(done: DoneFn, value: any, schema?: JSONSchema) { const index = 'test'; @@ -948,7 +948,7 @@ describe('LocalStorage with IndexedDB', () => { } - const getTestValues: [any, JSONSchema][] = [ + const getTestValues: [any, JSONSchema | undefined][] = [ ['hello', { type: 'string' }], ['', { type: 'string' }], [0, { type: 'number' }], @@ -958,14 +958,12 @@ describe('LocalStorage with IndexedDB', () => { // TODO: delete cast when TS 3.2 issue is fixed [[1, 2, 3], { type: 'array', items: { type: 'number' } }], [{ test: 'value' }, { type: 'object', properties: { test: { type: 'string' } } }], - [null, { type: 'null' }], - [undefined, { type: 'null' }], ]; for (const [getTestValue, getTestSchema] of getTestValues) { it(`should get a value on an index previously used by another lib API - (will be pending in IE/Firefox private mode and 1 pending in Edge/IE because of null)`, (done: DoneFn) => { + (will be pending in IE/Firefox private mode)`, (done: DoneFn) => { testGetCompatibilityWithNativeAPI(done, getTestValue, getTestSchema); @@ -973,6 +971,20 @@ describe('LocalStorage with IndexedDB', () => { } + it(`should get a null value on an index previously used by another lib API + (will be pending in IE/Firefox private mode and in Edge/IE because of null)`, (done: DoneFn) => { + + testGetCompatibilityWithNativeAPI(done, null); + + }); + + it(`should get a value on an index previously used by another lib API + (will be pending in IE/Firefox private mode)`, (done: DoneFn) => { + + testGetCompatibilityWithNativeAPI(done, undefined); + + }); + }); describe('LocalStorage with localStorage and a prefix', () => { diff --git a/projects/ngx-pwa/local-storage/src/lib/lib.service.ts b/projects/ngx-pwa/local-storage/src/lib/lib.service.ts index ddaf18ec..7d63ac27 100755 --- a/projects/ngx-pwa/local-storage/src/lib/lib.service.ts +++ b/projects/ngx-pwa/local-storage/src/lib/lib.service.ts @@ -3,7 +3,10 @@ import { Observable, throwError, of } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; import { LocalDatabase } from './databases/local-database'; -import { JSONSchema } from './validation/json-schema'; +import { + JSONSchema, JSONSchemaBoolean, JSONSchemaInteger, + JSONSchemaNumber, JSONSchemaString, JSONSchemaArrayOf +} from './validation/json-schema'; import { JSONValidator } from './validation/json-validator'; export interface LSGetItemOptions { @@ -25,16 +28,30 @@ export class LocalStorage { } protected readonly getItemOptionsDefault: LSGetItemOptions = { - schema: null + schema: null, }; constructor(protected database: LocalDatabase, protected jsonValidator: JSONValidator) {} /** - * Gets an item value in local storage + * Gets an item value in local storage. + * The signature has many overloads due to validation, please refer to the documentation. + * @see https://github.com/cyrilletuzi/angular-async-local-storage/blob/master/docs/VALIDATION.md * @param key The item's key - * @returns The item's value if the key exists, null otherwise, wrapped in an RxJS Observable + * @returns The item's value if the key exists, `null` otherwise, wrapped in an RxJS `Observable` */ + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaString }): Observable; + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaInteger | JSONSchemaNumber }): Observable; + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaBoolean }): Observable; + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaArrayOf }): Observable; + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaArrayOf }): Observable; + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaArrayOf }): Observable; getItem(key: string, options: LSGetItemOptions & { schema: JSONSchema }): Observable; getItem(key: string, options?: LSGetItemOptions): Observable; getItem(key: string, options = this.getItemOptionsDefault) { @@ -71,14 +88,13 @@ export class LocalStorage { } /** - * Gets an item value in local storage WITHOUT any validation. - * It is a convenience method for development only: do NOT use it in production code, - * as it can cause security issues and errors and may be removed in future versions. - * Use the normal .getItem() method instead. - * @ignore - * @deprecated + * Gets an item value in local storage *without* any validation. + * It is a convenience method for development only: **do not use it in production code**, + * as it can cause security issues and errors. + * @ignore Use the `.getItem()` method instead. + * @deprecated May be removed in future versions. * @param key The item's key - * @returns The item's value if the key exists, null otherwise, wrapped in an RxJS Observable + * @returns The item's value if the key exists, `null` otherwise, wrapped in an RxJS `Observable` */ getUnsafeItem(key: string): Observable { @@ -89,8 +105,8 @@ export class LocalStorage { /** * Sets an item in local storage * @param key The item's key - * @param data The item's value, must NOT be null or undefined - * @returns An RxJS Observable to wait the end of the operation + * @param data The item's value, **must not be `null` or `undefined`** + * @returns An RxJS `Observable` to wait the end of the operation */ setItem(key: string, data: any): Observable { @@ -101,7 +117,7 @@ export class LocalStorage { /** * Deletes an item in local storage * @param key The item's key - * @returns An RxJS Observable to wait the end of the operation + * @returns An RxJS `Observable` to wait the end of the operation */ removeItem(key: string): Observable { @@ -111,7 +127,7 @@ export class LocalStorage { /** * Deletes all items from local storage - * @returns An RxJS Observable to wait the end of the operation + * @returns An RxJS `Observable` to wait the end of the operation */ clear(): Observable { @@ -121,7 +137,7 @@ export class LocalStorage { /** * Get all keys stored in local storage - * @returns A RxJS Observable returning an array of the indexes + * @returns A RxJS `Observable` returning an array of the indexes */ keys(): Observable { @@ -131,7 +147,7 @@ export class LocalStorage { /** * Tells if a key exists in storage - * @returns A RxJS Observable telling if the key exists + * @returns A RxJS `Observable` telling if the key exists */ has(key: string): Observable { @@ -142,7 +158,7 @@ export class LocalStorage { /** * Sets an item in local storage, and auto-subscribes * @param key The item's key - * @param data The item's value, must NOT be null or undefined + * @param data The item's value, **must not be `null` or `undefined`** */ setItemSubscribe(key: string, data: any): void { diff --git a/projects/ngx-pwa/local-storage/src/lib/validation/json-schema.ts b/projects/ngx-pwa/local-storage/src/lib/validation/json-schema.ts index dc12b2ef..7155487d 100644 --- a/projects/ngx-pwa/local-storage/src/lib/validation/json-schema.ts +++ b/projects/ngx-pwa/local-storage/src/lib/validation/json-schema.ts @@ -1,50 +1,123 @@ /** - * Avoid: will be removed in v8 in favor of another interface. + * JSON Schema to describe a boolean value. + * @ignore Internal type, **do not use**, use `JSONSchema` instead */ -export interface JSONSchemaConst { +export interface JSONSchemaBoolean { + + /** + * Type for a boolean value. + */ + type: 'boolean'; /** * Checks if a value is strictly equal to this. - * Can't be an object or array, as two objects or arrays are never equal. */ - const: string | number | boolean | null; + const?: boolean; } /** - * Avoid: will be removed in v8 in favor of another interface. + * JSON Schema to describe a number value. + * @ignore Internal type, **do not use**, use `JSONSchema` instead */ -export interface JSONSchemaEnum { +export interface JSONSchemaNumber { + + /** + * Type for a numeric value. + */ + type: 'number'; + + /** + * Checks if a value is strictly equal to this. + */ + const?: number; /** * Checks if a value is strictly equal to one of the value of enum. - * Can't be an object or array, as two objects or arrays are never equal. */ - enum: (string | number | boolean | null)[]; + enum?: number[]; -} + /** + * Check if a number is a multiple of x. + * Must be strictly greater than 0. + */ + multipleOf?: number; -export interface JSONSchemaBoolean { + /** + * Check if a number is lower or equal than this maximum. + */ + maximum?: number; /** - * Type for a boolean value. + * Check if a number is strictly lower than this maximum. */ - type: 'boolean'; + exclusiveMaximum?: number; + + /** + * Check if a number is greater or equal than this minimum. + */ + minimum?: number; + + /** + * Check if a number is strictly greater than this minimum. + */ + exclusiveMinimum?: number; } /** - * Avoid: will be removed in v8. + * JSON Schema to describe an integer value. + * @ignore Internal type, **do not use**, use `JSONSchema` instead */ -export interface JSONSchemaNull { +export interface JSONSchemaInteger { /** - * Type for a null value. + * Type for an integer value. */ - type: 'null'; + type: 'integer'; + + /** + * Checks if a value is strictly equal to this. + */ + const?: number; + + /** + * Checks if a value is strictly equal to one of the value of enum. + */ + enum?: number[]; + + /** + * Check if a number is a multiple of x. + * Must be strictly greater than 0. + */ + multipleOf?: number; + + /** + * Check if a number is lower or equal than this maximum. + */ + maximum?: number; + + /** + * Check if a number is strictly lower than this maximum. + */ + exclusiveMaximum?: number; + + /** + * Check if a number is greater or equal than this minimum. + */ + minimum?: number; + + /** + * Check if a number is strictly greater than this minimum. + */ + exclusiveMinimum?: number; } +/** + * JSON Schema to describe a string value. + * @ignore Internal type, **do not use**, use `JSONSchema` instead + */ export interface JSONSchemaString { /** @@ -52,6 +125,16 @@ export interface JSONSchemaString { */ type: 'string'; + /** + * Checks if a value is strictly equal to this. + */ + const?: string; + + /** + * Checks if a value is strictly equal to one of the value of enum. + */ + enum?: string[]; + /** * Maxium length for a string. * Must be a non-negative integer. @@ -66,60 +149,69 @@ export interface JSONSchemaString { /** * Pattern to match for a string. - * Must be a valid regular expression, WITHOUT the / delimiters. + * Must be a valid regular expression, *without* the `/` delimiters. */ pattern?: string; } -export interface JSONSchemaNumeric { - - type: 'number' | 'integer'; +/** + * JSON schema to describe an array of values. + * @ignore Internal type, **do not use**, use `JSONSchema` instead + */ +export interface JSONSchemaArray { /** - * Check if a number is a multiple of x. - * Must be strictly greater than 0. + * Type for an array of values. */ - multipleOf?: number; + type: 'array'; /** - * Check if a number is less or equal than this maximum. + * Schema for the values of an array. */ - maximum?: number; + items: JSONSchema; /** - * Check if a number is strictly less than this maximum. + * Check if an array length is lower or equal to this value. + * Must be a non negative integer. */ - exclusiveMaximum?: number; + maxItems?: number; /** - * Check if a number is greater or equal than this minimum. + * Check if an array length is greater or equal to this value. + * Must be a non negative integer. */ - minimum?: number; + minItems?: number; /** - * Check if a number is strictly greater than this minimum. + * Check if an array only have unique values. */ - exclusiveMinimum?: number; + uniqueItems?: boolean; } -export interface JSONSchemaArray { +/** + * JSON Schema to describe an array of primitive values: + * - array of booleans: `JSONSchemaArrayOf`, + * - array of numbers: `JSONSchemaArrayOf`, + * - array of integers: `JSONSchemaArrayOf`, + * - array of strings: `JSONSchemaArrayOf`. + * @ignore Internal type, **do not use**, use `JSONSchema` instead + */ +export interface JSONSchemaArrayOf { /** - * Type for an array value. - * Will be *required* in v8, so explicit this now! + * Type for an array of values. */ - type?: 'array'; + type: 'array'; /** * Schema for the values of an array. - * Avoid to use an array of schemas, this feature will be removed in v8. */ - items: JSONSchema | JSONSchema[]; + items: T; /** - * Check if an array length is less or equal to this value. + * Check if an array length is lower or equal to this value. * Must be a non negative integer. */ maxItems?: number; @@ -137,16 +229,19 @@ export interface JSONSchemaArray { } +/** + * JSON schema to describe an object. + * @ignore Internal type, **do not use**, use `JSONSchema` instead + */ export interface JSONSchemaObject { /** - * Type for an object value. - * Will be *required* in v8, so explicit this now! + * Type for an object. */ - type?: 'object'; + type: 'object'; /** - * List of properties schemas for an object. + * List of properties of the object and their associated JSON schemas. */ properties: { [k: string]: JSONSchema; @@ -154,19 +249,20 @@ export interface JSONSchemaObject { /** * Array of names of the required properties for an object. - * Properties set as required should be present in 'properties' too. - * Note that in the last spec, booleans are not supported anymore. + * Properties set as required should be present in `properties` too. */ required?: string[]; } /** - * Subset of the JSON Schema. - * Types are enforced to validate everything: each value MUST have a ONE of either `type`. - * Not all validation features are supported: just follow the interface. + * @deprecated Available for backward-compatibility only, **do not use**, use `JSONSchema` instead + */ +export type JSONSchemaNumeric = JSONSchemaNumber | JSONSchemaInteger; + +/** + * Subset of the JSON Schema standard. + * Types are enforced to validate everything: each value **must** have a `type`. + * @see https://github.com/cyrilletuzi/angular-async-local-storage/blob/master/docs/VALIDATION.md */ -export type JSONSchema = (JSONSchemaConst | JSONSchemaEnum | - JSONSchemaBoolean | JSONSchemaNull | JSONSchemaString | JSONSchemaNumeric | - JSONSchemaArray | JSONSchemaObject) - & { [k: string]: any; }; +export type JSONSchema = JSONSchemaString | JSONSchemaNumber | JSONSchemaInteger | JSONSchemaBoolean | JSONSchemaArray | JSONSchemaObject; diff --git a/projects/ngx-pwa/local-storage/src/lib/validation/json-validation.spec.ts b/projects/ngx-pwa/local-storage/src/lib/validation/json-validation.spec.ts index 8778a9b4..8c93a0ed 100644 --- a/projects/ngx-pwa/local-storage/src/lib/validation/json-validation.spec.ts +++ b/projects/ngx-pwa/local-storage/src/lib/validation/json-validation.spec.ts @@ -16,7 +16,11 @@ describe(`JSONValidator`, () => { expect(() => { - jsonValidator.validate({ test: 'test' }, { properties: { test: { type: 'string' } }, additionalProperties: true }); + jsonValidator.validate({ test: 'test' }, { + type: 'object', + properties: { test: { type: 'string' } }, + additionalProperties: true + } as any); }).not.toThrow(); @@ -28,7 +32,7 @@ describe(`JSONValidator`, () => { it(`should return true on a string equal to a string const`, () => { - const test = jsonValidator.validate('test', { const: 'test' }); + const test = jsonValidator.validate('test', { type: 'string', const: 'test' }); expect(test).toBe(true); @@ -36,7 +40,7 @@ describe(`JSONValidator`, () => { it(`should return false on a string not equal to a string const`, () => { - const test = jsonValidator.validate('test2', { const: 'test' }); + const test = jsonValidator.validate('test2', { type: 'string', const: 'test' }); expect(test).toBe(false); @@ -44,7 +48,7 @@ describe(`JSONValidator`, () => { it(`should return true on a number equal to a number const`, () => { - const test = jsonValidator.validate(1.5, { const: 1.5 }); + const test = jsonValidator.validate(1.5, { type: 'number', const: 1.5 }); expect(test).toBe(true); @@ -52,7 +56,7 @@ describe(`JSONValidator`, () => { it(`should return false on a number not equal to a number const`, () => { - const test = jsonValidator.validate(2.5, { const: 1.5 }); + const test = jsonValidator.validate(2.5, { type: 'number', const: 1.5 }); expect(test).toBe(false); @@ -60,7 +64,7 @@ describe(`JSONValidator`, () => { it(`should return true on an integer equal to an integer const`, () => { - const test = jsonValidator.validate(1, { const: 1 }); + const test = jsonValidator.validate(1, { type: 'integer', const: 1 }); expect(test).toBe(true); @@ -68,7 +72,7 @@ describe(`JSONValidator`, () => { it(`should return false on an integer not equal to an integer const`, () => { - const test = jsonValidator.validate(2, { const: 1 }); + const test = jsonValidator.validate(2, { type: 'integer', const: 1 }); expect(test).toBe(false); @@ -76,7 +80,7 @@ describe(`JSONValidator`, () => { it(`should return true on a boolean equal to a boolean const`, () => { - const test = jsonValidator.validate(true, { const: true }); + const test = jsonValidator.validate(true, { type: 'boolean', const: true }); expect(test).toBe(true); @@ -84,23 +88,7 @@ describe(`JSONValidator`, () => { it(`should return false on a boolean not equal to a boolean const`, () => { - const test = jsonValidator.validate(false, { const: true }); - - expect(test).toBe(false); - - }); - - it(`should return true on null equal to a null const`, () => { - - const test = jsonValidator.validate(null, { const: null }); - - expect(test).toBe(true); - - }); - - it(`should return false on a value not equal to a null const`, () => { - - const test = jsonValidator.validate('test', { const: null }); + const test = jsonValidator.validate(false, { type: 'boolean', const: true }); expect(test).toBe(false); @@ -108,7 +96,7 @@ describe(`JSONValidator`, () => { it(`should return false on an empty string with a false const`, () => { - const test = jsonValidator.validate('', { const: false }); + const test = jsonValidator.validate('', { type: 'boolean', const: false }); expect(test).toBe(false); @@ -116,23 +104,7 @@ describe(`JSONValidator`, () => { it(`should return false on 0 with a false const`, () => { - const test = jsonValidator.validate(0, { const: false }); - - expect(test).toBe(false); - - }); - - it(`should return false on an empty string with a null const`, () => { - - const test = jsonValidator.validate('', { const: null }); - - expect(test).toBe(false); - - }); - - it(`should return false on 0 with a null const`, () => { - - const test = jsonValidator.validate(0, { const: null }); + const test = jsonValidator.validate(0, { type: 'boolean', const: false }); expect(test).toBe(false); @@ -144,7 +116,7 @@ describe(`JSONValidator`, () => { it(`should return true on a value included in an enum`, () => { - const test = jsonValidator.validate('test', { enum: ['test', 'hello'] }); + const test = jsonValidator.validate('test', { type: 'string', enum: ['test', 'hello'] }); expect(test).toBe(true); @@ -152,7 +124,7 @@ describe(`JSONValidator`, () => { it(`should return false on a value not included in an enum`, () => { - const test = jsonValidator.validate('test2', { enum: ['test', 'hello'] }); + const test = jsonValidator.validate('test2', { type: 'string', enum: ['test', 'hello'] }); expect(test).toBe(false); @@ -160,7 +132,7 @@ describe(`JSONValidator`, () => { it(`should return true on an empty string included in an enum`, () => { - const test = jsonValidator.validate('', { enum: ['', 'hello'] }); + const test = jsonValidator.validate('', { type: 'string', enum: ['', 'hello'] }); expect(test).toBe(true); @@ -168,7 +140,7 @@ describe(`JSONValidator`, () => { it(`should return true on 0 included in an enum`, () => { - const test = jsonValidator.validate(0, { enum: [0, 1] }); + const test = jsonValidator.validate(0, { type: 'number', enum: [0, 1] }); expect(test).toBe(true); @@ -194,14 +166,6 @@ describe(`JSONValidator`, () => { }); - it(`should return true on a null value with a null type`, () => { - - const test = jsonValidator.validate(null, { type: 'null' }); - - expect(test).toBe(true); - - }); - it(`should return false on a primitive value with a mismatched type`, () => { const test = jsonValidator.validate('test', { type: 'number' }); @@ -593,7 +557,11 @@ describe(`JSONValidator`, () => { it(`should throw if required properties are not described in 'properties'`, () => { expect(() => { - jsonValidator.validate({ test: 'test' }, { type: 'object', properties: { other: { type: 'string' } }, required: ['test'] }); + jsonValidator.validate({ test: 'test' }, { + type: 'object', + properties: { other: { type: 'string' } }, + required: ['test'] + }); }).toThrowError(); }); @@ -735,6 +703,7 @@ describe(`JSONValidator`, () => { type: 'string' }, test2: { + type: 'object', properties: { test3: { type: 'number' @@ -758,6 +727,7 @@ describe(`JSONValidator`, () => { type: 'string' }, test2: { + type: 'object', properties: { test3: { type: 'number' @@ -776,7 +746,7 @@ describe(`JSONValidator`, () => { const test = jsonValidator.validate({ test: ['', ''] }, { type: 'object', - properties: { test: { items: { type: 'string' } } } + properties: { test: { type: 'array', items: { type: 'string' } } } }); expect(test).toBe(true); @@ -813,7 +783,10 @@ describe(`JSONValidator`, () => { it(`should return true with valid nested arrays`, () => { - const test = jsonValidator.validate([[''], ['']], { type: 'array', items: { items: { type: 'string' } } }); + const test = jsonValidator.validate([[''], ['']], { + type: 'array', + items: { type: 'array', items: { type: 'string' } } + }); expect(test).toBe(true); @@ -821,7 +794,10 @@ describe(`JSONValidator`, () => { it(`should return false with invalid nested arrays`, () => { - const test = jsonValidator.validate([[''], ['']], { type: 'array', items: { items: { type: 'number' } } }); + const test = jsonValidator.validate([[''], ['']], { + type: 'array', + items: { type: 'array', items: { type: 'number' } } + }); expect(test).toBe(false); @@ -831,7 +807,7 @@ describe(`JSONValidator`, () => { const test = jsonValidator.validate([{ test: 'test' }, [{ test: 'test' }]], { type: 'array', - items: { properties: { test: { type: 'string' } } } + items: { type: 'object', properties: { test: { type: 'string' } } } }); expect(test).toBe(true); @@ -936,32 +912,4 @@ describe(`JSONValidator`, () => { }); - describe(`validateItemsList`, () => { - - it(`should return false if array length is not equel to schemas length`, () => { - - const test = jsonValidator.validate(['', 10], { type: 'array', items: [{ type: 'string' }] }); - - expect(test).toBe(false); - - }); - - it(`should return true if array values match schemas`, () => { - - const test = jsonValidator.validate(['', 10], { type: 'array', items: [{ type: 'string' }, { type: 'number' }] }); - - expect(test).toBe(true); - - }); - - it(`should return false if array values mismatch schemas`, () => { - - const test = jsonValidator.validate(['', 10], { type: 'array', items: [{ type: 'string' }, { type: 'boolean' }] }); - - expect(test).toBe(false); - - }); - - }); - }); diff --git a/projects/ngx-pwa/local-storage/src/lib/validation/json-validator.ts b/projects/ngx-pwa/local-storage/src/lib/validation/json-validator.ts index f4e6ed50..d60e2f24 100644 --- a/projects/ngx-pwa/local-storage/src/lib/validation/json-validator.ts +++ b/projects/ngx-pwa/local-storage/src/lib/validation/json-validator.ts @@ -1,5 +1,8 @@ import { Injectable } from '@angular/core'; -import { JSONSchema } from './json-schema'; +import { + JSONSchema, JSONSchemaString, JSONSchemaInteger, JSONSchemaNumber, JSONSchemaBoolean, + JSONSchemaArray, JSONSchemaObject +} from './json-schema'; /** * @todo Add other JSON Schema validation features @@ -21,60 +24,28 @@ export class JSONValidator { */ validate(data: any, schema: JSONSchema): boolean { - /** @todo When TS 2.8, explore if this is possible with conditional types */ - if (((!(schema.hasOwnProperty('const') && schema.const !== undefined) - && !(schema.hasOwnProperty('enum') && schema.enum != null) && !(schema.hasOwnProperty('type') && schema.type != null)) - || schema.type === 'array' || schema.type === 'object') - && !(schema.hasOwnProperty('properties') && schema.properties != null) && !(schema.hasOwnProperty('items') && schema.items != null)) { - - throw new Error(`Each value must have a 'type' or 'properties' or 'items' or 'const' or 'enum', to enforce strict types.`); - - } - - if (schema.hasOwnProperty('const') && schema.const !== undefined && (data !== schema.const)) { - return false; - } - - if (!this.validateEnum(data, schema)) { - return false; - } - - if (!this.validateType(data, schema)) { - return false; - } - - if (!this.validateItems(data, schema)) { - return false; - } + switch (schema.type) { - if (!this.validateProperties(data, schema)) { - return false; - } + case 'string': + return this.validateString(data, schema); + case 'number': + case 'integer': + return this.validateNumber(data, schema); + case 'boolean': + return this.validateBoolean(data, schema); + case 'array': + return this.validateArray(data, schema); + case 'object': + return this.validateObject(data, schema); - if (!this.validateRequired(data, schema)) { - return false; } - return true; - - } - - protected isObjectNotNull(value: any): boolean { - - return (value !== null) && (typeof value === 'object'); - } - protected validateProperties(data: { [k: string]: any; }, schema: JSONSchema): boolean { - - if (!schema.hasOwnProperty('properties') || (schema.properties == null)) { - return true; - } - - if (!this.isObjectNotNull(data)) { + protected validateObject(data: { [k: string]: any; }, schema: JSONSchemaObject): boolean { + if ((data === null) || (typeof data !== 'object')) { return false; - } /** @@ -82,9 +53,11 @@ export class JSONValidator { * Equivalent of additionalProperties: false */ if (Object.keys(schema.properties).length < Object.keys(data).length) { - return false; + } + if (!this.validateRequired(data, schema)) { + return false; } /* Recursively validate all properties */ @@ -106,32 +79,22 @@ export class JSONValidator { } - protected validateRequired(data: {}, schema: JSONSchema): boolean { + protected validateRequired(data: {}, schema: JSONSchemaObject): boolean { - if (!schema.hasOwnProperty('required') || (schema.required == null)) { + if (!schema.required) { return true; } - if (!this.isObjectNotNull(data)) { - - return false; - - } - for (const requiredProp of schema.required) { /* Checks if the property is present in the schema 'properties' */ - if (!schema.properties || !schema.properties.hasOwnProperty(requiredProp)) { - + if (!schema.properties.hasOwnProperty(requiredProp)) { throw new Error(`'required' properties must be described in 'properties' too.`); - } /* Checks if the property is present in the data */ if (!data.hasOwnProperty(requiredProp)) { - return false; - } } @@ -140,63 +103,37 @@ export class JSONValidator { } - protected validateEnum(data: any, schema: JSONSchema): boolean { + protected validateConst(data: any, schema: JSONSchemaBoolean | JSONSchemaInteger | JSONSchemaNumber | JSONSchemaString): boolean { - if (!schema.hasOwnProperty('enum') || (schema.enum == null)) { + if (!schema.const) { return true; } - /** @todo Move to ES2016 .includes() ? */ - return (schema.enum.indexOf(data) !== -1); + return (data === schema.const); } - protected validateType(data: any, schema: JSONSchema): boolean { + protected validateEnum(data: any, schema: JSONSchemaInteger | JSONSchemaNumber | JSONSchemaString): boolean { - if (!schema.hasOwnProperty('type') || (schema.type == null)) { + if (!schema.enum) { return true; } - switch (schema.type) { - - case 'null': - return data === null; - case 'string': - return this.validateString(data, schema); - case 'number': - case 'integer': - return this.validateNumber(data, schema); - case 'boolean': - return typeof data === 'boolean'; - case 'object': - return typeof data === 'object'; - case 'array': - return Array.isArray(data); - - } - - return true; + /** @todo Move to ES2016 .includes() ? */ + return ((schema.enum as any[]).indexOf(data) !== -1); } - protected validateItems(data: any[], schema: JSONSchema): boolean { - - if (!schema.hasOwnProperty('items') || (schema.items == null)) { - return true; - } + protected validateArray(data: any[], schema: JSONSchemaArray): boolean { if (!Array.isArray(data)) { - return false; - } - if (schema.hasOwnProperty('maxItems') && (schema.maxItems != null)) { + if (schema.hasOwnProperty('maxItems') && (schema.maxItems !== undefined)) { if (!Number.isInteger(schema.maxItems) || schema.maxItems < 0) { - throw new Error(`'maxItems' must be a non-negative integer.`); - } if (data.length > schema.maxItems) { @@ -205,12 +142,10 @@ export class JSONValidator { } - if (schema.hasOwnProperty('minItems') && (schema.minItems != null)) { + if (schema.hasOwnProperty('minItems') && (schema.minItems !== undefined)) { if (!Number.isInteger(schema.minItems) || schema.minItems < 0) { - throw new Error(`'minItems' must be a non-negative integer.`); - } if (data.length < schema.minItems) { @@ -219,7 +154,7 @@ export class JSONValidator { } - if (schema.hasOwnProperty('uniqueItems') && (schema.uniqueItems != null)) { + if (schema.hasOwnProperty('uniqueItems') && (schema.uniqueItems !== undefined)) { if (schema.uniqueItems) { @@ -233,12 +168,6 @@ export class JSONValidator { } - if (Array.isArray(schema.items)) { - - return this.validateItemsList(data, schema); - - } - for (const value of data) { if (!this.validate(value, schema.items)) { @@ -251,40 +180,24 @@ export class JSONValidator { } - protected validateItemsList(data: any, schema: JSONSchema): boolean { - - const items = schema.items as JSONSchema[]; - - if (data.length !== items.length) { + protected validateString(data: any, schema: JSONSchemaString): boolean { + if (typeof data !== 'string') { return false; - } - for (let i = 0; i < items.length; i += 1) { - - if (!this.validate(data[i], items[i])) { - return false; - } - + if (!this.validateConst(data, schema)) { + return false; } - return true; - - } - - protected validateString(data: any, schema: JSONSchema): boolean { - - if (typeof data !== 'string') { + if (!this.validateEnum(data, schema)) { return false; } - if (schema.hasOwnProperty('maxLength') && (schema.maxLength != null)) { + if (schema.hasOwnProperty('maxLength') && (schema.maxLength !== undefined)) { if (!Number.isInteger(schema.maxLength) || schema.maxLength < 0) { - throw new Error(`'maxLength' must be a non-negative integer.`); - } if (data.length > schema.maxLength) { @@ -293,12 +206,10 @@ export class JSONValidator { } - if (schema.hasOwnProperty('minLength') && (schema.minLength != null)) { + if (schema.hasOwnProperty('minLength') && (schema.minLength !== undefined)) { if (!Number.isInteger(schema.minLength) || schema.minLength < 0) { - throw new Error(`'minLength' must be a non-negative integer.`); - } if (data.length < schema.minLength) { @@ -307,7 +218,7 @@ export class JSONValidator { } - if (schema.hasOwnProperty('pattern') && (schema.pattern != null)) { + if (schema.pattern) { const regularExpression = new RegExp(schema.pattern); @@ -321,7 +232,21 @@ export class JSONValidator { } - protected validateNumber(data: any, schema: JSONSchema): boolean { + protected validateBoolean(data: any, schema: JSONSchemaBoolean): boolean { + + if (typeof data !== 'boolean') { + return false; + } + + if (!this.validateConst(data, schema)) { + return false; + } + + return true; + + } + + protected validateNumber(data: any, schema: JSONSchemaNumber | JSONSchemaInteger): boolean { if (typeof data !== 'number') { return false; @@ -331,12 +256,18 @@ export class JSONValidator { return false; } - if (schema.hasOwnProperty('multipleOf') && (schema.multipleOf != null)) { + if (!this.validateConst(data, schema)) { + return false; + } - if (schema.multipleOf <= 0) { + if (!this.validateEnum(data, schema)) { + return false; + } - throw new Error(`'multipleOf' must be a number strictly greater than 0.`); + if (schema.hasOwnProperty('multipleOf') && (schema.multipleOf !== undefined)) { + if (schema.multipleOf <= 0) { + throw new Error(`'multipleOf' must be a number strictly greater than 0.`); } if (!Number.isInteger(data / schema.multipleOf)) { @@ -345,36 +276,22 @@ export class JSONValidator { } - if (schema.hasOwnProperty('maximum') && (schema.maximum != null)) { - - if (data > schema.maximum) { + if (schema.hasOwnProperty('maximum') && (schema.maximum !== undefined) && (data > schema.maximum)) { return false; - } - } - if (schema.hasOwnProperty('exclusiveMaximum') && (schema.exclusiveMaximum != null)) { - - if (data >= schema.exclusiveMaximum) { - return false; - } + if (schema.hasOwnProperty('exclusiveMaximum') && (schema.exclusiveMaximum !== undefined) && (data >= schema.exclusiveMaximum)) { + return false; } - if (schema.hasOwnProperty('minimum') && (schema.minimum != null)) { - - if (data < schema.minimum) { - return false; - } + if (schema.hasOwnProperty('minimum') && (schema.minimum !== undefined) && (data < schema.minimum)) { + return false; } - if (schema.hasOwnProperty('exclusiveMinimum') && (schema.exclusiveMinimum != null)) { - - if (data <= schema.exclusiveMinimum) { + if (schema.hasOwnProperty('exclusiveMinimum') && (schema.exclusiveMinimum !== undefined) && (data <= schema.exclusiveMinimum)) { return false; - } - } return true; diff --git a/projects/ngx-pwa/local-storage/src/public_api.ts b/projects/ngx-pwa/local-storage/src/public_api.ts index a8ee1be8..a00ff6c3 100644 --- a/projects/ngx-pwa/local-storage/src/public_api.ts +++ b/projects/ngx-pwa/local-storage/src/public_api.ts @@ -3,8 +3,8 @@ */ export { - JSONSchema, JSONSchemaConst, JSONSchemaEnum, JSONSchemaBoolean, - JSONSchemaNumeric, JSONSchemaString, JSONSchemaArray, JSONSchemaObject + JSONSchema, JSONSchemaBoolean, JSONSchemaInteger, JSONSchemaNumber, + JSONSchemaNumeric, JSONSchemaString, JSONSchemaArray, JSONSchemaArrayOf, JSONSchemaObject } from './lib/validation/json-schema'; export { LocalDatabase } from './lib/databases/local-database'; export { IndexedDBDatabase } from './lib/databases/indexeddb-database';