Git Product home page Git Product logo

kurier's Introduction

Kurier

A TypeScript framework to create APIs following the 1.1 Spec of JSONAPI + the Operations proposal spec.

Features

  • Operation-driven API: JSONAPI is transport-layer independent, so it can be used for HTTP, WebSockets or any transport protocol of your choice.
  • Declarative style for resource definition: Every queryable object in JSONAPI is a resource, a representation of data that may come from any source, be it a database, an external API, etc. Kurier defines these resources in a declarative style.
  • CRUD database operations: Baked into Kurier, there is an operation processor which takes care of basic CRUD actions by using Knex. This allows the framework to support any database engine compatible with Knex. Also includes support for filtering fields by using common SQL operators, sorting and pagination.
  • Transport layer integrations: The framework supports JSONAPI operations via WebSockets, and it includes middlewares for Koa, Express and Vercel to automatically add HTTP endpoints for each declared resource and processor.
  • Relationships and sideloading: Resources can be related with belongsTo / hasMany helpers on their declarations. Kurier provides proper, compliant serialization to connect resources and even serve them all together on a single response.
  • Error handling: The framework includes some basic error responses to handle cases equivalent to HTTP status codes 401 (Unauthorized), 403 (Forbidden), 404 (Not Found) and 500 (Internal Server Error).
  • User/Role presence authorization: By building on top of the decorators syntax, Kurier allows you to inject user detection on specific operations or whole processors. The framework uses JSON Web Tokens as a way of verifying if a user is valid for a given operation.
  • Extensibility: Both resources and processors are open classes that you can extend to suit your use case. For example, do you need to serialize an existing, external API into JSONAPI format? Create a MyExternalApiProcessor extending from OperationProcessor and implement the necessary calls et voilà!.

Getting started

One-click way

Click right here to get started with TypeScript, a dockerized database, basic user management support, HTTP logs and more.

The second quickest possible way

Create your project using the GitHub CLI and with one of our starter packs:

# Create a TypeScript + Kurier API.
npx gh repo create my-api-with-kurier -p kurierjs/kurier-starter-pack-typescript

# Create a JavaScript + Kurier API.
npx gh repo create my-api-with-kurier -p kurierjs/kurier-starter-pack-javascript

The DIY way

Note: This example assumes a TypeScript environment with several dependencies preinstalled.

  1. Install the package with npm or yarn:

    $ npm i kurier # or yarn add kurier
  2. Create a Resource:

    import { Resource } from "kurier";
    
    export default class Author extends Resource {
      static schema = {
        attributes: {
          firstName: String,
          lastName: String,
        },
      };
    }
  3. Create an Application and inject it into your server. For example, let's say you've installed Koa in your Node application and want to expose JSONAPI via HTTP:

    import { Application, jsonApiKoa, KnexProcessor } from "kurier";
    import Koa from "koa";
    import Author from "./author";
    
    const app = new Application({
      namespace: "api",
      types: [Author],
      defaultProcessor: new KnexProcessor(/* your knex DB connection settings */),
    });
    
    const api = new Koa();
    
    api.use(jsonApiKoa(app));
    
    api.listen(3000);
  4. Run the Node app, open a browser and navigate to http://localhost:3000/api/authors. You should get an empty response like this:

    {
      "data": [],
      "included": []
    }
  5. Add some data to the "authors" table and go back to the previous URL. You'll start seeing your data!

    {
      "data": [
        {
          "id": 1,
          "type": "author",
          "attributes": {
            "firstName": "John",
            "lastName": "Katzenbach"
          }
        }
      ],
      "included": []
    }

Addons

Extend Kurier's features with these addons:

Build your own addon!

We've created a template repository for developers who want to build their own addons. Check it out here!

Starter packs

Jump-start your project with these preconfigured, opinionated starter packs. They all include a dockerized database, HTTP logs, linting and basic user management.

Documentation

Check out our updated docs at ReadTheDocs. There you will find more info and examples.

Contributing

We have a little contributors guide now! Take a look at it in here.

License

MIT

kurier's People

Contributors

aklkv avatar dependabot[bot] avatar ebryn avatar exelord avatar joelalejandro avatar marcemira avatar martinarbez avatar nelson6e65 avatar renanwilliam avatar rtablada avatar spersico avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

kurier's Issues

Relations are ignored when creating resources

[POST]

{
  "data": {
    "type": "comments",
    "attributes": {
      "body": "New message"
    },
    "relationships": {
      "user": {
        "data": { "type": "user", "id": "1" }
      }
    }
  }
}

Ignores the user relation completely, currently the work around is to add the userId: 1 to the attributes hash

[POST]

