Skip to content

Vladlen-Libraries/domain-js

Repository files navigation

Domain-js

Domain-js is a library contains generic classes that helps make CRUD operations easy with Domain-Driven Design principles.

  • Typescript support
  • Rest API approach
  • No dependencies

Installation

Domain-js requires Node.js v8.12.0+ to run.

Install the library with npm.

$ npm install --save @snap-alex/domain-js

Or yarn.

$ yarn add @snap-alex/domain-js

Basic Usage

Create a core resource based on url. Example with cross-fetch library;

import { FetchResource } from "@snap-alex/domain-js";
import fetch from "cross-fetch";

const httpResource = new FetchResource('https://www.books.com/api/v0', fetch);
export default httpResource;

Create Entity interface

The collection interface we want to manage

export interface Book {
  id?: number
  title: string
  created_at: string
  updated_at: string
}

Create entity rest resource

Use BaseRestResource class for create Entity resource endpoint. Based on previosly created httpResource instance.

import { BaseRestResource } from "@snap-alex/domain-js";
import httpResource from './core/infrastructure/httpResource';

const bookResource = new BaseRestResource(httpResource, 'books');
export default bookResource;

Already you can use your rest resource for requests

import { BaseRestResource } from "@snap-alex/domain-js";
import bookResource './books/bookResource';

// CREATE on https://www.books.com/api/v0/books
bookResource.create({ title: 'Tom' }) as Promise<any>;

// PUT on https://www.books.com/api/v0/books
bookResource.update({ title: 'Tom' }) as Promise<any>;

// PATCH on https://www.books.com/api/v0/books
bookResource.patch({ title: 'Tom' }) as Promise<any>;

// GET on https://www.books.com/api/v0/books?title=Tom
bookResource.get({ title: 'Tom' }) as Promise<any>;

// DELETE on https://www.books.com/api/v0/books
bookResource.delete({ title: 'Tom' }) as Promise<any>;

// Create child resource with uri - https://www.books.com/api/v0/books/additional
const newChildResource1: BaseRestResource = bookResource.child('additional');

// Create child resource with uri strings - https://www.books.com/api/v0/books/additional/path
const newChildResource2: BaseRestResource = bookResource.child('additional', 'path');

Let's start manage our collection through Repository!

But we have a model User and now we have to create Repository

import { BaseRepository } from "@snap-alex/domain-js";
import bookResource from './books/bookResource';
import { Book } from './books/Book';

const bookRepository = new BaseRepository<Book>(bookResource)

// Check Entity is new (by id existence)
userRepository.isEntityNew({ id: 1, title: 'Tom' }) as boolean;

// Create Book 
// POST on https://www.books.com/api/v0/books 
userRepository.create({ title: 'Tom' }) as Promise<Book>;

// Update Book 
// PUT on https://www.books.com/api/v0/books/1 
userRepository.update({ id: 1, title: 'Tom' }) as Promise<Book>;

// Patch Book 
// PATCH on https://www.books.com/api/v0/books/1 
userRepository.patch({ id: 1, title: 'Tom' }) as Promise<Book>;

// Load Books
// GET on https://www.books.com/api/v0/books
cosnt books = await userRepository.load() as Promise<ArrayMeta<Book>>;
books.forEach((book) => console.log(book.title))
books.meta // containts meta server information

// Load Book by ID
// GET on https://www.books.com/api/v0/books/1
userRepository.loadById(1) as Promise<Book>;

// Delete Book
// DELETE on https://www.books.com/api/v0/books/1
userRepository.delete({ id: 1, title: 'Tom' }) as Promise<void>;

// Search Books
// todo describe search pagination params in Readme

We can setup another Entity Id

import { BaseRepository } from "@snap-alex/domain-js";
import bookResource from './books/bookResource';
import { Book } from './books/Book';

class BookRepository extends BaseRepository {
  entityIdName = 'uuid';
}

const bookRepository = new BookRepository<Book>(bookResource)

// Update Book 
// PUT on https://www.books.com/api/v0/books/101js1mx12jkej
userRepository.update({ uuid: '101js1mx12jkej', title: 'Tom' }) as Promise<Book>;

Build repository for child entities dynamically (RepositoryBuilder)

For example we have users with subscriptions
Subscriptions are located at ../api/users/:id/subscriptions

