GithubHelp home page GithubHelp logo

sdk-core's Introduction

Plattar Logo

NPM

About

Facilitating Seamless Integration with Plattar Backend Services through Automated TypeScript SDK Generation and Runtime Support

Installation

  • Install using npm
npm install @plattar/sdk-core

Examples

Utilize the @plattar/sample-sdk example featuring Scene, Application, Page and File objects as references. Subsequently, substitute these samples with the corresponding objects from the SDK you are currently working with.

Important

Kindly be aware that the objects employed in these illustrations may vary based on the generated SDK produced by this module.

Service Configuration

Use Service.config() to set up a default global configuration that will be applied to all objects. Initialization options include unauthenticated, cookie authenticated, or token-based authentication.

Configuring Default Service without Authentication

import { Service } from "@plattar/sample-sdk";

Service.config({
    url: 'https://api.plattar.com'
});

Configuring Default Service with Cookie Authentication

import { Service } from "@plattar/sample-sdk";

Service.config({
    url: 'https://api.plattar.com',
    auth: {
        type: 'cookie'
    }
});

Configuring Default Service with Token Authentication

import { Service } from "@plattar/sample-sdk";

Service.config({
    url: 'https://api.plattar.com',
    auth: {
        type: 'token',
        token: 'your-plattar-auth-token'
    }
});

Handling Service Errors

The Service offers multiple error-handling configuration options. By default, errors are logged using console.error(). Your available options include:

  • silent: Does not log or throw any errors and silently returns.
  • console.error: Logs the error using console.error() and returns.
  • console.warn: Logs the error using console.warn() and returns.
  • throw: Throws the error, requiring you to catch it using a try/catch clause.
import { Service } from "@plattar/sample-sdk";

Service.config({
    url: 'https://api.plattar.com',
    options: {
      errorHandler: 'silent'
    }
});

You have the option to supply your own error listener, which receives all errors irrespective of the errorHandler setting. This feature is beneficial for analytics or serving as a global catch-all. It is set to null by default.

import { Service, CoreError } from "@plattar/sample-sdk";

Service.config({
    url: 'https://api.plattar.com',
    options: {
      errorHandler: 'silent',
      errorListener: (error:CoreError) => {
        console.error(error);
      }
    }
});

Basic Object Queries

Employ the predefined objects to make API queries. Each SDK comes with its unique set of objects and query functions. Consult the documentation of the SDK you are using for detailed information.

Individual Object Query

Some queries exclusively yield a single object instance. In such cases, the result will either be the object or null.

import { Scene } from "@plattar/sample-sdk";

const myScene:Scene | null = await new Scene("your-scene-id").query().get();

Alternatively, you have the option to execute the same query using the following approach

import { Scene } from "@plattar/sample-sdk";

const myScene:Scene | null = await Scene.query().get({id: "your-scene-id" });

Multiple Object Query

Some queries result in multiple objects due to the query type. In these instances, the outcome will be an array.

import { Scene } from "@plattar/sample-sdk";

const myScenes:Array<Scene> = await Scene("your-scene-id").query().list();

sdk-core's People

Contributors

davidarayan avatar

Watchers

Clark avatar  avatar  avatar

sdk-core's Issues

Proposed Interface & Examples #1

About

This is a proposed high-level interface for the new @plattar/api-sdk that takes full advantage of the new V3 API query engine. The new SDK will be written in TypeScript.

For the legacy SDK written in JavaScript see plattar-api.

This design is superseeded by sdk-core Issue 2 and should only be used as historical reference.

Setup

interfaces & skeleton-code

Expose the following baseline functionality to setup the SDK

interface ServiceConfig {
    // primary api url
    readonly url: string;
    // optionally disable TLS (for non-secure NodeJS) - enabled by default
    readonly tls?: boolean;
}

class Service {
    // these will automatically setup a default internally accessible service
    // if called more than once, will replace the last instance with the new one
    public static config(config:ServiceConfig):Service;
    public static setup(service: "staging" | "production" | "dev"):Service;
}

examples

Run the following Code to setup the SDK

  • Option 1 - manual setup