{
  "data": {
    "type": "comments",
    "attributes": {
      "body": "New message",
      "userId": "1"
    },
    "relationships": {
      "user": {
        "data": { "type": "user", "id": "1" }
      }
    }
  }
}

Add abstract transactional BulkOperationProcessor

I've edited this issue's description to include my analysis -- @joelalejandro

In order to implement /bulk operations, a new key { operations: Operation[] } must be defined in JsonApiDocument.

⚠️ operations cannot co-exist with data or errors.

An error should be triggered if this condition isn't met.

A bulk operation must be treated as a transaction:

T = { O1, O2, ..., On } => R

If any given Ox fails, all of T must be reverted:

T = O1 ==> O2 ==> O3 =/=> O4 = R
|___________________________|

Rollbacks are relatively trivial for processors derived of a KnexProcessor, where the database engine takes care of the transaction with little concern from JSONAPI-TS.

Implementing non-DB transactions (for later iterations)

However, for more generic processors, since we do not know what an operation could entail, we can't automatically figure out how to revert it.

We define RB(O) as a function around an operation O that can revert whatever impact (if any) ocurred from executing O.

RB(O) should only be necessary for add, update and remove, since get-ops should not have any side-effects.

At a semantic level, rollbacks would act as follows:

  • RB(add) = remove
  • RB(update) = update(Sn - 1), where S is a list of states for a given resource
  • RB(remove) = add

This could be auto-mapped and executed, but if an opertion has more complex side effects, the user should be able to provide definitions of how a rollback should proceed on such operations:

  • RB(add) = unadd = remove
  • RB(update) = unupdate = update
  • RB(remove) = unremove = add

A BulkOperationProcessor class should imply the following responsabilities:

  1. Maintain a list of every Operation (request) and every result (response). This is called a Transaction.
  2. Redirect execution of every Operation to the appropriate ResourceProcessor.
  3. Define a rollback operation for every write-like operation as described before.
  4. Extend from OperationProcessor with a generic resource class, since a bulk request can involve multiple operation types.

(1) is currently handled by the Application object. Maybe it's something that the BulkOperationProcessor has access to?

// Access current transaction
app.transaction.push(...);

This would require the existence of a Transaction class.

Currently, a Transaction is created by the app.createTransaction() function, which simply wraps the execution of operations in a Promise.all call.

A public API for a transaction must include:

  • A method to set which operation should be included in the transaction
  • An "execution" method that will attempt to run the operations; if it fails, it should delegate to a "rollback execution" method.

It should also hold a state array, defined as:

type State = {
  operation: Operation;
  success: boolean;
  error?: JsonApiError;
  result?: OperationResponse
}

express support

We can port our Koa middleware to use Express pretty easily

Write CRUD acceptance tests

@rtablada has added some basic infrastructure for tests.

Let's write tests that exercise CRUD operations via HTTP & the bulk operation endpoint

Essentially we'll make HTTP requests and assert their responses look as expected

Update documentation

