Skip to content

mathematic-inc/ts-japi

Repository files navigation

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.

Installation

npm install ts-japi

Getting Started

There are several classes used to serialize data (only Serializer is strictly required):

  • Serializer — primary resource serialization
  • Relator — relationships and included resources
  • Linker — document and resource links
  • Metaizer — metadata at any level
  • Paginator — pagination links
  • ErrorSerializer — error serialization
  • Cache — response caching
  • PolymorphicSerializer — polymorphic resource serialization

Examples

See the examples and test directories for usage. The full example shows nearly every Serializer option in use.

Serialization

Serializer is the only class required for basic serialization.

import { Serializer } from 'ts-japi';

const UserSerializer = new Serializer('users');

const user = { id: 'sample_user_id', createdAt: new Date() };

console.log(await UserSerializer.serialize(user));
// {
//   jsonapi: { version: '1.0' },
//   data: {
//     type: 'users',
//     id: 'sample_user_id',
//     attributes: { createdAt: '2020-05-20T15:44:37.650Z' }
//   }
// }

Links

Linker generates normalized document links. Its methods are not meant to be called directly — pass it to a serializer option.

import { Linker } from 'ts-japi';

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}`;
});

Pagination

Paginator generates pagination links.

import { Paginator } from 'ts-japi';

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;
});

Use it via SerializerOptions.linkers.paginator.

Relationships

Relator generates top-level included data and resource-level relationships.

import { Serializer, Relator } from 'ts-japi';

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

const UserSerializer = new Serializer<User>('users', {
  relators: UserArticleRelator,
});

Relator also accepts optional Linkers via the linkers option to define relationship and related resource links.

Metadata

Metaizer generates metadata. It can be used in:

  • ErrorSerializerOptions.metaizers
  • RelatorOptions.metaizer
  • SerializerOptions.metaizers
  • LinkerOptions.metaizer
import { Metaizer } from 'ts-japi';

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 };
});

Serializing Errors

ErrorSerializer serializes any object as an error. Alternatively (recommended), extend JapiError to construct typed server errors.

import { ErrorSerializer } from 'ts-japi';

const PrimitiveErrorSerializer = new ErrorSerializer();

console.log(PrimitiveErrorSerializer.serialize(new Error('badness')));
// {
//   errors: [ { code: 'Error', detail: 'badness' } ],
//   jsonapi: { version: '1.0' }
// }

Caching

Set cache: true in SerializerOptions for a default Cache, or pass a Cache instance for custom equality logic.

import { Serializer, Cache } from 'ts-japi';

const MyCache = new Cache({ resolver: (a, b) => a?.id === b?.id });
const UserSerializer = new Serializer('users', { cache: MyCache });

Deserialization

This library does not provide deserialization. Many clients already consume JSON:API endpoints (see implementations), and unmarshalling data is typically coupled to framework-specific code (e.g. React state). Tighter integration is recommended over an unnecessary abstraction.

API Reference

Serializer

new Serializer<PrimaryType>(collectionName: string, options?: Partial<SerializerOptions<PrimaryType>>)

Methods:

Method Description
serialize(data, options?) Serializes primary data. Returns a Promise<DataDocument>.
getRelators() Returns the relators associated with this serializer.
setRelators(relators) Sets relators (useful for breaking cyclic dependencies).
getIdKeyFieldName() Returns the name of the id field.

SerializerOptions<PrimaryType>:

Option Type Default Description
idKey keyof PrimaryType "id" The field name for the resource ID.
version string | null "1.0" JSON:API version. Set to null to omit.
relators Relator | Relator[] | Record<string, Relator> Relators that generate relationships and included resources.
linkers.document Linker Linker for the top-level self link.
linkers.resource Linker Linker for the resource-level self link.
linkers.paginator Paginator Paginator for pagination links.
metaizers.jsonapi Metaizer Metadata for the JSON:API object.
metaizers.document Metaizer Metadata for the top-level document.
metaizers.resource Metaizer Metadata for each resource object.
include number | string[] 0 Which relationships to include. A number includes all relationships up to that depth. An array of paths includes only those paths (e.g. ['articles', 'articles.comments']). Takes precedence over depth.
depth number 0 Depth of relators to recurse for included resources. Deprecated — use include instead.
projection Partial<Record<keyof PrimaryType, 0 | 1>> | null | undefined null Attribute projection. All 0s to hide, all 1s to show. null shows all. undefined omits attributes.
cache boolean | Cache false Enables response caching.
onlyIdentifier boolean false Serializes only resource identifier objects (no attributes).
onlyRelationship string false Serializes only the relationship linkage for the named relator.
nullData boolean false Forces data to be null.
asIncluded boolean false Moves primary data to included and uses identifier objects for data.

Relator

new Relator<PrimaryType, RelatedType>(
  fetch: (data: PrimaryType) => Promise<RelatedType | RelatedType[] | null | undefined>,
  serializer: Serializer<RelatedType> | (() => Serializer<RelatedType>),
  options?: Partial<RelatorOptions<PrimaryType, RelatedType>>
)

Passing a getter function () => Serializer instead of a Serializer instance breaks circular references between serializers. When using a getter, options.relatedName is required.

RelatorOptions<PrimaryType, RelatedType>:

Option Type Description
linkers.relationship Linker<[PrimaryType, RelatedType | RelatedType[] | null | undefined]> Linker for the relationship self link.
linkers.related Linker<[PrimaryType, RelatedType | RelatedType[] | null | undefined]> Linker for the related resource link.
metaizer Metaizer<[PrimaryType, RelatedType | RelatedType[] | null | undefined]> Metaizer for relationship metadata.
relatedName string Override for the relationship name (defaults to the serializer's collection name). Required when passing a serializer getter.

Linker

new Linker<Dependencies>(
  link: (...args: Dependencies) => string,
  options?: LinkerOptions<Dependencies>
)
// where Dependencies extends any[]

LinkerOptions<Dependencies>:

Option Type Description
metaizer Metaizer<Dependencies> Adds meta to the link object.

Metaizer

new Metaizer<Dependencies>(
  metaize: (...args: Dependencies) => Record<string, unknown> | Promise<Record<string, unknown>>
)
// where Dependencies extends any[]

Paginator

new Paginator<DataType>(
  paginate: (data: DataType | DataType[]) => {
    first?: string | null;
    last?: string | null;
    prev?: string | null;
    next?: string | null;
  } | undefined
)

The callback returns an object with optional first, last, prev, next string URLs (or null to explicitly omit a link).

ErrorSerializer

new ErrorSerializer<ErrorType>(options?: Partial<ErrorSerializerOptions<ErrorType>>)

Methods:

Method Description
serialize(errors, options?) Serializes one or more errors synchronously. Returns an ErrorDocument.

By default, ErrorSerializer maps fields from standard Error objects:

Error field JSON:API field
name code
message detail
id id
code status
reason title
location source.pointer

ErrorSerializerOptions<ErrorType>:

Option Type Description
attributes.id keyof ErrorType Field to use for error id. Default: "id"
attributes.status keyof ErrorType Field to use for HTTP status. Default: "code"
attributes.code keyof ErrorType Field to use for error code. Default: "name"
attributes.title keyof ErrorType Field to use for error title. Default: "reason"
attributes.detail keyof ErrorType Field to use for error detail. Default: "message"
attributes.source.pointer keyof ErrorType Field for JSON Pointer source. Default: "location"
attributes.source.parameter keyof ErrorType Field for query parameter source. Default: undefined
attributes.source.header keyof ErrorType Field for header source. Default: undefined
linkers.about Linker<[JapiError]> Linker for the error about link.
metaizers.jsonapi Metaizer<[]> Metadata for the JSON:API object.
metaizers.document Metaizer<[JapiError[]]> Metadata for the top-level document.
metaizers.error Metaizer<[JapiError]> Metadata for each error object.
version string | null JSON:API version. Default: "1.0"

Cache

new Cache<PrimaryType>(options?: Partial<CacheOptions<PrimaryType>>)

CacheOptions<PrimaryType>:

Option Type Default Description
limit number 10 Maximum number of documents to store before evicting the oldest.
resolver (stored, incoming) => boolean Object.is Equality function to determine cache hits.

PolymorphicSerializer

Serializes a mixed array of resources that share a common discriminant field. Each type is routed to its own Serializer.

new PolymorphicSerializer<PrimaryType>(
  commonName: string,
  key: keyof PrimaryType,
  serializers: Record<string, Serializer> | Record<string, () => Serializer>
)

Example:

import { PolymorphicSerializer, Serializer } from 'ts-japi';

const DogSerializer = new Serializer('dogs');
const CatSerializer = new Serializer('cats');

const AnimalSerializer = new PolymorphicSerializer('animals', 'type', {
  dog: DogSerializer,
  cat: CatSerializer,
});

const animals = [
  { id: '1', type: 'dog', name: 'Rex' },
  { id: '2', type: 'cat', name: 'Whiskers' },
];

console.log(await AnimalSerializer.serialize(animals));

JapiError

Extend JapiError to create typed errors that pass through ErrorSerializer unchanged.

import { JapiError, ErrorSerializer } from 'ts-japi';

class NotFoundError extends JapiError {
  constructor(id: string) {
    super({ status: '404', code: 'NOT_FOUND', title: 'Not Found', detail: `Resource ${id} not found` });
  }
}

const serializer = new ErrorSerializer();
console.log(serializer.serialize(new NotFoundError('42')));
// { errors: [{ status: '404', code: 'NOT_FOUND', title: 'Not Found', detail: 'Resource 42 not found' }], jsonapi: { version: '1.0' } }

JapiError fields:

Field Type Description
id string Unique identifier for this error occurrence.
status string HTTP status code as a string.
code string Application-specific error code.
title string Short, human-readable summary (should not change between occurrences).
detail string Human-readable explanation of this specific occurrence.
source.pointer string JSON Pointer to the source field (e.g. /data/attributes/name).
source.parameter string Query parameter that caused the error.
source.header string Request header that caused the error.
links object Links object (e.g. about link).
meta object Non-standard meta information.

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. Use the include or depth option on Serializer with caution — deep recursion can degrade performance significantly.

For Developers

To get started in developing this library, run pnpm install, pnpm build and pnpm 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. Feel free to open an issue if you find a bug.

License

Copyright © 2020 mathematic-inc.

Licensed under Apache 2.0.

This project is free and open-source work by a 501(c)(3) non-profit. If you find it useful, please consider donating.

About

Zero-dependency TypeScript library for serializing data to the JSON:API specification with full type safety, relationships, links, and pagination

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Sponsor this project

 

Packages

 
 
 

Contributors