diff --git a/.eslintignore b/.eslintignore index d849ec6cc..91cef1057 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,12 +3,26 @@ src/**/*.d.ts src/**/*.js.map src/Kuzzle.js +src/KuzzleError.js src/controllers/Auth.js src/controllers/Document.js +src/controllers/Realtime.js +src/controllers/Index.js +src/controllers/Collection.js src/controllers/Base.js src/core/security/User.js src/core/security/Profile.js src/core/security/Role.js +src/protocols/abstract/Base.js +src/protocols/abstract/Realtime.js +src/protocols/Http.js +src/protocols/WebSocket.js +src/protocols/index.js src/utils/interfaces.js +src/core/KuzzleEventEmitter.js src/core/searchResult/SearchResultBase.js src/core/searchResult/Document.js +src/core/searchResult/Profile.js +src/core/searchResult/Role.js +src/core/searchResult/Specifications.js +src/core/searchResult/User.js diff --git a/.eslintc-ts.json b/.eslintrc-ts.json similarity index 100% rename from .eslintc-ts.json rename to .eslintrc-ts.json diff --git a/.gitignore b/.gitignore index 1a1af6994..1f5aa706a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,12 +31,26 @@ test-*.js *.js.map index.js src/Kuzzle.js +src/KuzzleError.js src/controllers/Auth.js src/controllers/Document.js +src/controllers/Index.js +src/controllers/Collection.js src/controllers/Base.js +src/controllers/Realtime.js src/core/security/User.js src/core/security/Profile.js src/core/security/Role.js +src/protocols/abstract/Base.js +src/protocols/abstract/Realtime.js +src/protocols/Http.js +src/protocols/WebSocket.js +src/protocols/index.js src/utils/interfaces.js +src/core/KuzzleEventEmitter.js src/core/searchResult/SearchResultBase.js src/core/searchResult/Document.js +src/core/searchResult/Profile.js +src/core/searchResult/Role.js +src/core/searchResult/Specifications.js +src/core/searchResult/User.js diff --git a/.travis.yml b/.travis.yml index aa2ac4cef..e932f0495 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,7 @@ jobs: install: - npm install + - npm run build script: - npm run test:lint @@ -78,7 +79,7 @@ jobs: - npm install - npm run build script: - - npm run doc-testing + - travis_retry npm run doc-testing - stage: Tests name: Dead link check @@ -93,7 +94,7 @@ jobs: - $(npm bin)/kuzdoc framework:link -d /sdk/js/7/ -v 7 script: - gem install typhoeus - - cd doc/framework/ && HYDRA_MAX_CONCURRENCY=20 ruby .ci/dead-links.rb -p src/sdk/js/7/ + - cd doc/framework/ && HYDRA_MAX_CONCURRENCY=20 travis_retry ruby .ci/dead-links.rb -p src/sdk/js/7/ - stage: Deployment Doc Dev name: Deploy next-docs.kuzzle.io diff --git a/doc/7/controllers/auth/create-api-key/index.md b/doc/7/controllers/auth/create-api-key/index.md index d7a2ca918..f0f301b9f 100644 --- a/doc/7/controllers/auth/create-api-key/index.md +++ b/doc/7/controllers/auth/create-api-key/index.md @@ -56,7 +56,7 @@ The API key content has the following properties: | Name | Type | Description | | --------- | ----------------- | ---------------- | | `userId` |
string
| User kuid | -| `expiresAt` |
number
| Expiration date in UNIX micro-timestamp format (`-1` if the token never expires) | +| `expiresAt` |
number
| Expiration date in Epoch-millis format (`-1` if the token never expires) | | `ttl` |
number
| Original TTL | | `description` |
string
| API key description | | `token` |
string
| Authentication token associated with this API key | diff --git a/doc/7/controllers/collection/create/index.md b/doc/7/controllers/collection/create/index.md index 68ada4bda..4346f2a6d 100644 --- a/doc/7/controllers/collection/create/index.md +++ b/doc/7/controllers/collection/create/index.md @@ -7,17 +7,22 @@ description: Create a new collection # create -Creates a new [collection](/core/2/guides/essentials/store-access-data) in Kuzzle via the persistence engine, in the provided index. +Creates a new [collection](/core/2/guides/essentials/store-access-data) in the provided index. You can also provide an optional data mapping that allow you to exploit the full capabilities of our -persistent data storage layer, [ElasticSearch](https://www.elastic.co/elastic-stack) (check here the [mapping capabilities of ElasticSearch](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/mapping.html)). +persistent data storage layer, [ElasticSearch](https://www.elastic.co/elastic-stack) (check here the [mapping capabilities of ElasticSearch](/core/2/guides/essentials/database-mappings/)). This method will only update the mapping if the collection already exists. + + + +You can also provide Elasticsearch [index settings](https://www.elastic.co/guide/en/elasticsearch/reference/7.5/index-modules.html#index-modules-settings) when creating a new collection. +
```js -create(index, collection, [mapping], [options]); +create(index, collection, [definition], [options]); ```
@@ -26,17 +31,48 @@ create(index, collection, [mapping], [options]); | ------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `index` |
string
| Index name | | `collection` |
string
| Collection name | -| `mapping` |
object
| Describes the data mapping to associate to the new collection, using Elasticsearch [mapping format](https://www.elastic.co/guide/en/elasticsearch/reference/7.3/mapping.html) | +| `definition` |
object
| Describes the collection mappings and the ES index settings | | `options` |
object
| Query options | + -### mapping - -An object representing the data mapping of the collection. +### definition +An object containings: + - [collection mappings](/core/2/guides/essentials/database-mappings). + - Elasticsearch [index settings](https://www.elastic.co/guide/en/elasticsearch/reference/7.5/index-modules.html#index-modules-settings) The mapping must have a root field `properties` that contain the mapping definition: ```js -const mapping = { +const definition = { + mappings: { + properties: { + field1: { type: 'text' }, + field2: { + properties: { + nestedField: { type: 'keyword' } + } + } + } + }, + settings: { + + } +}; +``` + + + + + + +### definition + +An object representing the data mappings of the collection. + +The mappings must have a root field `properties` that contain the mappings properties definition: + +```js +const mappings = { properties: { field1: { type: 'text' }, field2: { @@ -50,6 +86,8 @@ const mapping = { More informations about database mappings [here](/core/2/guides/essentials/database-mappings). + + ### options Additional query options diff --git a/doc/7/controllers/collection/create/snippets/create.js b/doc/7/controllers/collection/create/snippets/create.js index 4c10502d6..89cc8a348 100644 --- a/doc/7/controllers/collection/create/snippets/create.js +++ b/doc/7/controllers/collection/create/snippets/create.js @@ -1,4 +1,4 @@ -const mapping = { +const mappings = { properties: { license: { type: 'keyword' }, driver: { @@ -11,7 +11,7 @@ const mapping = { }; try { - await kuzzle.collection.create('nyc-open-data', 'yellow-taxi', mapping); + await kuzzle.collection.create('nyc-open-data', 'yellow-taxi', { mappings }); console.log('Success'); } catch (error) { diff --git a/doc/7/controllers/collection/delete/index.md b/doc/7/controllers/collection/delete/index.md new file mode 100644 index 000000000..3cfa16394 --- /dev/null +++ b/doc/7/controllers/collection/delete/index.md @@ -0,0 +1,32 @@ +--- +code: true +type: page +title: delete +description: Deletes a collection +--- + +# delete + +Deletes a collection. + +
+ +```js +delete(index, collection); +``` + +
+ +| Arguments | Type | Description | +| ------------ | ----------------- | --------------- | +| `index` |
string
| Index name | +| `collection` |
string
| Collection name | + + +## Resolves + +Resolves if the collection is successfully deleted. + +## Usage + +<<< ./snippets/delete-specifications.js diff --git a/doc/7/controllers/collection/delete/snippets/delete-specifications.js b/doc/7/controllers/collection/delete/snippets/delete-specifications.js new file mode 100644 index 000000000..5dc6b5fd9 --- /dev/null +++ b/doc/7/controllers/collection/delete/snippets/delete-specifications.js @@ -0,0 +1,7 @@ +try { + await kuzzle.collection.delete('nyc-open-data', 'yellow-taxi'); + + console.log('Success'); +} catch (error) { + console.error(error.message); +} diff --git a/doc/7/controllers/collection/delete/snippets/delete-specifications.test.yml b/doc/7/controllers/collection/delete/snippets/delete-specifications.test.yml new file mode 100644 index 000000000..69c1d5c18 --- /dev/null +++ b/doc/7/controllers/collection/delete/snippets/delete-specifications.test.yml @@ -0,0 +1,7 @@ +name: collection#delete +description: Delete a collection +hooks: + before: curl -X POST kuzzle:7512/nyc-open-data/_create && curl -X PUT kuzzle:7512/nyc-open-data/yellow-taxi + after: +template: default +expected: Success diff --git a/doc/7/controllers/collection/exists/index.md b/doc/7/controllers/collection/exists/index.md index 8a8000677..012fc4300 100644 --- a/doc/7/controllers/collection/exists/index.md +++ b/doc/7/controllers/collection/exists/index.md @@ -7,7 +7,7 @@ description: Check if collection exists # exists -Check if a collection exists in Kuzzle. +Checks if a collection exists in Kuzzle.
diff --git a/doc/7/controllers/collection/get-mapping/index.md b/doc/7/controllers/collection/get-mapping/index.md index 0f0fdcade..c01a9177e 100644 --- a/doc/7/controllers/collection/get-mapping/index.md +++ b/doc/7/controllers/collection/get-mapping/index.md @@ -30,6 +30,7 @@ Additional query options | Property | Type
(default) | Description | | ---------- | ------------------------------- | ---------------------------------------------------------------------------- | | `queuable` |
boolean

(`true`) | If true, queues the request during downtime, until connected to Kuzzle again | +| `includeKuzzleMeta` |
boolean

(`true`) | If true, the returned mappings will contain [Kuzzle metadata](/core/2/guides/essentials/document-metadata/) | ## Resolves diff --git a/doc/7/controllers/collection/refresh/index.md b/doc/7/controllers/collection/refresh/index.md index b79d97a7d..407f0e6b5 100644 --- a/doc/7/controllers/collection/refresh/index.md +++ b/doc/7/controllers/collection/refresh/index.md @@ -7,7 +7,7 @@ description: Forces an Elasticsearch search index update # refresh -When writing or deleting documents in Kuzzle, the update needs to be indexed before being available in search results. +Refreshes a collection to reindex the written and deleted documents so they are available in search results. :::info A refresh operation comes with some performance costs. diff --git a/doc/7/controllers/collection/update-specifications/index.md b/doc/7/controllers/collection/update-specifications/index.md index af9b1981d..b7947f2d7 100644 --- a/doc/7/controllers/collection/update-specifications/index.md +++ b/doc/7/controllers/collection/update-specifications/index.md @@ -7,7 +7,7 @@ description: Update the validation specifications # updateSpecifications -The updateSpecifications method allows you to create or update the validation specifications for a collection. +Creates or updates the validation specifications for a collection. When the validation specification is not formatted correctly, a detailed error message is returned to help you to debug. diff --git a/doc/7/controllers/collection/update/index.md b/doc/7/controllers/collection/update/index.md index f7edd238a..c39bc0e6b 100644 --- a/doc/7/controllers/collection/update/index.md +++ b/doc/7/controllers/collection/update/index.md @@ -13,28 +13,66 @@ You can define the collection [dynamic mapping policy](/core/2/guides/essentials You can define [collection additional metadata](/core/2/guides/essentials/database-mappings#collection-metadata) within the `_meta` root field. + + + +You can also provide Elasticsearch [index settings](https://www.elastic.co/guide/en/elasticsearch/reference/7.5/index-modules.html#index-modules-settings) when creating a new collection. +
```js -update(index, collection, mapping); +update(index, collection, definition); ```
-| Arguments | Type | Description | -| ------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `index` |
string
| Index name | -| `collection` |
string
| Collection name | -| `mapping` |
object
| Describes the collection mapping | +| Arguments | Type | Description | +|--------------|-------------------|-------------------------------------------------------------| +| `index` |
string
| Index name | +| `collection` |
string
| Collection name | +| `definition` |
object
| Describes the collection mappings and the ES index settings | +| `options` |
object
| Query options | + + -### mapping +### definition -An object representing the collection data mapping. +An object containing: + - [collection mappings](/core/2/guides/essentials/database-mappings). + - Elasticsearch [index settings](https://www.elastic.co/guide/en/elasticsearch/reference/7.5/index-modules.html#index-modules-settings) -This object must have a root field `properties` that contain the mapping definition: ```js -const mapping = { +const definition = { + mappings: { + properties: { + field1: { type: 'text' }, + field2: { + properties: { + nestedField: { type: 'keyword' } + } + } + } + }, + settings: { + // index settings (e.g. analyzers) + } +}; +``` + + + + + + +### definition + +An object representing the data mappings of the collection. + +The mappings must have a root field `properties` that contain the mappings properties definition: + +```js +const mappings = { properties: { field1: { type: 'text' }, field2: { @@ -48,6 +86,9 @@ const mapping = { More informations about database mappings [here](/core/2/guides/essentials/database-mappings). + + + ## Resolves Resolve if the collection is successfully updated. diff --git a/doc/7/controllers/collection/validate-specifications/index.md b/doc/7/controllers/collection/validate-specifications/index.md index cd5cf676b..f10474abe 100644 --- a/doc/7/controllers/collection/validate-specifications/index.md +++ b/doc/7/controllers/collection/validate-specifications/index.md @@ -7,7 +7,7 @@ description: Validate specifications format # validateSpecifications -The validateSpecifications method checks if a validation specification is well formatted. It does not store or modify the existing specification. +Checks if a validation specification is well formatted. It does not store nor modify the existing specification. When the validation specification is not formatted correctly, a detailed error message is returned to help you to debug. diff --git a/doc/7/controllers/realtime/count/index.md b/doc/7/controllers/realtime/count/index.md index d8beddf88..dd5c460ed 100644 --- a/doc/7/controllers/realtime/count/index.md +++ b/doc/7/controllers/realtime/count/index.md @@ -32,7 +32,7 @@ Additional query options ## Resolves -Resolves to a number represensting active connections using the same provided subscription room. +Resolves to a number representing active connections using the same provided subscription room. ## Usage diff --git a/doc/7/controllers/security/create-api-key/index.md b/doc/7/controllers/security/create-api-key/index.md index 35202ecb1..ae4722cb0 100644 --- a/doc/7/controllers/security/create-api-key/index.md +++ b/doc/7/controllers/security/create-api-key/index.md @@ -57,7 +57,7 @@ The API key content has the following properties: | Name | Type | Description | | --------- | ----------------- | ---------------- | | `userId` |
string
| User kuid | -| `expiresAt` |
number
| Aexpiration date in UNIX micro-timestamp format (`-1` if the token never expires) | +| `expiresAt` |
number
| Aexpiration date in Epoch-millis format (`-1` if the token never expires) | | `ttl` |
number
| Original TTL | | `description` |
string
| API key description | | `token` |
string
| Authentication token associated with this API key | diff --git a/doc/7/controllers/security/m-delete-profiles/snippets/m-delete-profiles.test.yml b/doc/7/controllers/security/m-delete-profiles/snippets/m-delete-profiles.test.yml index 6fbf0d363..0c7f7e2de 100644 --- a/doc/7/controllers/security/m-delete-profiles/snippets/m-delete-profiles.test.yml +++ b/doc/7/controllers/security/m-delete-profiles/snippets/m-delete-profiles.test.yml @@ -8,4 +8,4 @@ hooks: }' kuzzle:7512/profiles/profile${i}/_create done template: default -expected: '^\[ ''profile\d'', ''profile\d'', ''profile\d'', ''profile\d', ''profile\d'' \]$' +expected: '^\[ ''profile\d'', ''profile\d'', ''profile\d'', ''profile\d'', ''profile\d'' \]$' diff --git a/doc/7/controllers/security/m-delete-roles/snippets/m-delete-roles.test.yml b/doc/7/controllers/security/m-delete-roles/snippets/m-delete-roles.test.yml index 5578d3834..1fb4967fa 100644 --- a/doc/7/controllers/security/m-delete-roles/snippets/m-delete-roles.test.yml +++ b/doc/7/controllers/security/m-delete-roles/snippets/m-delete-roles.test.yml @@ -14,4 +14,4 @@ hooks: }' kuzzle:7512/roles/role${i}/_create done template: default -expected: '^\[ ''role1'', ''role2'', ''role3'', ''role4'', ''role5'' \]$' +expected: '^\[ ''role\d'', ''role\d'', ''role\d'', ''role\d'', ''role\d'' \]$' diff --git a/doc/7/controllers/security/m-delete-users/snippets/m-delete-users.test.yml b/doc/7/controllers/security/m-delete-users/snippets/m-delete-users.test.yml index be9ebea3d..ef0acb81b 100644 --- a/doc/7/controllers/security/m-delete-users/snippets/m-delete-users.test.yml +++ b/doc/7/controllers/security/m-delete-users/snippets/m-delete-users.test.yml @@ -16,4 +16,4 @@ hooks: }' kuzzle:7512/users/user${i}/_create done template: default -expected: '^\[ ''user1'', ''user2'', ''user3'' \]$' +expected: '^\[ ''user\d'', ''user\d'', ''user\d'' \]$' diff --git a/doc/7/core-classes/kuzzle-error/properties/index.md b/doc/7/core-classes/kuzzle-error/properties/index.md index 1b5655e92..63b07a4f9 100644 --- a/doc/7/core-classes/kuzzle-error/properties/index.md +++ b/doc/7/core-classes/kuzzle-error/properties/index.md @@ -14,3 +14,6 @@ order: 10 | `message` |
string
| Error message | | `status` |
number
| Error status code | | `stack` |
string
| Error stacktrace (only in development mode) | +| `id` |
string
| Error unique identifier | +| `code` |
string
| Error unique code | + diff --git a/doc/7/core-classes/kuzzle/constructor/index.md b/doc/7/core-classes/kuzzle/constructor/index.md index 2203444bc..6f076596f 100644 --- a/doc/7/core-classes/kuzzle/constructor/index.md +++ b/doc/7/core-classes/kuzzle/constructor/index.md @@ -46,6 +46,7 @@ Kuzzle SDK instance options. | `queueTTL` |
number

