Skip to content

Latest commit

 

History

History
247 lines (196 loc) · 7.88 KB

SCHEMAS.md

File metadata and controls

247 lines (196 loc) · 7.88 KB

Notes on using/defining Schemas

This document outlines some potential gotchas when using and consuming the Schemas defined in this library as well as some samples below to help with understanding/advanced use cases.

The below links have information about the defining Schemas and how the validation is performed.

JTD Schemas for requests and responses validation (Via AJV).

Language

In this document it may use language such as:

  • MUST → this is something you always do
  • MUST NOT → this is something that you should never do
  • SHOULD → this is something that is a recommendation and generally is a good idea to follow to avoid issues

Request/Response types vs single type

You may notice whilst the Schemas are being rolled out that the same object may be used for the request and the response.

However this may not be best practice and in the future it may be better to split out the request and the response requests out to have different types.

On the request you would have an optional (i.e. field?: string | null;) property on the request.

Then on the response you could include the the field on the response as a required property (i.e. field: string | null).

This approach would allow you to introduce a new field with backwards compatibility immediately whilst you update consumers to using the new type whilst also emitting the new property in responses.

Gotchas

Request/Response type name clash with defined types

When installing the Schemas using the LOKE cli tool if you have request/response objects that are not reference types (i.e. defined using properties, optionalProperties , etc) then the LOKE cli will create types for you in the following format MethodNameRequest and MethodNameResponse.

When defining your types (in the definitions section) you MUST NOT define a type that matches the above format.

For example if you had the following:

// implementation
interface CreateCustomerRequest {
 name: string;
}

interface Customer {
 id: string;
 name: string;
}

CreateCustomer({orgId: string; request: CreateCustomerRequest}): Promise<Customer>

// schema definition
{
 ...,
 definitions: {
   Customer: {
     properties: {
       id: { type: "string" }
       name: { type: "string" }
     }
   },
   CreateCustomerRequest: {
     properties: {
       name: { type: "string" }
     }
   }
 },
 methods: {
   CreateCustomer: {
     requestTypeDef: {
       properties: {
         orgId: {type: "string" }
         request: { ref: "CreateCustomerRequest" }
       }
     },
     responseTypeDef: { ref: "Customer" }
   }
 }
}

When installing the above with the LOKE cli it will generate the following types:

// types created by definitions
export type CreateCustomerRequest = {
  name: string;
}

export type Customer {
  id: string;
  name: string;
}

// request type created by LOKE cli, response type is Customer above
export type CreateCustomerRequest = {
  orgId: string;
  request: CreateCustomerRequest
}

As you can see in the types above there are two CreateCustomerRequest types and this is invalid (caused by explicitly defining CreateCustomerRequest as a type AND the LOKE cli automaticaly creating a request type for the createCustomer() method). To avoid this you MUST ensure that your type names never match the format generated by the LOKE cli.

If a better name for type cannot be determined you SHOULD prefix or suffix the type name to avoid the clash. Some examples that have been used are Rpc prefix or Body/Payload/Details suffix but be consistent with existing code.

To fix the above implementation/code we simply change the name of the type that we have defined ourselves (i.e. renamed CreateCustomerRequest to CreateCustomerRequestPayload).

NOTE: When installing the types with the LOKE cli this would still create the CreateCustomerRequest but as we have renamed our custom type to not match this pattern there won't be a clash.

// implementation (rename CreateCustomerRequest to CreateCustomerRequestPayload)
interface CreateCustomerRequestPayload {
  name: string;
}

interface Customer {
  id: string;
  name: string;
}

CreateCustomer({orgId: string; request: CreateCustomerRequest}): Promise<Customer>

// schema definition
{
  ...,
  definitions: {
    Customer: {
      properties: {
        id: { type: "string" }
        name: { type: "string" }
      }
    },
    CreateCustomerRequestPayload: { // change type name to avoid clash
      properties: {
        name: { type: "string" }
      }
    }
  },
  methods: {
    CreateCustomer: {
      requestTypeDef: {
        properties: {
          orgId: {type: "string" }
          request: { ref: "CreateCustomerRequestPayload" }
        }
      },
      responseTypeDef: { ref: "Customer" }
    }
  }
}

Date response type

When using Schemas you MUST_NOT use Date types in your request/response objects. Instead use strings formatted to iso8601/rfc3339 (i.e. "2023-01-20T14:22:23Z") in your implementation and types and use { type: "timestamp" } in the Schema.

Void response type

When using the new Schemas (and serviceWithSchema) it has been observed that returning a void return no longer is acceptable as this then causes the request to fail in the http-rpc-client library due to the request not being able to be converted to json.

If you are adding Schemas to existing rpc services you MUST update any methods that return void to instead return something or simply return null (including the typescript type for the method).

In the Schema method definition you simply do not define response and this will be converted to an any by the LOKE cli.

Undefined response type

If a property from a rpc service is field: string | undefined as apposed to field?: string this may have issues when defining the Schema. In the Schema definition to specify something as undefined is specifying that it is OPTIONAL.

When the intention is that the field is "blank", returning null instead of undefined SHOULD be used (i.e. field: string | null).

Methods that have no request object

When methods have no request (this is very rare) then you simply do not define a request in the Schema definition. This then gets set to an any by the LOKE cli.

NOTE: When using the types generated by the LOKE cli in this cases you may have to pass an empty object as the request.

Sample

Taking the typescript type see the sample included below.

interface Location {
  name: string;
  maxPatrons: number;
  latitude: number;
  longitude: number;
  website: string | null;
  type: "FAST_FOOD" | "CAFE";
  tags: string[];
  hasDanceFloor: boolean;
  createdAt: string; // iso8601/rfc3339 timestamp string
  address: {
    streetAddress: string;
    locality: string;
    region: string;
    postalCode: string;
    country: string;
  } | null;
}

NOTE: The below definition for numbers (both integer and floating point) are using the largest sizes available and are not using signed vs unsigned values as are available in the JTD specification. Please use this example as a guide and use the appropriate JTD property types as required.

You would define this type as follows in the schema

{
  properties: {
    name: { type: "string" },
    maxPatrons: { type: "int32" },
    latitude: { type: "float32" },
    longitude: {type: "float32" },
    website: { type: "string", nullable: true },
    type: { enum: ["FAST_FOOD", "CAFE" ] },
    tags: { elements: { type: "string" } },
    hasDanceFloor: { type: "boolean" },
    createdAt: { type: "timestamp" },
    address: {
      properties: {
        streetAddress: { type: "string" },
        locality: { type: "string" },
        region: { type: "string" },
        postalCode: { type: "string" },
        country: { type: "string" }
      },
      nullable: true
    }
  }
}