import { 
  BaseRepository, 
  BaseRepositoryBuilder, 
  FetchResource, 
  BaseRestResource, 
  BaseEntity 
} from "@snap-alex/domain-js";

// Create base resource
const httpResource = new FetchResource('https://www.example.com/api/v0');

// Create resource for users
const userResource = new BaseRestResource(httpResource, 'users');

// Describe Subscription entity interface
interface Subscription extends BaseEntity {
  id: number
  expirationDate: string
}

// Create Subscription repository class
class SubscriptionRepository extends BaseRepository<Subscription> {
}

// Create our repository builder with additional method
class SubscriptionRepositoryBuilder extends BaseRepositoryBuilder<Subscription> {
  public buildWithUserId(userId: number): SubscriptionRepository {
    const resource = userResource.child(userId, 'subscriptions');
    return this.build(resource) as SubscriptionRepository;
  }
}

// Create builder instance with
const subscriptionRepositoryBuilder = new SubscriptionRepositoryBuilder(SubscriptionRepository);

// Create repository dynamically
const subscriptionRepository = subscriptionRepositoryBuilder.buildWithUserId(2);

// Load Subscriptions 
// GET on https://www.example.com/api/v0/users/2/subscriptions
subscriptionRepository.load();

Advanced

Using DataMappers

Date mappers are used to encode response from the server to format we need and reverse the conversion before sending Entity to server.

Mapping strategy

Let's define strategy for encode / decode our data

import { BaseMapType } from "@snap-alex/domain-js";

const mappingStrategy = {
  id: BaseMapType.number,
  nickname: BaseMapType.string,
  isOnline: BaseMapType.bool.asAttrMap('is_online'),
  createdAt: BaseMapType.dateTime.asAttrMap('created_at'),
  states: BaseMapType.arrayOf(BaseMapType.string),
  avatar: BaseMapType.shapeOf({
    id: BaseMapType.number,
    url: BaseMapType.string
  }),
  city: BaseMapType.decodeEntityKey()({
    id: BaseMapType.number,
    title: BaseMapType.string
  }),
  ticket: BaseMapType.decodeEntityKey('uuid')({
    uuid: BaseMapType.string,
    cinema: BaseMapType.string
  }),
  roleId: BaseMapType.encodeEntityKey()({
    id: BaseMapType.number,
    title: BaseMapType.string,
  }).asAttrMap('role'),
  customMap: {
    map: 'custom_map',
    encode: (value: any) => value && value.custom_map,
    decode: (value: any) => {
      return value && `${value.customMap.id}.${value.customMap.title}`;
    }
  }
}

We can define encoded / decoded interfaces (optional)

interface Encoded {
  id: number
  nickname: string
  isOnline: boolean
  createdAt: string
  states: string[]
  avatar: {
    id: number
    url: string
  }
  city: {
    id: number
    title: string
  }
  ticket: {
    uuid: string
    cinema: string
  }
  roleId: number
  customMap: {
    id: number
    title: string
  }
}

interface Decoded {
  id: number | string
  nickname: string
  is_online: boolean
  created_at: string
  states: string[]
  avatar: {
    id: number | string
    url: string
  }
  city: {
    id: number | string
    title: string
  }
  ticket: {
    uuid: string
    cinema: string
  }
  role: {
    id: number | string
    title: string
  }
  custom_map: {
    id: number | string
    title: string
  }
}

Create our DataMapper

const testDataMapper = new BaseDataMapper<Encoded, Decoded>(mappingStrategy)

Use it for decode / encode data

const encodedTest = testDataMapper.encode({
  id: '1',
  nickname: 'Bob',
  is_online: false,
  created_at: '2018-02-08',
  states: ['new'],
  avatar: {
    id: 1,
    url: 'url'
  }
})

// return an object
{
  id: 1
  nickname: 'Bob',
  isOnline: false,
  createdAt: '2018-02-08'
  states: ['new']
  avatar: {
    id: 1,
    url: 'url'
  }
}

Use DataMapper with Repository!

bookRepository will automatically use bookDataMapper for encode and decode data before receive / send data.

import { BaseRepository } from "@snap-alex/domain-js";
import bookResource from './books/bookResource';
import bookDataMapper from './boooks/bookDataMapper';
import { Book } from './books/Book';

const bookRepository = new BaseRepository<Book>(bookResource, bookDataMapper)

Todos

  • Implement GraphQL resource

License

MIT