(`120000`) | Time a queued request is kept during offline mode, in milliseconds | | `queueMaxSize` |
number

(`500`) | Number of maximum requests kept during offline mode | | `replayInterval` |
number

(`10`) | Delay between each replayed requests, in milliseconds | +| `tokenExpiredInterval` |
number

(`1000`) | Time (in ms) during which a TokenExpired event is ignored | | `volatile` |
object

(`{}`) | Common volatile data, will be sent to all future requests | ## Return diff --git a/doc/7/essentials/error-handling/index.md b/doc/7/essentials/error-handling/index.md index f19e943a8..fa56caac4 100644 --- a/doc/7/essentials/error-handling/index.md +++ b/doc/7/essentials/error-handling/index.md @@ -15,7 +15,9 @@ All SDK methods return a promise, that can be rejected with a `KuzzleError` valu | Property | Type | Description | | -------- | ----------------- | ------------------------------------------------------------------------------------------ | | `status` |
number
| Status following [HTTP Standards](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) | -| `stack` |
string
| Error stacktrace (Only in development mode) | +| `stack` |
string
| Error stacktrace (Only in development mode) | `id` |
string
| Error unique identifier | +| `code` |
string
| Error unique code | + You can find a detailed list of possible errors messages and statuses in the [documentation API](/core/2/api/essentials/error-handling). diff --git a/doc/7/essentials/realtime-notifications/index.md b/doc/7/essentials/realtime-notifications/index.md index cdc976ce7..86583db0d 100644 --- a/doc/7/essentials/realtime-notifications/index.md +++ b/doc/7/essentials/realtime-notifications/index.md @@ -23,7 +23,7 @@ These notifications represent [documents changes & messages](/core/2/api/essenti | `protocol` |
string
| Network protocol used to modify the document | | `result` |
object
| Notification content | | `room` |
string
| Subscription channel identifier. Can be used to link a notification to its corresponding subscription | -| `scope` |
string
| `in`: document enters (or stays) in the scope
string | `in`: document enters (or stays) in the scope
`out`: document leaves the scope | | `timestamp` |
number
| Timestamp of the event, in Epoch-millis format | | `type` |
string
| `document`: Notification type | | `volatile` |
object
| Request [volatile data](/core/2/api/essentials/volatile-data) | @@ -50,7 +50,7 @@ These notifications represent [user events](/core/2/api/essentials/notifications | `room` |
string
| Subscription channel identifier. Can be used to link a notification to its corresponding subscription | | `timestamp` |
number
| Timestamp of the event, in Epoch-millis format | | `type` |
string
| `user`: Notification type | -| `user` |
string
| `in`: a new user has subscribed to the same filters
string | `in`: a new user has subscribed to the same filters
`out`: a user cancelled a shared subscription | | `volatile` |
object
| Request [volatile data](/core/2/api/essentials/volatile-data) | The `result` object is the notification content, and it has the following structure: diff --git a/doc/7/protocols/http/constructor/index.md b/doc/7/protocols/http/constructor/index.md index d5ff3892f..a1aaddae1 100644 --- a/doc/7/protocols/http/constructor/index.md +++ b/doc/7/protocols/http/constructor/index.md @@ -30,7 +30,8 @@ Http protocol connection options. | Property | Type
(default) | Description | | --------------- | -------------------------------- | ----------------------------------- | | `port` |
number

(`7512`) | Kuzzle server port | -| `sslConnection` |
boolean

(`false`) | Use SSL to connect to Kuzzle server | +| `sslConnection` |
boolean

(`false`) | Use SSL to connect to Kuzzle server | +| `ssl` |
boolean

(`false`) | Use SSL to connect to Kuzzle server. Defaults to `true` for ports 443 and 7443. | | `customRoutes` |
object

(`{}`) | Add custom routes | | `timeout` |
number

(`0`) | Connection timeout in milliseconds (`0` means no timeout) | diff --git a/doc/7/protocols/websocket/constructor/index.md b/doc/7/protocols/websocket/constructor/index.md index 736df3deb..7f770d046 100644 --- a/doc/7/protocols/websocket/constructor/index.md +++ b/doc/7/protocols/websocket/constructor/index.md @@ -31,9 +31,10 @@ WebSocket protocol connection options. | ------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | | `autoReconnect` |
boolean

(`true`) | Automatically reconnect to kuzzle after a `disconnected` event | | `port` |
number

(`7512`) | Kuzzle server port | -| `headers` |
object
(`{}`) | Connection HTTP headers (e.g. origin, subprotocols, ...)
**(Not supported by browsers)** | +| `headers` |
object
(`{}`) | Connection custom HTTP headers (e.g. origin, subprotocols, ...)
**(Not supported by browsers)** | | `reconnectionDelay` |
number

(`1000`) | Number of milliseconds between reconnection attempts | -| `sslConnection` |
boolean

(`false`) | Use SSL to connect to Kuzzle server | +| `sslConnection` |
boolean

(`false`) | Use SSL to connect to Kuzzle server | +| `ssl` |
boolean

(`false`) | Use SSL to connect to Kuzzle server. Defaults to `true` for ports 443 and 7443. | ## Return diff --git a/index.ts b/index.ts index b88efa680..dc87dfb68 100644 --- a/index.ts +++ b/index.ts @@ -8,34 +8,15 @@ if (typeof window !== 'undefined' && typeof BUILT === 'undefined') { 'Learn more at https://github.com/kuzzleio/sdk-javascript/tree/master#browser'); } -import { Kuzzle } from './src/Kuzzle'; -import { Http, WebSocket } from './src/protocols'; -import { BaseController } from './src/controllers/Base'; -import { KuzzleAbstractProtocol } from './src/protocols/abstract/Base'; -import { KuzzleEventEmitter } from './src/core/KuzzleEventEmitter'; +export * from './src/Kuzzle'; +export * from './src/protocols'; +export * from './src/controllers/Base'; +export * from './src/protocols/abstract/Base'; +export * from './src/core/KuzzleEventEmitter'; -import { SearchResultBase } from './src/core/searchResult/SearchResultBase'; -import { DocumentSearchResult } from './src/core/searchResult/Document'; -import { ProfileSearchResult } from './src/core/searchResult/Profile'; -import { RoleSearchResult } from './src/core/searchResult/Role'; -import { SpecificationSearchResult } from './src/core/searchResult/Specifications'; -import { UserSearchResult } from './src/core/searchResult/User'; - -const exported = { - Kuzzle, - Http, - WebSocket, - BaseController, - KuzzleAbstractProtocol, - KuzzleEventEmitter, - SearchResultBase, - DocumentSearchResult, - ProfileSearchResult, - RoleSearchResult, - SpecificationSearchResult, - UserSearchResult -} - -export default exported; - -module.exports = exported; +export * from './src/core/searchResult/SearchResultBase'; +export * from './src/core/searchResult/Document'; +export * from './src/core/searchResult/Profile'; +export * from './src/core/searchResult/Role'; +export * from './src/core/searchResult/Specifications'; +export * from './src/core/searchResult/User'; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 87472d4ad..1328d1680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kuzzle-sdk", - "version": "7.3.1", + "version": "7.4.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9e6a88c71..cdf7366c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kuzzle-sdk", - "version": "7.3.1", + "version": "7.4.0", "description": "Official Javascript SDK for Kuzzle", "author": "The Kuzzle Team ", "repository": { @@ -24,7 +24,9 @@ "test:functional": "cucumber-js --exit --fail-fast", "test:lint": "npm run test:lint:js && npm run test:lint:ts", "test:lint:js": "eslint --max-warnings=0 ./src ./test ./features", - "test:lint:ts": "eslint ./src --ext .ts --config .eslintc-ts.json", + "test:lint:js:fix": "eslint --max-warnings=0 ./src ./test ./features --fix", + "test:lint:ts": "eslint ./src --ext .ts --config .eslintrc-ts.json", + "test:lint:ts:fix": "eslint ./src --ext .ts --config .eslintrc-ts.json --fix", "build": "npm run build-ts && node build.js", "build-ts": "tsc --build tsconfig.json", "doc": "docker-compose -f doc/docker-compose.yml up", diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index 98a27138e..b19d4ed90 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -1,4 +1,5 @@ import { KuzzleEventEmitter } from './core/KuzzleEventEmitter'; +import { KuzzleAbstractProtocol } from './protocols/abstract/Base'; import { AuthController } from './controllers/Auth'; import { BulkController } from './controllers/Bulk'; @@ -12,7 +13,7 @@ import { MemoryStorageController } from './controllers/MemoryStorage'; import { uuidv4 } from './utils/uuidv4'; import { proxify } from './utils/proxify'; -import { JSONObject, KuzzleRequest } from './utils/interfaces'; +import { JSONObject, KuzzleRequest, KuzzleResponse } from './utils/interfaces'; // Defined by webpack plugin declare const SDKVERSION: any; @@ -31,6 +32,9 @@ const events = [ ]; export class Kuzzle extends KuzzleEventEmitter { + // We need to define any string key because users can register new controllers + [key: string]: any; + /** * Protocol used by the SDK to communicate with Kuzzle. */ @@ -48,7 +52,7 @@ export class Kuzzle extends KuzzleEventEmitter { */ public sdkVersion: string; /** - * SDK name (e.g: js@7.4.2). + * SDK name (e.g: `js@7.4.2`). */ public sdkName: string; /** @@ -58,11 +62,11 @@ export class Kuzzle extends KuzzleEventEmitter { public auth: AuthController; public bulk: any; - public collection: any; + public collection: CollectionController; public document: DocumentController; - public index: any; + public index: IndexController; public ms: any; - public realtime: any; + public realtime: RealtimeController; public security: any; public server: any; @@ -82,10 +86,84 @@ export class Kuzzle extends KuzzleEventEmitter { private __proxy__: any; /** - * @param protocol - the protocol to use - * @param [options] - Kuzzle options + * Instantiate a new SDK + * + * @example + * + * import { Kuzzle, WebSocket } from 'kuzzle-sdk'; + * + * const kuzzle = new Kuzzle( + * new WebSocket('localhost') + * ); */ - constructor(protocol: any, options: any = {}) { + constructor( + /** + * Network protocol to connect to Kuzzle. (e.g. `Http` or `WebSocket`) + */ + protocol: KuzzleAbstractProtocol, + options: { + /** + * Automatically renew all subscriptions on a `reconnected` event + * Default: `true` + */ + autoResubscribe?: boolean; + /** + * Time (in ms) during which a similar event is ignored + * Default: `200` + */ + eventTimeout?: number; + /** + * Common volatile data, will be sent to all future requests + * Default: `{}` + */ + volatile?: JSONObject; + /** + * If `true`, automatically queues all requests during offline mode + * Default: `false` + */ + autoQueue?: boolean; + /** + * If `true`, automatically replays queued requests + * on a `reconnected` event + * Default: `false` + */ + autoReplay?: boolean; + /** + * Custom function called during offline mode to filter + * queued requests on-the-fly + */ + queueFilter?: (request: KuzzleRequest) => boolean; + /** + * Called before dequeuing requests after exiting offline mode, + * to add items at the beginning of the offline queue + */ + offlineQueueLoader?: (...any) => any; + /** + * Number of maximum requests kept during offline mode + * Default: `500` + */ + queueMaxSize?: number; + /** + * Time a queued request is kept during offline mode, in milliseconds + * Default: `120000` + */ + queueTTL?: number; + /** + * Delay between each replayed requests, in milliseconds + * Default: `10` + */ + replayInterval?: number; + /** + * Time (in ms) during which a TokenExpired event is ignored + * Default: `1000` + */ + tokenExpiredInterval?: number; + /** + * If set to `auto`, the `autoQueue` and `autoReplay` are also set to `true` + */ + offlineMode?: 'auto'; + } = {} + ) { super(); if (protocol === undefined || protocol === null) { @@ -324,13 +402,12 @@ export class Kuzzle extends KuzzleEventEmitter { return this._superEmit(eventName, ...payload); } - _superEmit (eventName, ...payload) { + private _superEmit (eventName, ...payload) { return super.emit(eventName, ...payload); } /** - * Connects to a Kuzzle instance using the provided host name - * @returns {Promise} + * Connects to a Kuzzle instance */ connect (): Promise { if (this.protocol.isReady()) { @@ -361,15 +438,10 @@ export class Kuzzle extends KuzzleEventEmitter { if (this.autoQueue) { this.startQueuing(); } - - this.realtime.disconnected(); - this.emit('networkError', error); }); this.protocol.addListener('disconnect', () => { - this.realtime.disconnected(); - this.emit('disconnected'); }); @@ -382,8 +454,6 @@ export class Kuzzle extends KuzzleEventEmitter { this.playQueue(); } - this.realtime.reconnected(); - if (this.auth.authenticationToken) { return this.auth.checkToken() .then(res => { @@ -421,7 +491,7 @@ export class Kuzzle extends KuzzleEventEmitter { return this._superAddListener(event, listener); } - _superAddListener (event, listener) { + private _superAddListener (event, listener) { return super.addListener(event, listener); } @@ -451,11 +521,10 @@ export class Kuzzle extends KuzzleEventEmitter { * - volatile (object, default: null): * Additional information passed to notifications to other users * - * @param {object} request - * @param {object} [options] - Optional arguments - * @returns {Promise} + * @param request + * @param options - Optional arguments */ - query (request: KuzzleRequest = {}, options: JSONObject = {}): Promise { + query (request: KuzzleRequest = {}, options: JSONObject = {}): Promise { if (typeof request !== 'object' || Array.isArray(request)) { throw new Error(`Kuzzle.query: Invalid request: ${JSON.stringify(request)}`); } @@ -570,11 +639,10 @@ Discarded request: ${JSON.stringify(request)}`)); /** * Adds a new controller and make it available in the SDK. * - * @param {BaseController} ControllerClass - * @param {string} accessor - * @returns {Kuzzle} + * @param ControllerClass + * @param accessor */ - useController (ControllerClass, accessor) { + useController (ControllerClass: any, accessor: string) { if (!(accessor && accessor.length > 0)) { throw new Error('You must provide a valid accessor.'); } @@ -601,7 +669,7 @@ Discarded request: ${JSON.stringify(request)}`)); return this; } - _checkPropertyType (prop, typestr, value) { + private _checkPropertyType (prop, typestr, value) { const wrongType = typestr === 'array' ? !Array.isArray(value) : typeof value !== typestr; if (wrongType) { @@ -612,7 +680,7 @@ Discarded request: ${JSON.stringify(request)}`)); /** * Clean up the queue, ensuring the queryTTL and queryMaxSize properties are respected */ - _cleanQueue () { + private _cleanQueue () { const now = Date.now(); let lastDocumentIndex = -1; @@ -644,7 +712,7 @@ Discarded request: ${JSON.stringify(request)}`)); /** * Play all queued requests, in order. */ - _dequeue() { + private _dequeue() { const uniqueQueue = {}, dequeuingProcess = () => { @@ -693,7 +761,4 @@ Discarded request: ${JSON.stringify(request)}`)); dequeuingProcess(); } - } - -module.exports = { Kuzzle }; diff --git a/src/KuzzleError.js b/src/KuzzleError.js deleted file mode 100644 index fd9ee4ce3..000000000 --- a/src/KuzzleError.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -class KuzzleError extends Error { - constructor (apiError) { - super(apiError.message); - - this.status = apiError.status; - this.stack = apiError.stack; - this.id = apiError.id; - this.code = apiError.code; - - // PartialError - if (this.status === 206) { - this.errors = apiError.errors; - this.count = apiError.count; - } - } -} - -module.exports = KuzzleError; diff --git a/src/KuzzleError.ts b/src/KuzzleError.ts new file mode 100644 index 000000000..505227ac4 --- /dev/null +++ b/src/KuzzleError.ts @@ -0,0 +1,67 @@ +'use strict'; + +/** + * Standard Kuzzle error. + * + * @see https://docs.kuzzle.io/core/2/api/essentials/error-handling/ + */ +export class KuzzleError extends Error { + /** + * Http status code + */ + public status: number; + /** + * Stacktrace + */ + public stack: string; + /** + * Kuzzle stacktrace (development mode only) + */ + public kuzzleStack?: string; + /** + * Unique ID + */ + public id: string; + /** + * Code + */ + public code: number; + + /** + * Associated errors + * (PartialError only) + */ + public errors?: Array; + /** + * Number of associated errors + * (PartialError only) + */ + public count?: number; + + constructor (apiError, stack = null) { + super(apiError.message); + + this.status = apiError.status; + if (apiError.stack) { + Reflect.defineProperty(this, 'kuzzleStack', { + value: apiError.stack + }); + } + + if (stack) { + const lines = stack.split('\n'); + lines[0] += apiError.message; + lines[3] = ' 🡆 ' + lines[3].trimStart(); + this.stack = lines.join('\n'); + } + + this.id = apiError.id; + this.code = apiError.code; + + // PartialError + if (this.status === 206) { + this.errors = apiError.errors; + this.count = apiError.count; + } + } +} diff --git a/src/controllers/Auth.ts b/src/controllers/Auth.ts index 2d8ef3ce6..9ed867c18 100644 --- a/src/controllers/Auth.ts +++ b/src/controllers/Auth.ts @@ -495,7 +495,7 @@ export class AuthController extends BaseController { */ _id: string; /** - * Expiration date in UNIX micro-timestamp format (-1 if the token never expires) + * Expiration date in Epoch-millis format (-1 if the token never expires) */ expiresAt: number; /** @@ -520,5 +520,3 @@ export class AuthController extends BaseController { }); } } - -module.exports = { AuthController }; diff --git a/src/controllers/Base.ts b/src/controllers/Base.ts index b67d63f58..8115fc82a 100644 --- a/src/controllers/Base.ts +++ b/src/controllers/Base.ts @@ -40,5 +40,3 @@ export class BaseController { return this._kuzzle.query(request, options); } } - -module.exports = { BaseController }; diff --git a/src/controllers/Collection.js b/src/controllers/Collection.js deleted file mode 100644 index 97727ab6b..000000000 --- a/src/controllers/Collection.js +++ /dev/null @@ -1,151 +0,0 @@ -const { BaseController } = require('./Base'); -const { SpecificationsSearchResult } = require('../core/searchResult/Specifications'); - -class CollectionController extends BaseController { - - /** - * @param {Kuzzle} kuzzle - */ - constructor (kuzzle) { - super(kuzzle, 'collection'); - } - - create (index, collection, mappings = {}, options = {}) { - const request = { - index, - collection, - body: mappings, - action: 'create' - }; - return this.query(request, options) - .then(response => response.result); - } - - deleteSpecifications (index, collection, options = {}) { - const request = { - index, - collection, - action: 'deleteSpecifications' - }; - return this.query(request, options) - .then(response => response.result); - } - - exists (index, collection, options = {}) { - return this.query({ - index, - collection, - action: 'exists' - }, options) - .then(response => response.result); - } - - refresh (index, collection, options = {}) { - return this.query({ - index, - collection, - action: 'refresh' - }, options) - .then(response => response.result); - } - - getMapping (index, collection, options = {}) { - const request = { - index, - collection, - action: 'getMapping', - includeKuzzleMeta: options.includeKuzzleMeta || false - }; - - return this.query(request, options) - .then(response => response.result); - } - - getSpecifications (index, collection, options = {}) { - return this.query({ - index, - collection, - action: 'getSpecifications' - }, options) - .then(response => response.result); - } - - list (index, options = {}) { - const request = { - index, - action: 'list', - size: options.size || 0, - from: options.from - }; - - return this.query(request, options) - .then(response => response.result); - } - - searchSpecifications (body = {}, options = {}) { - const request = { - body, - action: 'searchSpecifications' - }; - - for (const opt of ['from', 'size', 'scroll']) { - request[opt] = options[opt]; - } - - return this.query(request, options) - .then(response => new SpecificationsSearchResult(this.kuzzle, request, options, response.result)); - } - - truncate (index, collection, options = {}) { - const request = { - index, - collection, - action: 'truncate' - }; - return this.query(request, options) - .then(response => response.result); - } - - update(index, collection, body) { - return this.query({ - index, - collection, - body, - action: 'update' - }) - .then(response => response.result); - } - - // @deprecated - updateMapping (index, collection, body, options = {}) { - return this.query({ - index, - collection, - body, - action: 'updateMapping' - }, options) - .then(response => response.result); - } - - updateSpecifications (index, collection, specifications, options = {}) { - return this.query({ - index, - collection, - body: specifications, - action: 'updateSpecifications' - }, options) - .then(response => response.result); - } - - validateSpecifications (index, collection, specifications, options = {}) { - return this.query({ - index, - collection, - body: specifications, - action: 'validateSpecifications' - }, options) - .then(response => response.result); - } -} - -module.exports = { CollectionController }; diff --git a/src/controllers/Collection.ts b/src/controllers/Collection.ts new file mode 100644 index 000000000..6f1374b22 --- /dev/null +++ b/src/controllers/Collection.ts @@ -0,0 +1,428 @@ +import { BaseController } from './Base'; +import { SpecificationsSearchResult } from '../core/searchResult/Specifications'; +import { CollectionMappings, JSONObject } from '../utils/interfaces'; + +export class CollectionController extends BaseController { + constructor (kuzzle) { + super(kuzzle, 'collection'); + } + + /** + * Creates a new collection in the provided index. + * You can also provide optional mappings and settings that allow you to exploit + * the full capabilities of our persistent data storage layer. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/create/ + * @see https://docs.kuzzle.io/core/2/guides/essentials/database-mappings/ + * + * @param index Index name + * @param collection Collection name + * @param definition Collection mappings and settings + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + create ( + index: string, + collection: string, + definition: + { + /** + * Mappings definition + */ + mappings?: CollectionMappings; + /** + * Elasticsearch index settings + */ + settings?: JSONObject + } | CollectionMappings, + options: { queuable?: boolean } = {} + ): Promise { + const request = { + index, + collection, + body: definition, + action: 'create' + }; + + return this.query(request, options) + .then(() => undefined); + } + + /** + * Deletes validation specifications for a collection. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/delete-specifications/ + * + * @param index Index name + * @param collection Collection name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + deleteSpecifications ( + index: string, + collection: string, + options: { queuable?: boolean } = {} + ): Promise { + const request = { + index, + collection, + action: 'deleteSpecifications' + }; + return this.query(request, options) + .then(() => undefined); + } + + /** + * Checks if a collection exists in Kuzzle. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/exists/ + * + * @param index Index name + * @param collection Collection name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + exists ( + index: string, + collection: string, + options: { queuable?: boolean } = {} + ): Promise { + return this.query({ + index, + collection, + action: 'exists' + }, options) + .then(response => response.result); + } + + /** + * Refreshes a collection to reindex the writed and deleted documents + * so they are available in search results. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/refresh/ + * + * @param index Index name + * @param collection Collection name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + refresh ( + index: string, + collection: string, + options: { queuable?: boolean } = {} + ): Promise { + return this.query({ + index, + collection, + action: 'refresh' + }, options) + .then(() => undefined); + } + + /** + * Returns the collection mapping. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/get-mapping/ + * + * @param index Index name + * @param collection Collection name + * @param options Additional options + * - `includeKuzzleMeta` If true, the returned mappings will contain Kuzzle metadata + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + getMapping ( + index: string, + collection: string, + options: { includeKuzzleMeta?: boolean, queuable?: boolean } = {} + ): Promise { + const request = { + index, + collection, + action: 'getMapping', + includeKuzzleMeta: options.includeKuzzleMeta || false + }; + + return this.query(request, options) + .then(response => response.result); + } + + /** + * Returns the validation specifications associated to the given index and collection. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/get-specifications/ + * + * @param index Index name + * @param collection Collection name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + * + * @returns The specifications + */ + getSpecifications ( + index: string, + collection: string, + options: { queuable?: boolean } = {} + ): Promise { + return this.query({ + index, + collection, + action: 'getSpecifications' + }, options) + .then(response => response.result); + } + + /** + * Returns the list of collections associated to a provided index. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/list/ + * + * @param index Index name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + * + * @returns An object containing the collection list + */ + list ( + index: string, + options: { + queuable?: boolean; + /** + * @deprecated + */ + from?: number; + /** + * @deprecated + */ + size?: number; + } = {} + ): Promise<{ + /** + * Types of returned collections. + */ + type: string; + /** + * List of collections + */ + collections: Array<{ + /** + * Collection name + */ + name: string; + /** + * Collection type + */ + type: 'realtime' | 'stored' + }>; + }> { + const request = { + index, + action: 'list', + size: options.size || 0, + from: options.from + }; + + return this.query(request, options) + .then(response => response.result); + } + + /** + * Searches collection specifications. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/search-specifications/ + * + * @param query Search query + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + * - `from` Offset of the first document to fetch + * - `size` Maximum number of documents to retrieve per page + * - `scroll` When set, gets a forward-only cursor having its ttl set to the given value (e.g. `30s`) + */ + searchSpecifications ( + query: JSONObject = {}, + options: { + queuable?: boolean; + from?: number; + size?: number; + scroll?: string; + } = {} + ): Promise { + const request = { + body: query, + action: 'searchSpecifications' + }; + + for (const opt of ['from', 'size', 'scroll']) { + request[opt] = options[opt]; + } + + return this.query(request, options) + .then(response => ( + new SpecificationsSearchResult(this.kuzzle, request, options, response.result) + )); + } + + + /** + * Removes all documents from a collection, while keeping the associated mappings. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/truncate/ + * + * @param index Index name + * @param collection Collection name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + truncate ( + index: string, + collection: string, + options: { queuable?: boolean } = {} + ): Promise { + const request = { + index, + collection, + action: 'truncate' + }; + return this.query(request, options) + .then(() => undefined); + } + + /** + * Updates a collection informations + * You can also provide optional mappings and settings that allow you to exploit + * the full capabilities of our persistent data storage layer. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/update/ + * @see https://docs.kuzzle.io/core/2/guides/essentials/database-mappings/ + * + * @param index Index name + * @param collection Collection name + * @param definition Collection mappings and settings + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + update ( + index: string, + collection: string, + definition: + { + /** + * Mappings definition + */ + mappings?: CollectionMappings; + /** + * Elasticsearch index settings + */ + settings?: JSONObject + } | CollectionMappings, + options: { queuable?: boolean } = {} + ): Promise { + return this.query({ + index, + collection, + body: definition, + action: 'update' + }, options) + .then(() => undefined); + } + + /** + * @deprecated Use collection.update instead + */ + updateMapping ( + index: string, + collection: string, + mappings: CollectionMappings, + options: { queuable?: boolean } = {} + ): Promise { + return this.query({ + index, + collection, + body: mappings, + action: 'updateMapping' + }, options) + .then(response => response.result); + } + + /** + * Create or updates the validation specifications for a collection. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/update-specifications/ + * + * @param index Index name + * @param collection Collection name + * @param specifications Specifications to update + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + * + * @returns The updated specifications + */ + updateSpecifications ( + index: string, + collection: string, + specifications: JSONObject, + options: { queuable?: boolean } = {} + ): Promise { + return this.query({ + index, + collection, + body: specifications, + action: 'updateSpecifications' + }, options) + .then(response => response.result); + } + + /** + * Checks if a validation specification is well formatted. + * It does not store or modify the existing specification. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/validate-specifications/ + * + * @param index Index name + * @param collection Collection name + * @param specifications Specifications to validate + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + * + * @returns An object which contain information about the specifications validity. + */ + validateSpecifications ( + index: string, + collection: string, + specifications: JSONObject, + options: { queuable?: boolean } = {} + ): Promise<{ + valid: boolean; + details: Array; + description: string; + }> { + return this.query({ + index, + collection, + body: specifications, + action: 'validateSpecifications' + }, options) + .then(response => response.result); + } + + /** + * Deletes a collection. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/collection/delete/ + * + * @param index Index name + * @param collection Collection name + */ + delete ( + index: string, + collection: string + ): Promise { + const request = { + index, + collection, + action: 'delete' + }; + + return this.query(request) + .then(() => undefined); + } +} diff --git a/src/controllers/Document.ts b/src/controllers/Document.ts index 34141a911..60a3c77fc 100644 --- a/src/controllers/Document.ts +++ b/src/controllers/Document.ts @@ -27,7 +27,7 @@ export class DocumentController extends BaseController { count ( index: string, collection: string, - body: JSONObject = null, + body?: JSONObject, options: { queuable?: boolean } = {} ): Promise { const request = { @@ -801,5 +801,3 @@ export class DocumentController extends BaseController { .then(response => response.result); } } - -module.exports = { DocumentController }; diff --git a/src/controllers/Index.js b/src/controllers/Index.js deleted file mode 100644 index 626d8c949..000000000 --- a/src/controllers/Index.js +++ /dev/null @@ -1,58 +0,0 @@ -const { BaseController } = require('./Base'); - -class IndexController extends BaseController { - - /** - * @param {Kuzzle} kuzzle - */ - constructor (kuzzle) { - super(kuzzle, 'index'); - } - - create (index, options = {}) { - const request = { - index, - action: 'create' - }; - return this.query(request, options) - .then(response => response.result); - } - - delete (index, options = {}) { - const request = { - index, - action: 'delete' - }; - return this.query(request, options) - .then(response => response.result.acknowledged); - } - - exists (index, options = {}) { - return this.query({ - index, - action : 'exists' - }, options) - .then(response => response.result); - } - - list (options) { - return this.query({ - action: 'list' - }, options) - .then(response => response.result.indexes); - } - - mDelete (indexes, options = {}) { - const request = { - action: 'mDelete', - body: { - indexes - } - }; - - return this.query(request, options) - .then(response => response.result.deleted); - } -} - -module.exports = { IndexController }; diff --git a/src/controllers/Index.ts b/src/controllers/Index.ts new file mode 100644 index 000000000..61fc13c10 --- /dev/null +++ b/src/controllers/Index.ts @@ -0,0 +1,102 @@ +import { BaseController } from './Base'; + +export class IndexController extends BaseController { + + constructor (kuzzle) { + super(kuzzle, 'index'); + } + + /** + * Creates a new index + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/index/create/ + * + * @param index Index name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + create (index: string, options: { queuable?: boolean } = {}): Promise { + const request = { + index, + action: 'create' + }; + return this.query(request, options) + .then(() => undefined); + } + + /** + * Deletes an index + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/index/delete/ + * + * @param index Index name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + delete (index: string, options: { queuable?: boolean } = {}): Promise { + const request = { + index, + action: 'delete' + }; + return this.query(request, options) + .then(() => undefined); + } + + /** + * Checks if the given index exists. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/index/exists/ + * + * @param index Index name + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + exists (index: string, options: { queuable?: boolean } = {}): Promise { + return this.query({ + index, + action : 'exists' + }, options) + .then(response => response.result); + } + + /** + * Returns the complete list of indexes. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/index/list/ + * + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + list (options: { queuable?: boolean } = {}): Promise> { + return this.query({ + action: 'list' + }, options) + .then(response => response.result.indexes); + } + + /** + * Deletes multiple indexes + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/index/m-delete/ + * + * @param indexes List of index names to delete + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + * + * @returns Names of successfully deleted indexes + */ + mDelete ( + indexes: Array, + options: { queuable?: boolean } = {} + ): Promise> { + const request = { + action: 'mDelete', + body: { + indexes + } + }; + + return this.query(request, options) + .then(response => response.result.deleted); + } +} diff --git a/src/controllers/Realtime.js b/src/controllers/Realtime.js deleted file mode 100644 index e68478ba2..000000000 --- a/src/controllers/Realtime.js +++ /dev/null @@ -1,128 +0,0 @@ -const { BaseController } = require('./Base'); -const Room = require('../core/Room'); - - -class RealtimeController extends BaseController { - /** - * @param {Kuzzle} kuzzle - */ - constructor (kuzzle) { - super(kuzzle, 'realtime'); - - this.subscriptions = new Map(); - this.subscriptionsOff = new Map(); - - this.kuzzle.on('tokenExpired', () => this.tokenExpired()); - } - - count (roomId, options = {}) { - return this.query({ - action: 'count', - body: {roomId} - }, options) - .then(response => response.result.count); - } - - publish (index, collection, message, options = {}) { - const request = { - index, - collection, - body: message, - action: 'publish', - _id: options._id - }; - - return this.query(request, options) - .then(response => response.result.published); - } - - subscribe (index, collection, filters, callback, options = {}) { - const room = new Room(this, index, collection, filters, callback, options); - - return room.subscribe() - .then(() => { - if (!this.subscriptions.has(room.id)) { - this.subscriptions.set(room.id, []); - } - this.subscriptions.get(room.id).push(room); - return room.id; - }); - } - - unsubscribe (roomId, options = {}) { - const request = { - action: 'unsubscribe', - body: { roomId } - }; - - return this.query(request, options) - .then(response => { - const rooms = this.subscriptions.get(roomId); - - if (rooms) { - for (const room of rooms) { - room.removeListeners(); - } - - this.subscriptions.delete(roomId); - } - - return response.result; - }); - } - - // called on network error or disconnection - disconnected () { - for (const roomId of this.subscriptions.keys()) { - for (const room of this.subscriptions.get(roomId)) { - room.removeListeners(); - - if (room.autoResubscribe) { - if (!this.subscriptionsOff.has(roomId)) { - this.subscriptionsOff.set(roomId, []); - } - this.subscriptionsOff.get(roomId).push(room); - } - } - - this.subscriptions.delete(roomId); - } - } - - /** - * Called on kuzzle reconnection. - * Resubscribe to eligible disabled rooms. - */ - reconnected () { - for (const roomId of this.subscriptionsOff.keys()) { - for (const room of this.subscriptionsOff.get(roomId)) { - if (!this.subscriptions.has(roomId)) { - this.subscriptions.set(roomId, []); - } - this.subscriptions.get(roomId).push(room); - - room.subscribe() - .catch(() => this.kuzzle.emit('discarded', {request: room.request})); - } - - this.subscriptionsOff.delete(roomId); - } - } - - /** - * Removes all subscriptions. - */ - tokenExpired() { - for (const roomId of this.subscriptions.keys()) { - for (const room of this.subscriptions.get(roomId)) { - room.removeListeners(); - } - } - - this.subscriptions = new Map(); - this.subscriptionsOff = new Map(); - } - -} - -module.exports = { RealtimeController }; diff --git a/src/controllers/Realtime.ts b/src/controllers/Realtime.ts new file mode 100644 index 000000000..1d1b391c2 --- /dev/null +++ b/src/controllers/Realtime.ts @@ -0,0 +1,259 @@ +import { BaseController } from './Base'; +import Room from '../core/Room'; +import { JSONObject } from '../utils/interfaces'; + +/** + * Enum for `scope` option of realtime.subscribe method + */ +export enum ScopeOption { + /** + * Receive all document notifications + */ + all = 'all', + /** + * Receive notifications when document enter or stay in the scope + */ + in = 'in', + /** + * Receive notification when document exit the scope + */ + out = 'out', + /** + * Do not receive document notifications + */ + none = 'none' +} + +/** + * Enum for `user` option of realtime.subscribe method + */ +export enum UserOption { + /** + * Receive all user notifications + */ + all = 'all', + /** + * Receive notification when users join the room + */ + in = 'in', + /** + * Receive notifications when users leave the room + */ + out = 'out', + /** + * Do not receive user notifications + */ + none = 'none' +} + +export class RealtimeController extends BaseController { + private _subscriptions: Map>; + private _subscriptionsOff: Map>; + + constructor (kuzzle) { + super(kuzzle, 'realtime'); + + this._subscriptions = new Map(); + this._subscriptionsOff = new Map(); + + this.kuzzle.on('tokenExpired', () => this.removeSubscriptions()); + this.kuzzle.on('disconnected', () => this.saveSubscriptions()); + this.kuzzle.on('networkError', () => this.saveSubscriptions()); + this.kuzzle.on('reconnected', () => this.resubscribe()); + } + + /** + * Returns the number of other connections sharing the same subscription. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/realtime/count/ + * + * @param roomId Subscription room ID + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + * + * @returns A number represensting active connections using the same provided subscription room. + */ + count (roomId: string, options: { queuable?: boolean } = {}): Promise { + return this.query({ + action: 'count', + body: { roomId } + }, options) + .then(response => response.result.count); + } + + /** + * Sends a real-time message to Kuzzle. + * + * The message will be dispatched to all clients with subscriptions + * matching the index, the collection and the message content. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/realtime/count/ + * + * @param index Index name + * @param collection Collection name + * @param message Message to send (will be put in `_source` property of the notification) + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + * - `_id` Additional unique ID (will be put in the `_id` property of the notification) + */ + publish ( + index: string, + collection: string, + message: JSONObject, + options: { queuable?: boolean, _id?: string } = {} + ): Promise { + const request = { + index, + collection, + body: message, + action: 'publish', + _id: options._id + }; + + return this.query(request, options) + .then(response => response.result.published); + } + + /** + * Subscribes by providing a set of filters: messages, document changes + * and, optionally, user events matching the provided filters will generate + * real-time notifications. + * + * @see https://docs.kuzzle.io/sdk/js/7/controllers/realtime/subscribe/ + * @see https://docs.kuzzle.io/sdk/js/7/essentials/realtime-notifications/ + * + * @param index Index name + * @param collection Collection name + * @param filters Optional subscription filters (@see https://docs.kuzzle.io/core/2/guides/cookbooks/realtime-api) + * @param callback Callback function to handle notifications + * @param options Additional options + * - `scope` Subscribe to document entering or leaving the scope. (default: 'all') + * - `users` Subscribe to users entering or leaving the room. (default: 'none') + * - `subscribeToSelf` Subscribe to notifications fired by our own queries. (default: true) + * - `volatile` Subscription information sent alongside notifications + * + * @returns A string containing the room ID + */ + subscribe ( + index: string, + collection: string, + filters: JSONObject, + callback: (notification: Notification) => void | Promise, + options: { + /** + * Subscribe to document entering or leaving the scope. (default: 'all') + */ + scope?: ScopeOption; + /** + * Subscribe to users entering or leaving the room. (default: 'none') + */ + users?: UserOption; + /** + * Subscribe to notifications fired by our own queries. (default: true) + */ + subscribeToSelf?: boolean; + /** + * Subscription information sent alongside notifications + */ + volatile?: JSONObject; + } = {} + ): Promise { + const room = new Room(this, index, collection, filters, callback, options); + + return room.subscribe() + .then(() => { + if (!this._subscriptions.has(room.id)) { + this._subscriptions.set(room.id, []); + } + this._subscriptions.get(room.id).push(room); + return room.id; + }); + } + + /** + * Removes a subscription + * + * @param roomId Subscription room ID + * @param options Additional options + * - `queuable` If true, queues the request during downtime, until connected to Kuzzle again + */ + unsubscribe ( + roomId: string, + options: { queuable?: boolean } = {} + ): Promise { + const request = { + action: 'unsubscribe', + body: { roomId } + }; + + return this.query(request, options) + .then(response => { + const rooms = this._subscriptions.get(roomId); + + if (rooms) { + for (const room of rooms) { + room.removeListeners(); + } + + this._subscriptions.delete(roomId); + } + + return response.result; + }); + } + + /** + * Called when kuzzle is disconnected + */ + private saveSubscriptions () { + for (const roomId of this._subscriptions.keys()) { + for (const room of this._subscriptions.get(roomId)) { + room.removeListeners(); + + if (room.autoResubscribe) { + if (!this._subscriptionsOff.has(roomId)) { + this._subscriptionsOff.set(roomId, []); + } + this._subscriptionsOff.get(roomId).push(room); + } + } + + this._subscriptions.delete(roomId); + } + } + + /** + * Called on kuzzle reconnection + */ + private resubscribe () { + for (const roomId of this._subscriptionsOff.keys()) { + for (const room of this._subscriptionsOff.get(roomId)) { + if (!this._subscriptions.has(roomId)) { + this._subscriptions.set(roomId, []); + } + this._subscriptions.get(roomId).push(room); + + room.subscribe() + .catch(() => this.kuzzle.emit('discarded', { request: room.request })); + } + + this._subscriptionsOff.delete(roomId); + } + } + + /** + * Called when a token expire + */ + private removeSubscriptions() { + for (const roomId of this._subscriptions.keys()) { + for (const room of this._subscriptions.get(roomId)) { + room.removeListeners(); + } + } + + this._subscriptions = new Map(); + this._subscriptionsOff = new Map(); + } +} + +module.exports = { RealtimeController }; diff --git a/src/core/KuzzleEventEmitter.js b/src/core/KuzzleEventEmitter.ts similarity index 91% rename from src/core/KuzzleEventEmitter.js rename to src/core/KuzzleEventEmitter.ts index e88ee825c..c10f03b23 100644 --- a/src/core/KuzzleEventEmitter.js +++ b/src/core/KuzzleEventEmitter.ts @@ -1,16 +1,24 @@ class Listener { + public fn: (...any) => any; + public once: boolean; + constructor(fn, once = false) { this.fn = fn; this.once = once; } } -class KuzzleEventEmitter { - constructor() { +/** + * @todo proper TS conversion + */ +export class KuzzleEventEmitter { + private _events: Map>; + + constructor () { this._events = new Map(); } - _exists (listeners, fn) { + private _exists (listeners, fn) { return Boolean(listeners.find(listener => listener.fn === fn)); } @@ -98,7 +106,7 @@ class KuzzleEventEmitter { return this; } - removeAllListeners (eventName) { + removeAllListeners (eventName?: string) { if (eventName) { this._events.delete(eventName); } diff --git a/src/core/Room.js b/src/core/Room.js index ddf0c4af4..25fd7a6ed 100644 --- a/src/core/Room.js +++ b/src/core/Room.js @@ -1,3 +1,5 @@ +'use strict'; + class Room { /** diff --git a/src/core/searchResult/Profile.js b/src/core/searchResult/Profile.js deleted file mode 100644 index ac7de860b..000000000 --- a/src/core/searchResult/Profile.js +++ /dev/null @@ -1,29 +0,0 @@ -const { Profile } = require('../security/Profile'); -const { SearchResultBase } = require('./SearchResultBase'); - -class ProfileSearchResult extends SearchResultBase { - constructor (kuzzle, request, options, response) { - super(kuzzle, request, options, response); - - this._searchAction = 'searchProfiles'; - this._scrollAction = 'scrollProfiles'; - this.hits = response.hits.map( - hit => new Profile(this._kuzzle, hit._id, hit._source)); - } - - next () { - return super.next() - .then(nextSearchResult => { - if (! nextSearchResult) { - return null; - } - - nextSearchResult.hits = nextSearchResult._response.hits.map( - hit => new Profile(nextSearchResult._kuzzle, hit._id, hit._source)); - - return nextSearchResult; - }); - } -} - -module.exports = { ProfileSearchResult }; diff --git a/src/core/searchResult/Profile.ts b/src/core/searchResult/Profile.ts new file mode 100644 index 000000000..08d9e131c --- /dev/null +++ b/src/core/searchResult/Profile.ts @@ -0,0 +1,29 @@ +import { Profile } from '../security/Profile'; +import { SearchResultBase } from './SearchResultBase'; + +export class ProfileSearchResult extends SearchResultBase { + constructor (kuzzle, request, options, response) { + super(kuzzle, request, options, response); + + this._searchAction = 'searchProfiles'; + this._scrollAction = 'scrollProfiles'; + this.hits = response.hits.map(hit => ( + new Profile(this._kuzzle, hit._id, hit._source) + )); + } + + next () { + return super.next() + .then((nextSearchResult: ProfileSearchResult) => { + if (! nextSearchResult) { + return null; + } + + nextSearchResult.hits = nextSearchResult._response.hits.map(hit => ( + new Profile(nextSearchResult._kuzzle, hit._id, hit._source) + )); + + return nextSearchResult; + }); + } +} diff --git a/src/core/searchResult/Role.js b/src/core/searchResult/Role.ts similarity index 63% rename from src/core/searchResult/Role.js rename to src/core/searchResult/Role.ts index 241e310da..4cbbb48aa 100644 --- a/src/core/searchResult/Role.js +++ b/src/core/searchResult/Role.ts @@ -1,7 +1,7 @@ -const { Role } = require('../security/Role'); -const { SearchResultBase } = require('./SearchResultBase'); +import { Role } from '../security/Role'; +import { SearchResultBase } from './SearchResultBase'; -class RoleSearchResult extends SearchResultBase { +export class RoleSearchResult extends SearchResultBase { constructor (kuzzle, query, options, response) { super(kuzzle, query, options, response); @@ -9,7 +9,9 @@ class RoleSearchResult extends SearchResultBase { this._searchAction = 'searchRoles'; this._scrollAction = null; // scrollRoles action does not exists in Kuzzle API. - this.hits = this._response.hits.map(hit => new Role(this._kuzzle, hit._id, hit._source.controllers)); + this.hits = this._response.hits.map(hit => ( + new Role(this._kuzzle, hit._id, hit._source.controllers) + )); } next () { @@ -20,16 +22,16 @@ class RoleSearchResult extends SearchResultBase { } return super.next() - .then(nextSearchResult => { + .then((nextSearchResult: RoleSearchResult) => { if (! nextSearchResult) { return null; } - nextSearchResult.hits = nextSearchResult._response.hits.map(hit => new Role(nextSearchResult._kuzzle, hit._id, hit._source.controllers)); + nextSearchResult.hits = nextSearchResult._response.hits.map(hit => ( + new Role(nextSearchResult._kuzzle, hit._id, hit._source.controllers) + )); return nextSearchResult; }); } } - -module.exports = { RoleSearchResult }; diff --git a/src/core/searchResult/SearchResultBase.ts b/src/core/searchResult/SearchResultBase.ts index a6c2da445..c48ea9c71 100644 --- a/src/core/searchResult/SearchResultBase.ts +++ b/src/core/searchResult/SearchResultBase.ts @@ -55,17 +55,10 @@ export class SearchResultBase implements SearchResult { public fetched: number; - /** - * - * @param {Kuzzle} kuzzle - * @param {object} request - * @param {object} options - * @param {object} response - */ constructor ( kuzzle: Kuzzle, request: KuzzleRequest = {}, - options: any = {}, + options: JSONObject = {}, response: any = {} ) { Reflect.defineProperty(this, '_kuzzle', { @@ -80,10 +73,18 @@ export class SearchResultBase implements SearchResult { Reflect.defineProperty(this, '_response', { value: response }); - - this._controller = request.controller; - this._searchAction = 'search'; - this._scrollAction = 'scroll'; + Reflect.defineProperty(this, '_controller', { + value: request.controller, + writable: true + }); + Reflect.defineProperty(this, '_searchAction', { + value: 'search', + writable: true + }); + Reflect.defineProperty(this, '_scrollAction', { + value: 'scroll', + writable: true + }); this.aggregations = response.aggregations; this.hits = response.hits || []; @@ -166,7 +167,7 @@ export class SearchResultBase implements SearchResult { return Promise.reject(new Error('Unable to retrieve next results from search: missing scrollId, from/sort, or from/size params')); } - _get (object, path) { + protected _get (object, path) { if (!object) { return object; } @@ -179,7 +180,7 @@ export class SearchResultBase implements SearchResult { return this._get(object[key], path); } - _buildNextSearchResult (response) { + protected _buildNextSearchResult (response) { const Constructor: any = this.constructor; const nextSearchResult = new Constructor(this._kuzzle, this._request, this._options, response.result); @@ -187,8 +188,4 @@ export class SearchResultBase implements SearchResult { return nextSearchResult; } - } - - -module.exports = { SearchResultBase }; diff --git a/src/core/searchResult/Specifications.js b/src/core/searchResult/Specifications.ts similarity index 56% rename from src/core/searchResult/Specifications.js rename to src/core/searchResult/Specifications.ts index 15d2d2410..54b21df0f 100644 --- a/src/core/searchResult/Specifications.js +++ b/src/core/searchResult/Specifications.ts @@ -1,6 +1,7 @@ -const { SearchResultBase } = require('./SearchResultBase'); +import { SearchResultBase } from './SearchResultBase'; +import { JSONObject } from '../../utils/interfaces'; -class SpecificationsSearchResult extends SearchResultBase { +export class SpecificationsSearchResult extends SearchResultBase { constructor (kuzzle, query, options, response) { super(kuzzle, query, options, response); @@ -10,5 +11,3 @@ class SpecificationsSearchResult extends SearchResultBase { this._scrollAction = 'scrollSpecifications'; } } - -module.exports = { SpecificationsSearchResult }; diff --git a/src/core/searchResult/User.js b/src/core/searchResult/User.js deleted file mode 100644 index 20ec93dcb..000000000 --- a/src/core/searchResult/User.js +++ /dev/null @@ -1,27 +0,0 @@ -const { SearchResultBase } = require('./SearchResultBase'); -const { User } = require('../security/User'); - -class UserSearchResult extends SearchResultBase { - - constructor (kuzzle, query, options, response) { - super(kuzzle, query, options, response); - - this._searchAction = 'searchUsers'; - this._scrollAction = 'scrollUsers'; - this.hits = this._response.hits.map(hit => new User(this._kuzzle, hit._id, hit._source)); - } - - next () { - return super.next() - .then(nextSearchResult => { - if (! nextSearchResult) { - return null; - } - - nextSearchResult.hits = nextSearchResult._response.hits.map(hit => new User(nextSearchResult._kuzzle, hit._id, hit._source)); - return nextSearchResult; - }); - } -} - -module.exports = { UserSearchResult }; diff --git a/src/core/searchResult/User.ts b/src/core/searchResult/User.ts new file mode 100644 index 000000000..501b88b71 --- /dev/null +++ b/src/core/searchResult/User.ts @@ -0,0 +1,30 @@ +import { SearchResultBase } from './SearchResultBase'; +import { User } from '../security/User'; + +export class UserSearchResult extends SearchResultBase { + + constructor (kuzzle, query, options, response) { + super(kuzzle, query, options, response); + + this._searchAction = 'searchUsers'; + this._scrollAction = 'scrollUsers'; + this.hits = this._response.hits.map(hit => ( + new User(this._kuzzle, hit._id, hit._source) + )); + } + + next () { + return super.next() + .then((nextSearchResult: UserSearchResult) => { + if (! nextSearchResult) { + return null; + } + + nextSearchResult.hits = nextSearchResult._response.hits.map(hit => ( + new User(nextSearchResult._kuzzle, hit._id, hit._source) + )); + + return nextSearchResult; + }); + } +} diff --git a/src/core/security/Profile.ts b/src/core/security/Profile.ts index 12e18010a..044844635 100644 --- a/src/core/security/Profile.ts +++ b/src/core/security/Profile.ts @@ -52,6 +52,3 @@ export class Profile { options); } } - -module.exports = { Profile }; - diff --git a/src/protocols/Http.js b/src/protocols/Http.ts similarity index 77% rename from src/protocols/Http.js rename to src/protocols/Http.ts index e7f1d86fe..8525c9bfa 100644 --- a/src/protocols/Http.js +++ b/src/protocols/Http.ts @@ -1,10 +1,40 @@ 'use strict'; -const staticHttpRoutes = require('./routes.json'); -const { KuzzleAbstractProtocol } = require('./abstract/Base'); +import staticHttpRoutes from './routes.json'; +import { KuzzleAbstractProtocol } from './abstract/Base'; +import { HttpRoutes, JSONObject, KuzzleRequest } from '../utils/interfaces'; + +/** + * Http protocol used to connect to a Kuzzle server. + * + * The Http protocol cannot use the realtime capabilities of Kuzzle. + */ +export default class HttpProtocol extends KuzzleAbstractProtocol { + private _routes: HttpRoutes; + private _timeout: number; + private _customRoutes: HttpRoutes; -class HttpProtocol extends KuzzleAbstractProtocol { - constructor(host, options = {}) { + /** + * @param host Kuzzle server hostname or IP + * @param options Http connection options + * - `customRoutes` Add custom routes + * - `port` Kuzzle server port (default: `7512`) + * - `ssl` Use SSL to connect to Kuzzle server. Default `false` unless port is 443 or 7443. + * - `timeout` Connection timeout in milliseconds (default: `0`) + */ + constructor( + host: string, + options: { + port?: number; + /** + * @deprecated Use `ssl` instead + */ + sslConnection?: boolean; + ssl?: boolean; + customRoutes?: HttpRoutes; + timeout?: number + } = {} + ) { super(host, options, 'http'); if (typeof host !== 'string' || host === '') { @@ -15,10 +45,10 @@ class HttpProtocol extends KuzzleAbstractProtocol { this._timeout = options.timeout || 0; - this.customRoutes = options.customRoutes || {}; + this._customRoutes = options.customRoutes || {}; - for (const controller of Object.keys(this.customRoutes)) { - const definition = this.customRoutes[controller]; + for (const controller of Object.keys(this._customRoutes)) { + const definition = this._customRoutes[controller]; for (const action of Object.keys(definition)) { const route = definition[action]; @@ -35,35 +65,49 @@ class HttpProtocol extends KuzzleAbstractProtocol { } } - // @deprecated + /** + * @deprecated Use `routes` instead + */ get http () { return this.routes; } - get routes () { + /** + * Returns a list of available routes + */ + get routes (): HttpRoutes { return this._routes; } - get protocol () { + /** + * `http` or `https` + */ + get protocol (): string { return this.ssl ? 'https' : 'http'; } - get connected () { + /** + * Always returns `true` + */ + get connected (): true { return true; } - get timeout () { + /** + * Connection timeout in milliseconds + */ + get timeout (): number { return this._timeout; } - set timeout (timeout) { + set timeout (timeout: number) { this._timeout = timeout; } /** * Connect to the server */ - connect () { + connect (): Promise { if (this.state === 'ready') { return Promise.resolve(); } @@ -92,7 +136,6 @@ class HttpProtocol extends KuzzleAbstractProtocol { .then(({ result: res, error: err }) => { if (! err) { this._routes = this._constructRoutes(res.serverInfo.kuzzle.api.routes); - this._staticRoutes = false; return; } @@ -114,13 +157,13 @@ class HttpProtocol extends KuzzleAbstractProtocol { throw error; }) .then(() => { - this._routes = Object.assign(this._routes, this.customRoutes); + this._routes = Object.assign(this._routes, this._customRoutes); // Client is ready this.clientConnected(); }) .catch(err => { - const connectionError = new Error(`Unable to connect to kuzzle server at ${this.host}:${this.port}`); + const connectionError: any = new Error(`Unable to connect to kuzzle server at ${this.host}:${this.port}`); connectionError.internal = err; this.emit('networkError', connectionError); @@ -134,19 +177,19 @@ class HttpProtocol extends KuzzleAbstractProtocol { * @param {Object} data * @returns {Promise} */ - send (data, options = {}) { - const route = this.routes[data.controller] - && this.routes[data.controller][data.action]; + send (request: KuzzleRequest, options: JSONObject = {}) { + const route = this.routes[request.controller] + && this.routes[request.controller][request.action]; if (! route) { - const error = new Error(`No URL found for "${data.controller}:${data.action}".`); - this.emit(data.requestId, { status: 400, error }); + const error = new Error(`No URL found for "${request.controller}:${request.action}".`); + this.emit(request.requestId, { status: 400, error }); return; } const method = options.verb || route.verb; - const payload = { + const payload: any = { action: undefined, body: undefined, collection: undefined, @@ -160,8 +203,8 @@ class HttpProtocol extends KuzzleAbstractProtocol { }; const queryArgs = {}; - for (const key of Object.keys(data)) { - const value = data[key]; + for (const key of Object.keys(request)) { + const value = request[key]; if (key === 'body') { if (method === 'GET') { @@ -191,7 +234,7 @@ class HttpProtocol extends KuzzleAbstractProtocol { let matches = regex.exec(url); while (matches) { - const urlParam = data[ matches[1] ]; + const urlParam = request[ matches[1] ]; // check if an url param is missing (eg: "/:index/_create) if (!urlParam) { @@ -201,9 +244,9 @@ class HttpProtocol extends KuzzleAbstractProtocol { return; } - url = url.replace(regex, `/${encodeURIComponent(data[matches[1]])}`); + url = url.replace(regex, `/${encodeURIComponent(request[matches[1]])}`); - delete(queryArgs[ matches[1] ]); + delete queryArgs[matches[1]]; matches = regex.exec(url); } @@ -242,10 +285,11 @@ class HttpProtocol extends KuzzleAbstractProtocol { .catch(error => this.emit(payload.requestId, {error})); } - _sendHttpRequest (method, path, payload = {}) { + _sendHttpRequest (method, path, payload: any = {}) { if (typeof XMLHttpRequest === 'undefined') { // NodeJS implementation, using http.request: + // eslint-disable-next-line @typescript-eslint/no-var-requires const httpClient = require('min-req-promise'); if (path[0] !== '/') { @@ -328,8 +372,8 @@ class HttpProtocol extends KuzzleAbstractProtocol { return routes; }, {}); - for (const controller of Object.keys(this.customRoutes)) { - apiRoutes[controller] = this.customRoutes[controller]; + for (const controller of Object.keys(this._customRoutes)) { + apiRoutes[controller] = this._customRoutes[controller]; } return apiRoutes; @@ -371,5 +415,3 @@ function getCorrectRoute (routes) { // will be in the query string return sameLength ? getRoute : shortestRoute; } - -module.exports = HttpProtocol; diff --git a/src/protocols/WebSocket.js b/src/protocols/WebSocket.ts similarity index 66% rename from src/protocols/WebSocket.js rename to src/protocols/WebSocket.ts index f2d05e1fb..d1438cda3 100644 --- a/src/protocols/WebSocket.js +++ b/src/protocols/WebSocket.ts @@ -1,11 +1,62 @@ 'use strict'; -const KuzzleError = require('../KuzzleError'); -const BaseProtocolRealtime = require('./abstract/Realtime'); +import { KuzzleError } from '../KuzzleError'; +import { BaseProtocolRealtime } from './abstract/Realtime'; +import { JSONObject, KuzzleRequest } from '../utils/interfaces'; + +/** + * WebSocket protocol used to connect to a Kuzzle server. + */ +export default class WebSocketProtocol extends BaseProtocolRealtime { + private WebSocketClient: any; + private options: any; + private client: any; + private lasturl: any; -class WebSocketProtocol extends BaseProtocolRealtime { + /** + * Automatically reconnect after a connection loss + */ + public autoReconnect: boolean; + /** + * `true` if the socket is open + */ + public connected: boolean; + /** + * Kuzzle server host or IP + */ + public host: string; + /** + * Kuzzle server port + */ + public port: number; + /** + * `true` if ssl is active + */ + public ssl: boolean; - constructor(host, options = {}) { + /** + * @param host Kuzzle server hostname or IP + * @param options WebSocket connection options + * - `autoReconnect` Automatically reconnect to kuzzle after a `disconnected` event. (default: `true`) + * - `port` Kuzzle server port (default: `7512`) + * - `headers` Connection custom HTTP headers (Not supported by browsers) + * - `reconnectionDelay` Number of milliseconds between reconnection attempts (default: `1000`) + * - `ssl` Use SSL to connect to Kuzzle server. Default `false` unless port is 443 or 7443. + */ + constructor( + host: string, + options: { + autoReconnect?: boolean; + port?: number; + headers?: JSONObject; + reconnectionDelay?: number; + /** + * @deprecated Use `ssl` instead + */ + sslConnection?: boolean; + ssl?: boolean; + } = {} + ) { super(host, options, 'ws'); if (typeof host !== 'string' || host === '') { @@ -39,7 +90,7 @@ class WebSocketProtocol extends BaseProtocolRealtime { /** * Connect to the websocket server */ - connect () { + connect (): Promise { return new Promise((resolve, reject) => { const url = `${this.ssl ? 'wss' : 'ws'}://${this.host}:${this.port}`; @@ -79,7 +130,7 @@ class WebSocketProtocol extends BaseProtocolRealtime { // do not forward a connection close error if no // connection has been previously established else if (this.wasConnected) { - const error = new Error(reason); + const error: any = new Error(reason); error.status = status; this.clientNetworkError(error); @@ -127,9 +178,9 @@ class WebSocketProtocol extends BaseProtocolRealtime { * * @param {Object} payload */ - send (payload) { + send (request: KuzzleRequest) { if (this.client && this.client.readyState === this.client.OPEN) { - this.client.send(JSON.stringify(payload)); + this.client.send(JSON.stringify(request)); } } @@ -148,5 +199,3 @@ class WebSocketProtocol extends BaseProtocolRealtime { super.close(); } } - -module.exports = WebSocketProtocol; diff --git a/src/protocols/abstract/Base.js b/src/protocols/abstract/Base.ts similarity index 63% rename from src/protocols/abstract/Base.js rename to src/protocols/abstract/Base.ts index 258c6082e..ec60e6cc1 100644 --- a/src/protocols/abstract/Base.js +++ b/src/protocols/abstract/Base.ts @@ -1,12 +1,26 @@ 'use strict'; -const KuzzleError = require('../../KuzzleError'); -const { uuidv4 } = require('../../utils/uuidv4'); -const { KuzzleEventEmitter } = require('../../core/KuzzleEventEmitter'); -const PendingRequest = require('./PendingRequest'); - -class KuzzleAbstractProtocol extends KuzzleEventEmitter { - constructor (host, options = {}, name = undefined) { +import { KuzzleError } from '../../KuzzleError'; +import { uuidv4 } from '../../utils/uuidv4'; +import { KuzzleEventEmitter } from '../../core/KuzzleEventEmitter'; +import { PendingRequest } from './PendingRequest'; +import { KuzzleRequest, JSONObject } from '../../utils/interfaces'; + +/** + * @todo proper TS conversion + */ +export abstract class KuzzleAbstractProtocol extends KuzzleEventEmitter { + private _pendingRequests: Map; + private _host: string; + private _name: string; + private _port: number; + private _ssl: boolean; + + public id: string; + + public state: string; + + constructor (host: string, options: JSONObject = {}, name: string = undefined) { super(); this._pendingRequests = new Map(); @@ -14,7 +28,23 @@ class KuzzleAbstractProtocol extends KuzzleEventEmitter { this._name = name; const port = parseInt(options.port, 10); this._port = isNaN(port) ? 7512 : port; - this._ssl = typeof options.sslConnection === 'boolean' ? options.sslConnection : false; + + if (options.ssl !== undefined && options.sslConnection !== undefined) { + throw new Error('Both "ssl" and "sslConnection" options are set. Use only "ssl".'); + } + + if (typeof options.ssl === 'boolean') { + this._ssl = options.ssl; + } + else if (typeof options.sslConnection === 'boolean') { + this._ssl = options.sslConnection; + } + else if (port === 443 || port === 7443) { + this._ssl = true; + } + else { + this._ssl = false; + } this.id = uuidv4(); this.state = 'offline'; @@ -52,27 +82,14 @@ class KuzzleAbstractProtocol extends KuzzleEventEmitter { return this._pendingRequests; } - /** - * @abstract - * @returns {Promise} - */ - connect () { - throw new Error('Method "connect" is not implemented'); - } + abstract connect (): Promise - /** - * @abstract - * @param request - * @returns {Promise} - */ - send () { - throw new Error('Method "send" is not implemented'); - } + abstract send (request: KuzzleRequest, options: JSONObject): void /** * Called when the client's connection is established */ - clientConnected (state, wasConnected) { + clientConnected (state?: string, wasConnected?: boolean) { this.state = state || 'ready'; this.emit(wasConnected && 'reconnect' || 'connect'); } @@ -92,6 +109,8 @@ class KuzzleAbstractProtocol extends KuzzleEventEmitter { Discarded request: ${JSON.stringify(request)}`)); } + const stack = Error().stack; + const pending = new PendingRequest(request); this._pendingRequests.set(request.requestId, pending); @@ -99,7 +118,7 @@ Discarded request: ${JSON.stringify(request)}`)); this._pendingRequests.delete(request.requestId); if (response.error) { - const error = new KuzzleError(response.error); + const error = new KuzzleError(response.error, stack); this.emit('queryError', error, request); @@ -136,7 +155,4 @@ Discarded request: ${JSON.stringify(request)}`)); this._pendingRequests.clear(); } - } - -module.exports = { KuzzleAbstractProtocol }; diff --git a/src/protocols/abstract/PendingRequest.js b/src/protocols/abstract/PendingRequest.js index 852ea9a0b..73aa5dfbd 100644 --- a/src/protocols/abstract/PendingRequest.js +++ b/src/protocols/abstract/PendingRequest.js @@ -20,4 +20,4 @@ class PendingRequest { } } -module.exports = PendingRequest; +module.exports = { PendingRequest }; diff --git a/src/protocols/abstract/Realtime.js b/src/protocols/abstract/Realtime.ts similarity index 72% rename from src/protocols/abstract/Realtime.js rename to src/protocols/abstract/Realtime.ts index 0dab94019..74bb34cc0 100644 --- a/src/protocols/abstract/Realtime.js +++ b/src/protocols/abstract/Realtime.ts @@ -1,10 +1,16 @@ 'use strict'; -const { KuzzleAbstractProtocol } = require('./Base'); +import { KuzzleAbstractProtocol } from './Base'; -class BaseProtocolRealtime extends KuzzleAbstractProtocol { - constructor (host, options = {}) { - super(host, options); +export abstract class BaseProtocolRealtime extends KuzzleAbstractProtocol { + protected _autoReconnect: boolean; + protected _reconnectionDelay: number; + protected wasConnected: boolean; + protected stopRetryingToConnect: boolean; + protected retrying: boolean; + + constructor (host, options: any = {}, name: string) { + super(host, options, name); this._autoReconnect = typeof options.autoReconnect === 'boolean' ? options.autoReconnect : true; this._reconnectionDelay = typeof options.reconnectionDelay === 'number' ? options.reconnectionDelay : 1000; @@ -18,18 +24,23 @@ class BaseProtocolRealtime extends KuzzleAbstractProtocol { return this._autoReconnect; } - get reconnectionDelay () { + /** + * Number of milliseconds between reconnection attempts + */ + get reconnectionDelay (): number { return this._reconnectionDelay; } - connect() { + connect (): Promise { this.state = 'connecting'; + + return Promise.resolve(); } /** * Called when the client's connection is established */ - clientConnected() { + clientConnected () { super.clientConnected('connected', this.wasConnected); this.state = 'connected'; @@ -40,7 +51,7 @@ class BaseProtocolRealtime extends KuzzleAbstractProtocol { /** * Called when the client's connection is closed */ - clientDisconnected() { + clientDisconnected () { this.clear(); this.emit('disconnect'); } @@ -54,8 +65,7 @@ class BaseProtocolRealtime extends KuzzleAbstractProtocol { this.state = 'offline'; this.clear(); - const connectionError = new Error(`Unable to connect to kuzzle server at ${this.host}:${this.port}`); - connectionError.internal = error; + const connectionError = new Error(`Unable to connect to kuzzle server at ${this.host}:${this.port}: ${error.message} (ws status=${error.status})`); this.emit('networkError', connectionError); @@ -90,5 +100,3 @@ class BaseProtocolRealtime extends KuzzleAbstractProtocol { return this.state === 'connected'; } } - -module.exports = BaseProtocolRealtime; diff --git a/src/protocols/index.js b/src/protocols/index.js deleted file mode 100644 index 1e39ea546..000000000 --- a/src/protocols/index.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -const Http = require('./Http'); -const WebSocket = require('./WebSocket'); - -module.exports = { - Http, - WebSocket -}; diff --git a/src/protocols/index.ts b/src/protocols/index.ts new file mode 100644 index 000000000..51c13cd4a --- /dev/null +++ b/src/protocols/index.ts @@ -0,0 +1,2 @@ +export { default as WebSocket } from './WebSocket'; +export { default as Http } from './Http'; diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index 6062231a3..cfb3d9ae3 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -21,6 +21,31 @@ export interface KuzzleRequest extends JSONObject { jwt?: string; volatile?: JSONObject; body?: JSONObject; + [key: string]: any; +} + +/** + * Kuzzle API response + * + * @see https://docs.kuzzle.io/core/2/api/essentials/kuzzle-response/ + */ +export interface KuzzleResponse extends JSONObject { + controller: string; + action: string; + index?: string; + collection?: string; + error?: { + id: string; + code: number; + message: string; + status: number; + stack?: string; + }; + requestId: string; + result: JSONObject | null; + status: number; + volatile?: JSONObject; + room?: string; } /** @@ -88,8 +113,14 @@ export interface ProfilePolicy { * @see https://docs.kuzzle.io/core/2/guides/essentials/security#defining-roles */ export interface RoleRightsDefinition { + /** + * API controller name + */ [key: string]: { actions: { + /** + * API action name + */ [key: string]: boolean } } @@ -112,7 +143,7 @@ export interface ApiKey { */ userId: string; /** - * Expiration date in UNIX micro-timestamp format (-1 if the token never expires) + * Expiration date in Epoch-millis format (-1 if the token never expires) */ expiresAt: number; /** @@ -178,6 +209,11 @@ export interface Document { /** * Document retrieved from a search + * + * @property _id + * @property _version + * @property _source + * @property _score */ export interface DocumentHit extends Document { /** @@ -185,3 +221,214 @@ export interface DocumentHit extends Document { */ _score: number; } + +export interface MappingsProperties { + /** + * Properties types definition + * + * @see https://docs.kuzzle.io/core/2/guides/essentials/database-mappings/#properties-types-definition + */ + properties?: MappingsProperties, + /** + * Dynamic mapping policy + * + * @see https://docs.kuzzle.io/core/2/guides/essentials/database-mappings/#dynamic-mapping-policy + */ + dynamic?: 'true' | 'false' | 'strict' | boolean +} + +/** + * Collection mappings definition + * + * @see https://docs.kuzzle.io/core/2/guides/essentials/database-mappings/ + */ +export interface CollectionMappings { + /** + * Collection metadata + * + * @see https://docs.kuzzle.io/core/2/guides/essentials/database-mappings/#collection-metadata + */ + _meta?: JSONObject; + /** + * Properties types definition + * + * @see https://docs.kuzzle.io/core/2/guides/essentials/database-mappings/#properties-types-definition + */ + properties?: MappingsProperties, + /** + * Dynamic mapping policy + * + * @see https://docs.kuzzle.io/core/2/guides/essentials/database-mappings/#dynamic-mapping-policy + */ + dynamic?: 'true' | 'false' | 'strict' | boolean, +} + +/** + * Enum for notification types + */ +export enum ENotificationType { + document = 'document', + user = 'user', + TokenExpired = 'TokenExpired' +} + +/** + * Real-time notifications sent by Kuzzle. + * + */ +export interface Notification { + /** + * Notification type + */ + type: ENotificationType; +} + +export interface BaseNotification extends Notification { + /** + * Controller that triggered the notification + */ + controller: string; + /** + * Action that triggered the notification + */ + action: string; + /** + * Index name + */ + index: string; + /** + * Collection name + */ + collection: string; + /** + * Network protocol used to trigger the notification + */ + protocol: string; + /** + * Subscription channel identifier. + * Can be used to link a notification to its corresponding subscription + */ + room: string; + /** + * Timestamp of the event, in Epoch-millis format + */ + timestamp: number; + /** + * Request volatile data + * @see https://docs.kuzzle.io/core/2/guides/essentials/volatile-data/ + */ + volatile: JSONObject; +} + +/** + * State of the document regarding the scope + */ +export enum EDocumentScope { + /** + * Document enters or stays in the scope + */ + in = 'in', + /** + * Document exit the scope + */ + out = 'out' +} + +/** + * Notification triggered by a document change. + * (create, update, delete) + */ +export interface DocumentNotification extends BaseNotification { + /** + * Updated document that triggered the notification + */ + result: Document; + /** + * State of the document regarding the scope (`in` or `out`) + */ + scope: EDocumentScope; + + type: ENotificationType.document; +} + +/** + * Tells wether an user leave or join the subscription room + */ +export enum EUserScope { + /** + * User enters the subscription room + */ + in = 'in', + /** + * User leaves the subscription room + */ + out = 'out' +} + +/** + * Notification triggered by an user joining or leaving a subscription room + */ +export interface UserNotification extends BaseNotification { + /** + * Tell wether an user leave or join the subscription room (`in` or `out`) + */ + user: EUserScope; + + /** + * Contains the actual number of users in the subscription room + */ + result: { + /** + * Updated users count sharing the same subscription room + */ + count: number; + } + + type: ENotificationType.user; +} + +export interface ServerNotification extends BaseNotification { + /** + * Server message explaining why this notifications has been triggered + */ + message: string; + + type: ENotificationType.TokenExpired; +} + +/** + * HTTP routes definition format + * @example + * { + * : { + * : { verb: , url: } + * } + * } + * + * { + * 'my-plugin/my-controller': { + * action: { verb: 'GET', url: '/some/url' }, + * action2: { verb: 'GET', url: '/some/url/with/:parameter' } + * } + * } + */ +export interface HttpRoutes { + /** + * Controller name + */ + [key: string]: { + /** + * Action name + */ + [key: string]: { + /** + * HTTP verb + */ + verb: string, + /** + * URL + */ + url: string + } + } +} diff --git a/test/controllers/collection.test.js b/test/controllers/collection.test.js index 03e83489f..ae9ba248a 100644 --- a/test/controllers/collection.test.js +++ b/test/controllers/collection.test.js @@ -16,7 +16,7 @@ describe('Collection Controller', () => { }); describe('create', () => { - it('should call collection/create query and return a Promise which resolves an acknowledgement', () => { + it('should call collection/create query and return a Promise which resolves', () => { kuzzle.query.resolves({result: {acknowledged: true}}); return kuzzle.collection.create('index', 'collection', null, options) @@ -31,7 +31,7 @@ describe('Collection Controller', () => { collection: 'collection' }, options); - should(res.acknowledged).be.a.Boolean().and.be.true(); + should(res).be.undefined(); }); }); @@ -50,13 +50,13 @@ describe('Collection Controller', () => { collection: 'collection' }, options); - should(res.acknowledged).be.a.Boolean().and.be.true(); + should(res).be.undefined(); }); }); }); describe('deleteSpecifications', () => { - it('should call collection/deleteSpecifications query and return a Promise which resolves an acknowledgement', () => { + it('should call collection/deleteSpecifications query and return a Promise which resolves', () => { kuzzle.query.resolves({result: {acknowledged: true}}); return kuzzle.collection.deleteSpecifications('index', 'collection', options) @@ -70,7 +70,7 @@ describe('Collection Controller', () => { collection: 'collection' }, options); - should(res.acknowledged).be.a.Boolean().and.be.true(); + should(res).be.undefined(); }); }); }); @@ -110,7 +110,7 @@ describe('Collection Controller', () => { collection: 'collection' }, options); - should(res).be.Null(); + should(res).be.undefined(); }); }); }); @@ -301,7 +301,7 @@ describe('Collection Controller', () => { collection: 'collection' }, options); - should(res.acknowledged).be.a.Boolean().and.be.true(); + should(res).be.undefined(); }); }); }); @@ -345,7 +345,7 @@ describe('Collection Controller', () => { collection: 'collection' }); - should(res).match({ foo: 'bar' }); + should(res).be.undefined(); }); }); }); @@ -403,4 +403,22 @@ describe('Collection Controller', () => { }); }); }); + + describe('delete', () => { + it('should call collection/delete query and return a promise which resolves an acknowledgement', () => { + kuzzle.query.resolves({result: {acknowledged: true}}); + return kuzzle.collection.delete('index', 'collection') + .then(res => { + should(kuzzle.query) + .be.calledOnce() + .be.calledWith({ + controller: 'collection', + action: 'delete', + index: 'index', + collection: 'collection', + }); + should(res).be.undefined(); + }); + }); + }); }); diff --git a/test/controllers/index.test.js b/test/controllers/index.test.js index 409a9ed13..8bd2e901a 100644 --- a/test/controllers/index.test.js +++ b/test/controllers/index.test.js @@ -33,8 +33,7 @@ describe('Index Controller', () => { index: 'index' }, options); - should(res.acknowledged).be.a.Boolean().and.be.true(); - should(res.shards_acknowledged).be.a.Boolean().and.be.true(); + should(res).be.undefined(); }); }); }); @@ -55,7 +54,7 @@ describe('Index Controller', () => { index: 'index' }, options); - should(res).be.a.Boolean().and.be.true(); + should(res).be.undefined(); }); }); }); diff --git a/test/controllers/realtime.test.js b/test/controllers/realtime.test.js index c264a24cb..698ecd767 100644 --- a/test/controllers/realtime.test.js +++ b/test/controllers/realtime.test.js @@ -26,17 +26,52 @@ describe('Realtime Controller', () => { }); describe('on: tokenExpired', () => { - it('should call tokenExpired() method', () => { - kuzzle.realtime.tokenExpired = sinon.stub(); + it('should call removeSubscriptions() method', () => { + kuzzle.realtime.removeSubscriptions = sinon.stub(); kuzzle.emit('tokenExpired'); process.nextTick(() => { - should(kuzzle.realtime.tokenExpired).be.called(); + should(kuzzle.realtime.removeSubscriptions).be.called(); }); }); }); + describe('on: disconnected', () => { + it('should call saveSubscriptions() method', () => { + kuzzle.realtime.saveSubscriptions = sinon.stub(); + + kuzzle.emit('disconnected'); + + process.nextTick(() => { + should(kuzzle.realtime.saveSubscriptions).be.called(); + }); + }); + }); + + describe('on: networkError', () => { + it('should call saveSubscriptions() method', () => { + kuzzle.realtime.saveSubscriptions = sinon.stub(); + + kuzzle.emit('networkError'); + + process.nextTick(() => { + should(kuzzle.realtime.saveSubscriptions).be.called(); + }); + }); + }); + + describe('on: reconnected', () => { + it('should call resubscribe() method', () => { + kuzzle.realtime.resubscribe = sinon.stub(); + + kuzzle.emit('reconnected'); + + process.nextTick(() => { + should(kuzzle.realtime.resubscribe).be.called(); + }); + }); + }); describe('#count', () => { it('should call realtime/count query with the roomId and return a Promise which resolves a number', () => { @@ -139,17 +174,17 @@ describe('Realtime Controller', () => { body = {foo: 'bar'}, cb = sinon.stub(); - kuzzle.realtime.subscriptions = new Map(); + kuzzle.realtime._subscriptions = new Map(); return kuzzle.realtime.subscribe('index', 'collection', body, cb, options) .then(() => { - const subscriptions = kuzzle.realtime.subscriptions.get(roomId); + const subscriptions = kuzzle.realtime._subscriptions.get(roomId); should(subscriptions).be.an.Array(); should(subscriptions.length).be.exactly(1); should(subscriptions[0]).be.exactly(room); return kuzzle.realtime.subscribe('index', 'collection', body, cb, options); }).then(() => { - const subscriptions = kuzzle.realtime.subscriptions.get(roomId); + const subscriptions = kuzzle.realtime._subscriptions.get(roomId); should(subscriptions).be.an.Array(); should(subscriptions.length).be.exactly(2); @@ -175,8 +210,8 @@ describe('Realtime Controller', () => { room1.removeListeners.reset(); room2.removeListeners.reset(); - kuzzle.realtime.subscriptions.set(roomId, [room1, room2]); - kuzzle.realtime.subscriptions.set('foo', [room3]); + kuzzle.realtime._subscriptions.set(roomId, [room1, room2]); + kuzzle.realtime._subscriptions.set('foo', [room3]); kuzzle.query.resolves({result: roomId}); }); @@ -193,12 +228,12 @@ describe('Realtime Controller', () => { it('should delete rooms from local storage', () => { return kuzzle.realtime.unsubscribe(roomId) .then(() => { - should(kuzzle.realtime.subscriptions.get(roomId)).be.undefined(); + should(kuzzle.realtime._subscriptions.get(roomId)).be.undefined(); // Check we do not remove other registered rooms: - should(kuzzle.realtime.subscriptions.get('foo')).be.an.Array(); - should(kuzzle.realtime.subscriptions.get('foo').length).be.equal(1); - should(kuzzle.realtime.subscriptions.get('foo')[0]).be.equal(room3); + should(kuzzle.realtime._subscriptions.get('foo')).be.an.Array(); + should(kuzzle.realtime._subscriptions.get('foo').length).be.equal(1); + should(kuzzle.realtime._subscriptions.get('foo')[0]).be.equal(room3); }); }); @@ -218,7 +253,7 @@ describe('Realtime Controller', () => { }); }); - describe('#disconnected', () => { + describe('#saveSubscriptions', () => { it('should disable current subscriptions', () => { const roomA = { autoResubscribe: true, @@ -233,15 +268,15 @@ describe('Realtime Controller', () => { removeListeners: sinon.stub() }; - kuzzle.realtime.subscriptions = new Map([ + kuzzle.realtime._subscriptions = new Map([ ['foo', [roomA, roomB]], ['bar', [roomC]] ]); - kuzzle.realtime.disconnected(); + kuzzle.realtime.saveSubscriptions(); - should(kuzzle.realtime.subscriptions).be.empty(); - should(kuzzle.realtime.subscriptionsOff).eql(new Map([ + should(kuzzle.realtime._subscriptions).be.empty(); + should(kuzzle.realtime._subscriptionsOff).eql(new Map([ ['foo', [roomA]] ])); for (const room of [roomA, roomB, roomC]) { @@ -250,21 +285,21 @@ describe('Realtime Controller', () => { }); }); - describe('#reconnected', () => { + describe('#resubscribe', () => { it('should resubmit pending subcriptions', () => { const roomA = { subscribe: sinon.stub().resolves() }; const roomB = { subscribe: sinon.stub().resolves() }; const roomC = { subscribe: sinon.stub().resolves() }; - kuzzle.realtime.subscriptionsOff = new Map([ + kuzzle.realtime._subscriptionsOff = new Map([ ['foo', [roomA, roomB]], ['bar', [roomC]] ]); - kuzzle.realtime.reconnected(); + kuzzle.realtime.resubscribe(); - should(kuzzle.realtime.subscriptionsOff).be.empty(); - should(kuzzle.realtime.subscriptions).eql(new Map([ + should(kuzzle.realtime._subscriptionsOff).be.empty(); + should(kuzzle.realtime._subscriptions).eql(new Map([ ['foo', [roomA, roomB]], ['bar', [roomC]] ])); @@ -276,19 +311,19 @@ describe('Realtime Controller', () => { }); }); - describe('#tokenExpired', () => { - it('should clear all subscriptions and emit a "tokenExpired" event', () => { + describe('#removeSubscriptions', () => { + it('should clear all subscriptions', () => { const stub = sinon.stub(); kuzzle.jwt = 'foobar'; for (let i = 0; i < 10; i++) { - kuzzle.realtime.subscriptions.set(uuidv4(), [{removeListeners: stub}]); + kuzzle.realtime._subscriptions.set(uuidv4(), [{removeListeners: stub}]); } - kuzzle.realtime.tokenExpired(); + kuzzle.realtime.removeSubscriptions(); - should(kuzzle.realtime.subscriptions).be.empty(); + should(kuzzle.realtime._subscriptions).be.empty(); should(stub.callCount).be.eql(10); }); }); diff --git a/test/kuzzle/connect.test.js b/test/kuzzle/connect.test.js index 63c784b55..a151ffbd5 100644 --- a/test/kuzzle/connect.test.js +++ b/test/kuzzle/connect.test.js @@ -53,7 +53,7 @@ describe('Kuzzle connect', () => { kuzzle = new Kuzzle(protocols.nowhere), eventStub = sinon.stub(); - kuzzle.realtime.disconnected = sinon.stub(); + kuzzle.realtime.saveSubscriptions = sinon.stub(); kuzzle.addListener('networkError', eventStub); @@ -62,7 +62,7 @@ describe('Kuzzle connect', () => { throw new Error('should not happen'); }) .catch(() => { - should(kuzzle.realtime.disconnected).be.calledOnce(); + should(kuzzle.realtime.saveSubscriptions).be.calledOnce(); should(eventStub).be.calledOnce(); }); }); @@ -85,13 +85,13 @@ describe('Kuzzle connect', () => { kuzzle = new Kuzzle(protocols.somewhereagain), eventStub = sinon.stub(); - kuzzle.realtime.reconnected = sinon.stub(); + kuzzle.realtime.resubscribe = sinon.stub(); kuzzle.addListener('reconnected', eventStub); return kuzzle.connect() .then(() => { - should(kuzzle.realtime.reconnected).be.calledOnce(); + should(kuzzle.realtime.resubscribe).be.calledOnce(); should(eventStub).be.calledOnce(); }); }); @@ -137,14 +137,14 @@ describe('Kuzzle connect', () => { kuzzle = new Kuzzle(protocols.somewhere), eventStub = sinon.stub(); - kuzzle.realtime.disconnected = sinon.stub(); + kuzzle.realtime.saveSubscriptions = sinon.stub(); kuzzle.addListener('disconnected', eventStub); return kuzzle.connect() .then(() => kuzzle.protocol.disconnect()) .then(() => { - should(kuzzle.realtime.disconnected).be.calledOnce(); + should(kuzzle.realtime.saveSubscriptions).be.calledOnce(); should(eventStub).be.calledOnce(); }); }); diff --git a/test/kuzzle/listenersManagement.test.js b/test/kuzzle/listenersManagement.test.js index 0bd0599dc..a6d599315 100644 --- a/test/kuzzle/listenersManagement.test.js +++ b/test/kuzzle/listenersManagement.test.js @@ -2,7 +2,7 @@ const should = require('should'); const sinon = require('sinon'); const { Kuzzle } = require('../../src/Kuzzle'); -const { KuzzleEventEmitter } =require('../../src/core/KuzzleEventEmitter'); +const { KuzzleEventEmitter } = require('../../src/core/KuzzleEventEmitter'); const ProtocolMock = require('../mocks/protocol.mock'); describe('Kuzzle listeners management', () => { diff --git a/test/protocol/Base.test.js b/test/protocol/Base.test.js index bd2652459..bec5d3f0b 100644 --- a/test/protocol/Base.test.js +++ b/test/protocol/Base.test.js @@ -1,9 +1,9 @@ -const - should = require('should'), - sinon = require('sinon'), - KuzzleError = require('../../src/KuzzleError'), - { KuzzleAbstractProtocol } = require('../../src/protocols/abstract/Base'), - PendingRequest = require('../../src/protocols/abstract/PendingRequest'); +const should = require('should'); +const sinon = require('sinon'); + +const { KuzzleError } = require('../../src/KuzzleError'); +const { KuzzleAbstractProtocol } = require('../../src/protocols/abstract/Base'); +const { PendingRequest } = require('../../src/protocols/abstract/PendingRequest'); describe('Common Protocol', () => { let @@ -25,6 +25,30 @@ describe('Common Protocol', () => { should(protocol.port).be.eql(443); }); + it('should use ssl option if available and fallback to sslConnection option', () => { + protocol = new KuzzleAbstractProtocol('somewhere', { ssl: true }); + + should(protocol.ssl).be.true(); + + protocol = new KuzzleAbstractProtocol('somewhere', { sslConnection: true }); + + should(protocol.ssl).be.true(); + }); + + it('should use ssl connection when port is 443 or 7443 and option is not defined', () => { + protocol = new KuzzleAbstractProtocol('somewhere', { port: 443 }); + + should(protocol.ssl).be.true(); + + protocol = new KuzzleAbstractProtocol('somewhere', { port: 7443 }); + + should(protocol.ssl).be.true(); + + protocol = new KuzzleAbstractProtocol('somewhere', { port: 4242 }); + + should(protocol.ssl).be.false(); + }); + it('should use 7512 when no port is given or when port is not a parseable number', () => { protocol = new KuzzleAbstractProtocol('somewhere', { port: 'foobar' }); @@ -91,7 +115,7 @@ describe('Common Protocol', () => { const pending = protocol.pendingRequests.get('bar'); - pending.should.be.an.instanceOf(PendingRequest).and.match({request}); + should(pending).be.instanceOf(PendingRequest).and.match({request}); }); it('should fire a "queryError" event and reject if an error occurred', () => { @@ -161,7 +185,7 @@ describe('Common Protocol', () => { should(error).be.instanceOf(KuzzleError); should(error.message).be.eql('foo-bar'); should(error.status).be.eql(442); - should(error.stack).be.eql('you are the bug'); + should(error.kuzzleStack).be.eql('you are the bug'); }); }); @@ -185,7 +209,7 @@ describe('Common Protocol', () => { should(error).be.instanceOf(KuzzleError); should(error.message).be.eql('foo-bar'); should(error.status).be.eql(206); - should(error.stack).be.eql('you are the bug'); + should(error.kuzzleStack).be.eql('you are the bug'); should(error.errors).be.an.Array(); should(error.errors.length).eql(2); should(error.count).eql(42); diff --git a/test/protocol/Http.test.js b/test/protocol/Http.test.js index d435a1b1b..18e122f08 100644 --- a/test/protocol/Http.test.js +++ b/test/protocol/Http.test.js @@ -3,7 +3,7 @@ const should = require('should'); const sinon = require('sinon'); const staticHttpRoutes = require('../../src/protocols/routes.json'); -const Http = require('../../src/protocols/Http'); +const { default: Http } = require('../../src/protocols/Http'); describe('HTTP networking module', () => { let protocol; @@ -557,7 +557,7 @@ describe('HTTP networking module', () => { beforeEach(() => { httpRequestStub = sinon.stub().resolves({body: JSON.stringify(mockResponseBody)}); - const MockHttp = proxyquire('../../src/protocols/Http', { + const { default: MockHttp } = proxyquire('../../src/protocols/Http', { 'min-req-promise': {request: httpRequestStub} }); @@ -663,9 +663,7 @@ describe('HTTP networking module', () => { return xhrStub; }; - protocol = new Http('address', { - port: 1234 - }); + protocol = new Http('address', { port: 1234 }); }); afterEach(() => { @@ -829,13 +827,14 @@ describe('HTTP networking module', () => { } }, }; - - protocol.customRoutes = { + const customRoutes = { foo: { list: { verb: 'GET', url: '/overwrite/me/master' } } }; + protocol = new Http('address', { port: 1234, customRoutes }); + const routes = protocol._constructRoutes(publicApi); should(routes.foo.list.url).be.eql('/overwrite/me/master'); diff --git a/test/protocol/WebSocket.test.js b/test/protocol/WebSocket.test.js index cfd3d0989..205f0d443 100644 --- a/test/protocol/WebSocket.test.js +++ b/test/protocol/WebSocket.test.js @@ -1,10 +1,10 @@ -const - should = require('should'), - sinon = require('sinon'), - lolex = require('lolex'), - NodeWS = require('ws'), - WS = require('../../src/protocols/WebSocket'), - windowMock = require('../mocks/window.mock'); +const should = require('should'); +const sinon = require('sinon'); +const lolex = require('lolex'); +const NodeWS = require('ws'); + +const { default: WS } = require('../../src/protocols/WebSocket'); +const windowMock = require('../mocks/window.mock'); describe('WebSocket networking module', () => { let @@ -246,8 +246,6 @@ describe('WebSocket networking module', () => { clock.tick(10); should(cb).be.calledOnce(); should(cb.firstCall.args[0]).be.an.instanceOf(Error); - should(cb.firstCall.args[0].internal.status).be.equal(4666); - should(cb.firstCall.args[0].internal.message).be.equal('foobar'); should(websocket.listeners('networkError').length).be.eql(1); websocket.clear.should.be.calledOnce(); @@ -259,8 +257,6 @@ describe('WebSocket networking module', () => { clock.tick(10); should(cb).be.calledOnce(); should(cb.firstCall.args[0]).be.an.instanceOf(Error); - should(cb.firstCall.args[0].internal.status).be.equal(4666); - should(cb.firstCall.args[0].internal.message).be.equal('foobar'); should(websocket.listeners('networkError').length).be.eql(1); websocket.clear.should.be.calledOnce(); }); diff --git a/tsconfig.json b/tsconfig.json index 1ea049850..4af52bb73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "moduleResolution": "node", "sourceMap": true, "baseUrl": ".", - "resolveJsonModule": true + "resolveJsonModule": true, + "esModuleInterop": true }, "rootDir": "src/", "include": [