2.4.0 • Published 21 days ago

@gravity-ui/gateway v2.4.0

Weekly downloads
-
License
MIT
Repository
github
Last release
21 days ago

@gravity-ui/gateway · npm package CI

Express controller for working with REST/GRPC APIs.

Install

npm install --save-dev @gravity-ui/gateway

Usage

First of all, you need to create a controller where you will import Gateway and Schema, and then return the initialized gateway controller:

import getGatewayControllers from '@gravity-ui/gateway';
import Schema from '<schemas package>';

const config = {
  installation: 'external',
  env: 'production',
  includeProtoRoots: ['...'],
};

const {controller: gatewayController} = getGatewayControllers({root: Schema}, config);

export default gatewayController;

Next, the controller described above should be connected to a route of the following format (the project should use expresskit):

{
    'POST   /<prefix>/:scope/:service/:action': {target: '<controller>', afterAuth: ['credentials']}
}

The prefix can be any prefix for API endpoints (for example, /gateway/:service/:action).

Config Structure

import {AxiosRequestConfig} from 'axios';
import {IncomingHttpHeaders} from 'http';

interface OnUnknownActionData {
  service?: string;
  action?: string;
}

interface Stats {
  service: string;
  action: string;
  restStatus: number;
  grpcStatus?: number;
  requestId: string;
  requestTime: number;
  requestMethod: string;
  requestUrl: string;
  timestamp: number;
}

type SendStats = (
  stats: Stats,
  headers: IncomingHttpHeaders,
  ctx: CoreContext,
  meta: {debugHeaders: Headers},
) => void;

type ProxyHeadersFunction = (
  headers: IncomingHttpHeaders,
  type: ControllerType,
) => IncomingHttpHeaders;
type ProxyHeaders = string[] | ProxyHeadersFunction;
type ResponseContentType = AxiosResponse['headers']['Content-Type'];

interface GatewayConfig {
  // Gateway Installation (external/internal/...). If the configuration is not provided, it is determined from process.env.APP_INSTALLATION.
  installation?: string;
  // Gateway Environment (production/testing/...). If the configuration is not provided, it is determined from process.env.APP_ENV.
  env?: string;
  // Additional gRPC client options.
  grpcOptions?: object;
  // Additional Axios client options.
  axiosConfig?: AxiosRequestConfig;
  // List of actions that need to be connected from the schema. By default, all actions are connected.
  actions?: string[];
  // Called when an unknown service or action is provided.
  onUnknownAction?: (req: Request, res: Response, data: OnUnknownActionData) => any;
  // Called before the request is executed.
  onBeforeAction?: (
    req: Request,
    res: Response,
    scope: string,
    service: string,
    action: string,
    config?: ApiServiceActionConfig,
  ) => any;
  // Called upon successful completion of the request.
  onRequestSuccess?: (req: Request, res: Response, data: any) => any;
  // Called in case of unsuccessful request execution.
  onRequestFailed?: (req: Request, res: Response, error: any) => any;
  // List of paths to the necessary proto files for the gateway.
  includeProtoRoots?: string[];
  // Configuration of the path to the certificate in gRPC.
  // Set to null to use system certificates by default.
  caCertificatePath?: string | null;
  // Telemetry sending configuration.
  sendStats?: SendStats;
  // Configuration of headers sent to the API.
  proxyHeaders?: ProxyHeaders;
  // When passing a boolean value, it enables/disables debug headers in the response to the request.
  // For unary requests to gRPC backends, debug headers will include information from the trailing metadata returned by the backend.
  withDebugHeaders?: boolean;
  // Validation schema for parameters used when no schema is present in the action. Documentation: https://ajv.js.org/json-schema.html#json-data-type
  // You can use DEFAULT_VALIDATION_SCHEMA from lib/constants.ts.
  validationSchema?: object;
  // Enables encoding of REST path arguments (default is true).
  encodePathArgs?: boolean;
  // Configuration for automatic connection re-establishment upon connection error through L3 load balancer (default is true).
  grpcRecreateService?: boolean;
  // Enable verification of response contentType header. Actual only for REST actions. This value can be set / redefined the in action confg.
  expectedResponseContentType?: ResponseContentType | ResponseContentType[];
}

Usage in Node.js

Upon gateway initialization, in addition to exporting the controller, it also exports an api object, which represents the core for executing requests to the backend.

import getGatewayControllers from '@gravity-ui/gateway';
import Schema from '<schemas package>';

const config = {
  installation: 'external',
  env: 'production',
  includeProtoRoots: ['...'],
  timeout: 25000, // default 25 seconds
  caCertificatePath: '...',
};

const {api: gatewayApi} = getGatewayControllers({root: Schema}, config);

Subsequently, in the code, you can use it as follows:

gatewayApi[service][action](actionConfig);

actionConfig has the following structure:

interface ApiActionConfig<Context, TRequestData> {
  requestId: string;
  headers: Headers;
  args: TRequestData;
  ctx: Context;
  timeout?: number;
  callback?: (response: TResponseData) => void;
  authArgs?: Record<string, unknown>;
}

Schema Scopes

Each schema belongs to its own namespace. Service and action names between schemas are completely independent and can coincide. Each scope has an independent gRPC context, which eliminates naming conflicts between schemas in proto files. The scope name is the key in the first parameter of the object containing the schemas.