// staging server - manual setup with a provided configuration
const service:Service = Service.config({url: "https://api.plattar.space", tls: false});
  • Option 2 - automatic setup
// staging server - automatic setup
const service:Service = Service.setup("staging");

Queries

interfaces & skeleton-code

Expose the following baseline functionality for API Queries

abstract class PlattarObject {
    // properties to be defined
    public static query():Query;
}

class Scene extends PlattarObject {}
class Application extends PlattarObject {}

class Query {
    // allows running a query from a provided instance as a relationship
    public from(instance:PlattarObject):this;
    // allows either data-filtering or loose searching
    public where(variable:string, operation: "==" | "!=" | ">" | "<" | ">=" | "<=", value:string | number | boolean):this;
    // allows sparse-fieldsets (only returns provided fields)
    public fields(...fields:string):this;
    // further serialise relationships
    public include(...objects:PlattarObject):this;
    // returns soft-deleted or partially-deleted objects as part of the query
    public deleted(...objects:PlattarObject):this;
    // perform sorting operations
    public sort(variable:string, operation: "ascending" | "descending"):this;
    // perform pagination (only return objects matching page)
    public page(pageNumber:number, numberOfObjects:number):this;

    // returns a list of all objects matching the query
    public async get(service?:Service): Promise<PlattarObject[]>;
    // returns the first object matching the query
    public async first(service?:Service): Promise<PlattarObject | null>;
    // checks if an object matching the provided query exists
    public async exists(service?:Service): Promise<boolean>;
}

examples

Run the following Code to query the API

example 1 - getting a list of scenes from application

https://api.plattar.space/v3/application/cbdf/scene?filter[scene.scene_type][eq]=default&include=scene.application&sort=scene.updated_at
// get a list of all scenes related to application
const scenes:Scene[] = await Scene.query().from(new Application("cbdf")).where("scene_type", "==", "default").include(Application).sort("updated_at", "ascending").get();

example 2 - getting a single scene with some queries and an includes

https://api.plattar.space/v3/scene/cbdf?include=scene.application&fields[scene]=title,custom_json,application_id
// get a scene matching id
const scene:Scene | null = await Scene.query().where("id", "==", "cbdf").include(Application).fields("title", "custom_json", "application_id").first();

example 3 - just getting a plain-old scene without anything fancy

https://api.plattar.space/v3/scene/cbdf
// get a scene
const scene:Scene | null = await Scene.query().where("id", "==", "cbdf").first();

Shorter/Faster Queries

These are far less flexible than when using queries, but allows to perform the simple things faster with less code.

To use things like field-sets, sorting or includes must use the Query type.

example 1 - just getting a plain-old scene without anything fancy

https://api.plattar.space/v3/scene/cbdf
// get a scene using a query
const scene:Scene | null = await Scene.query().where("id", "==", "cbdf").first();
// get a scene using a shorthand/faster query
const scene:Scene = await new Scene("cbdf").get();

MVP Example

The most minimal working example using the above interfaces

Service.setup("staging");
const scene:Scene = await new Scene("cbdf").get();
console.log(scene);

Get one object from staging and another from production

const stagingService:Service = Service.setup("staging");
const productionService:Service = Service.setup("production");

const stagingScene:Scene = await new Scene("cbdf").get(stagingService);
console.log(stagingScene);

const productionScene:Scene = await new Scene("cbdf").get(productionService);
console.log(productionScene);

Proposed Relations Interfaces & Examples

About

All objects have related object types, for example Application object has a relation to Scene and vice-versa. Relation objects can be fetched either individually or through an includes query.

Proposal

We propose the following interface for dealing with object relations. Assume the following objects.

import { Application, Scene } from "@plattar/sdk";

Fetching related objects

As an array or list

const application:Application = new Application("app_id");
const scenes:Array<Scene> = await application.relationships.get(Scene);

As an individual object or null

const application:Application = new Application("app_id");
const scene:Scene | null = await application.relationships.first(Scene);

Optional search operations

As an array or list

