Package detail

ts-japi

mathematic-inc32.3kMIT1.11.5

A highly-modular (typescript-friendly)-framework agnostic library for serializing data to the JSON:API specification

json, api, json-api, json:api

readme

ts:japi


{ts:japi}

node-current License: Apache 2.0

A highly-modular (typescript-friendly)-framework agnostic library for serializing data to the JSON:API specification

Features

  • This is the only typescript-compatible library that fully types the JSON:API specification and performs proper serialization.
  • Zero dependencies.
  • This is the only library with resource recursion.
  • The modular framework laid out here highly promotes the specifications intentions:
    • Using links is no longer obfuscated.
    • Meta can truly be placed anywhere with possible dependencies laid out visibly.
  • This library is designed to adhere to the specifications "never remove, only add" policy, so we will remain backwards-compatible.

Documentation

The documentation has everything that is covered here and more.

Installation

You can install ts-japi in your project's directory as usual:

npm install ts-japi

Getting Started

There are fives classes that are used to serialize data (only one of which is necessarily required).

You can check the documentation for a deeper insight into the usage.

Examples

You can check the examples and the test folders to see some examples (such as the ones below). You can check this example to see almost every option of Serializer exhausted.

Serialization

The Serializer class is the only class required for basic serialization.

The following example constructs the most basic Serializer: (Note the await)

import { Serializer } from '../src';
import { User } from '../test/models';
import { getJSON } from '../test/utils/get-json';

const UserSerializer = new Serializer('users');

(async () => {
  const user = new User('sample_user_id');

  console.log('Output:', getJSON(await UserSerializer.serialize(user)));

  // Output: {
  //  jsonapi: { version: '1.0' },
  //  data: {
  //   type: 'users',
  //   id: 'sample_user_id',
  //   attributes: {
  //     createdAt: '2020-05-20T15:44:37.650Z',
  //     articles: [],
  //     comments: []
  //   }
  //  }
  // }
})();

Links

The Linker class is used to generate a normalized document link. Its methods are not meant to be called. See the FAQ for reasons.

The following example constructs a Linker for Users and Articles:

import { Linker } from '../src';
import { User, Article } from '../test/models';
import { getJSON } from '../test/utils/get-json';

// The last argument should almost always be an array or a single object type.
// The reason for this is the potential for linking several articles.
const UserArticleLinker = new Linker((user: User, articles: Article | Article[]) => {
  return Array.isArray(articles)
    ? `https://www.example.com/users/${user.id}/articles/`
    : `https://www.example.com/users/${user.id}/articles/${articles.id}`;
});

// ! The rest of this example is just to illustrate internal behavior.
(async () => {
  const user = new User('sample_user_id');
  const article = new Article('same_article_id', user);

  console.log('Output:', getJSON(UserArticleLinker.link(user, article)));

  // Output: https://www.example.com/users/sample_user_id/articles/same_article_id
})();

Pagination

The Paginator class is used to generate pagination links. Its methods are not meant to be called.

The following example constructs a Paginator:

import { Paginator } from '../src';
import { User, Article } from '../test/models';
import { getJSON } from '../test/utils/get-json';

const ArticlePaginator = new Paginator((articles: Article | Article[]) => {
  if (Array.isArray(articles)) {
    const nextPage = Number(articles[0].id) + 1;
    const prevPage = Number(articles[articles.length - 1].id) - 1;
    return {
      first: `https://www.example.com/articles/0`,
      last: `https://www.example.com/articles/10`,
      next: nextPage <= 10 ? `https://www.example.com/articles/${nextPage}` : null,
      prev: prevPage >= 0 ? `https://www.example.com/articles/${prevPage}` : null,
    };
  }
  return;
});

// ! The rest of this example is just to illustrate internal behavior.
(async () => {
  const user = new User('sample_user_id');
  const article = new Article('same_article_id', user);

  console.log('Output:', getJSON(ArticlePaginator.paginate([article])));

  // Output: {
  //  first: 'https://www.example.com/articles/0',
  //  last: 'https://www.example.com/articles/10',
  //  prev: null,
  //  next: null
  // }
})();

Relationships

The Relator class is used to generate top-level included data as well as resource-level relationships. Its methods are not meant to be called.

Relators may also take optional Linkers (using the linker option) to define relationship links and related resource links.

The following example constructs a Relator for Users and Articles:

import { Serializer, Relator } from '../src';
import { User, Article } from '../test/models';
import { getJSON } from '../test/utils/get-json';

const ArticleSerializer = new Serializer<Article>('articles');
const UserArticleRelator = new Relator<User, Article>(
  async (user) => user.getArticles(),
  ArticleSerializer
);