const schemasByScopes = {scope1: schema1, scope2: schema2};

Example with two scope namespaces: root and anotherScope.

import getGatewayControllers from '@gravity-ui/gateway';

const {
    controller, // Controller
    api, // API (for Node.js environment)
} = getGatewayControllers({ root: rootSchema, anotherScope: anotherSchema}, config);

// API calls are made by specifying the scope.
const resultFromRoot = api.rootSchema.<root-service>.<root-action>(params);
const resultFromAnother = api.anotherSchema.<another-service>.<another-action>(params);

There is a special scope called root. Its methods can be invoked without explicitly specifying the scope.

const resultFromRoot = api.rootSchema.<root-service>.<root-action>(params);
// Same result
const sameResultFromRoot = api.<root-service>.<root-action>(params);

The controller for the expresskit will also expect the :scope parameter.

{
    'POST   /<prefix>/:scope/:service/:action': {...}
}

If the scope parameter is not specified, the default scope is assumed to be root.

Connecting a Specific Set of Actions

When initializing the gateway, there is an option to explicitly specify the actions that need to be connected from the schemas. To do this, provide a list of available client-side actions in the actions field in the config. If actions are not provided, all actions from the schemas are connected by default.

import getGatewayControllers from '@gravity-ui/gateway';
import rootSchema from '<schemas package>';
import localSchema from '../shared/schemas';

const config = {
  installation: 'external',
  env: 'production',
  includeProtoRoots: ['...'],
  actions: ['local.*', 'root.serviceA.*', 'root.serviceB.get'], // List of actions to be connected from the schemas. By default, all actions are connected.
};

const {api: gatewayApi} = getGatewayControllers({root: rootSchema, local: localSchema}, config);

The following combinations are available for specifying connected actions:

  • <scope>.* - all actions from the scope scope are connected (for example, local.*)
  • <scope>.<service>.* - all actions from the service service are connected (for example, root.serviceA.*)
  • <scope>.<service>.action - only the specified action is connected (for example, root.serviceB.get)

Important. The actions configuration only affects the list of actions that will be accessible from the client (e.g., via the sdk). All actions from the schemas will continue to be accessible on Node.js.

GATEWAY_ENDPOINTS_OVERRIDES

Through the GATEWAY_ENDPOINTS_OVERRIDES environment variable, you can override specific endpoints. This can be useful for testing environments. A simple example: {"serviceName":{"endpoint":"https://example.com"}}. You can find a more detailed format in the OverrideParams interface and test examples.

gRPC Reflection for gRPC Actions

Instead of using gRPC proto files, a gRPC action can determine the structure of the service and the required method through reflection.

Enabling Reflection

To use reflection, you need to:

  • Install the grpc-reflection-js package as a peer dependency.
  • Apply patches to the protobufjs library. You can do this in the following ways:

    a) Add npx gateway-reflection-patch to the postinstall script in your project and execute it. This assumes that protobufjs is located in the root of node_modules.

    b) Copy the patch from the library's patches folder to your project's root, install patch-package, and add the patch-package command to the postinstall script. In this case, you need to keep an eye on updates to the patches in the gateway when updating it.

    If you encounter a "cannot run in wd ..." error during Docker build, you can add unsafe-perm = true to your .npmrc file as described here.

  • In the action configuration, replace the protoPath option with the reflection option and set its value to the appropriate GrpcReflection enum value. For reflection to work, the endpoint must support it.

Possible values for GrpcReflection, affecting the caching of reflection results:

  • OnFirstRequest - Perform reflection on the first action request. Use cached reflections.
  • OnEveryRequest - Perform reflection before every action request. Do not use cached reflections.

For the OnFirstRequest options you can specify the reflectionRefreshSec parameter, which indicates how often in seconds the reflection cache can be updated in the background. Cache updates happen asynchronously and don't block the current request. The initial reflection request with an empty cache might introduce some delay in the request.

Particularities

The cache key for reflections consists of protoKey and endpoint. Therefore, actions with shared keys will use a common cached version, which will be obtained from the earliest scenario (when the first action request with the OnFirstRequest strategy is made).

This function is experimental. Fixes have been applied to protobufjs using patch-package based on the following PRs:

  • Conversion of parameter names to camelCase PR 1073
  • Fix for handling Map PR 1478 grpc-reflection-js has also been patched to support custom options.

For development, you need to apply the patch locally using the command npx patch-package.

ChannelCredentials Type Mismatch Error

This error can occur due to duplicate installations of the @grpc/grpc-js library. It's recommended to ensure that all versions of this library are aligned and consistent to avoid this issue.

2.4.0

21 days ago

2.3.0

1 month ago

2.2.0

1 month ago

2.1.0

2 months ago

2.0.0

3 months ago

1.6.0

3 months ago

1.5.1

5 months ago

1.5.0

5 months ago

1.4.0

5 months ago

1.3.1

6 months ago

1.3.0

6 months ago

1.2.2

6 months ago

1.2.1

7 months ago

1.2.0

7 months ago

1.1.1

7 months ago

1.1.0

8 months ago

1.0.5

8 months ago

1.0.4

9 months ago

1.0.3

9 months ago

1.0.2

9 months ago

1.0.1

9 months ago

1.0.0

9 months ago