Outdated sections:

  • Relationships (#153)
  • Schema definition (need to talk about primaryKeyName) (#153)
  • Authentication/Authorization (#153)

Missing sections:

  • Bulk operations (#151)
  • WebSocket support
    • Using the jsonApiWebSocket handler (#151)
    • Authenticating on WebSockets (#159)
  • Addons (#152)
  • Serialization (#160)

type detection isn't working correctly

When defining compound type names like supplyCategory, the OperationProcessor.resourceFor() function fails to return the correct type.

Expected: SupplyCategory
Received: Supplycategory

Putting some logs revealed this:

2019-02-18T14:13:28.597530346Z checking if  SupplyCategory  matches  Supplycategory

The following error was thrown after:

2019-02-18T14:13:28.598537676Z JSONAPI-TS:  TypeError: resourceClass is not a constructor
2019-02-18T14:13:28.598555998Z     at records.map.record (/opt/app/node_modules/@ebryn/jsonapi-ts/dist/processors/knex-processor.js:54:20)
2019-02-18T14:13:28.598561872Z     at Array.map (<anonymous>)
2019-02-18T14:13:28.598566464Z     at KnexProcessor.convertToResources (/opt/app/node_modules/@ebryn/jsonapi-ts/dist/processors/knex-processor.js:49:24)
2019-02-18T14:13:28.598574428Z     at KnexProcessor.get (/opt/app/node_modules/@ebryn/jsonapi-ts/dist/processors/knex-processor.js:19:21)
2019-02-18T14:13:28.598578273Z     at <anonymous>

[undefined].schema is trying multiple times to be accessed

JSONAPI-TS:  TypeError: Cannot read property 'schema' of undefined    at ResourceProcessor.get (/c/dev/jsonapi-ts/src/processors/knex-processor.ts:73:47)
    at ResourceProcessor.execute (/c/dev/jsonapi-ts/src/processors/operation-processor.ts:20:60)    at Application.executeOperation (/c/dev/jsonapi-ts/src/application.ts:29:40)
    at process._tickCallback (internal/process/next_tick.js:68:7)

Weird thing about this bug is that apparently doesn't break functionality, but it floods the console for every request I do, using the dummy app.

JsonApiError objects clutter the logs with non-error promise rejection warnings

Since we're using POJOs to represent errors in JSONAPI-TS, Node is throwing the following warning every time a JsonApiError is thrown in an async function:

(node:1626) Warning: a promise was rejected with a non-error: [object Object]

We should consider refactoring the error system to work with actual Error objects that can be converted into JsonApiError-like POJOs.

Reflexive relations

Reflexive relations 0 .. 1 between the same entity are not currently supported.

Example schema

export default class Comment extends Resource {
  static get type() {
    return 'comment';
  }

  static schema = {
    attributes: {
      body: String,
      created_at: String,
      updated_at: String,
      parentCommentId: String
    },
    relationships: {
      user: {
        type: () => User,
        belongsTo: true,
        foreignKeyName: "userId"
      },
      parentComment: {
        type: () => Comment,
        belongsTo: true,
        foreignKeyName: "parentCommentId"
      },
      thread: {
        type: () => Thread,
        belongsTo: true,
        foreignKeyName: "threadId"
      }
    }
  };
}

It errors out when I do this query: /comments?include=parentComment with this error output:

JSONAPI-TS: { error: table name "comments" specified more than once

Included Relationship Resources aren't valid JSONAPI

Expands upon #81 .

Basically, this is about enforcing the same resource structure to all resource shown in responses.

Included Resources aren't being return as valid JSONAPI resources, just extracted into the included property of the response.

For example, the response to a GET /comments/3?include=parentComment is:
image

when it should be:
image

The function responsible for this is:
https://github.com/ebryn/jsonapi-ts/blob/d352cef3e19294d8da2ce329b02db7773d7fd627/src/application.ts#L180

IfUser() should support multiple possible values when checking for an attribute

A common use case is to check if a user can execute an action because they have a certain role.

Roles are usually based in some sort of privilege escalation, let's suppose we have two roles interacting with resources Book, Author and Invoice.

  • member: can write to Book, Author
  • admin: can write to Book, Author (because he exceeds member) and Invoice

While a better way to express this could be with numeric comparison, it's no as readable.

Currently supported behavior works using .every(), so the logical chain is bound by AND.

The target syntax is:

@Authorize(IfUser("role", ["member", "admin"]))

If one of the given values matches, the condition passes.

mirage integration

We should build a simple Mirage emulation layer that allows existing Mirage users to easily swap in jsonapi-ts.

For example, inside your mirage config you can define API handlers like this:

this.get('/authors', function(schema, request) {

});

Which could be serviced by a AuthorsProcessor and the callback could be a shorthand for overriding the get method on it.

Error Handling

Currently if an operation fails because of a database constraint or what ever, I can't handle them gracefully

async add(op: Operation): Promise<any> {
    const { password, ...user } = op.data.attributes;

    if (!password) {
      throw {
        // At the very least, you must declare a status and a code.
        status: HttpStatusCode.BadRequest,
        title: "A password must be entered.",
        code: "no_password"
      } as JsonApiError;
    }

    user.salt = uuid()
    user.hashedPassword = hash(password, user.salt);

    op.data.attributes = user;
    try {
      return super.add({ ...op, params: {} });
    } catch (e) {
      throw {
        status: HttpStatusCode.BadRequest,
        title: "the email already exists",
        code: "duplicated_user"
      } as JsonApiError;
    }
  }

This is what I would like/expect, but I don't know if its the best approach.

Cannot query fields by NULL

The current implementation of filtersToKnex apparently converts null to 0, which isn't accurate. This prevents to lookup records by absent values.

    Object.keys(filters).forEach(key => {
      let value = filters[key];
      const operator = getOperator(filters[key]) || "=";

      if (value.substring(value.indexOf(":") + 1)) {
        value = value.substring(value.indexOf(":") + 1);
      }

      value = value !== "null" ? value : 0;

      processedFilters.push({
        value,
        operator,
        column: camelize(key)
      });

IfUser() helper is not working

@Authorize is passing through without checking if the condition provided by IfUser() is satisfied. Apparently the decorate() function isn't passing the arguments correctly.

[GET]/:id should return an object in data

[GET]comments/5

{
  "data": [
       { 
         "id": 5,
         "type": "comments",
         "attributes": {}
       }
    ]
}

Expected

{
  "data": { 
         "id": 5,
         "type": "comments",
         "attributes": {}
       }
}

When show => { "data": {} }
When index => { "data": [{}, {}] }

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.