// ! The rest of this example is just to illustrate some internal behavior.
(async () => {
  const user = new User('sample_user_id');
  const article = new Article('same_article_id', user);
  User.save(user);
  Article.save(article);

  console.log('Output:', getJSON(await UserArticleRelator.getRelationship(user)));

  // Output: { data: [ { type: 'articles', id: 'same_article_id' } ] }
})();

Metadata

The Metaizer class is used to construct generate metadata given some dependencies. There are several locations Metaizer can be used:

Like Linker, its methods are not meant to be called.

The following example constructs a Metaizer:

import { User, Article } from '../test/models';
import { Metaizer } from '../src';
import { getJSON } from '../test/utils/get-json';

// The last argument should almost always be an array or a single object type.
// The reason for this is the potential for metaizing several articles.
const UserArticleMetaizer = new Metaizer((user: User, articles: Article | Article[]) => {
  return Array.isArray(articles)
    ? { user_created: user.createdAt, article_created: articles.map((a) => a.createdAt) }
    : { user_created: user.createdAt, article_created: articles.createdAt };
});

// ! The rest of this example is just to illustrate internal behavior.
(async () => {
  const user = new User('sample_user_id');
  const article = new Article('same_article_id', user);

  console.log('Output:', getJSON(UserArticleMetaizer.metaize(user, article)));

  // Output: {
  //  user_created: '2020-05-20T15:39:43.277Z',
  //  article_created: '2020-05-20T15:39:43.277Z'
  // }
})();

Serializing Errors

The ErrorSerializer class is used to serialize any object considered an error (the attributes option allows you to choose what attributes to use during serialization). Alternatively (recommended), you can construct custom errors by extending the JapiError class and use those for all server-to-client errors.

The error serializer test includes an example of the alternative solution.

The following example constructs the most basic ErrorSerializer: (Note the lack of await)

import { ErrorSerializer } from '../src';
import { getJSON } from '../test/utils/get-json';

const PrimitiveErrorSerializer = new ErrorSerializer();

(async () => {
  const error = new Error('badness');

  console.log('Output:', getJSON(PrimitiveErrorSerializer.serialize(error)));

  // Output: {
  //  errors: [ { code: 'Error', detail: 'badness' } ],
  //  jsonapi: { version: '1.0' }
  // }
})();

Caching

The Cache class can be placed in a Serializer's cache option. Alternatively, setting that option to true will provide a default Cache.

The default Cache uses the basic Object.is function to determine if input data are the same. If you want to adjust this, instantiate a new Cache with a resolver.

Deserialization

We stress the following: Given that there are many clients readily built to consume JSON:API endpoints (see here), we do not provide deserialization. In particular, since unmarshalling data is strongly related to the code it will be used in (e.g. React), tighter integration is recommended over an unnecessary abstraction.

Remarks

There are several model classes used inside TS:JAPI such as Resource and Relationships. These models are used for normalization as well as traversing a JSON:API document. If you plan to fork this repo, you can extend these models and reimplement them to create your own custom (non-standard, extended) serializer.

FAQ

Why not just allow optional functions that return the internal Link Class (or just a URI string)?

The Link class is defined to be as general as possible in case of changes in the specification. In particular, the implementation of metadata and the types in our library rely on the generality of the Link class. Relying on user arguments will generate a lot of overhead for both us and users whenever the specs change.

Why does the Meta class exist if it is essentially just a plain object?

In case the specification is updated to change the meta objects in some functional way.

What is "resource recursion"?

Due to compound documents, it is possible to recurse through related resources via their resource linkages and obtain included resources beyond primary data relations. This is should be done with caution (see SerializerOptions.depth and this example)

For Developers

To get started in developing this library, run yarn install, yarn build and yarn test (in this precise order) to assure everything is in working order.

Contributing

This project is maintained by the author, however contributions are welcome and appreciated. You can find TS:JAPI on GitHub: https://github.com/mathematic-inc/ts-japi

Feel free to submit an issue, but please do not submit pull requests unless it is to fix some issue. For more information, read the contribution guide.

License

Copyright © 2020 mathematic-inc.

Licensed under Apache 2.0.

changelog

Changelog

1.11.5 (2025-01-14)

Bug Fixes

  • issue #98 by improving performance of recurseRelators (#99) (e92de4d)

1.11.4 (2024-05-13)

Bug Fixes

1.11.3 (2024-04-19)

