diff --git a/CHANGELOG.md b/CHANGELOG.md index bf922e66..3f9eef94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,59 @@ This lib is fully documented and so you'll find detailed [migration guides](./MIGRATION.md). +## 7.5.0 (2019-01-18) + +### Features + +- Added predefined constants for basic JSON schemas to simplify validation: + - `SCHEMA_BOOLEAN` (shortcut for: `{ type: 'boolean' }`) + - `SCHEMA_INTEGER` (shortcut for: `{ type: 'integer' }`) + - `SCHEMA_NUMBER` (shortcut for: `{ type: 'number' }`) + - `SCHEMA_STRING` (shortcut for: `{ type: 'string' }`) + - `SCHEMA_ARRAY_OF_BOOLEANS` (shortcut for: `{ items: { type: 'boolean' } }`) + - `SCHEMA_ARRAY_OF_INTEGER` (shortcut for: `{ items: { type: 'integer' } }`) + - `SCHEMA_ARRAY_OF_NUMBER` (shortcut for: `{ items: { type: 'number' } }`) + - `SCHEMA_ARRAY_OF_STRINGS` (shortcut for: `{ items: { type: 'string' } }`) + +This is especially useful for advanced types (arrays, objects, nested types), +to avoid TypeScript limitations. + +- For the same types as above, the result type is now automatically infered, +meaning you don't need to cast anymore! + +Before v7.5: +```typescript +this.localStorage.getItem('test', { schema: { type: 'string' } }) +.subscribe((result) => { + result; // type: any :( +}); + +this.localStorage.getItem('test', { schema: { type: 'string' } }) +.subscribe((result) => { + result; // type: string :) +}); +``` + +From v7.5: +```typescript +import { SCHEMA_STRING } from '@ngx-pwa/local-storage'; + +this.localStorage.getItem('test', { schema: SCHEMA_STRING }) +.subscribe((result) => { + result; // type: string :D +}); +``` + +- Added better interfaces for some JSON schemas: + - `JSONSchemaArrayOf` + - `JSONSchemaConstOf` + - `JSONSchemaEnumOf` + +**See the [new validation guide](./docs/VALIDATION.md).** + +This is only a feature release, so don't worry: nothing changes in existing code. +It's just new tools to simplify validation and thus ease migration to v7. + ## 7.4.0 (2019-01-12) ### Feature diff --git a/README.md b/README.md index 80679237..d82f60bb 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,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 @@ -133,7 +131,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/MIGRATION_TO_V7.md b/docs/MIGRATION_TO_V7.md index 43de5e49..a9a3bbd1 100644 --- a/docs/MIGRATION_TO_V7.md +++ b/docs/MIGRATION_TO_V7.md @@ -69,13 +69,10 @@ this.localStorage.getItem('test').subscribe((result) => { }); ``` -It's only a convenience allowed by the library. It's useful when you're already validating the data with a JSON schema, -to cast the data type. - -But **when you were not validating the data, it was really bad**. +`getItem` **doesn't perform any real validation**. Casting like this only means: "TypeScript, trust me, I'm telling you it will be a string". But TypeScript won't do any real validation as it can't: -**TypeScript can only manage checks at compilation time, while client-side storage checks can only happen at runtime**. +**TypeScript can only manage checks at *compilation* time, while client-side storage checks can only happen at *runtime***. So you ended up with a `string` type while the real data may not be a `string` at all, leading to security issues and errors. @@ -89,7 +86,9 @@ The simpler and better way to validate your data is to search `getItem` in your and **use the JSON schema option proposed by the lib**. For example: ```typescript -this.localStorage.getItem('test', { schema: { type: 'string' } }) +import { SCHEMA_STRING } from '@ngx-pwa/local-storage'; + +this.localStorage.getItem('test', { schema: SCHEMA_STRING }) .subscribe((result) => { result; // type: string result.substr(0, 2); // OK @@ -98,6 +97,10 @@ this.localStorage.getItem('test', { schema: { type: 'string' } }) **A [full validation guide](./VALIDATION.md) is available with all the options.** +Note the above example only works with version >= 7.5, which has done a lot of things to simplify validation: +- predefined constants (like `SCHEMA_STRING`) are available for common structures, +- casting (`.getItem`) is no longer required for most types, as it's now automatically infered. + ### Solution 2: custom validation (painful) You can use all the native JavaScript operators and functions to validate. For example: diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 8054be69..2719f402 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -1,5 +1,11 @@ # Validation guide +## Guide for version >= 7.5 + +Version 7.5+ of this lib greatly simplified validation (we recommend to update). + +This is the up to date guide. For validation in older versions, see [the old validation guide](./VALIDATION_BEFORE_V7.5.md). + ## Why validation? Any client-side storage (cookies, `localStorage`, `indexedDb`...) is not secure by nature, @@ -9,8 +15,6 @@ It can cause obvious **security issues**, but also **errors** and thus crashes ( 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. @@ -20,91 +24,82 @@ It can have many uses (it's why you have autocompletion in some JSON files in Vi **In this lib, JSON schemas are used to validate the data retrieved from local storage.** The JSON schema standard has its own [documentation](https://json-schema.org/), -and you can also follow the `JSONSchema` interfaces exported by the lib. But as a convenience, we'll show here how to validate the common scenarios. +and you can also follow the `JSONSchema` interfaces exported by the lib. +But we recommend to following examples. -## TypeScript 3.2 issue +## How to validate simple data -If you're using TypeScript 3.2, be sure to follow the following examples, -ie. be sure to *externalize* your schema in a variable or constant -*and* to use the *specific* interfaces, due to a regression issue in TypeScript 3.2 -(see [#64](https://github.com/cyrilletuzi/angular-async-local-storage/issues/64) for more info). +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. -## How to validate? - -### Validating a boolean +### Shortcut for a boolean ```typescript -import { JSONSchemaBoolean } from '@ngx-pwa/local-storage'; - -const schema: JSONSchemaBoolean = { type: 'boolean' }; +import { SCHEMA_BOOLEAN } from '@ngx-pwa/local-storage'; -this.localStorage.getItem('test', { schema }) +this.localStorage.getItem('test', { schema: SCHEMA_BOOLEAN }) ``` -### Validating a number +### Shortcut for an integer -For a number: ```typescript -import { JSONSchemaNumeric } from '@ngx-pwa/local-storage'; - -const schema: JSONSchemaNumeric = { type: 'number' }; +import { SCHEMA_INTEGER } from '@ngx-pwa/local-storage'; -this.localStorage.getItem('test', { schema }) +this.localStorage.getItem('test', { schema: SCHEMA_INTEGER }) ``` -For an integer only: -```typescript -import { JSONSchemaNumeric } from '@ngx-pwa/local-storage'; +### Shortcut for a number -const schema: JSONSchemaNumeric = { type: 'integer' }; +```typescript +import { SCHEMA_NUMBER } from '@ngx-pwa/local-storage'; -this.localStorage.getItem('test', { schema }) +this.localStorage.getItem('test', { schema: SCHEMA_NUMBER }) ``` -For numbers and integers, in version >= 6, you can also add the following optional validations: -- `multipleOf` -- `maximum` -- `exclusiveMaximum` -- `minimum` -- `exclusiveMinimum` - -### Validating a string +### Shortcut for a string ```typescript -import { JSONSchemaString } from '@ngx-pwa/local-storage'; - -const schema: JSONSchemaString = { type: 'string' }; +import { SCHEMA_STRING } from '@ngx-pwa/local-storage'; -this.localStorage.getItem('test', { schema }) +this.localStorage.getItem('test', { schema: SCHEMA_STRING }) ``` -For strings, in version >= 6, you can also add the following optional validations: -- `maxLength` -- `minLength` -- `pattern` +### Shortcuts for arrays -### Validating an array +```typescript +import { SCHEMA_ARRAY_OF_BOOLEANS } from '@ngx-pwa/local-storage'; + +this.localStorage.getItem('test', { schema: SCHEMA_ARRAY_OF_BOOLEANS }) +``` ```typescript -import { JSONSchemaArray, JSONSchemaString } from '@ngx-pwa/local-storage'; +import { SCHEMA_ARRAY_OF_INTEGERS } from '@ngx-pwa/local-storage'; -const schema: JSONSchemaArray = { items: { type: 'string' } as JSONSchemaString }; +this.localStorage.getItem('test', { schema: SCHEMA_ARRAY_OF_INTEGERS }) +``` + +```typescript +import { SCHEMA_ARRAY_OF_NUMBERS } from '@ngx-pwa/local-storage'; -this.localStorage.getItem('test', { schema }) +this.localStorage.getItem('test', { schema: SCHEMA_ARRAY_OF_NUMBERS }) ``` -What's expected in `items` is another JSON schema, -so you can also add the other optional validations related to the chosen type. +```typescript +import { SCHEMA_ARRAY_OF_STRINGS } from '@ngx-pwa/local-storage'; -For arrays, in version >= 6, you can also add the following optional validations: -- `maxItems` -- `minItems` -- `uniqueItems` +this.localStorage.getItem('test', { schema: SCHEMA_ARRAY_OF_STRINGS }) +``` + +## How to validate objects -### Validating an object +As the properties of an object are dynamic (it can be anything), +validating an object requires more work: +- a full JSON schema must be used, +- a cast must be added (otherwise the result will be of type `any`). +For example: ```typescript -import { JSONSchemaObject, JSONSchemaString, JSONSchemaNumeric } from '@ngx-pwa/local-storage'; +import { JSONSchemaObject, SCHEMA_NUMBER, SCHEMA_STRING } from '@ngx-pwa/local-storage'; interface User { firstName: string; @@ -113,10 +108,11 @@ interface User { } const schema: JSONSchemaObject = { + type: 'object', properties: { - firstName: { type: 'string' } as JSONSchemaString, - lastName: { type: 'string' } as JSONSchemaString, - age: { type: 'number' } as JSONSchemaNumeric + firstName: SCHEMA_STRING, + lastName: SCHEMA_STRING, + age: SCHEMA_NUMBER }, required: ['firstName', 'lastName'] }; @@ -124,27 +120,142 @@ const schema: JSONSchemaObject = { this.localStorage.getItem('test', { schema }) ``` -What's expected for each property is another JSON schema, -so you can also add the other optional validations related to the chosen type. +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. + +For previous structures, which are static, we can infer the final result type based on the JSON schema. +But as in an object properties are dynamic (we can't now in advance what properties names and types there will be), +automatic inference is not possible here. + +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 + +### Const + +```typescript +import { JSONSchemaConstOf } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaConstOf = { const: 'some value' }; + +this.localStorage.getItem('test', { schema }) +``` + +Parameter type for `JSONSchemaConstOf` can be `string` or `number`. + +### Enum + +```typescript +import { JSONSchemaEnumOf } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaEnumOf = { enum: ['value 1', 'value 2'] }; + +this.localStorage.getItem('test', { schema }) +``` + +Parameter type for `JSONSchemaEnumOf` can be `string` or `number` or `boolean`. + +## Additional validation + +Some types have additional validation options. A full JSON schema must be used for those. + +### Options for integers and numbers + +- `multipleOf` +- `maximum` +- `exclusiveMaximum` +- `minimum` +- `exclusiveMinimum` + +For example: + +```typescript +import { JSONSchemaNumeric } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaNumeric = { + type: 'number', + maximum: 5 +}; + +this.localStorage.getItem('test', { schema }) +``` + +### Options for strings + +- `maxLength` +- `minLength` +- `pattern` + +For example: +```typescript +import { JSONSchemaString } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaString = { + type: 'string', + maxLength: 10 +}; + +this.localStorage.getItem('test', { schema }) +``` + +### Options for arrays -### Validating fixed values +- `maxItems` +- `minItems` +- `uniqueItems` -Since version >= 6, if it can only be a fixed value: +For example: ```typescript -import { JSONSchemaConst } from '@ngx-pwa/local-storage'; +import { JSONSchemaArrayOf, JSONSchemaString, SCHEMA_STRING } from '@ngx-pwa/local-storage'; -const schema: JSONSchemaConst = { const: 'some value' }; +const schema: JSONSchemaArrayOf = { + type: 'array', + items: SCHEMA_STRING, + maxItems: 5 +}; -this.localStorage.getItem('test', { schema }) +this.localStorage.getItem('test', { schema }) ``` -Since version >= 6, if it can only be a fixed value among a list: +What's expected in `items` is another JSON schema. + +## How to validate nested types + +Due to a limitation of TypeScript, when nesting array, objects or advanced types, +you'll need to cast subschemas: + ```typescript -import { JSONSchemaEnum } from '@ngx-pwa/local-storage'; +import { JSONSchemaArray, JSONSchemaObject, JSONSchemaString, SCHEMA_STRING } from '@ngx-pwa/local-storage'; -const schema: JSONSchemaEnum = { enum: ['value 1', 'value 2'] }; +interface User { + firstName: string; + lastName: string; +} -this.localStorage.getItem('test', { schema }) +const schema: JSONSchemaArray = { + type: 'array', + items: { + type: 'object', + properties: { + firstName: { + type: 'string', + maxLength: 10 + } as JSONSchemaString, + lastName: SCHEMA_STRING + }, + required: ['firstName', 'lastName'] + } as JSONSchemaObject +}; + +this.localStorage.getItem('test', { schema }) ``` ## Errors vs. `null` @@ -152,7 +263,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: SCHEMA_STRING }) .subscribe((result) => { // Called if data is valid or null }, (error) => { @@ -163,7 +274,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: SCHEMA_STRING }) .subscribe((result) => { result; // null }, (error) => { @@ -197,31 +308,17 @@ are *not* available in this lib: - `oneOf` - array for `type` -## 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: { type: 'string' } }`. - -It's because a cast only means: "TypeScript, trust me, I'm telling you it will be a string". -But TypeScript won't do any real validation as it can't: -**TypeScript can only manage checks at compilation time, while client-side storage checks can only happen at runtime**. -So the JSON schema is required for real validation. - -And in the opposite way, the JSON schema doesn't tell TypeScript what will be the type of the data. -So the cast is a convenience so you don't end up with `any` while you already checked the data. - -It means **you are responsible the casted type describes the same structure as the JSON schema**. -The lib can't check that. +## Other notes -## Why specific JSONSchema interfaces? +### Why specific JSONSchema interfaces? Unfortunately, the JSON schema standard is structured in such a way it's currently impossible to do an equivalent TypeScript interface, which would be generic for all types, but only allowing you the optional validations relative to the type you choose (for example, `maxLength` should only be allowed when `type` is set to `'string'`). Thus casting with a more specific interface (like `JSONSchemaString`) allows TypeScript to check your JSON Schema is really valid. -But it's not mandatory. -## ES6 shortcut +### ES6 shortcut In EcmaScript >= 6, this: diff --git a/docs/VALIDATION_BEFORE_V7.5.md b/docs/VALIDATION_BEFORE_V7.5.md new file mode 100644 index 00000000..db00b524 --- /dev/null +++ b/docs/VALIDATION_BEFORE_V7.5.md @@ -0,0 +1,254 @@ +# Validation guide + +## Old guide + +This is the validation guide for versions < 7.5. +[The up to date guide 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.** + +The JSON schema standard has its own [documentation](https://json-schema.org/), +and you can also follow the `JSONSchema` interfaces exported by the lib. But as a convenience, we'll show here how to validate the common scenarios. + +## TypeScript 3.2 issue + +If you're using TypeScript 3.2, be sure to follow the following examples, +ie. be sure to *externalize* your schema in a variable or constant +*and* to use the *specific* interfaces, due to a regression issue in TypeScript 3.2 +(see [#64](https://github.com/cyrilletuzi/angular-async-local-storage/issues/64) for more info). + +## How to validate? + +### Validating a boolean + +```typescript +import { JSONSchemaBoolean } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaBoolean = { type: 'boolean' }; + +this.localStorage.getItem('test', { schema }) +``` + +### Validating a number + +For a number: +```typescript +import { JSONSchemaNumeric } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaNumeric = { type: 'number' }; + +this.localStorage.getItem('test', { schema }) +``` + +For an integer only: +```typescript +import { JSONSchemaNumeric } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaNumeric = { type: 'integer' }; + +this.localStorage.getItem('test', { schema }) +``` + +For numbers and integers, in version >= 6, you can also add the following optional validations: +- `multipleOf` +- `maximum` +- `exclusiveMaximum` +- `minimum` +- `exclusiveMinimum` + +### Validating a string + +```typescript +import { JSONSchemaString } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaString = { type: 'string' }; + +this.localStorage.getItem('test', { schema }) +``` + +For strings, in version >= 6, you can also add the following optional validations: +- `maxLength` +- `minLength` +- `pattern` + +### Validating an array + +```typescript +import { JSONSchemaArray, JSONSchemaString } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaArray = { items: { type: 'string' } as JSONSchemaString }; + +this.localStorage.getItem('test', { schema }) +``` + +What's expected in `items` is another JSON schema, +so you can also add the other optional validations related to the chosen type. + +For arrays, in version >= 6, you can also add the following optional validations: +- `maxItems` +- `minItems` +- `uniqueItems` + +### Validating an object + +```typescript +import { JSONSchemaObject, JSONSchemaString, JSONSchemaNumeric } from '@ngx-pwa/local-storage'; + +interface User { + firstName: string; + lastName: string; + age?: number; +} + +const schema: JSONSchemaObject = { + properties: { + firstName: { type: 'string' } as JSONSchemaString, + lastName: { type: 'string' } as JSONSchemaString, + age: { type: 'number' } as JSONSchemaNumeric + }, + required: ['firstName', 'lastName'] +}; + +this.localStorage.getItem('test', { schema }) +``` + +What's expected for each property is another JSON schema, +so you can also add the other optional validations related to the chosen type. + +### Validating fixed values + +Since version >= 6, if it can only be a fixed value: +```typescript +import { JSONSchemaConst } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaConst = { const: 'some value' }; + +this.localStorage.getItem('test', { schema }) +``` + +Since version >= 6, if it can only be a fixed value among a list: +```typescript +import { JSONSchemaEnum } from '@ngx-pwa/local-storage'; + +const schema: JSONSchemaEnum = { enum: ['value 1', 'value 2'] }; + +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 either `type` or `properties` or `items` or `const` or `enum`. + +### 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` + +## 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: { type: 'string' } }`. + +It's because a cast only means: "TypeScript, trust me, I'm telling you it will be a string". +But TypeScript won't do any real validation as it can't: +**TypeScript can only manage checks at compilation time, while client-side storage checks can only happen at runtime**. +So the JSON schema is required for real validation. + +And in the opposite way, the JSON schema doesn't tell TypeScript what will be the type of the data. +So the cast is a convenience so you don't end up with `any` while you already checked the data. + +It means **you are responsible the casted type describes the same structure as the JSON schema**. +The lib can't check that. + +## Why specific JSONSchema interfaces? + +Unfortunately, the JSON schema standard is structured in such a way it's currently impossible to do an equivalent TypeScript interface, +which would be generic for all types, but only allowing you the optional validations relative to the type you choose +(for example, `maxLength` should only be allowed when `type` is set to `'string'`). + +Thus casting with a more specific interface (like `JSONSchemaString`) allows TypeScript to check your JSON Schema is really valid. +But it's not mandatory. + +## 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/projects/ngx-pwa/local-storage/package.json b/projects/ngx-pwa/local-storage/package.json index bdac2204..68dbac86 100644 --- a/projects/ngx-pwa/local-storage/package.json +++ b/projects/ngx-pwa/local-storage/package.json @@ -1,6 +1,6 @@ { "name": "@ngx-pwa/local-storage", - "version": "7.4.0", + "version": "7.5.0", "description": "Efficient local storage module for Angular apps and PWA: simple API based on native localStorage API, but internally stored via the asynchronous IndexedDB API for performance, and wrapped in RxJS observables to be homogeneous with other Angular modules.", "author": "Cyrille Tuzi", "license": "MIT", 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..376fde54 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/get-item-overloads.spec.ts @@ -0,0 +1,353 @@ +import { LocalStorage } from './lib.service'; +import { IndexedDBDatabase } from './databases/indexeddb-database'; +import { JSONValidator } from './validation/json-validator'; +import { SCHEMA_STRING, SCHEMA_ARRAY_OF_NUMBERS, SCHEMA_ARRAY_OF_BOOLEANS, SCHEMA_ARRAY_OF_STRINGS } from './validation/constants'; +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 predefined basic schema and without type param', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: SCHEMA_STRING }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile with predefined basic schema and with type param', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: SCHEMA_STRING }).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 boolean type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { type: 'boolean' } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for const string type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { const: 'test' } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for const number type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { const: 5 } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for const boolean type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { const: false } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for enum string type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { enum: ['test', 'test 2'] } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for enum number type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { enum: [1, 2] } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of strings', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: SCHEMA_ARRAY_OF_STRINGS }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of numbers', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: SCHEMA_ARRAY_OF_NUMBERS }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of booleans', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: SCHEMA_ARRAY_OF_BOOLEANS }).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 with predefined arrays', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: SCHEMA_ARRAY_OF_BOOLEANS }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for array of objects', (done: DoneFn) => { + + interface Test { + test: string; + } + + localStorageService.getItem('test', { schema: { items: { + properties: { + test: { type: 'string' } as JSONSchemaString + } + } } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for objects without param type', (done: DoneFn) => { + + localStorageService.getItem('test', { schema: { + properties: { + test: SCHEMA_STRING + } + } }).subscribe((_) => { + + expect().nothing(); + + done(); + + }); + + }); + + it('should compile for objects with param type', (done: DoneFn) => { + + interface Test { + test: string; + } + + localStorageService.getItem('test', { schema: { + properties: { + test: SCHEMA_STRING + } + } }).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 bf75ddf0..f2c25b26 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 @@ -6,8 +6,9 @@ import { LocalStorage } from './lib.service'; import { IndexedDBDatabase } from './databases/indexeddb-database'; import { LocalStorageDatabase } from './databases/localstorage-database'; import { MockLocalDatabase } from './databases/mock-local-database'; -import { JSONSchema, JSONSchemaString } from './validation/json-schema'; +import { JSONSchema } from './validation/json-schema'; import { JSONValidator } from './validation/json-validator'; +import { SCHEMA_STRING, SCHEMA_NUMBER, SCHEMA_BOOLEAN } from './validation/constants'; function testGetItem(type: 'primitive' | 'object', localStorageService: LocalStorage, value: T, done: DoneFn) { @@ -321,12 +322,9 @@ function tests(localStorageService: LocalStorage) { const value = { unexpected: 'value' }; - // TODO: delete cast when TS 3.2 issue is fixed const schema: JSONSchema = { properties: { - expected: { - type: 'string' - } as JSONSchemaString + expected: SCHEMA_STRING }, required: ['expected'] }; @@ -357,12 +355,9 @@ function tests(localStorageService: LocalStorage) { const value = { expected: 'value' }; - // TODO: delete cast when TS 3.2 issue is fixed const schema: JSONSchema = { properties: { - expected: { - type: 'string' - } as JSONSchemaString + expected: SCHEMA_STRING }, required: ['expected'] }; @@ -389,12 +384,9 @@ function tests(localStorageService: LocalStorage) { it('should return the data if the data is null (no validation)', (done: DoneFn) => { - // TODO: delete cast when TS 3.2 issue is fixed const schema: JSONSchema = { properties: { - expected: { - type: 'string' - } as JSONSchemaString + expected: SCHEMA_STRING }, required: ['expected'] }; @@ -949,15 +941,15 @@ describe('LocalStorage with IndexedDB', () => { } const getTestValues: [any, JSONSchema][] = [ - ['hello', { type: 'string' }], - ['', { type: 'string' }], - [0, { type: 'number' }], - [1, { type: 'number' }], - [true, { type: 'boolean' }], - [false, { type: 'boolean' }], + ['hello', SCHEMA_STRING], + ['', SCHEMA_STRING], + [0, SCHEMA_NUMBER], + [1, SCHEMA_NUMBER], + [true, SCHEMA_BOOLEAN], + [false, SCHEMA_BOOLEAN], // TODO: delete cast when TS 3.2 issue is fixed - [[1, 2, 3], { items: { type: 'number' } } as JSONSchema], - [{ test: 'value' }, { properties: { test: { type: 'string' } } } as JSONSchema], + [[1, 2, 3], { items: SCHEMA_NUMBER }], + [{ test: 'value' }, { properties: { test: SCHEMA_STRING } }], [null, { type: 'null' }], [undefined, { type: 'null' }], ]; 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..c7b9f7ff 100755 --- a/projects/ngx-pwa/local-storage/src/lib/lib.service.ts +++ b/projects/ngx-pwa/local-storage/src/lib/lib.service.ts @@ -3,10 +3,15 @@ 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, JSONSchemaString, JSONSchemaNumeric, JSONSchemaBoolean, + JSONSchemaArrayOf, JSONSchemaConstOf, JSONSchemaEnumOf +} from './validation/json-schema'; import { JSONValidator } from './validation/json-validator'; export interface LSGetItemOptions { + /** JSON schema which will be used to validate the data + * Predefined constants (`SCHEMA_`) are exported by the lib to simplify common scenarios */ schema?: JSONSchema | null; } @@ -33,8 +38,21 @@ export class LocalStorage { /** * Gets an item value in local storage * @param key The item's key + * @param options Current options include a JSON schema to validate the data * @returns The item's value if the key exists, null otherwise, wrapped in an RxJS Observable */ + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaBoolean | JSONSchemaConstOf }): Observable; + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaNumeric | JSONSchemaConstOf | JSONSchemaEnumOf }): Observable; + getItem(key: string, options: LSGetItemOptions & + { schema: JSONSchemaString | JSONSchemaConstOf | JSONSchemaEnumOf }): 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) { diff --git a/projects/ngx-pwa/local-storage/src/lib/validation/constants.ts b/projects/ngx-pwa/local-storage/src/lib/validation/constants.ts new file mode 100644 index 00000000..1db82d73 --- /dev/null +++ b/projects/ngx-pwa/local-storage/src/lib/validation/constants.ts @@ -0,0 +1,25 @@ +import { JSONSchemaBoolean, JSONSchemaNumeric, JSONSchemaString, JSONSchemaArrayOf } from './json-schema'; + +/* Schemas for primitive types */ +export const SCHEMA_BOOLEAN: JSONSchemaBoolean = { type: 'boolean' }; +export const SCHEMA_INTEGER: JSONSchemaNumeric = { type: 'integer' }; +export const SCHEMA_NUMBER: JSONSchemaNumeric = { type: 'number' }; +export const SCHEMA_STRING: JSONSchemaString = { type: 'string' }; + +/* Schemas for basic arrays */ +export const SCHEMA_ARRAY_OF_BOOLEANS: JSONSchemaArrayOf = { + type: 'array', + items: SCHEMA_BOOLEAN +}; +export const SCHEMA_ARRAY_OF_INTEGERS: JSONSchemaArrayOf = { + type: 'array', + items: SCHEMA_INTEGER +}; +export const SCHEMA_ARRAY_OF_NUMBERS: JSONSchemaArrayOf = { + type: 'array', + items: SCHEMA_NUMBER +}; +export const SCHEMA_ARRAY_OF_STRINGS: JSONSchemaArrayOf = { + type: 'array', + items: SCHEMA_STRING +}; 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 497555b3..cf479f03 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,3 +1,6 @@ +/** + * Prefer `JSONSchemaConstOf`, which is more precise + */ export interface JSONSchemaConst { /** @@ -8,6 +11,15 @@ export interface JSONSchemaConst { } +export interface JSONSchemaConstOf extends JSONSchemaConst { + + const: T; + +} + +/** + * Prefer `JSONSchemaEnumOf`, which is more precise + */ export interface JSONSchemaEnum { /** @@ -18,6 +30,12 @@ export interface JSONSchemaEnum { } +export interface JSONSchemaEnumOf extends JSONSchemaEnum { + + enum: T[]; + +} + export interface JSONSchemaBoolean { /** @@ -95,6 +113,10 @@ export interface JSONSchemaNumeric { } +/** + * For basic arrays (of booleans, integers, numbers or strings), + * prefer `JSONSchemaArrayOf`, which is more precise + */ export interface JSONSchemaArray { /** @@ -127,6 +149,14 @@ export interface JSONSchemaArray { } +export interface JSONSchemaArrayOf extends JSONSchemaArray { + + type: 'array'; + + items: T; + +} + export interface JSONSchemaObject { /** @@ -152,6 +182,8 @@ export interface JSONSchemaObject { /** * Subset of the JSON Schema. + * Due to limitations in TypeScript, prefer specific interfaces (`JSONSchemaBoolean`, `JSONSchemaString`...) + * as autocompletion and checks will be more accurate. * Types are enforced to validate everything: * each value MUST have just ONE of either 'type' or 'properties' or 'items' or 'const' or 'enum'. * Therefore, unlike the spec, booleans are not allowed as schemas. 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 7ef831fa..30c13ef6 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 @@ -1,8 +1,9 @@ import { JSONValidator } from './json-validator'; import { JSONSchemaConst, JSONSchemaEnum, JSONSchemaBoolean, JSONSchemaNull, - JSONSchemaNumeric, JSONSchemaString, JSONSchemaObject, JSONSchemaArray + JSONSchemaNumeric, JSONSchemaString, JSONSchemaObject, JSONSchemaArray, JSONSchema } from './json-schema'; +import { SCHEMA_BOOLEAN, SCHEMA_STRING, SCHEMA_NUMBER, SCHEMA_INTEGER } from './constants'; describe(`JSONValidator`, () => { @@ -20,7 +21,8 @@ describe(`JSONValidator`, () => { expect(() => { - jsonValidator.validate({ test: 'test' }, { properties: { test: { type: 'string' } }, additionalProperties: true }); + // TODO: remove casting when T3.2 issue is fixed + jsonValidator.validate({ test: 'test' }, { properties: { test: { type: 'string' } }, additionalProperties: true } as JSONSchema); }).not.toThrow(); @@ -188,6 +190,10 @@ describe(`JSONValidator`, () => { expect(test).toBe(true); + const test2 = jsonValidator.validate(true, SCHEMA_BOOLEAN); + + expect(test2).toBe(true); + }); it(`should return true on a false value with a boolean type`, () => { @@ -196,6 +202,10 @@ describe(`JSONValidator`, () => { expect(test).toBe(true); + const test2 = jsonValidator.validate(false, SCHEMA_BOOLEAN); + + expect(test2).toBe(true); + }); it(`should return true on a null value with a null type`, () => { @@ -224,6 +234,22 @@ describe(`JSONValidator`, () => { expect(test).toBe(true); + const test2 = jsonValidator.validate('test', SCHEMA_STRING); + + expect(test2).toBe(true); + + }); + + it(`should return false on a string value with a mismatched type`, () => { + + const test = jsonValidator.validate(10, { type: 'string' } as JSONSchemaString); + + expect(test).toBe(false); + + const test2 = jsonValidator.validate(10, SCHEMA_STRING); + + expect(test2).toBe(false); + }); it(`should throw if maxLength is not an integer`, () => { @@ -324,6 +350,10 @@ describe(`JSONValidator`, () => { expect(test).toBe(true); + const test2 = jsonValidator.validate(10, SCHEMA_NUMBER); + + expect(test2).toBe(true); + }); it(`should return true on a decimal value with a number type`, () => { @@ -332,6 +362,10 @@ describe(`JSONValidator`, () => { expect(test).toBe(true); + const test2 = jsonValidator.validate(10.1, SCHEMA_NUMBER); + + expect(test2).toBe(true); + }); it(`should return true on an integer value with an integer type`, () => { @@ -340,6 +374,10 @@ describe(`JSONValidator`, () => { expect(test).toBe(true); + const test2 = jsonValidator.validate(10, SCHEMA_INTEGER); + + expect(test2).toBe(true); + }); it(`should return false on a decimal value with an integer type`, () => { @@ -348,6 +386,10 @@ describe(`JSONValidator`, () => { expect(test).toBe(false); + const test2 = jsonValidator.validate(10.1, SCHEMA_INTEGER); + + expect(test2).toBe(false); + }); it(`should throw if multipleOf is not strictly greater than 0`, () => { diff --git a/projects/ngx-pwa/local-storage/src/public_api.ts b/projects/ngx-pwa/local-storage/src/public_api.ts index a8ee1be8..fcb828c0 100644 --- a/projects/ngx-pwa/local-storage/src/public_api.ts +++ b/projects/ngx-pwa/local-storage/src/public_api.ts @@ -4,7 +4,8 @@ export { JSONSchema, JSONSchemaConst, JSONSchemaEnum, JSONSchemaBoolean, - JSONSchemaNumeric, JSONSchemaString, JSONSchemaArray, JSONSchemaObject + JSONSchemaNumeric, JSONSchemaString, JSONSchemaArray, JSONSchemaObject, + JSONSchemaArrayOf, JSONSchemaEnumOf, JSONSchemaConstOf } from './lib/validation/json-schema'; export { LocalDatabase } from './lib/databases/local-database'; export { IndexedDBDatabase } from './lib/databases/indexeddb-database'; @@ -13,3 +14,7 @@ export { MockLocalDatabase } from './lib/databases/mock-local-database'; export { JSONValidator } from './lib/validation/json-validator'; export { LSGetItemOptions, LocalStorage } from './lib/lib.service'; export { localStorageProviders, LocalStorageProvidersConfig, LOCAL_STORAGE_PREFIX } from './lib/tokens'; +export { + SCHEMA_BOOLEAN, SCHEMA_INTEGER, SCHEMA_NUMBER, SCHEMA_STRING, + SCHEMA_ARRAY_OF_BOOLEANS, SCHEMA_ARRAY_OF_INTEGERS, SCHEMA_ARRAY_OF_NUMBERS, SCHEMA_ARRAY_OF_STRINGS +} from './lib/validation/constants';