const mySearch = (scene:Scene):boolean => {
    return scene.id === "my_id";
}

const application:Application = new Application("app_id");

// This will only return objects that matches the search function
const scenes:Array<Scene> = await application.relationships.get(Scene, mySearch);

Search operations can also be performed on individual objects, however search is terminated as soon as first object matches the query.

const mySearch = (scene:Scene):boolean => {
    return scene.id === "my_id";
}

const application:Application = new Application("app_id");

// This will only return objects that matches the search function
const scenes:Scene | null = await application.relationships.first(Scene, mySearch);

Using includes query operations

Relationships can be constructed when the initial object is fetched. Whilst relationship queries are still async because of the internal cache, the function will resolve immedietly.

const application:Application = await (new Application("app_id").query().include(Scene).get());

// even though this is async, the relationships.get() will resolve instantly
const scenes:Array<Scene> = await application.relationships.get(Scene);

Alternatively, the cached objects from the include query can be fetched without requiring an async operation. The cache supports the same functions as the relationships object.

const application:Application = await (new Application("app_id").query().include(Scene).get());
const scenes:Array<Scene> = application.relationships.cache.get(Scene);

Notes

Once an object relation is fetched once, it will not be fetched again for subsequent operations. To force a re-fetch of object relations a supported function is added to the relationships.cache object that works as follows.

const application:Application = await (new Application("app_id").query().include(Scene).get());
const scenes:Array<Scene> = await application.relationships.get(Scene);

// This will not actually re-fetch scenes from the API due to cache unless the cache is cleared
const newScenes:Array<Scene> = await application.relationships.get(Scene);

Clear cache to force a re-fetch for absolute up-to-date data

const application:Application = await (new Application("app_id").query().include(Scene).get());
const scenes:Array<Scene> = await application.relationships.get(Scene);

// cache.clear() can also accept a list of objects or nothing, if nothing is passed all cache is cleared
application.relationships.cache.clear(Scene);

// Since cache was cleared, this will re-fetch all Scene types
const newScenes:Array<Scene> = await application.relationships.get(Scene);

Proposed Interface & Examples #2

About

This is an alternative interface proposal for sdk-core based on the initial designs as outlined in sdk-core Issue 1.

Some of these issues were identified from the initial proposal.

  1. The plattar namespace should probably be removed as both api-core and sdk-core are now multi-product and multi-businesses codebases
  2. The role of sdk-core has changed to accomodate multiple services rather than a single service and will be used as a base for generative SDK based on api-core module
  3. Generative SDK runs automatically without developer interference based on the setup of api-core. This removes the need to manually create SDK's for interfacing with api-core services.

Proposal

⚠️ The following proposal is a guide only and the final implementation may change due to technical or structural reasons.

Setup

The initial setup of the SDK needs to accomodate the fact that a single source/website might need to interface with multiple independent backends. Everything should have reasonable defaults with expected outputs.

  1. Consider that a single website might require interfacing with multiple backends independently
  2. Consider that this tool will be used for generating an SDK for multiple services and as such, multiple SDK's might be used in a single project
export interface ServiceAuth {
    // when type == "cookie", the SDK will use the cookies as an auth type
    // when type == "token", the SDK will use the supplied token as an auth type
    readonly type: 'cookie' | 'token';

    // this needs to be set if type == 'token'. It will be ignored if type == 'cookie'
    readonly token?: string;

    // optionally disable TLS (for non-secure NodeJS) - disabled by default
    readonly tls?: boolean;
}

export interface ServiceConfig {
    // primary api url - eg: `https://api.plattar.com`
    readonly url: string;

    // optionally provide an authentication method - defaults to `cookie`
    readonly auth?: ServiceAuth;
}

// abstract class forces implementation by the generated SDK
export abstract class Service {
    // these will automatically setup a default internally accessible service
    // if called more than once, will replace the last instance with the new one
    public static config(config:ServiceConfig):Service;

    // this will return the current default configured Service instance
    // if no service is available/configured, this will throw an Error
    public static get default(): Service;
}

Example

Simple setup can be done as following. This needs to be done before the SDK can be used. Generated SDK should always setup a default version of the Service.

