This package has been deprecated

    Author message:

    Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.

    @serverless-contracts/core
    TypeScript icon, indicating that this package has built-in type declarations

    0.3.0 • Public • Published

    @serverless-contracts/core

    Generate and use type-safe contracts between your Serverless services.

    This package is part of the serverless-contracts project. See its documentation for more insights.

    Installation

    npm install @serverless-contracts/core

    or if using yarn

    yarn add @serverless-contracts/core

    Defining contracts

    ApiGateway

    ApiGateway is an AWS service that makes it possible to trigger lambda functions through HTTP. There are two types of ApiGateways (for more details, see AWS documentation):

    • HTTP API
    • REST API

    In our examples, we will use and HTTP API, but it is completely equivalent for REST APIs in terms of contracts.

    Let's create our first HttpApi contract. First we will need to define the subschemas for each part of our contract:

    • the id serves to uniquely identify the contract among all stacks. Please note that this id MUST be unique among all stacks. Use a convention to ensure unicity.
    • the path and the http method which will trigger the lambda
    • the integrationType: "httpApi" or "restApi"
    • then the different parts of the http request:
      • the path parameters: pathParametersSchema, which must correspond to a Record<string, string>
      • the query string parameters: queryStringParametersSchema, which must respect the same constraint
      • the headers: headersSchema, with the same constraint
      • the body bodySchema which is an unconstrained JSON schema
    • finally, the outputSchema in order to be able to validate the output of the lambda. It is also an unconstrained JSON schema.
    const pathParametersSchema = {
      type: 'object',
      properties: { userId: { type: 'string' }, pageNumber: { type: 'string' } },
      required: ['userId', 'pageNumber'],
      additionalProperties: false,
    } as const;
    
    const queryStringParametersSchema = {
      type: 'object',
      properties: { testId: { type: 'string' } },
      required: ['testId'],
      additionalProperties: false,
    } as const;
    
    const headersSchema = {
      type: 'object',
      properties: { myHeader: { type: 'string' } },
      required: ['myHeader'],
    } as const;
    
    const bodySchema = {
      type: 'object',
      properties: { foo: { type: 'string' } },
      required: ['foo'],
    } as const;
    
    const outputSchema = {
      type: 'object',
      properties: {
        id: { type: 'string' },
        name: { type: 'string' },
      },
      required: ['id', 'name'],
    } as const;
    
    const myContract = new ApiGatewayContract({
      id: 'my-unique-id',
      path: '/users/{userId}',
      method: 'GET',
      integrationType: 'httpApi',
      pathParametersSchema,
      queryStringParametersSchema,
      headersSchema,
      bodySchema,
      outputSchema,
    });

    Please note: In order to properly use Typescript's type inference:

    • All the schemas MUST be created using the as const directive. For more information, see json-schema-to-ts
    • If you do not wish to use one of the subschemas, you need to explicitely set it as undefined in the contract. For example, in order to define a contract without headers, we need to create it with:
    const myContract = new ApiGatewayContract({
      id: 'my-unique-id',
      path: '/users/{userId}',
      method: 'GET',
      integrationType: 'httpApi',
      pathParametersSchema,
      queryStringParametersSchema,
      headersSchema: undefined,
      bodySchema,
      outputSchema,
    });

    Provider-side usage

    Generate the lambda trigger

    In the config.ts file of our lambda, in the events section, we need to use the generated trigger to define the path and method that will trigger the lambda:

    export default {
      environment: {},
      handler: getHandlerPath(__dirname),
      events: [myContract.trigger],
    };

    This will only output the method and path. However, if you need a more fine-grained configuration for your lambda (such as defining an authorizer), you can use the getCompleteTrigger method.

    export default {
      environment: {},
      handler: getHandlerPath(__dirname),
      events: [myContract.getCompleteTrigger({ authorizer: 'arn::aws...' })],
    };

    The static typing helps here to prevent accidental overloading of path and method:

    export default {
      environment: {},
      handler: getHandlerPath(__dirname),
      events: [
        myContract.getCompleteTrigger({
          method: 'delete', // typescript will throw an error
        }),
      ],
    };

    Validate the lambda

    JSON Schemas are compatible with ajv and @middy/validator. You can use

    myContract.inputSchema;

    and

    myContract.outputSchema;

    in order to validate the input and/or the output of your lambda.

    Type the lambda input and output

    On the handler side, you can use the handler method on the contract to correctly infer the input and output types from the schema.

    const handler = myContract.handler(async event => {
      event.pathParameters.userId; // will have type 'string'
    
      event.toto; // will fail typing
      event.pathParameters.toto; // will also fail
    
      return { id: 'coucou', name: 'coucou' }; // also type-safe!
    });

    Consumer-side usage

    Simply call the axiosRequest method on the schema.

    await myContract.axiosRequest('https://my-site.com', {
      pathParameters: { userId: '15', pageNumber: '45' },
      headers: {
        myHeader: 'hello',
      },
      queryStringParameters: { testId: 'plop' },
      body: { foo: 'bar' },
    });

    All parameter types will be inferred from the schemas. The return type will be an axios response of the type inferred from the outputSchema.

    If you do not wish to use axios, you can use the type inference to generate request parameters with:

    myContract.getRequestParameters({
      pathParameters: { userId: '15', pageNumber: '45' },
      headers: {
        myHeader: 'hello',
      },
      queryStringParameters: { testId: 'plop' },
      body: { foo: 'bar' },
    });

    and then use them in your request.

    CloudFormation

    AWS CloudFormation is used by the Serverless Framework to manage resources. In certain cases, it may be necessary to share these resources between services. For example, authentication may be handled by a common authorizer, which should not be reimplemented on each service.

    The CloudFormation import/export syntax is very specific, but only one information is truly useful: the name of the export. This must be unique across CloudFormation stacks and serves as a global variable name for the related value.

    Defining a CloudFormation contract

    import { CloudFormationContract } from '@serverless-contracts/core';
    
    const myCloudFormationContract = new CloudFormationContract({
      name: 'mySuperExport',
    });

    Please note that here the export name is 'mySuperExport', and this value must be unique across stacks.

    Using a CloudFormation contract to export a value

    In the provider serverless.ts, add an Outputs key

    const serverlessConfiguration = {
      service: "my-provider-service",
    
      provider: {...},
      functions: {...},
      resources: {
        Resources: {...}
        Outputs: {
          MyAwesomeExport: myCloudFormationContract.exportValue({
            description: 'A nice description',
            value: { Ref: 'MyResourceLogicalId' },
          }),
        },
      },
    };

    Please note:

    • The Ref function is here an example, the CloudFormationContract is compatible with all CloudFormation functions. Please refer to the documentation for more examples
    • Here, the MyAwesomeExport key has no importance and is not taken into account for the export

    Using a CloudFormation contract to import a value

    In the consumer serverless.ts, you can use the import with:

    const serverlessConfiguration = {
      service: 'my-consumer-service',
      functions: {...},
      custom: {
        myImportedValue: myCloudFormationContract.importValue,
      },
    };

    The resolved imported value will be available as ${self:custom.myImportedValue} in your serverless files. See the Serverless variables documentation.

    About type inference

    TODO

    Install

    npm i @serverless-contracts/core

    DownloadsWeekly Downloads

    61

    Version

    0.3.0

    License

    MIT

    Unpacked Size

    182 kB

    Total Files

    85

    Last publish

    Collaborators

    • adriencaccia
    • fargito