From b35e27c78ad4d4b2062d1c665d20b628c2830068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9as=20Hanss?= Date: Thu, 2 Mar 2023 19:16:09 +0100 Subject: [PATCH 1/3] Add support and documentation to use API keys (#724) Simplify the API by adding a helper function to set the JWT only with kuzzle API key format. --- .../auth/create-api-key/snippets/create-api-key.js | 3 +++ src/Kuzzle.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/doc/7/controllers/auth/create-api-key/snippets/create-api-key.js b/doc/7/controllers/auth/create-api-key/snippets/create-api-key.js index 288a689ca..a4441b42c 100644 --- a/doc/7/controllers/auth/create-api-key/snippets/create-api-key.js +++ b/doc/7/controllers/auth/create-api-key/snippets/create-api-key.js @@ -18,6 +18,9 @@ try { */ console.log('API key successfully created'); + + // Then use it with your client. Note: You don't need to call login after this because this bypasses the authentication system. + kuzzle.setAPIKey(apiKey._source.token) } catch (e) { console.error(e); } diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index c474740bc..72b2fba83 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -620,6 +620,19 @@ export class Kuzzle extends KuzzleEventEmitter { return this.protocol.connect(); } + /** + * Set this client to use a specific API key. + * + * After doing this you don't need to use login as it bypasses the authentication process. + */ + public setAPIKey(apiKey: string) { + if (apiKey.match(/^kapikey-/) === null) { + throw new Error("Invalid API key. Missing the `kapikey-` prefix."); + } + + this.jwt = apiKey; + } + async _reconnect() { if (this._reconnectInProgress) { return; From 82a8aaff6532ff49662553def92e70c321f9a590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9as=20Hanss?= Date: Mon, 6 Mar 2023 16:45:34 +0100 Subject: [PATCH 2/3] Improve typings (#723) * Improve notification types to improve usage. * Set JSONObject to proper type * Add events handler for KuzzleEventEmitter * Implement various TypeScript fixes. Also implemented types coherence testing * Add clear separation about internal and external events. * Fix build error with test --- src/Kuzzle.ts | 56 ++++++++++++++++++++-- src/core/KuzzleEventEmitter.ts | 69 ++++++++++++++++++++++++---- src/protocols/DisconnectionOrigin.ts | 10 ++-- src/protocols/WebSocket.ts | 4 +- src/protocols/abstract/Realtime.ts | 9 ++-- src/types/JSONObject.ts | 4 +- src/types/Notification.ts | 22 +++++---- test/kuzzle-sdk-test.ts | 49 ++++++++++++++++++++ test/protocol/WebSocket.test.js | 3 +- tsconfig.json | 11 ++--- 10 files changed, 192 insertions(+), 45 deletions(-) create mode 100644 test/kuzzle-sdk-test.ts diff --git a/src/Kuzzle.ts b/src/Kuzzle.ts index 72b2fba83..9291d3dd3 100644 --- a/src/Kuzzle.ts +++ b/src/Kuzzle.ts @@ -1,4 +1,8 @@ -import { KuzzleEventEmitter } from "./core/KuzzleEventEmitter"; +import { + KuzzleEventEmitter, + PrivateAndPublicSDKEvents, + PublicKuzzleEvents, +} from "./core/KuzzleEventEmitter"; import { KuzzleAbstractProtocol } from "./protocols/abstract/Base"; import { AuthController } from "./controllers/Auth"; @@ -15,11 +19,13 @@ import { Deprecation } from "./utils/Deprecation"; import { uuidv4 } from "./utils/uuidv4"; import { proxify } from "./utils/proxify"; import { debug } from "./utils/debug"; -import { BaseRequest, JSONObject } from "./types"; +import { BaseRequest, JSONObject, Notification } from "./types"; import { RequestPayload } from "./types/RequestPayload"; import { ResponsePayload } from "./types/ResponsePayload"; import { RequestTimeoutError } from "./RequestTimeoutError"; import { BaseProtocolRealtime } from "./protocols/abstract/Realtime"; +import { KuzzleError } from "./KuzzleError"; +import { DisconnectionOrigin } from "./protocols/DisconnectionOrigin"; // Defined by webpack plugin declare const SDKVERSION: any; @@ -72,7 +78,7 @@ export class Kuzzle extends KuzzleEventEmitter { /** * List of every events emitted by the SDK. */ - public events = [ + public events: PublicKuzzleEvents[] = [ "callbackError", "connected", "discarded", @@ -542,7 +548,7 @@ export class Kuzzle extends KuzzleEventEmitter { * Emit an event to all registered listeners * An event cannot be emitted multiple times before a timeout has been reached. */ - emit(eventName: string, ...payload) { + public emit(eventName: PrivateAndPublicSDKEvents, ...payload: unknown[]) { const now = Date.now(), protectedEvent = this._protectedEvents[eventName]; @@ -563,6 +569,48 @@ export class Kuzzle extends KuzzleEventEmitter { return super.emit(eventName, ...payload); } + on( + eventName: "connected" | "reconnected" | "reAuthenticated" | "tokenExpired", + listener: () => void + ): this; + + on( + eventName: "logoutAttempt", + listener: (status: { success: true }) => void + ): this; + on( + eventName: "loginAttempt", + listener: (data: { success: boolean; error: string }) => void + ): this; + on(eventName: "discarded", listener: (request: RequestPayload) => void): this; + on( + eventName: "disconnected", + listener: (context: { origin: DisconnectionOrigin }) => void + ): this; + on( + eventName: "networkError" | "reconnectionError", + listener: (error: Error) => void + ): this; + on( + eventName: "offlineQueuePop", + listener: (request: RequestPayload) => void + ): this; + on( + eventName: "offlineQueuePush", + listener: (data: { request: RequestPayload }) => void + ): this; + on( + eventName: "queryError", + listener: (data: { error: KuzzleError; request: RequestPayload }) => void + ): this; + on( + eventName: "callbackError", + listener: (data: { error: KuzzleError; notification: Notification }) => void + ): this; + on(eventName: PublicKuzzleEvents, listener: (args: any) => void): this { + return super.on(eventName, listener); + } + /** * Connects to a Kuzzle instance */ diff --git a/src/core/KuzzleEventEmitter.ts b/src/core/KuzzleEventEmitter.ts index 3776190a8..1196957f4 100644 --- a/src/core/KuzzleEventEmitter.ts +++ b/src/core/KuzzleEventEmitter.ts @@ -1,5 +1,7 @@ +type ListenerFunction = (...args: unknown[]) => unknown; + class Listener { - public fn: (...any) => any; + public fn: ListenerFunction; public once: boolean; constructor(fn, once = false) { @@ -8,6 +10,37 @@ class Listener { } } +export type PublicKuzzleEvents = + | "callbackError" + | "connected" + | "discarded" + | "disconnected" + | "loginAttempt" + | "logoutAttempt" + | "networkError" + | "offlineQueuePush" + | "offlineQueuePop" + | "queryError" + | "reAuthenticated" + | "reconnected" + | "reconnectionError" + | "tokenExpired"; + +type PrivateKuzzleEvents = + | "connect" + | "reconnect" + | "disconnect" + | "offlineQueuePush" + | "websocketRenewalStart" + | "websocketRenewalDone"; + +/** + * For internal use only + */ +export type PrivateAndPublicSDKEvents = + | PublicKuzzleEvents + | PrivateKuzzleEvents; + /** * @todo proper TS conversion */ @@ -18,11 +51,11 @@ export class KuzzleEventEmitter { this._events = new Map(); } - private _exists(listeners, fn) { + private _exists(listeners: Listener[], fn: ListenerFunction) { return Boolean(listeners.find((listener) => listener.fn === fn)); } - listeners(eventName) { + listeners(eventName: PrivateAndPublicSDKEvents) { if (!this._events.has(eventName)) { return []; } @@ -30,11 +63,16 @@ export class KuzzleEventEmitter { return this._events.get(eventName).map((listener) => listener.fn); } - addListener(eventName, listener, once = false) { + addListener( + eventName: PrivateAndPublicSDKEvents, + listener: ListenerFunction, + once = false + ) { if (!eventName || !listener) { return this; } + // TODO: this check could be safely, when TypeScript type will be completed. const listenerType = typeof listener; if (listenerType !== "function") { @@ -54,7 +92,10 @@ export class KuzzleEventEmitter { return this; } - on(eventName, listener) { + on( + eventName: PrivateAndPublicSDKEvents, + listener: (args: any) => void + ): this { return this.addListener(eventName, listener); } @@ -90,7 +131,12 @@ export class KuzzleEventEmitter { return this.prependListener(eventName, listener, true); } - removeListener(eventName, listener) { + removeListener( + eventName: PrivateAndPublicSDKEvents, + listener: () => void + ): this; + removeListener(eventName: string, listener: () => void): this; + removeListener(eventName: string, listener: (...args: unknown[]) => void) { const listeners = this._events.get(eventName); if (!listeners || !listeners.length) { @@ -110,7 +156,9 @@ export class KuzzleEventEmitter { return this; } - removeAllListeners(eventName?: string) { + removeAllListeners(eventName?: PrivateAndPublicSDKEvents): this; + removeAllListeners(eventName?: string): this; + removeAllListeners(eventName?: string): this { if (eventName) { this._events.delete(eventName); } else { @@ -120,7 +168,10 @@ export class KuzzleEventEmitter { return this; } - emit(eventName, ...payload) { + // TODO: Improve these unknown type someday, to secure all emit events and be sure they match {@link KuzzleEventEmitter.on}. + emit(eventName: PrivateAndPublicSDKEvents, ...payload: unknown[]): boolean; + emit(eventName: string, ...payload: unknown[]): boolean; + emit(eventName: string, ...payload: unknown[]): boolean { const listeners = this._events.get(eventName); if (listeners === undefined) { @@ -148,7 +199,7 @@ export class KuzzleEventEmitter { return Array.from(this._events.keys()); } - listenerCount(eventName) { + listenerCount(eventName: PrivateAndPublicSDKEvents) { return ( (this._events.has(eventName) && this._events.get(eventName).length) || 0 ); diff --git a/src/protocols/DisconnectionOrigin.ts b/src/protocols/DisconnectionOrigin.ts index f153e9886..6d0530bfb 100644 --- a/src/protocols/DisconnectionOrigin.ts +++ b/src/protocols/DisconnectionOrigin.ts @@ -1,5 +1,5 @@ -const WEBSOCKET_AUTH_RENEWAL = "websocket/auth-renewal"; -const USER_CONNECTION_CLOSED = "user/connection-closed"; -const NETWORK_ERROR = "network/error"; - -export { WEBSOCKET_AUTH_RENEWAL, USER_CONNECTION_CLOSED, NETWORK_ERROR }; +export enum DisconnectionOrigin { + WEBSOCKET_AUTH_RENEWAL = "websocket/auth-renewal", + USER_CONNECTION_CLOSED = "user/connection-closed", + NETWORK_ERROR = "network/error", +} diff --git a/src/protocols/WebSocket.ts b/src/protocols/WebSocket.ts index 611a703cb..8e52fcbe6 100644 --- a/src/protocols/WebSocket.ts +++ b/src/protocols/WebSocket.ts @@ -5,7 +5,7 @@ import { BaseProtocolRealtime } from "./abstract/Realtime"; import { JSONObject } from "../types"; import { RequestPayload } from "../types/RequestPayload"; import HttpProtocol from "./Http"; -import * as DisconnectionOrigin from "./DisconnectionOrigin"; +import { DisconnectionOrigin } from "./DisconnectionOrigin"; /** * WebSocket protocol used to connect to a Kuzzle server. @@ -285,7 +285,7 @@ export default class WebSocketProtocol extends BaseProtocolRealtime { /** * @override */ - clientDisconnected(origin: string) { + clientDisconnected(origin: DisconnectionOrigin) { clearInterval(this.pingIntervalId); this.pingIntervalId = null; super.clientDisconnected(origin); diff --git a/src/protocols/abstract/Realtime.ts b/src/protocols/abstract/Realtime.ts index 9911f516a..00721f1c3 100644 --- a/src/protocols/abstract/Realtime.ts +++ b/src/protocols/abstract/Realtime.ts @@ -1,8 +1,9 @@ "use strict"; import { KuzzleAbstractProtocol } from "./Base"; -import * as DisconnectionOrigin from "../DisconnectionOrigin"; import { getBrowserWindow, isBrowser } from "../../utils/browser"; +import { DisconnectionOrigin } from "../DisconnectionOrigin"; +import { KuzzleError } from "../../KuzzleError"; export abstract class BaseProtocolRealtime extends KuzzleAbstractProtocol { protected _reconnectionDelay: number; @@ -56,7 +57,7 @@ export abstract class BaseProtocolRealtime extends KuzzleAbstractProtocol { * * @param {string} origin String that describe what is causing the disconnection */ - clientDisconnected(origin: string) { + clientDisconnected(origin: DisconnectionOrigin) { this.clear(); this.emit("disconnect", { origin }); } @@ -64,9 +65,9 @@ export abstract class BaseProtocolRealtime extends KuzzleAbstractProtocol { /** * Called when the client's connection is closed with an error state * - * @param {Error} error + * @param {KuzzleError} error */ - clientNetworkError(error) { + clientNetworkError(error: KuzzleError) { // Only emit disconnect once, if the connection was ready before if (this.isReady()) { this.emit("disconnect", { origin: DisconnectionOrigin.NETWORK_ERROR }); diff --git a/src/types/JSONObject.ts b/src/types/JSONObject.ts index f6bd473be..f87e9d53f 100644 --- a/src/types/JSONObject.ts +++ b/src/types/JSONObject.ts @@ -1,6 +1,4 @@ /** * An interface representing an object with string key and any value */ -export interface JSONObject { - [key: string]: JSONObject | any; -} +export type JSONObject = Record; diff --git a/src/types/Notification.ts b/src/types/Notification.ts index 49409ccd9..7b942f9aa 100644 --- a/src/types/Notification.ts +++ b/src/types/Notification.ts @@ -6,18 +6,12 @@ import { KDocument, KDocumentContentGeneric } from "."; */ export type NotificationType = "document" | "user" | "TokenExpired"; -/** - * Real-time notifications sent by Kuzzle. - * - */ -export interface Notification { +export interface BaseNotification { /** * Notification type */ type: NotificationType; -} -export interface BaseNotification extends Notification { /** * Controller that triggered the notification */ @@ -62,11 +56,13 @@ export interface BaseNotification extends Notification { * Notification triggered by a document change. * (create, update, delete) */ -export interface DocumentNotification extends BaseNotification { +export interface DocumentNotification< + TDocContent extends KDocumentContentGeneric = KDocumentContentGeneric +> extends BaseNotification { /** * Updated document that triggered the notification */ - result: KDocument; + result: KDocument; /** * State of the document regarding the scope (`in` or `out`) */ @@ -105,3 +101,11 @@ export interface ServerNotification extends BaseNotification { type: "TokenExpired"; } + +/** + * Real-time notifications sent by Kuzzle. + */ +export type Notification = + | DocumentNotification + | UserNotification + | ServerNotification; diff --git a/test/kuzzle-sdk-test.ts b/test/kuzzle-sdk-test.ts new file mode 100644 index 000000000..e662fe119 --- /dev/null +++ b/test/kuzzle-sdk-test.ts @@ -0,0 +1,49 @@ +/** + * This file only purpose is to check that type remains compliant with the spec for user usage. + * This is not meant to be executed. It is only a compilation check along the test battery. + * + * If you see red warning, it's likely that you broke the code somewhere and introduced a breaking change. + * + * TODO: This can be safely removed when test will be migrated to TypeScript. + * TODO: Due to how the stack actually build and test, this file is not part of the CI, check it on your own before pushing. + * + * Inspired by TS standard from DefinitelyTyped + * @see https://github.com/DefinitelyTyped/DefinitelyTyped#my-package-teststs + */ +import { Kuzzle } from "../src/Kuzzle"; +import WebSocket from "../src/protocols/WebSocket"; +const kuzzle = new Kuzzle(new WebSocket("toto")); + +// Events +kuzzle.on("connected", () => {}); +kuzzle.on("callbackError", ({ error, notification }) => {}); +kuzzle.on( + "discarded", + ({ + action, + controller, + _id, + body, + collection, + index, + jwt, + requestId, + volatile, + }) => {} +); +kuzzle.on("disconnected", ({ origin }) => {}); +kuzzle.on("loginAttempt", ({ success, error }) => {}); + +// Methods +kuzzle.connect().then(() => {}); +kuzzle.disconnect(); +kuzzle.isAuthenticated().then(() => {}); +kuzzle.query({ controller: "auth", action: "login" }).then(() => {}); + +kuzzle.authenticator = () => Promise.resolve(); +kuzzle.authenticator().then(() => {}); +kuzzle.authenticate().then(() => {}); +kuzzle + .login("local", { username: "ScreamZ", password: "some_password" }) + .then(() => {}); +kuzzle.logout().then(() => {}); diff --git a/test/protocol/WebSocket.test.js b/test/protocol/WebSocket.test.js index bdea89f4c..66d45f7f7 100644 --- a/test/protocol/WebSocket.test.js +++ b/test/protocol/WebSocket.test.js @@ -6,7 +6,8 @@ const NodeWS = require("ws"); const { default: WS } = require("../../src/protocols/WebSocket"); const windowMock = require("../mocks/window.mock"); const { default: HttpProtocol } = require("../../src/protocols/Http"); -const DisconnectionOrigin = require("../../src/protocols/DisconnectionOrigin"); +const DisconnectionOrigin = + require("../../src/protocols/DisconnectionOrigin").DisconnectionOrigin; describe("WebSocket networking module", () => { let clock, websocket, wsargs, clientStub; diff --git a/tsconfig.json b/tsconfig.json index 4af52bb73..452852dae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,11 +11,6 @@ "esModuleInterop": true }, "rootDir": "src/", - "include": [ - "index.ts", - "src/**/*.ts" - ], - "exclude": [ - "node_modules" - ] -} \ No newline at end of file + "include": ["index.ts", "src/**/*.ts"], + "exclude": ["node_modules"] +} From 065accf70ea4a0584c5e61096b9ca4c9bde2cd7f Mon Sep 17 00:00:00 2001 From: Aschen Date: Mon, 6 Mar 2023 16:48:10 +0100 Subject: [PATCH 3/3] Release 7.10.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 754367d7e..29ca74f45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kuzzle-sdk", - "version": "7.10.6", + "version": "7.10.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "kuzzle-sdk", - "version": "7.10.6", + "version": "7.10.7", "license": "Apache-2.0", "dependencies": { "min-req-promise": "^1.0.1", diff --git a/package.json b/package.json index d2f2e0e42..0b2ec1227 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kuzzle-sdk", - "version": "7.10.6", + "version": "7.10.7", "description": "Official Javascript SDK for Kuzzle", "author": "The Kuzzle Team ", "repository": {