Service.config({
    url: 'https://api.plattar.com'
});

More advanced version of a setup can look like the following - for example in NodeJS environments

Service.config({
    url: 'https://api.plattar.com',
    auth: {
        type: 'token',
        tls: true,
        token: 'my-plattar-auth-token'
    }
});

The default configuration (if none provided) will look as follows.

⚠️ The Generative SDK tool for each independent service will be REQUIRED to provide a default configuration for the respective service

// This is the default config if none is provided/setup - url will change based on generated SDK
Service.config({
    url: 'https://api.plattar.com',
    auth: {
        type: 'cookie',
        tls: false,
        token: undefined
    }
});

Base Object

All api-core object types extends the base core object to provide built-in functionality for interfacing with the API.

export abstract class CoreObject extends CoreQuery {
    // returns a new Query instance for this Core Object type.
    // optionally pass a Service instance to use, if missing will use Service.default instead
    public static query(service?:Service):Query;

    // force an implementation on the object type from inherited code
    // this will throw an Error if not implemented properly 
    public static get type():string;
}

Example

⚠️ The SDK generator will need to extend the CoreObject type to inherit functionality and extend it.

export class Scene extends CoreObject {
    public override static get type():string { return "scene"; }
}
export class Application extends CoreObject {
    public override static get type():string { return "application"; }
}

Queries

Queries is the primary method for interfacing with the API using the built-in query engine.

⚠️ These Queries run on the Server to optimise database operations for faster results from the API so use them whenever possible to reduce latency and increase performance

// this will be extended and filled in by the generator to pass required url parameters into the query engine
// for example /v3/scene/:id would contain `id` as a required parameter
// this is an empty interface by default
export interface QueryParameters {}

// this would be extended and filled in by the generator to pass required attributes for specific requests
// this is an empty interface by default
export interface QueryAttributes {}

// each object type will extend the Query directly
export abstract class CoreQuery {
    // allows either data-filtering or loose searching
    public abstract where(variable:string, operation: "==" | "!=" | ">" | "<" | ">=" | "<=", value:string | number | boolean):this;

    // allows sparse-fieldsets (only returns provided fields)
    public abstract fields(...fields:string):this;

    // further serialise relationships
    public abstract include(...objects:CoreObject):this;

    // returns soft-deleted or partially-deleted objects as part of the query
    public abstract deleted(...objects:CoreObject):this;

    // perform sorting operations
    public abstract sort(variable:string, operation: "ascending" | "descending"):this;

    // perform pagination (only return objects matching page)
    public abstract page(pageNumber:number, numberOfObjects:number):this;
}

Examples

Run the following Code to query the API

⚠️ The names of functions that performs the query are filled in based on the name of the endpoint as defined in api-core so they might change later on. For example, the get-by endpoint is named refer and is generated automatically. Same with the simple get endpoint for a single object.

👍 A shortcut of Application.query() functions is also available in the base object, so the following Application.query().get() is the same as Application.get() as CoreObject also extends CoreQuery type.

example 1 - getting a list of scenes from application

https://api.plattar.space/v3/application/cbdf/scene?filter[scene.scene_type][eq]=default&include=scene.application&sort=scene.updated_at
// get a list of all scenes related to application
const scenes:Scene[] = await Application.query().where("scene_type", "==", "default").include(Application).sort("updated_at", "ascending").refer({by:Scene, id:'cbdf'});

example 2 - getting a single scene with some queries and an includes with limited fields

https://api.plattar.space/v3/scene/cbdf?include=scene.application&fields[scene]=title,custom_json,application_id
// get a scene matching id
const scene:Scene | null = await Scene.query().include(Application).fields("title", "custom_json", "application_id").get({id: 'cbdf'});

example 3 - just getting a plain-old scene without anything fancy

https://api.plattar.space/v3/scene/cbdf
// get a scene
const scene:Scene | null = await Scene.query().get({id:'cbdf'});

example 4 - making a brand new scene

