### File

`src/lib/schema-validation.mixin.ts`

### Index

###### Properties

- Optional body
- Optional data
- headers
- status

### Properties

body
`body: `Data | null``
Type : `Data | null`
Optional

data
`data: `Data``
Type : `Data`
Optional

headers
`headers: `HttpHeaders``
Type : `HttpHeaders`

status
`status: `number``
Type : `number`

    import {
      HttpHeaders,
      HttpParams,
    } from '@angular/common/http';
    import { isDevMode } from '@angular/core';
    import { HttpRemoteMethodParameter } from '@rxap/remote-method/http';
    import {
      assertsObject,
      coerceArray,
      isPromiseLike,
      IsRecord,
    } from '@rxap/utilities';
    import Ajv from 'ajv';
    import { OpenAPIV3 } from 'openapi-types';
    import { RxapOpenApiError } from './error';
    import { OperationObjectWithMetadata } from './open-api';
    import {
      IsReferenceObject,
      NotContainsReferenceObjects,
    } from './utilities';

    export interface SchemaValidationResponse<Data> {
      headers: HttpHeaders;
      status: number;
      body?: Data | null;
      data?: Data;
    }

    export class SchemaValidationMixin<Response = any, Parameters extends Record<string, any> | void = any, RequestBody = any> {

      public static STRICT = false;

      protected disableSchemaValidation?: boolean;

      /**
       * Validates the parameters against the schema specified in the operation object
       *
       * @param operation
       * @param parameters
       * @param strict
       * @protected
       */
      public validateParameters(operation: OperationObjectWithMetadata, parameters?: Parameters, strict = false): void {

        const operationParameters = coerceArray(operation.parameters);

        if (!NotContainsReferenceObjects<OpenAPIV3.ParameterObject>(operationParameters)) {
          throw new RxapOpenApiError('The operation parameters contains ReferenceObject!');
        }

        if (parameters === undefined) {
          // TODO : find concept to definition witch parameter should not be checked if required
          // header parameters are never required if changes the semantic release manager breaks
          const requiredParameters = operationParameters.filter(parameter => parameter.required &&
            parameter.in !==
            'header');
          if (requiredParameters.length) {
            if (isDevMode()) {
              console.debug('Some operation parameters are required!', requiredParameters.map(p => p.name));
            }
            this.validationError('Some operation parameters are required!', strict);
          }

        } else if (IsRecord(parameters)) {

          for (const parameter of operationParameters) {

            if (parameter.required) {
              // header parameters are never required if changes the semantic release manager breaks
              if (parameter.in !== 'header') {
                if (!parameters.hasOwnProperty(parameter.name)) {
                  this.validationError(`The operation parameter '${ parameter.name }' is required!`, strict);
                }
              }
            }

            if (parameter.schema) {
              if (parameters.hasOwnProperty(parameter.name)) {

                const value = parameters[parameter.name];

                if (!this.validate(parameter.schema, value)) {
                  this.validationError(
                    `The parameter '${ parameter.name }' is not valid against the schema!`,
                    strict,
                    parameter.schema,
                    value,
                  );
                }

              }
            }

          }

        } else {
          if (operationParameters.length === 0 || operationParameters.every(op => !op.required)) {
            if (isDevMode()) {
              console.warn(
                `The operation ${ operation.operationId } does not expect any parameters. But a parameter is provided.`,
                parameters,
              );
            }
          } else {
            throw new Error('The parameters object is not a record');
          }
        }

      }

      /**
       * Validates the http response against the schema specified in the operation object
       *
       * @param operation
       * @param response
       * @param strict
       * @protected
       */
      public validateResponse(
        operation: OperationObjectWithMetadata,
        response: SchemaValidationResponse<Response>,
        strict = false,
      ): void {

        // region only validate the response if the content type is undefined or application/json

        const contentType = response.headers.get('Content-Type');

        if (contentType && contentType !== 'application/json') {
          if (isDevMode()) {
            console.warn('Response validation is only supported for content type application/json');
          }
          return;
        }

        // endregion

        if (operation.responses) {

          const status = response.status;

          // region extract the response object based on the response status
          let responseObject: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject | undefined;

          if (operation.responses[status]) {
            responseObject = operation.responses[status];
          } else {
            // use the default response object if no matching was found.
            responseObject = operation.responses['default'];
          }
          // endregion

          if (responseObject) {

            if (IsReferenceObject(responseObject)) {
              throw new RxapOpenApiError('Found a reference object. The operation config is not expand!');
            }

            // region validate the response against the schema if defined

            // TODO : create schema that validates all parameters at once

            if (responseObject.content &&
              responseObject.content['application/json'] &&
              responseObject.content['application/json'].schema) {

              const schema = responseObject.content['application/json'].schema;

              const data = response.body ?? response.data;

              if (!this.validate(schema, data)) {
                this.validationError('The response is not valid ageist the operation schema!', strict, schema, data);
              }

            }

            // endregion

          }

        }

      }

      public validateRequestBody(operation: OperationObjectWithMetadata, body?: RequestBody, strict = false): void {

        // only validate body if type of object
        if (typeof body !== 'object') {
          return;
        }

        if (operation.requestBody) {

          if (IsReferenceObject(operation.requestBody)) {
            throw new RxapOpenApiError('Found a reference object. The operation config is not expand!');
          }

          if (body === undefined && operation.requestBody.required) {
            this.validationError('The request body is required!', strict);
          }

          if (operation.requestBody.content && operation.requestBody.content['application/json']) {

            const schema = operation.requestBody.content['application/json'].schema;

            if (schema) {
              if (!this.validate(schema, body)) {
                this.validationError('The request body is not valid!', strict, schema, body);
              }
            }

          }

        }

      }

      public validationError(
        message: string,
        strict: boolean,
        schema?: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
        value?: any,
      ) {
        if (isDevMode() && schema !== undefined) {
          console.debug(
            message,
            {
              schema,
              value,
            },
          );
        }
        if (strict || SchemaValidationMixin.STRICT) {
          throw new RxapOpenApiError(message, '', 'SchemaValidationMixin');
        } else {
          console.warn(message);
        }
      }

      public buildHttpParams(
        operationParameters: OpenAPIV3.ParameterObject[],
        parameters?: Parameters,
        ignoreUndefined = true,
      ): HttpParams {

        let params = new HttpParams();

        if (IsRecord(parameters)) {

          for (const parameter of operationParameters.filter(p => p.in === 'query')) {
            const parameterName = parameter.name;

            if (parameters.hasOwnProperty(parameter.name)) {

              const value = parameters[parameter.name];

              if (value === undefined && ignoreUndefined) {
                continue;
              }

              if (Array.isArray(value)) {

                if (value.length) {

                  const first = value.shift();

                  params = params.set(parameterName, typeof first === 'object' ? JSON.stringify(first) : first);

                  for (const item of value) {
                    params = params.append(parameterName, typeof item === 'object' ? JSON.stringify(item) : item);
                  }

                }

              } else {
                params = params.set(parameter.name, typeof value === 'object' ? JSON.stringify(value) : value);
              }

            }

          }

        }

        return params;

      }

      public buildHttpHeaders(operationParameters: OpenAPIV3.ParameterObject[], parameters?: Parameters): HttpHeaders {

        const headers: Record<string, string | number | string[] | number[]> = {};

        if (IsRecord(parameters)) {

          for (const parameter of operationParameters.filter((p) => p.in === 'header')) {
            if (parameters.hasOwnProperty(parameter.name)) {
              const value = parameters[parameter.name];

              if (Array.isArray(value)) {
                if (value.length) {
                  if (value.length === 1) {
                    const first = value.shift();
                    headers[parameter.name] =
                      typeof first === 'object' ? JSON.stringify(first) : first;
                  } else {
                    headers[parameter.name] = value.map((item) =>
                      typeof item === 'object' ? JSON.stringify(item) : item
                    );
                  }
                }
              } else {
                headers[parameter.name] =
                  typeof value === 'object' ? JSON.stringify(value) : value;
              }
            }
          }

        }

        return new HttpHeaders(headers);

      }

      public buildHttpPathParams(
        operationParameters: OpenAPIV3.ParameterObject[],
        parameters?: Parameters,
      ): Record<string, string> {

        const pathParams: Record<string, any> = {};

        if (IsRecord(parameters)) {

          for (const parameter of operationParameters.filter(p => p.in === 'path')) {

            if (parameter.name in parameters) {
              pathParams[parameter.name] = this.normalizePathParamValue(parameters[parameter.name], parameter.name);
            }

          }

        }

        return pathParams;

      }

      protected normalizePathParamValue(value: any, paramName: string): string {
        if (!value) {
          throw new Error(`The path parameter '${paramName}' is empty or undefined!`);
        }
        if (Array.isArray(value)) {
          if (value.length === 0) {
            throw new Error(`The path parameter '${paramName}' is an empty array!`);
          }
          return value.map((item, index) => this.normalizePathParamValue(item, `${ paramName }[${index}]`)).join('/');
        } else if (typeof value === 'object') {
          return encodeURIComponent(JSON.stringify(value));
        } else if (typeof value === `string`) {
          return encodeURIComponent(value);
        }
        return value.toString();
      }

      /**
       * Converts open api parameters into the corresponding http options to
       * create a http request. The transformation is guided by the openapi definition
       *
       * @param operation
       * @param parameters
       * @param requestBody
       * @param ignoreUndefined
       */
      public buildHttpOptions(
        operation: OperationObjectWithMetadata,
        parameters?: Parameters,
        requestBody?: RequestBody,
        ignoreUndefined = true,
      ): HttpRemoteMethodParameter {

        const options: HttpRemoteMethodParameter = {};

        const operationParameters = coerceArray(operation.parameters);

        if (!NotContainsReferenceObjects<OpenAPIV3.ParameterObject>(operationParameters)) {
          throw new RxapOpenApiError('The operation parameters contains ReferenceObject!');
        }

        const params = this.buildHttpParams(operationParameters, parameters, ignoreUndefined);
        const headers = this.buildHttpHeaders(operationParameters, parameters);
        const pathParams: Record<string, any> = this.buildHttpPathParams(operationParameters, parameters);

        if (params.keys().length) {
          options.params = params;
        }

        if (headers.keys().length) {
          options.headers = headers;
        }

        if (Object.keys(pathParams).length) {
          options.pathParams = pathParams;
        }

        if (requestBody !== undefined) {
          const [ body, contentType ] = this.buildBody(operation, requestBody);
          if (body !== undefined) {
            options.body = body;
            if (contentType !== undefined) {
              options.headers ??= new HttpHeaders();
              options.headers.set('Content-Type', contentType);
            }
          }
        }

        return options;

      }

      private buildBody(operation: OperationObjectWithMetadata, requestBody: RequestBody): [ any, string | undefined ] {
        const accept: string[] = [];
        if (operation.requestBody && !IsReferenceObject(operation.requestBody)) {
          if (operation.requestBody.content) {
            for (const contentType of Object.keys(operation.requestBody.content)) {
              accept.push(contentType);
            }
          }
        }
        if (!accept.length) {
          console.warn('No content type found for the request body! Omitting the body!');
          return [ undefined, undefined ];
        }
        if (accept.length > 1) {
          console.warn('Multiple content types found for the request body! Using the first one!');
        }
        const contentType = accept[0];
        switch (contentType) {

          case 'application/json':
            assertsObject(requestBody);
            return [ requestBody, contentType ];

          case 'application/x-www-form-urlencoded':
            assertsObject(requestBody);
            // eslint-disable-next-line no-case-declarations
            let params = new HttpParams();
            for (const [ key, value ] of Object.entries(requestBody)) {
              params = params.set(key, value);
            }
            return [ params.toString(), contentType ];

          case 'multipart/form-data':
            assertsObject(requestBody);
            // eslint-disable-next-line no-case-declarations
            const formData = new FormData();

            // Iterate through the JSON object and append each field to FormData
            for (const [ key, value ] of Object.entries(requestBody)) {
              formData.append(key, value);
            }
            return [ formData, contentType ];

          default:
            return [ requestBody, contentType ];

        }

      }

      private validate(schema: string | boolean | object, value: any): boolean {

        if (this.disableSchemaValidation) {
          return true;
        }

        if (typeof schema !== 'object') {
          throw new Error('The schema must be an object!');
        }

        if (!schema) {
          throw new Error('The schema must not be null or undefined!');
        }

        let result: boolean | PromiseLike<any>;

        const ajv = new Ajv();
        const validate = ajv.compile(schema);

        try {
          result = validate(value);
        } catch (e: any) {
          console.error(e.message);
          return false;
        }

        if (isPromiseLike(result)) {
          throw new Error(
            'Async schema validation is not yet supported. Ensure the all refs in the openapi schema are internal!');
        }

        return result;
      }

    }

# results matching ""

# No results matching ""