README
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