Bug Fixes

  • issue 89: conditional logic handling links (#90) (a13aff9)

1.11.2 (2024-04-18)

Bug Fixes

  • better support for polymorphic inputs (#87) (f29f835)

1.11.1 (2024-04-17)

Bug Fixes

  • correctly serialize input array as array (#84) (37becd6)

1.11.0 (2024-04-08)

Features

1.10.1 (2024-04-06)

Bug Fixes

1.10.0 (2024-03-21)

Features

1.9.1 (2023-10-11)

Bug Fixes

  • fix logic where nested includes were not calculated correctly. Fixes #68 (#69) (701a7e6)

1.9.0 (2023-09-04)

Features

1.8.1 (2023-08-10)

Bug Fixes

  • allow relator serializer to be a getter (#63) (a55b4e9)

1.8.0 (2022-09-30)

Features

  • implement support for the include spec (#53) (fe4f276)

1.7.0 (2022-08-05)

Bug Fixes

  • ignore relationships if they're not set to support optional relations (#49) (7bd7d53)

1.6.2

Patch Changes

  • c5a8201: Add customisable relationship names

1.6.1

Patch Changes

  • d5cd4d1: Updated dependencies

1.6.0

Minor Changes

  • bd4c358: Add relatorDataCache in serialize method to avoid duplicate lookups during recurseRelators call

1.5.1

Patch Changes

  • e81d1b1: Adds assertions to issue-23 test (testing depth > 1)
  • Fixes #24

1.5.0

Minor Changes

1.4.0

Minor Changes

  • 3dc7c4c: Allow null for empty to-one relationships

[1.3.0] - 2020-06-23

Added

  • Added an isErrorDocument function to detect JSON:API Error documents. This function allows you to treat the argument as if it were an error document (there is obviously no way to know if it really is a JSON:API error document at runtime).
  • Added an isLikeJapiError function to detect JSON:API Error. This function allows you to treat the argument as if it were an JSON:API error (there is obviously no way to know if it really is a JSON:API error at runtime).

Changed

  • Exported a isPlainObject and isObjectObject functions from internal.

[1.2.7] - 2020-06-22

  • Fix for #10
  • Fix for #11

[1.2.6] - 2020-06-19

Changed

  • Changed user-level repo to org-level repo.
    • Links have been fixed in docs and README

[1.2.5] - 2020-06-19

Changed

  • Exported interfaces related to JSON:API.
    • The Error and Data document interfaces now require the "errors" and "data" properties respectively.
    • The Base document interface has been abstracted further by removing the "meta" property.
    • A new Meta document interface is now available for type-checking.

[1.2.4] - 2020-06-19

Changed

  • Smaller packaging

[1.2.3] - 2020-06-06

Changed

  • Updated license to Apache 2.0
  • Fixed some grammatical errors in README

[1.2.2] - 2020-05-27

Added

  • A new Cache class is now available to use for caching. You can set this in the cache option for a Serializer (use true if you want the built in cache).
  • With caching, there is a ~586% speed improvement (412,768 ops/sec over the previous 70,435 ops/sec). Without-caching rates have stayed the same.

[1.2.1] - 2020-05-27

Added

  • More keywords to package.json to help user search for this package.

[1.2.0] - 2020-05-26

So, ts-japi has only been released a few days, but after some significant use in my APIs, I have realized a few things:

  1. Linkers and certain classes should be allowed to parse nullish data (nullish meaning undefined or null).
  2. The relationships object should be allowed to have custom keys, not dependent on the relators options
    • Relators should always have a Serializer; otherwise, they wouldn't relate to any resource per se.
  3. Projections should be "choose included" or "choose excluded" similar to MongoDB's.
  4. The code can be faster.

With this in mind, here are the changes.

Changed

  • [Breaking Change] Every relator must define a Serializer as the second argument in its constructor (as opposed to the relator's options. Options can go in the third argument.
    • It may be subtle, but the reason for this lies in the fact relationships object must be keyed by the related object. If the relator has no serializer, then the relator has no related name, hence there is no canonical way to key the relationship.
    • We will now allow objects of relators to be defined as an option for relators on Serializers. By using objects, the key for the relationship generated by the relator will correspond to the same key for that of the relator's.
  • Several functional options now allow for nullish (null or undefined) arguments:
    • Resource Linkers can now type-safely use nullish arguments.
    • Resource Metaizers can now type-safely use nullish arguments.
  • Several plain options now allow for nullish (null or undefined):
    • Serializer projection option has changed significantly (see the option itself) with nullish values.
  • There is a ~33% speed improvement. (70,435 ops/sec over 52,843 ops/sec on a low-end Macbook Pro 15")

Added

  • Started a CHANGELOG to keep users updated.

Important Note

I want to say this IS unusual to have a breaking change without depreciation, but given the fact this package is only a few days old, I want to apologize if you are bothered by the above break. However, I will guarantee that API changes will go through depreciation before removal, so happy coding :)