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
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
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;
The collection interface we want to manage
export interface Book {
id?: number
title: string
created_at: string
updated_at: string
}
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');
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
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>;
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();
Date mappers are used to encode response from the server to format we need and reverse the conversion before sending Entity to server.
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'
}
}
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)
- Implement GraphQL resource
MIT