POST https://api.plattar.space/v3/scene
// create a new scene
const scene:Scene | null = await Scene.query().create({title: 'cbdf'});

👍 Another way of creating a new Scene is as follows, however need to be aware that this will override ALL attributes in the remote Scene

// create a new scene
const scene:Scene = new Scene();
scene.attributes.title = "cbdf";
const createdScene:Scene | null = await scene.query().create();

example 5 - update an existing scene

PUT https://api.plattar.space/v3/scene/cbdf
// initialise or GET an existing Scene instance
const scene:Scene | null = await Scene.query().get({id: 'cbdf'});
scene.attributes.title = "my new title";
const updatedScene:Scene | null = await scene.query().update();

👍 Another way of updating an existing Scene is as follows, however need to be aware that this will override ALL attributes in the remote Scene

// initialise or GET an existing Scene instance
const scene:Scene = new Scene("cbdf);
scene.attributes.title = "my new title";
const updatedScene:Scene | null = await scene.query().update();

Add exponential backoff mechanism

About

Occasionally, backend requests may encounter failures stemming from timeout issues or temporary resource unavailability caused by bottlenecks. Exponential Backoff facilitates automatic retries for resource retrieval before reaching a total failure. While this feature can be deactivated, it is enabled by default.

If options.retry is unspecified, it will default to {tries: 3}, with a maximum limit of 10 for options.retry. Exponential Backoff incorporates a retry timer, starting at rand(0,30) and increasing according to a specific algorithm with jitter.

During retries with exponential backoff, errors will not be reported at each attempt; only the final failure state will be reported.

The calculation for the wait time is defined as follows: const wait: number = rand(0, min(500, 15 * 2^attempt));

Consequently, the following table outlines the generated wait times:

  • Attempt 1: wait = rand(0, 30)
  • Attempt 2: wait = rand(0, 60)
  • Attempt 3: wait = rand(0, 120)
  • Attempt 4: wait = rand(0, 240)
  • Attempt 5: wait = rand(0, 480)
  • Attempt 6 - 10: wait = rand(0, 500)

Beyond 10 attempts (as the maximum), an error will be thrown.

Design

Service.config({
    url: '<BACKEND URL>',
    options: {
        retry: {
            tries: 5
        }
    }
});

Upgrade api-core module

About

Upgrade to the latest version of api-core that contains internal changes requiring an update to the generator

Add support for Join Queries

About

Adding the ability to join multiple queries into a single request allows performing queries/search on the server to reduce incoming payload/document size and thus increase overall performance.

Examples

The CoreQuery interface exposes a join function that accepts either a single or a list of external Queries. These queries are then joined into a single request and processed by the server.

Consider an example where you'd like to fetch a Scene from an Application that matches a certain keyword

const application: Application = new Application("app_id");

// this will include Scene in relationships only if Scene.title is roughly equal to my_title
const result: Application | null = await application.query()
    .include(Scene)
    .join(
        Scene.query().where("title", "~=", "my_title")
    ).first();

// this will only contain the list of Scenes whos title is roughly equal to my_title
const relatedScenes: Array<Scene> = result.relationships.cache.get(Scene);

The above code essentially generates the following request

../application/app_id?include=scene&query[scene.title]=my_title

Another (more complicated) example is as follows where multiple relations might be needed

const yesterday:string = new Date().setDate(new Date().getDate() - 1).toString();

const application: Application = new Application("app_id");

// this will include Scene in relationships only if Scene.title is roughly equal to my_title
const result: Application | null = await application.query()
    .include(Scene, Page)
    .join(
        Scene.query().where("title", "~=", "my_title"),
        Page.query().where("updated_at", ">=", yesterday)
    ).first();

// this will only contain the list of Scenes whos title is roughly equal to my_title
const relatedScenes: Array<Scene> = result.relationships.cache.get(Scene);

// this will only contain the list of Pages who's updated_at is >= yesterday
const relatedPages: Array<Page> = result.relationships.cache.get(Page);

The above code essentially generates the following request

../application/app_id?include=scene,page&query[scene.title]=my_title&?filter[page.updated_at][ge]=yesterday

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.